Expand description
The branch-condition expression language: a small, TOTAL, non-Turing-complete
language that gives meaning to the opaque string inside a
crate::document::BranchCondition::Expression.
§Why deliberately weak
A branch condition is evaluated inside a durable, replayed run: the same expression is re-evaluated against the same routed value during replay and again during a fork, so its result must be identical every time and forever. Two properties follow, and they shape every decision here:
- Total. Every syntactically valid expression produces a
boolfor every possible JSON value, with no panic, no error, and no undefined case. There is no runtime failure mode, so a valid condition can never turn a replay into an error. - Non-Turing-complete. No loops, no function calls, no arithmetic, no
regex. Evaluation is a single linear walk of an abstract syntax tree whose
size is bounded by the
MAX_EXPRESSION_LEN-character source cap, so eval time is bounded. A Turing-complete language in a durable log would make replay time unbounded.
§The grammar
or := and ( "||" and )*
and := unary ( "&&" unary )*
unary := "!" unary | atom
atom := "(" or ")" | comparison
comparison := operand ( cmp_op operand )?
cmp_op := "==" | "!=" | "<" | "<=" | ">" | ">="
operand := literal | path
literal := string | number | "true" | "false" | "null"
path := ident ( "." segment )*
segment := ident | integerPrecedence, loosest to tightest: ||, then &&, then !, then a
comparison. A comparison therefore binds tighter than any boolean operator
(!score > 0.8 parses as !(score > 0.8)), and comparisons do not chain
(a < b < c is a parse error). Parentheses group a whole boolean
sub-expression; they are not part of a comparison operand, so (a > b) > c
is a parse error rather than a comparison of a boolean.
A bare operand used where a boolean is expected (ready, or ready && ok)
is true only when it resolves to the JSON boolean true; every other value,
and a missing path, is false.
§Paths and array indexing
A path is dot-separated segments walked from the root of the routed value:
score, output.score, items.0.score. Array indexing IS supported
because routed values routinely carry lists (a tool that returns an array, a
map join), and without it a branch could not reach into one at all. The
container type decides how a segment is read: on a JSON object a segment is a
key, and on a JSON array a purely numeric segment is a zero-based index. A
numeric segment therefore never indexes an object, so an object key spelled
with digits ({"0": ...}) is not reachable; that ambiguity is traded away
deliberately for a single, deterministic rule.
§Total semantics (these are forever: replay and fork re-run them verbatim)
- Missing path. A path that does not resolve (an absent key, an index
past the end, or a descent into a non-container) is MISSING, which is
distinct from JSON
null. Any comparison with a missing operand isfalse, and a missing operand in boolean position isfalse. Rationale: JSONnullis a value an author chose, so conflating it with “absent” would letx == nullfire on a field that simply is not there; keeping them distinct means a branch never silently fires on absent data.!still lets an author test for absence deliberately (!(score > 0.8)is true whenscoreis missing). - Type mismatch. Equality (
==,!=) is defined across all types: values of different types are never equal, so"5" == 5is false. Ordering (<,<=,>,>=) is defined only for two numbers or two strings; any other ordered comparison (a number against a string, anything against a bool/null/object/array) isfalse. Rationale: ordering has an obvious total meaning only for numbers and strings, and yieldingfalseeverywhere else means a mismatched type can never satisfy an ordering branch. - Number comparison. Two numbers compare by mathematical value. When both
are integers they compare exactly through
i128, so two distinct large integers never collide; when either is a floating-point number both are compared asf64(so1 == 1.0is true). Rationale: exact integer comparison is the safe default, and dropping tof64only when a fractional value is actually involved confinesf64’s precision limit to the case that inherently needs it. JSON numbers are always finite (JSON cannot encodeNaNor infinity), so an unorderedf64result cannot arise; the code treats it asfalseregardless, keeping eval total for any constructed value.
Structs§
- Expr
- A parsed, ready-to-evaluate condition expression.
- Expr
Error - A failure to parse an expression. Its
Displaymessage is the human-readable diagnostic, and names the offending character position where one is known. - Reference
- A parsed reference into a routed value: a dot-separated path with array
indexing, the same
pathgrammar the expression language uses for an operand, standing alone as a value reference rather than inside a boolean.
Constants§
- MAX_
EXPRESSION_ LEN - The maximum length, in Unicode scalar values, of a condition expression.
Functions§
- parse
- Parses a condition expression, or reports why it is not well-formed.
- parse_
reference - Parses a bare reference path (
items,output.items,results.0.items), the standalone counterpart of apathoperand in the expression grammar.