Spec: expressions
esque is expression-oriented. Almost everything has a value.
Literals
Integer, float, bool, char, tensor, and string literals are values.
String literals have type string — see
Lexical → String literals for the surface that has
shipped and Planned: strings for the
rest.
Bindings
let introduces an immutable binding. There is no mut form
today; that is part of the
planned linear types.
Two forms exist: a statement form usable inside a block, and an expression form usable anywhere a value is expected.
Statement form
{
let x = e; // immutable
let x: T = e; // immutable, with explicit type
body
}
Expression form
let x = e in body
let x: T = e in body
Right-associative. The body sees x in scope. Multiple bindings
chain by nesting:
fn k() -> i32 =
let a = 10 in
let b = a * 2 in
a + b
The expression form is sugar for { let x = e; body }, so scoping
and shadowing follow the block rules above.
Arithmetic and element-wise operators
Scalar operators work on rank-0 numeric values:
+ - * / %
Element-wise operators work on tensors of equal type and shape:
.+ .- .* ./ .%
Broadcasting between mismatched shapes is (planned); today the two operands of an element-wise op must have equal shapes.
The @ operator is reserved for matrix multiplication; it parses
but does not yet codegen.
Reductions
<op>/(v)
<op>/ reduces a tensor to a scalar by left-folding op:
+/(v) # sum
-/(v) # running difference (left fold)
*/(v) # product
//(v) # running quotient (left fold)
The reduction operator is parametric in the operator: any binary
operator the element type defines can be used. Axis-aware reduction
(+/[axis=0]) is (planned).
Comparisons and logical operators
== != < <= > >= # any equality-supporting T, T -> bool
&& || # bool, bool -> bool
! # bool -> bool
Pipeline
x |> f # = f(x)
x |> f(y, z) # = f(x, y, z)
Left-associative; precedence 1 (the loosest binary op). The piped value always becomes the first argument.
Range expressions
lo..hi # exclusive
lo..=hi # inclusive
A range expression evaluates to a rank-1 i32 tensor of the
expected length. Both bounds must be integer literals today;
dynamic bounds (with const-eval) are (planned).
Tabulate
tabulate(N, |i| f(i))
Evaluates f(0), f(1), …, f(N-1) and packs the results into a
rank-1 tensor of length N. N must be a literal in
1..=1<<20. For N ≤ 32 the body is unrolled; for N > 32 the
implementation emits a counted runtime loop (OpTabulateLoop,
shipped in v0.11).
Scan
scan(init, |a, x| f(a, x), v)
Returns a tensor of the same shape as v whose element i is
f(... f(f(init, v[0]), v[1]) ..., v[i]). N ≤ 1<<20; for
N > 32 the implementation emits OpScanLoop (shipped in v0.11).
Iterate-until
iterate_until(init, |s| step(s), |s| pred(s), max)
Returns the value state would have in a real loop that ran
step until pred(state) was true, capped at max iterations.
max must be a literal ≤ 32 and the state must be scalar today;
unlike the other primitives, the runtime-loop counterpart
(OpIterateUntilLoop, with predicate-driven branching) is still
(planned).
Iterate
iterate(n, init, |s| step(s))
Run step exactly n times starting from init. Equivalent to
iterate_until(init, step, |_| false, n) but cheaper because there
is no predicate to thread. n must be a literal in 1..=1<<20;
for n > 32 the implementation emits OpIterateLoop (shipped in
v0.11).
Each
each(v, f)
Iterate side-effectingly. As of v0.13, f must be a named function
whose effect set fits the enclosing
function's. Pure f is accepted everywhere; @io f requires an
@io caller. Result type: unit.
Conditionals
if cond { e1 } else { e2 }
if cond { e1 } else if cond' { e2 } else { e3 }
The condition has type bool. Both arms must have a common type,
which is the type of the whole if. The else is required when
the if is used for its value.
Match
match e {
pattern1 => e1,
pattern2 if guard => e2,
_ => e3,
}
Arms are tried top-to-bottom; the first match wins. Patterns supported today:
- Integer literals (
0,-1,'A'). - Boolean literals (
true,false). - Identifier (binds the scrutinee).
_(wildcard).
Exhaustiveness checking is (planned).
Blocks
{ stmt; stmt; ...; expr }
A block evaluates each statement in order, then evaluates the final
expression and produces its value. A block whose final form is a
statement (expr;) has type unit.
Removed forms
Earlier drafts of the spec included:
for i in 0..N { body }— replaced bytabulate(N, |i| body), byeach(0..N, f), or by+/(tabulate(N, |i| body))for reductions.while cond { body }— replaced byiterate_until(init, step, |s| !cond(s), max).- Comprehensions
[[ f(i, j) for j in 0..N ] for i in 0..M ]— replaced bytabulate(M, |i| tabulate(N, |j| f(i, j))). mutand+=family — to be reintroduced under linear types.