Numeric precision
Lemma stores and computes numbers as exact rationals. This chapter explains how that works, where decimal appears, and what Spec authors and API clients need to know.
How Lemma handles numbers
Lemma uses three layers:
- Compute (ℚ): magnitudes stored as exact rationals (arbitrary-precision
BigIntnumerator/denominator). Arithmetic, comparisons, and unit conversions run in ℚ. No per-step decimal rounding. - Materialize (decimal): a single conversion to
rust_decimalat boundaries: API/JSON output, show/API serialization, transcendental function operands. - Output (decimal string): API responses send magnitudes as JSON strings (
"37","99.50"), never JSON number literals.
spec → parse (Decimal → ℚ) → plan → evaluate (ℚ) → try_to_decimal → decimal string
Input parsing and output materialization both enforce Decimal::MAX_SCALE (28 decimal digits).
Limits
| Layer | Constraint |
|---|---|
| Literals, JSON input, API input | Magnitude ±79,228,162,514,264,337,593,543,950,335 (~7.92×10²⁸); at most 28 decimal digits (Decimal::MAX_SCALE) |
| Internal compute (ℚ) | Arbitrary precision; bounded by available memory (all BigInt allocation is fallible) |
Intermediate values during evaluation may exceed the decimal range or use more precision than 28 digits. Internal computation never precision-fails. Only top-level rule results materialized for the response envelope are rounded to Decimal::MAX_SCALE. Magnitude overflow (|value| > Decimal::MAX) vetoes with Calculated result exceeds decimal value limit. Materialization applies when building the response envelope, not when storing in rule_results (which keeps exact ℚ values).
When an exact rational grows past what memory allows, evaluation vetoes with out of memory instead of crashing the process. This is resource exhaustion, not a decimal materialization failure.
JSON/API output never emits fraction strings ("37/47") or scientific notation.
Display vs API output
CLI and human-readable formatting show a decimal when [RationalInteger::try_to_decimal] succeeds. On magnitude overflow only, display falls back to numer/denom (debugging). JSON and API surfaces use decimal strings only; oversized top-level rule results veto instead of emitting a fraction.
Exact paths
These stay in ℚ end-to-end (no mid-pipeline decimal):
+,-,*,/,%on numbers, ratios, quantities- Unit conversion (
as <unit>):magnitude × from_factor ÷ to_factor - Comparisons
- Integer powers; rational powers when the result is exact (e.g.
4 ^ 0.5→2)
Long conversion chains telescope in ℚ. Example: 37 base through fourteen prime units forward and back stays exactly 37; stepwise decimal at fixed precision drifts to 36.999….
Approximation paths
| Case | Behavior |
|---|---|
^ on irrationals (e.g. 2 ^ 0.5) |
Decimal::MAX_SCALE digit fallback when no exact root |
sqrt, sin, cos, log, … |
Decimal::MAX_SCALE digit semantics; operands materialized via try_to_decimal |
floor, ceil, round on measures |
Operate at Decimal precision |
| ℚ allocation failure | Veto (out of memory); no decimal fallback |
| Division by zero | Literal zero divisor in a Rule (e.g. 1 / 0) → planning Error. Zero from runtime Data → Veto (never approximated) |
Boundary number contract
All API surfaces enforce a uniform rule for numeric data inputs:
| Input type | Accepted? | Rationale |
|---|---|---|
| Integer (native number) | Yes | Exact on all surfaces |
| Decimal / float (native number) | Rejected | IEEE 754 f64 cannot represent most decimals exactly |
String "0.1", "99.50" |
Yes | Parsed as exact decimal → ℚ |
This applies identically to the WASM/JavaScript API, the HTTP/JSON API, the Elixir NIF, and the Java Maven package (BigDecimal). Non-integer numeric values are rejected with "decimal values must be passed as strings to preserve exactness" (JS/HTTP/Elixir). Rust callers using Engine::run directly are unaffected (they already provide string magnitudes). Java callers use BigDecimal / string factories on FactValues — never double/float for domain decimals.
Clients (JavaScript / HTTP / Maven)
Sending data: pass decimal values as strings, integers may be native numbers:
engine.run(null, "pricing", null, { quantity: 42, rate: "0.075" });
HTTP/JSON uses the same rule (integers as numbers, decimals as strings in the request body). On Maven, use FactValues with BigDecimal or decimal strings — see Maven.
Reading results: parse numeric fields as decimal strings, not floats:
import Decimal from "decimal.js";
const n = new Decimal(json.results.price.number);
Never Number() / parseFloat() / float(). Precision breaks above ~9×10¹⁵. The same string magnitudes appear in HTTP JSON responses; parse them with a decimal library in your language. On Maven, rule magnitudes are already BigDecimal.
Spec authors
- Money, units, ratios: exact by default; define unit factors as clean literals.
decimals n: constrains decimal scale when validating Data inputs, suggestions, and value copies, not internal ℚ compute or intermediate Rule steps.- Keep deliverable top-level Rule results within the 28-digit decimal limit; magnitude overflow at output vetoes.
Related
- Reference: primitive types
- Veto: when results exceed limits or division by zero occurs at runtime
You have completed the Learn guide. For exhaustive syntax and operators, see the Language reference. To embed Lemma, see Tools & SDKs.