@ -220,6 +220,32 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
@@ -220,6 +220,32 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{
var builder = new StringBuilder ( ) ;
WriteGeneratedMembersHeader ( builder , source ) ;
WritePatternPlaceholder ( builder , source ) ;
WriteVisitorOverrides ( builder , source ) ;
WriteDoMatch ( builder , source ) ;
if ( source . Slots is { } slotsArray )
{
var slots = slotsArray . ToList ( ) ;
WriteSlotProperties ( builder , slots ) ;
WriteNameAccessors ( builder , source . NameAccessors ) ;
WriteConstructors ( builder , source , slots ) ;
WriteChildAccessors ( builder , slots ) ;
WriteSlotInfoFields ( builder , slots ) ;
WriteChildSlotInfo ( builder , slots ) ;
WriteCollectionLookup ( builder , slots ) ;
WriteCloneChildrenInto ( builder , source , slots ) ;
}
// Close the class, trimming the blank line the per-member spacer leaves before the brace.
string body = builder . ToString ( ) . TrimEnd ( ) + "\n}\n" ;
context . AddSource ( source . NodeName + ".g.cs" , SourceText . From ( body . Replace ( "\r\n" , "\n" ) , Encoding . UTF8 ) ) ;
}
static void WriteGeneratedMembersHeader ( StringBuilder builder , AstNodeAdditions source )
{
builder . AppendLine ( "// <auto-generated/>" ) ;
builder . AppendLine ( "#nullable enable" ) ;
builder . AppendLine ( ) ;
@ -237,19 +263,23 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
@@ -237,19 +263,23 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
builder . AppendLine ( $"partial class {source.NodeName}" ) ;
builder . AppendLine ( "{" ) ;
}
if ( source . NeedsPatternPlaceholder )
{
// The placeholder conversion is part of the pattern-construction DSL, where a non-null
// pattern is the invariant; the result is therefore non-nullable for the specific node
// types. AstNode (the base) and ParameterDeclaration keep the nullable contract, and only
// AstNode also accepts a nullable pattern.
bool nullableReturn = source . NodeName is "AstNode" or "ParameterDeclaration" ;
string returnQ = nullableReturn ? "?" : "" ;
string paramQ = source . NodeName = = "AstNode" ? "?" : "" ;
string forgive = nullableReturn ? "" : "!" ;
builder . Append (
$ @ " public static implicit operator {source.NodeName}{returnQ}(PatternMatching.Pattern{paramQ} pattern)
static void WritePatternPlaceholder ( StringBuilder builder , AstNodeAdditions source )
{
if ( ! source . NeedsPatternPlaceholder )
return ;
// The placeholder conversion is part of the pattern-construction DSL, where a non-null
// pattern is the invariant; the result is therefore non-nullable for the specific node
// types. AstNode (the base) and ParameterDeclaration keep the nullable contract, and only
// AstNode also accepts a nullable pattern.
bool nullableReturn = source . NodeName is "AstNode" or "ParameterDeclaration" ;
string returnQ = nullableReturn ? "?" : "" ;
string paramQ = source . NodeName = = "AstNode" ? "?" : "" ;
string forgive = nullableReturn ? "" : "!" ;
builder . Append (
$ @ " public static implicit operator {source.NodeName}{returnQ}(PatternMatching.Pattern{paramQ} pattern)
{ {
return pattern ! = null ? new PatternPlaceholder ( pattern ) : null { forgive } ;
} }
@ -289,12 +319,15 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
@@ -289,12 +319,15 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
} }
} }
"
) ;
}
) ;
}
if ( source . NeedsVisitor )
{
builder . Append ( $ @ " public override void AcceptVisitor(IAstVisitor visitor)
static void WriteVisitorOverrides ( StringBuilder builder , AstNodeAdditions source )
{
if ( ! source . NeedsVisitor )
return ;
builder . Append ( $ @ " public override void AcceptVisitor(IAstVisitor visitor)
{ {
visitor . Visit { source . VisitMethodName } ( this ) ;
} }
@ -310,381 +343,444 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
@@ -310,381 +343,444 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
} }
");
}
}
if ( source . MembersToMatch ! = null )
{
builder . Append ( $ @ " protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match)
static void WriteDoMatch ( StringBuilder builder , AstNodeAdditions source )
{
if ( source . MembersToMatch = = null )
return ;
builder . Append ( $ @ " protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match)
{ {
return other is { source . NodeName } o ");
foreach ( var ( member , typeName , recursive , hasAny , nullable ) in source . MembersToMatch )
{
if ( member = = "MatchAttributesAndModifiers" )
{
builder . Append ( $"\r\n\t\t\t&& this.MatchAttributesAndModifiers(o, match)" ) ;
}
else if ( recursive & & nullable & & typeName ! = "AstNodeCollection" )
{
// An optional single-value child is null when absent; match null-safely.
builder . Append ( $"\r\n\t\t\t&& MatchOptional(this.{member}, o.{member}, match)" ) ;
}
else if ( recursive )
{
builder . Append ( $"\r\n\t\t\t&& this.{member}.DoMatch(o.{member}, match)" ) ;
}
else if ( hasAny )
{
builder . Append ( $"\r\n\t\t\t&& (this.{member} == {typeName}.Any || this.{member} == o.{member})" ) ;
}
else if ( typeName = = "String" )
{
builder . Append ( $"\r\n\t\t\t&& MatchString(this.{member}, o.{member})" ) ;
}
else
{
builder . Append ( $"\r\n\t\t\t&& this.{member} == o.{member}" ) ;
}
}
foreach ( var ( member , typeName , recursive , hasAny , nullable ) in source . MembersToMatch )
builder . Append ( DoMatchTerm ( member , typeName , recursive , hasAny , nullable ) ) ;
builder . Append ( @ ";
builder . Append ( @ ";
}
");
}
static string DoMatchTerm ( string member , string typeName , bool recursive , bool hasAny , bool nullable )
{
if ( member = = "MatchAttributesAndModifiers" )
return $"\r\n\t\t\t&& this.MatchAttributesAndModifiers(o, match)" ;
// An optional single-value child is null when absent; match null-safely.
if ( recursive & & nullable & & typeName ! = "AstNodeCollection" )
return $"\r\n\t\t\t&& MatchOptional(this.{member}, o.{member}, match)" ;
if ( recursive )
return $"\r\n\t\t\t&& this.{member}.DoMatch(o.{member}, match)" ;
if ( hasAny )
return $"\r\n\t\t\t&& (this.{member} == {typeName}.Any || this.{member} == o.{member})" ;
if ( typeName = = "String" )
return $"\r\n\t\t\t&& MatchString(this.{member}, o.{member})" ;
return $"\r\n\t\t\t&& this.{member} == o.{member}" ;
}
static void WriteSlotProperties ( StringBuilder builder , List < SlotInfo > slots )
{
// Backing fields and the partial-property bodies. A single slot stores a nullable backing
// field (returned null-forgiving for a required child, nullable for an optional one); a
// collection slot owns a lazily created AstNodeCollection bound to this node. A slot re-declared from an inherited contract
// member (Part I.3 flatten) is an override.
// A collection can maintain its children's flattened indices incrementally only when it is the
// node's sole collection and its last slot: then it owns the contiguous range [slotIndex, ..)
// with nothing after it, so an element's index is slotIndex + its local position (all
// preceding slots are single children, one index each).
int collectionCount = slots . Count ( s = > s . IsCollection ) ;
for ( int slotIndex = 0 ; slotIndex < slots . Count ; slotIndex + + )
WriteSlotProperty ( builder , slots , slotIndex , collectionCount ) ;
}
static void WriteSlotProperty ( StringBuilder builder , List < SlotInfo > slots , int slotIndex , int collectionCount )
{
var ( isCollection , name , type , elementType , isOverride , isNullable , kindName , isPartial ) = slots [ slotIndex ] ;
string field = FieldName ( name ) ;
string partialKw = isPartial ? "partial " : "" ;
if ( isCollection )
{
bool supportsIncremental = collectionCount = = 1 & & slotIndex = = slots . Count - 1 ;
builder . AppendLine ( $"\tAstNodeCollection<{elementType}>? {field};" ) ;
builder . AppendLine ( $"\tpublic {(isOverride ? " override " : " ")}{partialKw}AstNodeCollection<{elementType}> {name} => {field} ??= new AstNodeCollection<{elementType}>(this, Slots.{kindName}, {slotIndex}, {(supportsIncremental ? " true " : " false ")});" ) ;
}
else
{
// A single slot occupies a fixed flattened index. When no collection precedes it, that
// index is the constant slotIndex, so the setter can assign childIndex directly (the ILAst
// pattern -- no invalidate, no renumber). After a collection the index is dynamic, so fall
// back to the index-less setter (which invalidates on a set/clear).
bool constIndex = ! slots . Take ( slotIndex ) . Any ( s = > s . IsCollection ) ;
builder . AppendLine ( $"\t{type}? {field};" ) ;
builder . AppendLine ( $"\tpublic {(isOverride ? " override " : " ")}{partialKw}{type}{(isNullable ? " ? " : " ")} {name}" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( $"\t\tget => {field}{(isNullable ? "" : " ! ")};" ) ;
if ( constIndex )
builder . AppendLine ( $"\t\tset => SetChildNode(ref {field}, value, {slotIndex});" ) ;
else
builder . AppendLine ( $"\t\tset => SetChildNode(ref {field}, value);" ) ;
builder . AppendLine ( "\t}" ) ;
}
builder . AppendLine ( ) ;
}
if ( source . Slots is { } slotsArray )
static void WriteNameAccessors ( StringBuilder builder , EquatableArray < NameAccessor > ? nameAccessors )
{
// A string [Slot] is a convenience accessor over its generated Identifier token slot.
if ( nameAccessors is not { } nameAccessorsArray )
return ;
foreach ( var ( stringName , tokenName , isOptional ) in nameAccessorsArray )
WriteNameAccessor ( builder , stringName , tokenName , isOptional ) ;
}
static void WriteNameAccessor ( StringBuilder builder , string stringName , string tokenName , bool isOptional )
{
// The token factory is the type 'Identifier'. A [Slot] string property literally named
// "Identifier" (e.g. SimpleType.Identifier) shadows that type inside its own setter, so the
// bare name would bind to the string property; qualify with global:: only in that case.
string createType = stringName = = "Identifier"
? "global::ICSharpCode.Decompiler.CSharp.Syntax.Identifier"
: "Identifier" ;
builder . AppendLine ( $"\tpublic partial string{(isOptional ? " ? " : " ")} {stringName}" ) ;
builder . AppendLine ( "\t{" ) ;
if ( isOptional )
{
var slots = slotsArray . ToList ( ) ;
// Optional name: the backing token is a nullable slot. An absent name reads as null; an
// empty or null name clears the token, so "" and null both mean "no name".
builder . AppendLine ( $"\t\tget => {tokenName}?.Name;" ) ;
builder . AppendLine ( $"\t\tset => {tokenName} = {createType}.CreateIfNotEmpty(value);" ) ;
}
else
{
builder . AppendLine ( $"\t\tget => {tokenName}.Name;" ) ;
builder . AppendLine ( $"\t\tset => {tokenName} = {createType}.Create(value);" ) ;
}
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
// Backing fields and the partial-property bodies. A single slot stores a nullable backing
// field (returned null-forgiving for a required child, nullable for an optional one); a
// collection slot owns a lazily created AstNodeCollection bound to this node. A slot re-declared from an inherited contract
// member (Part I.3 flatten) is an override.
// A collection can maintain its children's flattened indices incrementally only when it is the
// node's sole collection and its last slot: then it owns the contiguous range [slotIndex, ..)
// with nothing after it, so an element's index is slotIndex + its local position (all
// preceding slots are single children, one index each).
int collectionCount = slots . Count ( s = > s . IsCollection ) ;
for ( int slotIndex = 0 ; slotIndex < slots . Count ; slotIndex + + )
{
var ( isCollection , name , type , elementType , isOverride , isNullable , kindName , isPartial ) = slots [ slotIndex ] ;
string field = FieldName ( name ) ;
string partialKw = isPartial ? "partial " : "" ;
if ( isCollection )
{
bool supportsIncremental = collectionCount = = 1 & & slotIndex = = slots . Count - 1 ;
builder . AppendLine ( $"\tAstNodeCollection<{elementType}>? {field};" ) ;
builder . AppendLine ( $"\tpublic {(isOverride ? " override " : " ")}{partialKw}AstNodeCollection<{elementType}> {name} => {field} ??= new AstNodeCollection<{elementType}>(this, Slots.{kindName}, {slotIndex}, {(supportsIncremental ? " true " : " false ")});" ) ;
}
else
{
// A single slot occupies a fixed flattened index. When no collection precedes it, that
// index is the constant slotIndex, so the setter can assign childIndex directly (the ILAst
// pattern -- no invalidate, no renumber). After a collection the index is dynamic, so fall
// back to the index-less setter (which invalidates on a set/clear).
bool constIndex = ! slots . Take ( slotIndex ) . Any ( s = > s . IsCollection ) ;
builder . AppendLine ( $"\t{type}? {field};" ) ;
builder . AppendLine ( $"\tpublic {(isOverride ? " override " : " ")}{partialKw}{type}{(isNullable ? " ? " : " ")} {name}" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( $"\t\tget => {field}{(isNullable ? "" : " ! ")};" ) ;
if ( constIndex )
builder . AppendLine ( $"\t\tset => SetChildNode(ref {field}, value, {slotIndex});" ) ;
else
builder . AppendLine ( $"\t\tset => SetChildNode(ref {field}, value);" ) ;
builder . AppendLine ( "\t}" ) ;
}
builder . AppendLine ( ) ;
}
static void WriteConstructors ( StringBuilder builder , AstNodeAdditions source , List < SlotInfo > slots )
{
// Constructors. Parameters follow member source order and cover single/collection [Slot] children,
// the string [Slot], and settable enum scalars (Operator, FieldDirection, ...); a collection is
// an IEnumerable<T> param in its declared position. We emit the empty ctor (for object-initializer
// construction), then a ctor for the required prefix (through the last required param), one ending at
// each collection, and one with all params. A params T[] overload is added when a ctor's last param
// is the collection. Pure-scalar nodes (no [Slot], e.g. PrimitiveExpression) are excluded
// because their non-enum state (a literal value) is invisible here; those keep hand-written ctors.
if ( source . IsAbstract | | ! source . BaseHasDefaultConstructor | | slots . Count = = 0 | | source . CtorParams is not { } ctorParamsArray )
return ;
var cp = ctorParamsArray . ToList ( ) ;
builder . AppendLine ( $"\tpublic {source.NodeName}()" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
// A string [Slot] is a convenience accessor over its generated Identifier token slot.
if ( source . NameAccessors is { } nameAccessorsArray )
{
foreach ( var ( stringName , tokenName , isOptional ) in nameAccessorsArray )
{
// The token factory is the type 'Identifier'. A [Slot] string property literally named
// "Identifier" (e.g. SimpleType.Identifier) shadows that type inside its own setter, so the
// bare name would bind to the string property; qualify with global:: only in that case.
string createType = stringName = = "Identifier"
? "global::ICSharpCode.Decompiler.CSharp.Syntax.Identifier"
: "Identifier" ;
builder . AppendLine ( $"\tpublic partial string{(isOptional ? " ? " : " ")} {stringName}" ) ;
builder . AppendLine ( "\t{" ) ;
if ( isOptional )
{
// Optional name: the backing token is a nullable slot. An absent name reads as null; an
// empty or null name clears the token, so "" and null both mean "no name".
builder . AppendLine ( $"\t\tget => {tokenName}?.Name;" ) ;
builder . AppendLine ( $"\t\tset => {tokenName} = {createType}.CreateIfNotEmpty(value);" ) ;
}
else
{
builder . AppendLine ( $"\t\tget => {tokenName}.Name;" ) ;
builder . AppendLine ( $"\t\tset => {tokenName} = {createType}.Create(value);" ) ;
}
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
}
int reqLen = RequiredConstructorPrefixLength ( cp ) ;
int prev = 0 ;
foreach ( int len in ConstructorPrefixLengths ( cp , reqLen ) )
{
if ( len < = 0 )
continue ;
WriteConstructorPrefix ( builder , source . NodeName , cp , len , prev , paramsForm : false ) ;
if ( cp [ len - 1 ] . IsCollection )
WriteConstructorPrefix ( builder , source . NodeName , cp , len , len , paramsForm : true ) ;
prev = len ;
}
}
// Constructors. Parameters follow member source order and cover single/collection [Slot] children,
// the string [Slot], and settable enum scalars (Operator, FieldDirection, ...); a collection is
// an IEnumerable<T> param in its declared position. We emit the empty ctor (for object-initializer
// construction), then a ctor for the required prefix (through the last required param), one ending at
// each collection, and one with all params. A params T[] overload is added when a ctor's last param
// is the collection. Pure-scalar nodes (no [Slot], e.g. PrimitiveExpression) are excluded
// because their non-enum state (a literal value) is invisible here; those keep hand-written ctors.
if ( ! source . IsAbstract & & source . BaseHasDefaultConstructor & & slots . Count > 0 & & source . CtorParams is { } ctorParamsArray )
{
var cp = ctorParamsArray . ToList ( ) ;
string ParamName ( string n )
{
string p = char . ToLowerInvariant ( n [ 0 ] ) + n . Substring ( 1 ) ;
return SyntaxFacts . GetKeywordKind ( p ) ! = SyntaxKind . None ? "@" + p : p ;
}
string ParamType ( int i ) = > cp [ i ] . IsCollection
? $"IEnumerable<{cp[i].ElementType}>"
: cp [ i ] . ParamType ;
builder . AppendLine ( $"\tpublic {source.NodeName}()" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
// Required prefix: through the last non-optional param (an optional param before it is still
// positionally included so the required param after it can be passed).
int reqLen = 0 ;
for ( int i = 0 ; i < cp . Count ; i + + )
if ( ! cp [ i ] . IsOptional )
reqLen = i + 1 ;
// A normal prefix ctor forwards to the previous (shorter) emitted prefix via : this(...) and only
// sets the params between them; the shortest sets its params directly. A params T[] overload
// forwards to the IEnumerable overload of the same length.
void EmitPrefix ( int len , int chainLen , bool paramsForm )
{
var decls = new List < string > ( ) ;
for ( int i = 0 ; i < len ; i + + )
{
if ( paramsForm & & i = = len - 1 )
decls . Add ( $"params {cp[i].ElementType}[] {ParamName(cp[i].PropertyName)}" ) ;
else
decls . Add ( $"{ParamType(i)} {ParamName(cp[i].PropertyName)}" ) ;
}
builder . AppendLine ( $"\tpublic {source.NodeName}({string.Join(" , ", decls)})" ) ;
if ( paramsForm )
{
var args = new List < string > ( ) ;
for ( int i = 0 ; i < len ; i + + )
args . Add ( i = = len - 1
? $"(IEnumerable<{cp[i].ElementType}>){ParamName(cp[i].PropertyName)}"
: ParamName ( cp [ i ] . PropertyName ) ) ;
builder . AppendLine ( $"\t\t: this({string.Join(" , ", args)})" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( "\t}" ) ;
}
else
{
if ( chainLen > 0 )
builder . AppendLine ( $"\t\t: this({string.Join(" , ", Enumerable.Range(0, chainLen).Select(i => ParamName(cp[i].PropertyName)))})" ) ;
builder . AppendLine ( "\t{" ) ;
for ( int i = chainLen ; i < len ; i + + )
{
if ( cp [ i ] . IsCollection )
builder . AppendLine ( $"\t\tthis.{cp[i].PropertyName}.AddRange({ParamName(cp[i].PropertyName)});" ) ;
else
builder . AppendLine ( $"\t\tthis.{cp[i].PropertyName} = {ParamName(cp[i].PropertyName)};" ) ;
}
builder . AppendLine ( "\t}" ) ;
}
builder . AppendLine ( ) ;
}
static int RequiredConstructorPrefixLength ( List < CtorParam > cp )
{
// Required prefix: through the last non-optional param (an optional param before it is still
// positionally included so the required param after it can be passed).
int reqLen = 0 ;
for ( int i = 0 ; i < cp . Count ; i + + )
if ( ! cp [ i ] . IsOptional )
reqLen = i + 1 ;
return reqLen ;
}
var lengths = new SortedSet < int > ( ) ;
if ( reqLen > 0 )
lengths . Add ( reqLen ) ;
for ( int i = 0 ; i < cp . Count ; i + + )
if ( cp [ i ] . IsCollection & & i + 1 > = reqLen )
lengths . Add ( i + 1 ) ;
lengths . Add ( cp . Count ) ;
int prev = 0 ;
foreach ( int len in lengths )
{
if ( len < = 0 )
continue ;
EmitPrefix ( len , prev , paramsForm : false ) ;
if ( cp [ len - 1 ] . IsCollection )
EmitPrefix ( len , len , paramsForm : true ) ;
prev = len ;
}
}
static SortedSet < int > ConstructorPrefixLengths ( List < CtorParam > cp , int reqLen )
{
var lengths = new SortedSet < int > ( ) ;
if ( reqLen > 0 )
lengths . Add ( reqLen ) ;
for ( int i = 0 ; i < cp . Count ; i + + )
if ( cp [ i ] . IsCollection & & i + 1 > = reqLen )
lengths . Add ( i + 1 ) ;
lengths . Add ( cp . Count ) ;
return lengths ;
}
// Flattened child-index space: slots in declaration order, a single slot occupying one index
// (even when empty), a collection slot a contiguous run of its current length.
var countTerms = new List < string > ( ) ;
int singleSlotCount = slots . Count ( s = > ! s . IsCollection ) ;
if ( singleSlotCount > 0 )
countTerms . Add ( singleSlotCount . ToString ( ) ) ;
foreach ( var s in slots . Where ( s = > s . IsCollection ) )
countTerms . Add ( $"({FieldName(s.PropertyName)}?.Count ?? 0)" ) ;
builder . AppendLine ( $"\tinternal override int GetChildCount() => {string.Join(" + ", countTerms)};" ) ;
builder . AppendLine ( ) ;
static void WriteConstructorPrefix ( StringBuilder builder , string nodeName , List < CtorParam > cp , int len , int chainLen , bool paramsForm )
{
// A normal prefix ctor forwards to the previous (shorter) emitted prefix via : this(...) and only
// sets the params between them; the shortest sets its params directly. A params T[] overload
// forwards to the IEnumerable overload of the same length.
builder . AppendLine ( $"\tpublic {nodeName}({string.Join(" , ", ConstructorParameterDeclarations(cp, len, paramsForm))})" ) ;
if ( paramsForm )
WriteParamsConstructorBody ( builder , cp , len ) ;
else
WriteNormalConstructorBody ( builder , cp , len , chainLen ) ;
builder . AppendLine ( ) ;
}
bool anyCollection = slots . Any ( s = > s . IsCollection ) ;
static IEnumerable < string > ConstructorParameterDeclarations ( List < CtorParam > cp , int len , bool paramsForm )
{
for ( int i = 0 ; i < len ; i + + )
{
if ( paramsForm & & i = = len - 1 )
yield return $"params {cp[i].ElementType}[] {ParamName(cp[i].PropertyName)}" ;
else
yield return $"{ParamType(cp[i])} {ParamName(cp[i].PropertyName)}" ;
}
}
// Emits a method body that maps a flat child index to a slot and returns an expression for it.
// With only single slots the index is a constant offset, so a switch reads best; once a
// collection slot is present the widths are dynamic, so walk the slots subtracting each one's
// length from a running index.
void EmitReturnDispatch ( Func < int , string > singleExpr , Func < int , string > collectionExpr )
{
if ( ! anyCollection )
{
builder . AppendLine ( "\t\tswitch (index)" ) ;
builder . AppendLine ( "\t\t{" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
builder . AppendLine ( $"\t\t\tcase {k}:" ) ;
builder . AppendLine ( $"\t\t\t\treturn {singleExpr(k)};" ) ;
}
builder . AppendLine ( "\t\t\tdefault:" ) ;
builder . AppendLine ( "\t\t\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
builder . AppendLine ( "\t\t}" ) ;
return ;
}
builder . AppendLine ( "\t\tint i = index;" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
bool last = k = = slots . Count - 1 ;
if ( slots [ k ] . IsCollection )
{
string field = FieldName ( slots [ k ] . PropertyName ) ;
builder . AppendLine ( "\t\t{" ) ;
builder . AppendLine ( $"\t\t\tint n = {field}?.Count ?? 0;" ) ;
builder . AppendLine ( "\t\t\tif (i < n)" ) ;
builder . AppendLine ( $"\t\t\t\treturn {collectionExpr(k)};" ) ;
if ( ! last )
builder . AppendLine ( "\t\t\ti -= n;" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
else
{
builder . AppendLine ( "\t\tif (i == 0)" ) ;
builder . AppendLine ( $"\t\t\treturn {singleExpr(k)};" ) ;
if ( ! last )
builder . AppendLine ( "\t\ti--;" ) ;
}
}
builder . AppendLine ( "\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
}
static void WriteParamsConstructorBody ( StringBuilder builder , List < CtorParam > cp , int len )
{
builder . AppendLine ( $"\t\t: this({string.Join(" , ", ParamsConstructorArguments(cp, len))})" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( "\t}" ) ;
}
builder . AppendLine ( "\tinternal override AstNode? GetChild(int index)" ) ;
builder . AppendLine ( "\t{" ) ;
EmitReturnDispatch ( k = > FieldName ( slots [ k ] . PropertyName ) , k = > $"{FieldName(slots[k].PropertyName)}![i]" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
static IEnumerable < string > ParamsConstructorArguments ( List < CtorParam > cp , int len )
{
for ( int i = 0 ; i < len ; i + + )
{
if ( i = = len - 1 )
yield return $"(IEnumerable<{cp[i].ElementType}>){ParamName(cp[i].PropertyName)}" ;
else
yield return ParamName ( cp [ i ] . PropertyName ) ;
}
}
builder . AppendLine ( "\tinternal override void SetChild(int index, AstNode? value)" ) ;
builder . AppendLine ( "\t{" ) ;
if ( ! anyCollection )
{
builder . AppendLine ( "\t\tswitch (index)" ) ;
builder . AppendLine ( "\t\t{" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
builder . AppendLine ( $"\t\t\tcase {k}:" ) ;
builder . AppendLine ( $"\t\t\t\tSetChildNode(ref {FieldName(slots[k].PropertyName)}, ({slots[k].PropertyType}?)value, index);" ) ;
builder . AppendLine ( "\t\t\t\treturn;" ) ;
}
builder . AppendLine ( "\t\t\tdefault:" ) ;
builder . AppendLine ( "\t\t\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
static void WriteNormalConstructorBody ( StringBuilder builder , List < CtorParam > cp , int len , int chainLen )
{
if ( chainLen > 0 )
builder . AppendLine ( $"\t\t: this({string.Join(" , ", Enumerable.Range(0, chainLen).Select(i => ParamName(cp[i].PropertyName)))})" ) ;
builder . AppendLine ( "\t{" ) ;
for ( int i = chainLen ; i < len ; i + + )
{
if ( cp [ i ] . IsCollection )
builder . AppendLine ( $"\t\tthis.{cp[i].PropertyName}.AddRange({ParamName(cp[i].PropertyName)});" ) ;
else
{
builder . AppendLine ( "\t\tint i = index;" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
bool last = k = = slots . Count - 1 ;
var s = slots [ k ] ;
string field = FieldName ( s . PropertyName ) ;
if ( s . IsCollection )
{
builder . AppendLine ( "\t\t{" ) ;
builder . AppendLine ( $"\t\t\tint n = {field}?.Count ?? 0;" ) ;
builder . AppendLine ( "\t\t\tif (i < n)" ) ;
builder . AppendLine ( "\t\t\t{" ) ;
builder . AppendLine ( $"\t\t\t\t{field}![i] = ({s.ElementType})value!;" ) ;
builder . AppendLine ( "\t\t\t\treturn;" ) ;
builder . AppendLine ( "\t\t\t}" ) ;
if ( ! last )
builder . AppendLine ( "\t\t\ti -= n;" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
else
{
builder . AppendLine ( "\t\tif (i == 0)" ) ;
builder . AppendLine ( "\t\t{" ) ;
builder . AppendLine ( $"\t\t\tSetChildNode(ref {field}, ({s.PropertyType}?)value, index);" ) ;
builder . AppendLine ( "\t\t\treturn;" ) ;
builder . AppendLine ( "\t\t}" ) ;
if ( ! last )
builder . AppendLine ( "\t\ti--;" ) ;
}
}
builder . AppendLine ( "\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
}
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
builder . AppendLine ( $"\t\tthis.{cp[i].PropertyName} = {ParamName(cp[i].PropertyName)};" ) ;
}
builder . AppendLine ( "\t}" ) ;
}
// One typed CSharpSlotInfo<T> static per slot; node.Slot compares against these by object
// identity, and the typed child accessors infer the child type from the slot.
foreach ( var s in slots )
builder . AppendLine ( $"\tpublic static readonly CSharpSlotInfo<{s.ElementType}> {s.PropertyName}Slot = new CSharpSlotInfo<{s.ElementType}>(\" { s . PropertyName } \ ", {(s.IsCollection ? " true " : " false ")}, Slots.{s.KindName}, {(s.IsCollection || s.IsNullable ? " true " : " false ")});" ) ;
builder . AppendLine ( ) ;
static string ParamName ( string n )
{
string p = char . ToLowerInvariant ( n [ 0 ] ) + n . Substring ( 1 ) ;
return SyntaxFacts . GetKeywordKind ( p ) ! = SyntaxKind . None ? "@" + p : p ;
}
builder . AppendLine ( "\tinternal override CSharpSlotInfo GetChildSlotInfo(int index)" ) ;
builder . AppendLine ( "\t{" ) ;
EmitReturnDispatch ( k = > $"{slots[k].PropertyName}Slot" , k = > $"{slots[k].PropertyName}Slot" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
static string ParamType ( CtorParam cp ) = > cp . IsCollection ? $"IEnumerable<{cp.ElementType}>" : cp . ParamType ;
static void WriteChildAccessors ( StringBuilder builder , List < SlotInfo > slots )
{
// Flattened child-index space: slots in declaration order, a single slot occupying one index
// (even when empty), a collection slot a contiguous run of its current length.
var countTerms = new List < string > ( ) ;
int singleSlotCount = slots . Count ( s = > ! s . IsCollection ) ;
if ( singleSlotCount > 0 )
countTerms . Add ( singleSlotCount . ToString ( ) ) ;
foreach ( var s in slots . Where ( s = > s . IsCollection ) )
countTerms . Add ( $"({FieldName(s.PropertyName)}?.Count ?? 0)" ) ;
builder . AppendLine ( $"\tinternal override int GetChildCount() => {string.Join(" + ", countTerms)};" ) ;
builder . AppendLine ( ) ;
builder . AppendLine ( "\tinternal override AstNode? GetChild(int index)" ) ;
builder . AppendLine ( "\t{" ) ;
WriteReturnDispatch ( builder , slots , k = > FieldName ( slots [ k ] . PropertyName ) , k = > $"{FieldName(slots[k].PropertyName)}![i]" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
builder . AppendLine ( "\tinternal override void SetChild(int index, AstNode? value)" ) ;
builder . AppendLine ( "\t{" ) ;
if ( slots . Any ( s = > s . IsCollection ) )
WriteSetChildWithCollections ( builder , slots ) ;
else
WriteSetChildSwitch ( builder , slots ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
static void WriteReturnDispatch ( StringBuilder builder , List < SlotInfo > slots , Func < int , string > singleExpr , Func < int , string > collectionExpr )
{
// Emits a method body that maps a flat child index to a slot and returns an expression for it.
// With only single slots the index is a constant offset, so a switch reads best; once a
// collection slot is present the widths are dynamic, so walk the slots subtracting each one's
// length from a running index.
if ( slots . Any ( s = > s . IsCollection ) )
WriteReturnDispatchWithCollections ( builder , slots , singleExpr , collectionExpr ) ;
else
WriteReturnDispatchSwitch ( builder , slots , singleExpr ) ;
}
static void WriteReturnDispatchSwitch ( StringBuilder builder , List < SlotInfo > slots , Func < int , string > singleExpr )
{
builder . AppendLine ( "\t\tswitch (index)" ) ;
builder . AppendLine ( "\t\t{" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
builder . AppendLine ( $"\t\t\tcase {k}:" ) ;
builder . AppendLine ( $"\t\t\t\treturn {singleExpr(k)};" ) ;
}
builder . AppendLine ( "\t\t\tdefault:" ) ;
builder . AppendLine ( "\t\t\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
static void WriteReturnDispatchWithCollections ( StringBuilder builder , List < SlotInfo > slots , Func < int , string > singleExpr , Func < int , string > collectionExpr )
{
builder . AppendLine ( "\t\tint i = index;" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
bool last = k = = slots . Count - 1 ;
if ( slots [ k ] . IsCollection )
WriteCollectionReturnDispatchStep ( builder , slots [ k ] , k , collectionExpr , last ) ;
else
WriteSingleReturnDispatchStep ( builder , k , singleExpr , last ) ;
}
builder . AppendLine ( "\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
}
static void WriteCollectionReturnDispatchStep ( StringBuilder builder , SlotInfo slot , int index , Func < int , string > collectionExpr , bool last )
{
string field = FieldName ( slot . PropertyName ) ;
builder . AppendLine ( "\t\t{" ) ;
builder . AppendLine ( $"\t\t\tint n = {field}?.Count ?? 0;" ) ;
builder . AppendLine ( "\t\t\tif (i < n)" ) ;
builder . AppendLine ( $"\t\t\t\treturn {collectionExpr(index)};" ) ;
if ( ! last )
builder . AppendLine ( "\t\t\ti -= n;" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
static void WriteSingleReturnDispatchStep ( StringBuilder builder , int index , Func < int , string > singleExpr , bool last )
{
builder . AppendLine ( "\t\tif (i == 0)" ) ;
builder . AppendLine ( $"\t\t\treturn {singleExpr(index)};" ) ;
if ( ! last )
builder . AppendLine ( "\t\ti--;" ) ;
}
static void WriteSetChildSwitch ( StringBuilder builder , List < SlotInfo > slots )
{
builder . AppendLine ( "\t\tswitch (index)" ) ;
builder . AppendLine ( "\t\t{" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
builder . AppendLine ( $"\t\t\tcase {k}:" ) ;
builder . AppendLine ( $"\t\t\t\tSetChildNode(ref {FieldName(slots[k].PropertyName)}, ({slots[k].PropertyType}?)value, index);" ) ;
builder . AppendLine ( "\t\t\t\treturn;" ) ;
}
builder . AppendLine ( "\t\t\tdefault:" ) ;
builder . AppendLine ( "\t\t\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
static void WriteSetChildWithCollections ( StringBuilder builder , List < SlotInfo > slots )
{
builder . AppendLine ( "\t\tint i = index;" ) ;
for ( int k = 0 ; k < slots . Count ; k + + )
{
bool last = k = = slots . Count - 1 ;
var s = slots [ k ] ;
if ( s . IsCollection )
WriteSetCollectionChildStep ( builder , s , last ) ;
else
WriteSetSingleChildStep ( builder , s , last ) ;
}
builder . AppendLine ( "\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));" ) ;
}
static void WriteSetCollectionChildStep ( StringBuilder builder , SlotInfo slot , bool last )
{
string field = FieldName ( slot . PropertyName ) ;
builder . AppendLine ( "\t\t{" ) ;
builder . AppendLine ( $"\t\t\tint n = {field}?.Count ?? 0;" ) ;
builder . AppendLine ( "\t\t\tif (i < n)" ) ;
builder . AppendLine ( "\t\t\t{" ) ;
builder . AppendLine ( $"\t\t\t\t{field}![i] = ({slot.ElementType})value!;" ) ;
builder . AppendLine ( "\t\t\t\treturn;" ) ;
builder . AppendLine ( "\t\t\t}" ) ;
if ( ! last )
builder . AppendLine ( "\t\t\ti -= n;" ) ;
builder . AppendLine ( "\t\t}" ) ;
}
static void WriteSetSingleChildStep ( StringBuilder builder , SlotInfo slot , bool last )
{
string field = FieldName ( slot . PropertyName ) ;
builder . AppendLine ( "\t\tif (i == 0)" ) ;
builder . AppendLine ( "\t\t{" ) ;
builder . AppendLine ( $"\t\t\tSetChildNode(ref {field}, ({slot.PropertyType}?)value, index);" ) ;
builder . AppendLine ( "\t\t\treturn;" ) ;
builder . AppendLine ( "\t\t}" ) ;
if ( ! last )
builder . AppendLine ( "\t\ti--;" ) ;
}
static void WriteSlotInfoFields ( StringBuilder builder , List < SlotInfo > slots )
{
// One typed CSharpSlotInfo<T> static per slot; node.Slot compares against these by object
// identity, and the typed child accessors infer the child type from the slot.
foreach ( var s in slots )
builder . AppendLine ( $"\tpublic static readonly CSharpSlotInfo<{s.ElementType}> {s.PropertyName}Slot = new CSharpSlotInfo<{s.ElementType}>(\" { s . PropertyName } \ ", {(s.IsCollection ? " true " : " false ")}, Slots.{s.KindName}, {(s.IsCollection || s.IsNullable ? " true " : " false ")});" ) ;
builder . AppendLine ( ) ;
}
static void WriteChildSlotInfo ( StringBuilder builder , List < SlotInfo > slots )
{
builder . AppendLine ( "\tinternal override CSharpSlotInfo GetChildSlotInfo(int index)" ) ;
builder . AppendLine ( "\t{" ) ;
WriteReturnDispatch ( builder , slots , k = > $"{slots[k].PropertyName}Slot" , k = > $"{slots[k].PropertyName}Slot" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
static void WriteCollectionLookup ( StringBuilder builder , List < SlotInfo > slots )
{
if ( ! slots . Any ( s = > s . IsCollection ) )
return ;
builder . AppendLine ( "\tinternal override AstNodeCollection? GetCollectionByKind(CSharpSlotInfo kind)" ) ;
builder . AppendLine ( "\t{" ) ;
foreach ( var s in slots . Where ( s = > s . IsCollection ) )
builder . AppendLine ( $"\t\tif (kind == Slots.{s.KindName}) return {s.PropertyName};" ) ;
builder . AppendLine ( "\t\treturn base.GetCollectionByKind(kind);" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
if ( slots . Any ( s = > s . IsCollection ) )
static void WriteCloneChildrenInto ( StringBuilder builder , AstNodeAdditions source , List < SlotInfo > slots )
{
builder . AppendLine ( "\tinternal override void CloneChildrenInto(AstNode copyNode)" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( $"\t\tvar copy = ({source.NodeName})copyNode;" ) ;
foreach ( var s in slots )
builder . AppendLine ( $"\t\tcopy.{FieldName(s.PropertyName)} = null;" ) ;
foreach ( var s in slots )
{
string field = FieldName ( s . PropertyName ) ;
if ( s . IsCollection )
{
builder . AppendLine ( "\tinternal override AstNodeCollection? GetCollectionByKind(CSharpSlotInfo kind)" ) ;
builder . AppendLine ( "\t{" ) ;
foreach ( var s in slots . Where ( s = > s . IsCollection ) )
builder . AppendLine ( $"\t\tif (kind == Slots.{s.KindName}) return {s.PropertyName};" ) ;
builder . AppendLine ( "\t\treturn base.GetCollectionByKind(kind);" ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
builder . AppendLine ( $"\t\tif ({field} != null)" ) ;
builder . AppendLine ( $"\t\t\tforeach (var c in {field})" ) ;
builder . AppendLine ( $"\t\t\t\tcopy.{s.PropertyName}.Add(({s.ElementType})c.Clone());" ) ;
}
builder . AppendLine ( "\tinternal override void CloneChildrenInto(AstNode copyNode)" ) ;
builder . AppendLine ( "\t{" ) ;
builder . AppendLine ( $"\t\tvar copy = ({source.NodeName})copyNode;" ) ;
foreach ( var s in slots )
builder . AppendLine ( $"\t\tcopy.{FieldName(s.PropertyName)} = null;" ) ;
foreach ( var s in slots )
else
{
string field = FieldName ( s . PropertyName ) ;
if ( s . IsCollection )
{
builder . AppendLine ( $"\t\tif ({field} != null)" ) ;
builder . AppendLine ( $"\t\t\tforeach (var c in {field})" ) ;
builder . AppendLine ( $"\t\t\t\tcopy.{s.PropertyName}.Add(({s.ElementType})c.Clone());" ) ;
}
else
builder . AppendLine ( $"\t\tif ({field} != null) copy.{s.PropertyName} = ({s.PropertyType}){field}.Clone();" ) ;
builder . AppendLine ( $"\t\tif ({field} != null) copy.{s.PropertyName} = ({s.PropertyType}){field}.Clone();" ) ;
}
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
// Close the class, trimming the blank line the per-member spacer leaves before the brace.
string body = builder . ToString ( ) . TrimEnd ( ) + "\n}\n" ;
context . AddSource ( source . NodeName + ".g.cs" , SourceText . From ( body . Replace ( "\r\n" , "\n" ) , Encoding . UTF8 ) ) ;
builder . AppendLine ( "\t}" ) ;
builder . AppendLine ( ) ;
}
void WriteVisitors ( SourceProductionContext context , ImmutableArray < AstNodeAdditions > source )
@ -791,6 +887,20 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
@@ -791,6 +887,20 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
// back at it, and consumers compare node.Slot.Kind == Slots.X by object identity -- shared across node
// types, replacing the old polymorphic node.Role == Roles.X comparisons.
void WriteSlotKinds ( SourceProductionContext context , ImmutableArray < AstNodeAdditions > source )
{
var ( kindTypes , kindIsCollection ) = CollectSlotKinds ( source ) ;
// A slot kind names one child position, so it maps to a single child type (DSTG001 enforces this);
// the typed Slots constant therefore always carries that precise type. A kind that is a collection
// on one node and a single (or optional) child on another keeps a single type but no single arity,
// so its hard-coded IsCollection/isOptional flags are not authoritative -- the precise per-position
// flags live on the per-node slots, which is where consumers read them; the shared constant carries
// identity (and the now-precise child type), not those flags.
ReportSlotKindTypeConflicts ( context , kindTypes ) ;
WriteSlotKindsSource ( context , kindTypes , kindIsCollection ) ;
}
static ( SortedDictionary < string , SortedSet < string > > kindTypes , Dictionary < string , bool? > kindIsCollection ) CollectSlotKinds ( ImmutableArray < AstNodeAdditions > source )
{
// Per kind: the element types seen (to choose a typed slot's T) and whether it is a collection.
// kindIsCollection is null once a kind is seen as a collection on one node and a single child on
@ -799,34 +909,42 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
@@ -799,34 +909,42 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
var kindTypes = new SortedDictionary < string , SortedSet < string > > ( StringComparer . Ordinal ) ;
var kindIsCollection = new Dictionary < string , bool? > ( ) ;
foreach ( var node in source )
{
if ( node . Slots is { } slots )
{
foreach ( var s in slots )
{
if ( ! kindTypes . TryGetValue ( s . KindName , out var set ) )
kindTypes [ s . KindName ] = set = new SortedSet < string > ( StringComparer . Ordinal ) ;
set . Add ( s . ElementType ) ;
if ( ! kindIsCollection . TryGetValue ( s . KindName , out var arity ) )
kindIsCollection [ s . KindName ] = s . IsCollection ;
else if ( arity is bool b & & b ! = s . IsCollection )
kindIsCollection [ s . KindName ] = null ;
}
}
}
CollectNodeSlotKinds ( node , kindTypes , kindIsCollection ) ;
return ( kindTypes , kindIsCollection ) ;
}
// A slot kind names one child position, so it maps to a single child type (DSTG001 enforces this);
// the typed Slots constant therefore always carries that precise type. A kind that is a collection
// on one node and a single (or optional) child on another keeps a single type but no single arity,
// so its hard-coded IsCollection/isOptional flags are not authoritative -- the precise per-position
// flags live on the per-node slots, which is where consumers read them; the shared constant carries
// identity (and the now-precise child type), not those flags.
static void CollectNodeSlotKinds ( AstNodeAdditions node , SortedDictionary < string , SortedSet < string > > kindTypes , Dictionary < string , bool? > kindIsCollection )
{
if ( node . Slots is not { } slots )
return ;
foreach ( var s in slots )
CollectSlotKind ( s , kindTypes , kindIsCollection ) ;
}
static void CollectSlotKind ( SlotInfo slot , SortedDictionary < string , SortedSet < string > > kindTypes , Dictionary < string , bool? > kindIsCollection )
{
if ( ! kindTypes . TryGetValue ( slot . KindName , out var set ) )
kindTypes [ slot . KindName ] = set = new SortedSet < string > ( StringComparer . Ordinal ) ;
set . Add ( slot . ElementType ) ;
if ( ! kindIsCollection . TryGetValue ( slot . KindName , out var arity ) )
kindIsCollection [ slot . KindName ] = slot . IsCollection ;
else if ( arity is bool b & & b ! = slot . IsCollection )
kindIsCollection [ slot . KindName ] = null ;
}
void ReportSlotKindTypeConflicts ( SourceProductionContext context , SortedDictionary < string , SortedSet < string > > kindTypes )
{
foreach ( var kv in kindTypes )
{
if ( kv . Value . Count > 1 )
context . ReportDiagnostic ( Diagnostic . Create ( MultipleChildTypesForKind , Location . None , kv . Key , string . Join ( ", " , kv . Value ) ) ) ;
}
}
static void WriteSlotKindsSource ( SourceProductionContext context , SortedDictionary < string , SortedSet < string > > kindTypes , Dictionary < string , bool? > kindIsCollection )
{
var builder = new StringBuilder ( ) ;
builder . AppendLine ( "// <auto-generated/>" ) ;
builder . AppendLine ( ) ;