Browse Source

Format generated AST source like hand-written code

The per-node .g.cs files are read during debugging and review, so the emitter
now shapes them the way a person would: fixed-slot nodes dispatch child access
through a switch, collection nodes through a readable running-index walk (no dead
trailing decrement), GetChildCount folds its constant terms, and the fully
qualified System.Collections.Generic names collapse behind a using. The visitor
interface and SlotKind enum gain the auto-generated header and #nullable, and all
three writers normalize newlines to LF.

No behavior change: the dispatch logic is identical; only formatting and the
unreachable final decrement differ.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3807/head
Siegfried Pammer 3 weeks ago committed by Siegfried Pammer
parent
commit
560d4f9ff5
  1. 154
      ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

154
ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

@ -188,17 +188,18 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>"); builder.AppendLine("// <auto-generated/>");
builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); builder.AppendLine("#nullable enable");
builder.AppendLine(); builder.AppendLine();
bool hasSlots = source.Slots is not null;
if (hasSlots)
builder.AppendLine("using System.Collections.Generic;");
if (source.NeedsPatternPlaceholder) if (source.NeedsPatternPlaceholder)
{
builder.AppendLine("using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;"); builder.AppendLine("using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;");
} if (hasSlots || source.NeedsPatternPlaceholder)
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("#nullable enable"); builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine($"partial class {source.NodeName}"); builder.AppendLine($"partial class {source.NodeName}");
@ -387,7 +388,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
return SyntaxFacts.GetKeywordKind(p) != SyntaxKind.None ? "@" + p : p; return SyntaxFacts.GetKeywordKind(p) != SyntaxKind.None ? "@" + p : p;
} }
string ParamType(int i) => cp[i].IsCollection string ParamType(int i) => cp[i].IsCollection
? $"global::System.Collections.Generic.IEnumerable<{cp[i].ElementType}>" ? $"IEnumerable<{cp[i].ElementType}>"
: cp[i].ParamType; : cp[i].ParamType;
builder.AppendLine($"\tpublic {source.NodeName}()"); builder.AppendLine($"\tpublic {source.NodeName}()");
@ -421,7 +422,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
var args = new List<string>(); var args = new List<string>();
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
args.Add(i == len - 1 args.Add(i == len - 1
? $"(global::System.Collections.Generic.IEnumerable<{cp[i].ElementType}>){ParamName(cp[i].PropertyName)}" ? $"(IEnumerable<{cp[i].ElementType}>){ParamName(cp[i].PropertyName)}"
: ParamName(cp[i].PropertyName)); : ParamName(cp[i].PropertyName));
builder.AppendLine($"\t\t: this({string.Join(", ", args)})"); builder.AppendLine($"\t\t: this({string.Join(", ", args)})");
builder.AppendLine("\t{"); builder.AppendLine("\t{");
@ -465,46 +466,119 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
// Flattened child-index space: slots in declaration order, a single slot occupying one index // 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. // (even when empty), a collection slot a contiguous run of its current length.
builder.Append("\tinternal override int GetChildCount() => "); var countTerms = new List<string>();
builder.Append(string.Join(" + ", slots.Select(s => s.IsCollection ? $"({FieldName(s.PropertyName)}?.Count ?? 0)" : "1"))); int singleSlotCount = slots.Count(s => !s.IsCollection);
builder.AppendLine(";"); 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();
builder.AppendLine("\tinternal override AstNode? GetChild(int index)"); bool anyCollection = slots.Any(s => s.IsCollection);
builder.AppendLine("\t{");
// 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;"); builder.AppendLine("\t\tint i = index;");
foreach (var s in slots) for (int k = 0; k < slots.Count; k++)
{ {
if (s.IsCollection) bool last = k == slots.Count - 1;
if (slots[k].IsCollection)
{ {
string field = FieldName(s.PropertyName); string field = FieldName(slots[k].PropertyName);
builder.AppendLine($"\t\t{{ int n = {field}?.Count ?? 0; if (i < n) return {field}![i]; i -= n; }}"); 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 else
{ {
builder.AppendLine($"\t\tif (i == 0) return {FieldName(s.PropertyName)}; i -= 1;"); 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("\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));");
}
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("\t}");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("\tinternal override void SetChild(int index, AstNode? value)"); builder.AppendLine("\tinternal override void SetChild(int index, AstNode? value)");
builder.AppendLine("\t{"); 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);");
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
{
builder.AppendLine("\t\tint i = index;"); builder.AppendLine("\t\tint i = index;");
foreach (var s in slots) 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) if (s.IsCollection)
{ {
string field = FieldName(s.PropertyName); builder.AppendLine("\t\t{");
builder.AppendLine($"\t\t{{ int n = {field}?.Count ?? 0; if (i < n) {{ {field}![i] = ({s.ElementType})value!; return; }} i -= n; }}"); 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 else
{ {
builder.AppendLine($"\t\tif (i == 0) {{ SetChildNode(ref {FieldName(s.PropertyName)}, ({s.PropertyType}?)value); return; }} i -= 1;"); builder.AppendLine("\t\tif (i == 0)");
builder.AppendLine("\t\t{");
builder.AppendLine($"\t\t\tSetChildNode(ref {field}, ({s.PropertyType}?)value);");
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\tthrow new System.ArgumentOutOfRangeException(nameof(index));");
}
builder.AppendLine("\t}"); builder.AppendLine("\t}");
builder.AppendLine(); builder.AppendLine();
@ -515,15 +589,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
builder.AppendLine("\tinternal override CSharpSlotInfo GetChildSlotInfo(int index)"); builder.AppendLine("\tinternal override CSharpSlotInfo GetChildSlotInfo(int index)");
builder.AppendLine("\t{"); builder.AppendLine("\t{");
builder.AppendLine("\t\tint i = index;"); EmitReturnDispatch(k => $"{slots[k].PropertyName}Slot", k => $"{slots[k].PropertyName}Slot");
foreach (var s in slots)
{
if (s.IsCollection)
builder.AppendLine($"\t\t{{ int n = {FieldName(s.PropertyName)}?.Count ?? 0; if (i < n) return {s.PropertyName}Slot; i -= n; }}");
else
builder.AppendLine($"\t\tif (i == 0) return {s.PropertyName}Slot; i -= 1;");
}
builder.AppendLine("\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));");
builder.AppendLine("\t}"); builder.AppendLine("\t}");
builder.AppendLine(); builder.AppendLine();
@ -547,7 +613,11 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{ {
string field = FieldName(s.PropertyName); string field = FieldName(s.PropertyName);
if (s.IsCollection) if (s.IsCollection)
builder.AppendLine($"\t\tif ({field} != null) foreach (var c in {field}) copy.{s.PropertyName}.Add(({s.ElementType})c.Clone());"); {
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 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();");
} }
@ -558,9 +628,9 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
// and the visitors' VisitChildren, instead of the per-step O(slots) index scan (NextSibling). // and the visitors' VisitChildren, instead of the per-step O(slots) index scan (NextSibling).
// Collection slots iterate via their own enumerator, which tolerates removing/replacing the // Collection slots iterate via their own enumerator, which tolerates removing/replacing the
// current child during traversal. // current child during traversal.
builder.AppendLine("\tinternal override System.Collections.Generic.IEnumerable<AstNode> GetChildNodes()"); builder.AppendLine("\tinternal override IEnumerable<AstNode> GetChildNodes()");
builder.AppendLine("\t{"); builder.AppendLine("\t{");
builder.AppendLine("\t\tvar children = new System.Collections.Generic.List<AstNode>(GetChildCount());"); builder.AppendLine("\t\tvar children = new List<AstNode>(GetChildCount());");
foreach (var s in slots) foreach (var s in slots)
{ {
string field = FieldName(s.PropertyName); string field = FieldName(s.PropertyName);
@ -574,32 +644,38 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
builder.AppendLine(); builder.AppendLine();
} }
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(builder.ToString().Replace("\r\n", "\n"), Encoding.UTF8)); 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)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>");
builder.AppendLine("#nullable enable");
builder.AppendLine();
builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;");
builder.AppendLine();
source = source source = source
.Concat([new("PatternPlaceholder", true, false, true, false, "PatternPlaceholder", "AstNode", null, null, null, null)]) .Concat([new("PatternPlaceholder", true, false, true, false, "PatternPlaceholder", "AstNode", null, null, null, null)])
.ToImmutableArray(); .ToImmutableArray();
WriteInterface("IAstVisitor", "void", ""); WriteInterface("IAstVisitor", "void", "");
builder.AppendLine();
WriteInterface("IAstVisitor<out S>", "S", ""); WriteInterface("IAstVisitor<out S>", "S", "");
builder.AppendLine();
WriteInterface("IAstVisitor<in T, out S>", "S", ", T data"); WriteInterface("IAstVisitor<in T, out S>", "S", ", T data");
context.AddSource("IAstVisitor.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); context.AddSource("IAstVisitor.g.cs", SourceText.From(builder.ToString().Replace("\r\n", "\n"), Encoding.UTF8));
void WriteInterface(string name, string ret, string param) void WriteInterface(string name, string ret, string param)
{ {
builder.AppendLine($"public interface {name}"); builder.AppendLine($"public interface {name}");
builder.AppendLine("{"); builder.AppendLine("{");
foreach (var type in source.OrderBy(t => t.VisitMethodName)) foreach (var type in source.OrderBy(t => t.VisitMethodName))
{ {
if (!type.NeedsVisitor) if (!type.NeedsVisitor)
@ -696,6 +772,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
} }
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>");
builder.AppendLine();
builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("/// <summary>Identifies the kind of an AST child slot, shared across node types.</summary>"); builder.AppendLine("/// <summary>Identifies the kind of an AST child slot, shared across node types.</summary>");
@ -706,7 +784,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
builder.AppendLine($"\t{k},"); builder.AppendLine($"\t{k},");
builder.AppendLine("}"); builder.AppendLine("}");
context.AddSource("SlotKind.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); context.AddSource("SlotKind.g.cs", SourceText.From(builder.ToString().Replace("\r\n", "\n"), Encoding.UTF8));
} }
} }

Loading…
Cancel
Save