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 +}