Declarations
Members & Accessors
Every field, method, constructor, operator, and accessor is one idea: a typed slot bound to a label on a class. "Method" just means the slot's type happens to be callable — everything below is variation on that one theme, plus the rules for how a slot's write view can be constrained.
A class body is a set of labeled, typed slots. A field is a slot holding
a value. A method is a slot holding something callable, selected by a
name or — for operators — a symbol. A constructor is a specially-marked
callable slot selected at construction. An accessor (get/set)
is a pair of views over a slot rather than a slot itself. None of these
are separate mechanisms bolted together; they share one lookup, one
overload-resolution rule, and one argument-binding pass.
Methods & functions
A body is exactly one statement — a block
({ ... }) counts as one statement, so does a bare
expression-return with =>, so does any other single
statement. There's no separate "expression-bodied member" feature; it
falls out of the one-statement rule.
int f(int a, int b) { return a + b; } // block body
int f(int a, int b) => a + b; // arrow body (=> IS return)
void g() console.writeln("x"); // any single statement is a body
void h(); // empty body (interface req. / native intrinsic)
A method has this bound (the instance
side); a function does not (namespace scope, or a
class's static side). Overloading is resolved by argument types
everywhere — methods, functions, and constructors alike. A method may
declare its own type parameters, independent of the enclosing class's:
U remap<U>(U v) => v; // method-level type parameter, unrelated to the class's ownNamed arguments and default parameter values (see Named arguments & defaults below) apply uniformly to methods and functions — there's no separate rule for one or the other.
Constructors
new marks a constructor. There is no new at
the call site — construction reads like an ordinary call. The name
after new is only a selection label: it
need not match the class name, and a class may declare several
constructors distinguished by label and parameter types, resolved by
the same overload rule as any other member.
new ClassName() { ... } // 'new' marks a constructor
new AnyLabel(string s) { ... } // the name is only a selection label
new Configured(int port = 80) { ... } // constant default parameter value
Inside a constructor, Base::Ctor(args) runs a base
constructor against this — the derived class chooses when
and with what arguments, so it controls ordering relative to its own
field initialization:
class Widget : Base {
readonly int id;
new Widget(int id) {
Base::Ctor(); // base construction first, derived class controls the order
this.id = id; // then satisfy this class's own readonly field
}
}Constructor arguments may be named and constructor parameters may declare defaults, exactly as for methods — see Named arguments & defaults.
Operators as methods
An operator is a member whose selector is a symbol —
not a separate language feature. There's no operator
keyword and no privileged operator set: (+),
(-), (==) are ordinary members whose
selector happens to be symbolic. The parentheses are delimiters
marking a symbolic selector, not part of the operation itself — the
same disambiguation that separates (+) the selector from
infix + the use site. An object operator returns
its own type, the value that lands on the left-hand
side of the enclosing assignment; the comparison-vs-arithmetic split
falls out of the return type (bool vs the type) with no
special-casing.
| Selector | Use site |
|---|---|
| a name | a.name |
(+), (==), … | infix a + b |
([]) | indexing a[i] — see Accessors |
(()) planned | call a(...) |
class MyClass3 : MyClass, MyClass2 {
MyClass3 (+)(int val) => myInt + val;
MyClass3 (-)(int val) => myInt - val;
MyClass3 (*)(int val) => myInt * val;
MyClass3 (/)(int val) => myInt / val;
bool (==)(int val) => myInt == val;
}
(==) must return bool. (!=)
derives automatically as !(==) — defining
== gives you the pair for free. Both may still be
overridden individually, but overriding both forfeits the guarantee
that they agree with each other.
Overloading the known operator set (+,
==, [], …) is cheap and broadly useful.
Defining wholly new operator symbols is reserved for narrow,
already-legible domains — data/relational notation the reader
already knows (in, joins, set operations) — not for
shaping an operation to read like one English sentence.
Accessors: get / set
get and set declare views over a slot rather
than slots themselves. A parameterless accessor is a
view over a backing slot of the same name — declare a get
and/or set with no backing field and it's simply
discarded. A parameterized accessor is
computed and needs no backing slot at all — the
indexer being the canonical example, using the ([])
selector from the table above.
get value() => value; // read view over the 'value' slot
set value(int v) value = v * 2; // write view
get ([])(int i) => cells[i]; // indexer (computed accessor)
set ([])(int i, int v) cells[i] = v; // value parameter last
Inside an accessor body, the owning slot's own name is
raw access — writing value inside
get value() reads the backing slot directly, it does not
recurse into the accessor. Declaring only get makes a
member read-only; only set makes it write-only. A
set accessor may not be declared over a
const or
readonly field — there would be
nothing left for it to legally write.
Named arguments & defaults
A call is resolved as one set of argument-to-parameter bindings, applied uniformly to methods, functions, and constructors:
- Positional arguments bind from the first parameter onward.
- Named arguments bind parameters by name; an unknown name or a second binding to the same parameter is an error.
- Each omitted parameter is filled by its declared
= constantdefault, otherwise by a matching lexicalbind— a default takes precedence over ambient injection. - Supplied arguments are checked against their mapped parameter types. Most-specific wins; on an equal type score the candidate using fewer defaults/injections wins; an exact tie is first-declared.
void listen(int port = 80, string host = "localhost") { ... }
listen(); // port=80, host="localhost"
listen(host: "0.0.0.0"); // named argument, port keeps its default
listen(443, host: "example.com"); // positional + named together
Defaults must be compile-time constants and may not refer to another
parameter or to this; defaults on type-variable-typed
parameters aren't supported in v1. The checker rewrites every
successful call into a full positional argument list, so named and
defaulted calls carry no special runtime calling convention — by the
time it runs, it's an ordinary positional call.
Mutation control: const / readonly / weak
Mutation control is one general rule expressed as three orthogonal axes, not three special cases bolted on separately:
| Axis | Question | Mechanism |
|---|---|---|
| slot | When may the binding be written? | const / readonly |
| value | Does the value alias or copy? | struct / pure Array/Map — see Structs & Enums |
| view | Which access views are exposed? | get-only accessors |
The slot axis alone has two points, distinguished by
when the fixed value becomes known: const fixes
it at compile time (for a field) or at
declaration (for a local/global/parameter, where the
initializer may be any runtime expression); readonly
fixes it at construction time, and only exists for
instance fields, because "construction time" is a lifecycle only
instance fields have. Neither is a type: neither ever appears in a
type position, and neither affects assignability, overload
resolution, or generics. There is deliberately no fourth,
type-qualifier axis.
const
const scopes a slot's write view to its
initialization window; once that window closes, only
the read view remains. It applies to locals, fields, namespace/
top-level globals, parameters, and for-in bindings — each
with its own window:
const int maxRedirs = 50; // local: fixed at declaration
const var limit = 100; // composes with inference
const Array<string> args = std::sysArgs(); // namespace global: fixed at startup
class Session {
public const string id = "s-1"; // field: a named compile-time constant
const int SSL = 0x0800; // field: literals/operators over consts also fold
}
void handle(const Options o) { ... } // parameter
for (const string a in args) { ... } // per-iteration binding
A const field is stricter than a
const local: it must have an initializer, and that
initializer must be a compile-time constant — a
literal, None, an array of those, a reference to another
const/comptime value, or an arithmetic/
bitwise operator over constant operands. No constructor may ever
assign a const field; a value that's only known at
construction time belongs to readonly
instead.
const is also not transitive — it fixes
the binding, not the referent's contents:
const MyClass m = MyClass(); fixes m itself,
but m.field = 5; is still legal. Deep immutability is the
value axis's job (a struct, a pure
Array/Map) or the view axis's
(a get-only accessor).
readonly
readonly is const's
construction-time-fixed counterpart, and exists only for
instance fields. Its write view is either the
field's own initializer, or any of the declaring class's own
constructors — and it must be written exactly once.
class AuthController : Controller {
private readonly IUserService userService; // field, constructor form
new AuthController(IUserService userService) this.userService = userService;
}
class Session {
readonly string id = generateId(); // field, initializer form (runtime-computed OK)
}
Initializer form (readonly T x = v;) —
the initializer is the one write; no constructor may also assign
x. Constructor form
(readonly T x;, no initializer) — every constructor the
class declares must assign x exactly once, in a
definite-assignment style. A class with zero constructors and no
initializer is a compile error, since nothing could ever assign it.
Definite assignment only recognizes top-level
statements — direct children of the constructor body. An
exhaustive if (c) { x = a; } else { x = b; } is
rejected in v1 (sound, not yet complete).
Outside its write window — a non-constructor method, another class,
after construction, or a derived class reaching a base's
readonly field directly instead of through
Base::Ctor(...) — a write is an error. Like
const: not a type, not transitive, and a
set accessor may not be declared over it. A field may
not be marked both const and readonly.
weak fields
weak is an instance-field slot property
— never a value qualifier. Its declared type must be T?,
where T is a reference class or
interface (not a struct, string, array,
map, Block, or closure). A store accepts T,
T?, or None and does not retain the
referent; a read returns a fresh T? —
None once the referent's last strong reference has been
released, otherwise an ordinary owned value.
class Component {
weak IComponent? parent = None;
weak readonly IComponent? fixedParent = None;
}
Copy a weak read to a local before narrowing it — a weak field path is
intentionally non-narrowable, because every read
performs a fresh liveness check. Weakness belongs to the slot, never
the value: a live weak read, parameter, return value, or captured
read is strong in the ordinary way. weak readonly and
distinct weak are legal; weak const is not.
A weak field copied through spawn,
Channel, or std::sysThreadTransfer becomes
None on the destination side — the referent itself was
never copied.
Engine coverage: tree-walk, IR, emit-C++, and LLVM
engines — there is no
frozen-ELF lane for weak yet.