Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/development/docs/control-flow-drop-state.md

Path-Sensitive Aggregate Cleanup

This document owns the compiler architecture for runtime aggregate cleanup state introduced by v0.11.0 Phase 7. Public move, drop, and assignment rules remain in the ownership specification; this document defines how accepted programs reach native IR without duplicating ownership analysis.

Completion Status

Phase 7 completed on 2026-08-10. Native lowering now uses this model for non-terminal branches, matches, loops, value control flow, and short-circuit conditions. The obsolete aggregate-specific buildability rejections were removed after focused IR and native exactly-once cleanup tests covered their accepted programs.

Responsibility Boundary

Type checking and ownership analysis decide whether a source operation is legal on every reachable path. IR lowering does not recover that proof and does not turn an invalid use into a runtime check. Its narrower responsibility is to emit destruction exactly when a value is live on the path that reaches cleanup.

A purely static DropObligation is sufficient for straight-line code. It is insufficient after a branch whose paths do not perform the same whole-value operation: cloned lowering contexts can describe each branch, but neither clone is the runtime state after the join. The aggregate local is therefore promoted to a runtime live flag only when lowering encounters a path-sensitive move, drop, or reinitialization.

State Model

An aggregate local retains its slot, layout, copy capability, destructor plan, and static partial initialization obligation. It may also retain one BoolLocation with this meaning:

FlagRuntime meaning
truethe local owns a complete value that requires its configured cleanup
falsethe local has been moved, destroyed, or has not completed initialization

Promotion allocates the flag once and initializes it from the current static obligation before control flow can diverge. Repeated promotion returns the same location and emits no second initialization. Aggregates that are copyable, have no destructor plan, or remain straight-line do not need the flag.

Partial initialization retains its existing field, payload, or prefix drop state. If a partially initialized construction can cross a new control-flow boundary, the outer live state and the existing fine-grained state compose; neither replaces the other.

Transition Placement

Runtime transitions are part of the operation that changes ownership:

  • successful complete initialization or reinitialization emits SetBool(true) after the value is established
  • a whole-value move emits SetBool(false) after the value has been transferred and before later cleanup can run
  • explicit destruction emits SetBool(false) after destructor and field cleanup complete
  • failed construction leaves the flag false and uses the existing partial-initialization cleanup
  • copy operations do not change the source flag

Expression lowering must place transitions where evaluation happens. In particular, a move in the right operand of && or || belongs inside that operand's conditional instructions. Appending one unconditional transition after the whole condition would incorrectly mark an unevaluated value as moved.

Cleanup

PendingAggregateDrop is the single boundary between local state and cleanup lowering. When it carries a live flag, the generated complete or partial drop program is wrapped in an IR condition on that flag. Normal scope exits, returns, propagated failures, break, continue, match cleanup, and replacement assignment all consume this same representation.

Syntax-specific lowering may request promotion or emit an ownership transition. It must not build its own guarded destructor sequence. The independent DestructSignature remains the sole source of destructor identity and generic substitution.

Control-Flow Integration

Before lowering a non-terminal control-flow construct, the compiler discovers outer aggregate locals whose state may change in its conditions or reachable blocks and promotes those locals in the parent context. Branch clones therefore reference the same flag location. Their static obligations can still diverge for local cleanup decisions; the parent does not guess a merged compile-time state.

Loops execute the same transitions on each real iteration. break, continue, and returns append cleanup through their established scope marks. Whether the source may read a value on a later iteration remains an ownership-analysis question.

Invariants

  • one aggregate slot has at most one runtime live flag
  • promotion precedes every path that can mutate the flag
  • a transfer clears the source only after the transfer has consumed its bytes
  • initialization sets the destination only after the complete value exists
  • cleanup reads the flag through PendingAggregateDrop; it never infers liveness from branch shape
  • branch cloning never allocates competing flags for the same outer local
  • native buildability does not reject a construct that ownership and lowering both represent
  • semantic declaration and type identities, never source spelling or standard-library names, determine aggregate and destructor behavior

Verification

Focused IR tests must assert promotion, transition order, short-circuit placement, guarded cleanup, and the absence of flags in straight-line code. Native tests must use observable destructors to cover taken and untaken if/match paths, loop zero/one/multiple execution, move arguments, bindings, assignments, explicit drop, reinitialization, early exits, and imported alias types.

The completed phase gate passed all 3,502 tests through the repository verification script, warnings-denied Clippy, formatting, documentation generation, and a clean diff check. The generated website contains 119 pages.