Skip to content

feat(d5): D5.1b — per-call-site ownership-contract channel#113

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

feat(d5): D5.1b — per-call-site ownership-contract channel#113
PhysShell merged 3 commits into
mainfrom
claude/agenda-2rufsj

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 26, 2026

Copy link
Copy Markdown
Owner

P-005 D5.1b — the per-call-site ownership-contract channel

StreamReader(stream, leaveOpen: …) & friends are a per-call-site contract — the same constructor consumes or borrows depending on the bool literal — so they need a per-call effect channel, not a per-method summary. This slice builds that channel as a locally-testable mechanism.

What ships

The bridge pre-declares three fixed sink externs in every lowered Module:

_OWNERSHIP_SINK_EXTERNS = (
    ExternDecl("$consume",    [EffectParam(Effect.CONSUME,    "Disposable", 0)], None, 0),
    ExternDecl("$borrow",     [EffectParam(Effect.BORROW,     "Disposable", 0)], None, 0),
    ExternDecl("$borrow_mut", [EffectParam(Effect.BORROW_MUT, "Disposable", 0)], None, 0),
)

The extractor routes any call's per-argument ownership through them (call $consume [x]), and they resolve via the same collect_signatures + lower_call path as any contracted call — no new checker, no new flow lowering. Externs are declaration-only (never leak-checked themselves), and the $ prefix cannot collide with a real C# member name.

Transitive coherence

The summary solver also reads a forward-to-a-sink as a known transfer ($consume → must, $borrow* → no) via _SINK_PATH_ACTION. Without that, a param forwarded only to $consume would degrade to unknown and dilute to plain — so the channel now propagates transitively through first-party wrappers, not just at the direct call.

Tests (synthetic OwnIR, 137 → 141 bridge checks)

  • acquire x; call $consume [x]; use xOWN002 (ownership left at the call)
  • release after $consumeOWN002 (double-discharge)
  • acquire x; call $borrow [x] never released → OWN001 (borrow keeps ownership — no over-consume)
  • borrow-then-release → clean
  • param forwarded to $consume → method becomes a must-consumer → caller's later use is OWN002 (transitive)

Full suite green (run_tests.py exit 0), ruff + mypy --strict clean.

Out of scope (CI/C#-only)

The extractor emitting these sink calls from the bool literal / annotation — paired with an A/B sample on real extractor output — rides into D5.3 (Tier-B breadth).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added per-call-site ownership routing using dedicated sink operations for consume, borrow, and borrow-mutable, including correct behavior through wrapper forwarding and transitive paths.
  • Bug Fixes

    • Improved detection of use-after-consume and double-release scenarios.
    • Corrected borrow so it does not transfer ownership, while borrow-mutable preserves exclusivity without unintended downgrade.
  • Documentation

    • Updated D5 ownership-transfer notes with the shipped design and deferred transitive borrow-kind propagation.
  • Tests

    • Added P-005 coverage for D5.1b (direct and transitive wrapper cases).

Pre-declare three fixed sink externs ($consume / $borrow / $borrow_mut) in
every lowered Module so the extractor can route a call's per-argument ownership
through them. They resolve via the same collect_signatures + lower_call path as
any contracted call -- no new checker, no new flow lowering. The solver also
reads a forward to a sink as a known transfer, so the channel propagates
transitively through first-party wrappers.

This is the locally-testable mechanism for per-call-site contracts (BCL
leaveOpen, annotations); the extractor emitting the sink calls from the bool
literal is the CI/C#-only remaining piece.

Tests: use/double-release after $consume (OWN002), $borrow'd-then-leaked local
(OWN001), clean borrow-then-release, transitive propagation through a wrapper.

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: 45a184f5-3307-4aff-afa1-d751ed627ac2

📥 Commits

Reviewing files that changed from the base of the PR and between 129b73b and 25eb8bb.

📒 Files selected for processing (1)
  • docs/notes/d5-ownership-transfer.md
✅ Files skipped from review due to trivial changes (1)
  • docs/notes/d5-ownership-transfer.md

📝 Walkthrough

Walkthrough

This PR adds fixed ownership-sink externs to lowered OwnIR, routes forwarded calls through sink-specific path actions, and updates tests and notes for consume, borrow, and wrapper propagation behavior.

Changes

Ownership sink channel

Layer / File(s) Summary
Sink extern declarations
ownlang/ownir.py
OwnIR imports effect and extern AST nodes, declares $consume/$borrow/$borrow_mut sink externs, and includes them in lowered modules via externs.
Forward action resolution
ownlang/ownir.py
Forward targets now resolve through _forward_path_action, which returns sink-specific ownership actions for the declared externs and preserves generic forward handling otherwise.
D5.1b coverage and note
tests/test_ownir.py, docs/notes/d5-ownership-transfer.md
The tests assert consume, borrow, and wrapper-forward behavior, and the note records the shipped per-call-site ownership-contract channel.
Deferred borrow-kind propagation
docs/notes/d5-ownership-transfer.md
The note records that direct $borrow_mut keeps exclusivity, while transitive wrapper propagation is deferred.

Sequence Diagram(s)

sequenceDiagram
  participant to_module
  participant Module
  participant _build_skeletons
  participant _forward_path_action
  participant PathAction
  to_module->>Module: include $consume, $borrow, $borrow_mut externs
  _build_skeletons->>_forward_path_action: resolve forward target
  _forward_path_action->>PathAction: return dispose, borrow, or forward
  _build_skeletons->>PathAction: record parameter skeleton action
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#68: Extends the same cross-call ownership handoff and transitive disposal flow.
  • PhysShell/Own.NET#111: Modifies the same OwnIR-to-core bridge for ownership-transfer sinks and skeleton actions.
  • PhysShell/Own.NET#112: Updates the same forwarded-transfer path from OwnIR into interprocedural ownership contracts.

Poem

Hop hop, I carried the contract by night,
$consume and $borrow stayed snug and right.
A wrapper can waltz, and the trail still shows,
While my burrow keeps tidy the ownership flows. 🐇

🚥 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 is concise and accurately captures the main change: a D5.1b per-call-site ownership-contract channel.
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 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: 087214a73a

ℹ️ 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 Outdated
# `$borrow`/`$borrow_mut` is a kept loan — rather than a `forward` edge to an
# unsummarized callee (which the solver would degrade to `unknown`, diluting a real
# `must` to plain). Kept in sync with `_OWNERSHIP_SINK_EXTERNS`.
_SINK_PATH_ACTION = {"$consume": "dispose", "$borrow": "borrow", "$borrow_mut": "borrow"}

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 Preserve $borrow_mut through forwarded sink summaries

When a parameter is forwarded only to $borrow_mut, this map records the path as a plain borrow, so the solver returns Transfer.NO and _infer_param_effect lowers the wrapper's parameter to a shared borrow signature. Any first-party wrapper around a mutable-borrow sink therefore loses the exclusivity requirement: a caller passing a shared-borrow parameter is accepted through the wrapper even though a direct $borrow_mut call would require borrow_mut and the wrapper's internal OWN041 is later skipped by the bridge. The per-call-site channel should keep $borrow_mut distinct when propagating the contract transitively.

Useful? React with 👍 / 👎.

…ummaries (Codex P2)

The transfer lattice (no/must/may/unknown) carries no shared-vs-exclusive axis,
so the transitive sink shortcut mapping $borrow_mut to a plain borrow would
silently weaken an exclusive loan to a shared one in a wrapper's contract. Drop
$borrow_mut from the known-transfer map: it degrades to unknown -> the wrapper
param stays plain (no false shared-borrow claim). The direct call $borrow_mut
[x] channel is unaffected and keeps full exclusivity through lower_call.

Adds direct $borrow_mut channel tests (leak + clean) and documents the decline.

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 `@ownlang/ownir.py`:
- Around line 939-945: Preserve the mutable-borrow distinction in the sink
skeleton flow: `_SINK_PATH_ACTION` currently normalizes `$borrow_mut` to the
same action as `$borrow`, which causes `_build_skeletons()` and wrapper
inference to lose exclusivity semantics. Update the action/skeleton handling in
`ownlang/ownir.py` so `$borrow_mut` carries a mutability marker through the
layer instead of being collapsed to plain borrow, and ensure the wrapper path
still resolves to the `BORROW_MUT`-equivalent behavior used by the sink
inference logic.
🪄 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: 33d77baf-8607-474b-b4f1-2020c92e6a38

📥 Commits

Reviewing files that changed from the base of the PR and between 3523570 and 087214a.

📒 Files selected for processing (3)
  • docs/notes/d5-ownership-transfer.md
  • ownlang/ownir.py
  • tests/test_ownir.py

Comment thread ownlang/ownir.py Outdated
…D5.1c

The D5.0 summary solver is transfer-oriented and has no shared-vs-exclusive
axis, so it flattens borrow/borrow_mut into one bucket. The core checker already
distinguishes them (analysis.py _check_mut_borrowable vs _check_shared_borrowable),
so the direct $borrow_mut call keeps exclusivity; only the transitive wrapper
claim is lost, which D5.1b declines (precision-safe). Records the clean follow-up
shape -- an orthogonal borrow-kind axis on the summary, not another Transfer enum
value -- so the deferral is tracked, not buried (Codex P2 / CodeRabbit Major).

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