Expand description
Textual formula parser: "Revenue = Price * Quantity" -> Formula.
Hand-rolled tokenizer + recursive-descent parser (same shape as
improv_nl_formula), for the symbolic v1 grammar in
AGENT_FORMULA_LANGUAGE.md §4. Measure/category names resolve against a
Model (like NlContext); unknown names are a clean ParseError,
never a panic.
§Grammar (AGENT_FORMULA_LANGUAGE.md §4)
Formula = Identifier "=" Expression ;
Expression = OrExpr ;
OrExpr = AndExpr { "OR" AndExpr } ;
AndExpr = NotExpr { "AND" NotExpr } ;
NotExpr = "NOT" NotExpr | Comparison ; (* NOT a > b == NOT (a > b) *)
Comparison = Concat [ CmpOp Concat ] ; (* == <> != < <= > >= *)
Concat = Additive { "&" Additive } ;
Additive = Term { ("+" | "-") Term } ;
Term = Unary { ("*" | "/") Unary } ;
Unary = "-" Unary | Power ;
Power = Postfix [ "^" Unary ] ; (* right-assoc; -2^2 = -4 *)
Postfix = Primary { "%" } ; (* x% = x / 100 *)
Primary = Literal | Call | Special | MeasureRef | Local
| "(" Expression ")" ;
Literal = Number | Text | "TRUE" | "FALSE" | DateLiteral ; (* #2025-01-31# *)
MeasureRef = Name [ "[" Name { "," Name } "]" ] ;
Local = Identifier ; (* bound by LET / LAMBDA *)
Call = FuncName "(" [ Arg { Sep Arg } ] ")" ; (* Sep: "," or ";" (de) *)
Arg = Expression [ OverClause ] [ IntoClause ] ;
OverClause = "OVER" ( Cats [ Window ] | "ALL" [ "EXCEPT" Cats ] )
[ "WHERE" Expression ] ;
Window = "[" Bound ".." Bound "]" ; (* FIRST | LAST | THIS[+-n] *)
IntoClause = "INTO" Cats ; (* a list result, row-major *)
Cats = Name { "," Name } ;
Special = "LET" "(" { Identifier "," Expression "," } Expression ")"
| Lambda "(" Expression { "," Expression } ")" (* called on the spot *)
| "MAP" "(" Expression { "," Expression } "," Lambda ")"
| ("SCAN" | "REDUCE") "(" Expression "," MeasureRef "OVER" Name "," Lambda ")"
| ("BYROW" | "BYCOL") "(" MeasureRef "OVER" Name "," Lambda ")"
| "FIXPOINT" "(" Expression "," Lambda [ "," Expression [ "," Expression ] ] ")"
| "MAKEARRAY" "(" Name "," Name "," Lambda ")"
| "SEQUENCE" "(" Name [ "," Expression [ "," Expression ] ] ")"
| NamedLambda "(" [ Expression { "," Expression } ] ")"
(* an argument for a category parameter is a category Name *) ;
Lambda = "LAMBDA" "(" Identifier { "," Identifier } "," Expression ")" ;
Definition = Formula
| Identifier "=" "SQL" "(" Text ")" (* SQL-sourced input *)
| Identifier "=" "CALL" "(" Name { "," Name } ")" (* external fn *)
| Identifier "=" Lambda ; (* a named LAMBDA *)§Quoted identifiers ('Unit Price')
Anywhere a measure or category name may appear, it may instead be written
inside single quotes — the Quantrix spelling. This is the only way to
name a measure a CSV header produced (import_csv takes the header
verbatim, so Unit Price, Cost/Unit and 2024 Total are all real
measure names), and the only way to name one that collides with a grammar
keyword ('Over', 'SUM', 'NOT').
- Double quotes are untouched:
"..."is still a text literal,'...'is a name. They never overlap. - A literal
'inside a quoted name is written doubled:'Bob''s Rate'names the measureBob's Rate. (Quantrix/SQL convention; chosen over backslashes so the quoted form needs no escape character at all.) - A quoted name is never a keyword, a function,
TRUE/FALSEorOVER: it resolves against the model as a measure/category name, full stop. ''(empty) and an unterminated'are clean parse errors.
§Dotted qualification (Matrix.Measure) — parsed, not yet resolvable
Quantrix qualifies a name by its owning matrix
('Defined Input & Outputs'.'Income tax rate (Corporate)'). Improv has no
matrix concept yet: a measure belongs to the model, not to a matrix, so
there is nothing a qualifier could name. The tokenizer and parser therefore
accept the form and reject it with an error that names the unknown
qualifier, instead of the old unexpected character: '.'. When multiple
matrices per view land (GUI plan Step 3), Parser::parse_name is the one
place resolution has to learn about.
§The = ambiguity (assignment vs. equality)
= is only the top-level assignment separator: parse_formula splits
Identifier "=" ... first, then parses the RHS as an Expr. Inside an
expression, equality comparison is spelled == and not-equal is <>
(or !=). This keeps Revenue = Price * Quantity unambiguous while still
allowing flag = Price == 10.
§Aggregation DimensionSpec convention
SUM(Revenue OVER Time) -> Call(FuncId(1), [Ref(revenue, DimensionSpec { over: [Time], by: [], except: [] })]). The compiler (engine::compiler)
reads the arg ref’s over to pick the collapsed category, so only over
is set. Function ids come from crate::functions (SUM is 1).
§Not implemented (deferred, per §4/§11.3)
- (Date literals
#2025-01-01#and#...T...Z#ARE supported — seeparse_date_literal.) - Nothing else material: named scalar calls (
ABS,SQRT,MIN2, …) and the whole-RHS source formsCALL(...)/SQL("...")(viaparse_definition) are supported.
Structs§
- Formula
Text - A parsed formula: the target measure name (LHS of
=) plus its expression. - Lambda
Text - A parsed named-LAMBDA definition.
- Parse
Error - A formula parse failure.
positionis a byte offset into the source token stream’s originating text when known.
Enums§
- Definition
- A parsed measure definition — either an ordinary formula or a source form
(
SQL("...")/CALL(fn, m1, m2, ...)) that describes a host-side source rather than a differential-dataflow expression. Source forms carry the metadata a refresh consumes; they never enter the engine’s expression graph, preserving the deterministic core.
Constants§
Functions§
- parse_
definition - Parse a measure definition: an ordinary
Target = <expr>formula, or a source formTarget = SQL("...")/Target = CALL(fn, m1, m2, ...). The source forms are recognized only as the entire RHS (they are measure sources, not sub-expressions), keeping them out of the engine’s expression grammar and thus off the deterministic hot path. - parse_
expr - Parse just an expression (the RHS), with no target/assignment.
- parse_
formula - Parse a full
Target = Expressionformula. The LHS identifier is the target measure name (returned as-is; it need not exist yet). The RHS is resolved againstmodel. - scalar_
func - The built-in a name calls as
NAME(args...)(aggregations excluded): its id and parameter count, fromcrate::functions::all(case-insensitive, aliases included).