Skip to main content

Values & Numbers

This page explains what a value is, how numbers behave, and how operators group when more than one appears in the same expression.

The value model

A value in a rule-line expression is in exactly one of three states: a value, an explicit null, or absent. Absence and explicit null are distinguishable and mean different things — a connected system that sends null to mean "this field exists but has no value right now" is a real case, and collapsing it into absence would lose that distinction.

  • exists/notExists answer the absence question: is this data-access path present at all?
  • isNull answers the null question: is this path present and explicitly null?
  • Ordinary arithmetic and most operators do not ask either question themselves — they simply fail (see The failure contract) when handed null or an absent path, because rule-line requires an author to write the fallback (COALESCE, IF) explicitly rather than relying on a silent absorb.

Static analysis at save time rejects a path that the declared input schema proves can never exist, so at runtime an absent-path result should only ever occur on a genuinely optional field — Phase 2 supplies the resolver's reporting of why a path resolved to nothing (absent key, explicit null, or out of range); the schema-driven rejection itself is Phase 3's work.

The number model

Numbers are an exact decimal value type the engine owns — a scaled-integer representation, never an IEEE-754 host double. A SUM over several hundred invoice positions must not drift, and a financial booking must be exactly reconstructable from the trace; host doubles cannot make that promise.

Scale propagation is a declared rule per operation, not one invisible engine-wide working precision:

  • add/sub (+/-) take the maximum of the two operands' scales.
  • mul (*) adds the two operands' scales.
  • div (/) computes at the declared division working scale (DIVISION_WORKING_SCALE, generated on the Function & Operator Catalog page), never truncated to an integer.
  • ROUND is the one operation that sets scale explicitly, to its declared places argument.
  • ABS preserves the operand's scale.

ROUND defaults to half-up, away from zero — commercial rounding, 2.5 → 3 and −2.5 → −3 — because that is what German invoicing and a consultant authoring a template will assume without reading this page. An explicit mode argument selects an alternative (half-even, half-down, up, down, ceil, floor); the mode used is visible in the ruleset text, never a global setting.

Infinity and NaN never exist as engine values — a booking derived from Infinity is worse than no booking at all, and a NaN silently propagating through SUM into a webhook payload is a failure mode nobody could audit after the fact. Division by zero and every other operation with no defined result fails the run instead (see The failure contract).

Operator precedence

The parser reads one explicit, declared precedence and associativity table and nowhere else — conventional mathematical precedence, so an author writing arithmetic gets what they already expect. The table itself is rendered generated, on the Operators & Precedence page — never restated here in prose, because a hand-written copy of an exported table is a second copy that can drift and no staleness gate would ever catch it.

Loosest to tightest, in words: or binds loosest, then and, then the right-associative prefix not, then equality (==/!=), then comparison (</<=/>/>=), then the additive operators (+/-), and finally the multiplicative operators (*//) bind tightest.

A worked example per level:

  • or/and: a or b and c parses as a or (b and c)and binds tighter, so it groups first.
  • not: not true or true parses as (not true) or truenot's operand stops before or, which binds looser.
  • Equality vs. comparison: a < b == c < d parses as (a < b) == (c < d) — comparison binds tighter than equality, so each side of == is fully formed first.
  • Comparison vs. additive: a + 1 < b parses as (a + 1) < b — the addition completes before the comparison is evaluated.
  • Additive vs. multiplicative: 1 + 2 * 3 parses as 1 + (2 * 3), which is 7, not 9.
  • Unary minus: 2 - -3 parses as 2 - (-3) — unary minus binds tighter than every binary operator and never fails to find a left-hand operand for the following -.