Compiler design / 13. Borrow validation and drops

Data-flow over HIR enforces shared and exclusive access, infers moves and records advisory drops beside unchanged HIR.

Borrow checking, race prevention and inferred destruction

Two aliases may read the same value in peace. An overlapping mutation can poison every reader's assumptions while the bytes still sit in live memory. GC will not save you from that story. Live storage can still host a logic bug. Borrow validation catches the conflict at compile time.

Access modes and roots

Moth's access rule in plain form:

Analysis reasons about places (locations), storage roots and alias root sets. A local, a field path or a temporary can participate. Fresh rvalues passed to mutable call slots materialise into hidden locals first so the checker sees ordinary local access.

Moth is reference-semantic by default, copy-explicit and move-inferred. Reading or binding an existing value creates a shared read-only alias. It does not implicitly copy.

Side example, then back to greetings:


        -- conceptual: two shared readers of message
        -- then an exclusive write through ~message while a reader still needs the old value
    

The pattern is the lesson. The greeting project itself may stay simple. The rule still guards every module.

Shared and exclusive aliases use non-lexical, control-flow-sensitive activity. A shared alias blocks overlapping mutation only until its last potential use on the relevant path. An unused shared alias does not block later mutation.

Diagram placeholder: Shared reads overlap while an exclusive interval excludes them

Caption: exact access rule simplified. Question: which overlap invalidates the program?

Data-flow facts

Borrow validation is a data-flow analysis over HIR's control-flow graph. It tracks facts such as:

On branches, facts split. At joins, they merge conservatively. On loops, the analysis revisits blocks until a fixed point: further iteration stops changing the facts.

Conservative means "when unsure, assume the more restrictive story." That can reject programs a smarter oracle might accept. It should not accept programs that are unsafe.

Borrow diagnostics distinguish concrete access conflicts from conservative access-analysis limitations. The later lifetime-region analysis separately distinguishes topology proven invalid from topology it cannot prove legal.

Diagram placeholder: Facts flow around a loop until the join stabilises

Caption: analysis simplification. Question: why revisit earlier blocks?

Cross-function access

Modules compile separately. The checker cannot always open a callee's HIR as local control flow when the call crosses a boundary.

Public function interfaces export summaries:

Cross-module call transfer consumes those summaries. Missing or inconsistent summaries are internal CompilerError failures. Binding-backed functions resolve through semantic package metadata, not through source import spelling.

Future use and inferred moves

There is no move keyword. The compiler asks a sharper question: may this value be consumed here without losing a later valid use?

Future-use analysis answers. If a later path still needs the value, consumption is illegal. If this site is the last valid use on the paths that matter, an inferred transfer may proceed.

Optional inferred transfer is an optimisation path. When proof is unavailable, the compiler backs off to borrowing. GC still keeps the value correct. Source legality never depends on a successful ownership proof.

That design keeps source APIs small. It also means analysis precision limits how often ownership optimisation can fire.

Diagram placeholder: Last valid use permits inferred transfer and later use forbids it

Caption: teaching rule. Question: when can ownership move?

Aggregates, copies and exclusive access

Existing values stored in structs, choices, collections, maps or templates are shared aliases by default. Storing a value does not automatically make the aggregate an independent owner of a duplicated child.

copy place creates independent value semantics on purpose.

~place requests exclusive mutation-capable access to an existing mutable place. It is not a move marker and not a type constructor. Fresh rvalues satisfy ordinary mutable parameters without source ~. Mutable receiver methods still need an existing mutable place.

Assignment through a mutable alias writes through to the referent. It must not silently detach or rebind the alias.

Drops and backend use

When ownership facts exist, the compiler may record advisory drop sites and operations in the spirit of drop_if_owned under a unified ownership ABI story.

GC-backed backends may erase ownership operations. They must not skip validation. The same programs remain accepted or rejected either way.

Borrow validation reads validated HIR and writes side tables. It does not rewrite HIR. Backends may consume the side facts. They may not treat absence of a drop as permission to accept unsafe aliasing.

Diagram placeholder: Analysis facts sit beside unchanged HIR and feed optional drop lowering

Caption: exact side-table boundary. Question: what may a GC backend ignore?

Reactive liveness

Observable state that a template subscription can still see must remain live for that observation model. Borrow validation treats subscriptions as read-only dependencies, not as active borrow lifetimes that freeze the whole heap in Rust-like ways.

Return to the greeting example: static content may fold away entirely. Dynamic reactive content, when present, keeps source and sink metadata honest through HIR and validation without inventing a second type system.

Future-concurrency context

Imagine two workers and one mutable place. If both obtain conflicting access, you have a race-shaped hazard. Moth's access rules reject the pattern at the language level even when today's runtime does not expose general threaded execution.

Label that example carefully. It teaches why exclusivity exists. It does not claim current async actor syntax you have not shipped.

Diagram placeholder: Two labelled workers access one mutable place, with a rejected overlap

Caption: general teaching example, not current runtime support. Question: why prevent the pattern before threads arrive?

Ownership bit and drops

The ownership bit records destruction responsibility only. It does not prove uniqueness, identify the final observer or establish lifetime legality. Advisory drop sites and operations in the spirit of drop_if_owned are weaker than validated release permission or lifetime topology.

There is no ordinary mandatory-consuming source operation today. Optional inferred transfer always backs off to borrowing when proof is unavailable. Path-dependent optional transfer does not reject the program merely because transfer is ambiguous.

Handoff to lifetime validation

Borrow validation supplies access, alias, optional-transfer and reactive invalidation facts. Lifetime-region and escape validation is a separate later analysis that decides one lifetime owner, retained-edge legality, escapes and cycles before module artefacts feed target planning. See docs/src/docs/codebase/memory-management/lifetime-regions-and-escape-validation/.

What can fail here

Do not claim current threaded or async execution as present tense support. Do not claim full lifetime-region topology analysis as already shipped if the progress matrix still separates it from current borrow checking.

Why Moth does it this way

Alternative: expose source lifetimes and move syntax.

Choice: mandatory HIR analysis, optional inferred transfers that back off to borrowing, and GC as a legal representation for already-valid topology.

Benefit: source APIs stay small while access safety remains explicit to the compiler.

Cost: analysis precision limits ownership optimisation. Topology that cannot be proved legal is rejected by the separate lifetime analysis.

Roadmap and current status

Exit state

You leave with validated access facts, optional transfer facts and advisory drop sites. Next: lifetime-region validation, then how these facts become one immutable module artefact the rest of the build can reuse.

Compiler design / Previous / Next