Reference

Classes

The Rust implementation, pinned to de1b6c9e. Every builtin signature.

Classes group state and methods. Instances have shared identity: assigning an instance to another variable, or calling dup, refers to the same object. Arrays and hashes stored in its fields still follow collection value semantics. Classes are nominal and have no inheritance, so an instance’s type is exactly its class.

class Counter
  property count: int

  def initialize(@count: int = 0)
  end

  def increment(n: int = 1)
    @count += n
  end

  alias bump increment
end

def run -> [int, bool]
  counter = Counter.new(10)
  copy = counter.dup
  copy.bump(3)
  [counter.count, copy == counter]
end

This returns [13, true]. Constructors forward positional arguments, keywords and an attached block to initialize, which declares its parameter types like any method. The constructor returns the new instance regardless of the initializer’s return value. Without an initializer, new takes no arguments. A written initialize method is private by default.

Fields and accessors

@name reads or writes an instance variable, and every instance variable is declared: in the class body as @count: int = 0, which gives each instance that default before initialize runs; as @name: string without a default, which initialize must assign on every path (V0205); or by a property, getter or setter. Reading or assigning an undeclared instance variable is a compile error (V0204). The @name: T parameter shorthand assigns the bound argument to that declared field.

The @name: T initializer parameter shorthand assigns a field; it does not replace the class-body declaration or accessor declaration.

property generates a getter and setter; getter and setter generate one half. A member assignment such as counter.count = 3 calls the setter, and one without a setter is a compile error. Arrays or hashes returned by a generated getter are collection values: updating that result does not write through the getter. Methods update the backing field directly.

class Basket
  getter items: array<int>

  def initialize
    @items = [1]
  end

  def add(value: int)
    @items.push(value)
  end
end

def run -> array<array<int>>
  basket = Basket.new
  snapshot = basket.items
  basket.add(2)
  [snapshot, basket.items]
end

This returns [[1], [1, 2]]. Declared field types also guard the shorthand parameters, nested updates and values imported from the host; a rejected nested update preserves the previous field value. A nullable field, such as @next: Node? = nil, can hold nil.

Class state and visibility

Class methods use def self.name. Class variables are declared with a value, @@name: T = value, and are shared within one invocation. Every call starts with independent class state.

class Counter
  @@instances: int = 0

  def initialize
    @@instances += 1
  end

  def self.instances -> int
    @@instances
  end
end

def run -> int
  Counter.new
  Counter.new
  Counter.instances
end

This returns 2 on every call. Uppercase assignments in a class body define class constants, read as LIMIT inside the class and Counter::LIMIT outside it. LIMIT: int = 3 declares a constant’s type, which every assignment to it keeps. Nested classes are named through their scope, Outer::Inner, in types as in values.

Methods and accessors support public, private and protected sections, inline modifiers such as private def helper, and symbol directives. Ordinary private calls require an implicit receiver. Protected instance methods allow callers from the same class’s instances; protected class methods allow callers from that class’s class methods. Aliases preserve the target definition and its visibility at the alias declaration. With static types, a call its visibility forbids is a compile error (V0208).

An instance’s class is tested with value.is_type?(:Counter) and asserted with the checked cast value.as(Counter).

Operators and indexed access

Instances can define +, -, *, /, %, **, <<, &, ==, !=, <, <=, >, >= and <=>, each with typed parameters and a declared result. Operator syntax calls the left instance’s method, and an operator the class does not define is a compile error (V0108); < is not derived from <=>. Compound assignments use the corresponding operator and store its result. An explicit != takes precedence; otherwise != negates the result of ==.

class Counter
  getter value: int

  def initialize(@value: int)
  end

  def +(amount: int) -> Counter
    Counter.new(@value + amount)
  end

  def to_s -> string
    "count=#{@value}"
  end
end

def run -> [int, int, string]
  before = Counter.new(2)
  after = before + 3
  [before.value, after.value, "#{after}"]
end

This returns [2, 5, "count=5"]. Interpolation, output helpers and format call a to_s that accepts zero arguments, including private methods and methods with optional parameters. A required parameter or a non-string result preserves the default instance rendering. Containers keep their own element rendering, as does the string % operator. Errors and exhausted limits propagate through the conversion.

[] receives the index selectors; []= receives those selectors followed by the assigned value. Indexed assignment returns the assigned value. Plain assignment evaluates the right-hand side before its target; compound assignment evaluates its receiver and selectors once before reading and updating the value.

class Grid
  @cells: hash<string, int> = {}

  def [](row: int, column: int) -> int
    @cells.fetch("#{row}:#{column}", 0)
  end

  def []=(row: int, column: int, value: int)
    @cells["#{row}:#{column}"] = value
  end
end

def run -> [int, int]
  grid = Grid.new
  grid[1, 2] = 4
  grid[1, 2] += 5
  [grid[1, 2], grid[3, 4]]
end

This returns [9, 0]. Arrays and hashes returned by an index getter remain collection values. Updating the returned temporary does not write into stored collections or earlier snapshots; returned instances retain their shared identity. Operator and index syntax enforce method visibility and normal call boundaries.

Limits and retained values

Object fields, identity storage, imports and graph traversal are accounted. Cycles are supported and unreachable objects are reclaimed. Cancellation, deadlines and exhausted limits stay latched through constructors, methods and cleanup. A host may retain an instance after a successful or failed call.

Instances returned to Rust can be passed back to the same compiled script. Imports preserve shared references and cycles within the new call while isolating updates from the source value. Concurrent calls also get independent imported objects and class state.

Inheritance, singleton classes, super, and module mixins are outside the language.