You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem. When a statement-level first-party call receives a tracked resource, the Roslyn frontend decides the callee's ownership effect itself. It then lowers the call to one of three guesses, so the call itself never reaches the language-neutral core:
So the lowering still destroys two facts: that an interprocedural call happened, and which callee parameter received which resource. This causes two architectural problems.
The core cannot derive the correct partial semantics. The Inference rules that decide this case are all defined over call ops and the forward edges built from them:
A call that the frontend already lowered to release/use/escape can reach none of them.
Every frontend would have to reinvent interprocedural inference.ConsumesParam, DisposesLocal and IsDefiniteInBody re-implement, over Roslyn syntax, rules the core already owns: INF-S2's _definite_release and INF-S3's forward derivation in _build_skeletons. Another frontend (OwnTS exists as the cross-language seam proof, P-020) would have to port them again, and every port can drift from the core. ConsumesParam flattens guarded release into unconditional handoff, causing false OWN009 and false must summaries #380 was exactly that kind of drift. This defeats the language-neutral boundary of the core.
The correct path-insensitive transfer is may: an INF-S3 straight-line forward to a callee whose own INF-S2 summary is may.
Real code (dotnet/runtime@49e04fa0; evidence in Own.NET-paperwork 3a4c1ae):
C12/C13, SymmetricCngTestHelpers.VerifyPersistedKey(..., disposeKey: false). The false OWN009 at :36 and :355 came from the pre-collapse into release. fix: only a definite release makes a call-site handoff (INF-S2, #380) #381 removed them, but the local is now an escape and the caller's record is no longer emitted.
C16, Mono.Options GetArguments(TextReader). The false must became no after fix: only a definite release makes a call-site handoff (INF-S2, #380) #381. no is right for this call site (close: false), so the immediate false positive is fixed. The representation still cannot express partial forwarding for a wrapper that forwards its guard.
Exploratory probe (hand-edited facts, not an implementation). Starting from facts extracted from GuardedConsumeSample.cs at fb06adc, only ForwardDynamic's body use forwarded was replaced with {"op":"call","callee":"GuardedConsumeSample.MaybeClose","sig":"System.IO.Stream,System.Boolean","args":["forwarded"]}.
python -m ownlang summaries: forwarded moves from no to may. MaybeClose.s stays may and BorrowingWrapper.borrowed stays no.
A synthetic caller hands a tracked local x to ForwardDynamic through a call op. It gets [OWN051] cannot verify whether 'GuardedConsumeSample.ForwardDynamic' takes ownership of 'x' (inferred contract: may) from both own-cli ownir and python -m ownlang ownir, with byte-identical output.
So for this case the core-side semantics seem to exist already; what is missing is the call. The probe is evidence for the investigation, not a design.
None of these are P-037 failures. They follow from where the frontend/core boundary sits today.
Desired architectural property
A first-party call involving a tracked resource should survive frontend lowering as a canonical call relationship available to the language-neutral core, instead of being pre-decided into release/use/escape.
Minimum information to investigate:
resolved callee identity or a stable target key (today: callee plus the optional per-overload sig, OwnIR §5.1)
call-site identity and location (today: line, optional column)
binding of each actual argument to its formal parameter
identity of the tracked resource passed as the actual argument
receiver binding, where relevant
binding of named and reordered arguments
unresolved or degraded target state (virtual/interface dispatch, delegates, unresolved symbols)
enough information to tell a direct call apart from a frontend-inferred release/use/escape
Carrier first, schema second. Before inventing any schema, find out whether this belongs in one of these:
The existing OwnIR call flow op (callee, args, optional result, optional sig). Both engines already read it:
for call-site application: INF-A1, INF-A2, INF-A5.
What it does not carry explicitly today:
args is a positional list, and binding is implied by position in it;
the D5.2 emitter keeps only positional tracked identifiers;
there is no receiver slot and no unresolved/degraded state.
The MOS input (the skeletons derived from functions[].body). It takes its forward edges from that same call op, so this may be one decision, not two.
Another carrier that already exists.
By OwnIR §2, adding optional fields needs no OWNIR_VERSION bump. Adding a flow op or changing what one means does.
Settle one transition constraint up front: a canonical call must replace the frontend's guess for that argument, not sit beside it. A call to a definite consumer plus the legacy release would charge the resource twice (OWN003).
not arbitrary predicate solving (no guard cells, no guard evaluation);
not general alias analysis;
not whole-program symbolic execution;
not a Rust-style borrow checker;
not a verdict-policy change by itself. INF-A1/INF-A5 lowering stays as specified, and any verdict movement has to be explained by the representation change.
Acceptance direction
A future implementation should, at minimum, be able to show:
ForwardDynamic keeps a canonical first-party call visible to the core. The KNOWN-LIMITATION pin in tests/test_guarded_consume.py is built to fail loudly when that happens; it gets re-recorded deliberately, never silently.
The wrapper no longer has to become release or use just to encode the callee's ownership behaviour.
Existing definite-consumer behaviour stays intact: the handoff anchors keep their OWN002. These are GuardedConsumeSampleDefiniteHandoffThenUse and FinallyHandoffThenUse, and examples/gallery/cs/07_use_after_handoff.
Canonical calls: independent production work makes the call representation canonical. For example, the legacy body stops folding first-party forwards into release/use, so the honest call becomes the MOS's natural source.
That condition is an example of an external structural change that could later lower P-037's integration cost. This issue is independent architecture work, motivated by the frontend/core boundary described above. If it lands and really makes calls canonical, #304 can then be re-evaluated under its frozen governance. Nothing here claims in advance that the condition will be satisfied.
Описание
Problem. When a statement-level first-party call receives a tracked resource, the Roslyn frontend decides the callee's ownership effect itself. It then lowers the call to one of three guesses, so the call itself never reaches the language-neutral core:
ConsumesParam→DisposesLocal/IsDefiniteInBody)releaseof the argument at the call siteuse→ INF-S4borrow→ summarynofunctions[]record is not emitted at allCode at
fb06adc:EmitFlowExpr: L4159–L4167ConsumeReleaseArgsConsumesParamDisposesLocalIsDefiniteInBodyToday only one shape keeps an OwnIR
call: the factory initializervar r = FirstPartyFactory(args)(P-005 D5.2, L3651–L3684).call → fabricated release. That produced real false findings and falsemustsummaries.fb06adc), the fabricatedmustis gone. A partial or guarded call now degrades instead:use→ summaryno.So the lowering still destroys two facts: that an interprocedural call happened, and which callee parameter received which resource. This causes two architectural problems.
The core cannot derive the correct partial semantics. The Inference rules that decide this case are all defined over
callops and the forward edges built from them:may;may/no;may→ plain;A call that the frontend already lowered to
release/use/escape can reach none of them.Every frontend would have to reinvent interprocedural inference.
ConsumesParam,DisposesLocalandIsDefiniteInBodyre-implement, over Roslyn syntax, rules the core already owns: INF-S2's_definite_releaseand INF-S3's forward derivation in_build_skeletons. Another frontend (OwnTS exists as the cross-language seam proof, P-020) would have to port them again, and every port can drift from the core. ConsumesParam flattens guarded release into unconditional handoff, causing false OWN009 and false must summaries #380 was exactly that kind of drift. This defeats the language-neutral boundary of the core.Concrete witnesses
ConsumesParam flattens guarded release into unconditional handoff, causing false OWN009 and false must summaries #380 / fix: only a definite release makes a call-site handoff (INF-S2, #380) #381. The fix restores the precision floor by refusing to fabricate an unconditional consume. By design it does not produce
may.The pinned limitation (
410431a).forwarded = no.tests/test_guarded_consume.pyL159–L172, fixtureGuardedConsumeSample.csL114–L122.may: an INF-S3 straight-line forward to a callee whose own INF-S2 summary ismay.Real code (
dotnet/runtime@49e04fa0; evidence in Own.NET-paperwork3a4c1ae):SymmetricCngTestHelpers.VerifyPersistedKey(..., disposeKey: false). The false OWN009 at:36and:355came from the pre-collapse intorelease. fix: only a definite release makes a call-site handoff (INF-S2, #380) #381 removed them, but the local is now an escape and the caller's record is no longer emitted.GetArguments(TextReader). The falsemustbecamenoafter fix: only a definite release makes a call-site handoff (INF-S2, #380) #381.nois right for this call site (close: false), so the immediate false positive is fixed. The representation still cannot express partial forwarding for a wrapper that forwards its guard.call.Exploratory probe (hand-edited facts, not an implementation). Starting from facts extracted from
GuardedConsumeSample.csatfb06adc, onlyForwardDynamic's bodyuse forwardedwas replaced with{"op":"call","callee":"GuardedConsumeSample.MaybeClose","sig":"System.IO.Stream,System.Boolean","args":["forwarded"]}.python -m ownlang summaries:forwardedmoves fromnotomay.MaybeClose.sstaysmayandBorrowingWrapper.borrowedstaysno.xtoForwardDynamicthrough acallop. It gets[OWN051] cannot verify whether 'GuardedConsumeSample.ForwardDynamic' takes ownership of 'x' (inferred contract: may)from bothown-cli ownirandpython -m ownlang ownir, with byte-identical output.So for this case the core-side semantics seem to exist already; what is missing is the call. The probe is evidence for the investigation, not a design.
None of these are P-037 failures. They follow from where the frontend/core boundary sits today.
Desired architectural property
A first-party call involving a tracked resource should survive frontend lowering as a canonical call relationship available to the language-neutral core, instead of being pre-decided into
release/use/escape.Minimum information to investigate:
calleeplus the optional per-overloadsig, OwnIR §5.1)line, optionalcolumn)release/use/escapeCarrier first, schema second. Before inventing any schema, find out whether this belongs in one of these:
The existing OwnIR
callflow op (callee,args, optionalresult, optionalsig). Both engines already read it:_build_skeletonsand Rustown-bridgebuild_skeletons;What it does not carry explicitly today:
argsis a positional list, and binding is implied by position in it;The MOS input (the skeletons derived from
functions[].body). It takes its forward edges from that samecallop, so this may be one decision, not two.Another carrier that already exists.
By OwnIR §2, adding optional fields needs no
OWNIR_VERSIONbump. Adding a flow op or changing what one means does.Settle one transition constraint up front: a canonical call must replace the frontend's guess for that argument, not sit beside it. A
callto a definite consumer plus the legacyreleasewould charge the resource twice (OWN003).Non-goals
This issue is:
Acceptance direction
A future implementation should, at minimum, be able to show:
ForwardDynamickeeps a canonical first-party call visible to the core. The KNOWN-LIMITATION pin intests/test_guarded_consume.pyis built to fail loudly when that happens; it gets re-recorded deliberately, never silently.releaseorusejust to encode the callee's ownership behaviour.GuardedConsumeSampleDefiniteHandoffThenUseandFinallyHandoffThenUse, andexamples/gallery/cs/07_use_after_handoff.Relation to #304
The freeze ruling on #304 lists this as reopen condition 1:
That condition is an example of an external structural change that could later lower P-037's integration cost. This issue is independent architecture work, motivated by the frontend/core boundary described above. If it lands and really makes calls canonical, #304 can then be re-evaluated under its frozen governance. Nothing here claims in advance that the condition will be satisfied.
Two notes to keep the boundary clean:
guarded_factssidecar (OwnIR §5.2) also records call-shaped data. It is semantically inert by design and governed by Post-cutover: summary-backed lifecycle release reachability (generalize the landed #293/#302 predicates) #304, and this issue does not propose reading or promoting it.corpus/p037-shapes,scripts/p037_controls.py) will move once calls become canonical. That movement gets recorded as an explicit baseline transition, as fix: only a definite release makes a call-site handoff (INF-S2, #380) #381 did, not re-recorded silently.