1313// [ProtocolToken] a ref struct: every instance METHOD is a transition that consumes the
1414// token (and borrows the entity for the call); a PROPERTY is a read.
1515//
16+ // RESERVED NAMES. Matching by name makes the two names a convention the profile owns, but only
17+ // in those shapes: a method marked [ProtocolRegion] that takes a delegate, a ref struct marked
18+ // [ProtocolToken]. An attribute of the same name on anything else (a class, a plain struct, a
19+ // method with no callback) belongs to somebody else's code and is ignored — a scan of a
20+ // codebase that declares no state protocol is never refused over a name.
21+ //
1622// This is a LOWERING, not an analysis. It reads the syntax of one method at a time and looks
1723// symbols up; it never tracks state across statements, never follows aliases, and never
1824// decides whether a program is right. Everything it emits is checked by the core:
@@ -93,6 +99,9 @@ private sealed class Ctx
9399 public Dictionary < ISymbol , ( string Name , Region Region ) > Tokens { get ; } =
94100 new ( SymbolEqualityComparer . Default ) ;
95101 public HashSet < SyntaxNode > LoweredEntries { get ; } = new ( ) ;
102+ /// The lambdas that were lowered as region bodies: the only callables in consumer
103+ /// code that may take a token.
104+ public HashSet < SyntaxNode > LoweredBodies { get ; } = new ( ) ;
96105 public int Counter ;
97106 /// How many times the scan has lowered a mention of a borrowed entity. A construct
98107 /// that runs unknown code is refused UNLESS it moved this counter: then the core
@@ -118,6 +127,8 @@ public static Result Lower(Compilation compilation,
118127 var model = compilation . GetSemanticModel ( tree ) ;
119128 var root = tree . GetRoot ( ) ;
120129 var loweredEntries = new HashSet < SyntaxNode > ( ) ;
130+ var loweredBodies = new HashSet < SyntaxNode > ( ) ;
131+ var refusedMethods = new List < SyntaxNode > ( ) ;
121132
122133 foreach ( var method in root . DescendantNodes ( ) . OfType < BaseMethodDeclarationSyntax > ( ) )
123134 {
@@ -189,6 +200,7 @@ public static Result Lower(Compilation compilation,
189200 throw new Refused ( At ( file , v ,
190201 $ "protocol token '{ l . Name } ' is declared outside a protocol region") ) ;
191202 loweredEntries . UnionWith ( ctx . LoweredEntries ) ;
203+ loweredBodies . UnionWith ( ctx . LoweredBodies ) ;
192204
193205 if ( ops . Count > 0 )
194206 functions . Add ( new Dictionary < string , object ? >
@@ -199,9 +211,33 @@ public static Result Lower(Compilation compilation,
199211 catch ( Refused r )
200212 {
201213 refusals . Add ( r . Message ) ;
214+ refusedMethods . Add ( method ) ;
202215 }
203216 }
204217
218+ // A token exists only inside a region. A method cannot take one (refused above),
219+ // and a lambda or local function may take one only as the in-place body of a
220+ // region entry that was lowered. Any other callable with a token parameter is
221+ // consumer code that receives a token some other way — from a method of the
222+ // protocol's own type that is not marked as a region entry, typically — and that
223+ // nothing reads: without this it is simply absent from the facts, and a token
224+ // spent twice in it is clean.
225+ foreach ( var callable in root . DescendantNodes ( ) . Where ( n =>
226+ n is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax ) )
227+ {
228+ if ( loweredBodies . Contains ( callable )
229+ || refusedMethods . Any ( m => m . Span . Contains ( callable . Span ) ) )
230+ continue ; // a lowered region body, or inside a method already refused
231+ var symbol = callable is LocalFunctionStatementSyntax
232+ ? model . GetDeclaredSymbol ( callable ) as IMethodSymbol
233+ : model . GetSymbolInfo ( callable ) . Symbol as IMethodSymbol ;
234+ var taken = symbol ? . Parameters . FirstOrDefault ( p => IsToken ( p . Type ) ) ;
235+ if ( taken is null || Enclosing ( model , callable ) . Any ( IsApiType ) )
236+ continue ; // no token; or the protocol's own types, which hand tokens out
237+ refusals . Add ( At ( file , callable ,
238+ $ "a callback that takes protocol token '{ taken . Type . Name } ' is not the in-place body of a region entry: a token exists only inside a region, and code that receives one any other way is not analysed") ) ;
239+ }
240+
205241 // A region entry outside any method body (an accessor, a field initializer, a
206242 // top-level statement) is not modelled either.
207243 foreach ( var inv in root . DescendantNodes ( ) . OfType < InvocationExpressionSyntax > ( ) )
@@ -481,7 +517,7 @@ bool Outside(INamedTypeSymbol? owner)
481517 ? info . ConvertedType : info . Type ?? info . ConvertedType ) ? . OriginalDefinition ;
482518 if ( IsToken ( made ) && ! Within ( api ) )
483519 refusals . Add ( At ( file , node ,
484- $ "a protocol token '{ made ! . Name } ' is created outside the protocol's own types: a token comes only from a region entry or a transition") ) ;
520+ $ "a protocol token '{ made ! . Name } ' is created outside the protocol's own types: a token comes only from a region entry or a transition" + ReservedNames ) ) ;
485521 else if ( node is BaseObjectCreationExpressionSyntax
486522 && Bound ( model . GetSymbolInfo ( node ) ) is IMethodSymbol
487523 { DeclaredAccessibility : not Accessibility . Public } ctor
@@ -583,11 +619,37 @@ private static bool HasAttribute(ISymbol? symbol, string name) =>
583619 symbol is not null && symbol . GetAttributes ( ) . Any ( a =>
584620 a . AttributeClass ? . Name == name || a . AttributeClass ? . Name == name + "Attribute" ) ;
585621
586- private static bool IsToken ( ITypeSymbol ? type ) => HasAttribute ( type , "ProtocolToken" ) ;
622+ // The two attributes are matched by NAME, so the names are reserved — but only in the
623+ // SHAPE the profile gives them. A codebase that never declared a state protocol can own an
624+ // attribute called ProtocolToken (a wire-protocol token class, say), and a name alone must
625+ // not turn its scan into a refusal. So:
626+ //
627+ // a state token is a REF STRUCT marked [ProtocolToken];
628+ // a region entry is a method marked [ProtocolRegion] that TAKES A DELEGATE.
629+ //
630+ // Anything else carrying one of the names is not part of a protocol and is left alone.
631+ // The shapes are deliberately the widest ones that still catch a protocol declared wrongly:
632+ // a marked region entry whose callback does not take a token, or takes a marked type that
633+ // is not a ref struct, is still a region entry and is refused where it is used.
634+
635+ /// Marked [ProtocolToken], whatever it is. Only for saying WHY a marked type is not a token.
636+ private static bool HasTokenName ( ITypeSymbol ? type ) => HasAttribute ( type , "ProtocolToken" ) ;
637+
638+ private static bool IsToken ( ITypeSymbol ? type ) =>
639+ type is { IsRefLikeType : true } && HasTokenName ( type ) ;
640+
641+ private static bool IsRegionEntry ( IMethodSymbol ? method )
642+ {
643+ if ( method is null )
644+ return false ;
645+ var declared = ( method . ReducedFrom ?? method ) . OriginalDefinition ;
646+ // an UNRESOLVED parameter type may be the callback of a degraded scan: keep it in
647+ return HasAttribute ( declared , "ProtocolRegion" )
648+ && declared . Parameters . Any ( p => p . Type . TypeKind is TypeKind . Delegate or TypeKind . Error ) ;
649+ }
587650
588- private static bool IsRegionEntry ( IMethodSymbol ? method ) =>
589- method is not null && HasAttribute ( ( method . ReducedFrom ?? method ) . OriginalDefinition ,
590- "ProtocolRegion" ) ;
651+ private const string ReservedNames =
652+ " (the attribute names ProtocolRegion and ProtocolToken are reserved in these shapes: a method marked [ProtocolRegion] that takes a delegate is a region entry, a ref struct marked [ProtocolToken] is a state token)" ;
591653
592654 private static bool IsApiType ( INamedTypeSymbol ? type ) =>
593655 type is not null && ( IsToken ( type )
@@ -759,7 +821,7 @@ private static void LowerRegion(Ctx ctx, InvocationExpressionSyntax entry, List<
759821 {
760822 var args = entry . ArgumentList . Arguments ;
761823 if ( args . Count != 2 )
762- throw Refuse ( ctx , entry , "a protocol region entry takes (entity, callback)" ) ;
824+ throw Refuse ( ctx , entry , "a protocol region entry takes (entity, callback)" + ReservedNames ) ;
763825 var entityExpr = Unparen ( args [ 0 ] . Expression ) ;
764826 var entity = ctx . Model . GetSymbolInfo ( entityExpr ) . Symbol ;
765827 if ( entityExpr is not IdentifierNameSyntax || entity is not ( ILocalSymbol or IParameterSymbol ) )
@@ -779,9 +841,9 @@ private static void LowerRegion(Ctx ctx, InvocationExpressionSyntax entry, List<
779841 } ;
780842 if ( parameter is null
781843 || ctx . Model . GetDeclaredSymbol ( parameter ) is not IParameterSymbol token
782- || ! IsToken ( token . Type ) )
844+ || ! HasTokenName ( token . Type ) )
783845 throw Refuse ( ctx , entry ,
784- "the callback of a protocol region takes exactly one protocol token" ) ;
846+ "the callback of a protocol region takes exactly one protocol token" + ReservedNames ) ;
785847 // A token that is not a ref struct can be captured by a nested lambda, stored in a
786848 // field, or carried across an await — every one of which lets it reach ANOTHER
787849 // region, the shape the core cannot check. The language forbids all three for a
@@ -831,6 +893,7 @@ private static void LowerRegion(Ctx ctx, InvocationExpressionSyntax entry, List<
831893 if ( existing is null )
832894 ops . Add ( Release ( owner , line ) ) ;
833895 ctx . LoweredEntries . Add ( entry ) ;
896+ ctx . LoweredBodies . Add ( lambda ) ;
834897 }
835898
836899 private static void LowerTokenLocal ( Ctx ctx , ILocalSymbol local , ExpressionSyntax ? init ,
0 commit comments