GraphiteEditor / GraphiteEditor/Graphite
Tracking Issue: Math expression parser/calculator
- Lingua principale
- Rust
- Stelle
- 27.2k
- Fork
- 1.3k
- Merge medio
- 20h 5m
- PR unite (30g)
- 57
Descrizione
This is our library at [`/libraries/math-parser`](https://github.com/GraphiteEditor/Graphite/tree/master/libraries/math-parser) that uses a parsing framework that takes a string at runtime and calculates its result, including units and dimensional analysis.
## Roadmap
- [x] Scalars (real numbers)
- [x] 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
- [x] Infix operators:
- [x] Addition: `+`
- [x] Subtraction: `-`, `−`
- [x] Multiplication: `*`, `×`, `⋅`
- [x] Division: `/`, `÷`
- [x] Modulo: `%`
- [x] Exponentiation: `^`
- [x] Equals: `==`
- [x] Not Equals: `!=`, `≠`
- [x] Less Than or Equal To: `<=`, `≤`
- [x] Greater Than or Equal To: `>=`, `≥`
- [x] Less Than: `<`
- [x] Greater Than: `>`
- [x] Or: `||`, `∨`
- [x] And: `&&`, `∧`
- [x] Prefix operators:
- [x] Unary Plus: `+`
- [x] Unary Minus: `-`, `−`
- [x] Not: `!`, `¬`
- [x] Postfix operators:
- [x] Factorial: `!`
- [x] Grouping operators:
- [x] Parentheses: `( )`
- [x] Magnitude bars: `|x|` (absolute value on reals, extends to 4-component Euclidean magnitude; `|a||b|` lexes as the magnitude of `a` OR `b`, 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)
## 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.
- [x] `a < b < c` and longer chains assert the relation between each adjacent pair, like interval notation (`0 <= x < 1`), never C-style `(a < b) < c`
- [x] A chain must stay within one direction family, `<`/`<=`/`==` or `>`/`>=`/`==`; mixing directions is a parse error
- [x] `a != b != c` asserts that all pairs are distinct, matching Mathematica's `Unequal` (deliberately not Python's adjacent-pairs reading)
## Functions
- [x] Trig:
- [x] `sin()`, `cos()`, `tan()`
- [x] `csc()`, `sec()`, `cot()`
- [x] Inverse trig:
- [x] `asin()`, `acos()`, `atan()`, `atan2()`
- [x] `acsc()`, `asec()`, `acot()`
- [x] Hyperbolic:
- [x] `sinh()`, `cosh()`, `tanh()`
- [x] `csch()`, `sech()`, `coth()`
- [x] Inverse hyperbolic:
- [x] `asinh()`, `acosh()`, `atanh()`
- [x] `acsch()`, `asech()`, `acoth()`
- [x] Logarithm:
- [x] Natural log: `ln()`
- [x] Logarithm base N: `logN()` (alias: `log_N()`)
- Examples: `log2()`, `log3.25()`, `log_10()`
- [x] Logarithm with variable base: `log(x, b)`
- [x] Exponents:
- [x] Power of `e`: `exp(n)`
- [x] Power with variable exponent: the `^` operator, as `x^n`
- [x] Roots:
- [x] Square root: `sqrt()`
- [x] Cube root: `cbrt()`
- [x] Root base N: `rootN(x)` (alias: `root_N(x)`)
- Examples: `root2()`, `root3.25()`, `root_10()`
- [x] Root with variable degree: `root(x, n)`
- [ ] Rounding:
- [x] Floor: `floor()` (toward -∞)
- [x] Ceiling: `ceil()` (toward +∞; `ceil(-0.5)` is `0` because `-0` isn't a thing)
- [x] Round: `round()` (half away from 0: `round(2.5)` is `3`, `round(-2.5)` is `-3`)
- [x] Truncate: `trunc()` (toward 0)
- [x] Fractional part: `fract()` (`x - trunc(x)`, keeping the sign: `fract(-1.25)` = `-0.25`)
- [x] Sign: `sign()` (`-1`, `0`, or `1`, with `0` for zero)
- [ ] Range:
- [x] Clamp: `clamp(x, min, max)`
- [x] Minimum: `min(a, b, ...)`
- [x] Maximum: `max(a, b, ...)`
- [ ] Interpolation:
- [x] Lerp: `lerp(a, b, t)`
- [ ] Slerp: `slerp(a, b, t)` (unit quaternions)
- [x] Remap: `remap(value, inA, inB, outA, outB)`
- [ ] Integers:
- [x] Greatest common divisor: `gcd()`
- [x] Least common multiple: `lcm()`
- [ ] Statistics:
- [x] Average: `avg(a, b, ...)`
- [x] Geometric mean: `geomean(a, b, ...)`
- [x] Harmonic mean: `harmmean(a, b, ...)`
- [x] Root mean square: `rms(a, b, ...)`
- [x] Euclidean norm: `hypot(a, b, ...)` (`sqrt(|a|^2 + |b|^2 + ...)`)
- [x] Median: `median(a, b, ...)`
- [x] Mode: `mode(a, b, ...)` (smallest of the most frequent values; errors when no value repeats)
- [x] Variance: `variance(a, b, ...)` (population)
- [x] Standard deviation: `stddev(a, b, ...)` (population)
- [x] Count: `count(a, b, ...)`
- [ ] Combinatorics:
- [x] Combinations: `choose(n, r)` ("n choose r", the binomial coefficient)
- [x] Permutations: `pick(n, r)` ("n pick r", the falling factorial)
- [ ] Logical
- [ ] Piecewise: `{a if cond, b if cond, ...}`, the cases of math's `cases` notation, with an optional `c otherwise` case (`{-1 if x < 0, 0 if x == 0, 1 if x > 0}`, `{0 if x < 0, x if 0 <= x < 1, 1 otherwise}`)
- [x] Exclusive or: `xor()` (variadic parity; operands must each be 0 or 1)
- [ ] Vectors:
- [x] 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 parts `1, 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)` (from `a` to `b`, signed by the turn's direction seen from `+k`, so `rotate(a, angle(a, b))` is parallel to `b`; a heading is `angle(i, v)`, an inclination `angle(k, v)`, a complex argument `angle(i, [1;i] z)`)
- [ ] Rotate: `rotate(v, angle, axis = k)`
- [ ] Rotor: `rotor(angle, axis = k)` = `cos(θ/2) + sin(θ/2) axis` (applied as `q v conj(q)`, which scales by `|q|²` when `q` is not unit)
- [ ] Axis: `axis(q)` (a rotor's unit axis; its angle is `2 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)`
## 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.
- [x] Left-fold operators `+`, `-`, `*`, `/`, `%`, `&&`, `||` -> pairwise accumulation, left-associative: `((a + b) + c)`
- [x] Right-fold operator `^` -> the power tower `a ^ (b ^ c)`, matching the expression grammar's associativity
- [x] 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
- [x] 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, `+`, `-`, and `avg` are pointwise, `count` counts, and the other reducers are errors
- [x] 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)` -> the `m = 0` value times `e^(2π n̂ m / n)` (`n` is 2 for `sqrt`, 3 for `cbrt`); for whole-number `n` this wraps mod `n`, so every integer `m` is valid; the `^` operator stays principal-only, a chosen branch of a power being `exp(ln(x, m) y)`
- [ ] Natural log: `ln(x, m)` -> `ln(x) + 2π n̂ m`
- [ ] Log base b: `log(x, b, m)` and the suffixed forms `logN(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)` -> the `m = 0` value plus `πm`
- [ ] Inverse trig, alternating: `asin(x, m)`, `acsc(x, m)` -> `(-1)^m` times the `m = 0` value, plus `πm`
- [ ] Inverse trig, paired: `acos(x, m)`, `asec(x, m)` -> the `m = 0` value plus `πm` for even `m`; the negated `m = 0` value plus `π(m + 1)` for odd `m`
- [ ] 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̂ m` in place of `πm`
## Constants
- Value constants (lowercase only):
- [x] Infinity: `inf`, `infinity`, `∞`
- [x] Basis vector: `i` (complex numbers)
- [ ] Basis vectors: `j`, `k` (quaternions)
- [x] Pi: `pi`, `π`
- [x] Tau: `tau`, `τ`
- [x] Euler's Number: `e`
- [x] Golden Ratio: `phi`, `φ`
- [x] True and False: `true` = 1, `false` = 0
- Matrix constant (uppercase only):
- [ ] Identity matrix: `I`
## Variables
- [x] Single-letter variables (examples: `x`, `y`, `z`)
- [x] Multi-letter variables (examples: `theta`, `alpha`, `beta`, `gamma`)
- [x] Non-Latin letters (examples: `λ`, `あ`, `א`)
- [x] Names are Unicode identifiers as Rust spells them, `XID_Start` then `XID_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`, and `where` are never names, whether bound by the host or by `where`
- [ ] 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)
```
- [x] 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
- [x] Namespace prefixes: a name never begins with ASCII punctuation, so `#`, `$`, `~`, and `@` stay available to join `\` as prefixes whenever a host scope needs namespacing
- [x] Builtin namespace prefix: `\name` is 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
- [ ] `where` names 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 `where` clause 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 + 1` defines both and `where sin = 2` leaves `sin(x)` the sine
- [ ] Function application takes precedence over implicit multiplication for any function name in scope, with `where` functions shadowing host functions, which shadow builtins, so `where k = 2` leaves `k(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
- [x] 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
- [x] Basis `1`: Real numbers (examples: `0`, `42`, `-42`, `0.5`, `3.14159`, `-2.71828`)
- [x] 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`)
- [ ] 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 { 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.
- [x] Value identity: `n + 0i` is exactly `n`, and `-0` is 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 unweighted `Vector1`/`Vector2`/`Vector3` as 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 weight `w`, and values are written and queried in Hamilton's order `w, x, y, z`; the weight is not the trailing homogeneous coordinate of a graphics `xyzw` vector, since matrices never divide by it, so `w`-last does not apply
- [ ] `*` is always the Hamilton product, never componentwise (`v * v = -|v|²`; a complex number rotates the `1, i` plane, so rotating a canvas Vector2 is `rotate()`)
- [x] 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
- [x] Branch selection keys on value, not storage: `root(-8, 3)` and `root(-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
- [x] 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
- [x] Ordering is real-only: `<`, `<=`, `>`, `>=` are evaluation errors on values with nonzero vector parts
- [ ] Componentwise mapping: `floor`, `ceil`, `round`, `trunc`, `fract`, `sign`, `min`, `max`, and `clamp` act on each part of a vector (GLSL's split)
- [ ] Functions of a Particle3 (a quaternion) act in the input's own complex plane (spanned by `1` and its normalized vector part); `x / y = x * y⁻¹` and `q^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
- [x] 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
- [x] Logical contexts (`&&`, `||`, `xor()`, prefix `!`, piecewise conditions) require operands exactly 0 or 1 and error otherwise, so a general number becomes a truth value only through an explicit comparison
- [ ] `&&` and `||` evaluate both operands, and a deciding operand wins over an erroring one (`false && error` and `error && false` are `false`, `true || error` is `true`) while any other error raises, which is Kleene's strong three-valued logic as SQL applies it to `NULL`; both operators stay commutative and a guard like `x != 0 && 1/x > 2` works in either order, while `xor()` has no deciding operand, so any error in it raises
- [ ] Piecewise cases are an unordered set of disjoint conditions: every condition is evaluated, at most one may hold, and only the holding case's value is evaluated; `otherwise` holds exactly when no other case does
- [ ] Two holding cases are an evaluation error even when their values agree (`{x if x >= 0, -x if x <= 0}` errors at `0`, so one inequality is written strict), as is no holding case without `otherwise`, and a condition that fails to evaluate fails the whole expression
- [ ] Overlapping cases are also reported while typing wherever the conditions compare one variable against constants, which reduces to interval arithmetic
- [x] Query as `f{32, 64}`/`{u, i}{8, 16, 32, 64, 128}` (`1` basis -> `n`, the real part)
- [x] Query as `bool` (`1` basis -> the value, required to be exactly 0 or 1)
- [ ] Query as `Vector1` (`i` basis -> `x`; requires zero weight) and `Particle1` (`1, i` bases -> `w, x`, the complex numbers)
- [ ] Query as `Vector2` (`i, j` bases -> `x, y`; requires zero weight) and `Particle2` (`1, i, j` bases -> `w, x, y`)
- [ ] Query as `Vector3` (`i, j, k` bases -> `x, y, z`; requires zero weight) and `Particle3` (`1, i, j, k` bases -> `w, x, y, z`, the quaternion)
- [ ] Hosts present each rung as a weight plus a displacement along `x`, `xy`, or `xyz` (the weight drawn as a circle of its radius around the point), never as imaginary units or 4D rotations
- [ ] A weighted point's position is not premultiplied by its weight, so projective points at infinity (a direction with zero weight, given meaning only by the homogeneous divide) are not representable; an infinite coordinate is just `∞`, which cannot carry a direction and hits `∞ - ∞` under rotation. Rational-curve arithmetic belongs to the nodes that build those types
## Matrices
Matrices are a second sort beside values, named with uppercase-initial identifiers and built with `[ ]` literals: affine maps of quaternion space, `p -> M p + c`, a 4×4 linear part `M` on `w, x, y, z` plus a translation quaternion `c` (a 5×5 homogeneous matrix with a pinned bottom row, stored like glam as the pair).
- [ ] Row literal: `[a;b;c]` (rows; applying it takes each row's inner product with the input, so the row count fixes the output rung: 1 -> Scalar, 2 -> Vector2, 3 -> Vector3, 4 -> Particle3; short forms pad zero rows, `[a;b;c]` = `[0;a;b;c]`, `[a;b]` = `[0;a;b;0]`, `[a]` = `[a;0;0;0]`)
- [ ] Column literal: `[a,b,c]` (columns, the images of the basis directions, as in glam's `from_cols`; the column count fixes the input rung the same way; `[a;b]^T` = `[a,b]`)
- [ ] Entries are whole values in basis form, with one separator kind per literal; whitespace stays multiplication, so MATLAB's `[1 2; 3 4]` is `[1i + 2j; 3i + 4j]`
- [ ] Brackets always build a matrix, never a value: `[x, y]` is a 4×2 column matrix, and a vector is written `x i + y j`
- [ ] Row literal uses: swizzles (`[k;j;i] v`), parts (`[1] q`, `[0;i;j;k] q`), the Argand shuffle (`[1;i] z`, inverted by `[i;j;0;0] p`), projection (`[i;j;0] v`), scaled picks (`[1;i;2j;-k] q`), splat (`[1;1] s` = `(s, s)`), positional construction (`[x;y] 1`), and the dot product (`[a] b`)
- [ ] Affine literal: a value added to a matrix is its translation, so `I + 5i + 4j` is a pure translation, `A 0` a matrix's translation, and `A - A 0` its linear part
- [ ] Range literal: `a..b` (closed; the diagonal affine map sending parameter `0` to `a` and `1` to `b`, so `0..1` = `I`, `10..0` reverses, and `a..a` is singular; `..` binds looser than arithmetic and tighter than comparison, so `0..2pi` reaches `2π` and a literal is applied as `(0..10) t`)
- [ ] Box literal: the same `a..b` with vector corners spans the axes of the corners' join rung, with `0` as that rung's zero, and leaves every other axis untouched (`0..(3i + 4j)` is the rectangle from the origin to `(3, 4)`, `0..(i + j + k)` the unit cube, `0..1i` the unit segment of the `x` axis where `0..1` is the unit interval of the weight, and `0..(5 + 6i + 7j + 8k)` a box over all four axes); it is not `scale(b - a) + a`, which leaves the weight alone
- [ ] Applying a box to a value with no parts on its axes is a rung error rather than a silent pass-through (`(0..1i) 0.5`, `(0..10) (3i + 4j)`)
- [ ] Range uses: application `R t`, normalization `R^-1 x`, remap as composition `B A^-1 x`, and insideness `inside(p, R)`
- [ ] Inside: `inside(p, R)` (whether `R^-1 p` lies within `0..1` on every axis the range spans, boundary included, which for a Particle3 box is `0 <= [1] t <= 1 && 0 <= [i] t <= 1 && 0 <= [j] t <= 1 && 0 <= [k] t <= 1 where t = R^-1 p` and for a narrower range drops the axes it does not span; one test covers any parallelogram since `R^-1` undoes rotation and shear, and a singular range, which has no interior, is an error)
- [ ] General regions are the same sort, with `a..b` as the diagonal case: `rotation(θ) (0..(b - a)) + a` rotates a box about its corner (`rotation(θ) (a..b)` rotates it about the origin), `shear(i, j, s) (a..b)` shears it, and `[u, v] + c` is the parallelogram with corner `c` and edges `u` and `v`, which is a Footprint's transform
- [ ] Products: matrix·value is application (`M v` or `M * v`), matrix·matrix is composition, value·matrix is `q * M` = `L_q ∘ M` (so `2 M` scales); `v * M` is a sort error wherever a value is needed
- [ ] Sums: `+`/`-` between matrices are pointwise; between a value and a matrix they attach the value as translation
- [ ] Division and powers: `x / y = x * y⁻¹` on every sort; `A^n` is composition power, `A^-1` the inverse, `A^0` the identity, `A^T` the transpose (Linear only; postfix, so `A^T^-1` chains; `^` before a bare `T` is one token, free since no power by a matrix is valid); non-integer matrix powers and singular inverses are evaluation errors
- [ ] Determinant: `det(A)`
- [ ] Linear part: `linear(A)` = `A - A 0`
- [ ] Translation: `translation(A)` = `A 0`
- [ ] Left multiplication: `matrix(q)` = `L_q`
- [ ] Rotation: `rotation(angle, axis = k)`
- [ ] Scale: `scale(q)` (axis `x` scaled by `w + [i] q` and likewise `y`, `z`, so `scale(2)` is uniform and `scale(3i + 2j)` per-axis; `scale(a) b` is the componentwise product and `scale(a)^-1 b` the quotient)
- [ ] Shear: `shear(along, by, factor)` (displaces the `along` coordinate by `factor` times the `by` coordinate, so `shear(i, j, 0.5)` moves `x` by `0.5 y`; an angle-based skew is `shear(i, j, tan(θ))`, and Graphite's two-angle skew, which applies both at once with `1` on the diagonal, is the column literal `[i + tan(θy) j, tan(θx) i + j]`, not two shears composed)
- [ ] Comparisons: `==`/`!=` are pointwise; ordering, `|M|`, and `%` are errors
- [ ] Rungs and refinements: Linear (`c = 0`) ⊂ Affine, joined like the value ladder; orthogonal refinements are 2D (`z` untouched), 3D, and geometric (`w` untouched, so the weight never leaks into position; every Graphite transform is geometric)
- [ ] A box's refinement is read from its corners, since each axis outside their rung is untouched: a weightless box is geometric, a scalar range is a map of the weight alone, and particle corners touch every axis
- [ ] Identity and zero: `I` = `[1;i;j;k]`; `[0]` is the zero matrix
- [ ] Points and directions are one type, distinguished at the use site: `A p` for a point, `linear(A) d` for a direction, `linear(A)^T^-1 n` for a normal
- [ ] Binding and queries: `Linear2` binds as the `i, j` block with `c = 0`, `Affine2` with `c` as its translation, and `Linear3`/`Affine3` fill `z`; a result queried as one of these is checked losslessly (`c = 0` for the Linear types, `z` untouched for 2D, `w` untouched for all) and is an evaluation error otherwise
## Namespaces and static typing
The sort of every subexpression is fixed by its spelling, so the library type-checks and constant-folds without host knowledge.
- [ ] Uppercase-initial identifiers are matrices, all others values: a first character (after any sigil) with the Unicode Uppercase property names a matrix (`M`, `Rot`, `Λ`), so `m`, `rot`, `λ`, and `あ` are values, and uncased scripts need a leading capital (`M行列`)
- [ ] Sigils are provenance, not type: e.g. `#name` is a variable the host publishes from its own scope (Graphite's network-scope and call-context variables); `\name` is the language's own constant or function, never shadowed by a binding or a host-supplied function; the case rule applies after either sigil, a sigiled name never collides with an unsigiled one, and `#`, `$`, `~`, `@` are free to become sigils since punctuation never begins a name
- [ ] Hosts bind values and matrices through two providers dispatched by case; a binding whose sort disagrees with its spelling is a binding error; positional matrix bindings are `A, B, C` beside `a, b, c`
- [ ] Sorts are static: every literal, call, and operator has a fixed result sort (value·value and matrix·value are values, matrix·matrix and value·matrix are matrices, `+` joins like sorts or attaches a value to a matrix)
- [ ] Rungs are static given the bindings' rungs, as a lattice join the host instantiates from its wire types: `+`/`-` join, `*` follows the product's grade rules, `/` lifts Integer to Scalar, comparisons clamp to Bool, `floor`/`round`/`gcd` to Integer, `|x|`/`dot`/`[1] q` to Scalar, `[..] v` takes the row count's rung, a piecewise joins its cases' values
- [ ] Domain climbs count for variables and fold for constants: `sqrt`, `ln`, `log`, `root`, `asin`, `acos`, `acosh`, `atanh`, and `x^y` with a variable exponent type a Scalar variable as Particle1, while `sqrt(4)` folds to Integer, `sqrt(-4)` to `2i`, and `x^2` or odd `root(x, n)` stay Scalar; `[1] sqrt(x)` narrows explicitly, and host bounds like `#[hard(0..)]` refine by interval
- [ ] Matrix types carry a rung (Linear/Affine) and a shape (2D/3D), joined the same way; literals are Linear with their shape from their entries
- [ ] The host picks the output type by querying at a rung (Integer rounds to nearest, Bool requires exactly 0 or 1, Scalar requires a zero imaginary part, Vector2 requires zero weight and `z`; a matrix as a value or vice versa is a sort error); Graphite's expression nodes expose that as an output-type selection defaulting to the static type
## Implicit multiplication
- [x] Automatically handle multiplication without explicit `*` operator
- [x] Between numbers and variables/constants (examples: `3x`, `2pi`)
- [x] Between numbers and functions (example: `4sin(90deg)`)
- [x] Between space-separated adjacent variables/constants (examples: `x y`, `pi r^2`)
- [ ] Between a matrix and its operand (examples: `M v`, `[k;j;i] v`, `2 M`)
## Units
- In addition to the unit abbreviations, full unit names can be used in either singular or plural form
- Math expressions perform dimensional analysis
- The scalar part is simplified while the unit part is kept as a fraction if it cannot be simplified
- Units should only be combined if they are mixed with other units in either the numerator or denominator, defaulting to the unit that would best avoid loss of precision
- Example: `5m + 3m -> 8m`
- Example: `5ft + 3ft -> 8ft`
- Example: `5m + 3ft -> 5.9144m` (meters are chosen because 1ft = 0.3048m exactly, while its inverse can't be represented exactly)
- Example: `asin(0.5) + 30deg -> 1.0472rad` (`deg` and `turn` convert exactly, but `rad` converts to either through `π`, so a mix with `rad` keeps the coherent unit)
- Angle is its own dimension, so `rad/s` is never a frequency and a unitless value is not an angle in arithmetic or conversion (`asin(0.5) + 1` is an error where `+ 1rad` is not)
- The circular functions, the rotation builders, and an exponent with no scalar part (`e^(i θ)`, or `e^(θ k / 2)` for a rotor about `k`) take an angle and are generic over angles and scalars: a scalar there is multiplied by `rad` first (`sin(pi)` is `sin(pi rad)`, `e^(i pi)` is `e^(i pi rad)`, `rotation(pi/2)` is `rotation(pi/2 rad)`), the formula widget renders that implicit `rad`, and every other function (`sinh`, `ln`) takes scalars only
- An angle leaves a result only by division by the angle unit that counts as one, since arcs count radians and cycles count turns: an arc is `radius * angle / rad`, a rim speed `radius * rate / rad`, a frequency `rate / turn`, and `720deg / turn`, `/ deg`, and `/ rad` are `2`, `720`, and `4π`; a product like `px·deg` stays as written, and requesting it as a length fails with the division suggested
- Angle-valued results (`asin`, `acos`, `atan`, `atan2`, `angle`) are radians, exact where they are computed and convertible on request
- Values can always be requested for conversion to a specific unit in the API
## Unit list
- Unitless
- Pixels:
- `px`
- Length:
- Meter `m` and its prefixes
- `thou`, `pc`, `pt`, `in`/`"`, `ft`/`'`, `yd`, `mi`, `nmi`
- Area:
- length^2
- `acre`, `are`, `hectare`
- Volume:
- length^3, area * length
- Paper sizes: `A0`, `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `A7`, `A8`, `A9`, `A10`
- `floz`, `cup`, `pint`, `qt`, `gal`
- `l` and its prefixes
- Mass:
- Kilogram `kg` and its prefixes
- `gr`, `oz`, `lb`, `ton`, `tonne`
- Time:
- Second `s` and its prefixes
- `min`, `hr`, `day`
- Angle:
- Degrees: `deg`/`°`
- Radians: `rad` (the coherent angle unit)
- Turns: `turn`
- Velocity:
- length / time
- `m/s`, `km/h`, `mph`, `knot`
- Acceleration:
- velocity / time, length / time^2
- `m/s^2`, `ft/s^2`, `g`
- Angular velocity:
- angle / time
- `deg/s`, `rad/s`, `rpm`, `rps`
- Angular acceleration:
- angular velocity / time, angle / time^2
- `deg/s^2`, `rad/s^2`
- Frequency:
- 1 / time (cycles per second, distinct from angular velocity because angle is a dimension: `60rpm / turn` = `1Hz` and `1Hz * turn` = `1turn/s`)
- Hertz `Hz` and its prefixes
- Illumination
- TODO: Research and lay out what these are, like candela, nits, etc.
## Unit metric prefixes
- Nano (`n`)
- Micro (`µ`, `u`)
- Milli (`m`)
- Centi (`c`)
- ~~Deci (`d`)~~
- ~~Deca (`da`)~~
- ~~Hecto (`h`)~~
- Kilo (`k`)
- Mega (`M`)
- Giga (`G`)
- Tera (`T`)
## API features
- [x] Parsing/construction separate from evaluation so the same expression can be run repeatedly with different variables without wasted performance
- [x] Setting the result of one expression to a variable for use in subsequent expressions *(by binding values into a `ValueMap` between evaluations; there is no in-expression assignment syntax)*
- [ ] Custom supplied units, unit symbols, functions, and constants *(custom functions and values work via `FunctionProvider`/`ValueProvider`; custom units do not, since units are unimplemented)*
- [ ] Static typing API: `type_of(expression, binding types)` returning the sort, rung, and matrix shape
- [ ] Matrix bindings and queries (`Linear2`, `Affine2`, `Linear3`, `Affine3`) beside the value ones
- [ ] Syntax highlighting
## Cut features
Features removed from this list, or considered and rejected, and why.
- `abs()`: replaced by the magnitude bars `|x|`, one notation on every rung
- `‖v‖`: impossible, since `||` is Or
- `real()`, `imag()`, `jmag()`, `kmag()`: replaced by the one-row literals `[1] x`, `[i] x`, `[j] x`, `[k] x`
- `arg()` and the one-argument `angle(v)`: replaced by `angle(i, [1;i] z)` and `angle(i, v)`, which spell out the reference direction and which plane is read
- `angle(q)` reading a rotor: a pure Vector2 is also a half-turn rotor, so a one-argument `angle` cannot mean both a heading and a rotor angle; it is `angle(rotation(q))`
- `arg(x, m)`: covered by `atan2(y, x, m)`
- `polar(r, θ)`: `r e^(iθ)` is its own definition, and the canvas form is `r rotate(i, θ)`, the inverse of `angle(i, v)`, which writes the reference direction
- `arcsin` and the rest of the `arc` family: one spelling per function, `asin` and friends; the entry field's autocomplete maps the LaTeX names to ours
- `trace()`: little use and misleading on the identity-padded 4×4; a 2D transform's rotation is `angle(i, A i)`
- `rotate(v, q)`, `rotation(q)`, `angle(R)`, and `angle(rotation(q))`: overloads by rung or sort (a real is also a weight-only rotor, so `rotate(v, 2)` was ambiguous); the rotation builders share one shape, angle then an optional axis, a rotor is applied as `q v conj(q)` and read as `axis(q)` and `2 angle(1, q)`, and a 3D matrix's angle waits for a matrix-to-rotor conversion
- `transpose()` and `inverse()`: replaced by the postfix `A^T` and `A^-1`, since operators have no function spellings
- The `√` prefix operator: not typeable, a duplicate of `sqrt()`, and alone without `∛` and `∜`; functions and operators never duplicate each other, in either direction
- SI's dimensionless radian, `rad` = `1`: with a frequency unit, `rad/s` and `Hz` would share a dimension, so `60rpm` would read as `2π Hz` and `60rpm + 1Hz` would add silently; the bridge at trig, `exp`, and the rotation builders keeps `sin(pi)`, `e^(i pi)`, and unitless wires unit-free without it, and `turn` makes the `2π` explicit
- Any-casing constants (`PI`, `Pi`): uppercase-initial names are matrices, and `\pi` already reaches a shadowed builtin
- `#foo`, `$foo`, `~foo`, and `@foo` variables: the identifier rule already excludes ASCII punctuation, so no sigil needs reserving until a namespace gives it meaning
- Symbol and emoji names (`👍`, `∑`) and multi-scalar clusters (flags, ZWJ sequences, keycaps): names are Unicode identifiers, which exclude them, following Rust's precedent; symbols are the operators' territory
- Typeset constant spellings `ⅇ`, `ⅈ`, `ⅉ`, `𝕜`, and `𝐈`: Unicode has letterlike `ⅈ` and `ⅉ` but no `k`, so the quaternion units cannot share a typeset family (`𝕜` is a math-alphanumeric stand-in), and `ⅇ` and `𝐈` alone would be font variants of names the language already spells
- A `Vec4` query and a `[w, x, y, z]` constructor: the quaternion is a scalar with a Vector3 attached, not a four-lane container (a Blender-style 4D noise input is a Particle3 whose weight is the evolution axis)
- The `Complex`, `Quaternion`, and `WeightedPoint` queries: renamed `Particle1` and `Particle3`, one weighted type per rung
- `hadamard()` or a componentwise `*`: `*` is the Hamilton product on every rung; `scale(a) b` is the componentwise product and `scale(a)^-1 b` the quotient, composing with the other transforms and leaving the weight alone
- Projective matrices: a free bottom row needs the homogeneous divide, which conflicts with the weight, breaks `+` on matrices, and has no home for points at infinity; perspective is a node, exact on rational geometry through the weights
- Bracket grouping and whitespace-separated literal entries: `[ ]` builds matrices and whitespace is multiplication everywhere
- A `w = 1` constant: `1` is already the weight basis
- `[a, b]` as a range: brackets build matrices, and `[a, b]` is the column literal
- Half-open ranges, Rust's `a..b` excluding `b`: a continuous interval has no one past the end, and sampling endpoints, clamp bounds, and remap all want both ends
- A midpoint bias or a sample count inside a range: neither is affine, so either breaks the inverse and the remap composition (the gradient's two midpoint curves do not even compose with each other); both belong to the sampler, as an optional column/attribute beside a host's range value
- `if(cond, if_true, if_false)`: replaced by the piecewise `{a if c, b otherwise}`, since functions and operators never duplicate each other
- An n-ary `if(c1, v1, c2, v2, ..., default)` (Excel's `IFS`), condition-first `{c: v, ...}` (Desmos), and postfix `a if c else b` (Python): each reads conditions first or in order, where math writes each value before its condition in cases whose order means nothing
- First-match resolution, and overlapping cases allowed where their values agree (exactly or within an epsilon): first-match makes case order meaningful, exact agreement compares floats at the boundaries where rounding differs, and an epsilon has no scale-free choice, is intransitive, and would be a second equality beside the exact `==`; disjointness avoids all three at the cost of writing one inequality strict
- Short-circuiting `&&` and `||`: an erroring operand is masked on the right but raised on the left, making the operators order-dependent
- Derivatives and integrals: out of scope; if ever added they are binder functions, `deriv(x^2, x, ...)` by automatic differentiation and `integral(x^2, x, a, b)` by quadrature (no indefinite form, since its constant would depend on the integrand's spelling), so no postfix `'` is reserved
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Valutazione
Questa issue non è ancora stata valutata.