Skip to content

P-022 step 6b: implement Rust own-bridge with layered OwnIR parity #259

Description

@PhysShell

Parent: #250
Recommended tier: strong
Blocked by: #258
Preferably after: #255 and #256

Goal

Implement rust/crates/own-bridge as the production OwnIR-to-core boundary, following the normative contract from #258 and preserving Python behavior exactly.

The bridge consumes OwnIR facts, validates and lowers them into the Rust core, performs interprocedural MOS inference, drives own-analysis, and maps correct analysis results into the common diagnostic model.

Production dependencies

The crate may depend on:

own-ir
own-syntax
own-cfg
own-analysis
own-diagnostics

It must not create cycles or move presentation/analysis logic into the bridge.

Checkpoint status

Per the status-drift rule in #250, this body is a status surface and moves together with docs/proposals/P-022-rust-core-migration.md and the proposals index. The list below gained a 4b at the cp4 review: none of cp1–cp5 named the obligation-protocol analysis, while final acceptance requires it. Reconciled at 21fb0c3 (PR #341 merge) — final acceptance reached; closed by hand at that merge.

State of this issue, for the record. It was closed by automation when PR #339 merged: GitHub's keyword parser read that PR's own sentence "does not close #259" as close #259 — negation is not parsed — linked the PR in the Development panel, and closed the issue on merge while its final acceptance was still open. It was reopened, and it is now closed by hand, at the #341 merge, because final acceptance is reached. A checkpoint PR references an issue with Refs #N and never places a closing keyword before the number, negated or not — an issue's open/closed bit is a status surface too.

checkpoint status
1 — typed OwnIR validation complete — no known strict-door divergence (PR #325); a fourth census at final acceptance (PR #341)
2 — fact lowering complete
3 — interprocedural MOS complete for the stage-1 scalar-metadata domain
4 — analysis wiring complete at the checkpoint-4 surface (PR #336)
4b — protocol analysis (OBL001–005) complete — Layer 3 parity over the measured set, protocol family included (PR #340)
5 — full fact-to-verdict parity complete at the cp5 surface — every Finding member, every refusal in full, the rendered surfaces byte for byte (PR #339)
final acceptance reached (PR #341) — the coordinate-domain contract landed Python-first and mirrored in the port; the exclusion ledger names only the two OD-1 door controls (#294)

The wording this issue has earned, and no more: #259 final acceptance reached: Layer 3 parity over the full #258 family inventory at the full Finding and the rendered surfaces; declared boundary: the two OD-1 door controls (#294), measured, not open work. Not "shadow mode" (that is #260's acceptance, blocked on its own two decisions), not "P-022 done", not "Rust is the default" (#262's cutover). Counts live only in docs/generated/; the record is docs/notes/p022-bridge-verdict-final-acceptance.md.

Checkpoint 1: typed OwnIR validation

Implement:

  • serde types for the current schema;
  • version checks;
  • additive optional-field handling;
  • unknown vocabulary failure rules;
  • reference/location validation;
  • deterministic loading;
  • safe errors for untrusted input.

Acceptance

  • all valid Python fixtures accepted;
  • all invalid controls rejected with matching error class/category;
  • no panic on malformed JSON/OwnIR;
  • limits or defensive parsing documented for externally supplied inputs.

Status: complete via PR #325 — 216 controls, nothing excluded, no known strict-door divergence. The detail worth keeping (three censuses; a ledger that agreed with its own author; a taxonomy taken from where the reference raises rather than from the mechanism violated) is recorded in #250 and in P-022. A fourth census landed with final acceptance (PR #341): the coordinate domain flipped the ledger's accept-twins at the signed-64 edges and at a negative line into Location rejections, with the old why quoted beside the new one, and added a domain control per line-bearing field at every nesting shape. The count is generated, never typed: p022-cp1-census.md. The ledger compares accept/reject and, on rejection, the category — never the message text; that is deliberate on both sides (tests/test_ownir_validation_fixtures.py, own-ir/tests/validation_replay.rs) and final acceptance did not move it.

Checkpoint 2: fact lowering

Implement:

  • OwnIR functions/types/resources;
  • handle/RID minting;
  • facts → AST/CFG;
  • local map and kill-on-rebind;
  • branches, loops and merge preparation;
  • source-location preservation;
  • normalized lowered representation for differential testing.

Acceptance

  • Python-authored lowering fixture matches exactly/canonically;
  • ordering and ID permutations that are non-semantic do not alter output;
  • no analysis algorithm appears in the bridge.

Status: complete — the rust_replay cases are byte-exact. Since PR #341 the Layer 3 replay additionally asserts the §4.2 coordinate domain at Layer 2 over every replayed case.

Checkpoint 3: interprocedural MOS

Implement the #258 contract for:

  • Fresh/Borrow/Consume/Forward;
  • mixed and unknown summaries;
  • return skeletons and parameter effects;
  • overload sig resolution and fallback;
  • canonical names;
  • recursion/cycles;
  • BCL fresh-factory knowledge;
  • first-party/external behavior.

Acceptance

  • every mapped Python MOS test family has a frozen differential control;
  • generated call-graph controls cover cycles, overloads and fallback;
  • unknown behavior remains conservative;
  • output is deterministic.

Status: complete for the stage-1 scalar-metadata domain — the *.summaries.json goldens are byte-identical. Container-valued metadata is declared outside that domain (a separate #294-class door decision), not a silent gap.

Checkpoint 4: analysis wiring

Feed real bridge facts into:

  • ownership;
  • lifetime;
  • buffer policy;
  • effect;
  • DI.

The bridge must preserve analysis-selected primary anchors. It must not duplicate effect or DI graph algorithms.

Required controls

  • EFF001 through real OwnIR facts;
  • DI001–DI005 through real OwnIR facts;
  • DI004 call-site anchor;
  • DI005 cache-site anchor;
  • duplicate-site last-wins;
  • multi-file ordering;
  • ownership/lifetime cases already pinned by .own parity where representable through OwnIR.

Status: complete at the checkpoint-4 surface — via PR #336. Deliberately not "verdict parity complete": the claim was parity at the cp4 surface, over a measured set whose complement was named.

own_bridge::check_facts runs real OwnIR facts through the ported analyses (check_module: ownership, lifetime, buffer policy; the DI and effect finders from own-analysis) and maps ERROR-tier verdicts back onto fact handles through the verdict's subject, keeping the reference's map-or-raise refusal and the analysis-selected anchors (BR-V1/V2/V3/V5, BR-P1/P2, the OWN050/051/052 side paths, tiering and suppression BR-V6, dedup and the stable sort BR-V7/V8). Landing it needed one core change: own-analysis now stamps subject exactly where analysis.py/lifetimes.py do.

The comparison surface at cp4 was identity, anchor, kind and tiering — over the frozen Layer 3 ledger (tests/fixtures/verdicts/); cp5 widened it to every member (below). The differential over the replayed set is asserted, not restated here: the Rust replay compares every replayed case against its golden, collects every divergence without fail-fast, and fails the build if one exists — so a green replay is the claim. What sat outside that set was a named exclusion ledger the replay executes, not a silent gap, and two of its three families have since been promoted:

One declared comparison boundary on refusals at cp4: the map-or-raise text was compared up to its message= member, because the core's messages were still titles. Removed at cp5.2 — refusals now compare in full.

The measured census is not typed on any status surface — it is generated from the ledger by scripts/render_checkpoint_status.py and held in sync by the Python test gate: docs/generated/p022-cp4-census.md, with the recorded mutation campaign in docs/generated/p022-cp4-mutations.md.

Steady-state command (zero Python): cd rust && cargo test -p own-bridge --test verdicts.

Checkpoint 4b: protocol analysis (OBL001–005)

Port ownlang/obligations.py — the obligation-protocol analysis — and admit the protocol-bearing documents into the measured set.

This checkpoint did not exist in the original list, and its absence was the gap: none of cp1–cp5 names the protocol analysis, while Final acceptance below requires the full #258 family inventory, which includes the protocol rows (BR-P3). obligations.py is a path-sensitive analysis in its own right — lattice, walker, matching — so it is a checkpoint of its own rather than a fourth job for cp5, whose scope is messages, evidence and rendering. Folding a whole analysis family into the last checkpoint would have made that checkpoint a bag.

Acceptance

  • OBL001–005 produced from real OwnIR facts through the bridge;
  • the protocol-bearing documents move out of the exclusion ledger into the replayed set;
  • no protocol algorithm is duplicated in the bridge.

Status: complete via PR #340Layer 3 parity over the measured set, protocol family included.

  • OBL001–005 come from real OwnIR facts through the bridge. The analysis is own-analysis/src/obligation.rs (BR-B1 — the analysis owns its verdict): the {OPEN, CLOSED} set lattice with min-line provenance, the opens → closes → barriers leaf order with allow beating barrier, the never-invent asymmetry of an opaque write, exits anchored at the acquire, the loop's silent fixpoint and single emitting pass, the close-line evidence and the four-part sort key. The bridge maps BR-P3 in its BR-V1 place (after effects, before OWN050): the (kind, definite) table, the four deliberately line-free wordings, the identity derivations, the anchorless advisory OBL005 and the opened → barrier (→ late close) slice. refuse_protocols is gone.
  • Both protocol-bearing reference documents are promoted out of rust_replay_excluded into the replayed set, and neither golden was regenerated to get there.
  • No protocol algorithm lives in the bridge, and there is one grammar. own-ir/src/protocol.rs grew from validate-only to validate-and-construct, so the strict door and the analysis consume the same typed Protocol/MethodEvents values and cannot drift into two readings of one grammar; no strict-door error text or category moved (the cp1 ledger's protocol controls stayed green). Since PR feat(ownir): bound source coordinates to int32 and mirror it in the bridge (#259 final acceptance) #341 the grammar takes a Door: type and representability stay grammar and still skip a malformed entry whole; only the coordinate domain follows the door.
  • Oracle. A third analysis-level fact-parity family beside DI and effects: tests/test_obligation_fact_parity.py runs the reference's own check_protocols/unmatched_scopes and freezes every violation whole into tests/fixtures/obligation_fact_parity.json; own-analysis/tests/obligation_parity.rs replays the same raw documents with zero Python, so each side builds the values with its own half of the grammar. Synthetic Layer 3 and rendered controls close every row the corpus could not reach — the corpus had reached exactly one shape — insertion-stable.
  • Measured and recorded rather than claimed (note §6.7). BR-V5's "a slice shorter than two steps is dropped" is not applied on the protocol path: a leak off the end of a method carries a one-step slice; the port reproduces the reference, and whether the spec sentence or the code moves is a Python-first decision still owed (it does not block this issue). The family's append position is unobservable end to end, because the BR-V8 sort key's code component decides first. The tolerant door's protocol rules (skip a malformed protocol, skip a malformed method entry, first-wins on a duplicate name) are reachable end to end and pinned by replay — unlike the OD-1 effect-entry case cp4 recorded.
  • One golden family regenerated, for a stated reason: the shadow artifact and trace of the protocol document, whose Rust verdicts layer moves from refused to produced — the same shape of change feat(bridge): #259 cp5 — the full Finding, the refusal text and the rendered surfaces, proven #339 recorded for partialfull. The own-shadow reducer still refuses Layer 3: P-022 step 7a: add dual-engine shadow mode and zero-diff reproduction artifacts #260's boundary, unmoved.
  • Evidence, generated never typed: p022-cp4-census.md, p022-cp5-inventory.md, p022-cp4b-mutations.md; the checkpoint note.

Checkpoint 5: full fact-to-verdict parity

Compare normalized:

path
line
code
severity
message
subject
resource_kind
ordered Evidence

After #256, also compare canonical report JSON and SARIF.

Status: complete at the cp5 surface via PR #339Layer 3 parity over the measured set at the full Finding and the rendered surfaces. No pre-existing golden was regenerated or edited.

  • 5.0 — inventory. tests/verdict_surface_inventory.py: one ledger row per BR-V4 wording branch with who owns the string, per BR-V5 evidence family and degradation rule, per BR-V9 rule, each matched against the committed goldens so coverage is computed from the tree. A row at zero over the replayed set is a branch that must get a control, not one that may be declared. Generated: p022-cp5-inventory.md.
  • 5.1 — the full Finding. own_bridge::Finding carries message, related, flow; the BR-V4 matrix and the BR-V5 slice builders are ported; the replay compares every member against the goldens as committed. Branches no facts document can reach are pinned by controls whose expected text is read from tests/fixtures/unreachable_branches.json — the reference's own output, recorded by tests/test_unreachable_branch_probe.py (re-runnable; one source of truth, so the two sides cannot drift into agreeing with each other instead of with Python).
  • 5.2 — the refusal text. own_cfg::Diag carries the resolver's message the BR-V3 map-or-raise line interpolates; the message= cut is gone and refusals compare in full. Removing the cut exposed a port defect it had been hiding — py_repr quoting unconditionally with ' where CPython switches to " — fixed in Rust and pinned against CPython's own output. The unported remainder is an Option: a code whose text is missing renders as its title, and the first golden refusing on it goes red demanding the message.
  • 5.3 — the rendered surfaces (BR-V9). ownlang/renders.py (an observer beside verdicts.py), tests/fixtures/verdict_renders/ under its own frozen ledger, and a Rust replay comparing the bytes — SARIF key order is part of the surface, so the port emits through typed structs. codeFlows reuses own_diagnostics::code_flow; relatedLocations deliberately does not (the core's builder drops an empty-file step, the reference's inline comprehension does not — a golden pins the difference).
  • 5.4 — surfaces and campaigns. Re-run on a clean tree: p022-cp5-mutations.md. The cp4 campaign re-run against the cp5 tree turned its BR-V7 dedup-key mutations from caught to survived — putting message in the key made several members unobservable at the output — so those controls now drive dedup directly. Worth keeping: a comparison surface that gains a member can lose controls for the members it subsumes.

What cp5 did not claim, and what has happened to each since: the protocol family (landed at 4b), the u32 coordinate boundary (decided and landed at final acceptance, PR #341), the OD-1 door controls (a declared boundary, unchanged). own-shadow was not extended to Layer 3 — its verdict capture declares full, and the reducer still refuses the layer.

Final acceptance: the coordinate-domain contract (PR #341)

The last item final acceptance owed was a contract decision, not one more port: §4.2 bounded a source coordinate at signed 64 bits while the core holds a line as u32, and four verdict_boundary_* controls sat in the exclusion ledger because of the gap. The decision was the owner's and it landed Python-first, the way #326 did:

  • D1 — the domain. Every line is an integer in [0, 2147483647]; 0 is legal and means "unknown / file-level", because it is the reference's own default for an absent line and the corpus carries it everywhere; a negative line is rejected (no producer emits one); every column is in [1, 2147483647] or absent. The bound is int32 because int32 is the line type of every consumer this project feeds (Roslyn, LSP, .NET diagnostics) — never "Rust holds u32, so the reference is wrong".
  • D2 — where. The strict door (load()) rejects an out-of-domain coordinate as Location, for every line-bearing field — including the two §4.2 had recorded as validated nowhere (components[].subscriptions[].line, the flow-op line at every nesting shape), which now get type validation too. The representable-form rule (signed 64 bits, Shape) is unchanged and checked first; keeping the two axes apart is what stops a domain violation being filed as a shape one.
  • D3 — the tolerant door. check_facts() degrades an out-of-domain line to 0 and a column to absent — degrade, never clamp — exactly the §4.1 column precedent. The Rust side mirrors it: own-ir at the strict door and in the protocol grammar (a Door parameter, not a second parser), own-bridge in its lowering and readers. The four boundary controls left rust_replay_excluded with goldens regenerated by Python; the ledger now names only the two OD-1 door controls.

Measured rather than claimed (note §5–§6): the Rust column path had no u32 boundary to find; ast::core_line's degrade is unreachable end to end because the lowering degrades one layer earlier, so its control is a direct one that says so; a tolerant column reader on the Rust side had no upper bound and gained one so the two doors cannot disagree; the cp1 ledger compares category, never message text, and that surface did not move. Evidence, generated never typed: p022-cp1-census.md, p022-coord-census.md, p022-coord-mutations.md, with every earlier campaign re-run against the new tree.

Three tails from the final-acceptance review are carried to #260, not left here: own-bridge has two identical private tolerant line readers (lower.rs, verdict.rs) that should be one; the final-acceptance note's §3.2 diff stat predates its docs commit; and a campaign's expected_catchers can rot silently between runs (shadow-cp4's M61 did, after 4b), which --validate does not detect.

Fixture layers

  1. validation acceptance/rejection;
  2. normalized lowered representation;
  3. MOS summaries;
  4. final diagnostics;
  5. report/SARIF where available.

All fixtures are Python-authored. Rust steady-state tests invoke zero Python.

Guardrails

  • No schema redesign.
  • No new diagnostic behavior.
  • No bridge-side repair of wrong analysis locations/codes.
  • No duplication of ownership/lifetime/effect/DI algorithms.
  • No CLI cutover.
  • No performance pass before parity and profiling.
  • Python remains authoritative.

PR/checkpoint packet

Each checkpoint reports:

Python-only:
Rust-only:
Changed:
Ordering-only:
Unexplained:
Fixture changed:
Acceptance changed:
Production DAG changed:

Unexplained must be zero before proceeding.

Final acceptance

#259 final acceptance reached: Layer 3 parity over the full #258 family inventory at the full Finding and the rendered surfaces; declared boundary: the two OD-1 door controls (#294), measured, not open work.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions