LogoMist

Class Internals

How classes are compiled under the hood — vtable layout, code generation, and safety verification.

This page covers the implementation details of Mist classes. You don't need to read this to use classes — it's here for curiosity and compiler contributors.

Under the Hood

A class Dog : Animal generates:

  1. A Rust struct with a _super: Animal field (or _vptr: &'static [*const c_void] for root classes)
  2. A vtable constant with function pointers for each public method
  3. An impl block with Deref<Target = Animal> and DerefMut
  4. Method trampolines (__m_<name>) that are dispatched through the vtable
  5. A new() constructor that initializes via MaybeUninit and calls the user's constructor(&mut self)

The vtable is unified: parent entries are copied, overridden entries replace parent slots, and new methods are appended.

Safety

Classes use MaybeUninit::zeroed().assume_init() inside the generated new() function — an inherently unsafe operation. This raises a natural question: Are class constructors unsafe?

The answer is no. The compiler statically verifies that every field is initialized before the constructor returns, eliminating the undefined behavior that raw MaybeUninit would normally carry.

Static Field Initialization Verification

When a class has a constructor, the semantic checker (check_class_semantics) collects every declared field and walks the constructor body to prove each one is written to:

  1. Direct assignment tracking — Expressions like self.field = value are recognized as mutations of field. The analyzer checks for = and -> operators whose left-hand side is a self.field path.

  2. &mut self.field tracking — Taking a mutable reference to a field (&mut self.field) also counts as initializing it, since the reference can only be taken if the field is being set up.

  3. Transitive method calls — If the constructor calls self.helper(), the analyzer follows into helper's body and tracks which fields it initializes. This transitively propagates through nested calls:

class Player
{
    str& name;
    i32 health;

    pub constructor(str& name)
    {
        self.name = name;
        self.setup_health();
    }

    void setup_health(&mut self)
    {
        self.health = 100;  // Counts toward constructor's verification
    }
}
  1. Branch intersection — For if/else, match, and loops, fields must be initialized in all branches. If one branch initializes x but another does not, x is considered uninitialized. This ensures soundness regardless of the runtime path:
pub constructor(bool flag)
{
    if flag {
        self.health = 100;
    } else {
        self.health = 0;
    }
    // Both branches init health ✓
}
  1. Super initialization — When a class inherits, the _super field is added to the required-field list. Any assignment to super counts:
pub constructor(str& name)
{
    super = Super::new(name);
}

If any field is uninitialized after the full analysis, a compile-time error is reported with the field's exact source location:

class field `Player.health` is uninitialized

Override Validation at Compile Time

When a method uses the override keyword, the codegen emits a hidden test function that verifies the Deref chain at compile time:

#[allow(invalid_value)]
fn __test_vt() {
    let this: &Self = &unsafe { std::mem::MaybeUninit::<Self>::zeroed().assume_init() };
    let _: &Target = this;  // Forces compiler to check Deref<Target = Target>
}

This ensures &Self can always deref into the base class type. If the inheritance hierarchy is invalid, the Rust compiler rejects it.

VTable Safety

The _vptr (vtable pointer) is set twice during construction:

  1. Before the constructor body runs — enabling virtual dispatch inside the constructor itself
  2. After the constructor body — in case a base-class constructor ran and overwrote the pointer

This ensures that virtual method calls work correctly even during object construction, without exposing uninitialized memory through the vtable.

Summary

RiskMitigation
Uninitialized fields via MaybeUninitStatic field-initialization verification rejects incomplete constructors
UB from reading uninitialized fieldsIntersection analysis ensures all branches init the same fields
Invalid override signaturesCompile-time Deref test validates the inheritance chain
Vtable corruption during construction_vptr is set before and after the constructor body
Unsafe code in generated constructors#[allow(invalid_value)] is scoped to the generated new() only

The MaybeUninit pattern is an implementation detail of the generated code — the Mist compiler proves soundness at the language level, so the user's constructor body is safe Mist code with no manual unsafe annotations required.

On this page