Foundations
Expressions & Operators
How expressions combine: operator precedence, the primary forms an
expression bottoms out to, how calls and construction bind their
arguments, how . and :: reach a member, and
how method dispatch and operator overloading resolve at runtime.
Precedence
Loosest to tightest. Assignment and the ternary/range level are
right-associative; everything else below them is left-associative.
Postfix forms (.name, ::name,
(args), [index]) bind tightest of all.
| Level | Operators | Associativity |
|---|---|---|
| 1 (loosest) | = += -= *= /= %= | right |
| 2 | ?: (ternary), .. (range) | right |
| 3 | || | left |
| 4 | && | left |
| 5 | | & ^ | left |
| 6 | == != | left |
| 7 | < > <= >= | left |
| 8 | << >> | left |
| 9 | + - | left |
| 10 (tightest infix) | * / % | left |
| prefix | ! - ~ | — |
| postfix | .name ::name (args) [index] | left |
Primary expressions
Every expression bottoms out to one of these forms:
42 3.14 "s" true false // literals
name // variable / member / function / type
this // enclosing instance (or primitive receiver value)
(expr) // grouping
[e1, e2, ...] // array literal (ranges spread)
(p1, p2) => expr // lambda (params untyped; captures by closure)
await expr // suspension point — parks the current task
inject Type // explicit injection selector
a .. b // range value
Keywords such as get, set, is,
and in are reserved only at statement or declaration
start — they may still be used as member
names after ./::, and as method names
following a return type.
Match expressions
match (subject) { pattern => body; ... } — a pattern is
a type (Type =>, narrows the subject
in that arm), a value or range (0 =>,
1..9 =>), or else. First match wins.
Bodies are => expr; or => { block }.
match is an expression when its arms
yield a value, or a statement when written with no
trailing ;. It's exhaustive over a closed union with no
else required; an open hierarchy requires one. Type
patterns lower to the same is/IsType test
used by catch and is — one type-dispatch
path everywhere.
string describe(Shape s) => match (s) {
Circle c => "circle r=${c.radius}";
Square sq => "square side=${sq.side}";
else => "unknown shape";
};
match (code) { // statement form — no trailing `;`
0 => console.writeln("ok");
1..9 => console.writeln("client range");
else => console.writeln("other");
}match is a reserved word.
Lambdas
A lambda takes either an expression body or a full statement block. Parameters are untyped and inferred from context; a lambda captures its enclosing scope by closure.
var inc = (x) => x + 1; // expression body
var process = (x) => { // statement block body
var y = x * 2;
return y;
};String interpolation
"...${expr}..." (either quote style) desugars in the
parser to concatenation with .toString():
string msg = "code=${resp.status}!";
// exactly equivalent to:
string msg = "code=" + (resp.status).toString() + "!";
\${ escapes a literal ${; a bare
$ with no { stays literal — there is no
$name shorthand (that syntax belongs to quasiquote
holes and is untouched here). An empty hole (${}) or an
unterminated one is a compile error. A hole may contain any
expression, including one with a nested string literal,
as long as it uses the opposite quote style from the
outer literal — the lexer has no concept of interpolation, so a
nested string using the same quote character ends the outer
token right there, exactly as it would in any ordinary string:
"${'has a } in it'}" // OK — inner literal uses the opposite quote
A type without toString() fails an interpolation hole
the same way a hand-written .toString() call on it
would — see Strictness.
Calls & construction
f(args) // function call; overload chosen by argument types
f(x, label: value) // positional arguments first; named arguments may reorder
obj.method(args) // method call; overload chosen by argument types
Type(args) // construction — NO `new` at the call site
Type::Label(args) // constructor selected by label
NS::Type(args) // construction of a namespaced class, reached by qualification
NS::Type::Label(args) // ...combined with label selection
Base::Ctor(args) // inside a constructor: base constructor applied to `this`
Construction never writes new at the call site — a
class type used as a callable is construction. Constructor
candidates share a label; the overload is chosen by argument types
(most-specific wins, first-declared breaks ties). Generic type
arguments are inferred from the constructor arguments, then from the
target type.
class Point { Point(int x, int y) => ...; }
Point p1 = Point(3, 4); // positional
Point p2 = Point(x: 3, y: 4); // named — order may differ
Point p3 = Point(3, y: 4); // positional first, then named
class User {
User::FromName(string name) => ...;
User::FromId(int id) => ...;
}
User u = User::FromName("Ada"); // constructor selected by label
At every call site, positional arguments must precede named ones. A
named argument binds the parameter carrying that name and may appear
in any order among the named suffix. Functions, methods,
constructors, and attributes all use this same spelling and binding
rule. A ::-reached callable without a
following (...) is not a call at all — it's a
method reference, a first-class
function value.
Member access & qualification
. navigates an instance. ::
navigates the base / static / non-instantiated side:
base classes, constructor labels, namespaces, and class static sides.
Inside a generic callable, its left operand may also be a
callable-level type parameter, resolved per concrete instantiation.
One operator, one meaning: "the non-instantiated version."
this.Counter::value // a base class member, reached through `this`
Type::Label // a constructor label / static side
NS::name // a namespace-qualified nameA bare read that cannot be resolved by type — for example a
distinct-collided member with no qualifier — is a
compile error rather than a best-effort guess.
Method references
A ::-reached callable member in value
position (not immediately called) is a first-class function
value — the same way a ::-reached field read yields the
field's value; a member is a typed slot, and some slot types happen
to be executable.
var f = NS::fn; // namespace function: its signature directly
b.handler = Controller::Login; // instance method: UNBOUND — the receiver becomes
// the FIRST parameter: (Controller, Req) => Resp
var g = User::FromName; // labeled constructor: (string) => User
Route('/login', AuthController::Login) // as a call/constructor argument- The reference's type spells as an ordinary function
type (
(A, B) => R) — assignable to fields/params/locals of that type and storable in containers likeArray<(A,B)=>R>. - Resolution is by target function type. An
overloaded name resolves against the declared type of the slot being
assigned or initialized, or the chosen overload's parameter type in
argument position — deferred like a lambda argument, so it does not
help pick the outer overload. An overloaded name with
no target in context (
var h = C::overloaded;) is a compile error — annotate the target type. A missing member is a compile error at the reference site. - Dispatch follows the ordinary receiver-dispatch
rule, because the reference is the eta-expansion lambda
(C c, ...) => c.m(...): both an interface reference (IAnimal::speak) and a concrete-class reference (Animal::speak) dispatch on the runtime object — see Method dispatch. - v1 limits: a reference to a generic
callable (
M::identitywhereR identity<R>(R x)) is a compile error — its type parameters are unbound in value position. Each evaluation of a reference yields a fresh function value (like two identical hand-written lambdas), so identity comparison is not a "same handler" test — compare by key, or store the value once.
Bound method references
A .-reached method in value position captures its
receiver and removes that receiver from the function type:
Editor editor = Editor();
var save = editor.save; // equivalent to () => editor.save()
menu.onKey(this.onKeyDown); // `this` may be captured directly- The receiver must be a bare local, parameter, or
this. An embedded expression such asa.b.method,this.field.method, ormake().methodis a compile error with a "bind the receiver to a local first" fix — capture is a one-time snapshot, never a silent re-evaluation of an expression on every call. - The captured value is the object reference: later mutation of that object is visible, but rebinding the original local does not retarget the closure.
- Overloads use the same target-function-type resolution as unbound references, including argument and typed-container contexts. A closure-valued field wins ordinary field-read resolution over a same-named method.
- Generic methods remain unsupported in value position. Each evaluation is fresh, and runtime override dispatch is exactly the dispatch of the eta-expansion lambda.
Method dispatch
An unqualified instance-method call dispatches on the receiver's runtime class — uniformly for interface-typed and class-typed receivers. A subclass overriding a method runs its override through any base-typed binding, whether that binding is a field, a parameter, a local, or a method reference:
class Animal { string speak() => "..."; }
class Dog : Animal { string speak() => "Woof"; }
string callSpeak(Animal a) => a.speak();
callSpeak(Dog()); // "Woof" — the override runs, not the statically-named Animal::speak
The compiler devirtualizes to a direct call whenever
the candidate set is provably closed — no class anywhere in the
whole-program gather overrides the resolved method below the
receiver's static type. This is a whole-program optimization that
can only change how fast the call runs, never what
runs. Qualified access (this.Base::m()), operators,
constructor selection, and static/namespace functions remain
statically resolved and are unaffected.
Runtime dispatch is by name + arity — no type
disambiguation. An overridden method that shares its
(name, arity) with another overload on the same
receiver static type cannot be picked correctly at runtime and is
a compile error at the call site: give the
overloads distinct arities/names, or qualify the call explicitly.
class Animal { string speak(string s) => "a-str"; string speak(int n) => "a-int"; }
class Dog : Animal { string speak(int n) => "d-int"; } // overrides speak(int); ambiguous with speak(string)
Animal a = Dog();
a.speak(5); // compile error: shares its arity with another overloadDifferent-arity overridden overload sets — the common case — stay legal and dispatch correctly; only same-arity siblings are rejected. Signature-aware runtime dispatch (carrying the resolved parameter types into the by-name lookup) is roadmapped, and would benefit interface dispatch identically, which has carried this same name+arity limitation for longer.
Operators on objects
a op b resolves (op) on a's
class by the type of b — overloads are supported.
(==) must return bool; (!=)
derives automatically as !(==). Defining an operator
means declaring a member with a symbolic selector, the same shape as
an ordinary method:
class Vec2 {
int x; int y;
Vec2(int x, int y) => ...;
Vec2 (+)(Vec2 other) => Vec2(x + other.x, y + other.y); // operator: symbolic selector
bool (==)(Vec2 other) => x == other.x && y == other.y;
}
Vec2 sum = Vec2(1, 2) + Vec2(3, 4); // dispatches (+) on Vec2
bool eq = Vec2(1, 2) == Vec2(1, 2); // true; (!=) auto-derives to !(==)Integer operators
<< (shift left), >> (shift
right, arithmetic — sign-extending, since
int is signed 64-bit), ^ (xor), and prefix
~ (complement) are defined only on
int — no shifts on float, no
^/~ on bool (use
!=/!) or string. A shift count
outside 0..63 throws RuntimeException
("shift count out of range") rather than silently masking to 6 bits
(the x86 default) or invoking C++'s undefined behavior.
int x = 1 << 4; // 16
int y = 256 >> 4; // 16
int z = 5 ^ 3; // 6
int w = ~0; // -1
Resolution stays by type: on an object left operand,
<</>> still dispatch the
(<<)/(>>) operator method (the
stream transfer operators) — the same rule that lets +
be both int-add and a user-defined (+).
Indexing
a[i] dispatches to the ([]) get accessor
of a's class; a[i] = v dispatches to the
([]) set accessor. On a mutable object
the set accessor mutates in place. On a pure array,
a[i] = v is rebind-sugar: a is rebound to a
new array with slot i replaced.
class Grid {
get ([])(int i) => cells[i]; // indexer (computed accessor)
set ([])(int i, int v) cells[i] = v; // value parameter last
}
Grid g = Grid();
int first = g[0]; // dispatches the ([]) get accessor
g[0] = 9; // dispatches the ([]) set accessorStrictness
- An unknown name or function is a compile error
(
Systemexcepted until modeled;consoleis modeled). - A generic construction whose type argument has no inference source is a compile error — provide a target type or a type-bearing argument (inferred when recoverable, required when not — see Types & Generics).
- Runtime failures throw catchable
RuntimeExceptions: index out of bounds,Map.aton a missing key, unresolvable call targets, missing operators on a class.