The whole CAS compiled to JavaScript — no install, no sign-up, no server. Differentiate, integrate, solve, invert a matrix, take a Laplace transform, plot a function. Nothing you type leaves the tab.
Leonardo is a Scala 3 symbolic math library and Computer Algebra System (CAS). The name is an homage to Leonardo Pisano, commonly known as Fibonacci, the Italian mathematician and author of the Liber Abaci: with his works he introduced Indo/Arabic numerals and mathematical notation to the Western world.
Where it fits. Scala is well served for numerical work — Spire for generic numerics, Breeze for linear algebra — but symbolic mathematics has meant reaching outside the language, to SymPy on Python or Symja on Java. Leonardo fills that slot natively: it differentiates and integrates expressions, solves them symbolically, and keeps 1/3 exactly 1/3, with no Python runtime, no foreign binary, and no interop layer — just a dependency line.
This project was loosely inspired by the Scala project Cascala/Galileo, though the codebase has been completely rewritten from scratch and Leonardo is now basically unrelated to Galileo.
Leonardo is a lightweight CAS designed to parse, represent, and evaluate mathematical expressions. It builds an Abstract Syntax Tree (AST) from textual input and can evaluate expressions both numerically and symbolically.
Four independent one-liners from an actual session — the REPL needs no project and no build,
so you can run these yourself with the single cs launch command in
Installation below:
leonardo> solve(x^2 - 4 > 0, x) -- an inequality; the answer is a union
((x < -2.0) or (x > 2.0))
leonardo> domain(ln(x - 2), x) -- where is this expression defined?
(x > 2.0)
leonardo> curl([[-y], [x], [0]], x, y, z) -- a vector field is just a matrix
[[0.0], [0.0], [2.0]]
leonardo> exact on -- switch off floating point entirely
exact = on, working precision = 30
leonardo> 0.1 + 0.2 -- not 0.30000000000000004
3/10
leonardo> fib(100) -- exact well past what a Double can hold
354224848179261915075
Full detail for every entry below is in the feature reference.
-
Expression parsing — a recursive-descent parser over
scala-parser-combinators, with implicit multiplication (3sin(a)), unary minus in any operand position, multi-character names, and the constantspi,e,iandinf. -
Dual evaluation — every expression evaluates to a number when its variables are bound and to a simplified expression tree when they are not. That single rule is the spine of the library: nothing throws when an answer is not yet available, it simply stays symbolic.
-
Exact arithmetic (
exact on) — literals become exact rationals, so0.1 + 0.2is exactly3/10anddetof the 3×3 Hilbert matrix is exactly1/2160. Transcendentals are arbitrary-precision rather thanDouble, so raising the working precision genuinely sharpenssinandexp; off by default, leaving the floating-point path untouched. -
Declines rather than guesses — an operation outside what the library can actually compute returns unevaluated instead of approximating. A confidently wrong answer is treated as the worst possible outcome, worse than no answer at all.
-
LaTeX output (
latex on) — any expression as typeset-ready LaTeX. The work is a precedence pass rather than a transcription:(a+b)/(c+d)comes out as\frac{a + b}{c + d}with both pairs of parentheses gone, and every node without a rule degrades to\mathrm{…}, so the emitter is plain where it is incomplete and never wrong. The printed answer is unchanged — the LaTeX travels on a second channel, which the browser REPL typesets. -
Clean API — no global state.
Environmentis immutable andwithBindingreturns a copy, so evaluation is safe to share across threads. -
Performance — rounding happens only at display time, so no precision is lost mid-computation; free-variable sets are cached per node, and
derive/simplifyare memoised behind bounded thread-safe caches. Definite integrals compile the integrand to aDouble => Doubleclosure where they can. -
Domains — each is a package that imports
core, never the reverse:- Scalar algebra and calculus — differentiation, definite integration by Simpson's rule, and indefinite integration through a layered engine: a rule table of ~64 verified entries, integration by parts, trigonometric-power reduction, full rational partial fractions, u-substitution, trigonometric/hyperbolic substitution, and the Weierstrass half-angle substitution. Order functions too: the element-wise
maximum/minimum, the reductionsmax/min,clampand the smoothsoftplus, with derivatives at their kinks stated rather than guessed. - Matrices — literals of arbitrary expressions that collapse to dense
Doublekernels once every cell is numeric; determinant, inverse, integer powers, thelu/qr/eigen/eig/jordandecompositions, the Cholesky factorchol, which refuses any matrix that is not symmetric positive definite, and block assembly (hcat,vcat,blkdiag,kron,repmat,submatrix). - Equations —
solvefor a scalar or a matrix unknown (including the Sylvester and Lyapunov forms via Kronecker vectorization), inequalities whose answer is a union of intervals, andsolveSystemfor linear systems. - Logic — boolean, three-valued Kleene, symmetric ternary and fuzzy, all sharing one rule table: the classical truth tables fall out as the crisp special case rather than a separate code path.
- Probability and statistics — distributions as first-class values with closed-form CDFs, a linearity rule table for
expect/variance, descriptive statistics that stay exact, regression by QR and recursive least squares with forgetting, and Bayesian updating: Bayes' theorem over a finite set of hypotheses (exact on exact input) and conjugate posteriors for the Beta–Binomial, Gamma–Poisson and Normal–Normal pairs. - Optimization — every stationary point of a function, solved symbolically and classified; convexity proved or refuted; Lagrange multipliers and the Karush–Kuhn–Tucker conditions; and a numeric
minimize(gradient descent, Newton, BFGS, projected BFGS for bounds) that answers only with a certified point. - Vector calculus —
grad,div,curl,laplacian,jacobian,hessianover an explicit ordered coordinate tuple, in Cartesian, cylindrical and both spherical conventions. - Transforms and differential equations — Laplace, Fourier, inverse Laplace and the one-sided z-transform over a symbolic rule table; first-order initial-value problems solved in closed form where possible and by Runge–Kutta otherwise, constant-coefficient linear systems by the matrix exponential, and time-varying or nonlinear systems by Runge-Kutta, certified by step doubling; the integrator is also public as a step and a trajectory.
- Control systems — transfer functions as ordinary expressions rather than a carrier type: interconnection, poles and zeros, stability, step and impulse response, Bode and Nyquist, state space, discretisation by zero-order hold or Tustin, and the MPC prediction matrices.
- Series — Taylor and Maclaurin, numeric Fourier series, Padé approximants, and Laurent series about a pole.
- Special functions and sequences — the factorial and gamma family with
erf,digammaand the incomplete gamma and beta; Fibonacci and its relatives over one shared recurrence, plus a generictabulate. - Domain analysis —
domain,differentiableandsingularitiesreport where an expression is defined, describing what the library computes rather than what is mathematically true.
- Scalar algebra and calculus — differentiation, definite integration by Simpson's rule, and indefinite integration through a layered engine: a rule table of ~64 verified entries, integration by parts, trigonometric-power reduction, full rational partial fractions, u-substitution, trigonometric/hyperbolic substitution, and the Weierstrass half-angle substitution. Order functions too: the element-wise
Leonardo is published to Maven Central for Scala 3.3 LTS. The library is what you almost always want:
libraryDependencies += "it.grypho" %% "leonardo" % "3.8.2"The interactive REPL is a second artifact: a standalone program that drives Leonardo from the
command line. It depends on leonardo, so this line replaces the one above rather than
joining it:
libraryDependencies += "it.grypho" %% "leonardo-repl" % "3.8.2"To simply try the REPL, nothing needs to be cloned, built or added to a project — with coursier installed, one command fetches it and starts a session:
cs launch it.grypho:leonardo-repl_3:3.8.2 -M it.grypho.scala.leonardo.cli.repl
The REPL is a convenience; the library is the deliverable. Parse a string into an AST, transform it symbolically, and evaluate it only when you choose to:
import it.grypho.scala.leonardo.core.*
import it.grypho.scala.leonardo.scalar.*
import it.grypho.scala.leonardo.parser.Parser
val f = Parser.parse("x^3 + 2*x").get // ((x ^ 3.0) + (2.0 * x))
val df = simplify(derive(f, _Variable("x"))) // ((3.0 * (x ^ 2.0)) + 2.0)
df.eval(new Environment(5, Map("x" -> _Number(2.0))))
// Right(14.0) -- bound: a value
df.eval(new Environment())
// Left(((3.0 * (x ^ 2.0)) + 2.0)) -- unbound: the reduced expression, not an error
integrate(Parser.parse("sin(x)^2").get, _Variable("x"))
// (((-1.0 * (sin(x) * cos(x))) / 2.0) + (0.5 * x))That Either is the whole design: Right when every variable is bound, Left carrying the
most-reduced form when they are not. Nothing throws because an answer is not yet available.
See Getting Started for the import conventions and a fuller tour.
📖 Documentation site: lycogrypho.github.io/Leonardo — guides, examples, and the full Scaladoc API reference published to GitHub Pages.
📖 Individual pages (published; the sources under docs/src/ are mdoc input and show
their examples unevaluated):
From a clone, launch the REPL with the sbt repl alias (or the full
sbt "replModule/runMain it.grypho.scala.leonardo.cli.repl"); the cs launch line above
needs no clone at all:
leonardo> x := 3.001 -- bind a value (constant right-hand side)
leonardo> f := sin(x) + x -- define a function (free variables ⇒ definition)
leonardo> f -- evaluate against current bindings
3.14113
leonardo> derive(f, x) -- differentiation works through definitions
0.00987
leonardo> 10 * x = 2 * x + 1 -- bare "=" is an equation: true/false once bound
false
leonardo> 10 * x == 2 * x + 1 -- "==" is an equality check: same eval, not solvable
false
leonardo> h := x^2 = 4 -- bind a named equation
leonardo> solve(h, x) -- pass a named equation to solve
[[x = -2.0, x = 2.0]]
leonardo> solve(10 * x = 2 * x + 1, x) -- inline equation still works
x = 0.125
leonardo> limit(sin(x)/x, x, 0) -- L'Hôpital: 1.0
leonardo> limit(1/x, x, 0, +) -- one-sided: inf
leonardo> limit(atan(x), x, inf) -- limit at ∞: π/2
leonardo> laplace(sin(2*t), t, s) -- Laplace transform: 2/(s² + 4)
leonardo> fourier(exp(-2*t), t, w) -- Fourier transform: 1/(2 + i·w)
leonardo> invlaplace(2/(s^2+4), s, t) -- inverse Laplace: sin(2*t)
leonardo> ode(y, y, t, 0, 1, 1) -- solve y'=y, y(0)=1 at t=1: 2.71828 (e)
leonardo> simplify x + 0 -- structural simplification (ignores bindings)
x
leonardo> C := A * B -- with A, B matrices: simplify C executes the
leonardo> simplify C -- multiplication and simplifies each element
leonardo> g := consolidate(f + f) -- freeze the simplified+evaluated result (not late-bound)
leonardo> precision 8 -- set decimal precision
leonardo> env -- list precision, bindings, definitions
leonardo> :save session.txt -- write current state to a replayable script
leonardo> :load session.txt -- replay a session script
leonardo> quit
The prompt has full line editing and history (powered by JLine): use the arrow keys to
edit the current line and to recall earlier commands, which persist across sessions in
~/.leonardo_history. Ctrl-C abandons the current line without leaving the session;
Ctrl-D (or quit/exit) ends it. When no interactive console is attached (piped
input, CI), the prompt degrades gracefully to a plain line reader.
Token-level syntax highlighting colours the input as you type. Three built-in schemes are
available; switch with the colors command:
| Command | Scheme |
|---|---|
colors dark |
bold yellow commands · cyan functions · magenta constants · green numbers (default) |
colors light |
bold blue commands · green functions · magenta constants · red numbers |
colors none |
no colouring |
The active scheme is persisted by :save and restored by :load.
Matrices with two or more rows can be displayed multi-line with right-aligned columns via
the pretty on command (off by default; pretty off restores the single-line
[[…], […]] form, pretty shows the current setting). The setting is likewise persisted
by :save / :load:
leonardo> pretty on
leonardo> [[1, 200], [30, 4]]
[[ 1.0, 200.0]
[30.0, 4.0]]
Single-row matrices and decomposition results (a matrix of matrices) stay on one line.
Assignment uses := (the CAS convention): bare = always denotes an equation, so
x = 2*x + 1 is a relation to evaluate, never a binding. Session scripts emit :=;
old =-style :save files are not accepted and must be re-created.
Definitions are late-bound: redefining f also changes any g defined in terms
of f. Whether an assignment binds a value or defines a function is decided by the
right-hand side alone — constant expressions fold to a numeric binding, expressions
with free variables become definitions. name := consolidate(expr) is the opposite of a
late-bound definition: it freezes the result. The expression is simplified and evaluated
against the current bindings right away, and the snapshot is stored — so with x := 2 and
f := x + 1, g := consolidate(f + f) binds g to 6.0 and it stays 6.0 even after
x := 100. If free variables remain, the frozen simplified form is kept instead (with
a := 3, h := consolidate(a * y) stores h := (3.0 * y), unaffected by a later a := 9).
Differentiating with respect to a defined
function applies the chain rule: with f := sin(x) and g := f^2, derive(g, f)
computes dg/df as derive(g, x) / derive(f, x) over the definition's single free
variable (definitions with several free variables are rejected with a message). :save serializes the session (precision,
bindings, definitions) as a script that :load replays.
Candidate domains under consideration. They are placeholders rather than commitments, listed roughly by effort-to-value:
- Number theory — primes,
primepi, continued fractions, modular arithmetic and the classical integer functions; the cheapest of the open domains and already half-specified. - Optimization — the symbolic half only: stationary points from
grad, their classification fromhessian, and Lagrange multipliers. - Operational research — adopted in part rather than whole, taking the pieces that belong in a CAS and leaving general MILP solving outside it.
- Quaternions — a fifth sibling of
_Numberon the established value pattern, with non-commutative multiplication as the design risk to be handled deliberately. - Interval arithmetic —
_Interval(lo, hi)as a value, answering the question the exact tier cannot: how far a floating-point result can be trusted. - Linear ODE systems —
y' = A·ysolved in closed form ase^(A·t)·y₀through the matrix exponential that now exists. - Tensor algebra — generalising the matrix domain to N dimensions, with contraction and Einstein summation.
- Graph plotting and visualization — a user-facing interface for graphing functions and exploring solutions; the largest single piece of work on the list.
Leonardo is licensed under the Apache License, Version 2.0.
Copyright 2023-2026 Cosimo Attanasi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
You may use, modify and redistribute Leonardo — including in closed-source and commercial
work — provided you keep the licence and copyright notices and state what you changed. The
licence also grants an express patent licence from every contributor. See NOTICE
for the attribution notice you must carry when redistributing, and for the third-party
dependency inventory (all permissive: Apache-2.0, BSD and MIT).
Leonardo was released under GPL-3 until 2026-09-06; the relicence was made by the sole copyright holder to let the library be used from projects under any licence.
Design Credits: Leonardo's Logo and banner were created using Inkscape, elaborating the following elements:
- Rotunda Pommerania font by Peter Wiegel — free for commercial use (available at 1001fonts.com)
- Fibonacci's portrait — vectorized from "I benefattori dell'umanità" (vol. VI, Firenze: Ducci, 1850), sourced from Wikimedia Commons and used under Creative Commons license