Composite Types
Declaration
Types are user-defined composite types containing named fields. Use var for mutable fields and let for immutable fields:
type Point var x as int var y as intend 'Point'Type Literals
Create type instances using field initializers:
var p = Point{x: 10, y: 20}var origin = Point{x: 0, y: 0}Required Field Initialization
Every field of a type must be initialized when the type is constructed. A field counts as initialized if any of the following is true:
- The declaration supplies a default value:
var count = 0. Two forms are accepted:- Shorthand —
var name = literal. The type is inferred from the literal, which must be an integer, float,true/false, or an enum case (Priority.low). - Full form —
var name Type = expression. The type annotation is required whenever the default is something other than a literal. The expression can be any valid expression and is re-evaluated at every struct literal that omits the field:var items IntArray = IntArray.create(),var origin Point = Point.create(0, y: 0).
- Shorthand —
- The literal provides the field:
Counter{count: 5}. A value provided here always wins over a declared default (the default expression is not evaluated when the field is provided). - The literal is the direct return expression of a
staticfactory whose return type is the enclosing type, and the field is assigned viaself.field = expron every control-flow path that reaches the literal. In that case the field can be omitted from the literal. Prefer rule 1 (field default) when the value doesn’t depend on factory parameters; reach for rule 3 when the default needs access tocreate’s arguments.
A Self{} literal is only legal when every field has a default or is supplied via rule 3. Otherwise the compiler emits E3086 SemanticFieldNotInitialized listing the uninitialized fields.
type Counter export var value as Integer export var version = 0 // default
export static function create(initial Integer) returns Self self.value = initial // proof of initialization (rule 3) return Self{} // OK: value proven by self-assign; version defaulted end 'create'end 'Counter'The self-assignment form requires definite assignment: the write must reach the return on every control-flow path. A write in only one branch of an if/else, or only inside a loop body (which may execute zero times), is not sufficient and triggers E3086.
Field Access
Access fields using dot notation:
var p = Point{x: 10, y: 20}var xVal = p.x // Read fieldp.x = 15 // Write field (if var, not let)Methods
Methods are defined inside the type body and can access fields directly (implicit self):
type Point var x as int var y as int
function add(other Point) returns Point return Point{x: x + other.x, y: y + other.y} end 'add'
export function magnitude() returns float return sqrt((x * x + y * y) as Real) end 'magnitude'end 'Point'Method Syntax Rules:
- Methods must be declared inside the type body
- Methods access type fields directly without explicit
self - Use
exportkeyword beforefunctionto export individual methods - Methods are called using dot notation:
instance.method(args) - A local declaration inside an instance method (
let/var, parameter, match-pattern binding, tuple destructure, for-in loop variable, try/otherwise error binding) MUST NOT collide with a self-field name. Such a shadow is rejected at parse time with E3006 because reads and writes of the local would silently route throughself.fieldand produce type confusion when the local’s type differs from the field’s type. selfis a reserved identifier and cannot be bound by user code in any declaration (parameter,let/var,for-in variable, function name, type name, etc.). Lexer-strict positions reject it with E2010; positions that accept keyword-shaped names reject it with E2051. This prevents accidental shadowing of the implicit receiver.
Calling Methods
Methods are called using dot notation on instances. The receiver (self) is implicit:
var p1 = Point{x: 10, y: 20}var p2 = Point{x: 5, y: 10}var p3 = p1.add(p2)var mag = p1.magnitude()Sibling Method Calls
Inside a type body, instance methods can call other instance methods on self using a bare name (no explicit receiver):
type Calculator var base as Integer
function double() returns Integer return base * 2 end 'double'
function quadruple() returns Integer return double() * 2 // bare call — implicitly calls self.double() end 'quadruple'end 'Calculator'The compiler detects that double is an instance method of the current type and automatically prepends self as the receiver.
Static Methods
Static methods belong to a type but don’t have access to instance data. They are declared with the static keyword and called using TypeName.method() syntax:
type Point var x as int var y as int
static function origin() returns Point return Point{x: 0, y: 0} end 'origin'
static function create(x int, y int) returns Point return Point{x: x, y: y} end 'create'
function magnitude() returns float return sqrt((x * x + y * y) as Real) end 'magnitude'end 'Point'Calling Static Methods:
var p1 = Point.origin() // Static method callvar p2 = Point.create(10, y: 20) // First positional, second namedvar mag = p2.magnitude() // Instance method callStatic Method Rules:
- Declared with
static functioninside a type body - No implicit
selfparameter - cannot access instance fields - Called on the type name, not on instances:
TypeName.method() - Can be exported with
export static function - Commonly used for factory methods and utility functions
Differences from Instance Methods:
| Feature | Instance Method | Static Method |
|---|---|---|
Has self | Yes (implicit) | No |
| Can access fields | Yes | No |
| Call syntax | instance.method() | Type.method() |
| Declaration | function name() | static function name() |
Static Fields
Static fields are shared across all instances of a type. They are declared using static var (mutable) or static let (immutable):
type Counter static var count = 0 static let MAX_COUNT = 1000
var id as int
static function create() returns Counter Counter.count = Counter.count + 1 return Counter{id: Counter.count} end 'create'end 'Counter'Accessing Static Fields:
var c1 = Counter.create() // Counter.count becomes 1var c2 = Counter.create() // Counter.count becomes 2print(Counter.count) // Prints: 2print(Counter.MAX_COUNT) // Prints: 1000Static Field Rules:
- Declared with
static varorstatic letinside a type body - Must have an initializer value (no uninitialized static fields)
- Accessed using
TypeName.fieldNamesyntax (not instance syntax) static varfields can be reassigned;static letfields are immutable
Initialization behavior depends on the initializer expression:
- Constant initializers (integer, float, bool literals) are evaluated at compile time
- Complex initializers (function calls, struct literals, array literals) are evaluated lazily on first access — see Lazy Static Initializers below
Differences from Instance Fields:
| Feature | Instance Field | Static Field |
|---|---|---|
| Storage | Per instance | One per type |
| Access | instance.field | Type.field |
| Declaration | var field type | static var field = value |
| Requires initializer | No (can use type default) | Yes |
Lazy Static Initializers
Static fields initialized with complex expressions — function calls, struct literals, or array literals — are evaluated lazily. The initializer runs the first time the field is accessed, and the result is cached for all subsequent accesses.
typealias Tally = int(0 to u64.max)
type Config static var instance = Config.create()
static function _create() returns Config return Config{value: 42} end '_create'
export var value as Tally
export static function instance() returns Config return Config.instance // initializer runs on first call only end 'instance'end 'Config'Lazy initialization guarantees:
- The initializer executes exactly once, on the first access to the static field
- Subsequent accesses return the cached value without re-evaluating the initializer
static varfields can be reassigned after initialization; the initializer does not run againstatic letfields are immutable after initialization (planned)- Constant initializers (integer, float, bool literals) remain compile-time constants and are not lazy
Common patterns:
Caching expensive computations:
type WSCache static var ws = CharacterSet.whitespacesAndNewlines()
export static function isWhitespace(c Character) returns bool return WSCache.ws.contains(c) end 'isWhitespace'end 'WSCache'Struct literal defaults:
typealias Coord = int(0 to u64.max)
type Point export var x as Coord export var y as Coordend 'Point'
type Defaults static var origin = Point{x: 0, y: 0}
export static function getOrigin() returns Point return Defaults.origin end 'getOrigin'end 'Defaults'Array literal initialization:
typealias Integer = int(i64.min to i64.max)
type Lookup static var values = [10, 20, 30]
export static function get(index Integer) returns Integer return try Lookup.values.get(index) otherwise -1 end 'get'end 'Lookup'Multiple lazy statics in the same type are each initialized independently on first access:
typealias Tally = int(0 to u64.max)
type Cache static var a = Cache.buildA() static var b = Cache.buildB() export var n as Tally
static function _buildA() returns Cache return Cache{n: 10} end '_buildA'
static function _buildB() returns Cache return Cache{n: 20} end '_buildB'end 'Cache'Interfaces
Interfaces define a set of method signatures that types can implement:
interface Hashable function hash() returns intend 'Hashable'Structs declare conformance using the implements keyword:
type Point implements Hashable var x as int var y as int
function hash() returns int return x + y * 31 end 'hash'end 'Point'Static Interface Methods
Interfaces can declare static methods using the static keyword. Static interface methods don’t receive an implicit self parameter and are typically used for factory methods:
Interface Notes:
Selfin interface method parameters/returns refers to the conforming type- A type can conform to multiple interfaces:
type Foo implements A, B - Methods implementing interface requirements follow the same syntax as regular methods
- Static interface methods use
static function method()syntax in implementations - One interface can extend another with
interface Derived extends Base. A type that listsimplements Derivedmust declare every method fromDerivedand every method transitively inherited fromBase; missing methods inherited viaextendsare reported with a(from BaseName)suffix in the diagnostic
Interface-Typed Parameters
Functions can use interface types directly as parameter types. Any concrete type that implements the interface can be passed as an argument:
interface Drawable function draw() returns Integerend 'Drawable'
function render(item Drawable) returns Integer return item.draw()end 'render'The compiler monomorphizes the function at compile time, creating specialized copies for each concrete type used at call sites. This provides static dispatch with no runtime overhead. If the argument’s type does not implement the required interface, a compile error is reported. Interface inheritance is respected: a type implementing a derived interface also satisfies parameters typed with the base interface.
Interface-Typed Return Values
Functions can declare an interface as their return type. When every return in the body yields the same concrete implementing type, the compiler statically infers that type at the call site so chained method dispatch on the result resolves without runtime overhead:
interface Producer function produce() returns Integerend 'Producer'
type Widget implements Producer let value as Integer function produce() returns Integer return value end 'produce'end 'Widget'
function makeProducer() returns Producer return Widget{value: 42}end 'makeProducer'Interface-Typed Fields
Struct fields can declare an interface as their type. The field stores any value of a type that conforms to the interface, and methods invoked on the field dispatch to the implementing type. When the compiler can trace the concrete type stored into the field at construction, dispatch is resolved statically with no runtime overhead:
interface Tagged function tag() returns Integerend 'Tagged'
type Holder export let t as Tagged
static function create(t Tagged) returns Self return Self{t: t} end 'create'end 'Holder'Where Clauses (Type Parameter Constraints)
The where clause constrains type parameters to require specific interface conformance. This enables the compiler to verify method calls on type parameters and to reject concrete types that don’t satisfy the constraints.
type Map uses Key, Value implements BuiltinDictionaryLiteral where Key is Hashable // Key is guaranteed to have hash() methodend 'Map'Multiple interfaces on the same parameter use and:
type Container uses T where T is Hashable and EquatableMultiple constrained parameters use comma separation:
type Pair uses A, B where A is Hashable, B is CloneableWhen creating a type alias, the compiler checks that concrete types satisfy the constraints:
typealias Integer = int(i64.min to i64.max)typealias StringMap = Map with (String, Integer) // OK: String implements HashablePer-Instance Typealiases
A ranged typealias declared inside a generic type body produces a nominally distinct type for each concrete instantiation. This prevents accidentally mixing values between different instances of the same generic type.
type Pool uses T export typealias Idx = int(0 to u64.max)
export function push(item T) returns Idx // ... end 'push'
export function get(index Idx) returns T // ... end 'get'end 'Pool'
typealias Integer = int(i64.min to i64.max)typealias PoolA = Pool with Integertypealias PoolB = Pool with IntegerPoolA.Idx and PoolB.Idx are distinct types — passing one where the other is expected produces a compile error. Literal integers that fit the range are still accepted. To explicitly convert between compatible per-instance aliases, use as:
let bIdx = aIdx as PoolB.IdxDot-syntax as casts are also supported:
let idx = 0 as PoolA.IdxInterface Extensions
Extensions add methods to interfaces that are automatically available on all types conforming to that interface. Unlike regular interface methods that each conforming type must implement, extension methods have a single implementation that works for all conformers.
Declaration:
extension Iterable function count() returns int var n = 0 for _ in self 'loop' n = n + 1 end 'loop' return n end 'count'end 'Iterable'How Extensions Work:
- The method becomes available on all types that conform to the interface
- The
selfkeyword refers to the concrete type instance - Extension methods can call any method required by the interface
- Associated types from the interface are resolved to the concrete type’s bindings
Using Associated Types:
Extensions can use the interface’s associated types. These are automatically substituted with the concrete type’s associated type bindings:
interface Container uses Element function get(index int) returns Elementend 'Container'
extension Container function first() returns Element return self.get(0) end 'first'end 'Container'When called on a type like IntArray implements Container with Integer (where Integer is a ranged typealias for int), the return type Element becomes Integer.
Extension Method Synthesis:
When a type conforms to an interface that has extensions, the compiler synthesizes concrete methods for that type. For example, if IntArray conforms to Iterable, calling myArray.count() invokes a method specialized for IntArray.
Extension Rules:
- Declared with
extension InterfaceName ... end 'InterfaceName' - Methods use
selfto access the conforming type instance - Associated types resolve to the concrete type’s bindings
- Extensions from parent interfaces are applied transitively
Conditional Extensions
Extensions can include a where clause to restrict which conforming types receive the extension methods. Only types whose associated type bindings satisfy the constraints will have the methods synthesized.
Syntax:
extension Iterable where Element is Equatable function contains(element Element) returns bool for item in self 'loop' if item == element 'found' return true end 'found' end 'loop' return false end 'contains'end 'Iterable'The where clause follows the same syntax as type-level where clauses: where TypeParam is Interface, with and for multiple interfaces on the same parameter.
Behavior:
When a type conforms to the extended interface, the compiler checks whether the type’s associated type bindings satisfy the where constraints:
- If they do, the extension methods are synthesized for that type
- If they don’t, the methods are silently skipped (no error)
For example, Array with Integer (where Integer is a ranged typealias for int) conforms to Iterable. Since Integer implements Equatable, the contains method is available on Array with Integer. A hypothetical Array with SomeNonEquatableType would not receive the contains method.
Multiple Constraints:
Multiple constraints on the same parameter use and:
extension Container where Key is Hashable and Equatable // Methods available only when Key is both Hashable and Equatableend 'Container'Mixing Unconditional and Conditional Extensions:
An interface can have both unconditional extensions and conditional extensions. Types that don’t satisfy the where clause still receive the unconditional extension methods:
extension Seq function countItems() returns int var n = 0 for _ in self 'loop' n = n + 1 end 'loop' return n end 'countItems'end 'Seq'
extension Seq where Element is Equatable function includes(target Element) returns bool for item in self 'loop' if item == target 'yes' return true end 'yes' end 'loop' return false end 'includes'end 'Seq'In this example, all types conforming to Seq get countItems(), but only those whose Element implements Equatable get includes().
Conditional Interface Conformance
Extensions can add interface conformance conditionally using both implements and where clauses. When a concrete type alias satisfies the where constraints, the type gains the declared interface conformance.
Syntax:
extension Array implements Hashable, Equatable where Element is Hashable and Equatable function hash() returns HashValue // ... end 'hash'
function equals(other Self) returns bool // ... end 'equals'end 'Array'Behavior:
When a concrete type alias is created (e.g., typealias IntArr = Array with Integer), the compiler checks whether the element type satisfies the where constraints. If Integer implements both Hashable and Equatable, then IntArr automatically conforms to Hashable and Equatable, enabling it to be used as a Map key or Set element.
This applies both to explicit typealias declarations and to auto-generated type aliases created during monomorphization.
Tuples
Tuples are fixed-size, ordered collections of values with potentially different types. They use parenthesized syntax for both type annotations and literals.
Tuple Literals
Create tuples using parenthesized, comma-separated expressions:
var t = (10, 20) // 2-element int tuplevar mixed = (42, "hello") // int and Stringvar triple = (1, 2, 39) // 3-element tupleNote: A single parenthesized expression (expr) is NOT a tuple — it is a parenthesized expression. Tuples require at least two elements.
Element Access
Access tuple elements using positional dot syntax .0, .1, .2, etc.:
var t = (10, 20)t.0 // 10t.1 // 20Field Assignment
Tuple fields are mutable and can be assigned individually:
var t = (0, 0)t.0 = 20t.1 = 22// t is now (20, 22)Tuple Types
In function parameters and return types, tuple types use parenthesized type lists:
typealias Integer = int(i64.min to i64.max)
function sum(t (Integer, Integer)) returns Integer return t.0 + t.1end 'sum'
function makePair(a Integer, b Integer) returns (Integer, Integer) return (a, b)end 'makePair'Destructuring Declarations
Tuples can be destructured into new variables using var or let:
var (x, y) = makePair(10, b: 32) // x = 10, y = 32let (a, b) = (10, 20) // immutable bindingsUse _ to discard individual elements:
var (result, _) = compute() // discard second elementvar (_, status) = fetch() // discard first elementIf the function is pure, at least one element must be used:
(_, _) = pureFunc() // Error E3064: all elements discarded for pure functionTuple Assignment
Assign tuple values to existing mutable variables:
var x = 0var y = 0(x, y) = makePair(10, b: 32) // x = 10, y = 32Mixed declaration and assignment — combine existing variables with new declarations:
var x = 0(x, var y) = makePair(10, b: 32) // x existing, y newly declared(var a, var b) = makePair(10, b: 32) // both newly declared(x, let z) = makePair(3, b: 4) // x existing, z immutableDiscard elements:
(x, _) = makePair(42, b: 99) // discard second elementRules:
- All named variables (without
var/let) must already be declared withvar - Immutable (
let) variables cannot be reassignment targets (error E2013) - The number of names must exactly match the tuple’s element count (error E3005)
- Use
_to discard individual elements - If all elements are discarded and the function is pure, error E3064 is raised
Destructuring in For Loops
Tuple destructuring works in for loops when the iterator yields tuples:
var m = ["a": 1, "b": 2]for (key, value) in m 'loop' print("{key}: {value}\n")end 'loop'Use _ to discard loop variables:
for (_, value) in m 'loop' sum = sum + valueend 'loop'Memory Semantics
- Tuples are heap-allocated structs with reference counting
- Each field occupies 8 bytes (primitives and pointers)
- Tuples containing managed types (strings, structs) have their reference counts managed automatically
- Tuples are assigned by reference (like structs). Use
.clone()for an independent copy