Skip to content

Expressions

From tightest to loosest binding. All binary operators are left-associative except ?? (right) and the ternary ? :.

LevelOperators
primaryliterals · names · (…) · f(…) · .member · [index] · T { … } · new T { … } · .case(…)
postfixexpr? (try-unwrap) · ?.member (null-conditional) · with { … }
unary! not · unary + - ~ · & (address-of) · * (deref / by-ref)
multiplicative* / %
additive+ -
shift<< >> >>>
range..
comparison< <= > >=
equality== !=
bitwise and&
bitwise xor^
bitwise or|
logical and&& and
logical or|| or
null-coalescing??
ternary? :

Assignment and compound assignment are statements, not expressions, so an assignment never appears inside a larger expression.

Expr = Coalesce [ "?" Expr ":" Expr ] . // ternary
Coalesce = OrExpr { "??" OrExpr } .
OrExpr = AndExpr { ( "||" | "or" ) AndExpr } .
AndExpr = BitwiseOr { ( "&&" | "and" ) BitwiseOr } .
BitwiseOr = BitwiseXor { "|" BitwiseXor } .
BitwiseXor = BitwiseAnd { "^" BitwiseAnd } .
BitwiseAnd = Equality { "&" Equality } .
Equality = Comparison { ( "==" | "!=" ) Comparison } .
Comparison = Range { ( "<" | "<=" | ">" | ">=" ) Range } .
Range = Shift [ ".." Shift ] .
Shift = Additive { ( "<<" | ">>" | ">>>" ) Additive } .
Additive = Multiplicative { ( "+" | "-" ) Multiplicative } .
Multiplicative = Unary { ( "*" | "/" | "%" ) Unary } .
Unary = ( "!" | "not" | "+" | "-" | "~" | "&" | "*" ) Unary | Postfix .
Postfix = Primary { Selector } .
Selector = "." identifier // member access
| "(" [ ArgList ] ")" // call
| "<" TypeList ">" "(" [ ArgList ] ")" // generic call
| "[" Expr "]" // index
| "?" // try-unwrap
| "?." identifier // null-conditional
| "with" "{" FieldInitList "}" . // non-destructive update
Primary = literal | identifier
| "(" TupleArg { "," TupleArg } ")" // parenthesised / tuple, elements optionally labeled
| CompositeLit | NewExpr | ListLit
| DotCase | Lambda | MatchExpr
| "ok" "(" Expr ")" | "error" "(" Expr ")"
| "await" Unary | "spawn" Block
| Region . // see /spec/comptime/
TupleArg = [ identifier ":" ] Expr . // (q: a / b, r: a % b) , (count: 2, "hi")
CompositeLit = TypeName [ TypeArgs ] "{" [ FieldInitList ] "}" .
FieldInitList = FieldInit { ( "," | newline ) FieldInit } .
FieldInit = identifier ":" Expr .
NewExpr = "new" TypeName ( "{" [ FieldInitList ] "}" | "(" [ ArgList ] ")" ) .
ListLit = "[" [ Expr { "," Expr } ] "]" .
DotCase = "." identifier [ "(" [ ArgList ] ")" ] .
Lambda = "func" "(" [ ParamList ] ")" [ ReturnType ] ( Block | "=" Expr )
| "(" [ identifier { "," identifier } ] ")" "=>" Expr .
ArgList = Arg { "," Arg } .
Arg = [ "out" | "&" | "*" ] Expr .

A delimited region in value position — let page = @doc { … }, where doc is a sigil a referenced pack claims — is a primary expression. It shall yield exactly one expression (ES2944): there is no rule that would say which of several was meant. The region is selected by its sigil and never by the type it flows into, because no type information exists when the region is scanned.

Composite literal T { f: v } constructs a value of type T; field initializers are comma- or newline-separated, and field order is free. It builds a value struct in place (no heap allocation for the struct form), or a class directly — a composite literal over a class runs its primary constructor, and over a headered class (one with a capture header) it is rejected as ES2190. A composite literal that omits a required field is ES2189. new T { … } / new T(…) heap-allocates a value struct and yields a *T — the only allocation expression (Pointers → new vs &); new on a non-struct is ES2144, on a class is ES2003.

.case / ok / error construct a union variant or a Result side when the target type is known from context — a typed binding, parameter, return, or the operand of ok / error (Errors).

Collection literal [a, b] constructs a List<T> with T inferred from the elements; [] is List<object>. Elements are arbitrary expressions ([new Box { n: 10 }, new Box { n: 20 }]), and the result is indexed and mutated like any List<T>. A parenthesised comma list (a, b) constructs a System.ValueTuple, destructured by let (a, b) = … (Statements).

typeof(T) yields the System.Type for a type named in source.

let key = typeof(int) // System.Int32
let handlers = Dictionary<Type, string>()
handlers[typeof(string)] = "text"

The operand is a type, not an expression, and it is required — unlike default, which is target-typed when written bare, there is no expected type at a use site that could supply the operand of a typeof. It may name anything the type grammar accepts, including a type declared in this compilation and a closed generic (typeof(List<int>)).

The result is the runtime’s own interned Type instance, so two typeof of the same type are reference- equal — which is what makes one usable as a dictionary key. It emits as ldtoken followed by Type.GetTypeFromHandle, the CLR’s only way to name a type as a value.

typeof is what every type-keyed registry is built on: a dispatch table from Type to handler, a service key, a discovery walk over attributed types. Without it those shapes have no spelling at all, and the workaround is never local — it changes the design of the thing being registered.

expr with { f: v } yields a fresh value that copies expr and overrides the named fields, leaving expr unchanged — the value-semantic update operator. It works on a value struct (including a readonly struct, whose fields are otherwise immutable) and overrides an embedded field by its type name as well as an ordinary field:

readonly struct Transform { Vec2 scale: int } // Vec2 embedded
let t = Transform { x: 1, y: 2, scale: 3 }
let u = t with { Vec2: Vec2 { x: 10, y: 20 } } // fresh value; t is untouched
// u.x == 10, t.x == 1

It also works on a class carrying @derive(record), whose <Clone>$() supplies the copy. A class without it is ES2245: there would be nothing to copy, and updating the reference in place would mutate the very value being updated from — the opposite of what the operator means. The clone is virtual, so with on a base-typed reference to a derived record yields the derived type.

Member access e.m reads a field, a property, or a method (receiver or in-body); through a *T it auto-dereferences. Index e[i] indexes a list, array, dictionary, or any indexer. The index-from-end operator ^k counts from the end, so xs[^1] is the last element (a System.Index).

A range a..b is half-open — it covers a up to but excluding b. for i in 0..n iterates 0, 1, …, n-1 (so 0..5 yields five values, summing to 10), and an empty range (0..0) yields nothing.

In index position a range slices, and either endpoint may be omitted — xs[..k] (start elided → Index.Start), xs[k..] (end elided → Index.End), and xs[..] (both) — composing with ^k (data[1..^1]). Indexing an array, string, List<T>, or Span<T>/ReadOnlySpan<T> with a range yields a slice via the CLR this[Range] / Slice contract (no boxing, the same lowering as ^); a span, which has no this[Range] indexer, lowers the range to an absolute Slice(start, length) against its own Length. The open form is a slice-index feature, not a loop-range one — for i in ..n stays ill-formed (a counted loop needs a concrete start).

Try-unwrap expr? unwraps a Result: it yields the ok value, or returns the error from the enclosing function. It is an expression operator, valid anywhere an expression appears (Errors → the ? operator). expr?.member parses as the null-conditional operator, not unwrap-then-access; to unwrap then access, parenthesise ((expr?).member) or bind with a let.

Ternary vs. try-unwrap. A ? is disambiguated by lookahead: if the next token can begin an expression, ? is the ternary operator (c ? a : b); otherwise it is postfix try-unwrap.

Type arguments vs. comparison. A < after a name opens a type-argument list when the balanced group parses as one AND is followed by (, {, ., or [] — the four positions a generic name can occupy: Box<int>(1), Box<int> { v: 1 }, Comparer<int>.Default, Box<int>[](2). Anywhere else it is the comparison operator. a < b || c > (d) fails the type-argument parse and stays relational; a < b > (c) does not, and reads as a generic name, exactly as it does in C#. Whether that name then denotes a generic TYPE to construct or a generic FUNCTION to call is name resolution, not grammar: the compiler asks its symbol table, and a type wins when the name is one. Casing is not consulted.

Null-coalescing a ?? b yields a if non-null, else b (right-associative, so a ?? b ?? c groups as a ?? (b ?? c)). Null-conditional a?.m yields a.m if a is non-null, else null/default, and chains (a?.b?.c). Both short-circuit their right side.

Address-of &x yields a managed pointer to a variable, or a function pointer for a function name; *x / &x at a call site pass by reference (Pointers & by-ref). The unary word operators not / and / or are synonyms for ! / && / ||.

A class or struct may define an operator only through its companion static facet. The function name is one of the fixed operator tokens and canonical source places ( immediately after it:

struct Vec2 { x: double, y: double }
static Vec2 {
pub func +(left: Vec2, right: Vec2) -> Vec2 {
return Vec2 { x: left.x + right.x, y: left.y + right.y }
}
}

Resolution checks built-in primitive, string, decimal, and flag-set enum behavior first, then exact source operators on either operand owner, then public matching CLR op_* methods. A flag set has to be answered by the first tier: an enum is not a primitive, a static facet attaches to a class or struct rather than an enum, and the CLR emits no op_BitwiseOr on an enum — C# special-cases enum bitwise operators in its own compiler, so nothing reaches metadata for the third tier to find. It performs no overload ranking or implicit nonliteral numeric conversion. A raw numeric literal may bind contextually to a candidate parameter; multiple exact contextual matches are ambiguous, and T(value) resolves the choice explicitly. Operands evaluate left-to-right exactly once.

The overloadable set is unary + - ! ~; binary + - * / % & | ^ << >> >>>; and comparisons == != < > <= >=. Precedence is fixed by the table above and cannot be declared. &&, ||, assignments, ranges, casts, ++, and -- are not overloadable. Compound assignment derives from the corresponding binary operator and evaluates a member/index target exactly once.

Three operators go from a base, interface, or object to a concrete type; none narrows implicitly. They sit at relational precedence (tighter than &&/==, so s is T && s.m reads as (s is T) && s.m), and their target type is never nullable (as T already accounts for absence by yielding T?).

  • x is T / x is not T — a boolean type test (isinst). As a guard it smart-casts: inside the region the test holds, x has type T with no rebind — through &&, an if branch, and the guard-return idiom if x is not T { return }. Narrowing applies to stable bindings only: a let local, a parameter, or a let-field path with no intervening call. A var does not smart-cast, and relying on a narrow the compiler cannot prove stable is ES2173, never a silently-unsound cast. Over an open type, is not T does not narrow positively (the complement of one type is not a type); over a closed ref union hierarchy it does.
  • x as T — the safe cast: isinst, yielding the value or nil (T?). The idiomatic downcast, composing with ?? / ?. / match nil and the rest of the nullable machinery with no new control flow.
  • x as! T — the asserting cast: castclass (reference) / unbox.any (value), which throws InvalidCastException on a miss. The loud boundary form, for where you know the type.
if sym is MethodSym { return sym.declaredArity } // smart-cast — sym : MethodSym here
let t = sym as TypeSym // t : TypeSym?
return t?.arity ?? 0 // composes with ?. and ??
let n = boxed as! int // unbox.any; throws on miss

Because E# generics are reified, these tests are honest at full generic fidelity: x is List<int> distinguishes List<int> from List<string> at runtime — a real closed-generic test, not the erased-to-raw lie of the JVM. A match type pattern (name: T) is the multi-way form of is T + bind (Pattern matching).

func(…) -> T { … } is the explicit-signature function literal; (x) => expr and (x) => { … } are arrow literals with an inferred result. An arrow parameter may be annotated, otherwise it is target-typed when a callable destination supplies its shape. Both forms close over the enclosing scope; captures are mutable and shared, and a closure over a let may read but not assign it. The complete grammar, conversion, and representation rules are in Function values.

match appears in any expression position; every arm produces a value of one common type, which becomes the type of the match expression. Its grammar and full semantics — scrutinee typing, payload binding, exhaustiveness — are in Pattern matching.