@@ -1471,3 +1471,146 @@ references = [
14711471 "hyperpolymath/typed-wasm (coordination target)",
14721472 "hyperpolymath/ephapax (second producer — coordination target)",
14731473]
1474+
1475+ [[adr]]
1476+ id = "ADR-022"
1477+ status = "proposed"
1478+ date = "2026-05-26"
1479+ title = "Origin (region) variables and a Polonius-style loan solver for the borrow checker"
1480+ context = """
1481+ lib/borrow.ml (~1585 LOC, OCaml) is the operational borrow checker. Slices
1482+ A, B, C-light landed plus the in-flight ref-to-ref / Slice C' / Slice D
1483+ PRs (#395, #396, #397) close out the lexical / operational fragment of
1484+ CORE-01 (#177). The remaining residual, called out in lib/borrow.ml's
1485+ closing TODO comment (lines 1575-1578), is the architectural shift to
1486+ *origin (region) variables*: a region var on each TyRef / TyMut with
1487+ subset constraints and a datalog-style loan-live-at-point solver.
1488+
1489+ The operational checker is fundamentally lexical: each borrow has a span
1490+ where it was created and lives until its containing block exits, with NLL
1491+ last-use shortening via merge_arm_results, the ref_bindings graph, and
1492+ block-local sym tracking. Lifetime variables do not appear in types —
1493+ `&Int` is `TyRef TyInt`, no `'a` parameter. The checker cannot reason
1494+ about cross-function borrows that outlive a returned struct field
1495+ (callee_owned_params is a name-set heuristic, not a constraint
1496+ discharge), true reborrows through indirection (the matching residual
1497+ TODO at lib/borrow.ml:1570-1574), or CFG-join soundness beyond what
1498+ merge_arm_results handles inline — each new join point (ExprHandle,
1499+ ExprTry, ExprMatch) is retro-fitted with snapshot-restore-merge.
1500+
1501+ STATE 2026-05-26 names this the dominant single item on the path to 1.0,
1502+ ~6-12 weeks single-author.
1503+ """
1504+ decision = """
1505+ Adopt a Polonius-style origin/region constraint solver written in OCaml
1506+ in lib/borrow_polonius/, staged M1-M4 with the lexical checker as the
1507+ merge oracle through M3.
1508+
1509+ Option survey:
1510+ - (a) Keep lexical + patch gaps — rejected; the next two soundness gains
1511+ (cross-fn reborrow soundness; multi-iter loop soundness without
1512+ spurious re-assignment rejection) are not expressible in the current
1513+ shape.
1514+ - (b) Custom origin-based solver in OCaml under lib/borrow_polonius/
1515+ using Polonius's published `dlv_naive` shape (subset/3,
1516+ loan_live_at/2, loan_invalidated_at/2, error/1). CHOSEN.
1517+ - (c) Embed polonius-engine via Rust FFI — rejected; new toolchain dep,
1518+ cross-language error reporting, and the estate language policy
1519+ reserves Rust for performance-critical systems code with no native
1520+ alternative. There is one here (OCaml).
1521+
1522+ Surface syntax: FULL ELISION for v1. `&T` / `&mut T` retain their spelling;
1523+ origins are introduced implicitly by the elaborator. Function signatures
1524+ the user writes today remain accepted; inference attaches and constrains
1525+ the origins. A future surface escape hatch (`&'a T`, `where 'a: 'b`) is
1526+ NOT introduced here — that is ADR-Future-A, contingent on grammar work
1527+ under ADR-012's Menhir-conflict-disclosure rule.
1528+
1529+ AST changes (lib/ast.ml):
1530+ - `type origin_var = int`, fresh-int identity.
1531+ - `TyRef of origin_var option * type_expr` (was `TyRef of type_expr`).
1532+ - `TyMut of origin_var option * type_expr` (was `TyMut of type_expr`).
1533+ - TyOwn unchanged.
1534+
1535+ borrow.ml changes:
1536+ - `type borrow` gains `b_origin : origin_var`.
1537+ - M1 defaults all sites to a single shared `default_origin = 0` ⇒
1538+ semantically identical to today's lexical checker.
1539+
1540+ Datalog shape:
1541+ - borrow_at(L, P) input
1542+ - loan_origin(L, O) input
1543+ - subset(O1, O2, P) input
1544+ - killed(L, P) input
1545+ - cfg_edge(P1, P2) input
1546+ - loan_live_at(L, P) derived (LFP over cfg_edge + killed)
1547+ - loan_invalidated_at(L, P) derived (loan_live_at ∩ access-conflict)
1548+ - error(P) derived (any invalidated)
1549+
1550+ Subset constraints emitted at: let-binding of a ref RHS, assignment to
1551+ a ref binder, function call sites (formal-actual origin flow). Solver =
1552+ naive bottom-up worklist; complexity O((P × L) + (E × L)) per iteration;
1553+ adequate for the AffineScript corpus. Migrate to Polonius opt_naive only
1554+ if a corpus turns pathological.
1555+
1556+ Migration plan (each milestone = own PR, all behind full dune runtest gate):
1557+ - M1: AST adds `origin_var option` field defaulted to None; b_origin
1558+ on borrow defaults to shared id 0. lib/borrow_polonius/types.ml stub
1559+ created, NOT wired into bin/main.ml. No behaviour change.
1560+ - M2: Elaborator emits fresh origins at each borrow site as
1561+ loan_origin facts; no subset constraints yet. No behaviour change.
1562+ - M3: Subset constraints from let/assign/call; solve.ml implements the
1563+ fixpoint; bin/main.ml runs BOTH checkers and diffs; new soundness-gain
1564+ fixtures land. CI gate: zero divergence on existing corpus.
1565+ - M4: Solver-only; lib/borrow.ml shrinks ~30-40% as ref_bindings /
1566+ block_local_syms / callee_owned_params / merge_arm_results retire.
1567+ CAPABILITY-MATRIX flips CORE-01 to fully closed; the residual TODO
1568+ comment is deleted.
1569+
1570+ Test surface: each milestone keeps `dune runtest --force` green. M3 lands
1571+ ≥5 new negative fixtures (programs Polonius rejects that lexical accepts —
1572+ the soundness gain). M4 lands ≥10 fixtures across both axes (Polonius
1573+ accepts what lexical spuriously rejects; Polonius catches what lexical
1574+ missed).
1575+
1576+ This ADR records the SHAPE. It is opened as Draft pending owner
1577+ ratification of the shape before any code-changing PR follows. On
1578+ ratification, M1-M4 land as separate PRs.
1579+ """
1580+ consequences = """
1581+ - Closes the last named base-language soundness gap (CORE-01 residual
1582+ Slice C-full) → ROADMAP Phase 2 unblocked, Phase 3 (1.0) unblocked.
1583+ - lib/borrow.ml shrinks; each new join construct stops requiring a
1584+ bespoke retrofit.
1585+ - The reborrow-through-indirection residual TODO is discharged by M3 as
1586+ a side-effect — subset constraints chain reborrows naturally.
1587+ - Future surface origins (Rust-style `&'a T`) become additive — the type
1588+ system already carries the variable, the syntax PR exposes it.
1589+ - Multi-week effort (~6-12 weeks single-author per STATE estimate).
1590+ Smaller CORE-01 slices, INT-03, and CONV-04 can proceed in parallel.
1591+ - The OCaml solver carries proof-debt of its own (fixpoint must be
1592+ sound). M3's parallel-run diffing is the empirical correctness gate;
1593+ no mechanised solver proof is attempted here.
1594+ - lib/ast.ml shape change ripples through every pass that destructures
1595+ TyRef / TyMut. Mitigation: M1's `option` keeps case-pattern surface
1596+ compatible with one-liner `_ ->` wildcards in passes that don't yet
1597+ care.
1598+ - No new build-time dependency. No new surface syntax. The
1599+ affinescript.ownership typed-wasm carrier section is unaffected —
1600+ origins are internal to the borrow checker, never serialised.
1601+ - This decision is PROPOSED. Owner ratification required before M1
1602+ lands. Once ratified, this ADR is settled; do not reopen without
1603+ amending it.
1604+ """
1605+ references = [
1606+ "https://github.com/hyperpolymath/affinescript/issues/177",
1607+ "lib/borrow.ml:1569-1584 (residual TODO comment)",
1608+ "lib/borrow.ml:1570-1574 (matching reborrow-through-indirection TODO)",
1609+ "lib/ast.ml (TyRef/TyMut/TyOwn definitions, lines 60-62)",
1610+ "docs/decisions/0022-polonius-origin-variables.adoc (long-form ADR)",
1611+ "docs/STATE-2026-05-26.adoc §Dominant cost item / §Critical path",
1612+ "docs/CAPABILITY-MATRIX.adoc (CORE-01 row)",
1613+ "docs/specs/SETTLED-DECISIONS.adoc (section pending on ratification)",
1614+ "ADR-012 (grammar changes are correctness assertions — gates future surface-origin ADR)",
1615+ "Niko Matsakis, \"An alias-based formulation of the borrow checker\", 2018 (subset/3 + loan_live_at/2 shape)",
1616+ ]
0 commit comments