Skip to content

feat(d5.4): RID indirection in the core flow analysis (step 0, no-op)#129

Merged
PhysShell merged 3 commits into
mainfrom
claude/agenda-2rufsj
Jun 26, 2026
Merged

feat(d5.4): RID indirection in the core flow analysis (step 0, no-op)#129
PhysShell merged 3 commits into
mainfrom
claude/agenda-2rufsj

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 26, 2026

Copy link
Copy Markdown
Owner

D5.4 step 0 — the de-risking refactor (deliberately a no-op)

D5.4 (T4 wrap/adopt) is where Dapper/Polly-style "a factory returns a fresh disposable, the caller adopts it into an owning field" gets modeled. Per the design's three-commit cadence (docs/notes/d5-ownership-transfer.md §7 + §11), the core change is split so it can't silently break everything. This is step 0: move resource state from per-binding to per-RID, 1:1, behaviour byte-for-byte unchanged.

What changed (ownlang/analysis.py)

  • State.var is now keyed by RID (resource id), not by handle identity. A RID is the obligation carrying the {OWNED,MOVED,RELEASED,ESCAPED} state; a handle (local/param Symbol) denotes a RID through the new State.handle_rid map.
  • State.rid_of(sym) resolves a handle to its RID, defaulting to id(sym) for an un-aliased handle. Choosing RID == id(sym) is what makes this a no-op: every var key is the same int it was before, so _sym_by_id resolves a RID straight back to its symbol and leak attribution is unchanged.
  • State.mint(sym) binds a handle to a fresh resource (its own RID) — the 1:1 acquire. Every var write (Acquire/AcquireBuffer/MoveInto-dst/owned-param) goes through mint; every var read/overwrite (Release/Return/consume/state checks) through rid_of.
  • Loans stay keyed by handle id — borrow machinery is not in the alias-set model in v1 (the design's owning-only constraint, §11).
  • join() now also joins handle_rid via _join_handle_rid, which unions agreeing maps and asserts on a conflicting one — locking the step-0 single-mapping invariant the same way join already locks the block-scoped-loan invariant.

Why a no-op is the point

This PR delivers zero new detection — by design. It's pure indirection so step 1 (alias_join: a second owning handle joins an existing RID; per-RID leak/double/use-after evaluation) is a small, safe diff against a tested invariant rather than a behavioural change. Step 2 then turns on the extractor branches that emit aliasOf:i for verified wrapper/ctor-adopt sites (the BulkheadSemaphoreFactory shape the Polly oracle surfaced).

Proof it changed nothing

The entire green corpus passes unchanged — 19/19 corpus, 6/6 wpf, 20/20 loops, 23/23 spec, 169 bridge, 37 D5.0. ruff + mypy --strict clean.

tests/test_rid.py (new, 11 checks, wired into run_tests) pins the RID layer directly:

  • rid_of 1:1 default + no recording until mint
  • mint records the mapping and returns id(sym); distinct handles → distinct RIDs
  • copy() preserves handle_rid
  • _join_handle_rid unions disjoint/agreeing maps and asserts on a conflict
  • end-to-end: a 1:1 acquire still leaks OWN001, releases clean, double-releases OWN003

Refs docs/notes/d5-ownership-transfer.md §7, §11 (D5.4 step 0).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved resource ownership/loan tracking so analyses remain consistent even when the same resource is referenced through different handles.
    • Updated move/acquire/release/return and leak/permission checks to use this stabilized routing, reducing incorrect diagnostics and improving detection.
  • Tests

    • Added a new RID (resource-id) indirection test suite covering step-0 invariants, state copying, state-merge behavior, and end-to-end leak scenarios.
    • Included the new suite in the main test run and overall pass/fail evaluation.

D5.4 (T4 wrap/adopt) lands in a three-commit cadence so the core change is
de-risked. This is step 0: the no-op identity refactor that moves resource
state from per-binding to per-RID, with a 1:1 handle->RID mapping. Behaviour
is byte-for-byte unchanged — the whole green corpus is the proof — and step 1
(alias_join: a second owning handle joins an existing RID) now has a tested
foundation instead of having to re-derive the invariant under a behavioural
change.

What changed in ownlang/analysis.py:
- State.var is now keyed by RID (resource id), not by handle identity. A RID is
  the obligation carrying the {OWNED,MOVED,RELEASED,ESCAPED} state; a handle
  (local/param Symbol) denotes a RID through the new State.handle_rid map.
- State.rid_of(sym) resolves a handle to its RID, defaulting to id(sym) for an
  un-aliased handle. Choosing RID == id(sym) is what makes this a no-op: every
  var key is the same int it was before, so _sym_by_id resolves a RID straight
  back to its symbol and leak attribution is unchanged.
- State.mint(sym) binds a handle to a fresh resource (its own RID) — the 1:1
  acquire. Every var write (Acquire/AcquireBuffer/MoveInto-dst/owned-param)
  goes through mint; every var read/overwrite (Release/Return/consume/state
  checks) through rid_of. Loans stay keyed by handle id (borrow machinery is
  not in the alias-set model in v1, per the design's owning-only constraint).
- join() now also joins handle_rid via _join_handle_rid, which unions agreeing
  maps and asserts on a conflicting one — locking the step-0 single-mapping
  invariant the same way join already locks the block-scoped-loan invariant.

tests/test_rid.py (new, wired into run_tests): pins rid_of's 1:1 default, mint's
recording, the join union/assert, and end-to-end that a 1:1 acquire still leaks
(OWN001) / releases clean / double-releases (OWN003) — i.e. the indirection
changed nothing observable.

Refs docs/notes/d5-ownership-transfer.md sections 7 and 11 (D5.4 step 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 44582d22-80f9-41b3-88d3-f328f353fb6e

📥 Commits

Reviewing files that changed from the base of the PR and between bd0c97f and 03c72e4.

📒 Files selected for processing (1)
  • tests/test_rid.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_rid.py

📝 Walkthrough

Walkthrough

Ownership analysis now resolves symbol state by RID instead of handle identity. The transfer, release, return, leak, and escape paths were updated, and a new RID test suite was added to the test runner.

Changes

RID indirection in ownership analysis

Layer / File(s) Summary
RID-backed state model
ownlang/analysis.py
State stores handle-to-RID mappings, resolves handles through RID lookups, and joins those mappings at control-flow merges.
RID-based ownership transitions
ownlang/analysis.py
Ownership checks, leak detection, acquire, move, release, return, and escape updates read and write state by RID.
RID test coverage
tests/test_rid.py, tests/run_tests.py
A new RID test module checks lookup, minting, joins, and analysis outcomes, and the test runner includes its exit code.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A bunny hopped through RID-lit trails,
With minty hops and tiny nails.
The leaks went poof, the tests ran true,
And whiskers twitched at every clue.
🐰

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing RID indirection in core flow analysis as a step-0 no-op refactor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agenda-2rufsj

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_rid.py (1)

118-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a RID regression for the return/escape path.

These end-to-end checks cover leak/release, but the refactor also changed Return and the leak exclusion logic in ownlang/analysis.py. A bug in the RID-based escape path would still pass this suite, despite the step-0 “no behavior change” goal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_rid.py` around lines 118 - 129, Add a regression in
tests/test_rid.py that exercises the RID-based return/escape path, since the
current end-to-end cases only cover acquire/release and can miss bugs in the
Return flow and leak exclusion logic in ownlang/analysis.py. Extend the existing
_codes/_check-style assertions with a small function that returns or escapes a
connection so you can verify the expected RID outcome stays unchanged after the
refactor. Reference the Return handling in ownlang/analysis.py and the test
helpers _codes and _check to keep the new case aligned with the current suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ownlang/analysis.py`:
- Around line 137-141: The RID-merge check in the handle-to-RID merge logic
relies on assert, which is stripped under optimized Python and can silently
preserve the wrong mapping. Replace the assert in the merge path around the
handle/out check with an explicit runtime validation that always executes and
raises a clear exception when a handle maps to two different RIDs, using the
existing merge code in analysis.py to keep the D5.4 step-0 invariant loud.

---

Nitpick comments:
In `@tests/test_rid.py`:
- Around line 118-129: Add a regression in tests/test_rid.py that exercises the
RID-based return/escape path, since the current end-to-end cases only cover
acquire/release and can miss bugs in the Return flow and leak exclusion logic in
ownlang/analysis.py. Extend the existing _codes/_check-style assertions with a
small function that returns or escapes a connection so you can verify the
expected RID outcome stays unchanged after the refactor. Reference the Return
handling in ownlang/analysis.py and the test helpers _codes and _check to keep
the new case aligned with the current suite.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5013c9a4-4180-42a3-acca-fc797fd2dfa4

📥 Commits

Reviewing files that changed from the base of the PR and between 865d518 and 3c70fc2.

📒 Files selected for processing (3)
  • ownlang/analysis.py
  • tests/run_tests.py
  • tests/test_rid.py

Comment thread ownlang/analysis.py Outdated
…(CodeRabbit)

CodeRabbit review on #129:
- Minor: `_join_handle_rid` used `assert` for the conflicting-mapping invariant,
  which `python -O` strips — a wrong mapping would then be kept silently,
  defeating the "must be loud" step-0 contract. Replace with an explicit
  always-on `raise AssertionError` (same exception type, so any caller is
  unaffected). Verified it still raises under `python -O`.
- Nitpick: the end-to-end RID checks only covered acquire/release, but the
  refactor also routes the `Return` state-write and `leak_check`'s `exclude`
  through `rid_of`. Add two regressions: returning an owned resource escapes
  its RID clean, and a *sibling* RID left owned still leaks OWN001 before the
  return — proving the exclude spares only the returned RID, not all of them.

No behaviour change to the analysis; this hardens the invariant and the test net.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_rid.py`:
- Line 145: The check-count summary is stale because `run()` now makes 12
`_check(...)` calls while `n` is still hardcoded to 13. Update the total used by
the `test_rid.py` runner logic to match the actual number of `_check`
invocations in `run()`, and preferably derive it from the executed checks
instead of keeping a fixed constant so it cannot drift again.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 47b8952f-b740-4b3c-93b3-52d8a204308f

📥 Commits

Reviewing files that changed from the base of the PR and between 3c70fc2 and bd0c97f.

📒 Files selected for processing (2)
  • ownlang/analysis.py
  • tests/test_rid.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • ownlang/analysis.py

Comment thread tests/test_rid.py Outdated
…abbit)

The return/escape regressions took the suite to 12 `_check` calls but the
summary still printed a hardcoded `n = 13` — reporting a pass count that never
ran. Per CodeRabbit's "better" suggestion, derive the total from the checks that
actually executed: `_check` records each outcome in a module list and `run()`
reports `len(_RAN)`, so adding or removing a check can never desync the count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
@PhysShell PhysShell merged commit 0c71d8e into main Jun 26, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants