Expression grammar
expr ::= primary-postfix
((arith-op | comp-op) primary-postfix)*
(('not')? 'in' primary-postfix)?
('&&' expr | '||' expr)*
primary-postfix ::= unary-op* primary postfix*
unary-op ::= '!' | '-'
postfix ::= '.' Ident '(' expr-list ')' // n-arg method call
| '.' Ident // field access OR zero-arg method call
| '[' expr ']' // index
| '[' expr? '..' expr? ']' // slice
| '?' // try (Result propagation)
primary ::= literal
| '_' // wildcard / anonymous binding (rule-atom arg positions only)
| '(' expr ')'
| list-or-comprehension
| if-expr
| match-expr
| block-expr
| meta-call
| aggregate
| subquery // `query` (count/set/one/collect/exists braces)
| call-expr
| path
| struct-literal
| closure
literal ::= INT | REAL | STRING | DATE | DATETIME | BOOL
list-or-comprehension
::= '[' ']'
| '[' expr (',' expr)* ','? ']'
| '[' expr 'for' Ident 'in' expr ('where' expr)? ']'
if-expr ::= 'if' expr block ('else' (block | if-expr))?
match-expr ::= 'match' expr '{' arm (',' arm)* ','? '}'
block-expr ::= '{' stmt* expr? '}'
block ::= '{' stmt* expr? '}'
call-expr ::= path '(' expr-list ')'
meta-call ::= 'meta' '(' expr ')'
aggregate ::= ('sum'|'count'|'min'|'max'|'avg')
'(' expr ('for' Ident 'in' expr
('where' expr)? (',' rule-atom)*)? ')' // RFD 0029: trailing atoms refine the fold
struct-literal ::= path '{' (struct-init-list)? '}'
struct-init-list ::= field-init (',' field-init)* ','?
| '..' expr (',' field-init)* ','? // struct-update spread
field-init ::= Ident (':' expr)? // `name` shorthand or `name: expr`
closure ::= '|' closure-params? '|' ('->' TypeExpr)? closure-body
closure-params ::= closure-param (',' closure-param)* ','?
closure-param ::= Ident (':' TypeExpr)?
closure-body ::= expr | block
path ::= Ident ('::' Ident)*
expr-list ::= (expr (',' expr)*)?
arith-op ::= '+' | '-' | '*' | '/' | '%'
comp-op ::= '==' | '!=' | '<' | '<=' | '>' | '>=' // non-chainable
Precedence (tightest to loosest):
- Postfix (
.f(),[i],[a..b],.field,?) - Unary prefix (
!,-) - Multiplicative (
*,/,%) — left-associative - Additive (
+,-) — left-associative - Comparison (
==,!=,<,<=,>,>=) — non-associative (a < b < cis rejected; usea < b && b < c) - Membership (
in,not in) — non-associative - Logical AND (
&&) — left-associative, short-circuit - Logical OR (
||) — left-associative, short-circuit
Turbofish disambiguates generic call sites at expression position: sort::<Money>(prices).
Rounding builtins (RFD 0029). A small family of rounding functions is available in expression position — both in rule-body bindings and in the fn/compute plane — operating exactly over the numeric tower (Decimal stays Decimal, never via f64):
| Builtin | Meaning |
|---|---|
round(x) | nearest integer, ties away from zero |
round(x, n) | nearest multiple of 10⁻ⁿ, ties away from zero |
round_half_even(x, n) | nearest multiple of 10⁻ⁿ, ties to even (banker’s rounding) |
trunc(x, n) | toward zero at 10⁻ⁿ |
round_half_even is the money default: financial rounding rounds half to even to avoid the systematic upward bias of half-away-from-zero. All four return an exact value (integral results collapse to Int). A wrong argument count is refused at compile with OE1333 RoundingBuiltinArity.
Closures capture environment by value (Argon is value-semantic; no references). A closure’s type is fn(T1, T2) -> U — interchangeable with declared fn references at any function-typed argument or binding.
Property-style accessors. The paren-less postfix .Ident resolves to a stored field if the receiver type declares one with that name; otherwise to a call of a zero-argument method with that name. This unifies the .field and .zero_arg_method() forms — both read obj.name. The 30.days, 5.minutes, 60.seconds shorthand in Numeric tower and coercion is exactly this affordance over zero-arg methods on numeric types. Methods with one or more arguments always require explicit parens.
Struct-literal evaluation. A struct-literal T { … } evaluates each field initializer and assembles a struct value of type T (the data semantics — structural equality, immutability, no identity — are stated in struct and enum — language built-ins (data declarations)). The literal must state every field: a missing field is OE0249, an undeclared one is OE0250, and a field given the wrong type is OE0251. The field shorthand in field-init (a bare Ident with no : expr) takes the field’s value from a same-named binding in scope.
Functional update — the ..base spread. The ..expr form in a struct-init-list constructs a new struct value from an existing one: it evaluates base, copies every field, then overrides the fields the literal restates. Row { ..base, a: 9 } is base with a replaced by 9 and every other field carried over. The result is a fresh value; base is never modified (struct values are immutable, struct and enum — language built-ins (data declarations)). The spread supplies the fields the literal omits, so the every-field rule is satisfied by the base — a spread literal need only name the fields it changes.
pub struct Row { a: Int, b: Int }
test "spread overrides one field and carries the rest" {
let base = Row { a: 1, b: 2 };
assert Row { ..base, a: 9 } == Row { a: 9, b: 2 };
assert base == Row { a: 1, b: 2 }; // base unchanged
}
Field projection. The postfix .f reads field f off a struct value, by structure. Projection nests — outer.inner.v reads v off the inner struct off outer. A field the struct’s type does not declare is OE0254 StructFieldAccessUnknown.
pub struct Inner { v: Int }
pub struct Outer { inner: Inner, tag: String }
test "projection reads through nested struct values" {
let o = Outer { inner: Inner { v: 7 }, tag: "x" };
assert o.inner.v == 7;
assert o.inner == Inner { v: 7 };
}
Enum payload binding in match. The constant-pattern subset of match — payloadless enum constants, literals, or-patterns of those, and _ — executes (Pattern matching). A payload-binding arm (match opt { Some(x) => …x… }) destructures one payload value into one fresh variable, bound over the arm body. In value position — a let right-hand side, a comparison operand, a fn body, a return/require value — this binds and evaluates: let r = match opt { Some(x) => x, None => 0 }; is the idiomatic way to read an enum payload. The arm binds exactly one variable positionally; a zero-, multi-, or record-binder arm is OE0257 EnumPayloadPattern. A payload-binding arm in statement position (an effectful arm inside a mutate statement-match) refuses with OE1319 rather than mis-evaluating; bind the payload in value position first. The rule-body membership test path is Some(binder) / path is None is the other supported reader (struct and enum — language built-ins (data declarations), RFD 0007 Rule-atom grammar).