From ddf3efdf6e66f8075964467a9d483e9da261f4be Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 3 Jul 2026 22:09:16 +0200 Subject: [PATCH] Class safety #13 --- content/docs/guide/classes.mdx | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/content/docs/guide/classes.mdx b/content/docs/guide/classes.mdx index e4863d2..57cb07d 100644 --- a/content/docs/guide/classes.mdx +++ b/content/docs/guide/classes.mdx @@ -89,3 +89,102 @@ A class `Dog : Animal` generates: 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: + +```mist +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 + } +} +``` + +4. **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: + +```mist +pub constructor(bool flag) +{ + if flag { + self.health = 100; + } else { + self.health = 0; + } + // Both branches init health ✓ +} +``` + +5. **Super initialization** — When a class inherits, the `_super` field is added to the required-field list. Any assignment to `super` counts: + +```mist +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: + +```rust +#[allow(invalid_value)] +fn __test_vt() { + let this: &Self = &unsafe { std::mem::MaybeUninit::::zeroed().assume_init() }; + let _: &Target = this; // Forces compiler to check Deref +} +``` + +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 + +| Risk | Mitigation | +|------|-----------| +| Uninitialized fields via `MaybeUninit` | Static field-initialization verification rejects incomplete constructors | +| UB from reading uninitialized fields | Intersection analysis ensures all branches init the same fields | +| Invalid override signatures | Compile-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.