Skip to content

Commit ecf027f

Browse files
committed
P-037 PCS-0: KILL — driver 68 / 40, total 208 / 180, not run
The first complete, lint-clean draft exceeds the binding driver cap and the total (C# producer shadow 83/100 and tests + probe 57/60 are within). Per the pre-registered §E/§G this is a KILL of the gate; nothing was run (no falsifier, no C1 byte-identity control, no A18-0 row) and the draft was not reworked to fit after the count. The unrun draft is committed as the artifact; §I records the breakdown: the overrun is the standalone driver's own plumbing, not the producer shadow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiyfrkaG9sJruTcshM2oq
1 parent 96810e5 commit ecf027f

5 files changed

Lines changed: 319 additions & 32 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// P-037 PCS-0 probe (docs/notes/p037-pcs0-producer-canonical-shadow.md §D), not population.
2+
// F0: the natural transitive chain Top -> Outer -> Inner. The legacy body folds Top's
3+
// forward into a `release` because ConsumesParam(Outer) is true; the canonical shadow must
4+
// keep it an honest `call` so the ordinary MOS reads Outer's own `may`.
5+
// F1: the borrow chain Pass -> Peek. The shadow forwards it just the same and the MOS, not
6+
// the producer, keeps `no`.
7+
using System.IO;
8+
9+
static class PcsTransitive
10+
{
11+
static void Inner(Stream s, bool keep)
12+
{
13+
if (!keep)
14+
s.Dispose();
15+
}
16+
17+
static void Outer(Stream s, bool keep)
18+
{
19+
Inner(s, keep);
20+
}
21+
22+
static void Top(Stream s)
23+
{
24+
Outer(s, true);
25+
}
26+
27+
static void Peek(Stream s)
28+
{
29+
s.ReadByte();
30+
}
31+
32+
static void Pass(Stream s)
33+
{
34+
Peek(s);
35+
}
36+
}

‎docs/notes/p037-pcs0-producer-canonical-shadow.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,51 @@ This gate does not touch any of these:
224224
- the three P-037 classes and `own-guarded`.
225225

226226
There is no population run in this gate.
227+
228+
## I. RESULT: KILL — PCS-0 (BUDGET), not run
229+
230+
```text
231+
STATE: KILL — driver 68 / 40, total 208 / 180 (first complete, lint-clean draft)
232+
WITHIN: C# producer shadow 83 / 100 · tests + probe 57 / 60
233+
RUN: NONE — no falsifier, no C1 byte-identity control, no A18-0 row
234+
```
235+
236+
- **The count.** Measured with the §G rule (`git diff ca14336`; excludes
237+
blank, `//` and `#` lines; counts braces, docstrings and usage text), the
238+
first complete draft exceeds two binding caps:
239+
- the driver: 61 lines as first written, 68 once wrapped to the repo's
240+
100-column ruff limit;
241+
- the total.
242+
243+
Per §E/§G that is a KILL. The draft was not reworked to fit after the
244+
count, and nothing was run. The unrun, lint-clean draft is committed as
245+
the artifact.
246+
- **Where the lines are.**
247+
248+
| part | lines | cap |
249+
|------|------:|----:|
250+
| C#: `CanonicalForward` (the whole honest-forward representation) | 38 | |
251+
| C#: the A2.2 unwrap lifted into a shared `ValueUnwrap` (a move, counted as added) | 24 | |
252+
| C#: switch, `--fix-candidates` refusal, second lowering, shadow write, three `EmitFlowExpr` hook lines | 21 | |
253+
| **C# total** | **83** | 100 |
254+
| driver `scripts/p037_pcs0.py`: header and imports 18, `extract()` 13, `main()` 35, entry 2 | **68** | 40 |
255+
| falsifiers `tests/p037_pcs0_falsifiers.py` 32 + probe `Transitive.cs` 25 | **57** | 60 |
256+
| **total** | **208** | 180 |
257+
258+
- **Reading.** The premise-specific cap held with room: the producer shadow
259+
is 83/100 C# lines. It adds no schema change, and with the switch off the
260+
ordinary lowering path is unchanged. It makes no `ConsumesParam` call,
261+
computes no transfer value, builds no call graph, and shares the unwrap
262+
instead of copying it.
263+
- **Why it still died.** The overrun is entirely measurement plumbing. The
264+
driver was written as a **new standalone script**, so it pays its own
265+
docstring, imports, argparse, clean-tree refusal, frozen-tree
266+
materialization and evidence writing. That is about 45 of its 68 lines.
267+
§G budgeted "driver changes", and extending the existing A18-0 driver,
268+
which already has that plumbing, might have fit. That judgment comes after
269+
the count, so it was not acted on: reworking code until it fits a cap
270+
after seeing the count is what kill-first forbids.
271+
- **What the KILL does not say.** Nothing was measured, neither F0 nor the
272+
8 rows. The producer-shadow premise is neither confirmed nor refuted. Per
273+
§0 there is no PCS-1R2. Whether this is the point to freeze the P-037
274+
implementation as a whole, as §E anticipates, is the owner's decision.

‎frontend/roslyn/OwnSharp.Extractor/Program.cs‎

Lines changed: 114 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ events bind to real symbols instead of OWN050 (repeatable)
152152
--flow-locals path-sensitive flow analysis of non-escaping local IDisposables
153153
--stats print flow-locals coverage (requires --flow-locals)
154154
--dispatch-report FILE P-037 A14 measurement: dispatch facts per relevant call (not OwnIR)
155+
--p037-canonical-shadow FILE P-037 PCS-0 measurement: also write a canonical-forward shadow OwnIR
155156
--body-throw-edges treat escaping body-level may-throw as a dispose-on-throw point (needs --flow-locals)
156157
-h, --help show this help and exit
157158
""";
@@ -187,6 +188,8 @@ events bind to real symbols instead of OWN050 (repeatable)
187188
else if (args0[i] == "--stats") reportStats = true;
188189
// P-037 B1 A14 measurement (DispatchReport.cs): a separate JSON, never OwnIR.
189190
else if (args0[i] == "--dispatch-report" && i + 1 < args0.Length) DispatchReport.Path = args0[++i];
191+
// P-037 PCS-0 measurement: a second, canonical-forward OwnIR document (see CanonicalForward).
192+
else if (args0[i] == "--p037-canonical-shadow" && i + 1 < args0.Length) CanonicalShadowPath = args0[++i];
190193
else rawInputs.Add(args0[i]);
191194
}
192195

@@ -196,6 +199,12 @@ events bind to real symbols instead of OWN050 (repeatable)
196199
Console.Error.WriteLine(" ownsharp-extract --help for the full option list");
197200
return 2;
198201
}
202+
// PCS-0: the shadow is defined for the plain facts shape only.
203+
if (CanonicalShadowPath is not null && emitFixCandidates)
204+
{
205+
Console.Error.WriteLine("extractor: --p037-canonical-shadow is not defined with --fix-candidates");
206+
return 2;
207+
}
199208

200209
// --stats reports flow-locals coverage; the counters only move inside the
201210
// --flow-locals pass. Without it they would all be zero and the summary would
@@ -2955,6 +2964,39 @@ bool Refers(ExpressionSyntax? e) =>
29552964
return true;
29562965
}
29572966

2967+
// A2.2: a VALUE-PRESERVING wrapper is the same value as its operand, so it is looked
2968+
// through before classifying — parentheses, the null-forgiving `!` (no runtime effect),
2969+
// and a cast whose conversion is identity or a non-user-defined reference conversion
2970+
// (the same object, only a different static type). Any other cast (boxing, unboxing,
2971+
// numeric, user-defined) produces a different value and is left in place.
2972+
static ExpressionSyntax? ValueUnwrap(ExpressionSyntax? e, SemanticModel model)
2973+
{
2974+
while (true)
2975+
{
2976+
e = StripParens(e);
2977+
switch (e)
2978+
{
2979+
case PostfixUnaryExpressionSyntax bang
2980+
when bang.IsKind(SyntaxKind.SuppressNullableWarningExpression):
2981+
e = bang.Operand;
2982+
continue;
2983+
// The cast's OWN conversion (operand -> cast type). `GetConversion(cast)`
2984+
// would answer the contextual conversion of the cast node instead — for
2985+
// `Sink((object)h)` that is object -> object, identity, and a boxing cast
2986+
// would pass as value-preserving (caught by corpus/p037-shapes/arg-cast-and-bang).
2987+
case CastExpressionSyntax cast
2988+
when model.GetTypeInfo(cast.Type).Type is { } castTo
2989+
&& model.ClassifyConversion(cast.Expression, castTo, isExplicitInSource: true)
2990+
is var conv
2991+
&& (conv.IsIdentity || (conv.IsReference && !conv.IsUserDefined)):
2992+
e = cast.Expression;
2993+
continue;
2994+
default:
2995+
return e;
2996+
}
2997+
}
2998+
}
2999+
29583000
static object? BuildGuardedFacts(BaseMethodDeclarationSyntax method, BlockSyntax mbody,
29593001
HashSet<string> handles, HashSet<string> ownedParamNames,
29603002
SemanticModel model, string where)
@@ -2977,38 +3019,8 @@ bool Stable(IParameterSymbol p)
29773019
return known;
29783020
}
29793021

2980-
// A2.2: a VALUE-PRESERVING wrapper is the same value as its operand, so it is looked
2981-
// through before classifying — parentheses, the null-forgiving `!` (no runtime effect),
2982-
// and a cast whose conversion is identity or a non-user-defined reference conversion
2983-
// (the same object, only a different static type). Any other cast (boxing, unboxing,
2984-
// numeric, user-defined) produces a different value and is left in place.
2985-
ExpressionSyntax? Unwrap(ExpressionSyntax? e)
2986-
{
2987-
while (true)
2988-
{
2989-
e = StripParens(e);
2990-
switch (e)
2991-
{
2992-
case PostfixUnaryExpressionSyntax bang
2993-
when bang.IsKind(SyntaxKind.SuppressNullableWarningExpression):
2994-
e = bang.Operand;
2995-
continue;
2996-
// The cast's OWN conversion (operand -> cast type). `GetConversion(cast)`
2997-
// would answer the contextual conversion of the cast node instead — for
2998-
// `Sink((object)h)` that is object -> object, identity, and a boxing cast
2999-
// would pass as value-preserving (caught by corpus/p037-shapes/arg-cast-and-bang).
3000-
case CastExpressionSyntax cast
3001-
when model.GetTypeInfo(cast.Type).Type is { } castTo
3002-
&& model.ClassifyConversion(cast.Expression, castTo, isExplicitInSource: true)
3003-
is var conv
3004-
&& (conv.IsIdentity || (conv.IsReference && !conv.IsUserDefined)):
3005-
e = cast.Expression;
3006-
continue;
3007-
default:
3008-
return e;
3009-
}
3010-
}
3011-
}
3022+
// A2.2 value-preserving unwrap, shared with the PCS-0 shadow (ValueUnwrap).
3023+
ExpressionSyntax? Unwrap(ExpressionSyntax? e) => ValueUnwrap(e, model);
30123024

30133025
// One argument expression -> one raw fact, plus whether a HANDLE of this method flowed.
30143026
(Dictionary<string, object?> fact, bool handle) ArgFact(ExpressionSyntax? raw)
@@ -4247,7 +4259,11 @@ static void EmitFlowExpr(ExpressionSyntax expr, HashSet<string> tracked, Semanti
42474259
// modelled at the call site like pool Return). A later use of an argument is then a
42484260
// use-after-handoff (OWN002). Do NOT return — other tracked arguments of the same call
42494261
// (`Consume(s, t)`) still need their `use` below; a consumed arg is excluded from it.
4262+
// P-037 PCS-0: under the shadow switch only, honest forwards become one `call` op and get
4263+
// neither the handoff `release` nor a `use` below. With the switch off `forwarded` is empty.
4264+
var forwarded = CanonicalForwards ? CanonicalForward(expr, tracked, model, nodes) : new List<string>();
42504265
var consumed = ConsumeReleaseArgs(expr, model);
4266+
consumed.RemoveAll(forwarded.Contains);
42514267
foreach (var c in consumed)
42524268
if (tracked.Contains(c))
42534269
nodes.Add(new { op = "release", var = c, line = LineOf(expr) });
@@ -4277,6 +4293,7 @@ static void EmitFlowExpr(ExpressionSyntax expr, HashSet<string> tracked, Semanti
42774293
&& tracked.Contains(owner) && !consumed.Contains(owner))
42784294
used.Add(owner);
42794295
}
4296+
used.ExceptWith(forwarded);
42804297
foreach (var u in used)
42814298
nodes.Add(new { op = "use", var = u, line = LineOf(expr) });
42824299
// POOL005: a full-length view of a pooled buffer anywhere in this expression -> overspan/OWN025.
@@ -5007,6 +5024,49 @@ static List<string> ConsumeReleaseArgs(ExpressionSyntax e, SemanticModel model)
50075024
return consumed;
50085025
}
50095026

5027+
// P-037 PCS-0 (measurement only; runs solely under --p037-canonical-shadow): the HONEST
5028+
// representation of a forwarding call. Each argument that, after the A2.2 value-preserving
5029+
// unwrap, is a tracked identifier bound to one of the callee's `functions[].params` becomes a
5030+
// positional slot of ONE legacy `call` op (the D5.2 op the bridge already reads as a forward),
5031+
// instead of the `release`/`use` the ordinary lowering folds it into. It decides nothing — no
5032+
// ConsumesParam, no transfer value: the MOS reads the callee's summary. Returns the forwarded
5033+
// identifiers, which then get no `release`/`use` op for this expression.
5034+
static List<string> CanonicalForward(ExpressionSyntax e, HashSet<string> tracked,
5035+
SemanticModel model, List<object> nodes)
5036+
{
5037+
var forwarded = new List<string>();
5038+
if (e is not InvocationExpressionSyntax inv
5039+
|| model.GetSymbolInfo(inv).Symbol is not IMethodSymbol { ReducedFrom: null } sym
5040+
|| sym.DeclaringSyntaxReferences.Length == 0)
5041+
return forwarded;
5042+
var decl = sym.OriginalDefinition;
5043+
// The callee's params[] list, by the predicate that builds it: by-value, owned disposable.
5044+
var owned = decl.Parameters.Where(p => p.RefKind == RefKind.None
5045+
&& p.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is ParameterSyntax { Type: { } pt } ps
5046+
&& IsOwnedDisposableType(pt, model.Compilation.GetSemanticModel(ps.SyntaxTree))).ToList();
5047+
var slots = new List<string>();
5048+
var args = inv.ArgumentList.Arguments;
5049+
for (var i = 0; i < args.Count; i++)
5050+
{
5051+
var p = args[i].NameColon is { } nc
5052+
? decl.Parameters.FirstOrDefault(q => q.Name == nc.Name.Identifier.Text)
5053+
: (i < decl.Parameters.Length ? decl.Parameters[i] : null);
5054+
var k = p is null || p.IsParams ? -1 : owned.FindIndex(q => SymbolEqualityComparer.Default.Equals(q, p));
5055+
if (k < 0 || !args[i].RefKindKeyword.IsKind(SyntaxKind.None)
5056+
|| ValueUnwrap(args[i].Expression, model) is not IdentifierNameSyntax id
5057+
|| !tracked.Contains(id.Identifier.Text))
5058+
continue;
5059+
while (slots.Count <= k)
5060+
slots.Add("_");
5061+
slots[k] = id.Identifier.Text;
5062+
forwarded.Add(id.Identifier.Text);
5063+
}
5064+
if (forwarded.Count > 0)
5065+
nodes.Add(new { op = "call", callee = $"{decl.ContainingType.ToDisplayString()}.{decl.Name}",
5066+
sig = CanonicalSig(sym), args = slots, line = LineOf(e) });
5067+
return forwarded;
5068+
}
5069+
50105070
// The body of a first-party method or LOCAL FUNCTION (block or expression-bodied), scanning
50115071
// partial declarations; null for an interface/abstract/extern method (no body to inspect). A
50125072
// directly-called local function runs synchronously, so a forwarding chain through one must be
@@ -5661,6 +5721,8 @@ static bool IsPublicCtor(SyntaxTokenList modifiers)
56615721
var components = new List<object>();
56625722
// P-016 B0b/B2: per-method flow bodies (only when --flow-locals).
56635723
var flowFunctions = new List<object>();
5724+
// P-037 PCS-0: the same records with the canonical-forward body (only under the shadow flag).
5725+
var shadowFunctions = new List<object>();
56645726

56655727
// Parse every input into a syntax tree first (keeping the file path we report
56665728
// it under), then build ONE compilation over all of them so the SemanticModel
@@ -7207,6 +7269,15 @@ or ImplicitObjectCreationExpressionSyntax } init
72077269
if (guardedFacts is not null)
72087270
record["guarded_facts"] = guardedFacts;
72097271
flowFunctions.Add(record);
7272+
// P-037 PCS-0: lower the same body a second time with honest forwards; the
7273+
// record above (and so `-o`) was built with the switch off and is untouched.
7274+
if (CanonicalShadowPath is not null)
7275+
{
7276+
CanonicalForwards = true;
7277+
var shadowBody = LowerFlowBody(mbody, flowNames, model);
7278+
CanonicalForwards = false;
7279+
shadowFunctions.Add(new Dictionary<string, object?>(record) { ["body"] = shadowBody });
7280+
}
72107281
}
72117282

72127283
if (subs.Count > 0)
@@ -7287,6 +7358,12 @@ or ImplicitObjectCreationExpressionSyntax } init
72877358

72887359
if (outPath is null) Console.WriteLine(json);
72897360
else File.WriteAllText(outPath, json);
7361+
// P-037 PCS-0: the shadow document — the plain shape, only `functions[].body` differs.
7362+
if (CanonicalShadowPath is not null)
7363+
File.WriteAllText(CanonicalShadowPath, JsonSerializer.Serialize(
7364+
new { ownir_version = 0, module = "Extracted", components, services = factServices,
7365+
functions = shadowFunctions, stats = factStats },
7366+
new JsonSerializerOptions { WriteIndented = true }));
72907367
DispatchReport.Write();
72917368
return 0;
72927369

@@ -7298,6 +7375,11 @@ partial class Program
72987375
{
72997376
internal static bool BodyThrowEdges;
73007377

7378+
// P-037 PCS-0 (measurement only): where the canonical-forward shadow document goes, and
7379+
// the switch the SECOND lowering of each method runs under. Off for the ordinary lowering.
7380+
internal static string? CanonicalShadowPath;
7381+
internal static bool CanonicalForwards;
7382+
73017383
// #240 (Codex review): full paths of files compiled by a Fody-enabled PROJECT, recorded
73027384
// while expanding its <Compile> items — a linked source outside the project directory
73037385
// would otherwise dodge the ancestor-walk weaver check (the FodyWeavers.xml lives next

0 commit comments

Comments
 (0)