Skip to content

The Phero Lang expression language (Tier 2)

Phero Lang’s expression sublanguage is the deliberate replacement for Eclipse Xbase inside .mox bodies and derived-feature expressions. It is deliberately small: literals (including date("YYYY-MM-DD") date constants), field/feature access, method calls on model-declared operations, comparison and arithmetic, if/let, a fixed collection algebra (first, filter, map, any, size, sum), and a small typed calendar algebra on dates (plus_days, plus_months, diff_days). There are no arbitrary host-library calls — that is the discipline that keeps expressions portable across backends — and no clock: expressions stay pure, and “as at date D” queries take date values as operation parameters or attributes. datetime does not exist in this milestone; date is timezone-less and time-less by construction.

The pipeline is: parse (phero-expr) → type-check once in Rust against the Core IR (phero-expr, over a phero_ir::Model) → each backend lowers the typed expression tree to idiomatic target code. Because backends see typed trees, semantics must be pinned down explicitly where target languages disagree — integer overflow, string equality, string concatenation, null/Option, division by zero, and the calendar rules for dates are the disagreeing corners. Those are rules R1–R9 below; they are numbered so tests and backends can cite them.

Status: this document is the normative spec for the expression surface and typing (the front half). Backend lowering rules land with each backend; the Rust lowering is pinned below — each rule carries a “Rust lowering” subsection, and the exact Rust shapes for names, feature access, and the algebra are in “Rust lowering”.

  • Comments: // to end of line, /* ... */ block comments.
  • Strings: double-quoted with \" and \\ escapes (same as .mox).
  • Integers: decimal digits, non-negative in the token stream; a leading - is the unary minus operator, never part of the literal. A literal that does not fit in i64 is a lex error.
  • Floats: digits.digits — one or more digits, a ., one or more digits, non-negative in the token stream (a leading - is, again, the unary operator). Digits are required on both sides of the dot, so member access never lexes as a float. No exponent notation.
  • Identifiers: [A-Za-z_][A-Za-z0-9_]*. if else let true false null are reserved keywords. first filter map any size sum are contextual: they name the collection algebra only in name("...") position after a . (or ?.), so a model feature may freely be called size or first. The word date is likewise contextual: it names the date constructor only in the date ( STRING ) primary form, and a bare date is still an ordinary name.
  • Operators: == = != < <= > >= + - * / && || ! and the navigation operators . ?. ?:, plus => (lambda arrow) and ; (let separator).
expr := let_expr | lambda_expr | coalesce
let_expr := "let" NAME "=" expr ";" expr
lambda_expr := NAME "=>" expr
coalesce := logic_or ("?:" logic_or)* -- left-associative
logic_or := logic_and ("||" logic_and)*
logic_and := comparison ("&&" comparison)*
comparison := additive (cmp_op additive)* -- left-associative chain
cmp_op := "==" | "=" | "!=" | "<" | "<=" | ">" | ">="
additive := multiplicative (("+" | "-") multiplicative)*
multiplicative := unary (("*" | "/") unary)*
unary := ("!" | "-") unary | postfix
postfix := primary ((("." | "?.") member))*
member := NAME ("(" args? ")")?
args := expr ("," expr)*
primary := INT | FLOAT | STRING | "true" | "false" | "null" | NAME
| "date" "(" STRING ")"
| NAME "(" args ")"
| "(" expr ")"
| "[" (expr ("," expr)*)? "]"
| "if" expr "{" expr "}" "else" "{" expr "}"

Notes:

  • The date constructor’s argument must be a string literal — any other expression in the parentheses is a parse error (rule R6’s syntactic half). The calendar validity of the text is checked later, by the type-checker.

  • let, lambdas, and ?: chains appear at the top level of an expression or inside parentheses/brackets/argument lists. A lambda body extends as far right as possible; parenthesize to use a lambda as a binary operand.

  • == is the canonical equality operator. A single = is accepted where an operator is expected and means exactly the same thing (see R2); it cannot be confused with the = of let, which follows the let NAME prefix.

  • NAME ("(" args? ")") after . is a call. If NAME is one of the six algebra names, the argument shape is validated syntactically (see the algebra table): first takes an optional lambda, filter/map/any require exactly one lambda, size/sum take none. Any other NAME with parentheses is an operation call on a model-declared op.

  • A bare NAME is a variable reference — a let binding or a lambda parameter — except in the bare-call form NAME "(" args ")", which is a package-function call (see “Function calls” under the type system). A bare NAME with no parentheses is never a call. Feature access always has an explicit receiver; the one receiver that is not a variable is the enum-literal namespace (rule L3): EnumName.Member where EnumName is not a scope binding is a typed member literal.

Level Operators Associativity
let / lambda prefix forms body maximal
coalesce ?: left
logic or `
logic and && left
comparison == = != < <= > >= left
additive + - left
multiplicative * / left
unary ! - prefix
postfix . ?. calls, algebra left

if is a primary: its condition and both branche bodies are full expressions.

Types (Ty):

  • Primitives: string int long short float double boolean byte char date (mirroring phero_ir::PrimitiveType). A date is a timezone-less calendar day (YYYY-MM-DD); it has no time-of-day component and no timezone.
  • Named types: class, enum, datatype, interface, vocabulary — always package-qualified in the checker (Named { kind, package, name }), because they come from a resolved phero_ir::Model.
  • List<T> — a to-many feature value or algebra result.
  • Option<T> — an optional (0..1) feature value; also the result of ?. chains and first.
  • Null — the type of the null literal. It is not Option<T>; it exists so null can participate in equality (R2) and ?: (R3) and nowhere else.

Booleans are boolean (true/false literals). Comparisons yield boolean.

L1 (literal polymorphism). An integer literal is typed int by default. In a context whose expected type is long — the other operand of an arithmetic/comparison operator, an operation argument, or a list element base — an integer literal is typed long instead (its value always fits i64). A float literal (digits.digits) is the float analogue: typed double by default, adapting to float in a float context — the other operand of an arithmetic/comparison operator, an operation argument, or a list element base — in either operand order (the feature’s float type wins; see the float special cases of L2). Everywhere else, and for everything that is not a numeric literal, there are no implicit conversions: in particular there is no implicit int → long or float ↔ double (see U1). A literal typed int whose value does not fit in 32 bits is an R1 constant-overflow error; float literals have no compile-time range rule.

L2 (operand rule). + - * / require both operands to have the same numeric type and produce it — except +, which additionally admits two string operands and concatenates them (rule R9). < <= > >= require the same numeric type — or two date values (rule R7) — and produce boolean. && || ! operate on boolean only; unary - takes a numeric. There is no ordering on string; strings support equality (R2) and + concatenation (R9). Dates take no arithmetic operators at all: calendar arithmetic is the typed method algebra described under R8, never +/-.

L3 (enum-literal namespace and closed membership). Enums carry a closed literal set, and expressions can name members as typed values: a dotted name EnumName.Member whose receiver is not a scope binding resolves through the enum-member registry (DomainTypes::insert_enum, fed from the model’s EnumDef literals), and the member types as the enum — so status == PayRunStatus.Draft is the typed spelling of an enum comparison, in either operand order, == or !=. Scope wins: a bound name shadowing an enum name takes the ordinary feature-access path. A bare enum name without .Member stays an ordinary name (a type, not a value); a member chain past the literal (EnumName.Member.x) is an unknown-feature error. A bare name declared by several packages is ambiguous — a type error naming every declaring package (the package-function precedent). The .ddd machine-state binding (docs/DDD.md “Condition binding”) predates and composes with this rule: bound state names are scope bindings, so they keep winning over the namespace.

The same closed set backs two membership checks, both compile-time:

  • A dotted access naming an undeclared member is a type error (`Member` is not a literal of enum `EnumName`, with a did-you-mean hint when exactly one declared member is within edit distance 2).
  • A string literal compared (==/!=) against a plain enum-typed operand where the literal names no declared member is the same closed-set error — never the generic R2 mismatch. A declared member still fails R2’s same-type rule: the anti-magic-strings stance is deliberate, and the typed spelling is the dotted form, never a bare string (docs/DDD.md). Option-/List-wrapped enums and enums registered without a member set (the kind-only insert_named host seam) defer to the ordinary R2 rule.

Vocabularies are deliberately outside the registry: their entries are lockfile-backed facets with different member semantics (entry keys, not nominal literals), and an entry set can be legitimately empty before a snapshot is vendored — a closed-set check over an absent snapshot would false-positive. Vocabulary members stay unchecked: no dotted namespace, no membership check.

C1 (.ddd service-guard condition scope). A .ddd service operation’s when guard (issue pherolang#180) types under a scope the driver assembles, not the checker alone; the rule is test-citable so crates/phero-driver/tests/ddd_conditions.rs can cite it. The scope’s bindings, in shadowing order (later wins):

  1. the implicit-self features — the module’s unique machine-bearing class, or the class a resolved transition binding pins (issue pherolang#176); absent in a multi-machine module without a pin;
  2. in a multi-machine module, each machine’s bound field as an enum-typed name — by its authored field name when that name is unique across the module’s machines, and always by the enum-derived spelling (lowerCamel of the machine’s enum type name: machine status: PayRunStatus binds payRunStatus);
  3. the machine states as enum-typed names (the machine-bound rule; a state two machines declare never binds);
  4. the service’s injected repositories as class-typed roots (inject TimecardRepository; binds TimecardRepository to the repository design’s class), so TimecardRepository.status navigates as ordinary feature access.

An unresolvable reference is the driver’s E0353/E0354 (the driver.ddd band), not a checker error; a resolved path that mistypes is the ordinary L2/R2 rules with the condition span. The driver lowers the resolution into the artifact as conditionPaths — see docs/DDD.md “Condition scoping” for the row discipline.

Guard lint. A string literal compared (==/!=) against a String-typed operand inside a guard expression type-checks (it is a string equality), but records a lint: TypeChecker::guard_string_lints returns it, and every guard-hosting surface (.ddd conditions, .actor when, .evt filters, .cmmn criteria, .uc step guards) emits it as a WARNING suggesting the field be declared as an enum so the compared literals are checked against its members — the use-based companion of the name-based bare-String declaration rule. The empty string is excepted: field != "" is a sentinel/emptiness check, and an empty string can never be a declared member, so empty-literal comparisons stay silent. The lint is name-blind and type-driven: it fires only where a guard actually compares, and defers silently whenever a type cannot be resolved (no type context, unbound names — the domains: None contract).

U1 (if unification). if requires a boolean condition. Both branches must have the same type — compared exactly, with no implicit int → long and no literal adaptation. The if expression’s type is that type.

Feature typing (mirrors the resolver’s IR):

  • attribute/derived: to-many (is_many) → List(T); lower bound 0 → Option(T); otherwise T (the declared type).
  • contains/refers: to-many → List(Named); single-valued → Named (never Option — a single containment/reference is always present).
  • container: always Option(Named), whatever the declared multiplicity.
  • op call: the declared return type lifted by the operation’s return_multiplicity (0..* → List(T), 0..1 → Option(T), unset → the plain type — the same mapping as a bare function call), after argument checking (arity and per-parameter types; a class-typed parameter expects that Named type). L1 applies to arguments. Domain operations are single-valued and never set the multiplicity; the .ddd body checker’s synthetic repository operations use it for findById/findAll.

Function calls. The bare form name(args) (no receiver) resolves against the sigil-lowered package functions of the domain union (phero_ir::Package::functions), indexed by bare name across every package. A name declared by several packages is a type error naming them all; an unknown name is a type error at the name span (with a did-you-mean hint when exactly one indexed function is within edit distance 2). Each parameter expects its declared type mapped by multiplicity — is_many → List(T), lower bound 0 → Option(T), else T — with L1 literal adaptation and the R1–R4 argument rules unchanged; there is no R3 auto-propagation, so an Option argument does not satisfy a plain parameter. The call’s type is the declared return type lifted by the function’s return multiplicity, the same mapping as the parameter rule above: 0..* → List(T), 0..1 → Option(T), the single-valued convention (unset, or a required (1..1)) → T (a sigil func output’s cardinality lowers into the multiplicity; wire contract rule 15, and the Rust backend’s emitted signature mirrors it). There is no ?. form (a function has no receiver to be optional). Receiver calls (x.name(...)) never consult the function index — they resolve class operations only.

Unknown features, unknown operations, and unknown names are type errors whose diagnostic span points at the offending name token.

Navigation. Accessing a feature or operation on a receiver whose type is Option(T) without ?. is a type error (R3). ?. unwraps an optional receiver for the access and makes the whole access yield Option of what the access would have yielded, wrapping only if the result is not already Option. Collection algebra requires a List receiver (List is never optional in this typing: a to-many feature is List(T) even when its lower bound is 0).

These nine are where target languages disagree. They are normative: every backend lowering a typed expression tree must preserve them, and the type-checker enforces their compile-time halves. Each rule is cited by tests as R1–R9. R1–R4 predate the date primitive; R5–R8 pin the calendar semantics that came with it (issue #9); R9 adds string concatenation to +.

Arithmetic on int/long is checked: overflow is a runtime panic contract. The lowered Rust backend uses plain +/-/* (Rust panics on overflow in debug builds); every other target must trap equivalently — never wrap, never saturate.

Compile-time half: constants that overflow are a type-check error. Two integer constants combined by +/-/* are folded with checked arithmetic and must fit the result type (int = 32-bit signed, long = 64-bit signed); an int-typed literal whose value does not fit 32 bits is likewise rejected. Unary minus folds too (-i64::MIN overflows). Non-constant overflow is a runtime panic per the contract above; long constant pairs never fold in practice because a literal only becomes long in a non-constant context (L1), so long overflow remains purely a runtime contract.

Rust lowering. Plain +/-/* on the literal-suffixed operands (2i32/2i64 per L1); Rust’s debug-build overflow panic is the contract:

(self.pages * 2i32) // `pages * 2`; panics on overflow in debug builds

== (and its alias =) on string operands is value equality. Equality is null-safe: null == x is false unless x is null (and symmetrically); null compares with a value of any type.

== on Option<T> is element-wise: Some(a) == Some(b) iff a == b (recursively, so nested options and strings compare by value); None == None is true; None == Some(_) is false. ==/!= yield boolean.

Typing: both operands must have the same type (L1 literal adaptation applies), or one of them must be the null literal. Comparing values of two different named/primitive types is a type error.

Rust lowering. Plain ==/!= (Rust String equality is value equality; Option<T>’s PartialEq is exactly the element-wise rule; == borrows both sides, so nothing moves). Against null: is_none() when the other operand is optional, otherwise the constant answer (false/true — a required value can never be absent). A string literal operand emits a bare &str, since String == &str is value equality:

self.title.clone() == "Dune" // value equality, no allocation
book.library.is_none() // `book.library == null`

Navigating an optional feature (multiplicity 0..1, or a container) yields Option<T>.

  • Calling a feature or operation on an optional receiver without ?. is a type error.
  • ?. propagates None: a?.b evaluates to None when a is None, otherwise to the access result, wrapped in Option (unless it already is one, e.g. another ?. hop or a container feature).
  • ?: default coalesces: a ?: b evaluates to a unless a is None, in which case it evaluates to b. Its type is the inner type T of Option<T> (or the right-hand type when the left is the null literal).
  • A to-many contains/refers feature yields List<T> — the empty list is a valid value; there is no Option around a collection.

Rust lowering. ?. over an optional receiver chains and_then (the lookup) with map (or a second and_then when the feature itself is optional-valued); ?. over a non-optional receiver yields Some(..) unless the result already is an Option. ?: lowers to unwrap_or — the default is evaluated eagerly in Rust (expressions are pure, so the only visible difference would be a panicking default):

lib_id.and_then(|id| res.library(id)).map(|o| o.name.clone())
// `lib_id?.name`
lib_id.and_then(|id| res.library(id)).map(|o| o.name.clone())
.unwrap_or("anon".to_string()) // `... ?: "anon"`

Integer division by zero is a runtime panic contract — the same behavior as Rust: / on int/long with a zero divisor panics; other targets must trap equivalently. (Floating-point / follows IEEE semantics and never panics.)

/ on integers is truncating (rounds toward zero, the Rust/C family behavior), for both int and long.

Compile-time half: a constant zero divisor (any expression that folds to the integer constant 0, including -0) is a type-check error. The constant-folding half of R1/R4 is integer-only: const_of never folds float literals, so float division is never compile-checked (IEEE semantics, per the parenthetical above).

Float literal lowering. A float literal lowers with its L1-adapted type as the suffix — 1.5f64 by default, 1.5f32 in a float context (mirroring 2i32/2i64).

Rust lowering. Plain / — Rust integer division is already truncating toward zero and panics on a zero divisor, which is exactly the contract:

(self.pages / 2i32) // truncating; panics when the divisor is zero

A date is an i32 count of days since the epoch 1970-01-01 (negative before it) in the proleptic Gregorian civil calendar. plus_days(n) and diff_days(d) are checked i32 arithmetic on that day number: overflow is a runtime panic contract — never wrap, never saturate. plus_months obeys the same contract at its bounds (R8).

Compile-time half: a date("…") literal whose day number does not fit the i32 scale is rejected at compile time under R6, so a checked literal can never fail to construct downstream.

Rust lowering. The runtime type phero_runtime::Date implements the arithmetic with checked_add/checked_sub and panics with an R5-citing message on overflow:

(self.effective_date.plus_days(30i32)) // checked; panics past the i32 scale

date("YYYY-MM-DD") is a constant, not a constructor call: there is no runtime constructor in the language (generated code may build its target date type directly). Two halves, both compile-time:

  • Parse-time (syntactic). The argument must be a string literal; any other expression in the parentheses is a parse error.
  • Check-time (semantic). The text must be a strict ISO-8601 calendar date: a year of at least four digits (a leading - admits extended negative years), two-digit month 01–12, two-digit day, and a day that exists in the proleptic Gregorian calendar (leap years included). Anything else — 2026-9-17, 2026-13-01, 2026-02-30, 2023-02-29 — is a type-check error whose span is the string literal, plus the R5 day-number bound. A valid literal types as date.

Rust lowering. The literal lowers to the runtime’s strict parser with the validity already proven, so the expect documents an invariant:

phero_runtime::Date::from_str("2026-09-17").expect("R6-checked date literal")

The six comparison operators are total on date:

  • < <= > >= order two dates by the total civil-calendar order — exactly the day-number order of R5 (there is no separate “granularity”: dates are days).
  • ==/!= are value equality (an R2 extension), null-safe like every equality (an optional date compares against null per R2/R3).

A date and a non-date are a type error for all six, per L2/L1 (no implicit conversions, no literal adaptation — the date("…") form is its own type).

Rust lowering. Plain operators: phero_runtime::Date derives PartialEq and Ord over the day number, so ==/</… are integer comparisons:

(self.effective_date == self.due_date)

plus_months(n) shifts the calendar month and clamps the day to the target month’s length: Jan 31 + 1 month = Feb 28 (Feb 29 in a leap year), Aug 31 + 1 month = Sep 30, Dec 31 + 2 months = Feb 28/29. The clamp never carries into the next month, and it is not sticky: each step clamps against its own target month, so (Jan 31 + 1 month) + 1 month = Feb 28 + 1 month = Mar 28. plus_days and diff_days are plain day arithmetic and never clamp. R5’s panic contract applies when the result leaves the i32 scale.

Rust lowering. Implemented once, in phero_runtime::Date::plus_months (month arithmetic in i64, day clamped with min(days_in_month), R5 panic on overflow); lowered calls are plain method calls:

(self.effective_date.plus_months(6i32)) // 2026-01-31 + 6 months → 2026-07-31

+ admits two string operands, in which case it concatenates them: a + b is the string whose characters are a’s followed by b’s. Chains are left-associative (the additive level), so title + " - " + synopsis is (title + " - ") + synopsis.

Optionality follows R3: an absent operand yields an absent result — Option<string> + string, string + Option<string>, and Option<string> + Option<string> all type as Option<string>, and the concatenation happens only when every operand is present. The null literal is not a string operand: it behaves exactly as on the numeric path, which rejects it, so null + s and s + null are type errors (null exists only for equality, R2, and ?:, R3).

There are no implicit conversions between string and any numeric, boolean, or date type (L2): when either operand of + is a string (or Option<string>), the other must be too, and anything else — an integer, boolean, date, enum, class value, list — is a type error naming the mismatch. When neither operand is a string, + is entirely the numeric operator: L1 literal adaptation, the L2 same-type rule, and the R1/R4 compile-time halves apply unchanged.

Rust lowering. format!("{}{}", lhs, rhs) on the owned String operands — Rust’s consuming Add is not implemented for String + String, so plain + cannot carry the rule. Every binary node lowers to one format!, so left-associative chains nest safely:

format!("{}{}", format!("{}{}", self.title.clone(), " - ".to_string()),
self.synopsis.clone())

An operand that may be absent propagates through Option::zip (the present side rides in Some), per R3:

self.citation(res).zip(Some(self.title.clone()))
.map(|(concat_lhs, concat_rhs)| format!("{concat_lhs}{concat_rhs}"))

A sigil-lowered Rune function body (the "rune" target, wire contract rule 15) is a statement layer above this expression language — not a second semantics: every expression slot in it (guards, alias bindings, set/add steps) is a phero-expr program, checked and lowered by the same checker and lowerings as any expr body, so the rules above apply inside those slots unchanged. Nothing Rune-specific re-derives overflow, equality, Option, or division semantics — the carry-over is a pinned contract, not a lowering accident. The statement layer itself (guard phases, binding scoping, accumulation order, the driver’s typechecking and deferral contract) is specified in docs/RUNE-BODIES.md; the Rust lowering of a checked body is pinned in docs/BACKENDS.md.

The algebra is fixed — these six, and nothing else. Lowerings must emit the target language’s idiomatic equivalent; no other collection operations exist in the language (no host-library calls, per the mandate).

# Form Receiver Lambda Result Notes
A1 x.first() / x.first(p => e) List(T) optional; T → boolean Option(T) first matching / first element
A2 x.filter(p => e) List(T) required; T → boolean List(T) order preserved
A3 x.map(p => e) List(T) required; T → U List(U) any U, including boolean
A4 x.any(p => e) List(T) required; T → boolean boolean false when empty
A5 x.size() List(T) none int element count
A6 x.sum() List(T), T numeric none T 0 when empty

Lambda parameters are single, untyped, and inferred: the parameter of a lambda passed to an algebra call has the receiver’s element type T. A lambda outside an algebra call (where its parameter type cannot be inferred) is a type error. Inside the lambda body the parameter shadows outer bindings of the same name (shadowing is always allowed).

The algebra is generic over the element type wherever the table says so, which includes date elements: first/filter/map/any/size work on List<date> unchanged (and the calendar-algebra methods may appear inside their lambdas), while sum remains numeric-only — summing dates is a type error.

The Rust backend (phero-backend-rust) lowers the typed tree with expr_lower::lower_expr. The shapes below are normative for the Rust target and pinned by crates/phero-backend-rust/tests/expr_lower.rs.

Values, names, and self. Class-typed values are their typed ids (BookId, always Copy); every other type is its Rust value. The implicit self (the class owning the body) puts each of its features in scope as a bare name, read directly off the struct (books → self.books.clone(); derived features call the generated accessor, citation → self.citation(res)). Operation parameters and let bindings are locals: Copy values are used plainly, everything else clones on use — generated code is correct in every context and never moves out of self. Inside an emitted closure the same rule holds (.clone() compiles whether the capture was by reference or by move). if lowers to a Rust if expression, let to a scoped block { let x = ..; .. }, list literals to vec![..], and every binary node is parenthesized, so precedence is safe by construction.

Feature access through res. Reading a feature of a class-typed receiver goes through the arena: res.book(RECV).map(|o| FIELD).expect("dangling Book id"). The expect asserts the arena invariant that stored ids resolve (generated mutators never delete objects); it is the one failure mode beyond R1/R4/Option. Field shapes on the resolved object o: &Book: value features clone when non-Copy (o.title.clone()) and read plainly when Copy (o.pages); reference/container features are the id or Option<id> field (o.library); to-many features clone the vector (o.authors.clone()). Safe navigation (a?.b) propagates None with and_then/map as described under R3. Operation calls become method calls on the resolved object: res.book(b).expect("dangling Book id").find_book(title.clone()).

A1–A6 in Rust. The receiver expression X lowers independently and the closure parameter is the element reference (one reference layer for map/any items, two for the find/filter predicates’ &Self::Item):

# Form Rust shape (result)
A1 X.first(p => PRED) X.iter().find(|b| PRED).copied() / .cloned() → Option<T>
A1 X.first() X.first().copied() / .cloned() → Option<T>
A2 X.filter(p => PRED) X.iter().filter(|b| PRED).copied()/.cloned().collect::<Vec<_>>()
A3 X.map(p => BODY) X.iter().map(|b| BODY).collect::<Vec<_>>()
A4 X.any(p => PRED) X.iter().any(|b| PRED) → bool
A5 X.size() X.len() as i32 → int
A6 X.sum() X.iter().sum::<i32/i64/…>() (annotated element width; 0 when empty)

.copied() adapts Copy element types; .cloned() clones only the elements actually selected (for A2, only the matches). A6 sums by shared borrow (iter

  • Sum<&T>), so it works on stored fields and temporaries alike; the sum is plain +, keeping the R1 contract. An A1 result is naturally Option; the remaining forms yield plain values, so their ?. forms wrap in Some(..).

Dates in Rust. The date primitive maps to phero_runtime::Date, the runtime support crate’s Copy newtype over the R5 day number. A literal lowers to the strict-parse-plus-expect shape of R6; the calendar algebra lowers to plain method calls (plus_days/plus_months/diff_days), and ?. over an optional date propagates with map:

(self.effective_date.plus_months(6i32))
(self.maturity_date.map(|__phero_date| __phero_date.plus_days(1i32)))
  • let x = e1; e2: x is bound to the value of e1 while checking/evaluating e2. Shadowing an existing binding (including lambda parameters) is allowed, including shadowing with a different type.
  • Expressions are total in the checker’s model: every well-typed expression yields a value of its type or panics per R1/R4/R5 — there is no third failure mode other than Option (R3).

phero_expr::parse never panics and performs best-effort recovery like phero-syntax: it returns the recovered expression (if any) plus every error with byte spans. TypeChecker::type_of collects all type errors in one pass (subexpression errors do not cascade into spurious operator errors). The guard lint of rule L3 is a separate opt-in pass (TypeChecker::guard_string_lints), not part of type_of: it changes no typing and only guard-hosting drivers run it.