Accepted mandatory topology, deferred validation
The accepted language contract requires mandatory, backend-independent lifetime-region and escape validation. That validation remains deferred in the current compiler. Once implemented, it will prove every retained reference legal under one lifetime owner, and GC won't bypass it.
After that proof succeeds, the compiler selects a physical strategy for each allocation: stack placement, cleanup at a proven final use, an inferred region, bulk release with a declared region, selective Retained Edge Counting or a garbage-collected representation. REC may handle a runtime-dependent number of persistent stored edges that disappear independently when a region would retain storage materially beyond its useful lifetime. Strategy selection changes memory quality, never program behaviour and never which programs are valid. When no narrower strategy applies, storage remains under its owning region until that region ends.
Backends that advertise full memory control lower release builds with no tracing collector. That is a property of the backend, not a mode you select in source or project config.
The rules below define the accepted end state rather than current compiler enforcement. Use the progress matrix for implementation status.
One semantic lifetime owner
Every runtime allocation has exactly one semantic lifetime owner. Multiple bindings, fields, elements or returned values may alias one allocation without becoming additional owners. Cleanup responsibility is a separate runtime fact that may move between paths but never duplicates. It doesn't prove uniqueness, and the semantic lifetime owner stays fixed regardless of where cleanup happens.
The retained-edge outlives rule
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.
R_value >= R_container
where R_value lives at least as long as R_container.
Valid edges include a local retaining a longer-lived parameter, a value owned by a child declared region retaining a value owned by a parent declared region, or two fields of one page-owned aggregate sharing one page-owned child.
Invalid edges include a longer-lived object retaining shorter-lived storage, a parent retaining a value owned by a child declared region, or a returned local outliving its source region without independent storage.
Lexical scope doesn't define allocation lifetime
Shared aliases may outlive the lexical binding that first named the storage. Escaping or retained aliases remain under one lifetime owner and must satisfy the stored-edge outlives rule. Lexical scope controls name visibility and control-flow exits, not allocation lifetime.
Nearest-existing-ancestor widening
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.
Widening follows 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.
Siblings don't form one ordered chain. Independently ending lifetime domains cannot be laterally promoted across each other. Sharing across sibling domains requires one of:
- an already-existing common semantic owner
- an enclosing declared region
- a builder-declared common lifecycle
- independent storage created by
copy
Fresh result roots
A fresh result root has a new root allocation but may retain legal references to older allocations. Fresh results include literals, templates, constructor calls and computed aggregates.
name = "Priya"
greeting = [: Hello, [name]]
items = {1, 2, 3}
A fresh result root may retain parameters or other pre-existing values only when every retained edge satisfies the destination lifetime's outlives constraints.
Alias results
An alias result reuses an existing root or projection. Shared bindings, field access and shared function returns produce alias results. An alias result stays tied to its existing lifetime owner and cannot become new declared-region-owned storage through into.
original = load_items()
view = original
first = original.get(0) catch:
assert(false, "known valid index")
;
Projection roots
Interior projections remain rooted in their containing allocation family. Returning, storing or escaping a projection retains that family. A projection doesn't silently become an independent allocation or independent copy.
Interior projection detachment means moving a projected field out of its containing allocation family. It remains deferred until field-sensitive splitting has established separate ownership. This restriction does not prohibit a container-detached result from remove, which kills a collection-retained edge and returns the already-stored value under ordinary lifetime rules.
Result binding versus allocation identity
Every returned value enters a fresh caller binding slot. Binding-slot freshness and allocation freshness are separate facts: a fresh slot may contain a value that aliases an older allocation. Rebinding that slot replaces its current value provenance; it does not rebind or mutate another binding that still observes the older allocation.
original ~= Point(1)
returned ~= identity(original)
returned = Point(2)
returned.x = 3
original.x = 4
A call result owns a separate binding even when its value aliases an argument. A fallible return also has a separate carrier while its success payload may alias an argument allocation. Rebinding either result detaches that binding from its old allocation without rebinding another alias.
Independent result graphs
An independent result graph has no retained Moth reference to pre-existing storage. copy produces independent results. WIT value-only lifting produces independent results. An independent acyclic graph may enter an unrelated destination lifetime without retained-edge constraints to its source graph. A copied cyclic graph preserves its internal alias topology, so the whole graph must enter one declared region. It cannot enter an ordinary inferred region.
Aggregate storage
Existing values stored in structs, choices, collections, maps, tuples, templates or other aggregates retain shared reference semantics by default. copy creates independent storage.
Maps own their entry structure while keys and values follow the same shared/copy/inferred-transfer rules. Map lookup keys are borrowed. get returns a shared alias. remove returns the removed value under normal lifetime and ownership rules.
Return and multi-return aliasing
Function lifetime summaries cover fresh result roots, parameter aliases, projection aliases, detached stored results, result-to-result aliases, independent result graphs, retained-parameter constraints, persistent-retention effects, retention cardinality, whole-domain kills, outcome-sensitive success and error effects and required outlives relationships.
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.
Current multi-return analysis may conservatively treat each projected result as aliasing any parameter root returned by the function. It may reject a mutation that more precise per-result analysis could prove independent.
Cycles
Cross-region cycles are invalid. Every strongly connected allocation graph must belong to one lifetime region. A declared region is the only source mechanism for building a direct cycle: the compiler never invents a cyclic region on your behalf. copy preserves internal alias topology, but copying a cycle into an ordinary inferred region remains invalid. Direct source construction of cyclic graphs is deferred with the rest of declared-region implementation.
Reactive and builder-owned lifetimes
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.
External boundaries
External bindings use closed semantic boundary profiles. They don't expose arbitrary user-defined lifetime graphs.
WIT value-only V1 imports lower shared reads into independent component values and lift results into independent Moth result graphs. No Moth alias, lifetime owner or destruction responsibility crosses the component boundary.
Restricted host-binding profiles let ordinary Moth values cross by value, while host code may not retain references into ordinary Moth storage and opaque handles represent foreign identities rather than Moth reference types.
Optional inferred transfer
Moth has no move syntax and no ordinary mandatory-consuming value operation. Inferred transfer is optional. When safe transfer isn't proven on every relevant path, the operation remains a borrow.
Immutable and mutable parameters may both receive inferred destruction responsibility at a proven final-use call site. Parameter access mode remains separate from optional ownership effect.
Source-visible lifetime consequences
- A shared alias blocks overlapping mutation until its last potential use.
- A live shared value returned by
get prevents mutation of the same map. remove returns the removed value under normal lifetime rules.- Aggregate storage retains shared references unless
copy is explicit.
Read next
For the formal lifetime-region rules, see Lifetime regions and escape validation.