Interactive REPL
import it.grypho.scala.leonardo.cli.Session
The REPL provides an interactive session over the library. Launch with sbt repl (or sbt "runMain it.grypho.scala.leonardo.cli.repl").
Session.execute(line): String is the pure, IO-free core — all examples below use it directly and are verified at documentation build time.
Value bindings
:= with a constant right-hand side binds a numeric value:
val s = Session()
s.execute("x := 3")
// res0: String = "x := 3.0"
s.execute("x")
// res1: String = "3.0"
Function definitions
:= with free variables creates a late-bound definition. Redefining f automatically updates any g defined in terms of f:
s.execute("f := sin(x) + x")
// res2: String = "f := (sin(x) + x)"
s.execute("f") // evaluated at current x = 3
// res3: String = "3.14112"
s.execute("derive(f, x)")
// res4: String = "0.01001"
Equations
Bare = is always a relation, never a binding:
s.execute("10 * x = 2 * x + 1")
// res5: String = "false"
Store an equation and solve it:
s.execute("h := x^2 = 9")
// res6: String = "h := (x ^ 2.0) = 9.0"
s.execute("solve(h, x)")
// res7: String = """x_1 := -3.0
// x_2 := 3.0"""
Simplification and expansion
s.execute("simplify x + 0 + 1*x")
// res8: String = "(2.0 * x)"
s.execute("expand (x + 1)^3")
// res9: String = "((((x * (x * x)) + (x * (x * 1.0))) + ((x * (1.0 * x)) + (x * (1.0 * 1.0)))) + (((1.0 * (x * x)) + (1.0 * (x * 1.0))) + ((1.0 * (1.0 * x)) + (1.0 * (1.0 * 1.0)))))"
Environment inspection
s.execute("env")
// res10: String = """precision = 5
// x := 3.0
// x_1 := -3.0
// x_2 := 3.0
// f := (sin(x) + x)
// h := (x ^ 2.0) = 9.0"""
Precision
s.execute("precision 8")
// res11: String = "precision = 8"
s.execute("sin(x)") // now at 8-digit precision
// res12: String = "0.14112001"
Reset to default:
s.execute("precision 5")
// res13: String = "precision = 5"
Exact arithmetic
exact on switches numeric literals from Double to exact rationals, so results that a binary float cannot represent come out right:
val e = new Session()
// e: Session = it.grypho.scala.leonardo.cli.Session@3385b06f
e.execute("exact on")
// res14: String = "exact = on, working precision = 30"
e.execute("0.1 + 0.2") // exactly three tenths, not 0.30000000000000004
// res15: String = "3/10"
e.execute("1/3 * 3") // exactly one
// res16: String = "1"
The mode is decided when the input is parsed, and it has to be: once 0.1 has been read as a Double the tenth that was meant is already gone, and no later stage can recover it.
Exact are +, -, *, / and integer powers. Everything else is irrational — every transcendental function, any fractional power — so it is computed and then re-approximated to the working precision, a separate setting from the display precision:
e.execute("exact precision 40")
// res17: String = "exact = on, working precision = 40"
e.execute("exact")
// res18: String = "exact = on, working precision = 40"
Those kernels are genuinely arbitrary-precision, so raising the working precision sharpens the sin() itself and not merely the arithmetic around it:
e.execute("exact precision 40")
// res19: String = "exact = on, working precision = 40"
e.execute("exp(1000)") // 435 digits; floating point gives Infinity
// res20: String = "1.9701E+434"
A short result displays as a fraction and a long one as a decimal, but :save always writes the exact fraction, so nothing is lost across a round-trip:
e.execute("pi") // readable
// res21: String = "3.14159"
Matrices and factorials are exact too:
e.execute("exact precision 30")
// res22: String = "exact = on, working precision = 30"
e.execute("H := [[1, 1/2, 1/3], [1/2, 1/3, 1/4], [1/3, 1/4, 1/5]]")
// res23: String = "H := [[1, (1 / 2), (1 / 3)], [(1 / 2), (1 / 3), (1 / 4)], [(1 / 3), (1 / 4), (1 / 5)]]"
e.execute("det(H)") // exactly 1/2160; the Double path reports 4.6E-4
// res24: String = "1/2160"
e.execute("H * inv(H)") // exactly the identity
// res25: String = "[[1, 0, 0], [0, 1, 0], [0, 0, 1]]"
fact loses the 170! ceiling that existed only because a Double overflows there — though a compute cap remains, since an unbounded factorial is easy to type by accident.
What still computes in Double: the iterative decompositions (lu, qr, eigen, eig, jordan), which cannot be exact whatever their input, and A^n. They demote an exact operand rather than refuse it; write A * A for an exact product.
Mixing an exact value with an inexact one gives an inexact result. That is deliberate: absorbing the Double would be lossless, but it would dress representation error up as an exact answer.
Matrix display
Matrices with two or more rows can be shown multi-line with right-aligned columns (pretty on; pretty off restores the single-line form). Single-row matrices and decomposition results stay on one line. The setting is persisted by :save / :load.
s.execute("pretty on")
// res26: String = "pretty = on"
s.execute("[[1, 200], [30, 4]]")
// res27: String = """[[ 1.0, 200.0]
// [30.0, 4.0]]"""
s.execute("pretty off")
// res28: String = "pretty = off"
Range sampling
s.execute("samples x*x x 0 1 5")
// res29: String = """0.0 0.0
// 0.25 0.0625
// 0.5 0.25
// 0.75 0.5625
// 1.0 1.0"""
The same sampling backs the browser REPL’s plots, so a figure and this table always show the same numbers — Session.samplePoints returns the points and samples prints them. Note that the printed table is rounded to the session precision while the data is not, which is why a plot is never built from this text.
In the browser
The browser REPL runs this same Session compiled to JavaScript. Every command on this page works there unchanged, including :save and :load — which keep their exact spelling but write to your browser’s storage rather than to files. Nothing you type leaves the tab.
Two commands exist only there, because a terminal cannot honour them:
| Command | Draws |
|---|---|
plot <expr> <var> <lo> <hi> [<n>] | y = f(x) as a line — the arguments are samples’ arguments |
points <expr> <var> <lo> <hi> [<n>] | the same sampling as points, with the axes locked to the same scale |
bode <expr> <var> <wMin> <wMax> [<n>] | gain and unwrapped phase over a logarithmic frequency axis |
nyquist <expr> <var> <wMin> <wMax> [<n>] | the same sweep in the complex plane, axes locked |
For bode and nyquist the bounds are a frequency band, so wMin must be greater than zero — a geometric grid cannot start at zero. See Control Systems for what the sweep does and why the phase has to be unwrapped.
Use points whenever the picture is geometry rather than a function of one variable — a vector drawn as a coordinate, or anything in the complex plane. On unequal axes a circle looks like an ellipse, so the lock is a correctness matter rather than a preference.
The Copy shareable link button puts the whole session in the URL fragment. Opening that link restores the bindings and definitions; because it is a fragment, it is never sent to any server.
Unset a binding
s.execute("unset x")
// res30: String = "x unset"
s.execute("f") // x is free again — stays symbolic
// res31: String = "(sin(x) + x)"
Session scripts
:save file serialises the session (precision, bindings, definitions) as a replayable script; :load file replays it. The pure Session.script and Session.load methods are IO-free:
val s2 = Session()
s2.execute("a := 2")
s2.execute("g := a * x")
val script = s2.script
script
// res34: String = """precision 5
// colors dark
// pretty off
// logic minmax
// logic symmetric off
// exact precision 30
// exact off
// a := 2.0
// g := (a * x)"""
Restore into a fresh session:
val s3 = Session()
s3.load(script)
s3.execute("g")
// res36: String = "(2.0 * x)"
Inline help
Session().execute("help :=")
// res37: String = """Bind a name or define a function.
// Constant RHS -> numeric value; RHS with free variables -> late-bound definition.
// x := 3.001 bind a numeric value
// f := sin(x) + x define a function (late-bound: redefining f updates g := f^2)
// h := x^2 = 4 bind a named equation (pass to solve(h, x))
// L, U, P := lu(A) bind multiple names to elements of a 1xn result (any decomposition)"""
Use help <command> for any REPL keyword, or bare help for the full listing. ? is an alias for help.
Quick reference
| Input | Meaning |
|---|---|
x := 3 | Bind value |
f := sin(x) | Define function |
h := lhs = rhs | Bind equation |
g := consolidate(expr) | Freeze the current value/simplified form (not late-bound) |
L, U, P := lu(A) | Tuple binding from a 1×n decomposition result |
lhs = rhs | Evaluate relation (true/false) |
lhs == rhs | Explicit equality check |
simplify <expr> | Structural simplification |
expand <expr> | Distribute products over sums |
eval <expr> | Force numeric evaluation |
samples e v lo hi [n] | Sample function over range |
truth <expr> | Boolean truth table over the free variables |
truth3 <expr> | Three-valued (Kleene) truth table |
logic symmetric on\|off | Spell truth values as -1 / 0 / 1 |
logic minmax\|product\|lukasiewicz | Select the fuzzy t-norm family |
precision <n> | Set decimal digits |
exact on\|off | Exact rational arithmetic (default off) |
exact precision <n> | Digits an irrational is approximated to |
pretty on / off | Multi-line, column-aligned matrix display |
colors dark\|light\|none | Syntax-highlight scheme |
env / vars | Show session state |
unset <name> | Remove binding or definition |
:save <file> | Write replayable script |
:load <file> | Replay a script |
help [topic] | Show help |
quit / exit | Leave the REPL |
The cheat sheet lists every command and expression form on one page.