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