Tracking Issue: Math expression parser/calculator
This is our library at /libraries/math-parser that uses a parsing framework that takes a string at runtime and calculates its result, including units and dimensional analysis.
Roadmap
- Scalars (real numbers)
- Particle1 (complex numbers)
- Vector1/Vector2/Vector3 and Particle2/Particle3 (up to quaternions)
- Matrices (linear and affine maps)
- Static typing: sorts (values vs. matrices), the value ladder, the matrix diamond (Linear/Affine × 2D/3D), and refinements
- Units
- Documentation
- Error reporting (partially done: parse errors carry byte spans and evaluation errors are typed, but there is no source-snippet rendering)
- Compiling into Graphene AST
Operators
- Infix operators:
- Addition:
+ - Subtraction:
-,− - Multiplication:
*,×,⋅ - Division:
/,÷ - Modulo:
%(floored, so the result takes the sign of the modulus and(-3.2) % 2is0.8, matching the Modulo node's default;x % turnwraps an angle into one revolution) - Exponentiation:
^ - Equals:
== - Not Equals:
!=,≠ - Less Than or Equal To:
<=,≤ - Greater Than or Equal To:
>=,≥ - Less Than:
< - Greater Than:
> - Or:
||,∨ - And:
&&,∧
- Addition:
- Prefix operators:
- Unary Plus:
+ - Unary Minus:
-,− - Not:
!,¬
- Unary Plus:
- Postfix operators:
- Factorial:
!
- Factorial:
- Grouping operators:
- Parentheses:
( ) - Magnitude bars:
|x|(the Euclidean magnitude over all parts, the absolute value on a scalar; the per-part fold isabs();|a||b|lexes as the magnitude ofaORb, so users must write|a| |b|, while a||with two magnitudes open closes both, so|a*|b||is valid but|a|b||is an error)
- Parentheses:
Chained comparisons
A chain like a < b < c is a single n-ary predicate: each comparison reads only the original operands and the individual results meet in a conjunction, so no comparison's result ever feeds another comparison.
-
a < b < cand longer chains assert the relation between each adjacent pair, like interval notation (0 <= x < 1), never C-style(a < b) < c - A chain must stay within one direction family,
</<=/==or>/>=/==; mixing directions is a parse error -
a != b != casserts that all pairs are distinct, matching Mathematica'sUnequal(deliberately not Python's adjacent-pairs reading)
Functions
- Trig:
-
sin(),cos(),tan() -
csc(),sec(),cot()
-
- Inverse trig:
-
asin(),acos(),atan(),atan2() -
acsc(),asec(),acot()
-
- Hyperbolic:
-
sinh(),cosh(),tanh() -
csch(),sech(),coth()
-
- Inverse hyperbolic:
-
asinh(),acosh(),atanh() -
acsch(),asech(),acoth()
-
- Logarithm:
- Natural log:
ln() - Logarithm base N:
logN()(alias:log_N())- Examples:
log2(),log3.25(),log_10()
- Examples:
- Logarithm with variable base:
log(x, b)
- Natural log:
- Exponents:
- Power of
e:exp(n) - Power with variable exponent: the
^operator, asx^n
- Power of
- Roots:
- Square root:
sqrt() - Cube root:
cbrt() - Root base N:
rootN(x)(alias:root_N(x))- Examples:
root2(),root3.25(),root_10()
- Examples:
- Root with variable degree:
root(x, n)
- Square root:
- Rounding:
- Floor:
floor()(toward -∞) - Ceiling:
ceil()(toward +∞;ceil(-0.5)is0because-0isn't a thing) - Round:
round()(half away from 0:round(2.5)is3,round(-2.5)is-3) - Truncate:
trunc()(toward 0) - Fractional part:
fract()(x - trunc(x), keeping the sign:fract(-1.25)=-0.25) - Sign:
sign()(-1,0, or1, with0for zero) - Absolute value:
abs()(per part, soabs(p)folds a point into the first quadrant or octant, the SDF box being|max(d, 0)| + min(max([i] d, [j] d), 0) where d = abs(p) - b; equals|x|on a scalar, while the bars are the Euclidean magnitude on every rung) - Snap:
snap(x, step)=round(x / step) step
- Floor:
- Range:
- Clamp:
clamp(x, R)(to the range on every axis it spans:R^-1 xclamped to0..1and mapped back, so a rotated box clamps to itself) - Minimum:
min(a, b, ...) - Maximum:
max(a, b, ...)
- Clamp:
- Interpolation:
- Lerp:
lerp(a, b, t) - Slerp:
slerp(a, b, t)(unit quaternions) - Remap:
remap(x, A, B)(from rangeAto rangeB; equalsB A^-1 x)
- Lerp:
- Integers:
- Greatest common divisor:
gcd() - Least common multiple:
lcm()
- Greatest common divisor:
- Statistics:
- Average:
avg(a, b, ...) - Geometric mean:
geomean(a, b, ...) - Harmonic mean:
harmmean(a, b, ...) - Root mean square:
rms(a, b, ...) - Euclidean norm:
hypot(a, b, ...)(sqrt(|a|^2 + |b|^2 + ...)) - Median:
median(a, b, ...) - Mode:
mode(a, b, ...)(smallest of the most frequent values; errors when no value repeats) - Variance:
variance(a, b, ...)(population) - Standard deviation:
stddev(a, b, ...)(population) - Count:
count(a, b, ...)
- Average:
- Combinatorics:
- Combinations:
choose(n, r)("n choose r", the binomial coefficient) - Permutations:
pick(n, r)("n pick r", the falling factorial)
- Combinations:
- Logical
- Piecewise:
{a if cond, b if cond, ...}, the cases of math'scasesnotation, with an optionalc otherwisecase ({-1 if x < 0, 0 if x == 0, 1 if x > 0},{0 if x < 0, x if 0 <= x < 1, 1 otherwise}) - Exclusive or:
xor()(variadic parity; operands must each be 0 or 1)
- Piecewise:
- Vectors:
- Conjugate:
conj()(negates the vector part on every rung; equals[1;-i;-j;-k] q) - Dot product:
dot(a, b)(the inner product over all parts1, i, j, k; equals[1](a * conj(b))) - Cross product:
cross(a, b)(of the vector parts, with zero weight; equals(a*b - b*a) / 2) - Normalize:
normalize()(evaluation error at zero) - Angle:
angle(a, b)(fromatob, signed by the turn's direction seen from+k, sorotate(a, angle(a, b))is parallel tob; a heading isangle(i, v), an inclinationangle(k, v), a complex argumentangle(i, [1;i] z)) - Rotate:
rotate(v, angle, axis = k) - Rotor:
rotor(angle, axis = k)=cos(θ/2) + sin(θ/2) axis(applied asq v conj(q), which scales by|q|²whenqis not unit) - Axis:
axis(q)(a rotor's unit axis; its angle is2 angle(1, q)) - Project:
project(a, b) - Reject:
reject(a, b) - Reflect:
reflect(v, n) - Distance:
distance(a, b)=|a - b| - Perpendicular:
perp(v)=cross(k, v)
- Conjugate:
- Noise (deterministic, so the same input always gives the same value):
- Smooth noise:
noise(p, tile)(Perlin, the Noise Pattern node's default, one 4D field in0..1so(10..20) noise(p)lands on a range; a lower rung samples a slice with the missing axes at zero, and a seed is a Particle3 offset added by hand,noise(p + seed), so a 2D pattern moves to a fresh slice or evolves smoothly alongw; the optionaltilerange makes the field repeat with the range's extent on each axis it spans, each extent a whole number of lattice units, so any box of that size tiles seamlessly; octaves are written out,noise(p) + noise(2p) / 2) - White noise:
random(p)(an uncorrelated value in0..1for each distinctp, whose every part must be a whole number, an error otherwise, so an index or id is used as is and a position is quantized first,random(floor(p / 10px));(-5..5) random(p)scatters within a range; seeded the same way,random(p + seed))
- Smooth noise:
Reducer classification
The library classifies an input string as either a lone reducer token or an expression, for hosts whose UI offers an operator-only input mode. A lone token expands to the expression written out over the host's whole item list: operators interleave (+ becomes a + b + c), functions wrap (min becomes min(a, b, c)). A token is legal only if that expansion is well-formed at every item count.
- Left-fold operators
+,-,*,/,%,&&,||-> pairwise accumulation, left-associative:((a + b) + c) - Right-fold operator
^-> the power towera ^ (b ^ c), matching the expression grammar's associativity - Chain comparisons
<,<=,>,>=,==,!=-> the chained-comparison semantics above; zero or one items evaluate to 1 (true), since no pair exists to fail the relation, matching Mathematica - Variadic functions
min,max,gcd,lcm,hypot,avg,geomean,harmmean,rms,median,mode,variance,stddev,count,xor-> a single call over all items - Over a matrix list:
*and/compose in list order,+,-, andavgare pointwise,countcounts, and the other reducers are errors - An empty item list yields the operator's identity element (
+-> 0,*-> 1,&&-> 1,||-> 0), an error for operators without one (-,/,%,^), or the function's own zero-argument behavior (count()-> 0,min()-> error)
Branch indices (multi-valued functions)
Every multi-valued function accepts one extra optional trailing argument m, an integer (rounded to the nearest whole number) selecting which of the function's mathematically-many values to return. Omitting m is identical to m = 0, which returns exactly the value the function produces today, so all existing expressions are unchanged. Offsets that leave the real line lie in the input's own complex plane, along its normalized vector part n̂ (i for a real input). The language always returns a single value; enumerating branches is done by the graph evaluating the expression across a range of supplied m values, and a nonzero m types the result as Particle1 like a domain climb.
- Roots:
sqrt(x, m),cbrt(x, m),root(x, n, m),rootN(x, m)-> them = 0value timese^(2π n̂ m / n)(nis 2 forsqrt, 3 forcbrt); for whole-numbernthis wraps modn, so every integermis valid; the^operator stays principal-only, a chosen branch of a power beingexp(ln(x, m) y) - Natural log:
ln(x, m)->ln(x) + 2π n̂ m - Log base b:
log(x, b, m)and the suffixed formslogN(x, m)->(ln(x) + 2π n̂ m) / ln(b), with the base's log always principal - Two-argument arctangent:
atan2(y, x, m)->atan2(y, x) + 2πm - Inverse trig with period π:
atan(x, m),acot(x, m)-> them = 0value plusπm - Inverse trig, alternating:
asin(x, m),acsc(x, m)->(-1)^mtimes them = 0value, plusπm - Inverse trig, paired:
acos(x, m),asec(x, m)-> them = 0value plusπmfor evenm; the negatedm = 0value plusπ(m + 1)for oddm - Inverse hyperbolic:
asinh(x, m)/acsch(x, m),acosh(x, m)/asech(x, m),atanh(x, m)/acoth(x, m)-> the same three formulas as their trig counterparts, withπ n̂ min place ofπm
Constants
- Value constants (lowercase only):
- Infinity:
inf,infinity,∞ - Basis vector:
i(complex numbers) - Basis vectors:
j,k(quaternions) - Pi:
pi,π - Tau:
tau,τ - Euler's Number:
e - Golden Ratio:
phi,φ - True and False:
true= 1,false= 0
- Infinity:
- Matrix constant (uppercase only):
- Identity matrix:
I
- Identity matrix:
Variables
- Single-letter variables (examples:
x,y,z) - Multi-letter variables (examples:
theta,alpha,beta,gamma) - Non-Latin letters (examples:
λ,あ,א) - Names are Unicode identifiers as Rust spells them,
XID_StartthenXID_Continue, so a name begins with a letter and may carry combining marks (example: a decomposedé), while digits, marks, invisible formatting characters, and (unlike Rust, which adds it as a special case) an underscore cannot begin one - The keywords
if,otherwise, andwhereare never names, whether bound by the host or bywhere - Deferred until a host scope needs to publish names an identifier cannot spell: backtick-quoted names for variables with spaces or operator characters; the quoted text is the whole bound symbol, namespace prefix included, and the case rule reads its first character after any prefix. Markdown's code-span rule: a run of N backticks opens and the next run of exactly N closes, so a name containing backticks is quoted with a longer run, and one space is stripped from each end when both are present, so a name may begin or end with a backtick or consist only of them
`Layer Name` `x-offset` `2nd Point` `#Stroke Width` ``Layer `Old` Name`` `` ` `` (a variable named as the backtick character) - Environment-bound variables are case-sensitive and shadow the constants above, except
∞, which is a literal rather than a name; the\prefix always reaches the builtin - Uppercase-initial identifiers name matrices
- Non-uppercase-initial identifiers name values
- Namespace prefixes: a name never begins with ASCII punctuation, so
#,$,~, and@stay available to join\as prefixes whenever a host scope needs namespacing - Builtin namespace prefix:
\nameis the language's own constant or function regardless of bindings (\pi,\e,\i,\I,\sin), an error if no builtin has that name; the case rule applies after the prefix
Bindings
-
wherenames values for the expression before it (examples:V where r = d/2, d = 4,{sin(r)/r if r != 0, 1 otherwise} where r = hypot(x, y)) - Function bindings with a fixed, nonzero arity (example:
f(0) + f(1) where f(t) = t^2 + c, c = 3) - A
whereclause stands at the top level of the expression or directly inside parentheses and runs to their closing parenthesis or the end of input, so every comma in it separates bindings (example:2 (a + b where a = x^2, b = y^2)) - Bindings are ordered by dependency, not position: a binding's value or a function's body may use any other binding in its clause, functions included, and a cycle is an error (recursion, mutual recursion, or self-reference like
x = x + 1), so every expression finishes - Scoping is lexical: a clause's names are visible only within its parentheses, parameters shadow every outer name, and a clause shadows enclosing clauses, host bindings, and builtins (still reachable through
\); defining one name twice in the same clause is an error - Calls and values are separate namespaces told apart by position, so
f = 2, f(t) = t + 1defines both andwhere sin = 2leavessin(x)the sine - Function application takes precedence over implicit multiplication for any function name in scope, with
wherefunctions shadowing host functions, which shadow builtins, sowhere k = 2leavesk(x + 1)a product - Evaluation is lazy and memoized: a binding is evaluated at most once and only if used, and arguments pass unevaluated, so an error the result never reaches is never raised; an unused binding is not an error
- Functions are second-class: they are defined and called, never passed, returned, or stored as values
- Bound names and parameters follow the case rule, and each call is typed as its substituted body, so one function serves every rung it is called with
Number and unit representations
- Scientific notation (examples:
1e-6,2.5E3) - Units (examples:
5m,5 m,3.5kg,3.5 kg,2.5m/s^2,2.5 m/s^2)
Number systems
- Basis
1: Real numbers (examples:0,42,-42,0.5,3.14159,-2.71828) - Bases
1, i: Complex numbers (examples:3i,-2.5i,1.5e-3i,2.5e3 + 2.1e-2i) - Bases
1, i, j, k: Quaternions- Vector1, zero weight (examples:
3i,-2.5i) - Vector2, zero weight (examples:
3i + 2j,-3i - 2j) - Vector3, zero weight (examples:
3i + 2j + k,-3i - 2j - k) - Particle1, the complex numbers (examples:
4 + 3i,-4 - 2.5i) - Particle2 (examples:
4 + 3i + 2j,-4 - 3i - 2j) - Particle3, the full quaternion (examples:
4 + 3i + 2j + k,-4 - 3i - 2j - k)
- Vector1, zero weight (examples:
- Matrices:
[a; ...]by rows and[a, ...]by columns (see Matrices)
Value semantics and number ladder
Every value is semantically a quaternion; narrower storage (integer, real, complex) is an internal optimization that is never observable. All behavior is decided by a value's mathematical content, never its storage form.
For consumption outside the library, setting inputs and querying results, the subset ladder ⊂ ℕ ⊂ ℤ ⊂ ℝ ⊂ ℂ ⊂ ℍ maps to concrete types: = {0, 1} is bool, ℕ is the u{8, 16, 32, 64, 128} types and #[hard(0..)] node bounds, ℤ is the i{8, 16, 32, 64, 128} types, ℝ is f32 and f64 (the Scalar rung, which Graphite labels Number), ℂ is Particle1, and ℍ is Particle3, with Particle2 between them. The weighted types are one generic, Weighted<V> { w: f64, vector: V }, with Particle1, Particle2, and Particle3 as its instances over the unweighted Vector1(f64), Vector2([f64; 2]), and Vector3([f64; 3]), which are their weight-zero refinements. The boundary is these structs and plain arrays with no graphics dependency; Graphite converts them to its glam-backed wire types in one adapter.
- Value identity:
n + 0iis exactlyn, and-0is exactly+0; adding or removing zero-valued parts can never change any result - Ladder rungs: Bool ⊂ Integer ⊂ Scalar (
1) ⊂ Particle1 (1, i, the complex numbers) ⊂ Particle2 (1, i, j) ⊂ Particle3 (1, i, j, k, the quaternions), each rung being the values whose remaining bases are zero, with the unweightedVector1/Vector2/Vector3as each particle rung's weight-zero refinement (naturals are a nonnegativity constraint on integers, not a rung; rationals are skipped, so integer division promotes directly to Scalar) - Basis order:
ijk = xyz, the real part is the weightw, and values are written and queried in Hamilton's orderw, x, y, z; the weight is not the trailing homogeneous coordinate of a graphicsxyzwvector, since matrices never divide by it, sow-last does not apply -
*is always the Hamilton product, never componentwise (v * v = -|v|²; a complex number rotates the1, iplane, so rotating a canvas Vector2 isrotate()) - Promotion: an operation whose answer does not exist at a value's rung climbs minimally (
sqrt(-4)->2i,ln(-1)->iπ,asin(2)-> its complex value); a real input needing a complex answer resolves into the(1, i)plane - Branch selection keys on value, not storage:
root(-8, 3)androot(-8 + 0i, 3)are both-2 - Integer exactness: integer-rung arithmetic (
+,-,*,%,gcd,lcm) is exact beyond f64's 2^53 limit; overflow past the widest storage promotes to Scalar - No NaN: no operation returns NaN; indeterminate forms (
0/0,∞ - ∞,0 * ∞) are evaluation errors, and a NaN arriving through a host binding is an evaluation error at its point of use - Ordering is real-only:
<,<=,>,>=are evaluation errors on values with nonzero vector parts - Componentwise mapping:
floor,ceil,round,trunc,fract,sign,abs,min,max, andclampact on each part of a vector (GLSL's split, withabsper part and|x|the length) - Functions of a Particle3 (a quaternion) act in the input's own complex plane (spanned by
1and its normalized vector part);x / y = x * y⁻¹andq^p = exp(ln(q) * p) - Minimal representation: the evaluation API reports the lowest rung that losslessly holds a result, so hosts can choose the right query
- Booleans are the refinement
{0, 1}of the integer rung, not a separate type: comparisons evaluate to 1 or 0, and host boolean bindings enter as 1 or 0 - Logical contexts (
&&,||,xor(), prefix!, piecewise conditions) require operands exactly 0 or 1 and error otherwise, so a general number becomes a truth val
Source: GraphiteEditor/Graphite