Research / Formal and PL core

P04 · short · draft

Ownership Analyses That Ride the IR: Unique Accumulation and Owned/Borrowed Teardown Obligations

partial short Formal and PL core

State of the work. The accumulation analysis is on main with deterministic element-copy counts on Python and TypeScript, and the owned/borrowed checks have tests; no timings, no soundness argument and no real-code coverage numbers exist, and shared handles are specified but not built.

This is a draft, not peer reviewed. Repository references link to revl at commit 45bec431f.

Pitch

Decide once, in the frontend, whether a name uniquely owns its object, stamp that fact on the IR, and let each backend choose what to do with it. On the Python backend this took accumulation loops from quadratic to linear copying, and three of six backends read the marker today.

Abstract

Compilers that target several runtimes face a repeated problem: a source-level aliasing fact is needed on every target, and each backend re-derives it. In revl, a language compiled to six targets, out = out.push(x) is a persistent write that copies its receiver, so the loop a programmer wrote as linear was emitted as quadratic on each audited target that copies on write (Python, Go and TypeScript). Two backends had independently written the same unsoundness-prone aliasing proof. We moved it into the frontend as a flow-sensitive forward dataflow with a fixpoint on loop back edges, plus a whole-program retention summary for calls, and stamp the result on the IR as a fact (unique: true or "copy"), not an instruction. On the Python backend, element copies when building a 1000-element list drop from 500500 to 1000, growth per doubling from x4.0 to x2.0, and the library list_sort and list_dedup reach the hand-written baseline in copy count. The TypeScript and Go backends read the same marker; Rust discharges the obligation with its borrow checker; Java and WebAssembly decline it for named representation reasons. We also describe a second ownership analysis on the same pattern, over the resource teardown accumulator: acquired handles are nominal opaque types (R0), the declared inverse cannot be hand-called (O1), and a seven-clause check keeps borrows from escaping (B1). The first analysis is on main and measured by deterministic element-copy counts (no timings); the second ships for owned and borrowed modes, with shared handles specified and deferred.

Problem and claim

Two separate ownership questions recur in revl. For accumulation: is the object this local names reachable through any other name at this write? For resources: which component owns the inverse of an acquired handle, and can any other position close it or keep it? Both are questions about the source program, not about a target language, and both have a wrong answer that is silent: an aliasing bug that changes a value, or a double close.

The claim of the first half: computing the uniqueness fact once, flow-sensitively and per write, in src/revl/ownership.py, is designed to fail safe, with every uncertainty resolving to not owned. That safety is argued from the fallback and the test suite, not proved. The analysis gives backends a shared fact in place of per-backend proofs. Ignoring the marker is always correct, which is what lets backends adopt it independently. The claim of the second half: owned and borrowed modes can be inferred over the teardown accumulator with no new grammar, and a does-not-escape check over the existing scope structure is enough for v1.

These are two analyses with different machinery (a dataflow over locals, a nominal-type and escape discipline over handles). They share a design principle (compute in the frontend, ride the IR or the checker, keep the conservative direction) but not code beyond a common statement of aliasing. The paper either leads with the first half and treats the second as a second application, or is split.

Contributions

  1. A flow-sensitive forward dataflow (lattice FRESH < COPY < absent, join is maximum, births kill escapes, escapes gen) that decides per write whether a self-rebinding assign owns its object (src/revl/ownership.py:1-111).
  2. A marker protocol that carries a fact and not an instruction: unique: true, unique: "copy" with unique_birth on the introducing let; absent when no write qualifies, so IR for other programs is byte-identical (src/revl/ownership.py:27-47).
  3. A whole-program retention summary (least fixpoint over the call graph, optimistic start) that answers whether a callee retains a parameter, closing the list_dedup case (src/revl/ownership.py:617-678).
  4. Measurements on the Python backend with a deterministic element-copy counter that rewrites the emitted module's AST (bench/codegen/python/copycount.py:1-30): 500500 to 1000 at n=1000, list_sort 171700 to 5050 at n=100 and 1353400 to 20100 at n=200, list_dedup 5050 to 100 at n=200.
  5. A backend-by-backend account of what the marker buys and where it is declined: TypeScript and Go read true only, Python also reads "copy", Rust relies on its borrow checker, Java's list container is immutable, and Wasm's list header has no capacity field.
  6. For resources: R0 (acquire returns are nominal opaque handles), O1 (no hand-call of a declared inverse, owner or borrower), B1 (a borrow does not escape, seven clauses), with owned and borrowed landed and shared teardown specified for a counted-grant ledger and deferred (docs/design/308-effect-ownership-modes.md:1-22, :130-220, :398-440).

Evidence on main

ClaimWhereStatusNote
Frontend analysis exists, flow-sensitive with loop fixpoint, conservative fallbacksrc/revl/ownership.py:1-111 (701 lines)shippedCalled from lowering: src/revl/lower.py:9446 (ownership.annotate_ir)
Marker semantics: unique, "copy", unique_birthsrc/revl/ownership.py:27-47shipped
Python backend honours true and "copy"backends/python/emit.py:4364-4381; tests/test_py_inplace_accumulation.py (16 tests, for example test_push_loop_becomes_append:77, test_local_born_off_a_name_takes_one_defensive_copy:135)shipped
Flow-sensitivity case: escaped accumulator re-born from a literal is owned againtests/test_py_inplace_accumulation.py:440shippedThe list_sort shape
Library push loops are lineartests/test_py_inplace_accumulation.py:118shipped
Aliasing negatives (escape keeps the copy, parameter never written, iterating keeps the copy)tests/test_py_inplace_accumulation.py:249, :376, :392shipped
Retention summary, extern calls always retaintests/test_py_inplace_accumulation.py:258, :286, :310, :330shipped
TypeScript honours true, declines "copy"backends/typescript/emit.py:3754-3802; backends/typescript/test_unique_writes_ts.pyshipped
Go honours true onlybackends/go/emit.py:7821-7826, :8148shippedThe Go lowering predates the frontend marker; its tests were not opened for this write-up
Self-host reimplementation's IR functions section equals the reference (parsed-JSON equality) on the whole lower corpusselfhost/lower.rvl:29569 and the own_* block; tests/test_selfhost_lower_ir.py:221, :980 (CORPUS_UNIQUE_AWARE = CORPUS, so every corpus document, not an accumulation-only subset)shippedThe earlier strict-xfail was deleted
Self-host emitters read the markerselfhost/emit_py.rvl:1269-1290; selfhost/emit_ts.rvl:1512-1524; selfhost/emit_go.rvl:1826shipped
Rust, Java, Wasm do not read the markerno unique reads in backends/rust/emit.py, backends/java/emit.py, backends/wasm (grep); reasons in docs/v2.0-roadmap.md:1860-1861shipped (as a documented non-adoption)Java and Wasm were first "not audited here" (line 1859), later given concrete blockers
500500 to 1000 element copies, x4.0 to x2.0 growthdocs/v2.0-roadmap.md:1754, :1769; bench/codegen/python/copycount.py; reproduce with PYTHONPATH=src python bench/codegen/python/run.pyshippedNo timings were taken or quoted, by the audit's own choice
list_sort 171700 to 5050 (n=100), 1353400 to 20100 (n=200); list_dedup 5050 to 100, 20100 to 200docs/v2.0-roadmap.md:1857-1858shippedCounts match the hand-written arm
TypeScript: 79800 to 0 element copies at n=400docs/v2.0-roadmap.md:1862; backends/typescript/test_unique_writes_ts.py:1-20shippedThe TypeScript bench in bench/codegen/typescript was not opened
R0, O1, B1 checks, owned and borrowed v1docs/design/308-effect-ownership-modes.md:1-22; src/revl/resources.py:1-40; tests/test_ownership_modes_308.py (49 tests, for example test_r0_primitive_acquire_return_refused:73, test_o1_borrower_close_refused:111, test_b1_clause2_closure_capture_refused_owner_too:167, test_owner_holds_and_lends_admits:279)shippedO1 and B1 live in src/revl/lower.py per the resources.py header
Method-scope early release decided, refused shape stays refusedtests/test_ownership_modes_308.py:328-408; tests/test_ownership_f9_runtime_308.py (5 tests)shipped
Retention audit, report onlytests/test_ownership_modes_308.py:437-487shipped
Shared and transferdocs/design/308-effect-ownership-modes.md:3-22; tests/test_ownership_modes_308.py:510-630designed / partialThe shared marker is carried and checked at acquire; the counted-grant ledger teardown "stays deferred" (design doc line 15)
Closure capture decision: by-value only, reference capture refuseddocs/closures.md:1-60; tests/test_v2_emit.py:364shippedPinned by test_arrow_captures_var_by_value_at_creation_time

Numbers that exist

Paper outline

  1. Introduction: one aliasing proof written twice, six backends; why a fact on the IR.
  2. The marker protocol: unique, "copy", unique_birth; why a fact and not an instruction; byte-identical IR when absent.
  3. The analysis: lattice, birth, escape, self-rebind kills, loop fixpoint, conservative fallback list.
  4. Calls: retention summary as a least fixpoint; extern, host root and function-value callees always retain.
  5. Backends: what each does with the marker (Python, TypeScript, Go lower it; Rust relies on rustc; Java and Wasm blockers).
  6. Evaluation: copy counts, growth curves, timings (to be added), real-code coverage, fuzzing.
  7. Resource ownership as a second application: R0, O1, B1, owned and borrowed, shared deferred; closure capture decision (by value only) as the rule that keeps both analyses simple.
  8. Related work and limits.

Threats to validity and limits

Fact-check

Claims in this draft were checked against revl main at commit 45bec431f: 50 checked, 11 corrected.

Cite this draft

@misc{revlp04,
  author       = {Thomas Moussajee},
  title        = {Ownership Analyses That Ride the IR: Unique Accumulation and Owned/Borrowed Teardown Obligations},
  year         = {2026},
  howpublished = {\url{https://inso1337.github.io/revl/research/P04.html}},
  note         = {Research draft, not peer reviewed. Written against revl commit 45bec431f. Licensed CC BY 4.0.}
}

Text licensed CC BY 4.0, author Thomas Moussajee.

← All research drafts