Memory management / Declared regions
Moth is reference-semantic by default, copy-explicit and move-inferred. It omits explicit reference types and lifetime syntax, not references themselves.
A declared region is a hard semantic lifetime region. Values placed into a declared region belong to that declared region for their full lifetime. Accepted design with implementation deferred.
A declared region is a hard semantic lifetime region and one bulk cleanup domain. Values placed into a declared region belong to that declared region for their full lifetime. They may retain references to allocations owned by the same declared region or by a region statically known to outlive the declared region. No declared-region-owned value, projection, or alias may outlive the declared region.
Declared regions are the only source mechanism for reference cycles, and declared-region-owned storage is count-free. An overly broad declared region is a visible programmer choice, not a compiler failure.
Declared-region identity is semantic lifetime metadata. It is not a value, type, field, parameter, generic argument, trait, allocator object, or lifetime annotation, and it must not enter TypeId or a source signature.
Moth is reference-semantic by default, copy-explicit and move-inferred. It omits explicit reference types and lifetime syntax, not references themselves.
Accepted end-state design with implementation deferred. This page is the canonical semantic authority for name: / into name. Implementation sequencing, prerequisites and deferred optimisation investigations live in docs/roadmap/roadmap.md. Current implementation support lives in the progress matrix.
Declared regions extend, rather than replace, Moth's existing memory model:
copy.A garbage-collected representation must not weaken declared-region escape rules. A GC-native backend may ignore physical declared-region cleanup, but it must accept and reject the same declared-region source as every other backend.
Canonical general topology rules, including the one-owner invariant, the stored-edge outlives rule, narrowing and widening, cycles and projection families, live in docs/src/developer-docs/memory-management/lifetime-regions-and-escape-validation/.
A source and HIR scope that controls name visibility and control-flow exits.
A compiler-inferred lifetime owner for ordinary allocations. The compiler may merge, widen, split or physically ignore inferred regions when behaviour remains unchanged, subject to the nearest-existing-ancestor rule.
A declared lifetime region introduced by source name:. It is a hard lifetime boundary. The compiler must not silently widen it to make an invalid escape legal.
One possible backend representation for one or more lifetime regions. An arena is not a source value and is not synonymous with a semantic region.
A target representation that locates a runtime value, such as a JavaScript object reference, Wasm offset, pointer or table index.
The obligation to release or safely transfer storage. It is affine: it may move or be discharged, but it never duplicates. It does not prove uniqueness, and declared-region-owned storage never carries it individually.
The one semantic region responsible for keeping an allocation alive and eventually ending its lifetime.
These rules are locked accepted design.
A declared-region-owned allocation is never released before its declared region exits, even when last-use analysis proves that no alias survives. Early-drop optimisation is deliberately ignored for declared-region-owned storage: predictable bulk reclamation is the point of using a declared region.
This is what makes declared regions count-free. Because no declared-region-owned allocation can be released individually, its targets carry no independent obligation, so a declared region never needs a counter for the storage it owns. Outgoing edges into externally owned counted families are a separate matter and stay counted in those external families. See Retained Edge Counting for the counted strategy that declared regions deliberately avoid.
Count-free is a property of the target owner, not of every edge source located inside a declared region. A declared-region-owned target allocation family carries no REC counter. An internal retained edge to a target owned by the same declared region is count-free because the declared region's bulk exit already covers it.
declared-region-owned target:
count-free
edge from declared-region-owned storage to a target owned by that declared region:
count-free internal declared region edge
edge from declared-region-owned storage to an external REC family:
counted persistent obligation on the external target
at declared-region exit:
release outgoing REC obligations
-> bulk reclaim declared-region-owned storageAn external target is destroyed only if its own count reaches zero. Declared region teardown never destroys an external family directly.
A declared region is the only source mechanism for direct retained-edge cycles in a Moth program. Every member of a direct reference cycle must be placed into the same declared region.
Cycles inside one declared region are safe without a cycle collector because the declared region's bulk exit reclaims every member together, regardless of the edges between them. Cross-region cycles remain invalid, and the compiler never invents a cyclic inferred region to accept one.
Direct source construction of cyclic graphs remains deferred with the rest of declared-region implementation.
request:
parsed ParsedPost into request = parse_post(post)
html String into request = render_post(parsed)
;name [access/type] into region_name = expressionRules:
name: is valid only in runtime executable bodies.group keyword._: is invalid and does not create an anonymous declared region or lexical block.into region_name appears only on declaration receiving boundaries in V1.return, return!, break, recovery exit, and checked-operation failure path.The declared region name is local to the current executable body scope. It cannot collide with a visible value, type, dependency binding, constant, reactive source or active declared region. A declared region is not a value. Declared regions are invalid in constants, config, signatures, fields, choices, traits and export surfaces.
Ordinary declarations inside a declared region are not implicitly declared-region-owned. Only declarations with into region_name place fresh results or independent graphs into the declared region.
A declared region with no direct or straight-line nested placement targeting it emits an unused-region warning. A plain declaration does not count as placement merely because it is lexically inside the declared region.
Moth has no dedicated general lexical-block construct. block is an ordinary identifier, so block: has the same declared-region meaning as any other valid name: header. Exact _: is invalid, and the source language has no control-flow labels. Keyword-led semantic scopes such as async: remain separate because their keyword carries language-defined execution and lifetime behaviour.
parsed into scratch = parse_post(post)
parsed ParsedPost into scratch = parse_post(post)
rows ~{Row} into scratch = {}
maybe_name String? into scratch = find_name(id)?into region_name appears after access or type syntax and before =.
V1 has no expression-site placement. Prefer:
row Row into scratch = parse_row(raw)
~rows.push(row)Do not initially add:
-- INVALID: expression-site placement is not supported in V1
~rows.push(parse_row(raw) into scratch)Placement stays attached to closed receiving boundaries.
A declaration targeting an ancestor declared region is a narrow V1 escape mechanism, not a general definite-assignment system.
Canonical V1 rule:
if branches;catch branches;if true: remains authored conditional syntax and does not make ancestor placement legal.if, match, or catch.Valid nested placement:
request:
scratch:
parsed ParsedPost into scratch = parse_post(post)
html String into request = render_post(parsed)
;
use(html)
;Invalid branch placement:
request:
if condition:
html String into request = render_post() -- invalid in V1
;
use(html)
;Conditional replacement:
request:
html String into request = if condition:
then render_post()
else
then render_fallback()
;
;Invalid loop placement:
request:
loop posts |post|:
html String into request = render_post(post) -- invalid repeated declaration
;
;Loops must mutate a destination-owned aggregate instead of repeatedly declaring an ancestor-owned name.
An ordinary declaration's binding visibility follows its destination declared region. Visibility begins at the declaration point, not at the declared region opening. Name collisions are checked in the destination declared region scope. Ordinary declarations without into retain normal lexical visibility.
Define:
copy and WIT value lifting produce this stronger form.A declaration may be placed into a declared region when its result root is fresh and every retained edge is legal for the destination declared region, or when it is an independent result graph such as an explicit copy. An alias result cannot become new declared-region-owned storage through into.
A fresh result root may retain parameters or other pre-existing values only when every retained edge satisfies the destination lifetime's outlives constraints. An independent acyclic result graph can enter an unrelated destination lifetime without retained-edge constraints to its source graph. A WIT value-only result is an independent result graph, not merely a fresh result root. An explicit copy result is an independent result graph while preserving internal alias topology inside the copied graph. If that graph contains a cycle, the whole copied graph must enter one declared region. copy cannot create a cyclic inferred region.
Nested declared regions are valid.
request:
config Config into request = load_config()
scratch:
parsed ParsedPost into scratch = parse_post(post)
html String into request = render_post(parsed, config)
;
use(html)
;For a child declared region nested in a parent declared region:
| Retained edge | Rule | |---|---| | child value -> parent value | valid | | child value -> same-child value | valid | | parent value -> child value | invalid | | sibling value -> value owned by a sibling declared region | invalid | | child value -> unrelated shorter-lived value | invalid |
A declared-region-owned aggregate may retain:
It must not retain values owned by a child, sibling or otherwise shorter-lived region. A parent, sibling or otherwise longer-lived region must not retain a value owned by a child declared region.
A declared region has one lexical entry and explicit exits. Values placed into the declared region cannot escape it. The compiler must not silently widen a declared region. A child declared region ends before its parent.
A value crosses from a shorter-lived declared region into a longer-lived declared region only by producing independent storage in the destination lifetime or by invoking a fresh producer that allocates directly into the destination. V1 has no declared-region extraction, declared-region adoption or unrestricted declared-region transfer operation.
Use one of:
copy into the destination declared regionThe restriction on extraction means moving a declared-region-owned allocation out of its declared region or retroactively detaching an interior projection from its allocation family. It does not prohibit builtin collection or map remove, which kills a container-retained edge and returns the already-stored value under ordinary lifetime rules.
This avoids graph splitting, alias rewriting and drop-list reparenting.
Lifetime-region and escape validation must reject every path where a declared-region-owned value or alias can outlive the declared region.
Invalid escapes include:
Backend GC representation does not legalise these cases.
request:
scratch:
label String into scratch = [: temporary]
saved String into request = copy label
;
use(saved)
;A fresh producer may avoid the copy by allocating directly into the destination declared region.
An interior alias rooted in declared-region-owned storage cannot escape its declared region by itself.
load_name |id String| -> String, Error!:
scratch:
user User into scratch = load_user(id)!
return user.name -- invalid
;
;Use copy or produce a fresh value directly in a longer-lived declared region. Interior projections remain rooted in their containing allocation family; see the lifetime-regions leaf for the general projection rule.
V1 has no into placement on reassignment. A mutable binding already owned by a declared region may be reassigned only with a fresh result root valid for that same declared region, an independent copy allocated into that declared region, or a value already owned by the same declared region reused or rebound at a proven final use. Cleanup responsibility remains with the declared region.
A mutable binding declared into a declared region remains declared-region-owned. Reassignment does not let a declared-region-owned binding switch into a borrowed alias of ancestor, sibling or external storage. Use a separate ordinary alias binding for that purpose.
Declared-region-owned values passed to ordinary functions are normally passed as borrowed. The callee may read or mutate according to the source access contract, but it must not individually destroy the declared-region-owned allocation.
Functions whose result root is fresh may allocate that root directly into a caller-selected destination. The function summary classifies the result root as fresh and records retained-edge constraints separately.
A hidden destination:
A fresh-result-root summary and its retained-edge constraints must be backend-neutral parts of the function's semantic effect information.
Affine cleanup responsibility records which runtime path may perform individual cleanup. It does not prove uniqueness, identify the final observer or establish lifetime legality. A declared region owns release for its whole storage family, so a callee never receives individual cleanup responsibility for a declared-region-owned allocation and never releases one. Declared-region-owned handles are passed borrowed and uncounted.
Reactive storage can outlive the lexical function that creates it. V1 must reject:
A GC backend may use ordinary reachability for already-legal topology.
A future collector-free HTML-Wasm path should use builder-owned lifecycle regions such as:
Page region
├── page reactive state
├── Mount A region
│ ├── subscriptions
│ └── render-generation region
└── Mount B region
├── subscriptions
└── render-generation regionBuilder-owned regions obey the same retained-edge rule as source declared regions, but they are created and ended by the project runtime rather than ordinary source blocks. Builders cannot change source legality.
Declared region-specific diagnostics must identify:
Remedy order remains:
copy;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.
There is no backend-specific escape from semantic lifetime diagnostics. A GC-native backend may ignore physical grouping while preserving the source contract.
Deferred work includes:
Final-use interior projection detachment from an allocation family is not accepted current design and is not part of the first declared-region implementation. It remains deferred follow-up tracked in docs/roadmap/roadmap.md.