Skip to content

Commit c36978f

Browse files
committed
resource-effects Stage 2: body-derived effects E1 and E2 behind OWEN_RE_BODY=1
EXPLORATORY (research/resource-effects-v1; pre-registered in Own.NET-paperwork stage2-body-effects-prereg-v1.json). E1: a return outside any try whose expression is an object creation of an owned disposable, a first-party disposable factory call, or a conditional of those is lowered as the acquire/call of $ret and return $ret (an expression-bodied factory gets a record of just that); E2: an instance method whose body definitely calls this type's IDisposable.Dispose / IAsyncDisposable.DisposeAsync implementation on `this` releases its receiver, and the predicate is a consume signal in DisposesLocal. Decided by resolved symbols, never by a name; facts byte-identical off. Witness shapes s15, s08b, s08c added; test rows pinned. Measured in stage2-body-effects-v1.json: s15 exactly as pre-registered, direct-return wrappers fresh, the mixed skeleton (s08c Make7) stays none (Stage 3 E3), only case 2 moves in the population (a true fresh factory; verdict a type-model question), 148 lines against a 120-line budget (recorded). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am9eQwzNfbugH72eVKetC2
1 parent 890d1c4 commit c36978f

5 files changed

Lines changed: 287 additions & 12 deletions

File tree

‎corpus/re-cfg-probe/s08b-caller.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Stage 2 E1 caller twin (pre-registered expected: with OWEN_RE_BODY=1 the three direct-return factories are fresh and
2+
// Drop1..Drop3 leak; Make4 (a conditional of new and a forward) stays `none` until the Stage 3 engine rule E3).
3+
// Stage 2 E1 caller twin: a method that drops the result of each factory shape (truth: three leaks).
4+
using System;
5+
sealed class R : IDisposable { public bool IsOpen => true; public void Touch() { } public void Dispose() { } }
6+
static class S08B
7+
{
8+
static R Make() { var r = new R(); return r; }
9+
static R Make2() { return Make(); }
10+
static R Make3() { return new R(); }
11+
static R Make4(bool b) => b ? new R() : Make();
12+
static void Drop1() { var a = Make(); a.Touch(); }
13+
static void Drop2() { var a = Make2(); a.Touch(); }
14+
static void Drop3() { var a = Make3(); a.Touch(); }
15+
static void Drop4(bool b) { var a = Make4(b); a.Touch(); }
16+
}

‎corpus/re-cfg-probe/s08c-mixed.cs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Stage 2 / Stage 3 E3 witness: Make5 (both arms new) and Make6 (both arms forwards) are fresh under OWEN_RE_BODY=1;
2+
// Make7 (a new on one path, a forward on the other) is `none` until the engine rule E3 (Stage 3).
3+
using System;
4+
sealed class R : IDisposable { public void Touch() { } public void Dispose() { } }
5+
static class S08C
6+
{
7+
static R Make() { var r = new R(); return r; }
8+
static R Make5(bool b) => b ? new R() : new R();
9+
static R Make6(bool b) => b ? Make() : Make();
10+
static R Make7(bool b) { if (b) { return new R(); } return Make(); }
11+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# resource-effects Stage 2 / 2A — body-derived effects and the CFG falsifier (EXPLORATORY)
2+
3+
Research branch `research/resource-effects-v1`. Pre-registered in Own.NET-paperwork
4+
`paper-eval/resource-effects/stage2a-cfg-probe-prereg-v1.json` and `stage2-body-effects-prereg-v1.json`;
5+
evidence in `stage2a-cfg-probe-v1.json` and `stage2-body-effects-v1.json`. #304 frozen and unchanged; #382
6+
untouched; nothing here is a production result. No frozen real witness was expected to move here (the Stage 0
7+
narrowing), and none did.
8+
9+
## 1. Stage 2A: the CFG/IOperation cheap falsifier (KILLED, K6)
10+
11+
`frontend/roslyn/OwnSharp.CfgProbe` (research-only, never referenced by the extractor) answers the
12+
pre-registered definite-release question over the 14 shapes in `corpus/re-cfg-probe` three ways: the
13+
production syntax pipeline (extractor + Python reference), an IOperation walk, and a 28-line must-analysis
14+
over `ControlFlowGraph.Create(body)` (intersection at joins, finally regions applied along leaving branches).
15+
16+
Tally: the CFG is strictly more precise than the production pipeline on **0 of 14** shapes (the pass
17+
condition needed 3); the production pipeline is strictly better on 3 (the transitive consume s07, the
18+
named-argument handoff s09, the relational site s10); 8 are equal; on s11 (reassignment) both are wrong and
19+
the CFG would need SSA; on s12 (local function) the CFG would need a second graph inlined. KILL GATE CFG
20+
triggers on three of its conditions; RQ-E4 is answered NO. Nothing Roslyn-specific was integrated.
21+
22+
The probe surfaced two SYNTAX-level gaps instead: no fresh summary for direct returns (`return new T()`,
23+
`return Factory()`, conditional expressions of those — s08 Make2/Make3 had no record) and no receiver
24+
release from a first-party body (s15 measured four OWN001, three of them false, and a missed OWN002).
25+
26+
## 2. Stage 2: E1 and E2 behind `OWEN_RE_BODY=1` (+148/-8 handwritten lines; byte-identical off)
27+
28+
- **E1 fresh for direct returns.** A `return` outside any try whose expression is an object creation of an
29+
owned disposable, a first-party disposable factory call, or a conditional of those (through parentheses
30+
and casts) is lowered as `acquire $ret` / `call … result=$ret` / `if` + `return $ret` — the facts
31+
`var r = new R(); return r;` already produces, so the core's R3/R4 decide `fresh`. An expression-bodied
32+
method of that shape gets a record of just that lowering.
33+
- **E2 receiver release from a body.** An instance method of a first-party type whose body definitely
34+
(IsDefiniteInBody) calls this type's `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync`
35+
implementation on `this`, directly or through another such method, releases its receiver: `x.M()` on a
36+
tracked local is a `release`, and the same predicate is a consume signal in DisposesLocal. Decided by the
37+
resolved interface implementation, never by a name; a conditional body stays a `use`.
38+
39+
Measured (tests/test_re_controls.py pins the rows): s15 gives exactly the pre-registered `OWN001 x1
40+
(RunMaybe) + OWN002 x1 (RunUse)`; `Drop(c) { c.Kill(); }` consumes. s08: Make2 and Make3 are fresh and
41+
the callers dropping their results leak (true positives). `Make4(bool b) => b ? new R() : Make()` stays
42+
`none`: the engine keeps INF-R3 (all returns acquired here) and INF-R4 (a forwarded call result) apart, so
43+
a MIXED skeleton is R5 `none` (s08c isolates it: both-`new` and both-forward conditionals are fresh). That
44+
generic engine rule (E3) is pre-registered for Stage 3 with the two-token `CreateLinkedTokenSource`
45+
overload of the dependency source as its real witness.
46+
47+
Population, measured against the sub-stage 1b outputs: the six cases move only in case 2, where E1 makes
48+
`CreateRequestMessage(...) => new HttpRequestMessage(...)` a fresh factory and three `Get*Async` callers
49+
show an undisposed `request` (OWN001 x3). The effect is TRUE (a fresh, caller-owned object every call, so
50+
the E1 kill condition — a borrowed-return method mis-proven fresh — is not met); whether an undisposed GET
51+
`HttpRequestMessage` is a leak is a TYPE-MODEL question (dotnet/runtime itself never disposes them), the
52+
same class of external knowledge as RQ-E3, recorded as UNDISPOSED_OPTIONAL and not counted as a witness.
53+
The 82-file tree: E1 fires twice, no verdict moves. The 137-document corpus: no fact moves — a grep finds
54+
no direct-return factory of a disposable type and no receiver method disposing `this` in it, so the corpus
55+
cannot exercise E1/E2 (absence of shapes, not evidence of precision). Every control is unchanged.
56+
57+
Budget: the prereg allowed 120 extractor lines for E1+E2; the patch is 148 (the expression-bodied factory
58+
path, which the 4A witness needs, is the excess). Recorded as a breach, not re-expected.

0 commit comments

Comments
 (0)