Skip to content

Bridge: compositional ownership contracts on the C# side (P-006/2b)#36

Merged
PhysShell merged 5 commits into
mainfrom
claude/zen-pasteur-76hfs1
Jun 19, 2026
Merged

Bridge: compositional ownership contracts on the C# side (P-006/2b)#36
PhysShell merged 5 commits into
mainfrom
claude/zen-pasteur-76hfs1

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Bridge: compositional ownership contracts on the C# side (P-006/2b)

Bring the inter-procedural island to C#-derived facts. A method's ownership
CONTRACT -- its parameters' effects -- now lowers through the bridge, and a call
flow op lowers to the core's Call, so cross-method ownership transfer is checked
compositionally by the SAME analyze() the .own DSL uses. No new checker, no
whole-program analysis: the callee's signature is the cut point.

Mechanism (reusing the existing engine end to end):

  • a function fact may carry params with an effect
    (consume/borrow/borrow_mut/plain). _lower_fn_params encodes each as the
    TypeRef collect_signatures reads (resource-typed value => CONSUME, borrowed =>
    BORROW/BORROW_MUT), so the core signature table resolves the contract.
  • a call flow op lowers to A.Call; build_cfg.lower_call resolves each arg's
    effect from that signature -> Invoke; analyze applies it (consume => the arg
    escapes, a later use is OWN002; an undischarged owned param leaks OWN001).
  • a consume param is an owned obligation in the callee, exactly as in .own --
    the release obligation moves IN from the caller.

handoff_contract.facts.json proves it through the real CLI:
archive(consume s) -> clean (obligation moved in, discharged)
leak() -> OWN001 (never discharged)
run() -> OWN002 (use after the handoff consumed s)
run_ok() -> SILENT (correct handoff is NOT a false leak, though it
never releases: verified against archive's
contract, not its body)

This is the .own corpus case ownership-handoff-consume, now reproduced on
C#-shaped facts -- the compositional island on the product (Track 2b) side.
Additive + zero regression (gated on the new optional params/call). ownir
67/67, full suite, ruff, mypy --strict green.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED

Summary by CodeRabbit

  • New Features
    • Added region-escape diagnostics for dependency-injected event sources with OWN014, including correct routing based on DI lifetime information.
    • Expanded inter-procedural ownership handoff analysis for consume-style contracts, improving detection of use-after-handoff and leaks across calls.
  • Bug Fixes
    • Improved diagnostic accuracy by attributing parameter-related ownership/borrowing findings to stable parameter origins.
  • Documentation
    • Added and clarified ownership handoff guidance with stream-handling examples.
  • Tests
    • Updated and added fixtures to cover DI-capture scenarios and inter-procedural handoff expectations.

claude added 4 commits June 18, 2026 16:06
Bump the push-triggered oracle sentinel to re-run the cross-tool comparison
(Own.NET vs CodeQL vs Infer#) on corpus/fixtures/systemevents-console after the
OWN001 -> OWN014 migration for static-event subscriptions (PR #35). The fixture's
subscription leak now reports as OWN014; with OWN014 in the comparator's OWN_LEAK
set it must still land "Own.NET-only" (the oracles have no subscription-leak
query). Confirms the differentiation survived on real tool output, not just the
--selftest fixtures. Eval-only; no product code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
…-006 + P-004)

Slice 1 of injected-tier -> region: prove the region engine can catch a zombie VM
whose event source is a DI-injected dependency, by sourcing the source's lifetime
from the registration graph instead of a local annotation.

.NET DI lifetimes ARE a region order (singleton=Process > scoped > transient), so
the bridge extends the capture region chain and, for an injected subscription
carrying `source_type`, looks the type up in the `services` graph and routes it
through the SAME check_lifetimes engine:
  - source provably outlives the subscriber (singleton vs an un-registered
    Window/VM, or singleton vs scoped) -> OWN014 captive/region escape;
  - provably shorter-or-equal (transient) -> proven SAFE, silent (no honest-warning
    hedge once the lifetime is known);
  - unresolved source_type / no services -> unchanged OWN001 warning.

Additive + zero regression: gated on the new optional `source_type` field, which no
current fact/extractor emits. ownir 64/64, full suite, ruff, mypy --strict green.
Verified end-to-end through the CLI on di_capture.facts (singleton IEventBus ->
OWN014; transient IProbe -> silent).

Answers whether the model can reason about lifetimes it cannot know
intra-procedurally: yes, from the DI registration graph. Slice 2 (extractor emits
source_type + the subscriber's registration; real-code type-resolution risk) next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Add a real-world corpus case demonstrating cross-procedure ownership transfer
verified compositionally against a callee's contract -- the "island" mechanism,
end to end, on running code rather than prose.

`archive(s: Stream)` takes a resource-typed param by value => a CONSUME contract:
the release obligation moves INTO archive across the call. The case shows all
three faces in one file:
  - leak()   -> OWN001  (linear must-discharge: owned, never released/handed off)
  - run()    -> OWN002  (affine: use after ownership was consumed by archive)
  - run_ok() -> silent  (handoff DISCHARGES the caller's obligation -- no false
                         leak, though run_ok never calls release, because it is
                         verified against archive's signature, not its body)

The cross-procedure proof composes two intra-procedural checks glued at the
`consume Stream` cut point -- modular against the signature, exactly like Rust's
borrow checker, no whole-program points-to. before.cs/after.cs carry the real C#
shape (an IDisposable sink that owns and disposes its input); notes.md keeps the
standard honesty caveat (case.own is a hand reduction, not ingested C#).

corpus 5/5, full suite, ruff, mypy --strict all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Bring the inter-procedural island to C#-derived facts. A method's ownership
CONTRACT -- its parameters' effects -- now lowers through the bridge, and a `call`
flow op lowers to the core's Call, so cross-method ownership transfer is checked
compositionally by the SAME analyze() the .own DSL uses. No new checker, no
whole-program analysis: the callee's signature is the cut point.

Mechanism (reusing the existing engine end to end):
  - a function fact may carry `params` with an `effect`
    (consume/borrow/borrow_mut/plain). _lower_fn_params encodes each as the
    TypeRef collect_signatures reads (resource-typed value => CONSUME, borrowed =>
    BORROW/BORROW_MUT), so the core signature table resolves the contract.
  - a `call` flow op lowers to A.Call; build_cfg.lower_call resolves each arg's
    effect from that signature -> Invoke; analyze applies it (consume => the arg
    escapes, a later use is OWN002; an undischarged owned param leaks OWN001).
  - a consume param is an owned obligation in the callee, exactly as in .own --
    the release obligation moves IN from the caller.

handoff_contract.facts.json proves it through the real CLI:
  archive(consume s) -> clean (obligation moved in, discharged)
  leak()             -> OWN001 (never discharged)
  run()              -> OWN002 (use after the handoff consumed s)
  run_ok()           -> SILENT  (correct handoff is NOT a false leak, though it
                                 never releases: verified against archive's
                                 contract, not its body)

This is the .own corpus case `ownership-handoff-consume`, now reproduced on
C#-shaped facts -- the compositional island on the product (Track 2b) side.
Additive + zero regression (gated on the new optional `params`/`call`). ownir
67/67, full suite, ruff, mypy --strict green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fee01a3-8a69-4e0a-b48b-cf0d79698b7f

📥 Commits

Reviewing files that changed from the base of the PR and between b76ef81 and babe5fc.

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

📝 Walkthrough

Walkthrough

Two features are added to the OwnIR bridge: (1) DI-lifetime region-escape detection that maps DI service lifetimes to regions and lowers unreleased DI-sourced subscriptions as capture handles emitting OWN014 findings; (2) inter-procedural ownership-contract lowering that validates effect parameters, builds contract local maps, and emits call nodes to detect OWN001/OWN002 handoff errors. Both features are backed by new corpus cases, fixtures, and regression tests.

Changes

DI-sourced region escape (OWN014)

Layer / File(s) Summary
Parameter origin tracking
ownlang/cfg.py
Function parameters receive stable {name}#{line} origin identity during CFG construction, enabling diagnostic attribution to the correct parameter definition.
DI region machinery
ownlang/ownir.py
Module docstring documents DI integration; AST imports extended for Call, Expr, VarRef; region/lifetime helpers added for DI lifetime-to-region mapping and subscriber-region selection; load() validates optional subscription source_type field.
DI-aware capture lowering
ownlang/ownir.py
to_own() and to_module() compute DI-derived subscriber region, create di_source_life-annotated capture handles for unreleased DI-sourced subscriptions, emit region-scoped EventSource params, and update lifetime annotations.
OWN014 rendering
ownlang/ownir.py
check_facts() recognizes di_source_life annotations and emits tailored captive/region-escape Finding with OWN014 kind.
DI capture fixture and regression tests
tests/fixtures/ownir/di_capture.facts.json, tests/test_ownir.py
di_capture.facts.json defines DiZombie module with components and service registrations. Tests assert OWN014 for singleton-lifetime DI sources, silence for transient, and OWN001 fallback when DI graph is absent.

Inter-procedural ownership handoff (OWN001/OWN002)

Layer / File(s) Summary
Ownership contract and call-flow lowering
ownlang/ownir.py
load() validates function-parameter effect values. _lower_fn_params() generates synthetic contract-derived Params and populates localmap. Flow-body lowering consumes localmap to resolve call argument VarRefs.
Ownership handoff corpus case
corpus/real-world/ownership-handoff-consume/before.cs, .../after.cs, .../case.own, .../expected-diagnostics.txt, .../notes.md
before.cs defines Archiver with Archive/Leak/Run/RunOk demonstrating leak and use-after-handoff bugs. after.cs shows corrected versions. case.own encodes OwnLang consume-contract model. expected-diagnostics.txt declares OWN001/OWN002. notes.md documents the pattern and checker behavior.
Handoff fixture and regression tests
tests/fixtures/ownir/handoff_contract.facts.json, tests/test_ownir.py
handoff_contract.facts.json defines HandoffBridge with four function contracts. Tests assert OWN001 for leak, OWN002 for run (use-after-handoff), silence for correct archive and run_ok paths, and verify use-after-handoff message rendering. Regressions validate undischarged parameters and non-subscription resource scoping.
Oracle comment update
corpus/oracle-target.txt
Comments updated to reflect OWN014 replacing OWN001 for fixture subscription leak, Own.NET-only requirement for #1, and expected distribution of dispose leaks across result buckets.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PhysShell/Own.NET#28: Modifies the same check_facts path in ownlang/ownir.py for injected-source OWN001 messaging and severity, directly overlapping with the OWN014 rendering branch added here.
  • PhysShell/Own.NET#35: Implements static-event += lowering to capture to trigger OWN014 in the same region-escape pipeline extended here for DI-sourced captures.
  • PhysShell/Own.NET#15: Modifies per-function "flow body" ingestion/lowering (P-016) in ownlang/ownir.py, directly overlapping with the handoff/call lowering work in this PR.

Poem

🐇 Hop, hop, through the ownership maze,
Where streams are consumed and regions ablaze!
DI lifetimes mapped to regions with care,
OWN014 fires when escapes fill the air.
Handoff the stream, then touch it no more—
The rabbit checks contracts, right to the core! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Bridge: compositional ownership contracts on the C# side (P-006/2b)' directly reflects the main change: implementing compositional ownership contracts for C#-derived facts through the bridge.
Docstring Coverage ✅ Passed Docstring coverage is 85.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/zen-pasteur-76hfs1

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

@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: b76ef818eb

ℹ️ 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

@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 451-454: The condition at the location where `sub.get("source") ==
"injected"` is checked needs to be restricted to subscription resources only.
Currently, any unreleased fact with source set to "injected" triggers DI
lifetime rerouting, which incorrectly affects non-subscription resources that
may have incidental source or source_type fields. Add an additional condition to
verify that the resource is actually a subscription fact before proceeding with
the DI capture path rerouting. Apply this same fix to both occurrences of this
pattern (the one around line 451 and the one around line 592-595) to ensure
consistency across the codebase.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59bf6835-2991-4c49-b7cf-6f2ebbfe7573

📥 Commits

Reviewing files that changed from the base of the PR and between 48a91ab and b76ef81.

📒 Files selected for processing (10)
  • corpus/oracle-target.txt
  • corpus/real-world/ownership-handoff-consume/after.cs
  • corpus/real-world/ownership-handoff-consume/before.cs
  • corpus/real-world/ownership-handoff-consume/case.own
  • corpus/real-world/ownership-handoff-consume/expected-diagnostics.txt
  • corpus/real-world/ownership-handoff-consume/notes.md
  • ownlang/ownir.py
  • tests/fixtures/ownir/di_capture.facts.json
  • tests/fixtures/ownir/handoff_contract.facts.json
  • tests/test_ownir.py

Comment thread ownlang/ownir.py Outdated
…ions

Two review fixes for PR #36.

codex (P2) -- an undischarged `consume` parameter crashed check_facts. The core
stamps an origin (name#line) on acquired locals but NOT on parameters, so the
OWN001 it emits for an owned param carried subject=None; the bridge's
origin-based handle mapping then raised "cannot map back" instead of reporting
the leak. Fix in the core (cfg.py): give every parameter symbol a stable origin,
exactly like an acquired local -- a param can be the subject of a diagnostic
(owned-param leak OWN001, borrow misuse OWN013/OWN004), so it must be
attributable. Now an undischarged consume param maps to OWN001 at the parameter.

CodeRabbit (Major) -- the DI region-escape reroute fired for any unreleased fact
with source == "injected", regardless of resource kind. A non-subscription
resource (e.g. a timer) with an incidental injected source/source_type could be
lowered into the DI capture path and mis-reported. Scope both reroute sites
(to_own and to_module) to rkind == "subscription"; a timer/disposable now keeps
its own analysis path.

Regression tests for both: an undischarged consume param maps to OWN001 (not a
crash); a timer with an injected source stays a timer leak, not OWN014. ownir
69/69, full suite, ruff, mypy --strict green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
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