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 true — not'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 -.