Skip to content

feat(d5.4): alias_join primitive + per-RID T4 wrap/adopt (step 1)#133

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

feat(d5.4): alias_join primitive + per-RID T4 wrap/adopt (step 1)#133
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 1 — the alias_join lowering

Builds on step 0's RID indirection (#129) to model T4 wrap/adopt: a factory returning a wrapper of an argument, or a ctor adopting an argument into an owning field. T4a (return-aliases-arg) and T4b (stored-in-owning-field) are the same fact — a new owning handle joins the arg's alias set — so this is one core primitive with two extractor recognisers (the recognisers land in step 2).

Core primitive (AST + CFG + analysis)

  • New AliasJoin(handle, src) instruction. handle becomes a new owning alias of src's RID: the two handles denote one obligation; src stays owning (unlike move, which invalidates the source). analysis.step just points handle_rid[id(handle)] at src's RID — no new resource is minted.
  • Because state lives on the RID (step 0), the existing per-RID checks now do T4 for free:
    • releasing/escaping through either alias discharges the one resource → clean
    • a leak of the shared RID is reported once (not per-handle)
    • releasing bothOWN003
    • using either after release → OWN002
  • Wired through build_cfg (lower_alias_join: src must be owned; handle inherits src's origin/type/kind so a per-RID finding attributes to the one resource) and codegen (var name = src; owning-alias emission, for exhaustiveness).

Bridge (ownir.py)

  • New alias_join flow op (var = new owning handle, src = adopted local) lowers to the core primitive. An alias_join over an untracked src makes no claim — optimistic-silent, the v1 must-only rule (§11).

The Dapper precision win (proven)

A factory that acquires inner locally, alias_joins a wrapper w, and returns w drops inner as a local — but its obligation escaped through w, so per-RID evaluation reports no leak. That is the own-only-0-with-a-reason case the whole D5.4 model exists for. Without the alias set this dropped local would be a false OWN001.

Tests

test_ownir.py gains 7 D5.4 cases (185/185 bridge checks): release-wrapper / release-inner clean, drop-both leaks once @ the inner origin, double-release OWN003, use-after OWN002, untracked-src silent, and the return-wrapper precision win. Full suite green; ruff + mypy --strict clean. The entire pre-existing corpus is unchangedAliasJoin is only emitted by the new flow op.

Honest scope

  • Still synthetic-driven: the extractor doesn't recognise wrapper/adopt sites yet (that's step 2 — the Roslyn recognisers that emit alias_join for BulkheadSemaphoreFactory-shaped adoption). So this fires on hand-authored facts, not real C# yet.
  • Straight-line only in v1: an alias minted inside one branch of an if that merges with a non-aliasing path is out of scope; the conflicting-merge raise in _join_handle_rid keeps that loud, not silently wrong.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for owning aliases so multiple handles can refer to the same underlying resource obligation.
    • Expanded wrapper/adopt handling so these patterns are recognized and compiled consistently.
  • Bug Fixes

    • Improved ownership/permission checking to track shared obligations across alias handles (including borrow/release/use-after-release and leak/double-dispose cases).
    • Returning a wrapper no longer causes incorrect leak reports for the inner value.
  • Documentation

    • Updated the D5.4 rollout plan with clarified shipped and upcoming implementation steps and test expectations.
  • Tests

    • Added D5.4 coverage for alias-sharing scenarios and new RID-layer checks for aliasing with borrows.

D5.4 step 1 — "add the alias_join lowering". Builds on step 0's RID indirection
to model T4 (wrap/adopt): a factory returning a wrapper of an argument, or a
ctor adopting an argument into an owning field. T4a (return-aliases-arg) and T4b
(stored-in-owning-field) are the same fact — a new owning handle joins the arg's
alias set — so this is ONE core primitive with two extractor recognisers (the
recognisers land in step 2).

Core primitive (AST + CFG + analysis):
- New `AliasJoin(handle, src)` instruction. `handle` becomes a new owning alias
  of `src`'s RID: the two handles denote one obligation; `src` stays owning
  (unlike `move`, which invalidates the source). `analysis.step` just points
  `handle_rid[id(handle)]` at `src`'s RID — no new resource is minted.
- Because state lives on the RID (step 0), the existing per-RID checks now do T4
  for free: releasing/escaping through EITHER alias discharges the one resource
  (clean), a leak of the shared RID is reported ONCE (not per-handle), releasing
  BOTH is OWN003, and using either after release is OWN002.
- Wired through build_cfg (`lower_alias_join`: src must be owned; handle inherits
  src's origin/type/kind so a per-RID finding attributes to the one resource) and
  codegen (`var name = src;` owning-alias emission, for exhaustiveness).

Bridge (ownir.py):
- New `alias_join` flow op (`var` = new owning handle, `src` = adopted local)
  lowers to the core primitive. An alias_join over an untracked `src` makes no
  claim — optimistic-silent, the v1 must-only rule (§11).

The Dapper precision win is proven: a factory that acquires `inner` locally,
alias_joins a wrapper `w`, and RETURNS `w` drops `inner` as a local but its
obligation escaped through `w` — so per-RID evaluation reports NO leak. That is
the own-only-0-with-a-reason case the whole D5.4 model exists to model.

Tests: test_ownir.py gains 7 D5.4 cases (release-wrapper / release-inner clean,
drop-both leaks once @ the inner origin, double-release OWN003, use-after OWN002,
untracked-src silent, return-wrapper precision). Full suite green; the entire
pre-existing corpus is unchanged (AliasJoin is only emitted by the new flow op).
Limitation documented: alias_join is straight-line only in v1 (the conflicting-
merge raise keeps a branched alias loud rather than silently wrong).

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

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: b4442f8d-c290-4e55-a77d-0ffb80d317f4

📥 Commits

Reviewing files that changed from the base of the PR and between 69209e7 and 62df059.

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

📝 Walkthrough

Walkthrough

Adds AliasJoin through documentation, AST/CFG lowering, RID-based analysis, code generation, and tests, while expanding the D5.4 rollout note with staged implementation details.

Changes

AliasJoin ownership flow

Layer / File(s) Summary
Contract and lowering
docs/notes/d5-ownership-transfer.md, ownlang/ast_nodes.py, ownlang/cfg.py, ownlang/ownir.py
AliasJoin is added to the AST and CFG, OwnIR lowers alias_join into it, and the D5.4 note now describes the staged rollout.
RID tracking and emission
ownlang/analysis.py, ownlang/codegen.py
The analyzer resolves loans and alias joins by RID, and inline codegen copies alias bookkeeping before emitting the alias assignment.
Regression coverage
tests/test_ownir.py, tests/test_rid.py
New tests cover alias_join release, leak, double-dispose, use-after-release, untracked source, and RID borrow interactions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PhysShell/Own.NET#129: Updates the RID-centric analyzer base that this PR extends with AliasJoin handling.

Poem

I hopped through the code with a whisker and grin,
Joined two little handles so one debt lives in.
If one bunny lets go, the other still knows,
The RID keeps the trail where the ownership goes.
🐇✨ Now my burrow of aliases glows.

🚥 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 and concisely summarizes the main change: introducing alias_join and per-RID T4 wrap/adopt behavior for step 1.
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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 355a932fd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ownlang/ownir.py
# tracked local; an unknown `src` makes no claim (precision-first).
name = str(n.get("var", "?"))
src_h = localmap.get(str(n.get("src", "")))
if src_h is not None and name not in hoisted:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear overwritten aliases with unknown sources

When an alias_join assigns to a local whose src is untracked, this guard skips the whole operation without removing any existing localmap entry for var. In a flow such as acquire w; alias_join w <- ghost; release w, the release is incorrectly applied to the old w obligation, so the overwritten resource is reported clean instead of leaking. Please kill the stale binding even when the new alias cannot be tracked.

Useful? React with 👍 / 👎.

Comment thread ownlang/analysis.py
# NOT touch `src`'s state (it stays owning, unlike a move). If `src` was
# already released/escaped, point at its RID anyway so a later use/release
# of `handle` resolves to that (released) RID and reports correctly.
st.handle_rid[id(ins.handle)] = st.rid_of(ins.src)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make alias loans follow the shared RID

After this maps the new handle onto src's RID, active loans are still recorded and queried by the original handle id (Loan.owner / loans_on), not by RID. That means alias_join w inner; borrow inner as b { release w; } is accepted even though releasing another owning alias of the borrowed resource should be OWN008, and the same gap applies to mutable-borrow/use checks through the other alias. The loan owner needs to resolve through the shared RID once aliases exist.

Useful? React with 👍 / 👎.

…ed RID (Codex)

Two Codex P2 findings on #133, both real and both precision-safe (they close
recall gaps; behaviour in the 1:1 non-alias case is unchanged):

1. alias_join overwrite with an untracked src (ownir.py). `acquire w;
   alias_join w <- ghost; release w` overwrote `w` with an alias whose src is
   not tracked, but the guard skipped the whole op WITHOUT dropping the stale
   `localmap[w]` — so the later `release w` discharged the original obligation
   and the overwritten resource read clean instead of leaking. Fix: drop the
   stale binding FIRST (same rule as the D5.2 call-result overwrite), even when
   the new alias cannot be tracked — the original now leaks OWN001.

2. alias loans follow the shared RID (analysis.py). A loan recorded its owner by
   handle id, so a borrow of one owning alias was invisible to the other:
   `alias_join w inner; borrow inner { release w }` missed OWN008, and the
   mut-borrow/use path missed OWN013. Fix: BorrowStart records the owner by
   `rid_of(owner)` and loans_on queries by `rid_of(owner)`, so a loan is seen
   through every owning alias of the resource. In the 1:1 case rid_of is
   identity, so the whole existing corpus is unchanged.

Tests: test_ownir.py gains the untracked-src overwrite leak; test_rid.py gains
two direct-CFG cases (release an owning alias of a borrowed RID -> OWN008; use an
owning alias of a mut-borrowed RID -> OWN013) — the bridge has no borrow op, so
the alias<->loan interaction is exercised on a hand-built CFG. Full suite green.

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

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 `@ownlang/codegen.py`:
- Around line 453-463: Clear any existing buffer bookkeeping for `st.name`
before creating the alias in the `A.AliasJoin` handling inside `codegen.py`;
this branch currently copies `buffer_cleanup` and `buffer_vars` from `st.src`
but does not remove stale state already attached to `st.name`, so a later
`release` can use the wrong cleanup. Mirror the shadowing cleanup behavior used
in `_stmt_inline(A.Let)` by dropping any previous `buffer_cleanup`/`buffer_vars`
entry for `st.name` before rebinding it, then continue copying the shared
obligation state from `st.src` as before.
🪄 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: b644f419-dbc4-4afd-b332-424946138a16

📥 Commits

Reviewing files that changed from the base of the PR and between ce2fee3 and 69209e7.

📒 Files selected for processing (8)
  • docs/notes/d5-ownership-transfer.md
  • ownlang/analysis.py
  • ownlang/ast_nodes.py
  • ownlang/cfg.py
  • ownlang/codegen.py
  • ownlang/ownir.py
  • tests/test_ownir.py
  • tests/test_rid.py

Comment thread ownlang/codegen.py
…gen (CodeRabbit)

CodeRabbit Major on #133: the `A.AliasJoin` codegen branch copied buffer_cleanup/
buffer_vars from `src` but did not drop any stale entry already attached to
`st.name`. If the same name previously belonged to a branchy buffer, a later
`release {st.name}` could emit that stale buffer cleanup instead of disposing the
aliased resource. Mirror the shadowing-cleanup the Let branch already does: pop
buffer_cleanup/buffer_vars for `st.name` before rebinding. (Codegen-only; no
surface syntax emits AliasJoin yet, but kept consistent and correct.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
@PhysShell PhysShell merged commit 23ba55c into main Jun 27, 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