Compiler design / 12. Memory management and GC

GC answers liveness. Access validation answers conflicting use. Moth runs both on purpose.

Automatic memory management before the borrow checker

Heap values must stay usable while something can still observe them. They must also release resources without letting code touch storage that already died. Those two needs sound like one problem. They are at least two.

Garbage collection answers liveness: is this value still reachable? Access validation answers conflicting use: may two aliases overlap right now? Moth runs both on purpose.

The sentence to keep

Moth is reference-semantic by default, copy-explicit and move-inferred. It omits explicit reference types and lifetime syntax, not references themselves.

If you know Rust, that sentence is the pivot. Rust makes references and lifetimes part of the source surface. Moth still has shared aliases and exclusive mutation. It does not ask you to write &T, &mut T or lifetime parameters. The compiler tracks the relationships. You write smaller source.

Five memory strategies

Compare strategies by when storage ends, not by marketing labels.

Manual allocation. You call allocate and free. Maximum control. Maximum chance to free too early, free twice or forget entirely.

Reference counting. Each alias bumps a count. When the count hits zero, storage frees. Cycles need special care. Costs sit on every alias edge.

Tracing GC. A collector walks from roots and reclaims what it cannot reach. You stop writing most frees. Pauses and throughput become runtime concerns.

Ownership. Each value has a unique owner responsible for destruction. Transfers move that duty. Alias rules get strict. Deterministic drops become natural.

Region or arena allocation. Many values share a bulk lifetime. The region frees as a unit. Great for bursty work with clear epochs. Awkward when lifetimes tangle.

Real systems mix these. Moth's semantic baseline is GC. Ownership-aware lowering may ride on top when proof exists. A deferred grouped-memory surface (group / into) aims at arena-style bulk lifetimes later. It is accepted design with implementation still deferred.

Diagram placeholder: Five strategies show when values become reachable and reclaimable

Caption: teaching comparison. Question: who decides when storage ends in each model?

Failure modes

Name the hazards:

GC aims at lifetime hazards. It does not automatically bless conflicting mutation. Two aliases can both point at live memory and still disagree about who may write.

What GC solves

A tracing collector (or a host GC such as the JavaScript heap) tracks reachability from roots. Unreachable objects become reclaimable.

That property underwrites Moth's semantic promise: correctness does not depend on proving a deterministic drop on every path. If ownership analysis cannot prove a transfer, the value may remain GC-managed. Observable behaviour stays stable.

GC does not make this safe:


        -- conceptual: shared reader plus overlapping exclusive mutation
        -- both aliases can be live under GC and still conflict
    

Live memory can still host a logic hazard. Access rules still matter.

Diagram placeholder: GC tracks lifetime while borrow rules track conflicting access

Caption: exact conceptual separation. Question: why need both?

What ownership and borrowing add

Ownership thinking gives deterministic destruction and clearer resource release when the compiler can prove a last use. Borrow checking gives alias safety: many shared readers, or one exclusive writer, never both at once on conflicting places.

Rust packages those ideas into source-visible types and lifetimes. Moth refuses that source tax. There is no lifetime syntax and no move keyword. Moves are inferred. Copies are explicit. Existing values default to shared read-only access. Mutation needs explicit exclusive access through ~place.

Think of shared access as several vines reading the same stem. Exclusive access is one vine that may rewrite the stem. GC keeps the stem alive while anything still reads it. Borrow validation stops two vines from fighting over the same rewrite window.

Moth's semantic baseline

Hold these rules together:

  1. GC is a permitted runtime representation for a statically legal lifetime topology. It is not a legality escape for invalid retained edges or unproven topology.
  2. Borrow validation is mandatory, even under GC.
  3. Lifetime-region validation is mandatory after borrow validation. Two proof layers stay distinct: mandatory topology/access legality versus optional ownership optimisation.
  4. Shared access is the default for existing values. Binding, passing, returning or storing an existing value creates a shared alias unless exclusive access or explicit copy applies.
  5. copy creates an independent value on purpose. Ordinary assignment does not clone.
  6. ~place requests temporary exclusive access to an existing mutable place. It is not a move marker and not a type constructor.
  7. Moves are inferred at safe consumption points. Optional inferred transfer always backs off to borrowing when proof is unavailable.
  8. Ownership-aware lowering is optional optimisation layered over the same accepted programs.

GC-only and ownership-aware backends must accept and reject the same source programs. Optimisation may not widen or shrink the language. GC does not accept any lifetime graph merely because deterministic lowering cannot prove ownership.

Shared access is a source-semantic relationship. It does not require every scalar to be heap-allocated or represented by a pointer. A small integer can still follow shared-access rules at the language level while living in a register at runtime.

Diagram placeholder: Source access rules flow through HIR and validation into GC or ownership-aware lowering

Caption: accepted architecture simplified. Question: where can optimisation change without changing meaning?

How this differs from Rust and from plain GC languages

Versus Rust. Rust puts references and lifetimes in the type system you write. Moth keeps reference relationships in the compiler. Source stays smaller. You lose the ability to spell complex lifetime contracts in APIs. You gain APIs that do not carry lifetime parameters.

Versus JavaScript or Java. Those languages lean on GC and mostly leave alias conflicts to the programmer. Moth still validates shared versus exclusive access at compile time. GC alone is not the whole safety story.

Versus pure Rust-without-GC. Moth does not require every value to prove a unique owner at runtime. When proof is thin, GC remains correct. Ownership optimisation is a bonus, not a correctness requirement.

Backend choices

Targets pick runtime machinery:

Those choices implement one semantic contract. They do not redefine shared access or exclusivity. Ownership optimisation stays subordinate to language semantics.

Diagram placeholder: JavaScript host GC and Wasm runtime choices meet one semantic contract

Caption: backend design space, not a final implementation matrix. Question: what stays invariant across runtimes?

In the greeting project, strings and template values cross runtime operations. Under GC they remain safe while reachable. Under ownership-aware Wasm lowering they might drop earlier when proof exists. Priya's paragraph still means the same thing.

What can fail here

Why Moth does it this way

Alternative: expose Rust-style lifetimes, references and owned-or-borrowed signatures in every API.

Choice: keep GC as a legal representation for already-valid topology, validate access and lifetime topology separately, and optimise ownership when proof exists.

Benefit: safer source rules with a backend escape route when proof is thin. Source APIs stay readable.

Cost: collector work remains, and ownership-aware destruction cannot always prove a transfer.

Roadmap and current status

Deep detail lives under docs/src/docs/codebase/memory-management/. This article places the model on the chronological route.

Exit state

You leave with a memory model where GC preserves correctness and analysis enforces access rules. Next: how borrow validation computes those rules over HIR.

Compiler design / Previous / Next