From 560d4f9ff5f6d64632b735e41a1387c78f1bc577 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 19 Jun 2026 09:48:17 +0200 Subject: [PATCH] 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 --- .../DecompilerSyntaxTreeGenerator.cs | 170 +++++++++++++----- 1 file changed, 124 insertions(+), 46 deletions(-) diff --git a/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs b/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs index 306982c44..edba37131 100644 --- a/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs +++ b/ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs @@ -188,17 +188,18 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator var builder = new StringBuilder(); builder.AppendLine("// "); - builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); + builder.AppendLine("#nullable enable"); builder.AppendLine(); + bool hasSlots = source.Slots is not null; + if (hasSlots) + builder.AppendLine("using System.Collections.Generic;"); if (source.NeedsPatternPlaceholder) - { builder.AppendLine("using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;"); - } - - builder.AppendLine(); + if (hasSlots || source.NeedsPatternPlaceholder) + builder.AppendLine(); - builder.AppendLine("#nullable enable"); + builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); builder.AppendLine(); builder.AppendLine($"partial class {source.NodeName}"); @@ -387,7 +388,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator return SyntaxFacts.GetKeywordKind(p) != SyntaxKind.None ? "@" + p : p; } string ParamType(int i) => cp[i].IsCollection - ? $"global::System.Collections.Generic.IEnumerable<{cp[i].ElementType}>" + ? $"IEnumerable<{cp[i].ElementType}>" : cp[i].ParamType; builder.AppendLine($"\tpublic {source.NodeName}()"); @@ -421,7 +422,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator var args = new List(); for (int i = 0; i < len; i++) 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)); builder.AppendLine($"\t\t: this({string.Join(", ", args)})"); 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 // (even when empty), a collection slot a contiguous run of its current length. - builder.Append("\tinternal override int GetChildCount() => "); - builder.Append(string.Join(" + ", slots.Select(s => s.IsCollection ? $"({FieldName(s.PropertyName)}?.Count ?? 0)" : "1"))); - builder.AppendLine(";"); + 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{"); - builder.AppendLine("\t\tint i = index;"); - foreach (var s in slots) + bool anyCollection = slots.Any(s => s.IsCollection); + + // 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 (s.IsCollection) + if (!anyCollection) { - string field = FieldName(s.PropertyName); - builder.AppendLine($"\t\t{{ int n = {field}?.Count ?? 0; if (i < n) return {field}![i]; i -= n; }}"); + 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; } - else + builder.AppendLine("\t\tint i = index;"); + for (int k = 0; k < slots.Count; k++) { - builder.AppendLine($"\t\tif (i == 0) return {FieldName(s.PropertyName)}; i -= 1;"); + 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("\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(); builder.AppendLine("\tinternal override void SetChild(int index, AstNode? value)"); builder.AppendLine("\t{"); - builder.AppendLine("\t\tint i = index;"); - foreach (var s in slots) + if (!anyCollection) { - if (s.IsCollection) + builder.AppendLine("\t\tswitch (index)"); + builder.AppendLine("\t\t{"); + for (int k = 0; k < slots.Count; k++) { - string field = FieldName(s.PropertyName); - builder.AppendLine($"\t\t{{ int n = {field}?.Count ?? 0; if (i < n) {{ {field}![i] = ({s.ElementType})value!; return; }} i -= n; }}"); + 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;"); } - else + 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;"); + for (int k = 0; k < slots.Count; k++) { - builder.AppendLine($"\t\tif (i == 0) {{ SetChildNode(ref {FieldName(s.PropertyName)}, ({s.PropertyType}?)value); return; }} i -= 1;"); + 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);"); + 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(); @@ -515,15 +589,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator builder.AppendLine("\tinternal override CSharpSlotInfo GetChildSlotInfo(int index)"); builder.AppendLine("\t{"); - builder.AppendLine("\t\tint i = index;"); - 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));"); + EmitReturnDispatch(k => $"{slots[k].PropertyName}Slot", k => $"{slots[k].PropertyName}Slot"); builder.AppendLine("\t}"); builder.AppendLine(); @@ -547,7 +613,11 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator { string field = FieldName(s.PropertyName); 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 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). // Collection slots iterate via their own enumerator, which tolerates removing/replacing the // current child during traversal. - builder.AppendLine("\tinternal override System.Collections.Generic.IEnumerable GetChildNodes()"); + builder.AppendLine("\tinternal override IEnumerable GetChildNodes()"); builder.AppendLine("\t{"); - builder.AppendLine("\t\tvar children = new System.Collections.Generic.List(GetChildCount());"); + builder.AppendLine("\t\tvar children = new List(GetChildCount());"); foreach (var s in slots) { string field = FieldName(s.PropertyName); @@ -574,32 +644,38 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator 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 source) { var builder = new StringBuilder(); + builder.AppendLine("// "); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); + builder.AppendLine(); source = source .Concat([new("PatternPlaceholder", true, false, true, false, "PatternPlaceholder", "AstNode", null, null, null, null)]) .ToImmutableArray(); WriteInterface("IAstVisitor", "void", ""); + builder.AppendLine(); WriteInterface("IAstVisitor", "S", ""); + builder.AppendLine(); WriteInterface("IAstVisitor", "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) { builder.AppendLine($"public interface {name}"); builder.AppendLine("{"); - foreach (var type in source.OrderBy(t => t.VisitMethodName)) { if (!type.NeedsVisitor) @@ -696,6 +772,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax } var builder = new StringBuilder(); + builder.AppendLine("// "); + builder.AppendLine(); builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); builder.AppendLine(); builder.AppendLine("/// Identifies the kind of an AST child slot, shared across node types."); @@ -706,7 +784,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax builder.AppendLine($"\t{k},"); 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)); } }