LEVIATHAN v0.1 · in development

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 own

Named 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.

SelectorUse site
a namea.name
(+), (==), …infix a + b
([])indexing a[i] — see Accessors
(()) plannedcall 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 vs. inventing operators

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:

  1. Positional arguments bind from the first parameter onward.
  2. Named arguments bind parameters by name; an unknown name or a second binding to the same parameter is an error.
  3. Each omitted parameter is filled by its declared = constant default, otherwise by a matching lexical bind — a default takes precedence over ambient injection.
  4. 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:

AxisQuestionMechanism
slotWhen may the binding be written?const / readonly
valueDoes the value alias or copy?struct / pure Array/Map — see Structs & Enums
viewWhich 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.

v1 definite-assignment restriction

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.

Weak fields don't survive a transfer

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.