ADR-0014: Ownership and access-semantics analysis on IR values

Status: Accepted

Context

The typed SSA IR (ADR-0012) with explicit allocation sites (ADR-0013) gives a stable place to attach per-value facts, but nothing yet computes them. Several compiler enhancements want to know, for each value: how many references exist, whether the value outlives the current function, and how it is used (read / written / compared by identity). Today each would re-derive this ad hoc or pessimize:

JavaScript semantics forbid a Rust-style rejection of programs that fail ownership rules. What we can do is infer where ownership and access properties provably hold.

Decision

Add a flow-sensitive, intra-procedural, may-escape analysis (src/ir/analysis/ownership.ts) that annotates each IR value with two independent lattice values (src/ir/analysis/lattice.ts):

The analysis is a monotone worklist over the control-flow graph. Allocations (instrs carrying an alloc id, ADR-0013) seed at owned / {}; imported references (params, captures, globals) seed at shared / { read }. Each operation widens its operands (field read → read; field write → write and the stored value escapes; opaque call / return / capture → operands escape). States join at CFG merges and across branch-args; the result is the meet-over-all-paths join.

Results are written to the AllocSiteRegistry ownership namespace (ADR-0013) for allocated values and exposed per-function via OwnershipResult.of(value) / .ownershipOf / .accessOf.

Conservative defaults (the safety property)

Anything the analysis cannot reason about gets the TOP element of each lattice: shared ownership, full access set. No consumer may assume a tighter annotation than the analysis returns — consumers check whether the result is tight enough for their optimization and otherwise take the conservative path. This is what makes the pass purely an optimization aid: removing it cannot change observable behavior.

Not a borrow checker

"Ownership" here is inferred classification, not a declared type. Programs that would fail Rust's borrow checker compile fine; they simply receive conservative annotations. The Rust framing is deliberately disclaimed.

Inert + gated

The pass does not mutate the IR; registry annotations are inert at lowering. It runs only behind JS2WASM_IR_OWNERSHIP=1 during rollout. The demonstration consumer (analysis/stack-alloc.ts) is likewise annotation-only in Phase 1: it marks proven-owned-non-escaped small allocations as stack-allocation candidates without changing the emitted representation. The actual stack/scalar lowering is a follow-up.

Phasing

Phase 1 (this ADR): intra-procedural analysis, both lattices, registry integration, one annotation-only consumer. Phase 2: inter-procedural function summaries. Phase 3: precision (better loops, conditional refinement, escape-via-exception) and joint analysis with encoding tracking (#1588).

Consequences