Expression language: denotational semantics
In plain terms
The expression language panproto-expr is what you use to describe field-level transforms inside a migration (“the new full_name field is the old first plus a space plus the old last”) and predicates inside a query (“only records where created_at > '2024-01-01'”). Everything in the language is pure: no IO, no mutation, no clock reads, no random numbers. Two expressions that look the same always do the same thing. Every expression terminates within a fixed number of evaluation steps.
This page describes what those properties mean and what the evaluator actually computes.
Surface syntax
The Haskell-style surface, parsed by panproto-expr-parser:
expr ::= literal
| ident
| expr expr -- application
| "\\" ident "->" expr -- lambda
| "let" ident "=" expr "in" expr
| "if" expr "then" expr "else" expr
| "case" expr "of" alts -- pattern match
| builtin "(" expr,... ")" -- builtin application
| expr "." ident -- field access
| expr "[" expr "]" -- index
literal ::= int | float | str | bool | "null"
| "[" expr,... "]" -- list
| "{" ident ":" expr,... "}" -- record
The parser desugars if c then a else b to a Match over a boolean scrutinee with two arms, and case e of ... to a Match directly. There is no separate If or Case node in the abstract syntax.
Abstract syntax
pub enum Expr {
Var(Arc<str>),
Lam(Arc<str>, Box<Self>),
App(Box<Self>, Box<Self>),
Lit(Literal),
Record(Vec<(Arc<str>, Self)>),
List(Vec<Self>),
Field(Box<Self>, Arc<str>),
Index(Box<Self>, Box<Self>),
Match { scrutinee: Box<Self>, arms: Vec<(Pattern, Self)> },
Let { name: Arc<str>, value: Box<Self>, body: Box<Self> },
Builtin(BuiltinOp, Vec<Self>),
}
Match covers both if/then/else and case/of; pattern matching is the only branching primitive. Each arm pairs a Pattern (whose variants include Wildcard, Var, Lit, Record, List, and Constructor) with a body expression. The full enum lives at crates/panproto-expr/src/expr.rs.
Type system
The type-formation grammar:
A typing context is a finite map from variable names to types. The typing relation is defined inductively by the usual rules; selected:
Builtin signatures have type schemes given in reference/expression-language; each Builtin(op, \overline{e}) rule plugs in ’s scheme and checks that the arguments match.
Semantic domain
Let be the recursive sum
interpreting Null as the singleton and the function space as partial continuous maps. Lift to to adjoin a bottom for divergence under the step budget. Environments live in , and ranked environments in to track the remaining step budget.
Semantic function
The denotational semantics is the family
defined by structural recursion on . Write and for the budget decrement. The equations:
The budget rule fires before any equation if the remaining steps are zero; otherwise the relevant equation applies and the recursive sub-denotations are evaluated with the budget decremented. Operationally this is EvalState::tick in crates/panproto-expr/src/eval.rs; when is returned the implementation surfaces ExprError::StepLimitExceeded(max_steps).
The auxiliary is the standard pattern-match search: try each in order, attempting to unify against the scrutinee value; on the first success bind the pattern variables into and evaluate ; on exhaustion raise NonExhaustiveMatch. The auxiliary is the partial function defined in crates/panproto-expr/src/builtin.rs.
Builtin side conditions
The builtins listed under reference/expression-language are individually total or partial. The partial ones return a non- error rather than :
Div,Modwith zero divisor:DivisionByZero.- Integer arithmetic overflow:
Overflow(i64::checked_*). *ToInt/*ToFloaton unparseable input:ParseError.- List index out of bounds:
IndexOutOfBounds; list operations past the configured maximum:ListLengthExceeded. - Record access on a missing field:
FieldNotFound. Matchexhaustion:NonExhaustiveMatch.Appof a non-function value:NotAFunction.
Errors are distinct from . models resource exhaustion (StepLimitExceeded and DepthExceeded); errors model defined failure and propagate as Err(ExprError) from the implementation.
Soundness
The evaluator satisfies:
- Type preservation. If and and with , then .
- Totality (within the budget). For every well-typed and well-typed , terminates in finitely many steps with either a value or .
- Determinism. If and then .
Type preservation is enforced by the type-checker in crates/panproto-expr/src/typecheck.rs and by builtin signatures rejecting ill-typed arguments. Totality follows from the budget rule. Determinism follows from the absence of mutation and IO.
What is intentionally not modelled
- Performance. Two expressions can have the same denotation but very different cost. The semantics fixes only what is computed, not how much it costs.
- Step-budget tuning. The budget is a parameter set at the outermost call. The semantics treats it as fixed; the language itself does not expose it.
- Floating-point determinism across architectures.
Floatoperations follow IEEE 754, but bit-level reproducibility across hardware is not guaranteed.
See also
- Reference: expression-language for the builtin catalogue.
- How-to: apply field transforms for usage.
- Lens DSL for how
panproto-exprappears inside lens specifications.