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.


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