Skip to content

Commit 03c926e

Browse files
docs(adr): ADR-022 — origin variables + Polonius-style loan solver (Refs #177) — surgical refile of #398 (#407)
## Summary Surgical refile of #398. The original branch `core-01/polonius-adr` carried 386 commits' worth of phantom history (commits with main equivalents at different SHAs after squash-merges), and a naive cherry-pick onto current main would have clobbered the `linear_bindings` work landed today via #397. This branch carries **only** the 3 intended changes from #398: - `docs/decisions/0022-polonius-origin-variables.adoc` (347 lines) — long-form ADR - `.machine_readable/6a2/META.a2ml` (143 lines) — structured `[[adr]]` block - `lib/borrow.ml` (5 lines net) — single in-comment edit in the closing residual-TODO block, pointing readers at ADR-022 Owner ratified the architectural bundle 2026-05-27: OCaml Polonius solver in `lib/borrow_polonius/`, surface-syntax fully elided for v1, M1–M4 stage gates with lexical checker as merge oracle through M3. ## Closes Closes #398 (the stale-base original). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 02a5914 commit 03c926e

3 files changed

Lines changed: 494 additions & 1 deletion

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)