@@ -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+
29583000static 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)
56615721var components = new List < object > ( ) ;
56625722// P-016 B0b/B2: per-method flow bodies (only when --flow-locals).
56635723var 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
72887359if ( outPath is null ) Console . WriteLine ( json ) ;
72897360else 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 } ) ) ;
72907367DispatchReport . Write ( ) ;
72917368return 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