Memory management / Lifetime regions and escape validation
Every allocation has exactly one semantic lifetime owner. GC cannot bypass topology validation.
Moth is reference-semantic by default, copy-explicit and move-inferred. It omits explicit reference types and lifetime syntax, not references themselves.
Lifetime-region and escape validation is the mandatory semantic proof that every retained reference is legal under one lifetime owner. It is distinct from physical memory-strategy selection, which happens afterwards.
Mandatory semantic proof covers:
User-authored topology that is invalid or cannot be proven legal produces a structured CompilerDiagnostic. Missing or inconsistent compiler-owned summaries, boundary classifications, lifecycle roots, or metadata produce CompilerError.
It also owns compiler-generated non-lexical intervals, retained-edge liveness and final cleanup frontiers. These decide how precisely a legal topology can be reclaimed, not whether it is legal.
Physical strategy selection happens after this proof succeeds. It chooses stack placement, static affine cleanup, inferred region allocation, declared-region bulk reclamation, Retained Edge Counting or a host garbage-collected representation. Imprecise planning results in conservative retention, never in rejecting a legal program. A missing physical strategy after successful topology validation is a CompilerError, not a silent collector fallback.
GC is a permitted runtime representation for a statically legal lifetime topology on debug profiles and GC-native backends. It is not a mechanism for accepting invalid retained edges, lateral lifetime promotion, unowned escapes or cross-region cycles, and a capable full-control release backend must not fall back to it.
Implementation of this analysis is deferred. This leaf is the canonical design authority for lifetime topology. Canonical declared-region semantics live under Declared regions. The roadmap file owns implementation sequencing and deferred follow-up only.
Term | Definition |
|---|---|
Allocation | One runtime storage unit created for a value graph node. |
Allocation family | The containing allocation plus projections that remain rooted in it. |
Lifetime owner | The one semantic region responsible for an allocation's lifetime. |
Lifetime region | A semantic lifetime-owner domain with an outlives relation to other regions. |
Lexical scope | Name visibility and control-flow exit structure. It does not define allocation lifetime. |
Inferred region | A compiler-generated non-lexical lifetime region. It may end before the lexical scope that introduced its allocation. |
Retained-edge liveness | Whether any retained edge into an allocation family or region can still be observed on the current path, tracked separately from the binding lifetime of the aggregate that holds it. |
Semantic commit point | The program point at which a retention-sensitive mutation's retained-edge effects take effect atomically on the successful path. |
Committed retained-edge state | The set of direct retained edges that hold in one reachable committed program state. |
May-coexist edge relation | Two direct retained edges may coexist when analysis cannot prove they never hold together in one reachable committed state. |
Final cleanup frontier | A CFG point after which every relevant retained edge into an allocation family or inferred region is proven gone. |
Region epoch | One compiler-generated population-and-teardown cycle of a region whose retaining aggregate outlives it. |
Retained reference | A stored reference that survives the immediate operation. |
Retained edge | A directed storage relationship from a retaining object to a referenced allocation. |
Outlives relation | |
Declared region | A source-declared hard lifetime region introduced by |
Builder lifecycle root | A page, mount, request, frame or other lifetime owner supplied by builder architecture. |
Physical representation | Stack, inline, affine heap, inferred arena, declared-region arena, REC-counted or garbage-collected storage, chosen after topology is validated. |
Physical arena | One possible backend representation of one or more lifetime regions. It is not source semantics and is not synonymous with a semantic region. |
Fresh result root | Newly allocated result root that may retain legal references to older allocations. |
Alias result | Result root that is an existing argument, projection, external allocation, or another result. |
Independent result graph | New graph with no mutable sharing or retained Moth reference to pre-existing storage. |
The complete semantic topology consists of inferred regions, declared regions and builder lifecycle regions or roots. These are all lifetime regions; physical arenas are later planner-owned representations.
An allocation family is the containing allocation plus field, element and other projections that remain rooted in it. A projection does not silently become an independent allocation.
Every runtime allocation has exactly one semantic lifetime owner.
Multiple bindings, fields, elements or returned values may alias one allocation without becoming additional lifetime owners.
Page region owns Theme
header.theme ─┐
footer.theme ──┼── Theme allocation
theme binding ─┘Affine cleanup responsibility records which runtime path may perform individual cleanup. It does not prove uniqueness, identify the final observer, establish lifetime legality or create dynamic shared ownership. Semantic lifetime ownership stays static even when cleanup responsibility moves at runtime.
Every allocation has exactly one semantic lifetime owner. An object may retain a reference only when the referenced allocation belongs to the same lifetime region or to a region statically known to outlive the retaining object.
Formal rule:
R_value >= R_containerwhere R_value lives at least as long as R_container.
Valid edges:
Invalid edges:
Shared aliases may outlive the lexical binding that first named the storage.
Lexical scope does not define allocation lifetime. Escaping or retained aliases do not create unrestricted dynamic shared ownership. They remain under one lifetime owner and must satisfy the stored-edge outlives rule.
Inferred regions are compiler-generated non-lexical CFG intervals, not merely lexical scopes. An inferred region may begin and end at points that no source scope boundary names, may end before the enclosing scope finishes, and may differ between branches of the same function.
An ordinary allocation begins in the narrowest inferred region capable of owning its initial uses. The compiler may widen that allocation only to the nearest existing ancestor on the same ordered owner chain that outlives every retained observer.
The inference sequence:
choose the narrowest initial owner
-> collect retained observers
-> widen only when required
-> choose the nearest existing ancestor on the same owner chainInferred-region analysis may not silently promote an allocation laterally across independently ending sibling lifetime domains.
Widening may follow one ordered owner chain only. The compiler must not widen farther than necessary or invent a page-, application- or process-lifetime owner merely to avoid a diagnostic.
Independently ending sibling lifetime domains do not form one ordered chain.
Sharing across sibling domains requires one of:
copyDeclared regions are hard boundaries and cannot be silently widened.
Fresh escaping values should be allocated directly into the destination lifetime where possible.
Existing values may transfer affine cleanup responsibility only at a proven final use. When later use exists, the outcome is path-dependent, or proof is imprecise, the operation remains a borrow.
Inferred transfer is optional. When safe transfer is not proven on every relevant path, the operation remains a borrow. Failure to prove an ownership optimisation must not make an otherwise valid source program invalid.
copy place creates independent storage. A copied acyclic graph may be allocated directly into an inferred or declared destination region. A copied cyclic graph must be allocated wholly into one declared region. Copying a cycle into an ordinary inferred region is invalid. Preserving internal alias topology does not override the rule that direct source-created cycles require one declared region. See Access and aliasing for the complete graph-level copy contract.
Interior projections remain rooted in their containing allocation family. Returning, storing, or escaping a projection retains that family. At a proven final use, analysis may transfer the containing allocation family. A child is independently releasable only when it was already established as a separately owned allocation before the escape.
A projection does not silently become an independent allocation or independent copy. Returning, storing or escaping a field or element alias retains the base family under the stored-edge rule.
Field-sensitive allocation splitting may prove that a projected field is an independent allocation family with its own lifetime owner, after which strategy selection reruns for the new families. It applies only when the compiler establishes separate child ownership before the alias escapes. It is not retroactive interior projection detachment.
Mandatory source topology must already be valid without field-sensitive splitting. Splitting is a physical refinement of an accepted topology, never a way to make invalid source legal.
Because splitting depends on physical layout, target/layout-aware splitting happens after build-owned target partition and target-contract validation, once a candidate physical variant exists. For each candidate variant, field-sensitive family refinement rebuilds the affected direct family graph and revalidates affected outlives, family-base and may-coexist SCC facts:
validated unsplit topology
-> target/layout-aware split candidate
-> rebuild affected direct family graph
-> revalidate affected outlives, family-base and may-coexist SCC factsIf refinement succeeds, memory planning uses the refined families. If it cannot be proven:
reject the optimisation
-> keep the original unsplit family
-> retain conservativelyFailure to prove a split keeps the unsplit family and causes conservative retention. It never produces a source diagnostic, and it never changes source legality in one physical variant relative to another.
Function lifetime summaries cover:
Multiple return values may alias one allocation when they remain under one caller lifetime owner.
A caller may place a fresh result root directly into a destination region only when every retained edge in the result is legal for that destination. An alias result remains tied to its existing lifetime owner. An independent acyclic result graph may enter an unrelated destination lifetime without retained edges to its source graph. A copied cyclic result graph must instead enter one declared region.
Borrow validation supplies preliminary return-root alias evidence. It does not publish final result provenance or retained-edge summaries.
Lifetime and retained-edge analysis owns the final summary vocabulary:
The final summary distinguishes fresh roots, parameter aliases, projections, detached stored results, result-to-result aliases and independent result graphs. It also preserves exit-specific retention effects, whole-domain kills and frontier-enabling facts. These stable semantic facts cross module and package boundaries. Donor-local family, region, counter and concrete-frontier indexes do not.
A module or package interface may export:
The caller and link-level lifetime analysis derives the concrete CleanupFrontier, region exit and region epoch after combining those effects with caller-local aliases, other live retention domains, future edge-creation ability and builder lifecycle roots. A donor-local frontier ID never crosses a module boundary.
Lifetime inference tracks retained-edge liveness separately from aggregate binding lifetime. A collection or aggregate may continue to live after the region holding its retained values has ended.
Path-sensitive future-use facts establish both sides of a cleanup frontier. They prove an edge is dead when no later use can observe that retained edge on the path. They separately prove loss of recreation capability when no source alias capable of recreating the edge survives the point. A possible future use or surviving capable source keeps the relevant edge obligation live.
A final cleanup frontier for allocation family A requires proof that:
A from the relevant domains is gone on that pathAA after the pointFrontiers may be path-sensitive and may have different exits on different branches.
The initial whole-domain kill primitives are:
clearIndividual remove, set or partial cleanup does not establish uniqueness or a frontier by itself. Uniqueness scans, alias registries and adaptive owner recovery are rejected from the design.
populate(~index)
use(index)
~index.clear()The collection may continue living. If no other live alias or retention domain can retain a target allocation family, clear() can become that family's final cleanup frontier. The collection itself may continue to live. Declared-region-owned storage still remains until declared-region exit, even though clear() kills the declared region's collection edges logically.
Repeated population and whole-domain cleanup may form compiler-generated region epochs. The aggregate object and its backing capacity outlive each retained-value epoch.
R_index_epoch_0
population 0
-> first clear
R_index_epoch_1
population 1
-> second clearRetained-edge liveness is part of lifetime topology. It is not a separate runtime ownership system.
A retention-sensitive mutation has a semantic commit point: the program point at which its retained-edge effects take effect atomically on the successful path. Successful mutations remove old retained edges and add new retained edges atomically at the semantic commit point. The committed retained-edge state therefore contains the old topology before the commit or the new topology after it, never a temporary union of both.
A failed mutation does not commit its retained-edge effects. It preserves the old topology and the retained-edge obligations that topology holds, while an incoming affine responsibility follows the transfer or discharge path planned for the failure outcome. Path and epoch separation may prove that edge sets cannot coexist in one reachable committed state.
For example, two branches can produce opposite edges without producing one runtime cycle:
branch A:
A -> B
branch B:
B -> AA path-insensitive union must not automatically be presented as proof that a runtime cycle exists. Analysis may preserve branch separation. If coexistence cannot be disproved, the topology is not proven legal and receives the normal topology diagnostic.
A replacement likewise transitions between committed states:
old committed state:
A -> B
new committed state:
B -> AThe validator reasons about committed states rather than a temporary union of pre-commit and post-commit edges.
When an allocation family has runtime-dependent persistent retained-edge multiplicity, those edges disappear independently, and no affine cleanup, inferred region, complete cleanup frontier, declared region, field split or cheap bounded retention gives a precise enough answer, the memory planner may select Retained Edge Counting for that family.
Eligibility is a planning outcome, never a legality question. This analysis never counts anything and never selects a strategy; it supplies the family, edge and frontier facts that make selection possible. See Retained Edge Counting for the selection ladder and the counted representation.
Cross-region cycles are invalid. Cycle validation computes strongly connected components over direct retained edges that may coexist in one reachable committed program state. A path-insensitive union of edge sets is not itself a committed-state SCC. Path-sensitive or epoch-sensitive facts may show that candidate edge sets cannot coexist.
When analysis cannot disprove coexistence, the topology is not proven legal and receives the normal topology diagnostic. It is not silently accepted, and an imprecise union is not presented as proof that a runtime cycle exists.
Every real direct source-created retained-edge self-cycle or multi-family SCC still requires one declared region. Inferred-region analysis never invents a cyclic region. Direct source-created reference cycles are legal only inside one declared region, where storage is count-free and reclaimed in bulk at declared-region exit. Direct source construction of cyclic reference graphs remains deferred with the rest of declared-region implementation. V1 programs should model cyclic domain graphs through scalar IDs, indexes or keys inside one region when needed.
Last-use and future-use facts may prove that an edge is dead, that no source capable of recreating the edge survives, or that edge sets occur on disjoint paths or epochs. These facts can prevent a false cycle coexistence result, but they never reclaim a real retained-edge SCC.
Reactive subscriptions are read-only dependencies, not active borrow lifetimes.
Builder-supplied page, mount, request, frame and arena roots are lifecycle inputs. They obey the same retained-edge rule as source regions. Builders supply lifecycle roots but cannot change source legality.
Reactive storage that outlives a lexical function must still satisfy one lifetime owner and the stored-edge outlives rule. A collector-free path may use builder-owned lifecycle regions. A GC path may represent the same already-proven topology with ordinary reachability.
External bindings use closed semantic boundary profiles. They do not expose arbitrary user-defined lifetime graphs.
WIT value-only V1 profile. Moth imports general external Wasm libraries through a value-only WIT component profile. Supported arguments are lowered from shared reads into independent component values, and results are lifted into independent Moth result graphs. No Moth alias, lifetime owner, ownership state or destruction responsibility crosses the component boundary.
Restricted host-binding profile. Existing Core, Builder and JavaScript-backed bindings use a restricted host-binding profile. Ordinary Moth values cross by value, host code may not retain references into ordinary Moth storage, and opaque handles represent foreign identities rather than Moth reference types.
Richer resource-capable profiles are deferred and require a separate complete design. Detail lives in Runtime and backend
lowering, docs/build-system-design.md and docs/src/docs/packages/external-binding-contracts.mtf.
Lifetime-topology legality belongs to a distinct backend-neutral analysis after borrow validation and before target planning.
Do not invent a numbered Stage 7 for this boundary. Stage 6 remains borrow validation. Lifetime-region and escape validation is an explicit unnumbered analysis after Stage 6.
Per-function and module analysis produces local allocation, alias, retention, escape, result and outlives constraints plus exported summaries.
It reads validated HIR and read-only borrow/effect facts. It writes immutable side-table facts and summaries. It does not rewrite HIR. It does not choose JS versus Wasm. It does not choose physical allocation representation. It does not make source syntax decisions.
Build/link analysis instantiates those summaries over the reachable call graph and builder-supplied lifecycle roots. Local module compilation cannot validate every cross-module or builder-lifecycle relationship by itself.
Backends receive a validated topology and may not reconsider source legality.
After topology, interval, frontier and epoch completion, this analysis publishes one final shared handoff: backend-neutral memory requirements. They are target-independent and profile-independent, and they are the last artefact shared by every physical variant.
They may contain allocation-family identity, the validated lifetime owner, intervals, frontiers and epochs, retained-edge and retention-domain facts, retention cardinality, REC candidacy facts, declared-region membership, affine transfer and cleanup candidates, hidden-destination constraints, lifecycle constraints and external-boundary constraints.
They must not contain any selected physical strategy: no selected REC representation, no selected host-GC representation, no target allocator choice, no concrete counter layout, no target-specific arena layout and no target-specific handle representation. Those belong to target/profile-aware memory planning, which runs after target partition and target-contract validation.
Lifetime diagnostics are part of the memory model, not an afterthought. They must identify the failed topology and steer the programmer toward direct destination allocation, a common lifetime declared region, independent storage through copy, or a shorter retained edge.
High-quality diagnostics are a central reason Moth can reject hidden dynamic shared ownership without exposing source-level RC.
Lifetime diagnostics must distinguish:
User-facing diagnostics use stable codes and structured reason payloads. Internal impossible or inconsistent metadata uses CompilerError.
Every lifetime diagnostic should identify, where applicable:
Remedies must be ranked in this order:
copyThere is no backend-specific escape from semantic lifetime diagnostics.
Stronger future analysis may prove more legal programs or narrower owners, but cannot change the meaning of already-accepted programs.
Conservative analysis may reject topology it cannot prove legal. That is a source diagnostic, not permission to fall back to a tracing garbage collector for an unproven graph.
~ validity.TypeId or source signatures.remove or set.TypeId.name: / into name surface and hard destination lifetimes