Wado

WEP: Reference Representation and Mutation Write-Back

Context

Wado has no raw pointers and no borrow checker. A reference (&T / &mut T) is always a GC-managed handle (see Value Semantics and Reference Stores). How that handle is represented on Wasm GC has never been written down, even though the choice differs by referent type and is what makes mutation through a &mut observable at the original place.

This WEP specifies the intended representation and &mut semantics, and catalogs where the current implementation diverges. The divergences are bugs to fix so the implementation conforms — not behavior to preserve. The motivating symptom is a silent miscompile (issue #1333): a write through &mut xs[i] or &mut s.f of certain element/field types is discarded with no diagnostic.

Design (normative)

The dividing line: in-place mutation vs replace-on-assign

A &mut T must let a write be observed at the original place. The representation follows from how the value is mutated through the reference:

The axis is mutation mode, not "scalar vs heap". A variant is a heap GC struct yet is replace-on-assign (see below). Mutability (& vs &mut) is a type-level distinction, not a representational one: a reference to a replace-on-assign type is Box<T> whether & or &mut.

Classification of types

Category Types &T / &mut T Mutation through the reference
In-place (reference types) struct; List<T> (raw GC Array<T>); String; i128 / u128 shared GC handle field / element write on the shared object
Replace-on-assign (value/boxed) primitive (except i128 / u128); enum; flags; variant; fn / fn mut Box<T> *r = v replaces the box's content
Handle resource (opaque i32; conceptually i32 / externref) see divergence D6 replace-on-assign conceptually

Notes on the non-obvious entries:

Connecting a &mut to a place: the box must be the place's storage

For a replace-on-assign type, a write through &mut is observable at the original place only when that place's storage is the box cell. The compiler establishes this for locals via the address-taken-locals boxing pass:

let mut c = Color::Red;
let r = &mut c;   // c's slot promoted to Box<Color>; r shares that box
*r = Color::Blue; // writes the box
code(c);          // reads c (auto-unboxed) -> Blue

A non-local place — a List / array element xs[i] or a struct field s.f — has no local to promote.

The boxed-as-value set is a single predicate

The set of types boxed by reference (the replace-on-assign category) MUST be defined by one predicate (e.g. is_boxed_reference_target(T)), and the boxing pass, the forbid rule, and the carve-out below MUST all consume that predicate. No component re-lists the member types by hand; that is how fn and flags already drifted out of sync (see D2D4).

&mut to a non-local replace-on-assign place

In-place places — &mut <local> of any type, and &mut of a struct / List / String reference mutated in place — are always allowed and unaffected.

Decision

Each carve-out narrows the forbid for the case it handles; the escaping case is never carved out.

Known implementation divergences

These are gaps between the design above and the current tree. Each is a bug to fix to conform; none should be preserved.

Consequences

Positive

Negative

References