Expression grammar
한국어.
This contract defines condition expressions and value-returning ternaries in TypeScript, PHP, Go and Rust. Expressions used by design settings and conditional validation use the same token, AST and evaluation contract. Behavior scripts are page JavaScript and are not evaluated by this engine.
0. Processing
The lexer converts source text to tokens. The parser converts tokens to an AST.
The evaluator receives the AST, form data and the current field path. PHP, Go and
Rust provide these stages as well as TypeScript. Evaluation does not use eval.
1. Tokens
- CUXP-001
DOT— Pattern:.; Example:.field - CUXP-002
DOT_DOT— Pattern: two or more dots; Example:..field - CUXP-003
ASTERISK— Pattern:*; Example:items.*.name - CUXP-004
IDENTIFIER— Pattern:[A-Za-z_][A-Za-z0-9_]*; Example:status - CUXP-005
STRING— Pattern: single or double quoted text; Example:'active' - CUXP-006
NUMBER— Pattern: integer or decimal; Example:42 - CUXP-007
BOOLEAN— Pattern:trueorfalse; Example:true - CUXP-008
NULL— Pattern:null; Example:null - CUXP-009
EQ NE— Pattern:==!=; Example:.status == active - CUXP-010
GT GE LT LE— Pattern:>>=<<=; Example:.count > 0 - CUXP-011
AND OR NOT— Pattern:&&||!; Example:!.enabled - CUXP-012
IN NOT_IN— Pattern:innot in; Example:.country in [US, CA] - CUXP-013
QUESTION COLON— Pattern:?:; Example:.enabled ? 'yes' : 'no' - CUXP-014
LPAREN RPAREN— Pattern:(); Example:(.a || .b) - CUXP-015
LBRACKET RBRACKET COMMA— Pattern:[],; Example:[US, CA] - CUXP-016
WHITESPACE— Pattern: whitespace, omitted from output; Example: space - CUXP-017
EOF— Pattern: end of input; Example: end of input
2. Grammar
The parser accepts one comparison per comparison expression. Parentheses group logical expressions. Ternary branches may contain further ternaries.
expression = ternary ;
ternary = logic_or [ "?" ternary ":" ternary ] ;
logic_or = logic_and { "||" logic_and } ;
logic_and = negation { "&&" negation } ;
negation = "!" negation | comparison ;
comparison = primary [ ( "==" | "!=" | ">" | ">=" | "<" | "<=" ) compare_value
| ( "in" | "not in" ) value_list ] ;
primary = path | literal | "(" logic_or ")" ;
path = [ dots ] IDENTIFIER { "." ( IDENTIFIER | NUMBER | "*" ) } ;
dots = "." { "." } ;
compare_value = primary | IDENTIFIER ;
value_list = "[" value { "," value } "]" | value { "," value } ;
value = literal | IDENTIFIER ;
literal = STRING | NUMBER | BOOLEAN | NULL ;
On the right side of a comparison, a bare identifier is a string. An identifier
followed by a dot is a path; a leading dot also identifies a path. Thus
.status == active compares with a string and .a < .b compares field values.
Ternary branch strings must be quoted. An unquoted branch identifier is a data
path, so .vip ? gold : plain reads fields named gold and plain.
An expression's AST is at most 64 nodes deep, counted from the root to the
deepest leaf. Every parenthesis, negation, ternary and operator adds a level:
((.a)) is three levels deep and .a || .b || .c three as well, because the
chain is a left-nested Binary pair. A deeper expression is a parse error with
the message Expression is nested more than 64 levels deep; the parser stops at
the limit, so no input can exhaust its stack or the evaluator's.
3. Precedence
From lowest to highest:
- CUXP-018
?:, right associative - CUXP-019
|| - CUXP-020
&& - CUXP-021
!, applied to the following comparison or negation - CUXP-022
== != > >= < <= in not in, one comparison - CUXP-023 Paths, literals and parenthesized logical expressions
For example, !.a == .b means !(.a == .b).
4. AST
The node types are Ternary, Binary, Unary, In, Path, Literal and
Group. Paths contain identifier, wildcard or numeric-index segments. Shared
fixtures compare semantic fields, excluding source positions. Tokens include
type, value and literal; token streams omit whitespace and end with EOF.
The shared fixture format defines the
serialized fields used in cross-language checks.
5. Evaluation
Condition evaluation returns a boolean. Value evaluation returns the selected branch value for a ternary, including nested ternaries; other expressions return their boolean condition result. A condition map returns its selected value as stored. A literal path outside a ternary does not return raw form data. A string in a conditional setting is an expression only when it parses completely under this grammar; any other string is a literal value.
Logical operators short circuit. Membership uses the same equality operation as
==; not in negates membership. Equality compares matching scalar types
directly; different scalar types use numeric conversion when both convert,
otherwise string comparison. Compared with a scalar, an array or an object has
no number and the empty text: .o == '[object Object]' and
.o in '[object Object]' are false for an object, 'a' == .tags is false for
tags: ['a'], 0 == .tags is false for an empty array, and '' == .tags is
true for any array. In that string comparison the text of a boolean is true or
false: .f == 'true' is true for f: true, and .f == .tags is false for
f: false and any array.
Two arrays or two objects are equal when they hold the same JSON value. Two arrays
have the same length and equal elements in the same order; two objects have the
same set of keys and equal values under each key, in any key order. Elements and
member values are compared as JSON values without conversion: values of different
types are unequal, numbers are equal by value (1 equals 1.0), strings by their
characters, and null equals null. An array never equals an object or null.
An array that a path without a wildcard names is one value, not the candidates of
a wildcard: .a == .b compares the two arrays, and .tags in a, b is false for
tags: ['a']. Relational comparisons convert both sides to numbers. A string uses its leading numeric part, or zero when no number is
available. This condition conversion is separate from numeric validation rules.
A relative .field resolves beside the current field. ..field moves one parent
level further; each additional dot moves one additional level. A row of a repeated
field is one level: its row key and the name of its collection are left together.
In a field of a group row, .field reads a field of the same row and ..field the
field beside the collection; for a value of a repeated field, .field reads the
field beside the collection. The evaluator receives with the current path the
positions of its row keys, which the runtime that builds the path knows from the
specification, so a segment is a row key only at such a position, whatever its
text: __0000000000001__, 7 and r1 resolve alike, and a field named 7 is not
a row key. An unprefixed path resolves from the data root.
| Current path | Row keys | .x |
..x |
|---|---|---|---|
a.b |
none | a.x |
x |
items.__k__.name |
__k__ |
items.__k__.x |
x |
group.items.7.name |
7 |
group.items.7.x |
group.x |
tags.__k__ (a value of the repeated field tags) |
__k__ |
x |
x: the root has no parent |
| Missing values are false in a condition. | |||
| Default field values are prepared by form binding; the expression evaluator does | |||
| not load a specification to find defaults. |
This conversion answers a condition; it does not determine whether required
input exists. The required rule rejects empty arrays and objects. Numeric zero
and boolean false are valid supplied values for that rule. Collection count and
visibility are separate contracts; see empty collections.
6. Boolean conversion
null, a missing value, false, numeric zero and the empty string are false.
Nonempty strings, including "0" and "false", are true. Arrays and objects,
including empty arrays and empty objects, are true.
7. Wildcards
A path such as items.*.name resolves repeated entries. The default evaluation
uses current row indexes when the field context supplies them. Otherwise the
remaining wildcard resolves candidate values, and a comparison succeeds when
any candidate matches. TypeScript also exposes explicit ANY and ALL
strategies to its path evaluator; those options are not part of the shared
expression facade.
8. Condition maps
Condition maps evaluate non-default keys in declaration order and return the
first matching value. The literal key true supplies the default only after all
other conditions fail, regardless of its position. With no match and no default,
the result is null.
class:
".status == 'active'": status-active
".status == 'pending'": status-pending
true: status-default
Map values are returned without expression evaluation. Each non-default key uses the same parser and evaluator as a standalone condition.
9. Verification
All four languages and the PHP extension load the same expression fixtures and compare token streams, serialized ASTs, boolean results and value results. A rejected expression is compared by its parse error message; the extension, which reports no message, only fails to parse it. The fixture generator records TypeScript output; conformance tests rerun every implementation against those expectations. Generation alone does not establish correctness. Expected semantics must agree with this contract. Implementation and test results belong in feature status.
10. Unsupported operations
Arithmetic · function calls · method calls · regular-expression evaluation · assignment · bitwise operations · slash-prefixed root paths · eval.
A validation rule can accept a regular-expression parameter independently of the expression engine. Behavior scripts remain separate from condition evaluation.