diff --git a/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs b/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs
index 00d214ea7..c5ea313e1 100644
--- a/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs
+++ b/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs
@@ -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("// ");
builder.AppendLine("#nullable enable");
builder.AppendLine();
@@ -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
}}
}}
"
- );
- }
+ );
+ }
- 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
}}
");
- }
+ }
- 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 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 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? 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 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 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 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();
- 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();
- 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 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();
- 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 ConstructorPrefixLengths(List cp, int reqLen)
+ {
+ var lengths = new SortedSet();
+ 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();
- 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 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 ConstructorParameterDeclarations(List 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 singleExpr, Func 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 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 ParamsConstructorArguments(List 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 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 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 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();
+ 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 slots, Func singleExpr, Func 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 slots, Func 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 slots, Func singleExpr, Func 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 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 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 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 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 slots)
+ {
+ // One typed CSharpSlotInfo 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 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 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 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 source)
@@ -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 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> kindTypes, Dictionary kindIsCollection) CollectSlotKinds(ImmutableArray 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
var kindTypes = new SortedDictionary>(StringComparer.Ordinal);
var kindIsCollection = new Dictionary();
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(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> kindTypes, Dictionary kindIsCollection)
+ {
+ if (node.Slots is not { } slots)
+ return;
+
+ foreach (var s in slots)
+ CollectSlotKind(s, kindTypes, kindIsCollection);
+ }
+
+ static void CollectSlotKind(SlotInfo slot, SortedDictionary> kindTypes, Dictionary kindIsCollection)
+ {
+ if (!kindTypes.TryGetValue(slot.KindName, out var set))
+ kindTypes[slot.KindName] = set = new SortedSet(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> 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> kindTypes, Dictionary kindIsCollection)
+ {
var builder = new StringBuilder();
builder.AppendLine("// ");
builder.AppendLine();
@@ -906,4 +1024,4 @@ static class EquatableArrayExtensions
{
return new EquatableArray(array.ToArray());
}
-}
\ No newline at end of file
+}