Expressions
Expressions compute values.
Syntax
expression = with_expr | let_expr | if_expr | for_expr | loop_expr | lambda | binary_expr .
primary = literal | identifier | "self" | "Self"
| "(" expression ")" | list_literal | map_literal | struct_literal .
list_literal = "[" [ expression { "," expression } ] "]" .
map_literal = "{" [ map_entry { "," map_entry } ] "}" .
map_entry = expression ":" expression .
struct_literal = type_path "{" [ field_init { "," field_init } ] "}" .
field_init = identifier [ ":" expression ] .
Postfix Expressions
postfix_expr = primary { postfix_op } .
postfix_op = "." identifier [ call_args ]
| "[" expression "]"
| call_args
| "?" .
call_args = "(" [ call_arg { "," call_arg } ] ")" .
call_arg = named_arg | positional_arg .
named_arg = identifier ":" expression .
positional_arg = expression .
Field and Method Access
point.x
list.len()
Index Access
list[0]
list[# - 1] // # is length within brackets
map["key"] // returns Option<V>
Lists/strings panic on out-of-bounds; maps return Option.
Function Call
add(a: 1, b: 2)
fetch_user(id: 1)
print(msg: "hello")
assert_eq(actual: result, expected: 10)
Named arguments are required for direct function and method calls. Argument names must match parameter names. Argument order is irrelevant.
Positional arguments are permitted in two cases:
- Type conversion functions (
int,float,str,byte):
int(3.14) // OK: type conversion
float(42) // OK: type conversion
str(value) // OK: type conversion
- Calls through function variables (parameter names are unknowable):
let f = (x: int) -> x + 1
f(5) // OK: calling through variable
let apply = (fn: (int) -> int, val: int) -> fn(val)
apply(fn: inc, val: 10) // outer call: named required
// inner fn(val): positional OK
It is a compile-time error to use positional arguments in direct function or method calls.
Error Propagation
value? // returns Err early if Err
Unary Expressions
unary_expr = [ "!" | "-" | "~" ] postfix_expr .
! logical not, - negation, ~ bitwise not.
Binary Expressions
binary_expr = or_expr .
or_expr = and_expr { "||" and_expr } .
and_expr = bit_or_expr { "&&" bit_or_expr } .
bit_or_expr = bit_xor_expr { "|" bit_xor_expr } .
bit_xor_expr = bit_and_expr { "^" bit_and_expr } .
bit_and_expr = eq_expr { "&" eq_expr } .
eq_expr = cmp_expr { ( "==" | "!=" ) cmp_expr } .
cmp_expr = range_expr { ( "<" | ">" | "<=" | ">=" ) range_expr } .
range_expr = shift_expr [ ( ".." | "..=" ) shift_expr ] .
shift_expr = add_expr { ( "<<" | ">>" ) add_expr } .
add_expr = mul_expr { ( "+" | "-" ) mul_expr } .
mul_expr = unary_expr { ( "*" | "/" | "%" | "div" ) unary_expr } .
| Operator | Operation |
|---|---|
+ - * / | Arithmetic |
% | Modulo |
div | Floor division |
== != < > <= >= | Comparison |
&& || | Logical (short-circuit) |
& | ^ ~ | Bitwise |
<< >> | Shift |
.. ..= | Range |
?? | Coalesce (None/Err → default) |
Operator Type Constraints
Binary operators require operands of matching types. No implicit conversions.
Arithmetic (+ - * /):
| Left | Right | Result |
|---|---|---|
int | int | int |
float | float | float |
String concatenation (+):
| Left | Right | Result |
|---|---|---|
str | str | str |
Integer-only (% div << >> & | ^):
| Left | Right | Result |
|---|---|---|
int | int | int |
Comparison (< > <= >=):
Operands must be the same type implementing Comparable. Returns bool.
Equality (== !=):
Operands must be the same type implementing Eq. Returns bool.
Mixed-type operations are compile errors:
1 + 2.0 // error: mismatched types int and float
float(1) + 2.0 // OK: 3.0
1 + int(2.0) // OK: 3
Numeric Behavior
Integer overflow: Wraps using two’s complement. No panic.
let max: int = 9223372036854775807
max + 1 // -9223372036854775808 (wraps)
Integer division by zero: Panics.
5 / 0 // panic: division by zero
5 % 0 // panic: modulo by zero
Float division by zero: Returns infinity or NaN per IEEE 754.
1.0 / 0.0 // Inf
-1.0 / 0.0 // -Inf
0.0 / 0.0 // NaN
Float NaN propagation: Any operation involving NaN produces NaN.
NaN + 1.0 // NaN
NaN == NaN // false (IEEE 754)
NaN != NaN // true
Float comparison: Exact bit comparison. No epsilon tolerance.
0.1 + 0.2 == 0.3 // false (floating-point representation)
## With Expression
```ebnf
with_expr = "with" identifier "=" expression "in" expression .
with Http = MockHttp { ... } in fetch("/data")
Let Binding
let_expr = "let" [ "mut" ] pattern [ ":" type ] "=" expression .
let x = 5
let mut counter = 0
let { x, y } = point
Conditional
if_expr = "if" expression "then" expression
{ "else" "if" expression "then" expression }
[ "else" expression ] .
if x > 0 then "positive" else "non-positive"
Condition must be bool. When else is present, branches must have compatible types.
When else is omitted, the expression has type void. The then branch must also have type void (or type Never, which is compatible with any type).
// Valid: then-branch is void
if debug then print(msg: "debug mode")
// Valid: then-branch is Never (panic returns Never)
if !valid then panic(msg: "invalid state")
// Invalid: then-branch has non-void type without else
if x > 0 then "positive" // error: non-void then-branch requires else
For Expression
for_expr = "for" identifier "in" expression [ "if" expression ] ( "do" | "yield" ) expression .
for item in items do print(item)
for n in numbers if n > 0 yield n * n
do returns void; yield collects results.
Loop Expression
loop_expr = "loop" "(" expression ")" .
loop(
match(ch.receive(),
Some(v) -> process(v),
None -> break,
),
)
break exits; continue skips to next iteration.
Lambda
lambda = simple_lambda | typed_lambda .
simple_lambda = lambda_params "->" expression .
typed_lambda = "(" [ typed_param { "," typed_param } ] ")" "->" type "=" expression .
lambda_params = identifier | "(" [ identifier { "," identifier } ] ")" .
x -> x * 2
(x, y) -> x + y
(x: int) -> int = x * 2
Evaluation
Expressions are evaluated left-to-right. This order is guaranteed and observable.
Operand Evaluation
Binary operators evaluate the left operand before the right:
left() + right() // left() called first, then right()
Argument Evaluation
Function arguments are evaluated left-to-right as written, before the call:
foo(a: first(), b: second(), c: third())
// Order: first(), second(), third(), then foo()
Named arguments evaluate in written order, not parameter order:
foo(c: third(), a: first(), b: second())
// Order: third(), first(), second(), then foo()
Compound Expressions
Postfix operations evaluate left-to-right:
list[index()].method(arg())
// Order: list, index(), method lookup, arg(), method call
List and Map Literals
Elements evaluate left-to-right:
[first(), second(), third()]
{"a": first(), "b": second()}
Assignment
The right side evaluates before assignment:
x = compute() // compute() evaluated, then assigned to x
Short-Circuit Evaluation
Logical and coalesce operators may skip the right operand:
| Operator | Skips right when |
|---|---|
&& | Left is false |
|| | Left is true |
?? | Left is Some/Ok |
false && expensive() // expensive() not called
true \|\| expensive() // expensive() not called
Some(x) ?? expensive() // expensive() not called
Conditional Branches
Only the taken branch is evaluated:
if condition then
only_if_true()
else
only_if_false()
See Control Flow for details on conditionals and loops.