Mova0.5 pre-release⌕
☰
home / docs / math — the SLM's basic mathematics module

math — the SLM's basic mathematics module

math is exact mathematics for the small model: rationals, symbolic expressions, units with dimensions, steps with independent checks, and English questions read into expressions through UD trees.

The aim is the level of a university mathematics student. v1 is level 1: arithmetic, units, percentages, statistics, probability. Its application is a free weather chatbot: SLM only, deterministic, on CPU, in milliseconds.

How it works

modulewhat it does
rat.rsrational numbers on i128: always reduced; overflow is an explicit Overflow error, no silent f64; comparison via continued fractions, without overflow
units.rsdimensions (length, mass, time, temperature; fraction is dimensionless); units are an enum with exact factors to SI; the °C/°F/K scales are affine, Δ°C/Δ°F/ΔK are multiplicative
expr.rssymbolic core: an expression is a tree (rational constants, symbols x, y…, constants π, e, operations, functions sin/cos/tan/exp/ln/sqrt/abs, aggregates, percentages, probability); quantities and absolute temperatures are nodes of the same tree
eval.rsnumeric evaluation is one of the operations on the tree: quantities with units, steps (rule, formula, substitution, result), gate checks
fcheck.rsan independent path for the gate: the same tree, different arithmetic (f64 in SI, its own scale constants)
lex.rs + build.rsdictionary data/lexicon.tsv → a constant match (form → enum); a duplicate is a build error
understand.rsEnglish question → Expr via the UD tree from en
suite.rs, main.rsrunning suites; CLI

Exact core and units

Operations

Steps and checks — the gate

Every operation is a step: rule: formula | substitution | = result. Example:

question: If the chance of rain is 30% each day, what is the chance of rain at least once in 3 days?
expr:    P(at least once)[p=30%, n=3]
  1. complement: 1 − p  |  1 − 30%  |  = 70 %
  2. never (independent events): ∏ (1 − pᵢ)  |  0.7^3  |  = 34.3 %
  3. at least once: 1 − ∏ (1 − pᵢ)  |  1 − 0.343  |  = 65.7 %
  ✓ enumeration of all outcomes — 2^3 = 8 outcomes, sum of probabilities 1, target 0.657
  ✓ independent f64 path (SI)
assumption: I treat the events (days) as independent — otherwise the formula does not hold
answer: 65.7 %

Checking is easier than generating, so the check is a gate: an answer that does not pass is not returned (CheckFailed). v1 checks:

Negative control of the gate (gate_rejects_wrong_answers): a deliberately wrong answer (16.09 km instead of 16.09344 km, 13 instead of 12) turns red.

Understanding via the UD tree

  1. The en tokenizer, small boundary fixes ("C." → "C" ".", "))" → ")" ")").
  2. A dictionary chunker: numbers in digits and words (one…ninety, hundred, thousand, million, dozen, half, third, quarter, twice; "two dozen", "a third", "twenty-one", "1,000", "3/4", "-5"), quantities with units ("72 °F", "72 degrees Fahrenheit", "72 F", "10 miles per hour", "inches of mercury"), bare target units.
  3. Placeholders for the parser: "72 °F" → "72 degrees", "10 mi" → "10 units", so the en parser does not stumble on "°" and "F"; en parse (model ud-ewt-eslspok.bin, 0.12 s) and mapping of nodes to elements.
  4. Frames by lexical triggers, roles by the tree:
    • condition clause: advcl with mark if/when, a phrase with case with, preceding sentences;
    • case markers of quantities (of/from/to/in/between/by/than/for/on) — children with case;
    • function arguments — the subtree of its "of" phrase;
    • linear order is the fallback; how many markers were taken from the tree and how many linearly is visible in the trace. Arithmetic chains ("12 plus 30", "3 + 4 * 2", "(3 + 4) * 2") are read by an operator grammar in linear order: UD for "times/plus" is unreliable (in EWT "6 times 7" — times NOUN, 7 flat).
  5. If it does not come together — an honest "I don't understand" with a reason: an unknown content word ("color", "banana"), no numbers, an operator without an operand, "of" between numbers without a percentage or fraction, degrees without a scale, a source without a unit ("Convert 72 to Celsius"), several candidates for the daily probability.

CLI

cargo build --release
../target/release/math eval "What is 15% of 80?"      # expression, steps, checks, answer, trace
../target/release/math test data/suite-v1.tsv          # run a suite, summary by category
../target/release/math parse "Convert 72 °F to °C ."   # elements and tree (debugging)

The en model: MATH_EN_MODEL or en.bin.

v1 measurements (26.09)

suitequestionscorrecthonest "I don't understand"wrong
suite-v1.tsv, first run15815521
suite-v1.tsv, after fixes15815710
holdout-v1.tsv, single run303000

v2: a curriculum-based solver

v2 learns to solve problems from small to large, up to top-level world problems; for speed it calls a Rust library for exact mathematics.

Division of labour

v2 modules

modulewhat it does
big.rsbig integers and rationals — dashu (chosen by benchmark, see below); parsing and printing
nt.rsGCD/LCM with a Bézout certificate (own extended Euclid), modpow by two paths, inverse, general CRT (moduli not necessarily coprime), primality: deterministic Miller–Rabin for u64 (12 bases), for big numbers MR with 30 bases and a Pocklington certificate or an honest "probably prime"; factorization: trial division up to 10^6 and Pollard–Brent with a step and time budget; φ, τ, σ_k, μ, divisors, π(n) by sieve, k-th prime, digits in base b
comb.rsn! by a product tree, C(n,k) (second path — Legendre's prime factorization), P(n,k), Catalan, Stirling I/II, Bell, partitions p(n) (Euler versus dynamic programming), into k parts, into distinct parts (= into odd parts), derangements, Fibonacci (Cassini identity), multinomials
poly.rspolynomials over Q: arithmetic, GCD, derivative, Yun's square-free factorization; rational roots; quadratics exactly a ± b√d (d < 0 — complex) with a check in Q(√d); degree ≥ 3 without rational roots — numerically (Aberth) marked "≈"; factorization over Q; inequalities N/D ⋈ 0 by signs on intervals; expression → rational function with a memory of denominators (a root of the denominator is discarded)
linsys.rslinear systems of any size — Gauss–Jordan over the rationals: unique / infinitely many (free variables) / inconsistent (witness row)
deriv.rssymbolic derivative with rule steps (sum, product, quotient, power, chain, exponential, u^v, trigonometric, inverse) and a central-difference check at 8 points
calc.rsthe plan expression language: exact evaluation, ~90 functions, enumeration with a bound variable (sum, count, forall, first…) with step and time limits; the independent path is the same formula in f64 or a fingerprint modulo 2^61−1 (for huge integers, where f64 lies in the low digits)
plan.rsLLM plan → execution → trace with checks → gate; negative controls
vmm.rsclaude -p the same way as annot/src/llm.rs and prag/src/opus.rs (api_env(), effort via --settings), a calls.jsonl log and a spending cap before every call
curric.rslevels, sampling with seed 20260926, comparison with the gold standard, prompts
memory.rsproblem features, perceptron, plan templates
solve.rsconfigurations A, B, C and level 4 (Lean)
lean.rslevel 4: the statement is fixed, the LLM writes the proof, lake env lean checks it
bin/mathsolve.rsCLI

v1 (rat.rs on i128) serves units and weather; an i128 overflow in math eval falls through to the exact v2 core: "What is 3 to the power of 100?" → 515377520732011331036461129765621272702107522001.

Exact arithmetic: library benchmark

We do not write exact arithmetic ourselves — we take a ready one. The criterion is the license (the product is closed), then speed. Measurement bigbench/ (cargo run --release, min of 3, single thread; identical algorithms on all libraries, results cross-checked):

taskdashu 0.6.1num-bigint 0.4.8num-bigint 0.5.1
100 000! (product tree)0.036 s0.028 s0.028 s
100 000! → decimal string (456 574 digits)0.064 s0.067 s0.067 s
3^1 000 0000.0125 s0.020 s0.020 s
3^1 000 000 → decimal string0.075 s0.073 s0.073 s
GCD of two 10^5-digit numbers0.033 s0.550 s0.550 s
modpow 2048 bits × 1000.437 s0.385 s0.380 s
H(3000) exactly, rationals (num-rational 0.4.2)0.0022 s0.256 s—

dashu was chosen: GCD is 16 times faster, rationals ~100 times faster. An exact rational core does a GCD on every operation. The rest is even (±25%).

Where pure Rust falls short. Factoring semiprimes with two large factors: 25 digits (two primes ~10^12) — 0.1 s; 31 digits (~10^15) — 1.5 s; 37 digits (~10^18) — Pollard–Brent does not finish within the limits (20 s, 5·10^7 steps) and honestly says "not factored". What is needed here is not another language runtime but another algorithm: ECM or the quadratic sieve. Ready-made options — FLINT (fmpz_factor, LGPL: only dynamic linking of the system library, subject to an owner decision) or our own ECM on dashu. Such numbers did not come up in the plans of this wave.

Not used for the closed product: PARI/GP (GPL), malachite (LGPL-3.0, static Rust linking violates its terms), symbolica (commercial license). GMP/MPFR/FLINT (LGPL) — only dynamically and only by an owner decision; not connected in v2.

Plan language

The LLM's answer is one JSON line per problem:

{"id":"gsm8k-test-1173","method":"percent","steps":[
  {"id":"pods","op":"compute","expr":"100 * (1 + 20/100)"},
  {"id":"per_dollar","op":"compute","expr":"pods / 12"},
  {"op":"check","expr":"per_dollar * 12 == 120","why":"cycles per dollar times price gives the 120 pods"}],
 "answer":"per_dollar","type":"integer"}
operationwhat the SLM doesindependent check
computeexact evaluation of the expressionthe same formula in f64 or a fingerprint modulo 2^61−1; a certificate for each function (GCD — Bézout, C(n,k) — Legendre, p(n) — another algorithm, modpow — other code, remainder — `0 ≤ r < m, m
solvelinear systems (Gauss), a single polynomial or rational equation (roots with multiplicities, radicals), an inequalitysubstituting each solution into the original equations (radicals — exactly in Q(√d)), completeness of roots, product of factors; denominator roots are discarded
factorinteger — prime factors; polynomial — factorization over Qproduct = original, each factor is prime
expandexpansion into coefficientsvalues match at three rational points
derivederivative with rule stepscentral difference at 8 points
enumeratenested enumeration (bounds depend on outer variables), count/sum/min/max/list/firststep and time limits; completeness of enumeration
checkan LLM condition (substituting the answer into the problem statement)must be true
claiman LLM conjectureenumeration of all small cases; a counterexample means rejection

The answer gate: all checks green; type (integer — a count of items); for multiple-choice tests — the computed value matches the chosen option. If it fails, the plan goes back to the LLM with the reasons (≤ 2 retries), then "not solved". A panic inside the SLM is also a gate failure with a reason, not a crash of the run.

Curriculum: from small to large

A higher level is entered only when, on the held-out sample of the current level, LLM + SLM checked and got ≥ 90% right. Samples are fixed by seed 20260926 (curric.rs); each level is split in half: dev (memory and learning) and held (held out).

levelwhatsuites (license)problems
0arithmetic and word problemsGSM8K test (MIT)200
1algebra: equations, systems, polynomialsTAL-SCQ5K-EN, "equations" and "formulas", easy (MIT) — 40; MMLU high_school_mathematics, algebra (MIT) — 2060
2number theory and combinatoricsTheoremQA, integer answers, topics GCD/primes/remainders/partitions/Stirling… (MIT) — 30; TAL-SCQ5K-EN, "number theory" and "counting" (MIT) — 3060
3competition levelTAL-SCQ5K-EN difficulty ≥ 2, mathematical subtopics only (MIT) — 20; MMLU college_mathematics (MIT) — 2040
4proofsProofNet# — Lean 4 statements from textbooks, without Putnam (MIT)10

SLM priors: for GSM8K the answer is an integer (all answers in the suite are counts), for TheoremQA — the answer type from the suite's metadata; for multiple-choice tests — the options.

Not taken: hendrycks/competition_math and derivatives (MATH-500, PRM800K, MetaMathQA, NuminaMath — DMCA or AoPS content), AIME/AMC suites (AoPS/MAA), deepmind/math_dataset (no license on the HF card, the original on GitHub was not checked), MathOdyssey (no license on the card). Grey zone — only by an owner decision: IMO-AnswerBench (CC BY 4.0, 400 problems with a short answer, but the problems come from the IMO Shortlist and national olympiads), OlympiadBench (Apache-2.0, competition problems; downloaded, not used). For level 4 next — Nemotron-IMO-Bench (CC BY 4.0, 200 olympiad proof problems written for the suite; "for evaluation, not for training"). Suite cards — cards. Level 3 takes mathematical subtopics only: TAL difficulty ≥ 2 without a topic filter also holds AP statistics, biology and physics.

Measurements (26.09)

LLM — claude-opus-5-5, effort high, batches of 10 problems, 2 calls in parallel. A — the LLM alone (answer only); B — the LLM writes a plan, the SLM executes and checks, ≤ 2 retries; C — the SLM alone (perceptron + template from memory), where it cannot — B's answer. Price and tokens are the per-problem share of a call.

levelconf.problemscorrectheld correctchecked by SLM$$/problemoutput tokens/problem
0A200193 (96.5%)98/100—0.940.004751
0B200195 (97.5%)99/1002001.690.0085208
1A6059 (98.3%)30/30—0.320.005379
1B6059 (98.3%)30/30600.930.0155431
2A6056 (93.3%)29/30—0.320.005489
2B6056 (93.3%)29/30600.760.0126299
3A4040 (100%)20/20—0.230.005899
3B4040 (100%)20/20400.580.0146388

C — the SLM chooses the method on its own.

levelheldperceptron method = LLM methodsolved by the SLM alone (correct)
0100660
13071 (1)
230231 (1); first policy — 3 (1)
32090

The first policy (perceptron margin ≥ 0.5, word Jaccard ≥ 0.35) gave two wrong answers at level 2: a CRT template was spoiled by a "1" from the statement that could not be told apart from a plan constant, and "ordered subsets" and "subsets" are similar in words but not in structure. The second policy — precision over coverage: the template must be complete (every number of the statement is a slot) and unambiguous (there are no 0 and 1 among the statement's numbers), the statement structure is a skeleton with "#" instead of numbers, LCS ≥ 0.9. It was devised after seeing the L2 held-out sample, so the clean check is only on L3 (0 attempts, 0 wrong). Both solutions by the SLM alone are near-verbatim duplicates within the suites. Conclusion: with 360 solutions in memory the perceptron already guesses the LLM's method in 66% of GSM8K problems, but the SLM builds a plan on its own only for problems of the same structure. To ask the LLM less often, memory needs to be orders of magnitude larger, with templates at the level of steps rather than whole plans.

Memory: 360 verified solutions — memory.jsonl and table solutions in mathsolve.duckdb (also records — each problem × configuration, calls — each LLM call with price and tokens).

Negative controls (mathsolve controls, 7 of 7 red): substituting an intermediate result in the trace (3^40 + 17 → +1 — fingerprint modulo; 48/2 → 25 — f64), the false conjecture "n² + n + 41 is prime" (counterexample n = 40), Fermat's false conjecture "2^(2^n) + 1 is prime" (n = 5), a false LLM check (roots [2, 4]), the chosen option does not match the computed one, a non-integer result where a count is expected. The core unit tests also have red controls (wrong GCD, wrong C(n,k), wrong derivative, wrong substitution into a system).

Trace examples (mathsolve show <id>)

GSM8K, 1173 (checked in the first round):

[pods] compute 100 * (1 + 20/100) → 120            ✓ independent path: f64 gave 120
[per_dollar] compute pods / 12 → 10                 ✓ independent path: f64 gave 10
[_3] check per_dollar * 12 == 120                   ✓ LLM check: cycles per dollar × price = 120 pods
[answer] per_dollar → 10                            ✓ fingerprint modulo 2^61−1; ✓ type: integer

TheoremQA-356, "how many n ≤ 25 make n² + 3n + 2 divisible by 6" (gold 13 is wrong):

[cnt] compute count((n^2 + 3*n + 2) % 6 == 0, n, 1, 25) → 17     · enumeration of 25 cases = 17
[_2] check cnt == 25 - idiv(25, 3)     ✓ (n+1)(n+2) is always even and divisible by 3 if 3 ∤ n
[answer] cnt → 17                      ✓ fingerprint; ✓ type: integer

MMLU college_mathematics 18, work of a force along a curve (the gate returned the plan):

round 0: (-(1-0) + 0·(1²-0²) + (1³-0³)) / sqrt(2) → ≈ 0   ✗ type: integer declared, got an approximation
round 1: [dot_num] (-1)·(1-0) + 0·(1²-0²) + 1·(1³-0³) → 0   ✓ fingerprint
         [line_int] (-1 + 1³) - (-0 + 0³) → 0               ✓ (antiderivative −t + t³ of F·r′)
         check line_int == dot_num ✓; check work == 0 ✓; ✓ test: 0 — option C

CLI v2

cargo build --release
../target/release/mathsolve prep                    # level suites from $MOVA_DATA/raw/mathsolve (seed 20260926)
../target/release/mathsolve run 0 B --split held    # A | B; --limit, --batch, --par, --reserve; cap MATH2_CAP
../target/release/mathsolve c 2                     # C: the SLM alone on the level's held-out sample
../target/release/mathsolve report                  # level × sample × configuration
../target/release/mathsolve show gsm8k-test-1173 B  # problem, plan, trace, gate history
../target/release/mathsolve memory                  # memory.jsonl → $MOVA_DATA/db/mathsolve.duckdb
../target/release/mathsolve controls                # negative controls of the gate (must be red)
../target/release/mathsolve calc "binom(1000,500) mod (10^9+7)"
../target/release/mathsolve lean-prep | lean-controls | lean-run

Run directory: MATH2_RUN or math2-2026-09-26 (levels/, records.jsonl, records-c.jsonl, calls.jsonl, memory.jsonl, lean/). A repeated run skips problems already solved — we do not pay twice.

v2 licenses

dependencyversionlicensewhy
dashu (-int, -ratio, -base, -float, -cmplx, -macros)0.6.1MIT OR Apache-2.0big integers and rationals; the fastest of the permissive ones (benchmark above)
num-modular, num-order (via dashu)0.6.5, 1.2.0Apache-2.0modular arithmetic inside dashu
serde, serde_json1.0.229, 1.0.151MIT OR Apache-2.0LLM plans, traces, memory — JSON
anyhow1.0.104MIT OR Apache-2.0CLI errors
other transitive: cfg-if, itoa, memchr, pastey, proc-macro2, quote, syn, static_assertions, unicode-ident, zmij—MIT / Apache-2.0 / Unlicense; unicode-ident also Unicode-3.0utility, via serde and dashu
num-bigint 0.4.8 and 0.5.1, num-rational 0.4.2, num-integer 0.1.47, num-traits 0.2.19—MIT OR Apache-2.0only bigbench/ (measurement); not part of the math crate

Lean 4 and Mathlib (Apache-2.0) are a separate verification tool in mathproofs, not linked. Problem suites are only for measurement and the solution memory; licenses are in the curriculum table above.

Level 4: proofs in Lean (pilot)

theoremsproved$output tokens
A: LLM alone (first attempt)106 (dev 3/5, held 3/5)1.5875 345
B: LLM + Lean gate, ≤ 2 retries107 (dev 3/5, held 4/5)2.2094 253

A retry rescued Herstein 4.6.2 (irreducibility of x³ + 3x + 2 over Q: le_or_lt disappeared in the new Mathlib — the LLM rewrote it after the Lean error). Not proved: Axler 6.16 (U.orthogonal = ⊥ ↔ U = ⊤ without finite dimensionality — type classes are not synthesized; the statement seems false in general), Dummit–Foote 4.5.28 (a group of order 105 with a normal Sylow 3-subgroup is abelian — 120 lines, "motive is not type correct"), Dummit–Foote 7.2.2. The first call took 385 s for 5 theorems: Lean proofs are ~20 times more expensive than numeric plans.

Independent tests (tests3, 26.09)

What next

  1. Level 4 wider: ProofNet# in full (371 statements) — when there is a budget; a memory of verified proofs as hints (similar statements → tactics). Olympiad proofs (Nemotron-IMO-Bench) — formalization first: the LLM writes a Lean statement, the SLM checks that it is not trivial (simp/decide do not close it, the negation is not proved quickly, small cases agree with a claim enumeration), and only then the proof.
  2. Grey zone — owner decision: IMO-AnswerBench (CC BY 4.0, competition problems) is exactly the "top-level world problems" with a short answer; without it level 3 is only student/junior competition level.
  3. Ask the LLM less often: templates at the level of steps (not whole plans), memory orders of magnitude larger (large measurements — when there is a budget), the perceptron also for choosing a subset of the plan language in the prompt (a shorter prompt).
  4. Core: ECM for factoring large semiprimes (own on dashu or FLINT dynamically — owner decision), Cardano/Ferrari radicals, nonlinear systems (resultants), integrals (then Rubi rules as engine data), linear algebra (determinant, inverse, rank), symbolic root values in the plan language (currently irrational roots are approximate numbers, the exact form is only in the trace).
  5. Merging the cores: v1 (rat.rs, i128, units) and v2 (calc.rs, dashu) — a single expression tree.

Licenses

What v1 lacks

Roadmap

Division of labour: the LLM states the problem and writes the plan, the SLM computes exactly and checks. The core is symbolic from the start: numeric evaluation is only one of the operations on the tree; every result comes with an independent check where possible (substitute a root back, differentiate an integral, compare A·A⁻¹ with I).

  1. Arithmetic, units, percentages, statistics, probability — v1.
  2. Algebra: simplification, expansion and factorization, equations — linear, quadratic, systems by Gauss over the rationals (equations, systems, factorization and derivatives are in v2).
  3. Analysis: derivatives, limits, Taylor series, integrals — table, substitution, by parts. Then Rubi rules as engine data (from memory — MIT; verify the license).
  4. Linear algebra over the rationals: determinant, inverse, rank, eigenvalues via the characteristic polynomial.
  5. Discrete mathematics and number theory: combinatorics on big integers, GCD, primes, modular arithmetic.
  6. Probability and statistics: distributions, expectation, variance.
  7. First-order ordinary differential equations.

Measurement — open suites: GSM8K and MMLU college mathematics (used in v2), Rubi and SymPy tests. Verify each license before use.

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/math/