Skip to content

Bridge: contract inference — derive a callee's effects from its body (P-006/2b v1)#37

Merged
PhysShell merged 2 commits into
mainfrom
claude/contract-inference-v1
Jun 19, 2026
Merged

Bridge: contract inference — derive a callee's effects from its body (P-006/2b v1)#37
PhysShell merged 2 commits into
mainfrom
claude/contract-inference-v1

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 19, 2026

Copy link
Copy Markdown
Owner

The bounded inter-procedural step that lets first-party C# be checked without annotating every method. When a function parameter carries no explicit effect, infer its ownership contract from the callee's own body:

  • the body discharges/escapes the param (release / return) → CONSUME (ownership taken);
  • the param is only read and retained (use, no discharge) → BORROW (the caller keeps ownership and must still release it);
  • the param is handed to another call → ambiguous without that callee's contract, so not inferred (stays plain / awaiting an annotation or a later transitive pass).

Inference fires only on unambiguous signals and never guesses a consume/borrow, and an explicit effect in the fact always wins over it. It reuses the existing engine end to end: the inferred effect just picks the param's TypeRef, so collect_signatures + lower_call resolve calls against the inferred signature with no new checker.

Proof (contract_inference.facts.json, zero annotations)

callee body inferred caller verdict
archive releases s consume runOWN002 (use-after-handoff)
peek only uses s borrow keepOWN001 (caller still owns), ok → clean (caller releases)

Notes

  • Additive + zero regression: only un-annotated params change behavior; explicit contracts and annotation-free non-contract functions are unaffected.
  • ownir 72/72, full suite, ruff, mypy --strict all green.
  • Next: transitive inference (param → call → callee's contract, fixpoint over the island's SCC) removes the last "ambiguous" case above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Automatic ownership contract inference: Function parameters can now automatically infer ownership contracts from code flow analysis when explicit annotations are omitted. The system detects usage patterns to determine appropriate ownership semantics.
  • Tests

    • Added test fixtures and validation suite for contract inference, covering multiple scenarios with and without explicit annotations.

…(P-006/2b v1)

The bounded inter-procedural step that lets first-party C# be checked without
annotating every method. When a function parameter carries no explicit `effect`,
infer its ownership contract from the callee's OWN body:

  - the body discharges/escapes the param (release / return) -> CONSUME
    (ownership taken);
  - the param is only read and retained (use, no discharge)  -> BORROW
    (the caller keeps ownership and must still release it);
  - the param is handed to another call -> ambiguous without that callee's
    contract, so NOT inferred (stays plain / awaiting an annotation or a later
    transitive pass).

Inference fires only on unambiguous signals and never guesses a consume/borrow,
and an explicit `effect` in the fact always wins over it. It reuses the existing
engine end to end: the inferred effect just picks the param's TypeRef, so
collect_signatures + lower_call resolve calls against the inferred signature with
no new checker.

contract_inference.facts.json proves it with ZERO annotations:
  archive (releases s)  -> inferred consume -> run trips OWN002 (use-after-handoff)
  peek    (only uses s) -> inferred borrow  -> keep leaks OWN001 (caller still owns),
                                               ok is clean (caller releases)

Additive + zero regression (only un-annotated params change behavior; explicit
contracts and annotation-free non-contract functions are unaffected). ownir
72/72, 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

📝 Walkthrough

Walkthrough

ownlang/ownir.py gains two helper functions, _param_signals and _infer_param_effect, that scan a function's flow body to derive a parameter's ownership contract (consume, borrow, or None) when the OwnIR fact omits an explicit effect string. _lower_fn_params is updated to invoke this inference path. A new test fixture (contract_inference.facts.json) and corresponding tests validate the behavior.

Changes

v1 Contract Inference for OwnIR Parameters

Layer / File(s) Summary
Inference helpers and _lower_fn_params integration
ownlang/ownir.py
Adds _param_signals (flow-body scanner with if/while recursion) and _infer_param_effect (maps release/returnconsume, use-only → borrow, call-arg → None). _lower_fn_params now falls through to _infer_param_effect when effect is not a string. load() comment updated to document that a missing effect is inferred rather than strictly required.
Contract inference fixture and tests
tests/fixtures/ownir/contract_inference.facts.json, tests/test_ownir.py
New fixture JSON defines module InferBridge with five functions (archive, peek, run, keep, ok) and their operation traces. New test section (P-006/2b) asserts inferred borrow vs. consume verdicts, verifies no false positives on correct callees, and checks that an explicit effect: "consume" overrides inference and produces OWN001.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • PhysShell/Own.NET#36: Modifies the same _lower_fn_params and ownership-contract pipeline in ownlang/ownir.py, directly preceding the inference hook added in this PR.

Poem

🐇 A rabbit reads the body's flow,
No effect tagged? It still will know!
Release means consume, borrow for use,
Ambiguous callers? We can't deduce.
Annotations remain the final say —
Contracts inferred, the smart OWN way! 🥕

🚥 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 clearly and specifically describes the main feature being implemented: contract inference that derives parameter ownership effects from method bodies, with the specification reference (P-006/2b v1) providing context.
Docstring Coverage ✅ Passed Docstring coverage is 100.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/contract-inference-v1

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: 7c46a89580

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

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ownlang/ownir.py (1)

384-390: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate parameter identity fields before effect inference.

Line 385 validates only effect; malformed params[*].name/line still pass shape-check and can be silently coerced downstream, which can mis-infer ownership contracts instead of failing fast on invalid OwnIR input.

Suggested patch
         for p in ps:
+            pname = p.get("name")
+            if not isinstance(pname, str) or not pname:
+                raise OwnIRError(
+                    f"parameter 'name' must be a non-empty string, got {pname!r}")
+            pline = p.get("line", 0)
+            if not isinstance(pline, int) or isinstance(pline, bool):
+                raise OwnIRError(
+                    f"parameter 'line' must be an integer, got {pline!r}")
             peff = p.get("effect")
             if peff is not None and peff not in ("consume", "borrow", "borrow_mut",
                                                  "plain"):
                 raise OwnIRError(
                     f"parameter 'effect' must be consume/borrow/borrow_mut/plain, "
                     f"got {peff!r}")
🤖 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 `@ownlang/ownir.py` around lines 384 - 390, The parameter validation loop that
checks the effect field at line 385-388 only validates the effect field, but
parameters also have identity fields like name and line that should be validated
before effect inference occurs. Add validation checks for these required
identity fields (name and line) in the same loop that validates the effect
field, ensuring they exist and have valid values before any downstream
processing or coercion happens. This will catch malformed parameter definitions
early rather than allowing them to silently propagate and cause incorrect
ownership contract inference.
🧹 Nitpick comments (1)
tests/test_ownir.py (1)

693-725: ⚡ Quick win

Add a direct test for the “passed-to-call is ambiguous” branch.

This block validates consume/borrow + explicit override, but not the third rule (passed => no inference). Adding one focused case will lock the v1 contract boundary and prevent future regressions.

Example assertion to add
     if [(x.component, x.code) for x in ov] != [("g", "OWN001")]:
         fails.append(f"explicit `consume` should win over inferred borrow (owned "
                      f"param leaks OWN001), got {[(x.component, x.code) for x in ov]}")
+
+    # ambiguous handoff: param forwarded to another call => no inferred effect.
+    checks += 1
+    amb = check_facts({"module": "M", "functions": [
+        {"name": "forward", "file": "F.cs",
+         "params": [{"name": "s", "line": 1}],
+         "body": [{"op": "call", "callee": "sink", "args": ["s"], "line": 2}]},
+        {"name": "sink", "file": "F.cs",
+         "params": [{"name": "x", "effect": "consume", "line": 5}],
+         "body": [{"op": "release", "var": "x", "line": 6}]},
+        {"name": "caller", "file": "F.cs",
+         "body": [{"op": "acquire", "var": "s", "line": 10},
+                  {"op": "call", "callee": "forward", "args": ["s"], "line": 11},
+                  {"op": "release", "var": "s", "line": 12}]}]})
+    if any(x.component == "caller" for x in amb):
+        fails.append(f"ambiguous pass-through should not infer consume/borrow, got "
+                     f"{[(x.component, x.code) for x in amb]}")
🤖 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_ownir.py` around lines 693 - 725, The test block validates
contract inference and the explicit override behavior, but does not cover the
third rule where parameters passed-to-call (passed as arguments to other
function calls) should not have inference applied. Add a new focused test case
after the existing explicit consume override test using check_facts that creates
a simple function with a parameter that is passed to another function call,
verify the expected ownership behavior to ensure inference is correctly
suppressed in this case, and add an assertion that validates the findings match
the expected output to prevent future regressions.
🤖 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.

Outside diff comments:
In `@ownlang/ownir.py`:
- Around line 384-390: The parameter validation loop that checks the effect
field at line 385-388 only validates the effect field, but parameters also have
identity fields like name and line that should be validated before effect
inference occurs. Add validation checks for these required identity fields (name
and line) in the same loop that validates the effect field, ensuring they exist
and have valid values before any downstream processing or coercion happens. This
will catch malformed parameter definitions early rather than allowing them to
silently propagate and cause incorrect ownership contract inference.

---

Nitpick comments:
In `@tests/test_ownir.py`:
- Around line 693-725: The test block validates contract inference and the
explicit override behavior, but does not cover the third rule where parameters
passed-to-call (passed as arguments to other function calls) should not have
inference applied. Add a new focused test case after the existing explicit
consume override test using check_facts that creates a simple function with a
parameter that is passed to another function call, verify the expected ownership
behavior to ensure inference is correctly suppressed in this case, and add an
assertion that validates the findings match the expected output to prevent
future regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c7a2159b-d462-475e-b47e-398955adfdca

📥 Commits

Reviewing files that changed from the base of the PR and between fb7f8c7 and 7c46a89.

📒 Files selected for processing (3)
  • ownlang/ownir.py
  • tests/fixtures/ownir/contract_inference.facts.json
  • tests/test_ownir.py

Three review fixes for the contract-inference PR.

codex (P2) -- don't infer consume from returns, and stop value-returns crashing.
Two parts:
  * inference no longer treats `return <param>` as a consume signal: the bridge
    does not model returned (owned) VALUES, so a returned param is consume-and-
    handed-back, not a plain consume -- inferring consume there is unsound until
    return types are represented.
  * a value-bearing `return` previously crashed (the bridge lowers every flow
    function void, so the core's OWN035 "no return type but returns a value" had
    no subject and check_facts raised). Now a body that returns a value gets an
    owned return type, so the core models `return s` as a valid ESCAPE (the value
    is discharged) -- no false leak -- and the return-type diagnostics the bridge
    cannot represent (OWN033/OWN035) are skipped instead of crashing.

CodeRabbit (nitpick) -- the "passed-to-call is ambiguous" branch was untested and
crashed: an un-inferred (plain) param forwarded to a typed parameter produced a
subject-less OWN041 ("plain cannot satisfy a resource parameter") that raised.
Effect-kind mismatches (OWN034/OWN041) are bridge modeling-gap artifacts (an
ambiguous contract that could not be inferred), not real C# bugs, so they are
skipped too; the pass-through and its caller stay silent (no false positive).
Added a regression test for the branch.

CodeRabbit (major) -- validate params[*].name (non-empty string) and line
(integer) in load(), so malformed OwnIR input fails fast instead of being
silently coerced and mis-inferring a contract.

Regression tests for value-returns and the ambiguous pass-through. ownir 74/74,
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

Copy link
Copy Markdown
Owner Author

Following up on the CodeRabbit review — both points addressed in 6932733:

  • Major (validate parameter identity fields): load() now validates params[*].name (non-empty string) and line (integer) before any inference, so malformed OwnIR fails fast instead of being silently coerced.
  • Nitpick (passed-to-call ambiguous branch test): added a regression test for it. Investigating it also surfaced an adjacent crash — an un-inferred (plain) param forwarded to a typed parameter produced a subject-less OWN041, which check_facts raised on. Those effect-mismatch artifacts (OWN034/OWN041) are now skipped, so the pass-through and its caller stay silent (no false positive) until transitive inference resolves the contract.

Also from the codex review (same commit): inference no longer treats return as a consume signal, and value-bearing returns get an owned return type so return s models a valid escape instead of crashing.

ownir 74/74, full suite, ruff, mypy --strict green.


Generated by Claude Code

@PhysShell

Copy link
Copy Markdown
Owner Author

@codex plz review

@PhysShell
PhysShell merged commit 1679a91 into main Jun 19, 2026
16 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