Skip to main content

Math Engine

Evaluation Pipeline​

Input processing follows a three-stage pipeline:

bytes ──► lexer ──► tokens ──► compiler ──► bytecode ──► vm ──► result
  1. Lexer (lexer::tokenise_expression) — character-by-character state machine, emits up to 64 Token values into a flat [Token; 64] array. Numbers are converted to Q31.32 immediately. Unary minus detection uses context-sensitive lookback. Mode-gated tokens: i → imaginary unit only in Advanced mode; [/] → matrix brackets only in Matrix mode; E → exponent suffix only in Scientific mode.

  2. Compiler (compiler::compile) — recursive-descent precedence climbing (same grammar as the original parser) emits opcodes into a 256 B Bytecode buffer. No AST is ever built in memory. 12 opcodes cover all operations, functions, and control flow. The bytecode is a linear program ending in Halt.

  3. VM (vm::execute) — flat loop { match op { ... } } dispatch with a 16-entry Option<Matrix> value stack on the C stack. Zero C recursion — all operand storage is in the explicit value stack. Overflow tracked via a local overflow: Option<(i64, bool)> variable rather than bubbling through call frames.

The private API exposed to the runtime consists of two entry points: engine::evaluate_expression() and engine::format_result().

Q31.32 Contract​

All numeric values are stored as signed 64-bit integers under the Q31.32 fixed-point convention, with the binary point positioned between bits 31 and 32:

xreal=xstored232x_{\text{real}} = \frac{x_{\text{stored}}}{2^{32}}

The constant FIXED_ONE equals 1≪32=4 294 967 2961 \ll 32 = 4\,294\,967\,296, giving a precision of 2−32≈2.33×10−102^{-32} \approx 2.33 \times 10^{-10}, or approximately nine decimal digits. The representable range is approximately [−2.147, +2.147)[-2.147,\,+2.147) billion in real terms.

Multiplication promotes to i128:

(a⋅b)Q31.32=⌊a⋅b232+12⌋(a \cdot b)_{\text{Q31.32}} = \left\lfloor\frac{a \cdot b}{2^{32}} + \frac{1}{2}\right\rfloor

Symmetric rounding (half away from zero) is applied via absolute-value arithmetic before the final truncation.

Division computes:

(a/b)Q31.32=⌊a≪32b⌋(a / b)_{\text{Q31.32}} = \left\lfloor\frac{a \ll 32}{b}\right\rfloor

Both return None on overflow or division by zero. Addition and subtraction use saturating arithmetic (.saturating_add, .saturating_sub) so overflow clamps to i64::MIN or i64::MAX instead of wrapping.

Error model: EvalResult​

The evaluator returns EvalResult, a tri-state enum:

enum EvalResult {
Matrix(Matrix),
Overflow { mantissa: i64, exponent: i32, negative: bool },
DomainError,
}
  • Matrix — a normal result. The unified Matrix type can represent a scalar (1×1), complex number (1×2), matrix (up to 4×4), or scientific notation value (1×2 with kind = Scientific). Results are formatted to 6 decimal places with trailing zeros stripped.
  • Overflow — the result exceeds Q31.32 range or the scientific notation display limit (|exponent| > 99). The overflow subsystem stores a log10 estimate and sign via a thread-local static, then computes a scientific-notation display at the top-level return.
  • DomainError — an invalid input (e.g. sqrt(-1) in Standard mode), rendered as ! error.

Constants from fixed_point.rs (exact Q31.32 values):

ConstantDecimal valueQ31.32 value
FIXED_PIπ\pi13,493,037,705
FIXED_Eee11,674,931,555
FIXED_LN2ln⁡2\ln 22,977,044,472
FIXED_LN10ln⁡10\ln 109,889,527,671
FIXED_SQRT22\sqrt{2}6,074,001,000
FIXED_PI_OVER_180π/180\pi/18074,961,321
FIXED_180_OVER_PI180/π180/\pi246,083,499,208
CORDIC_GAINK≈0.60725K \approx 0.607252,608,131,496

Opcodes​

12 opcodes encoded in a single byte, some followed by operand bytes:

OpcodeByteFollowed byStack effect
PushI640x018 bytes LE i64→ val
PushReg0x021 byte register→ val
PushAns0x03—→ ans
PushConst*0x04–0x06—→ π/e/i
PushMatReg0x071 byte register→ matrix
PushMatLit0x081 byte cache index→ matrix
ConstructSci0x09—→ Scientific(mant, exp)
Add/Sub/Mul/Div/Mod/Pow/Neg0x10–0x16—pop 2 → push 1
CallFunction0x201 byte function indexpop 1 → push 1
CallBinomP/PoissonP/ChiCDF/NthRoot0x40–0x43—pop 2–3 → push 1
CallMatrixFunc0x501 byte function indexpop 1 → push 1
Sto/StoMat0x60–0x611 byte registerpop 1 → push 1
LoopSum/LoopInt0x70–0x715 bytes headerpop 2 → push 1
Halt0xFF——

CallFunction replaces 22 individual opcodes (CallSin through CallLnGamma) by encoding the function index as a second byte. CallMatrixFunc similarly replaces 6 individual opcodes (CallDet through CallAdjugate).

CORDIC — Sine and Cosine​

CORDIC (COordinate Rotation DIgital Computer) computes sin⁡θ\sin\theta and cos⁡θ\cos\theta simultaneously using only shifts, adds, and a small lookup table.

Algorithm​

The rotation-mode CORDIC iteratively rotates a vector (x,y)(x,y) toward the target angle zz. Starting from:

x0=K≈0.60725,y0=0,z0=θx_0 = K \approx 0.60725,\quad y_0 = 0,\quad z_0 = \theta

where K=∏i=0n−1cos⁡(arctan⁡2−i)K = \prod_{i=0}^{n-1} \cos(\arctan 2^{-i}) is the CORDIC gain pre-multiplied into x0x_0 so the result is gain-compensated. At each iteration i=0,…,21i = 0,\dots,21:

σi=sign⁡(zi)xi+1=xi−σi⋅yi⋅2−iyi+1=yi+σi⋅xi⋅2−izi+1=zi−σi⋅arctan⁡(2−i)\begin{aligned} \sigma_i &= \operatorname{sign}(z_i) \\ x_{i+1} &= x_i - \sigma_i \cdot y_i \cdot 2^{-i} \\ y_{i+1} &= y_i + \sigma_i \cdot x_i \cdot 2^{-i} \\ z_{i+1} &= z_i - \sigma_i \cdot \arctan(2^{-i}) \end{aligned}

After 22 iterations the residual angle is ∣δ∣=∣z22∣<2−21≈4.77×10−7|\delta| = |z_{22}| < 2^{-21} \approx 4.77 \times 10^{-7} radians.

Taylor Correction​

To eliminate the residual error, a first-order Taylor correction is applied:

cos⁡(θ0+δ)≈cos⁡θ0−δ⋅sin⁡θ0sin⁡(θ0+δ)≈sin⁡θ0+δ⋅cos⁡θ0\begin{aligned} \cos(\theta_0 + \delta) &\approx \cos\theta_0 - \delta \cdot \sin\theta_0 \\ \sin(\theta_0 + \delta) &\approx \sin\theta_0 + \delta \cdot \cos\theta_0 \end{aligned}

In CORDIC coordinates (x,y)=(Kcos⁡θ0, Ksin⁡θ0)(x,y) = (K\cos\theta_0,\,K\sin\theta_0):

xfinal=x−y⋅δyfinal=y+x⋅δ\begin{aligned} x_{\text{final}} &= x - y \cdot \delta \\ y_{\text{final}} &= y + x \cdot \delta \end{aligned}

This reduces worst-case error from ∣δ∣<4.77×10−7|\delta| < 4.77 \times 10^{-7} to O(δ2)<2.3×10−13O(\delta^2) < 2.3 \times 10^{-13}.

Quadrant Folding​

Angles outside (−π/2,π/2)(-\pi/2, \pi/2) are folded using reflection identities. The function reduce_angle_to_principal normalises any angle to [−π,π][-\pi, \pi] using angle % TWO_PI.

Arctan Table​

22 entries for i=0,…,21i = 0,\dots,21 (176 bytes), each entry arctan⁡(2−i)\arctan(2^{-i}) in Q31.32. Values range from 3,373,259,426 (arctan⁡1\arctan 1) down to 2,048 (arctan⁡2−21\arctan 2^{-21}).

Arctangent — Rational Minimax​

The arctangent uses a rational minimax approximation (Ganssle-Homer form) rather than CORDIC vectoring mode, saving ∼700\sim 700 cycles. Max error <1.6×10−10< 1.6 \times 10^{-10} radians.

For ∣x∣≤1|x| \le 1:

r=∣x∣,t=r2r = |x|, \quad t = r^2

atan⁡(r)≈r⋅p0+p2t+p4t2+p6t3+p8t41+q2t+q4t2+q6t3+q8t4\operatorname{atan}(r) \approx r \cdot \frac{p_0 + p_2 t + p_4 t^2 + p_6 t^3 + p_8 t^4}{1 + q_2 t + q_4 t^2 + q_6 t^3 + q_8 t^4}

For ∣x∣>1|x| > 1, the identity atan⁡(x)=π/2−atan⁡(1/x)\operatorname{atan}(x) = \pi/2 - \operatorname{atan}(1/x) is used. Coefficients are least-squares fit on Chebyshev nodes, Q31.32 quantised.

atan2⁡(y,x)\operatorname{atan2}(y, x)​

Uses the smaller of ∣y/x∣|y/x| or ∣x/y∣|x/y| to avoid divide overflow, then corrects into the proper quadrant based on signs of yy and xx.

asin⁡(x)\operatorname{asin}(x) and acos⁡(x)\operatorname{acos}(x)​

asin⁡(x)=atan⁡(x1−x2),acos⁡(x)=π2−asin⁡(x)\operatorname{asin}(x) = \operatorname{atan}\left(\frac{x}{\sqrt{1 - x^2}}\right), \quad \operatorname{acos}(x) = \frac{\pi}{2} - \operatorname{asin}(x)

Domain: ∣x∣≤1|x| \le 1. Returns None otherwise.

Exponential — exe^x​

Range reduction to x=k⋅ln⁡2+rx = k \cdot \ln 2 + r, ∣r∣≤ln⁡2/2|r| \le \ln 2 / 2, then degree-7 minimax polynomial via Horner. Max error ∼5.95×10−11\sim 5.95 \times 10^{-11}. Returns None for overflow (k>30k > 30), Some(0) for underflow (x<−21.5x < -21.5).

Natural Logarithm — ln⁡x\ln x​

Range reduction to x=2k⋅mx = 2^k \cdot m with m∈[1/2,2)m \in [1/\sqrt{2}, \sqrt{2}), then degree-10 minimax polynomial ln⁡(1+t)≈t⋅p(t)\ln(1+t) \approx t \cdot p(t). Max error ∼1.62×10−9\sim 1.62 \times 10^{-9}. Returns None for x≤0x \le 0.

log⁡10x\log_{10} x and log⁡2x\log_2 x​

log⁡10x=ln⁡xln⁡10,log⁡2x=ln⁡xln⁡2\log_{10} x = \frac{\ln x}{\ln 10}, \quad \log_2 x = \frac{\ln x}{\ln 2}

Square Root — x\sqrt{x}​

CLZ-based initial guess via 32-entry LUT (<2%<2\% error), 3 Newton iterations on 1/x1/\sqrt{x}, then 1 final Newton step on x\sqrt{x}. Max relative error ∼3.7×10−8\sim 3.7 \times 10^{-8}.

Integer Power​

Binary exponentiation (exponentiation by squaring), O(log⁡∣exp⁡∣)O(\log |\exp|) multiplies. Negative exponents compute 1/integer_power(base,−exp⁡)1/\text{integer\_power}(\text{base}, -\exp).

Nth Root​

Newton iteration for integer n≥3n \ge 3, delegates to sqrt for n=2n=2, to exp⁡(ln⁡(x)/n)\exp(\ln(x)/n) for non-integer nn.

Hyperbolic Functions​

All identity-based: sinh⁡x=(ex−e−x)/2\sinh x = (e^x - e^{-x})/2, cosh⁡x=(ex+e−x)/2\cosh x = (e^x + e^{-x})/2, tanh⁡x=sinh⁡x/cosh⁡x\tanh x = \sinh x / \cosh x. tanh⁡\tanh saturates at ∣x∣≥12|x| \ge 12 to ±1\pm 1.

Inverse hyperbolic functions use standard logarithmic forms.

Complex Arithmetic​

Complex multiplication uses the direct formula. Complex division uses Smith's overflow-safe formula — dividing through by the larger component (∣c∣|c| or ∣d∣|d|) avoids the intermediate overflow of the naive formula. All complex transcendentals use analytic continuations (see the source for the full formula table in complex.rs).

Scientific Notation (Scientific mode)​

Scientific notation stores values as a pair (mantissa, exponent) where the mantissa is a Q31.32 value normalised to [1.0,10.0)[1.0, 10.0) and the exponent is an integer in [−99,99][-99, 99].

Literal syntax​

1.5E+10 → mantissa = 1.5·SCALE, exponent = +10
2E-5 → mantissa = 2.0·SCALE, exponent = -5
9.99E+99 → mantissa = 9.99·SCALE, exponent = +99

Fractional exponents (e.g. 2E1.2) are rejected as lex errors — the exponent must be an integer.

Arithmetic rules​

OperationFormulaRenormalisation
S1⋅S2S_1 \cdot S_2(m1⋅m2)×10e1+e2(m_1 \cdot m_2) \times 10^{e_1+e_2}Renormalise mantissa to [1,10)[1,10)
S1/S2S_1 / S_2(m1/m2)×10e1−e2(m_1 / m_2) \times 10^{e_1-e_2}Renormalise mantissa to [1,10)[1,10)
S1+S2S_1 + S_2Align exponents, add mantissasRenormalise; small addend negligible if $
S1−S2S_1 - S_2Align exponents, subtract mantissasRenormalise; result is scalar 0 if mantissas cancel
S kS^{\,k}(m k)×10e⋅k(m^{\,k}) \times 10^{e \cdot k}Compute via pow(m,k)pow(m, k), then multiply exponent

Auto-conversion to Scalar​

When a Scientific result has a mantissa with no fractional bits (mantissa & (SCALE-1) == 0) and exponent 0≤e≤80 \le e \le 8, it is automatically converted to a plain Scalar via m×10em \times 10^e. For example: 1E5 → 100000, 9E99*9E-99 → 81. Fractional mantissas (like 8.1E7) stay in Scientific notation to avoid amplifying the inherent 1/101/10 truncation error.

Overflow​

If a computation produces ∣e∣>99|e| > 99, the value cannot be stored or displayed. The runtime prints ! overflow. This is a hard limit enforced at every arithmetic step and at literal construction.

Matrix Operations (Matrix mode)​

Matrices are stored as a single Matrix type with kind = Mat. Max dimension 4×4 (16 cells). Operations:

OperationDescriptionComplexity
A+BA + BElementwise saturating addO(n2)O(n^2)
A−BA - BElementwise saturating subtractO(n2)O(n^2)
A⋅BA \cdot BStandard matrix multiply, i128 intermediatesO(n3)O(n^3)
k⋅Ak \cdot AScalar broadcast (both orders)O(n2)O(n^2)
det⁡(A)\det(A)1×1–3×3 direct formula, 4×4 Gaussian eliminationO(n3)O(n^3)
A⊤A^\topTransposeO(n2)O(n^2)
A−1A^{-1}Adjugate / determinantO(n3)O(n^3)
cofactor⁡(A)\operatorname{cofactor}(A)Minor → determinant → signO(n4)O(n^4)
adj⁡(A)\operatorname{adj}(A)Transpose of cofactorO(n3)O(n^3)
identity⁡(n)\operatorname{identity}(n)Generate InI_nO(n)O(n)

Matrix literal syntax​

[(1, 2)(3, 4)] → [1 2; 3 4] (2×2)
[(1,2,3)] → [1 2 3] (1×3)
[(1)(2)(3)] → [1; 2; 3] (3×1)

Rows are comma-separated inside parentheses, cells are comma-separated inside a row. The entire matrix is wrapped in square brackets.

4-Digit Constants​

SCALE = 2**32

def to_q3132(x: float) -> int:
return round(x * SCALE)

def from_q3132(x: int) -> float:
return x / SCALE
ConstantQ31.32 decimalFloat equivalent
FIXED_ONE4,294,967,2961.0
FIXED_PI13,493,037,705π\pi
FIXED_E11,674,931,555ee
FIXED_LN22,977,044,472ln⁡2\ln 2
FIXED_LN109,889,527,671ln⁡10\ln 10
FIXED_PI_OVER_18074,961,321π/180\pi/180
FIXED_180_OVER_PI246,083,499,208180/π180/\pi
CORDIC_GAIN2,608,131,496K≈0.60725K \approx 0.60725