Passive Objects

Part of: Reef Language Reference Last reviewed on version: 0.9.0 Status: Implemented (0.9)


Overview

A passive object is a heap-allocated class instance with single inheritance and virtual methods. It is not an Active Object: there is no monitor, no run() thread, and no await. The exclusive / shared markers on a passive method are a requirement on the caller's owner context, not a lock the object takes.

Kind Heap Inheritance Concurrency
Struct optional (new or by-value literal) none none
Passive object always (new Class(...)) single, virtual methods owner-context discipline
Active Object always none (an AO may own objects) monitor; optional run()

Use a struct for a data record. Use a passive object when you want a hierarchy and virtual dispatch (widgets, devices, AST nodes). Use an Active Object when the thing is the concurrency boundary.

See 045_ACTIVE_OBJECTS.md for the monitor model. Traits on objects are in Traits on objects below and in 050_TRAITS.md.

object is a reserved word. extends and override are contextual (special only in an object declaration). inherited and typecase are reserved.


Declaration

Fields first, then methods. Field access is always self.x — there is no implicit field lookup, so a local cannot shadow a field.

object Point
    x: int
    y: int

    init(x: int, y: int)
        self.x = x
        self.y = y
    end init

    shared fn sum(): int
        return self.x + self.y
    end sum
end Point

proc main()
    let p = new Point(3, 4)
    println("${p.sum()}")
end main

Every method is shared (read-safe) or exclusive (may mutate self). init and finalize take no marker.


Construction

new Class(args) allocates, then runs init. A class with no init zero-initializes (new Class()). init is not virtual: each class has its own. A derived init must call inherited init(...) as its first statement.

object Counter
    n: int

    init()
        self.n = 0
    end init

    exclusive proc bump()
        self.n = self.n + 1
    end bump

    shared fn get(): int
        return self.n
    end get
end Counter

proc main()
    let c = new Counter()
    c.bump()
    println("${c.get()}")
end main

Inheritance and virtual dispatch

extends names one base class. Methods are virtual by default. override is mandatory on a redefinition and rejected when nothing of that name exists to override. The exclusive/shared marker is part of the signature and may not change.

inherited m(args) statically calls the defining class's base's implementation of m (not the dynamic receiver's).

A Base-typed reference still dispatches to the override:

object Widget
    x: int
    y: int

    init(x: int, y: int)
        self.x = x
        self.y = y
    end init

    shared proc paint()
        println("Widget(${self.x},${self.y})")
    end paint
end Widget

object Button extends Widget
    label: string

    init(x: int, y: int, label: string)
        inherited init(x, y)
        self.label = label
    end init

    override shared proc paint()
        inherited paint()
        println("  label=${self.label}")
    end paint
end Button

proc main()
    let b = new Button(1, 2, "ok")
    let w: Widget = b
    w.paint()
end main

A Derived value inhabits any Base slot: bindings, parameters, returns, call arguments, and array elements. Array types stay invariant ([Button] is not a [Widget]).

== / != on objects is reference identity. Ordering (<, >) is rejected.


is, as, and typecase

w is Button walks the runtime class chain. w as Button aborts the process on mismatch (safe Reef has no nil for user types). Prefer typecase when several arms are needed — the binder is the narrowed view. First matching arm wins; a later arm whose type is equal to or a subclass of an earlier arm is a type error (statically unreachable).

object Widget
    n: int
    init(n: int)
        self.n = n
    end init
end Widget

object Button extends Widget
    init(n: int)
        inherited init(n)
    end init
end Button

proc tag(w: Widget)
    typecase w
        Button b =>
            println("button ${b.n}")
        end
        else =>
            println("other ${w.n}")
        end
    end typecase
end tag

proc main()
    let b = new Button(1)
    let w: Widget = b
    if w is Button
        let d = w as Button
        println("as ${d.n}")
    end if
    tag(b)
    tag(new Widget(2))
end main

type_name / type_id report the dynamic class.


Traits on objects

impl Trait for Class works the same as for a struct. Subclasses inherit the base's impls for method syntax; a subclass's own impl wins for its static type. Trait methods are statically dispatched to the impl target — they are not virtual.

The pattern that does give you "one impl, per-class behaviour" is delegate-to-virtual: the class method is the virtual; the trait impl forwards to it. The two names must differ. An own method and an impl method of the same name on the same class is a type error (one name, one method).

trait Drawable
    fn draw(): string;
end Drawable

object Widget
    n: int
    init(n: int)
        self.n = n
    end init
    shared fn describe(): string
        return "widget"
    end describe
end Widget

object Button extends Widget
    init(n: int)
        inherited init(n)
    end init
    override shared fn describe(): string
        return "button"
    end describe
end Button

impl Drawable for Widget
    fn draw(): string
        return self.describe()
    end draw
end impl

fn show[T](x: T): string where T: Drawable
    return x.draw()
end show

proc main()
    let b = new Button(1)
    let w: Widget = b
    println("${b.draw()} ${w.draw()} ${show(b)}")
end main

b.draw() and w.draw() both run Widget's impl. That body calls virtual describe(), so a Button receiver prints "button". show(b) is the same impl reached through a where T: Drawable bound — a subclass inhabits the bound when a base implements the trait.

Do not write impl Drawable for Widget with a method also named describe. Rename the trait method, or drop the impl and call the virtual directly.

A working tree that combines inheritance, virtuals, typecase, and this trait pattern is examples/widget_tree.reef.


finalize

Optional. Runs on reclaim, not virtually. After a derived finalize returns, the base's finalize (if any) runs automatically — do not write inherited finalize(), and do not mark finalize override. A finalizer must not allocate (it runs while the heap lock is held) and must not touch owned object graphs (external resources only).

object Handle
    fd: int
    init(fd: int)
        self.fd = fd
    end init
    finalize()
    end finalize
end Handle

proc main()
    let h = new Handle(3)
end main

What 0.9 does not do

  • No dyn Trait. Traits stay compile-time (monomorphized where bounds). Runtime dispatch is the class vtable.
  • No generic objects. object Stack[T] and extends Container[int] are type errors.
  • No object-typed spawn arguments. A spawned thread is never the owner.
  • No multiple inheritance, and no inheritance into or out of Active Objects. An AO may own a passive graph.
  • No qualification of colliding trait methods. Two impls providing the same method name on one type is a type error; rename.

Owner-check harness

Owner-check probes are opt-in (reefc --owner-harness). The runtime stamps each object at new with the current owner (innermost held Active Object, else the allocating thread) and aborts on a mismatch at exclusive method entry, shared method entry (read), and field writes. Finalize-time access always fires. Default builds emit no probes. Pins: reef-compiler/examples/test_owner_*.reef.

unsafe does not relax this rule: a cross-owner method call or field write is still a cross-owner access. The harness is how you catch it.