Browse Source

Extract syntax tree generator emit helpers

Reduce nested generation routines by moving constructor, child-dispatch, slot, and slot-kind emission into focused helpers. This keeps generator behavior intact while making the source-generator control flow easier to review and maintain.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
pull/3829/head
Siegfried Pammer 5 days ago committed by Siegfried Pammer
parent
commit
0d2a137f02
  1. 886
      ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

886
ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

@ -220,6 +220,32 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{ {
var builder = new StringBuilder(); 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("// <auto-generated/>");
builder.AppendLine("#nullable enable"); builder.AppendLine("#nullable enable");
builder.AppendLine(); builder.AppendLine();
@ -237,19 +263,23 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
builder.AppendLine($"partial class {source.NodeName}"); builder.AppendLine($"partial class {source.NodeName}");
builder.AppendLine("{"); builder.AppendLine("{");
}
if (source.NeedsPatternPlaceholder) static void WritePatternPlaceholder(StringBuilder builder, AstNodeAdditions source)
{ {
// The placeholder conversion is part of the pattern-construction DSL, where a non-null if (!source.NeedsPatternPlaceholder)
// pattern is the invariant; the result is therefore non-nullable for the specific node return;
// types. AstNode (the base) and ParameterDeclaration keep the nullable contract, and only
// AstNode also accepts a nullable pattern. // The placeholder conversion is part of the pattern-construction DSL, where a non-null
bool nullableReturn = source.NodeName is "AstNode" or "ParameterDeclaration"; // pattern is the invariant; the result is therefore non-nullable for the specific node
string returnQ = nullableReturn ? "?" : ""; // types. AstNode (the base) and ParameterDeclaration keep the nullable contract, and only
string paramQ = source.NodeName == "AstNode" ? "?" : ""; // AstNode also accepts a nullable pattern.
string forgive = nullableReturn ? "" : "!"; bool nullableReturn = source.NodeName is "AstNode" or "ParameterDeclaration";
builder.Append( string returnQ = nullableReturn ? "?" : "";
$@" public static implicit operator {source.NodeName}{returnQ}(PatternMatching.Pattern{paramQ} pattern) 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}; return pattern != null ? new PatternPlaceholder(pattern) : null{forgive};
}} }}
@ -289,12 +319,15 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
}} }}
}} }}
" "
); );
} }
if (source.NeedsVisitor) static void WriteVisitorOverrides(StringBuilder builder, AstNodeAdditions source)
{ {
builder.Append($@" public override void AcceptVisitor(IAstVisitor visitor) if (!source.NeedsVisitor)
return;
builder.Append($@" public override void AcceptVisitor(IAstVisitor visitor)
{{ {{
visitor.Visit{source.VisitMethodName}(this); visitor.Visit{source.VisitMethodName}(this);
}} }}
@ -310,381 +343,444 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
}} }}
"); ");
} }
if (source.MembersToMatch != null) static void WriteDoMatch(StringBuilder builder, AstNodeAdditions source)
{ {
builder.Append($@" protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match) if (source.MembersToMatch == null)
return;
builder.Append($@" protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match)
{{ {{
return other is {source.NodeName} o"); return other is {source.NodeName} o");
foreach (var (member, typeName, recursive, hasAny, nullable) in source.MembersToMatch) foreach (var (member, typeName, recursive, hasAny, nullable) in source.MembersToMatch)
{ builder.Append(DoMatchTerm(member, typeName, recursive, hasAny, nullable));
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}");
}
}
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 static void WriteConstructors(StringBuilder builder, AstNodeAdditions source, List<SlotInfo> slots)
// 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 // Constructors. Parameters follow member source order and cover single/collection [Slot] children,
// member (Part I.3 flatten) is an override. // the string [Slot], and settable enum scalars (Operator, FieldDirection, ...); a collection is
// A collection can maintain its children's flattened indices incrementally only when it is the // an IEnumerable<T> param in its declared position. We emit the empty ctor (for object-initializer
// node's sole collection and its last slot: then it owns the contiguous range [slotIndex, ..) // construction), then a ctor for the required prefix (through the last required param), one ending at
// with nothing after it, so an element's index is slotIndex + its local position (all // each collection, and one with all params. A params T[] overload is added when a ctor's last param
// preceding slots are single children, one index each). // is the collection. Pure-scalar nodes (no [Slot], e.g. PrimitiveExpression) are excluded
int collectionCount = slots.Count(s => s.IsCollection); // because their non-enum state (a literal value) is invisible here; those keep hand-written ctors.
for (int slotIndex = 0; slotIndex < slots.Count; slotIndex++) if (source.IsAbstract || !source.BaseHasDefaultConstructor || slots.Count == 0 || source.CtorParams is not { } ctorParamsArray)
{ return;
var (isCollection, name, type, elementType, isOverride, isNullable, kindName, isPartial) = slots[slotIndex];
string field = FieldName(name); var cp = ctorParamsArray.ToList();
string partialKw = isPartial ? "partial " : ""; builder.AppendLine($"\tpublic {source.NodeName}()");
if (isCollection) builder.AppendLine("\t{");
{ builder.AppendLine("\t}");
bool supportsIncremental = collectionCount == 1 && slotIndex == slots.Count - 1; builder.AppendLine();
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();
}
// A string [Slot] is a convenience accessor over its generated Identifier token slot. int reqLen = RequiredConstructorPrefixLength(cp);
if (source.NameAccessors is { } nameAccessorsArray) int prev = 0;
{ foreach (int len in ConstructorPrefixLengths(cp, reqLen))
foreach (var (stringName, tokenName, isOptional) in nameAccessorsArray) {
{ if (len <= 0)
// The token factory is the type 'Identifier'. A [Slot] string property literally named continue;
// "Identifier" (e.g. SimpleType.Identifier) shadows that type inside its own setter, so the WriteConstructorPrefix(builder, source.NodeName, cp, len, prev, paramsForm: false);
// bare name would bind to the string property; qualify with global:: only in that case. if (cp[len - 1].IsCollection)
string createType = stringName == "Identifier" WriteConstructorPrefix(builder, source.NodeName, cp, len, len, paramsForm: true);
? "global::ICSharpCode.Decompiler.CSharp.Syntax.Identifier" prev = len;
: "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();
}
}
// Constructors. Parameters follow member source order and cover single/collection [Slot] children, static int RequiredConstructorPrefixLength(List<CtorParam> cp)
// 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 // Required prefix: through the last non-optional param (an optional param before it is still
// construction), then a ctor for the required prefix (through the last required param), one ending at // positionally included so the required param after it can be passed).
// each collection, and one with all params. A params T[] overload is added when a ctor's last param int reqLen = 0;
// is the collection. Pure-scalar nodes (no [Slot], e.g. PrimitiveExpression) are excluded for (int i = 0; i < cp.Count; i++)
// because their non-enum state (a literal value) is invisible here; those keep hand-written ctors. if (!cp[i].IsOptional)
if (!source.IsAbstract && source.BaseHasDefaultConstructor && slots.Count > 0 && source.CtorParams is { } ctorParamsArray) reqLen = i + 1;
{ return reqLen;
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();
}
var lengths = new SortedSet<int>(); static SortedSet<int> ConstructorPrefixLengths(List<CtorParam> cp, int reqLen)
if (reqLen > 0) {
lengths.Add(reqLen); var lengths = new SortedSet<int>();
for (int i = 0; i < cp.Count; i++) if (reqLen > 0)
if (cp[i].IsCollection && i + 1 >= reqLen) lengths.Add(reqLen);
lengths.Add(i + 1); for (int i = 0; i < cp.Count; i++)
lengths.Add(cp.Count); if (cp[i].IsCollection && i + 1 >= reqLen)
int prev = 0; lengths.Add(i + 1);
foreach (int len in lengths) lengths.Add(cp.Count);
{ return lengths;
if (len <= 0) }
continue;
EmitPrefix(len, prev, paramsForm: false);
if (cp[len - 1].IsCollection)
EmitPrefix(len, len, paramsForm: true);
prev = len;
}
}
// Flattened child-index space: slots in declaration order, a single slot occupying one index static void WriteConstructorPrefix(StringBuilder builder, string nodeName, List<CtorParam> cp, int len, int chainLen, bool paramsForm)
// (even when empty), a collection slot a contiguous run of its current length. {
var countTerms = new List<string>(); // A normal prefix ctor forwards to the previous (shorter) emitted prefix via : this(...) and only
int singleSlotCount = slots.Count(s => !s.IsCollection); // sets the params between them; the shortest sets its params directly. A params T[] overload
if (singleSlotCount > 0) // forwards to the IEnumerable overload of the same length.
countTerms.Add(singleSlotCount.ToString()); builder.AppendLine($"\tpublic {nodeName}({string.Join(", ", ConstructorParameterDeclarations(cp, len, paramsForm))})");
foreach (var s in slots.Where(s => s.IsCollection)) if (paramsForm)
countTerms.Add($"({FieldName(s.PropertyName)}?.Count ?? 0)"); WriteParamsConstructorBody(builder, cp, len);
builder.AppendLine($"\tinternal override int GetChildCount() => {string.Join(" + ", countTerms)};"); else
builder.AppendLine(); 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. static void WriteParamsConstructorBody(StringBuilder builder, List<CtorParam> cp, int len)
// 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 builder.AppendLine($"\t\t: this({string.Join(", ", ParamsConstructorArguments(cp, len))})");
// length from a running index. builder.AppendLine("\t{");
void EmitReturnDispatch(Func<int, string> singleExpr, Func<int, string> collectionExpr) 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\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));");
}
builder.AppendLine("\tinternal override AstNode? GetChild(int index)"); static IEnumerable<string> ParamsConstructorArguments(List<CtorParam> cp, int len)
builder.AppendLine("\t{"); {
EmitReturnDispatch(k => FieldName(slots[k].PropertyName), k => $"{FieldName(slots[k].PropertyName)}![i]"); for (int i = 0; i < len; i++)
builder.AppendLine("\t}"); {
builder.AppendLine(); 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)"); static void WriteNormalConstructorBody(StringBuilder builder, List<CtorParam> cp, int len, int chainLen)
builder.AppendLine("\t{"); {
if (!anyCollection) if (chainLen > 0)
{ builder.AppendLine($"\t\t: this({string.Join(", ", Enumerable.Range(0, chainLen).Select(i => ParamName(cp[i].PropertyName)))})");
builder.AppendLine("\t\tswitch (index)"); builder.AppendLine("\t{");
builder.AppendLine("\t\t{"); for (int i = chainLen; i < len; i++)
for (int k = 0; k < slots.Count; k++) {
{ if (cp[i].IsCollection)
builder.AppendLine($"\t\t\tcase {k}:"); builder.AppendLine($"\t\tthis.{cp[i].PropertyName}.AddRange({ParamName(cp[i].PropertyName)});");
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}");
}
else else
{ builder.AppendLine($"\t\tthis.{cp[i].PropertyName} = {ParamName(cp[i].PropertyName)};");
builder.AppendLine("\t\tint i = index;"); }
for (int k = 0; k < slots.Count; k++) builder.AppendLine("\t}");
{ }
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();
// One typed CSharpSlotInfo<T> static per slot; node.Slot compares against these by object static string ParamName(string n)
// identity, and the typed child accessors infer the child type from the slot. {
foreach (var s in slots) string p = char.ToLowerInvariant(n[0]) + n.Substring(1);
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")});"); return SyntaxFacts.GetKeywordKind(p) != SyntaxKind.None ? "@" + p : p;
builder.AppendLine(); }
builder.AppendLine("\tinternal override CSharpSlotInfo GetChildSlotInfo(int index)"); static string ParamType(CtorParam cp) => cp.IsCollection ? $"IEnumerable<{cp.ElementType}>" : cp.ParamType;
builder.AppendLine("\t{");
EmitReturnDispatch(k => $"{slots[k].PropertyName}Slot", k => $"{slots[k].PropertyName}Slot"); static void WriteChildAccessors(StringBuilder builder, List<SlotInfo> slots)
builder.AppendLine("\t}"); {
builder.AppendLine(); // 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\tif ({field} != null)");
builder.AppendLine("\t{"); builder.AppendLine($"\t\t\tforeach (var c in {field})");
foreach (var s in slots.Where(s => s.IsCollection)) builder.AppendLine($"\t\t\t\tcopy.{s.PropertyName}.Add(({s.ElementType})c.Clone());");
builder.AppendLine($"\t\tif (kind == Slots.{s.KindName}) return {s.PropertyName};");
builder.AppendLine("\t\treturn base.GetCollectionByKind(kind);");
builder.AppendLine("\t}");
builder.AppendLine();
} }
else
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); builder.AppendLine($"\t\tif ({field} != null) copy.{s.PropertyName} = ({s.PropertyType}){field}.Clone();");
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}");
builder.AppendLine();
} }
builder.AppendLine("\t}");
// Close the class, trimming the blank line the per-member spacer leaves before the brace. builder.AppendLine();
string body = builder.ToString().TrimEnd() + "\n}\n";
context.AddSource(source.NodeName + ".g.cs", SourceText.From(body.Replace("\r\n", "\n"), Encoding.UTF8));
} }
void WriteVisitors(SourceProductionContext context, ImmutableArray<AstNodeAdditions> source) void WriteVisitors(SourceProductionContext context, ImmutableArray<AstNodeAdditions> 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 // 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. // types, replacing the old polymorphic node.Role == Roles.X comparisons.
void WriteSlotKinds(SourceProductionContext context, ImmutableArray<AstNodeAdditions> source) 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. // 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 // 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<string, SortedSet<string>>(StringComparer.Ordinal); var kindTypes = new SortedDictionary<string, SortedSet<string>>(StringComparer.Ordinal);
var kindIsCollection = new Dictionary<string, bool?>(); var kindIsCollection = new Dictionary<string, bool?>();
foreach (var node in source) foreach (var node in source)
{ CollectNodeSlotKinds(node, kindTypes, kindIsCollection);
if (node.Slots is { } slots) return (kindTypes, kindIsCollection);
{ }
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;
}
}
}
// A slot kind names one child position, so it maps to a single child type (DSTG001 enforces this); static void CollectNodeSlotKinds(AstNodeAdditions node, SortedDictionary<string, SortedSet<string>> kindTypes, Dictionary<string, bool?> kindIsCollection)
// 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, if (node.Slots is not { } slots)
// so its hard-coded IsCollection/isOptional flags are not authoritative -- the precise per-position return;
// 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. 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) foreach (var kv in kindTypes)
{ {
if (kv.Value.Count > 1) if (kv.Value.Count > 1)
context.ReportDiagnostic(Diagnostic.Create(MultipleChildTypesForKind, Location.None, kv.Key, string.Join(", ", kv.Value))); 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(); var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>"); builder.AppendLine("// <auto-generated/>");
builder.AppendLine(); builder.AppendLine();

Loading…
Cancel
Save