Expressions & Evaluation
import it.grypho.scala.leonardo.core.*
import it.grypho.scala.leonardo.scalar.*
The expression hierarchy
Every node in the AST extends _Expression. The main families are:
| Type | Examples |
|---|---|
Values (_Value) | _Number(d), _Complex(re, im), _Rational, _MatrixValue, _Bool, _Truth |
| Atoms | _Variable("x") |
| Operations | Sum, Product, Ratio, Power |
| Functions | Sin, Cos, Tg, Exp, Ln, LogBase, Asin, Acos, Atan, plus the hyperbolic and reciprocal families |
| Functionals | _Derivative, _Integral, _DefIntegral, _Limit, _Taylor, _Laplace, … |
All nodes are immutable; the tree is value-typed and safe to share across threads.
The dual eval model
Every _Expression implements:
def eval(env: Environment): Either[_Expression, _Value]
Right[_Value] means the expression reduced to a concrete result. Left[_Expression] means it stayed symbolic: variables remain free, or the operation is undefined at the point — 1/0 and ln(0) stay symbolic rather than throwing or producing an infinity.
val x = _Variable("x")
val y = _Variable("y")
val env = new Environment()
A plain variable without a binding stays left (symbolic):
x.eval(env)
// res0: Either[_Expression, _Value] = Left(value = _Variable(variable = "x"))
A constant folds immediately to right:
_Number(42.0).eval(env)
// res1: Either[_Expression, _Value] = Right(value = _Number(d = 42.0))
A compound expression reduces as far as the bindings allow:
val expr = Sum(Product(_Number(2.0), x), y)
// expr: Sum = Sum(
// a = Product(a = _Number(d = 2.0), b = _Variable(variable = "x")),
// b = _Variable(variable = "y")
// )
// 2x + y — both free, stays symbolic
expr.eval(env)
// res2: Either[_Expression, _Value] = Left(
// value = Sum(
// a = Product(a = _Number(d = 2.0), b = _Variable(variable = "x")),
// b = _Variable(variable = "y")
// )
// )
Bind x only — y remains free:
val envX = new Environment(5, Map("x" -> _Number(3.0)))
// envX: Environment = it.grypho.scala.leonardo.core.Environment@6705345f
expr.eval(envX)
// res3: Either[_Expression, _Value] = Left(
// value = Sum(a = _Number(d = 6.0), b = _Variable(variable = "y"))
// )
Bind both — reduces to a number:
val envXY = new Environment(5, Map("x" -> _Number(3.0), "y" -> _Number(1.0)))
// envXY: Environment = it.grypho.scala.leonardo.core.Environment@31056731
expr.eval(envXY)
// res4: Either[_Expression, _Value] = Right(value = _Number(d = 7.0))
Free variables
Every node caches its free variable set after the first traversal (O(1) thereafter):
expr.freeVars
// res5: Set[String] = Set("x", "y")
_Number(1.0).freeVars
// res6: Set[String] = Set()
Simplification
simplify performs a single structural pass; simplifyFully repeats until the tree stops changing:
simplify(Sum(x, _Number(0.0))).toString
// res7: String = "x"
simplify(Product(_Number(1.0), x)).toString
// res8: String = "x"
// Constant folding
simplify(Sum(_Number(3.0), _Number(4.0))).toString
// res9: String = "7.0"
// Inverse function pairs
simplify(Ln(Exp(x))).toString
// res10: String = "x"
Expansion
expand distributes multiplication over addition and expands integer powers of sums:
val a = _Variable("a")
// a: _Variable = _Variable(variable = "a")
val b = _Variable("b")
// b: _Variable = _Variable(variable = "b")
expand(Power(Sum(a, b), _Number(2.0))).toString
// res11: String = "(((a * a) + (a * b)) + ((b * a) + (b * b)))"
expand(Product(Sum(a, _Number(1.0)), Sum(b, _Number(2.0)))).toString
// res12: String = "(((a * b) + (a * 2.0)) + ((1.0 * b) + (1.0 * 2.0)))"
Environment and precision
Environment is immutable. withBinding returns a new instance — the original is unchanged, making concurrent evaluation safe:
val base = new Environment(5, Map("x" -> _Number(1.0)))
val withY = base.withBinding("y", _Number(2.0))
base.isBound("y")
// res13: Boolean = false
withY.isBound("y")
// res14: Boolean = true
The precision field controls rounding at display time only — no precision is lost during intermediate computation:
_Number(math.Pi).display(3)
// res15: String = "3.142"
_Number(math.Pi).display(10)
// res16: String = "3.1415926536"