Repository navigation
Version 4.12.20, 10/1/2026
Exact decimal arithmetic for money and rates, and a deterministic package format for related documents, both on both engines
in lock-step (Rust 4.12.20, Increments 144, 145 and 147). graph.math gains a DECIMAL statement and Event Script gains seven
f:decimal* plugins, one specification and one set of shared vectors; a new CanonicalPackager turns a set of maps into one
byte array whose bytes depend only on its content, proven byte for byte against the Rust engine by a shared vector file.
Upgrade action: read items 4 and 5. A numeric-looking string now compares as a number, and round of a negative exact
half moves away from zero; a graph that never says DECIMAL is affected by both.
Added
-
DECIMALstatement forgraph.math— the high-precisionCOMPUTE(RFC-0001).DECIMAL: var -> expression
computes in exact decimal arithmetic and stores a canonical decimal string at{node}.result.{var}(plain notation,
the computed scale kept, a zero of any scale written"0"), so the value survivesgraph.suspend/graph.resumeand
every event hop unchanged.+ - *are exact,/is the exact quotient when it terminates and otherwise 34 significant
digits half-even,%and**/powwith a whole-number exponent (-999 to 999), andround(x, scale, mode)always names
its mode (HALF_UP,HALF_EVEN,HALF_DOWN,UP,DOWN,CEILING,FLOOR). What cannot be exact (sqrt,log,
trigonometry,random(),PI,E) and a boolean result fail by name. An operand may be a string or a JSON number; a
number is taken through the shortest decimal text it prints as, so send money as strings.COMPUTEis untouched.Upgrade action: none for the statement itself —
COMPUTEis unchanged; read item 2 for the comparison change. -
f:decimalAdd,f:decimalSubtract,f:decimalMultiply,f:decimalDiv,f:decimalMod,f:decimalRoundandf:decimalComparesimple plugins (RFC-0001 item 8, ADR-0025).
Exact decimal arithmetic for Event Script flows and mapper nodes, with the same rules as theDECIMALstatement: a
canonical decimal string result, scales propagated (+ -the larger,*the sum),/exact when it terminates and
otherwise 34 digits half-even, anddecimalRound(x, scale, mode)with an explicit mode only. An operand is a whole
number, a canonical-number string or a double (through its shortest decimal text); a boolean, anulland a
non-canonical string are errors. Thef:addfamily is unchanged. The shared plugin vectors
(decimal-plugin-vectors.json) run in the engine tests and cross-check the statement, so the two cannot drift.Upgrade action: none — new plugin names only.
-
CanonicalPackager— a deterministic MsgPack packager (RFC-0002, ADR-0026; #481, #482).org.platformlambda.core.serializers,
besideMsgPack: the same content always gives the same bytes. Every map is written with its keys sorted at every depth in
UTF-8 byte order (the ordering is done by the packager, since Gson has no ordered-keys option and aString-order sort
differs from another engine's bytes above U+FFFF); a package is{manifest, maps}withformatandformat_version
written by the packager, caller-defined string manifest fields (by conventiongraph_id) and the maps keyed by entry
name in sorted order. The canonical profile: nulls kept, smallest integers, finite float64 only (aFloatis widened
through its shortest decimal text; NaN and Infinity are rejected), shortest str and bin headers, exact numbers as strings (aBigDecimalin plain notation, a zero
of any scale"0"), dates as ISO-8601 strings, nothing else.unpackreturns ordered maps and, by default, re-encodes
the content and rejects bytes that are not canonical. Integrity is not part of it: a hash or a signature, and the algorithm,
are the user application's decision. 21 tests and a shared vector file,canonical-package-vectors.json, whose expected
bytes come from an independent encoder written from the specification (73 values, 6 packages with SHA-256, 25 rejection
cases, a seeded 60-document differential corpus, and the nesting bound of 64 levels): the Rust twin (Increment 147) passes
the same byte-identical file, so the two engines agree byte for byte. Upgrade action: none — a new class.
Changed
-
A canonical numeric string compares as a number in
== != < <= > >=(RFC-0001). Every value is rendered into
the statement text before it is parsed, so a substituted string was indistinguishable from one the author typed; a
string in plain numeric notation now compares as a number ('200' == 200,{price.result.rounded} > 100), exactly:
two distinct 20-digit ids never collapse into equal numbers.Upgrade action: read if a graph compares numeric-looking strings and relied on the string comparison.
-
graph.mathroundis half up, away from zero, the same asf:round(#472). The dialect usedMath.round
(half toward positive infinity), soround(-2.5)was-2while thef:roundplugin gave-3. It now rounds through
BigDecimalHALF_UP:round(-2.5)is-3; positive halves and non-halves are unchanged. NaN and Infinity still reach
the finite check.Upgrade action: read if a graph rounds a negative value that is exactly x.5 — the result moves away from zero.
Documentation
-
The DECIMAL guide gains the money loop, the transaction patterns and the compatibility correction. The command reference
adds an engine-verifiedDECIMALtwin of the line-totals example (theCOMPUTEversion accumulates in double; money uses the
twin), the skills reference adds Transaction patterns (what stays on the graph, what is agraph.task, the zero rule, the
remainder's sign, the decimal plugins in a mapping, the input rules), and the DECIMAL intro no longer says a graph that never
saysDECIMALbehaves exactly as before: the numeric-string comparison androundhalf-up reach it, as items 4 and 5 say. The
in-Playground help andminigraph-commands.jsoncarry the same. RFC-0003 (thefor_eachindex, Parked) and RFC-0004 (a bracket
lookup plugin, Withdrawn) record two suggestions from an external review. -
The
graph.mathexpression dialect is documented as a closed set and pinned (#467). The skills reference lists every
operator, the eighteen functions and the two constants, the command grammar no longer says "no function calls", and the
in-Playground help andminigraph-commands.json(expression_dialect) carry the same catalog; a claim and a test
(ClaimMathExpressionDialectTest) fail the build if the evaluator and the documentation drift apart in either direction. -
The decisions are recorded. RFC-0001 (exact decimal arithmetic) and RFC-0002 (the canonical packager) were raised in the
RFC register (#470) and promoted to ADR-0025 (#473) and ADR-0026 (#484); RFC-0003 (expose thefor_eachindex, Parked) and
RFC-0004 (a bracket-lookup plugin, Withdrawn) record two review suggestions. -
The packager is documented: a language-neutral spec and the Java API. The new guide page Canonical Package Format is
self-contained (the package structure, the canonical profile, key ordering by UTF-8 bytes, the strict read and its
rejections, a worked example whose bytes a test pins, and the shared vector file that proves Java and Rust write identical
bytes), and the API overview gains Deterministic packaging withCanonicalPackager(builder,unpack, why it writes
through msgpack-core and notMsgPack.pack, and that integrity is the application's decision). Both are inllms.txt.
Build
- Internal, no behavior change.
SimpleMappertakes explicit imports, pins its pretty-print style (two-space indent, LF)
and suppresses Sonarjava:S2143on the class, with byte-identical output (#469); test-only changes: the dialect claim
test split into cohesive methods (#468), thef:roundagreement test (#477), the Sonar cleanups on the new code (#474,
#478, #483).