Skip to content

v4.12.20

Latest

Choose a tag to compare

@acn-ericlaw acn-ericlaw released this 01 Oct 03:11
· 110 commits to main since this release
Immutable release. Only release title and notes can be modified.

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

  1. DECIMAL statement for graph.math — the high-precision COMPUTE (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 survives graph.suspend/graph.resume and
    every event hop unchanged. + - * are exact, / is the exact quotient when it terminates and otherwise 34 significant
    digits half-even, % and **/pow with a whole-number exponent (-999 to 999), and round(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. COMPUTE is untouched.

    Upgrade action: none for the statement itself — COMPUTE is unchanged; read item 2 for the comparison change.

  2. f:decimalAdd, f:decimalSubtract, f:decimalMultiply, f:decimalDiv, f:decimalMod, f:decimalRound and f:decimalCompare simple plugins (RFC-0001 item 8, ADR-0025).
    Exact decimal arithmetic for Event Script flows and mapper nodes, with the same rules as the DECIMAL statement: a
    canonical decimal string result, scales propagated (+ - the larger, * the sum), / exact when it terminates and
    otherwise 34 digits half-even, and decimalRound(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, a null and a
    non-canonical string are errors. The f:add family 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.

  3. CanonicalPackager — a deterministic MsgPack packager (RFC-0002, ADR-0026; #481, #482). org.platformlambda.core.serializers,
    beside MsgPack: 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 a String-order sort
    differs from another engine's bytes above U+FFFF); a package is {manifest, maps} with format and format_version
    written by the packager, caller-defined string manifest fields (by convention graph_id) and the maps keyed by entry
    name in sorted order. The canonical profile: nulls kept, smallest integers, finite float64 only (a Float is widened
    through its shortest decimal text; NaN and Infinity are rejected), shortest str and bin headers, exact numbers as strings (a BigDecimal in plain notation, a zero
    of any scale "0"), dates as ISO-8601 strings, nothing else. unpack returns 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

  1. 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.

  2. graph.math round is half up, away from zero, the same as f:round (#472). The dialect used Math.round
    (half toward positive infinity), so round(-2.5) was -2 while the f:round plugin gave -3. It now rounds through
    BigDecimal HALF_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

  1. The DECIMAL guide gains the money loop, the transaction patterns and the compatibility correction. The command reference
    adds an engine-verified DECIMAL twin of the line-totals example (the COMPUTE version accumulates in double; money uses the
    twin), the skills reference adds Transaction patterns (what stays on the graph, what is a graph.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
    says DECIMAL behaves exactly as before: the numeric-string comparison and round half-up reach it, as items 4 and 5 say. The
    in-Playground help and minigraph-commands.json carry the same. RFC-0003 (the for_each index, Parked) and RFC-0004 (a bracket
    lookup plugin, Withdrawn) record two suggestions from an external review.

  2. The graph.math expression 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 and minigraph-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.

  3. 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 the for_each index, Parked) and
    RFC-0004 (a bracket-lookup plugin, Withdrawn) record two review suggestions.

  4. 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 with CanonicalPackager (builder, unpack, why it writes
    through msgpack-core and not MsgPack.pack, and that integrity is the application's decision). Both are in llms.txt.

Build

  1. Internal, no behavior change. SimpleMapper takes explicit imports, pins its pretty-print style (two-space indent, LF)
    and suppresses Sonar java:S2143 on the class, with byte-identical output (#469); test-only changes: the dialect claim
    test split into cohesive methods (#468), the f:round agreement test (#477), the Sonar cleanups on the new code (#474,
    #478, #483).