Architecture

Leonardo is structured as a layered set of packages. Dependencies point inward: every domain package imports core (and usually scalar); nothing imports cli or parser; the graph is a strict DAG.

                            cli
                             │
                           parser
   ┌────────┬─────────┬──────┼──────┬─────────┬─────────┬────────┐
 matrix  equation  transform ode  logic  probability  domain  vector … control
   └────────┴─────────┴──────┴──────┴─────────┴─────────┴────────┘
                           scalar
                             │
                            core

The picture is a simplification — some domains also import each other in one direction (equation imports logic, statistics imports probability and matrix, control imports transform and matrix) — but the invariants hold everywhere: core imports no domain, every arrow is one-way, and parser and cli sit above all of them.

Modules

That layering is also the artifact boundary. Because nothing imports cli, it can be published separately — and it is, so that a library consumer never resolves JLine:

Artifact Contains Extra dependencies
it.grypho:leonardo Every package except cli scala-parser-combinators, spire
it.grypho:leonardo-repl cli only; depends on leonardo jline

The split cost no code change, which is the practical dividend of the layering rule: cli being a leaf that nothing imports is exactly what made it detachable. It also left the library free of any terminal dependency — the one thing that would have blocked a Scala.js cross-build.

Running off the JVM

Both modules cross-build for Scala.js, and the whole suite runs on Node as well as the JVM: 1781 library cases on each, plus the REPL’s. The port needed two platform-specific pieces in the library and four in the REPL; everything else is the same source.

Platform-specific Why
Parallel block dispatch java.util.stream has no Scala.js counterpart, and a browser is single-threaded anyway
Double rendering Scala.js renders a Double the JavaScript way — 1 for 1.0, 0.00003 for 3.0E-5
JLine read loop, highlighter a terminal line editor has no meaning in a browser
:save / :load storage files on the JVM, localStorage in a browser — the same commands either way

The interesting part is what did not need changing. Session — the entire REPL behaviour, including command parsing, display and session scripts — is untouched, because Session.execute, Session.script and Session.load were already pure string in / string out. The file system was the only JVM-bound part of :save, and it never lived inside Session.

Nothing Scala.js is published yet: the cross-build exists to prove the library runs in a browser and to keep that true, since CI runs the JavaScript suite on every push.

The browser front end

A third project, web, is the page itself. It is JS-only, publishes nothing, and is kept out of the root aggregate so that linking a bundle is never in the path of an ordinary library change; CI names it explicitly instead.

It is almost entirely not there, which is the point. Session.step already dispatches every command on both platforms, so the page is a text box, a transcript and a history ring. Only two things are genuinely browser-only: the plot / points commands, which a terminal cannot honour, and the shareable link.

Weight over the wire  
Leonardo, the whole CAS 3.46 MB raw, 475 KB gzipped
Plotly (cartesian distribution, vendored) 1.3 MB raw, 436 KB gzipped

Under a megabyte for a computer algebra system with interactive plotting, which is what removed the one real risk in the plan: no module splitting and no lazy loading is needed.

Plotting adopts no Scala dependency. Plotly’s input is plain JSON, so the facade is a string builder — pure, and therefore unit-tested without a browser. The one line of it that chose Plotly over Vega-Lite is scaleanchor: a Nyquist diagram, a pole-zero map or a two-element vector drawn as a point is geometry, and on unequal axes the unit circle becomes an ellipse. Vega-Lite has no aspect lock, and deriving one from the data range stops holding the moment the reader zooms — a confidently wrong picture rather than a refusal.

Nothing the user types leaves the tab: the CAS is in the bundle, sessions live in localStorage, and a shared link carries its session in the URL fragment, which browsers never transmit.

The diagram below is generated automatically from docs/structure.puml by running sbt puml (or sbt site which runs the full pipeline).

Class and package diagram

Leonardo architecture diagram

Drag to pan · scroll, double-click, or use the corner controls to zoom · open the diagram in its own tab

Package guide

Package Role Documentation
core _Expression trait, _Value marker, _Number, _Bool, _Complex, _Rational, _Truth, _Based, _Variable, _MatrixValue, Environment — the foundation shared by every domain Expressions & Evaluation
scalar AST nodes (Sum, Product, Power, functions, functionals) and all algorithms: derive, integrate, simplify, expand, normalize, compile, sample, series expansions, domain analysis, plus a data-driven rewrite-rule engine (Rewrite) backing the parameterised table of integrals (IntegralRules) Expressions & Evaluation · Calculus
matrix _Matrix symbolic node + _MatrixOperation nodes, constructors, the decompositions and expm; dense _MatrixValue kernels live in core Matrices
equation _Equation, _Comparison, _EqualityCheck relation nodes; solve (equations, inequalities, matrix unknowns) and solveSystem Equations & Complex Numbers
transform Laplace, Fourier, inverse Laplace, and the one-sided z-transform and its inverse Features
ode _ODE node; closed-form linear and separable tiers, Runge–Kutta fallback Features
logic The five connectives over one Kleene/fuzzy rule table; simplification, CNF/DNF, truth tables, membership curves, defuzzification Logic
probability Distributions as first-class values; pdf/cdf/prob/quantile; expect/variance by a linearity rule table Features
statistics Descriptive statistics, regression by QR, elementary inference (ttest, confint, chisqtest) Features
domain Renders the neutral domain analysis (domain, differentiable, singularities) into relation nodes Features
vector grad/div/curl/laplacian/jacobian/hessian over an ordered coordinate tuple, in three coordinate systems Calculus
control Transfer-function algebra, stability, time and frequency response, state space, discretisation Control Systems
parser Recursive-descent Parser (extends JavaTokenParsers); produces all AST node types; ReservedWords guard Getting Started
cli Interactive Session (pure, IO-free core) + repl read loop; session scripts (:save/:load). Ships as the separate leonardo-repl artifact Interactive REPL

Key design decisions

Dual eval modeleval(env): Either[_Expression, _Value] is the single reduction point. Right means fully concrete; Left means one or more variables remain free. Every node type implements it; no special dispatch needed.

_ElementWise marker — nodes that are plain containers (matrix literals, matrix sums, transposes, equations) implement this trait. Algorithms such as derive, simplify, and expand use children/rebuild to distribute over them without domain-specific cases in the algorithm code.

Memoizationderive and simplify cache results in a bounded ConcurrentHashMap (see Memo.scala). This is the biggest win for simplifyFully’s fixpoint loop and for _DefIntegral’s Simpson integration, which re-derives the same subexpressions hundreds of times per evaluation.

compile fast pathcompile(e, v, env): Option[Double ⇒ Double] translates an expression into a JVM closure when all nodes are compilable. Used by _DefIntegral (Simpson’s rule) and sample; eliminates per-step AST allocation and environment lookup.

A new carrier type must earn its place — a domain gets a _Value or a wrapper of its own only when something must read a value as one. A vector field is an n×1 _Matrix; a transfer function is an ordinary Ratio; a state-space model is the same 1×4 row of matrices lu and qr already return; an inequality’s solution set is a _Comparison. The reason is that a carrier is a wall: simplify, derive, substitute, the exact tier and the parser all operate on expressions, so a type outside that set would have to be taught to each of them or be cut off from all of them. The price is paid in ergonomics — with no type to dispatch on, control must take its frequency variable as an explicit argument everywhere — and in documentation, which is why Control Systems opens by saying so.


This site uses Just the Docs, a documentation theme for Jekyll.