diff --git a/.scripts/update_ilspy.sh b/.scripts/update_ilspy.sh
new file mode 100755
index 00000000..8c5dd727
--- /dev/null
+++ b/.scripts/update_ilspy.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if [[ -z "${ILSPY_DIR:-}" ]]; then
+ echo "Error: ILSPY_DIR is not set."
+ exit 1
+fi
+
+if [[ ! -d "$ILSPY_DIR" ]]; then
+ echo "Error: ILSPY_DIR does not exist: $ILSPY_DIR"
+ exit 1
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
+DEPS_DIR="$WORKSPACE_DIR/modules/gdsdecomp/godot-mono-decomp/dependencies"
+CSPROJ_PATH="$WORKSPACE_DIR/modules/gdsdecomp/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj"
+
+mkdir -p "$DEPS_DIR"
+
+echo "Packing ILSpy packages into: $DEPS_DIR"
+(
+ cd "$ILSPY_DIR"
+ dotnet pack -c Release ICSharpCode.Decompiler -o "$DEPS_DIR"
+ dotnet pack -c Release ICSharpCode.ILSpyX -o "$DEPS_DIR"
+)
+
+latest_package_version() {
+ local package_name="$1"
+ local newest
+
+ newest="$(
+ ls -t "$DEPS_DIR/${package_name}."*.nupkg 2>/dev/null \
+ | while IFS= read -r path; do
+ [[ "$path" == *.snupkg ]] && continue
+ printf '%s\n' "$path"
+ done \
+ | head -n 1
+ )"
+
+ if [[ -z "$newest" ]]; then
+ echo "Error: Could not find packed package for $package_name in $DEPS_DIR"
+ exit 1
+ fi
+
+ local filename version
+ filename="${newest##*/}"
+ version="${filename#${package_name}.}"
+ version="${version%.nupkg}"
+ printf '%s\n' "$version"
+}
+
+prune_older_packages() {
+ local package_name="$1"
+ local keep_version="$2"
+
+ shopt -s nullglob
+ local files=(
+ "$DEPS_DIR/${package_name}."*.nupkg
+ "$DEPS_DIR/${package_name}."*.snupkg
+ )
+ shopt -u nullglob
+
+ for file in "${files[@]}"; do
+ case "$file" in
+ "$DEPS_DIR/${package_name}.${keep_version}.nupkg" | "$DEPS_DIR/${package_name}.${keep_version}.snupkg")
+ ;;
+ *)
+ rm -f "$file"
+ ;;
+ esac
+ done
+}
+
+decompiler_version="$(latest_package_version "ICSharpCode.Decompiler")"
+ilspyx_version="$(latest_package_version "ICSharpCode.ILSpyX")"
+
+prune_older_packages "ICSharpCode.Decompiler" "$decompiler_version"
+prune_older_packages "ICSharpCode.ILSpyX" "$ilspyx_version"
+
+if [[ ! -f "$CSPROJ_PATH" ]]; then
+ echo "Error: csproj not found: $CSPROJ_PATH"
+ exit 1
+fi
+
+# Remove existing Decompiler/ILSpyX references (active or commented), then insert updated package refs.
+sed -i.bak '/PackageReference Include="ICSharpCode\.Decompiler"/d' "$CSPROJ_PATH"
+sed -i.bak '/PackageReference Include="ICSharpCode\.ILSpyX"/d' "$CSPROJ_PATH"
+sed -i.bak '/ProjectReference Include=".*ICSharpCode\.Decompiler\.csproj"/d' "$CSPROJ_PATH"
+sed -i.bak '/ProjectReference Include=".*ICSharpCode\.ILSpyX\.csproj"/d' "$CSPROJ_PATH"
+
+sed -i.bak "/\\
+
+" "$CSPROJ_PATH"
+
+rm -f "$CSPROJ_PATH.bak"
+
+echo "Updated $CSPROJ_PATH"
+echo " - ICSharpCode.Decompiler: $decompiler_version"
+echo " - ICSharpCode.ILSpyX: $ilspyx_version"
diff --git a/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs b/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs
new file mode 100644
index 00000000..43351cdf
--- /dev/null
+++ b/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs
@@ -0,0 +1,65 @@
+using System.IO;
+using ICSharpCode.Decompiler.CSharp;
+using ICSharpCode.Decompiler.CSharp.OutputVisitor;
+using ICSharpCode.Decompiler.CSharp.Syntax;
+
+namespace GodotMonoDecomp;
+
+internal sealed class CollectionExpressionArrayAnnotation
+{
+ public static readonly CollectionExpressionArrayAnnotation Instance = new();
+
+ private CollectionExpressionArrayAnnotation()
+ {
+ }
+}
+
+internal sealed class CollectionExpressionSpreadElementAnnotation
+{
+ public static readonly CollectionExpressionSpreadElementAnnotation Instance = new();
+
+ private CollectionExpressionSpreadElementAnnotation()
+ {
+ }
+}
+
+public class GodotCSharpOutputVisitor : CSharpOutputVisitor
+{
+ public GodotCSharpOutputVisitor(TextWriter w, CSharpFormattingOptions formattingOptions)
+ : base(w, formattingOptions)
+ {
+ }
+
+ public override void VisitArrayInitializerExpression(ArrayInitializerExpression arrayInitializerExpression)
+ {
+ if (arrayInitializerExpression.Annotation() == null)
+ {
+ base.VisitArrayInitializerExpression(arrayInitializerExpression);
+ return;
+ }
+
+ StartNode(arrayInitializerExpression);
+ WriteToken(Roles.LBracket);
+
+ bool first = true;
+ foreach (var element in arrayInitializerExpression.Elements)
+ {
+ if (!first)
+ {
+ WriteToken(Roles.Comma);
+ Space();
+ }
+
+ if (element.Annotation() != null)
+ {
+ WriteToken(BinaryOperatorExpression.RangeRole);
+ }
+
+ element.AcceptVisitor(this);
+ first = false;
+ }
+
+ WriteToken(Roles.RBracket);
+ EndNode(arrayInitializerExpression);
+ }
+}
diff --git a/godot-mono-decomp/GodotMonoDecomp/Common.cs b/godot-mono-decomp/GodotMonoDecomp/Common.cs
index 8ff2eff3..995174f2 100644
--- a/godot-mono-decomp/GodotMonoDecomp/Common.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/Common.cs
@@ -277,4 +277,86 @@ public static Guid GenerateDeterministicGuidFromString(string input)
return new Guid(hashBytes);
}
}
+
+ private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute";
+ public static bool RemoveGeneratedCodeAttributes(ICSharpCode.Decompiler.CSharp.Syntax.AstNodeCollection attributeSections, string generatorName)
+ {
+ bool removedAny = false;
+ foreach (var section in attributeSections.ToArray())
+ {
+ foreach (var attribute in section.Attributes.ToArray())
+ {
+ if (IsGeneratedCodeAttributeForTool(attribute, generatorName))
+ {
+ attribute.Remove();
+ removedAny = true;
+ }
+ }
+
+ if (section.Attributes.Count == 0)
+ {
+ section.Remove();
+ }
+ }
+
+ return removedAny;
+ }
+
+ public static bool HasMatchingShortAttributeName(string typeName, string expectedShortName)
+ {
+ if (typeName.StartsWith("global::", StringComparison.Ordinal))
+ {
+ typeName = typeName.Substring("global::".Length);
+ }
+
+ int lastDot = typeName.LastIndexOf('.');
+ if (lastDot >= 0 && lastDot < typeName.Length - 1)
+ {
+ typeName = typeName.Substring(lastDot + 1);
+ }
+
+ if (typeName.EndsWith("Attribute", StringComparison.Ordinal))
+ {
+ typeName = typeName.Substring(0, typeName.Length - "Attribute".Length);
+ }
+
+ return string.Equals(typeName, expectedShortName, StringComparison.Ordinal);
+ }
+
+ public static bool IsAttribute(ICSharpCode.Decompiler.CSharp.Syntax.Attribute attribute, string expectedFullName, string expectedShortName)
+ {
+ if (attribute.Type.Annotation() is { Type: { } typeResult })
+ {
+ if (string.Equals(typeResult.FullName, expectedFullName, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ if (attribute.GetSymbol() is IMethod method && method.DeclaringType != null)
+ {
+ if (string.Equals(method.DeclaringType.FullName, expectedFullName, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return HasMatchingShortAttributeName(attribute.Type.ToString(), expectedShortName);
+ }
+
+
+ public static bool IsGeneratedCodeAttributeForTool(ICSharpCode.Decompiler.CSharp.Syntax.Attribute attribute, string generatorName)
+ {
+ if (!IsAttribute(attribute, GeneratedCodeAttributeFullName, "GeneratedCode"))
+ {
+ return false;
+ }
+
+ if (attribute.Arguments.FirstOrDefault() is ICSharpCode.Decompiler.CSharp.Syntax.PrimitiveExpression { Value: string toolName })
+ {
+ return string.Equals(toolName, generatorName, StringComparison.Ordinal);
+ }
+
+ return false;
+ }
}
diff --git a/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs b/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs
index 8eb716b7..e91248dc 100644
--- a/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs
@@ -233,7 +233,7 @@ public static string GetDepPath(string assemblyPath)
return null;
}
- public async Task StartResolvePackageAndCheckHash(CancellationToken cancellationToken)
+ public async Task StartResolvePackageAndCheckHash(bool checkOnline, CancellationToken cancellationToken)
{
if (!Serviceable || Type != "package" || string.IsNullOrEmpty(Sha512) || !Sha512.StartsWith("sha512-"))
{
@@ -245,7 +245,7 @@ public async Task StartResolvePackageAndCheckHash(CancellationToken cancellation
string? hash;
try
{
- hash = await NugetDetails.ResolvePackageAndGetContentHash(Name, Version, cancellationToken);
+ hash = await NugetDetails.ResolvePackageAndGetContentHash(Name, Version, checkOnline, cancellationToken);
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
diff --git a/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs b/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs
new file mode 100644
index 00000000..796c55b5
--- /dev/null
+++ b/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs
@@ -0,0 +1,88 @@
+using ICSharpCode.Decompiler.CSharp;
+using ICSharpCode.Decompiler.CSharp.Syntax;
+using ICSharpCode.Decompiler.CSharp.Transforms;
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace GodotMonoDecomp;
+
+///
+/// Intended to fix switch expressions that do not have enough context to determine the best type as a result of a parent member reference expression.
+///
+///
+/// e.g:
+/// ```c#
+/// public Animal GetAnimal(AnimalType animalType)
+/// {
+/// return (animalType switch
+/// {
+/// // at least one of these should have a cast to 'Animal' or the member reference expression below will cause the switch expression to fail to compile.
+/// AnimalType.Dog => new Dog(),
+/// AnimalType.Fish => new Fish(),
+/// _ => throw new ArgumentException("Invalid animal type")
+/// }).ToValidated();
+/// }
+/// ```
+///
+public class FixSwitchExpressionCasts : DepthFirstAstVisitor, IAstTransform
+{
+ private TransformContext? context;
+
+ public void Run(AstNode rootNode, TransformContext context)
+ {
+ this.context = context;
+ rootNode.AcceptVisitor(this);
+ }
+
+ private static bool IsCastableSwitchSectionExpressionBody(Expression body)
+ {
+ return body is not PrimitiveExpression && body is not NullReferenceExpression && body is not ThrowExpression;
+ }
+
+ public override void VisitSwitchExpression(SwitchExpression switchExpr)
+ {
+ if (context?.TypeSystemAstBuilder is null || switchExpr.Parent is not MemberReferenceExpression)
+ {
+ base.VisitSwitchExpression(switchExpr);
+ return;
+ }
+ var resolved = switchExpr.GetResolveResult();
+
+ if (!resolved.IsError && switchExpr.SwitchSections.Count > 1 && resolved.Type is not null)
+ {
+ if (!switchExpr.SwitchSections.Any(s => s.Body is CastExpression))
+ {
+ var resolvedTypeDefinition = resolved.Type.GetDefinition();
+ HashSet allDefs = [];
+ var mismatchedSections = switchExpr.SwitchSections.Where(s => {
+ if (!IsCastableSwitchSectionExpressionBody(s.Body))
+ {
+ return false;
+ }
+ var rr = s.Body.GetResolveResult();
+ if (rr is not null && !rr.IsError)
+ {
+ var def = rr.Type.GetDefinition();
+ if (def is not null)
+ {
+ allDefs.Add(def);
+ if (!def.Equals(resolvedTypeDefinition))
+ {
+ return true;
+ }
+ }
+
+ }
+ return false;
+ }).ToArray();
+ if (mismatchedSections.Length > 0 && allDefs.Count > 1) {
+ var first = mismatchedSections.FirstOrDefault()!;
+ var body = first.Body;
+ first.Body = null;
+ first.Body = new CastExpression(context.TypeSystemAstBuilder.ConvertType(resolved.Type), body);
+ }
+ }
+ }
+
+ base.VisitSwitchExpression(switchExpr);
+ }
+}
diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs b/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs
index 2d6a37b3..cb027605 100644
--- a/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs
@@ -26,7 +26,7 @@ public class GodotModule
public readonly Guid moduleGuid;
public readonly IDebugInfoProvider? debugInfoProvider;
public readonly string? SubDirectory;
- public Dictionary fileMap;
+ public Dictionary> fileMap;
private GodotProjectDecompiler ProjectDecompiler;
@@ -246,39 +246,50 @@ public GodotModuleDecompiler(string assemblyPath, string[]? originalProjectFiles
}
- if (Settings.VerifyNuGetPackageIsFromNugetOrg)
+ foreach (var module in new List { MainModule }.Concat(AdditionalModules))
{
- foreach (var module in new List { MainModule }.Concat(AdditionalModules))
+ foreach (var dep in module.depInfo?.deps.Where(d =>
+ d is { Type: "package" } &&
+ (!ProjectFileWriterGodotStyle.ImplicitGodotReferences.Contains(d.Name) ||
+ string.Equals(d.Name, "GodotSharp", StringComparison.OrdinalIgnoreCase))) ?? [])
{
- foreach (var dep in module.depInfo?.deps.Where(d =>
- d is { Type: "package" } &&
- (!ProjectFileWriterGodotStyle.ImplicitGodotReferences.Contains(d.Name) ||
- string.Equals(d.Name, "GodotSharp", StringComparison.OrdinalIgnoreCase))) ?? [])
- {
- packageHashTasks.Add(
- Task.Run(async () => await dep.StartResolvePackageAndCheckHash(packageHashTaskCancelSrc.Token), packageHashTaskCancelSrc.Token)
- );
- }
+ packageHashTasks.Add(
+ Task.Run(async () => await dep.StartResolvePackageAndCheckHash(Settings.VerifyNuGetPackageIsFromNugetOrg, packageHashTaskCancelSrc.Token), packageHashTaskCancelSrc.Token)
+ );
}
}
HashSet excludeSubdirs = AdditionalModules.Select(module => module.SubDirectory ?? "").Where(subdir => !string.IsNullOrEmpty(subdir)).ToHashSet();
+ HashSet GetGodotClassHandles(GodotModule module, IEnumerable handles)
+ {
+ var handleSet = handles.ToHashSet();
+ var decompiler = module.CreateCSharpDecompilerWithPartials(handleSet);
+ var godotHandles = new HashSet();
+ foreach (var h in handleSet)
+ {
+ var typeDef = decompiler.TypeSystem.MainModule.GetDefinition(h);
+ if (typeDef != null && GodotStuff.IsGodotClass(typeDef))
+ {
+ godotHandles.Add(h);
+ }
+ }
+ return godotHandles;
+ }
+
var typesToDecompile = MainModule.GetProjectDecompiler().GetTypesToDecompile(MainModule.Module).ToHashSet();
- MainModule.fileMap = GodotStuff.CreateFileMap(MainModule.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, excludeSubdirs, true);
+ var mainGodotClassHandles = GetGodotClassHandles(MainModule, typesToDecompile);
+ MainModule.fileMap = GodotStuff.CreateFileMap(MainModule.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, excludeSubdirs, true, mainGodotClassHandles);
var additionalModuleCount = 0;
- var fileToModuleMap = MainModule.fileMap.ToDictionary(
- pair => pair.Key,
- pair => MainModule,//.Module.FileName,
- StringComparer.OrdinalIgnoreCase);
// var moduleFileNameToMouduleMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var module in AdditionalModules)
{
// TODO: make CreateFileMap() work with multiple modules
typesToDecompile = module.GetProjectDecompiler().GetTypesToDecompile(module.Module).ToHashSet();
+ var moduleGodotClassHandles = GetGodotClassHandles(module, typesToDecompile);
- var nfileMap = GodotStuff.CreateFileMap(module.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, null, true);
+ var nfileMap = GodotStuff.CreateFileMap(module.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, null, true, moduleGodotClassHandles);
additionalModuleCount += nfileMap.Count;
string moduleName = module.Module.FileName;
@@ -292,7 +303,14 @@ public GodotModuleDecompiler(string assemblyPath, string[]? originalProjectFiles
{
fixedPath = module.SubDirectory + "/" + pair.Key;
}
- module.fileMap.Add(fixedPath, pair.Value);
+ if (!module.fileMap.TryGetValue(fixedPath, out var existingHandles))
+ {
+ module.fileMap.Add(fixedPath, pair.Value);
+ }
+ else
+ {
+ existingHandles.AddRange(pair.Value.Where(h => !existingHandles.Contains(h)));
+ }
}
if (module.fileMap.Count == 0)
@@ -377,12 +395,19 @@ ProjectItem decompileFile(GodotModule module, string csprojPath)
removeIfExists(csprojPath);
ProjectId projectId;
- var typesToExclude = excludeFiles?.Select(file => Common.TrimPrefix(file, "res://")).Where(module.fileMap.ContainsKey).Select(file => module.fileMap[file]).ToHashSet() ?? [];
+ var typesToExclude = excludeFiles?.Select(file => Common.TrimPrefix(file, "res://"))
+ .Where(module.fileMap.ContainsKey)
+ .SelectMany(file => module.fileMap[file])
+ .ToHashSet() ?? [];
+ var handleToFileMap = module.fileMap
+ .SelectMany(pair => pair.Value.Select(h => new KeyValuePair(h, pair.Key)))
+ .GroupBy(p => p.Key)
+ .ToDictionary(group => group.Key, group => group.First().Value);
using (var projectFileWriter = new StreamWriter(File.OpenWrite(csprojPath)))
{
projectId = godotProjectDecompiler.DecompileGodotProject(
- module.Module, targetDirectory, projectFileWriter, typesToExclude, module.fileMap.ToDictionary(pair => pair.Value, pair => pair.Key), moduleToCsProjPath, module.depInfo, CustomVersionDetected, token);
+ module.Module, targetDirectory, projectFileWriter, typesToExclude, handleToFileMap, moduleToCsProjPath, module.depInfo, CustomVersionDetected, token);
}
ProjectItem item = new ProjectItem(csprojPath, projectId.PlatformName, projectId.Guid, projectId.TypeGuid);
@@ -427,19 +452,19 @@ ProjectItem decompileFile(GodotModule module, string csprojPath)
}
public const string error_message = "// ERROR: Could not find file '{0}' in assembly '{1}.dll'.";
- private (GodotModule?, TypeDefinitionHandle) GetScriptModuleAndType(string file)
+ private (GodotModule?, List) GetScriptModuleAndTypes(string file)
{
var path = Common.TrimPrefix(file, "res://");
if (!string.IsNullOrEmpty(path))
{
- TypeDefinitionHandle foundType;
+ List? foundTypes;
GodotModule? module = MainModule;
- if (!module.fileMap.TryGetValue(path, out foundType))
+ if (!module.fileMap.TryGetValue(path, out foundTypes))
{
module = null;
foreach (var m in AdditionalModules)
{
- if (m.fileMap.TryGetValue(path, out foundType))
+ if (m.fileMap.TryGetValue(path, out foundTypes))
{
module = m;
break;
@@ -447,17 +472,17 @@ ProjectItem decompileFile(GodotModule module, string csprojPath)
}
}
- return (module, foundType);
+ return (module, foundTypes ?? []);
}
- return (null, default(TypeDefinitionHandle));
+ return (null, []);
}
public string DecompileIndividualFile(string file)
{
var path = Common.TrimPrefix(file, "res://");
- var (module, type) = GetScriptModuleAndType(file);
- if (module == null || type == default)
+ var (module, types) = GetScriptModuleAndTypes(file);
+ if (module == null || types.Count == 0)
{
return string.Format(error_message, file, MainModule.Name) + (
originalProjectFiles.Contains(path)
@@ -466,8 +491,11 @@ public string DecompileIndividualFile(string file)
);
}
- var decompiler = module.CreateCSharpDecompilerWithPartials([type]);
- return decompiler.DecompileTypesAsString([type]);
+ var decompiler = module.CreateCSharpDecompilerWithPartials(types);
+ var tree = decompiler.DecompileTypes(types);
+ var stringWriter = new StringWriter();
+ tree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions));
+ return stringWriter.ToString();
}
private string GetPathForType(ITypeDefinition? typeDef){
@@ -475,11 +503,11 @@ private string GetPathForType(ITypeDefinition? typeDef){
return "";
}
if (typeDef.ParentModule.AssemblyName == MainModule.Name){
- return MainModule.fileMap.FirstOrDefault(pair => pair.Value == (TypeDefinitionHandle)typeDef.MetadataToken).Key;
+ return MainModule.fileMap.FirstOrDefault(pair => pair.Value.Contains((TypeDefinitionHandle)typeDef.MetadataToken)).Key;
}
foreach (var module in AdditionalModules){
if (module.Name == typeDef.ParentModule.AssemblyName){
- return module.fileMap.FirstOrDefault(pair => pair.Value == (TypeDefinitionHandle)typeDef.MetadataToken).Key;
+ return module.fileMap.FirstOrDefault(pair => pair.Value.Contains((TypeDefinitionHandle)typeDef.MetadataToken)).Key;
}
}
return "";
@@ -487,14 +515,23 @@ private string GetPathForType(ITypeDefinition? typeDef){
public GodotScriptInfo? GetScriptInfo(string file)
{
- var (module, type) = GetScriptModuleAndType(file);
- if (module == null || type == default)
+ var (module, types) = GetScriptModuleAndTypes(file);
+ if (module == null || types.Count == 0)
{
return null;
}
var projectDecompiler = module.GetProjectDecompiler();
- var decompiler = module.CreateCSharpDecompilerWithPartials([type]);
+ var decompiler = module.CreateCSharpDecompilerWithPartials(types);
+ var type = types.FirstOrDefault(h =>
+ {
+ var maybeTypeDef = decompiler.TypeSystem.MainModule.GetDefinition(h);
+ return maybeTypeDef != null && GodotStuff.IsGodotClass(maybeTypeDef);
+ });
+ if (type == default)
+ {
+ return null;
+ }
var typeDef = decompiler.TypeSystem.MainModule.GetDefinition(type);
if (typeDef == null)
@@ -636,8 +673,13 @@ private string GetPathForType(ITypeDefinition? typeDef){
iconPath = attr.FixedArguments[0].Value as string ?? "";
}
}
+ // if we had more than one top-level-type in the file, we need to re-deccompile for all types
+ if (types.Count > 1)
+ {
+ syntaxTree = decompiler.DecompileTypes(types);
+ }
StringWriter stringWriter = new StringWriter();
- syntaxTree.AcceptVisitor(new CSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions));
+ syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions));
var scriptText = stringWriter.ToString();
var scriptInfo = new GodotScriptInfo(
diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj
index 1dc943aa..3d3a570e 100644
--- a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj
+++ b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj
@@ -9,8 +9,8 @@
true
-
-
+
+
diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs
index 4f04907b..d54d9dbe 100644
--- a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs
@@ -39,26 +39,45 @@ public class GodotMonoDecompSettings : DecompilerSettings
///
public Version? GodotVersionOverride { get; set; } = null;
- public GodotMonoDecompSettings() : base()
+ ///
+ /// Whether to remove the body of the generated JsonSourceGeneration context classes.
+ ///
+ public bool RemoveGeneratedJsonContextBody { get; set; } = false;
+
+ ///
+ /// Whether to run LiftCollectionInitializers.
+ /// If false, the legacy RemoveBogusBaseConstructorCalls transform is used instead.
+ ///
+ public bool EnableCollectionInitializerLifting { get; set; } = true;
+
+ private void InitializeDefaultSettings()
{
UseNestedDirectoriesForNamespaces = true;
+ // This avoids certain race conditions during static initialization when attempting to run the decompiled project.
+ AlwaysMoveInitializer = true;
+ }
+
+ public GodotMonoDecompSettings() : base()
+ {
+ InitializeDefaultSettings();
}
public GodotMonoDecompSettings(LanguageVersion languageVersion) : base(languageVersion)
{
- UseNestedDirectoriesForNamespaces = true;
+ InitializeDefaultSettings();
}
public new GodotMonoDecompSettings Clone()
{
- var settings = (GodotMonoDecompSettings) base.Clone();
+ var settings = (GodotMonoDecompSettings)base.Clone();
settings.WriteNuGetPackageReferences = WriteNuGetPackageReferences;
settings.VerifyNuGetPackageIsFromNugetOrg = VerifyNuGetPackageIsFromNugetOrg;
settings.CopyOutOfTreeReferences = CopyOutOfTreeReferences;
settings.CreateAdditionalProjectsForProjectReferences = CreateAdditionalProjectsForProjectReferences;
settings.OverrideLanguageVersion = OverrideLanguageVersion;
settings.GodotVersionOverride = GodotVersionOverride;
+ settings.EnableCollectionInitializerLifting = EnableCollectionInitializerLifting;
return settings;
}
diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs b/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs
index 6057d4e2..3b0787d6 100644
--- a/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs
@@ -308,7 +308,21 @@ public virtual CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts)
decompiler.AstTransforms.Add(new RemoveGodotScriptPathAttribute());
decompiler.AstTransforms.Add(new RemoveAutoAccessor());
decompiler.AstTransforms.Add(new GodotMonoDecomp.RemoveEmbeddedAttributes());
+ decompiler.AstTransforms.Add(new RestoreGeneratedRegexMethods());
decompiler.AstTransforms.Add(new RemoveGeneratedExceptionThrows());
+ if (Settings.EnableCollectionInitializerLifting)
+ {
+ decompiler.AstTransforms.Add(new LiftCollectionInitializers());
+ }
+ else
+ {
+ decompiler.AstTransforms.Add(new RemoveBogusBaseConstructorCalls());
+ }
+ decompiler.AstTransforms.Add(new FixSwitchExpressionCasts());
+ if (Settings.RemoveGeneratedJsonContextBody)
+ {
+ decompiler.AstTransforms.Add(new RemoveJsonSourceGenerationClassBody());
+ }
if (Settings.GodotVersionOverride?.Major == 3)
{
decompiler.AstTransforms.Add(new RemoveMathF3x());
@@ -329,7 +343,7 @@ IEnumerable WriteAssemblyInfo(DecompilerTypeSystem ts, Cancella
string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs");
using (var w = CreateFile(Path.Combine(TargetDirectory, assemblyInfo)))
{
- syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions));
+ syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings.CSharpFormattingOptions));
}
return new[] { new ProjectItemInfo("Compile", assemblyInfo) };
}
@@ -455,7 +469,7 @@ void ProcessFiles(List> files)
var path = Path.Combine(TargetDirectory, file.Key);
using StreamWriter w = new StreamWriter(path);
- syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions));
+ syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings.CSharpFormattingOptions));
}
catch (Exception innerException) when (!(innerException is OperationCanceledException || innerException is DecompilerException))
{
diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs b/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs
index 7f93e3de..4cd304e8 100644
--- a/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs
@@ -336,16 +336,24 @@ public static Dictionary> DeduceParentNamespaceDirectori
}
- public static Dictionary CreateFileMap(MetadataFile module,
+ public static Dictionary> CreateFileMap(MetadataFile module,
IEnumerable typesToDecompile,
List filesInOriginal,
Dictionary? scriptMetadata,
IEnumerable? excludedSubdirectories,
- bool useNestedDirectoriesForNamespaces)
+ bool useNestedDirectoriesForNamespaces,
+ ISet? godotClassHandles = null)
{
- var fileMap = new Dictionary();
+ var fileMap = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ godotClassHandles ??= new HashSet();
+ var canonicalPaths = new HashSet();
+ var canonicalHandles = new HashSet();
var metadata = module.Metadata;
Dictionary? metadataFQNToFileMap = null;
+ // look at the files in the original project and find a common root for all the files
+ // var allFiles = filesInOriginal.Select(f => Path.GetDirectoryName(f) ?? "").Where(d => !string.IsNullOrEmpty(d) && !d.StartsWith("addons", StringComparison.OrdinalIgnoreCase)).ToHashSet();
+ string? globalCommonRoot = null;//Common.FindCommonRoot(allFiles) ?? null;
+
if (scriptMetadata != null)
{
// create a map of metadata FQN to file path
@@ -402,6 +410,99 @@ bool IsInExcludedSubdir(string? file)
return IsExcludedSubdir(Path.GetDirectoryName(file));
}
+ void AddHandleToPath(string path, TypeDefinitionHandle h)
+ {
+ path = _NormalizePath(path);
+ if (!fileMap.TryGetValue(path, out var handles))
+ {
+ handles = [];
+ fileMap[path] = handles;
+ }
+ if (!handles.Contains(h))
+ {
+ handles.Add(h);
+ }
+ }
+
+ string RelocateByPrefixingUnderscoreDirectory(string path)
+ {
+ path = _NormalizePath(path);
+ var fileName = Path.GetFileName(path);
+ var dir = Path.GetDirectoryName(path);
+ // dir = string.IsNullOrEmpty(dir) ? "_" : "_" + _NormalizePath(dir);
+ dir = string.IsNullOrEmpty(dir) ? "_collision" : _PathCombine(dir, "_collision");
+
+ return _PathCombine(dir, fileName);
+ }
+
+ void RelocateExistingGlobalHandle(string currentPath, TypeDefinitionHandle existingGlobalHandle)
+ {
+ currentPath = _NormalizePath(currentPath);
+ if (!fileMap.TryGetValue(currentPath, out var currentHandles))
+ {
+ return;
+ }
+ if (!currentHandles.Remove(existingGlobalHandle))
+ {
+ return;
+ }
+ if (currentHandles.Count == 0)
+ {
+ fileMap.Remove(currentPath);
+ }
+ var relocatedPath = RelocateByPrefixingUnderscoreDirectory(currentPath);
+ PlaceHandleAtPath(relocatedPath, existingGlobalHandle);
+ }
+
+ void PlaceHandleAtPath(string path, TypeDefinitionHandle h)
+ {
+ path = _NormalizePath(path);
+ if (!godotClassHandles.Contains(h))
+ {
+ AddHandleToPath(path, h);
+ return;
+ }
+
+ while (true)
+ {
+ var existingHandles = fileMap.TryGetValue(path, out var hs) ? hs : [];
+ var existingGlobals = existingHandles.Where(godotClassHandles.Contains).ToList();
+
+ if (existingGlobals.Count == 0)
+ {
+ AddHandleToPath(path, h);
+ return;
+ }
+
+ bool incomingIsCanonical = canonicalHandles.Contains(h);
+ bool anyExistingCanonical = existingGlobals.Any(canonicalHandles.Contains);
+ if (incomingIsCanonical && anyExistingCanonical)
+ {
+ Console.Error.WriteLine(
+ $"Multiple canonical Godot class types map to '{path}'. Canonical collisions are not allowed.");
+ // just add it anyway
+ AddHandleToPath(path, h);
+ return;
+ }
+ if (!incomingIsCanonical && anyExistingCanonical)
+ {
+ path = RelocateByPrefixingUnderscoreDirectory(path);
+ continue;
+ }
+ if (incomingIsCanonical)
+ {
+ foreach (var existingGlobal in existingGlobals.Where(g => !canonicalHandles.Contains(g)).ToList())
+ {
+ RelocateExistingGlobalHandle(path, existingGlobal);
+ }
+ AddHandleToPath(path, h);
+ return;
+ }
+
+ path = RelocateByPrefixingUnderscoreDirectory(path);
+ }
+ }
+
var processAgain = new HashSet();
var namespaceToDirectory = new Dictionary>();
void addToNamespaceToFile(string ns, string file)
@@ -420,6 +521,15 @@ void addToNamespaceToFile(string ns, string file)
namespaceToDirectory[ns] = new HashSet { dir };
}
}
+ void addToCanonicalPaths(string path, TypeDefinitionHandle h, TypeDefinition type)
+ {
+ path = _NormalizePath(path);
+ canonicalPaths.Add(path);
+ canonicalHandles.Add(h);
+ addToNamespaceToFile(metadata.GetString(type.Namespace), path);
+ PlaceHandleAtPath(path, h);
+ }
+
foreach (var h in typesToDecompile)
{
var type = metadata.GetTypeDefinition(h);
@@ -429,8 +539,7 @@ void addToNamespaceToFile(string ns, string file)
// that the file is referenced by other files in the project, so the path MUST match.
if (!string.IsNullOrEmpty(scriptPath))
{
- addToNamespaceToFile(metadata.GetString(type.Namespace),scriptPath);
- fileMap[scriptPath] = h;
+ addToCanonicalPaths(scriptPath, h, type);
}
else
{
@@ -442,17 +551,21 @@ void addToNamespaceToFile(string ns, string file)
if (metadataFQNToFileMap.TryGetValue(fqn, out var filePath))
{
filePath = Common.TrimPrefix(filePath, "res://");
- addToNamespaceToFile(metadata.GetString(type.Namespace), filePath);
- fileMap[filePath] = h;
+ addToCanonicalPaths(filePath, h, type);
continue;
}
}
processAgain.Add(h);
}
}
- namespaceToDirectory = DeduceParentNamespaceDirectories(namespaceToDirectory);
- string default_dir = "src";
+ // 3.x games have much less canonical file paths than 4.x games, so do it after `GetPathFromOriginalFiles` step
+ if (scriptMetadata == null)
+ {
+ namespaceToDirectory = DeduceParentNamespaceDirectories(namespaceToDirectory);
+ }
+
+ string default_dir = !string.IsNullOrEmpty(globalCommonRoot) ? globalCommonRoot : "src";
while (IsExcludedSubdir(default_dir)){
default_dir = "_" + default_dir;
}
@@ -471,6 +584,10 @@ string GetAutoFileNameForHandle(TypeDefinitionHandle h)
else
{
string dir = useNestedDirectoriesForNamespaces ? GodotProjectDecompiler.CleanUpPath(ns) : GodotProjectDecompiler.CleanUpDirectoryName(ns);
+ if (!string.IsNullOrEmpty(globalCommonRoot) && !dir.StartsWith(globalCommonRoot, StringComparison.OrdinalIgnoreCase))
+ {
+ dir = _PathCombine(globalCommonRoot, dir);
+ }
// ensure dir separator is '/'
dir = dir.Replace(Path.DirectorySeparatorChar, '/');
// TODO: come back to this
@@ -488,7 +605,6 @@ string GetPathFromOriginalFiles(string file_path)
string scriptPath = "";
// empty vector of strings
var possibles = filesInOriginal.Where(f =>
- !fileMap.ContainsKey(f) &&
Path.GetFileName(f) == Path.GetFileName(file_path)
&& !IsInExcludedSubdir(f)
)
@@ -497,7 +613,6 @@ string GetPathFromOriginalFiles(string file_path)
if (possibles.Count == 0)
{
possibles = filesInOriginal.Where(f =>
- !fileMap.ContainsKey(f) &&
Path.GetFileName(f).ToLower() == Path.GetFileName(file_path).ToLower()
&& !IsInExcludedSubdir(f)
).ToList();
@@ -552,19 +667,21 @@ string GetPathFromOriginalFiles(string file_path)
foreach (var pair in potentialMap)
{
- if (pair.Value.Count == 1)
+ foreach (var h in pair.Value)
{
- var type = metadata.GetTypeDefinition(pair.Value[0]);
+ var type = metadata.GetTypeDefinition(h);
addToNamespaceToFile(metadata.GetString(type.Namespace), pair.Key);
- fileMap[pair.Key] = pair.Value[0];
- } else {
- foreach (var h in pair.Value)
- {
- processAgainAgain.Add(h);
- }
+ PlaceHandleAtPath(pair.Key, h);
}
}
+ if (scriptMetadata != null)
+ {
+ namespaceToDirectory = DeduceParentNamespaceDirectories(namespaceToDirectory);
+ }
+
+
+
HashSet GetNamespaceDirectories(string ns)
{
@@ -577,20 +694,6 @@ HashSet GetNamespaceDirectories(string ns)
: [];
}
- string AppendNumberToFile(string path){
- var fileName = Path.GetFileName(path);
- var fileStem = Path.GetFileNameWithoutExtension(fileName);
- var fileExtension = Path.GetExtension(fileName);
- var parentDir = Path.GetDirectoryName(path) ?? "";
- var number = 1;
- while (fileMap.ContainsKey(path))
- {
- path = _PathCombine(Path.GetDirectoryName(path) ?? "", fileStem + number.ToString() + fileExtension);
- number++;
- }
- return path;
- }
-
foreach (var h in processAgainAgain)
{
var type = metadata.GetTypeDefinition(h);
@@ -651,12 +754,8 @@ string AppendNumberToFile(string path){
p = _PathCombine(default_dir, p);
}
}
- if (fileMap.ContainsKey(p))
- {
- p = AppendNumberToFile(p);
- }
p = _NormalizePath(p);
- fileMap[p] = h;
+ PlaceHandleAtPath(p, h);
}
foreach (var h in dupes)
@@ -664,7 +763,7 @@ string AppendNumberToFile(string path){
var auto_path = GetAutoFileNameForHandle(h);
var scriptPath = GetPathFromOriginalFiles(auto_path);
- if (scriptPath == "" || scriptPath == "" || fileMap.ContainsKey(scriptPath))
+ if (scriptPath == "" || scriptPath == "")
{
scriptPath = auto_path;
}
@@ -673,17 +772,40 @@ string AppendNumberToFile(string path){
{
scriptPath = _PathCombine(default_dir, scriptPath);
}
- if (fileMap.ContainsKey(scriptPath))
+ PlaceHandleAtPath(scriptPath, h);
+ }
+ var caselessDict = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ foreach (var pair in fileMap)
+ {
+ var isGodotClass = pair.Value.Any(h => godotClassHandles.Contains(h));
+
+ var key = pair.Key;
+ var existingHandles = caselessDict.TryGetValue(key, out var hs) ? hs : [];
+ if (existingHandles.Count > 0)
{
- scriptPath = AppendNumberToFile(scriptPath);
- }
+ var caselessPair = caselessDict.First(p => p.Key.Equals(pair.Key, StringComparison.OrdinalIgnoreCase));
- fileMap[scriptPath] = h;
+ if (canonicalPaths.Contains(caselessPair.Key))
+ {
+ key = caselessPair.Key;
+ }
+ else if (!canonicalPaths.Contains(pair.Key)) // else if the current key is NOT in the canonical paths...
+ {
+ if (caselessPair.Key.Count(char.IsUpper) > pair.Key.Count(char.IsUpper))
+ {
+ key = caselessPair.Key;
+ }
+ }
+ if (key != caselessPair.Key)
+ {
+ // remove the current key and then add it back below, so that we replace any key collisions
+ caselessDict.Remove(caselessPair.Key);
+ }
+ }
+ var newHandles = existingHandles.Concat(pair.Value).Distinct().ToList();
+ caselessDict[key] = newHandles;
}
- return fileMap.ToDictionary(
- pair => pair.Key,
- pair => pair.Value,
- StringComparer.OrdinalIgnoreCase);
+ return caselessDict;
}
diff --git a/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs b/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs
new file mode 100644
index 00000000..281f7fff
--- /dev/null
+++ b/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs
@@ -0,0 +1,1914 @@
+using ICSharpCode.Decompiler.CSharp.Syntax;
+using ICSharpCode.Decompiler.CSharp.Transforms;
+
+namespace GodotMonoDecomp;
+
+///
+/// Lifts a narrow set of constructor prelude initializers back to declaration initializers.
+/// This targets list-prelude patterns that ILSpy lowers into SetCount/AsSpan/indexed writes.
+///
+public class LiftCollectionInitializers : DepthFirstAstVisitor, IAstTransform
+{
+ private TypeDeclaration? currentType;
+
+ public void Run(AstNode rootNode, TransformContext context)
+ {
+ rootNode.AcceptVisitor(this);
+ }
+
+ public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration)
+ {
+ var previous = currentType;
+ currentType = typeDeclaration;
+ try
+ {
+ base.VisitTypeDeclaration(typeDeclaration);
+ }
+ finally
+ {
+ currentType = previous;
+ }
+ }
+
+ public List CollectMembersWithInitializers(TypeDeclaration typeDeclaration)
+ {
+ var participatingMembers = new List();
+ foreach (var member in typeDeclaration.Members)
+ {
+ if (member is FieldDeclaration field)
+ {
+ foreach (var variable in field.Variables)
+ {
+ if (!variable.AssignToken.IsNull || !variable.Initializer.IsNull)
+ {
+ foreach (var v in field.Variables)
+ {
+ participatingMembers.Add(variable.Name);
+ }
+ break;
+ }
+ }
+ }
+ else if (member is PropertyDeclaration property)
+ {
+ if (!property.Initializer.IsNull || !property.AssignToken.IsNull)
+ {
+ participatingMembers.Add(property.Name);
+ }
+ }
+ }
+ return participatingMembers.Distinct().ToList();
+ }
+
+
+ public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration)
+ {
+ if (currentType == null || constructorDeclaration.Body.IsNull)
+ {
+ base.VisitConstructorDeclaration(constructorDeclaration);
+ return;
+ }
+
+ bool isStaticConstructor = (constructorDeclaration.Modifiers & Modifiers.Static) == Modifiers.Static;
+ var statements = constructorDeclaration.Body.Statements.ToArray();
+ int boundaryIndex = isStaticConstructor ? statements.Length : FindConstructorBoundaryIndex(statements);
+ if (!isStaticConstructor && boundaryIndex < 0)
+ {
+ base.VisitConstructorDeclaration(constructorDeclaration);
+ return;
+ }
+
+ var memberMap = BuildMemberMap(currentType);
+ var recoveredMemberOrder = CollectMembersWithInitializers(currentType);
+ var matchedListInitByLocal = new Dictionary(StringComparer.Ordinal);
+ var statementsToRemove = new HashSet();
+ var localNames = CollectConstructorLocalNames(statements, boundaryIndex);
+
+ int i = 0;
+ while (i < boundaryIndex)
+ {
+ if (TryMatchListPrelude(statements, i, boundaryIndex, out var listMatch))
+ {
+ if (TryApplyInitializer(memberMap, listMatch.TargetMemberName, listMatch.InitializerExpression, isStaticConstructor))
+ {
+ recoveredMemberOrder.Add(listMatch.TargetMemberName);
+ matchedListInitByLocal[listMatch.ListVariableName] = listMatch.InitializerExpression;
+ foreach (var statement in listMatch.MatchedStatements)
+ {
+ statementsToRemove.Add(statement);
+ }
+ }
+
+ i = listMatch.NextIndex;
+ continue;
+ }
+
+ if (TryMatchListAddRangeSpreadPrelude(statements, i, boundaryIndex, out var listSpreadMatch))
+ {
+ if (TryApplyInitializer(memberMap, listSpreadMatch.TargetMemberName, listSpreadMatch.InitializerExpression, isStaticConstructor))
+ {
+ recoveredMemberOrder.Add(listSpreadMatch.TargetMemberName);
+ matchedListInitByLocal[listSpreadMatch.ListVariableName] = listSpreadMatch.InitializerExpression;
+ foreach (var statement in listSpreadMatch.MatchedStatements)
+ {
+ statementsToRemove.Add(statement);
+ }
+ }
+
+ i = listSpreadMatch.NextIndex;
+ continue;
+ }
+
+ if (TryMatchHashSetForeachSpreadPrelude(statements, i, boundaryIndex, out var hashSetSpreadMatch))
+ {
+ if (TryApplyInitializer(memberMap, hashSetSpreadMatch.TargetMemberName, hashSetSpreadMatch.InitializerExpression, isStaticConstructor))
+ {
+ recoveredMemberOrder.Add(hashSetSpreadMatch.TargetMemberName);
+ matchedListInitByLocal[hashSetSpreadMatch.ListVariableName] = hashSetSpreadMatch.InitializerExpression;
+ foreach (var statement in hashSetSpreadMatch.MatchedStatements)
+ {
+ statementsToRemove.Add(statement);
+ }
+ }
+
+ i = hashSetSpreadMatch.NextIndex;
+ continue;
+ }
+
+ if (TryMatchListSpreadBuilderWrapperAssignment(statements, i, boundaryIndex, currentType, out var wrappedSpreadMatch))
+ {
+ if (!ReferencesCtorLocal(wrappedSpreadMatch.InitializerExpression, localNames)
+ && TryApplyInitializer(memberMap, wrappedSpreadMatch.TargetMemberName, wrappedSpreadMatch.InitializerExpression, isStaticConstructor))
+ {
+ recoveredMemberOrder.Add(wrappedSpreadMatch.TargetMemberName);
+ matchedListInitByLocal[wrappedSpreadMatch.ListVariableName] = wrappedSpreadMatch.InitializerExpression;
+ foreach (var statement in wrappedSpreadMatch.MatchedStatements)
+ {
+ statementsToRemove.Add(statement);
+ }
+ }
+
+ i = wrappedSpreadMatch.NextIndex;
+ continue;
+ }
+
+ if (TryMatchConditionalTempAssignment(statements, i, boundaryIndex, currentType, out var conditionalTempMatch))
+ {
+ if (!ReferencesCtorLocal(conditionalTempMatch.InitializerExpression, localNames)
+ && TryApplyInitializer(memberMap, conditionalTempMatch.TargetMemberName, conditionalTempMatch.InitializerExpression, isStaticConstructor))
+ {
+ recoveredMemberOrder.Add(conditionalTempMatch.TargetMemberName);
+ foreach (var statement in conditionalTempMatch.MatchedStatements)
+ {
+ statementsToRemove.Add(statement);
+ }
+ }
+
+ i = conditionalTempMatch.NextIndex;
+ continue;
+ }
+
+ if (TryMatchSimpleAssignment(statements[i], currentType, out var simpleTarget, out var simpleInitializer))
+ {
+ var candidateInitializer = simpleInitializer;
+ if (ReferencesCtorLocal(candidateInitializer, localNames)
+ && TryRewriteInitializerWithRecoveredLocals(candidateInitializer, matchedListInitByLocal, out var rewrittenInitializer))
+ {
+ candidateInitializer = rewrittenInitializer;
+ }
+ candidateInitializer = SimplifyRedundantReadOnlyListCollectionWrappers(candidateInitializer);
+
+ if (!ReferencesCtorLocal(candidateInitializer, localNames)
+ && TryApplyInitializer(memberMap, simpleTarget, candidateInitializer, isStaticConstructor))
+ {
+ recoveredMemberOrder.Add(simpleTarget);
+ statementsToRemove.Add(statements[i]);
+ }
+ i++;
+ continue;
+ }
+
+ // check if this is an assignment to a member at all; if so, break. We do not want to lift any more lest we change the ordering of the initializers
+ if (isStaticConstructor || boundaryIndex < 0)
+ {
+ if (statements[i] is ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment })
+ {
+ if (TryGetAssignedMemberName(assignment.Left, out var memberName) && memberMap.ContainsKey(memberName))
+ {
+ break;
+ }
+ }
+ }
+
+ i++;
+ }
+
+ if (!isStaticConstructor)
+ {
+ Statement? boundaryStatement = statements[boundaryIndex];
+ if (TryBuildConstructorInitializer(
+ constructorDeclaration,
+ statements,
+ boundaryIndex,
+ boundaryStatement,
+ matchedListInitByLocal,
+ out var constructorInitializer,
+ out var ctorPreludeStatements))
+ {
+ constructorDeclaration.Initializer = constructorInitializer;
+ statementsToRemove.Add(boundaryStatement);
+ foreach (var statement in ctorPreludeStatements)
+ {
+ statementsToRemove.Add(statement);
+ }
+ }
+ }
+
+ foreach (var statement in statements.Reverse())
+ {
+ if (statementsToRemove.Contains(statement))
+ {
+ statement.Remove();
+ }
+ }
+
+ // Only run temp-noise cleanup when this transform actually consumed generated prelude statements.
+ if (statementsToRemove.Count > 0)
+ {
+ CleanupLeadingTempNoise(constructorDeclaration);
+ }
+
+ // If lifting consumed generated ctor statements and left no body/initializer content,
+ // drop the constructor declaration entirely.
+ if (statementsToRemove.Count > 0
+ && constructorDeclaration.Body.Statements.Count == 0
+ && constructorDeclaration.Initializer.IsNull)
+ {
+ constructorDeclaration.Remove();
+ ReorderRecoveredMembers(currentType, recoveredMemberOrder, memberMap);
+ return;
+ }
+
+ ReorderRecoveredMembers(currentType, recoveredMemberOrder, memberMap);
+ }
+
+ private static int FindConstructorBoundaryIndex(IReadOnlyList statements)
+ {
+ for (int i = 0; i < statements.Count; i++)
+ {
+ if (TryGetCtorInvocation(statements[i], out _, out _))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static bool TryGetCtorInvocation(
+ Statement statement,
+ out ConstructorInitializerType initializerType,
+ out InvocationExpression invocation)
+ {
+ initializerType = ConstructorInitializerType.Any;
+ invocation = null!;
+ if (statement is not ExpressionStatement { Expression: InvocationExpression invocationExpression })
+ {
+ return false;
+ }
+
+ if (invocationExpression.Target is not MemberReferenceExpression memberReference
+ || !string.Equals(memberReference.MemberName, "_002Ector", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (memberReference.Target is BaseReferenceExpression)
+ {
+ initializerType = ConstructorInitializerType.Base;
+ }
+ else if (memberReference.Target is ThisReferenceExpression)
+ {
+ initializerType = ConstructorInitializerType.This;
+ }
+ else
+ {
+ return false;
+ }
+
+ invocation = invocationExpression;
+ return true;
+ }
+
+ private static Dictionary BuildMemberMap(TypeDeclaration typeDeclaration)
+ {
+ var map = new Dictionary(StringComparer.Ordinal);
+ foreach (var member in typeDeclaration.Members)
+ {
+ if (member is FieldDeclaration field)
+ {
+ foreach (var variable in field.Variables)
+ {
+ map.TryAdd(variable.Name, field);
+ }
+ }
+ else if (member is PropertyDeclaration property)
+ {
+ map.TryAdd(property.Name, property);
+ }
+ }
+
+ return map;
+ }
+
+ private static bool TryApplyInitializer(
+ Dictionary memberMap,
+ string memberName,
+ Expression initializer,
+ bool expectStaticMember)
+ {
+ if (!memberMap.TryGetValue(memberName, out var member))
+ {
+ return false;
+ }
+ if (IsStaticMember(member) != expectStaticMember)
+ {
+ return false;
+ }
+
+ switch (member)
+ {
+ case FieldDeclaration field:
+ var variable = field.Variables.FirstOrDefault(v => string.Equals(v.Name, memberName, StringComparison.Ordinal));
+ if (variable == null)
+ {
+ return false;
+ }
+ variable.Initializer = initializer.Clone();
+ return true;
+ case PropertyDeclaration property:
+ if (!property.IsAutomaticProperty)
+ {
+ return false;
+ }
+ property.Initializer = initializer.Clone();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static bool IsStaticMember(EntityDeclaration member)
+ {
+ return (member.Modifiers & Modifiers.Static) == Modifiers.Static;
+ }
+
+ private static HashSet CollectConstructorLocalNames(IReadOnlyList statements, int boundaryIndex)
+ {
+ var localNames = new HashSet(StringComparer.Ordinal);
+ for (int i = 0; i < boundaryIndex && i < statements.Count; i++)
+ {
+ if (statements[i] is not VariableDeclarationStatement declaration)
+ {
+ continue;
+ }
+
+ foreach (var variable in declaration.Variables)
+ {
+ if (!string.IsNullOrEmpty(variable.Name))
+ {
+ localNames.Add(variable.Name);
+ }
+ }
+ }
+
+ return localNames;
+ }
+
+ private static bool ReferencesCtorLocal(Expression expression, HashSet localNames)
+ {
+ if (localNames.Count == 0)
+ {
+ return false;
+ }
+
+ if (expression is IdentifierExpression { Identifier: var identifier }
+ && localNames.Contains(identifier))
+ {
+ return true;
+ }
+
+ return expression.Descendants.OfType()
+ .Any(id => localNames.Contains(id.Identifier));
+ }
+
+ private static bool TryMatchSimpleAssignment(Statement statement, TypeDeclaration currentType, out string memberName, out Expression initializer)
+ {
+ memberName = string.Empty;
+ initializer = Expression.Null;
+ if (statement is not ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment })
+ {
+ return false;
+ }
+
+ if (!TryGetAssignedMemberName(assignment.Left, out memberName))
+ {
+ return false;
+ }
+
+ // Keep conservative: only side-effect free literal/create/delegate expressions.
+ if (IsAllowedLiftInitializerExpression(assignment.Right, currentType))
+ {
+ initializer = assignment.Right;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryMatchConditionalTempAssignment(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ TypeDeclaration currentType,
+ out ConditionalTempAssignmentMatch match)
+ {
+ match = default;
+ if (startIndex + 2 >= boundaryIndex
+ || statements[startIndex] is not VariableDeclarationStatement tempDecl
+ || tempDecl.Variables.Count != 1
+ || tempDecl.Variables.FirstOrDefault() is not { } tempVar
+ || !tempVar.Initializer.IsNull)
+ {
+ return false;
+ }
+
+ string tempVarName = tempVar.Name;
+ if (string.IsNullOrEmpty(tempVarName)
+ || statements[startIndex + 1] is not IfElseStatement ifElse
+ || ifElse.TrueStatement is not BlockStatement trueBlock
+ || ifElse.FalseStatement is not BlockStatement falseBlock
+ || !TryMatchConditionalTempBranch(trueBlock, tempVarName, currentType, out var trueInitializer)
+ || !TryMatchConditionalTempBranch(falseBlock, tempVarName, currentType, out var falseInitializer))
+ {
+ return false;
+ }
+
+ if (!TryMatchTerminalAssignment(statements[startIndex + 2], tempVarName, out var memberName))
+ {
+ return false;
+ }
+
+ var conditionalInitializer = new ConditionalExpression(
+ ifElse.Condition.Clone(),
+ trueInitializer.Clone(),
+ falseInitializer.Clone());
+ match = new ConditionalTempAssignmentMatch(
+ memberName,
+ conditionalInitializer,
+ new List { statements[startIndex], statements[startIndex + 1], statements[startIndex + 2] },
+ startIndex + 3
+ );
+ return true;
+ }
+
+ private static bool TryMatchConditionalTempBranch(
+ BlockStatement branchBlock,
+ string tempVarName,
+ TypeDeclaration currentType,
+ out Expression branchInitializer)
+ {
+ branchInitializer = Expression.Null;
+ if (branchBlock.Statements.Count != 2
+ || branchBlock.Statements.FirstOrDefault() is not VariableDeclarationStatement localDecl
+ || localDecl.Variables.Count != 1
+ || localDecl.Variables.FirstOrDefault() is not { } localVar
+ || localVar.Initializer is not { } localInitializer
+ || localInitializer.IsNull
+ || !IsAllowedLiftInitializerExpression(localInitializer, currentType)
+ || branchBlock.Statements.LastOrDefault() is not ExpressionStatement
+ {
+ Expression: AssignmentExpression
+ {
+ Operator: AssignmentOperatorType.Assign,
+ Left: IdentifierExpression { Identifier: var assignedTempVarName },
+ Right: IdentifierExpression { Identifier: var assignedLocalName }
+ }
+ }
+ || !string.Equals(assignedTempVarName, tempVarName, StringComparison.Ordinal)
+ || !string.Equals(assignedLocalName, localVar.Name, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ branchInitializer = localInitializer.Clone();
+ return true;
+ }
+
+ private static bool TryRewriteInitializerWithRecoveredLocals(
+ Expression initializer,
+ IReadOnlyDictionary recoveredInitializersByLocal,
+ out Expression rewritten)
+ {
+ rewritten = initializer;
+ if (recoveredInitializersByLocal.Count == 0)
+ {
+ return false;
+ }
+
+ var clone = initializer.Clone();
+ bool changed = false;
+ if (clone is IdentifierExpression { Identifier: var rootIdentifier }
+ && recoveredInitializersByLocal.TryGetValue(rootIdentifier, out var rootReplacement))
+ {
+ clone = rootReplacement.Clone();
+ changed = true;
+ }
+
+ foreach (var identifier in clone.Descendants.OfType().ToArray())
+ {
+ if (!recoveredInitializersByLocal.TryGetValue(identifier.Identifier, out var replacement))
+ {
+ continue;
+ }
+
+ identifier.ReplaceWith(replacement.Clone());
+ changed = true;
+ }
+
+ if (!changed)
+ {
+ return false;
+ }
+
+ rewritten = clone;
+ return true;
+ }
+
+ private static Expression SimplifyRedundantReadOnlyListCollectionWrappers(Expression initializer)
+ {
+ Expression expression = initializer.Clone();
+
+ while (expression is ObjectCreateExpression rootCreate
+ && TryUnwrapReadOnlyListOfListCollection(rootCreate, out var rootReplacement))
+ {
+ expression = rootReplacement.Clone();
+ }
+
+ foreach (var objectCreate in expression.Descendants.OfType().ToArray())
+ {
+ if (TryUnwrapReadOnlyListOfListCollection(objectCreate, out var replacement))
+ {
+ objectCreate.ReplaceWith(replacement.Clone());
+ }
+ }
+
+ return expression;
+ }
+
+ private static bool TryUnwrapReadOnlyListOfListCollection(ObjectCreateExpression readOnlyListCreate, out Expression collectionExpression)
+ {
+ collectionExpression = Expression.Null;
+ if (!IsTypeIdentifier(readOnlyListCreate.Type, "_003C_003Ez__ReadOnlyList")
+ || readOnlyListCreate.Arguments.Count != 1
+ || readOnlyListCreate.Arguments.FirstOrDefault() is not ObjectCreateExpression listCreate
+ || !IsTypeIdentifier(listCreate.Type, "List")
+ || listCreate.Arguments.Count != 1
+ || listCreate.Arguments.FirstOrDefault() is not ArrayInitializerExpression arrayInitializer
+ || arrayInitializer.Annotation() == null)
+ {
+ return false;
+ }
+
+ collectionExpression = arrayInitializer.Clone();
+ return true;
+ }
+
+ private static bool IsTypeIdentifier(AstType type, string identifier)
+ {
+ return type is SimpleType { Identifier: var typeIdentifier }
+ && string.Equals(typeIdentifier, identifier, StringComparison.Ordinal);
+ }
+
+ private static bool IsAllowedMemberReferenceOrInvocation(Expression expression, TypeDeclaration currentType)
+ {
+ if (expression is not MemberReferenceExpression && expression is not InvocationExpression)
+ {
+ return false;
+ }
+
+ var typeMemberNames = CollectTypeMemberNames(currentType);
+ if (ReferencesCurrentTypeMembers(expression, currentType, typeMemberNames))
+ {
+ return false;
+ }
+
+ // Unqualified invocation can bind to a ctor-local function or type method; keep this conservative.
+ if (expression is InvocationExpression { Target: IdentifierExpression })
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static HashSet CollectTypeMemberNames(TypeDeclaration currentType)
+ {
+ var names = new HashSet(StringComparer.Ordinal);
+ foreach (var member in currentType.Members)
+ {
+ switch (member)
+ {
+ case FieldDeclaration field:
+ foreach (var variable in field.Variables)
+ {
+ if (!string.IsNullOrEmpty(variable.Name))
+ {
+ names.Add(variable.Name);
+ }
+ }
+ break;
+ case PropertyDeclaration property:
+ if (!string.IsNullOrEmpty(property.Name))
+ {
+ names.Add(property.Name);
+ }
+ break;
+ case MethodDeclaration method:
+ if (!string.IsNullOrEmpty(method.Name))
+ {
+ names.Add(method.Name);
+ }
+ break;
+ case TypeDeclaration nestedType:
+ if (!string.IsNullOrEmpty(nestedType.Name))
+ {
+ names.Add(nestedType.Name);
+ }
+ break;
+ }
+ }
+
+ return names;
+ }
+
+ private static bool ReferencesCurrentTypeMembers(Expression expression, TypeDeclaration currentType, HashSet typeMemberNames)
+ {
+ if (expression is ThisReferenceExpression
+ || expression is BaseReferenceExpression)
+ {
+ return true;
+ }
+
+ if (expression is IdentifierExpression { Identifier: var identifier }
+ && typeMemberNames.Contains(identifier))
+ {
+ return true;
+ }
+
+ if (expression is MemberReferenceExpression memberReference
+ && memberReference.Target is IdentifierExpression { Identifier: var targetIdentifier }
+ && string.Equals(targetIdentifier, currentType.Name, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ if (expression is MemberReferenceExpression
+ {
+ Target: TypeReferenceExpression
+ {
+ Type: SimpleType { Identifier: var targetTypeIdentifier }
+ }
+ }
+ && string.Equals(targetTypeIdentifier, currentType.Name, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ if (expression.Descendants.OfType().Any()
+ || expression.Descendants.OfType().Any())
+ {
+ return true;
+ }
+
+ if (expression.Descendants.OfType()
+ .Any(id => typeMemberNames.Contains(id.Identifier)))
+ {
+ return true;
+ }
+
+ if (expression.Descendants.OfType()
+ .Any(member => member.Target is IdentifierExpression { Identifier: var id }
+ && string.Equals(id, currentType.Name, StringComparison.Ordinal)))
+ {
+ return true;
+ }
+
+ return expression.Descendants.OfType()
+ .Any(member => member.Target is TypeReferenceExpression { Type: SimpleType { Identifier: var id } }
+ && string.Equals(id, currentType.Name, StringComparison.Ordinal));
+ }
+
+ private static bool IsAllowedLiftInitializerExpression(Expression expression, TypeDeclaration currentType)
+ {
+ return IsAllowedSimpleInitializerExpression(expression)
+ || IsAllowedMemberReferenceOrInvocation(expression, currentType);
+ }
+
+ private static bool IsAllowedSimpleInitializerExpression(Expression expression)
+ {
+ return expression is PrimitiveExpression
+ || expression is NullReferenceExpression
+ || expression is ObjectCreateExpression
+ || expression is LambdaExpression
+ || expression is AnonymousMethodExpression;
+ }
+
+ private static bool TryGetAssignedMemberName(Expression left, out string memberName)
+ {
+ memberName = string.Empty;
+ if (left is IdentifierExpression ident)
+ {
+ memberName = ident.Identifier;
+ return true;
+ }
+
+ if (left is MemberReferenceExpression memberRef
+ && memberRef.Target is ThisReferenceExpression or IdentifierExpression)
+ {
+ memberName = memberRef.MemberName;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryMatchListPrelude(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ out ListPreludeMatch match)
+ {
+ match = default;
+ if (startIndex + 3 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (statements[startIndex] is not VariableDeclarationStatement listDecl
+ || listDecl.Variables.Count != 1
+ || listDecl.Variables.FirstOrDefault() is not { } listVariable
+ || listVariable.Initializer is not ObjectCreateExpression listCreate)
+ {
+ return false;
+ }
+
+ string listVarName = listVariable.Name;
+ if (string.IsNullOrEmpty(listVarName))
+ {
+ return false;
+ }
+
+ if (!IsCollectionsMarshalCall(statements[startIndex + 1], "SetCount", listVarName))
+ {
+ return false;
+ }
+
+ if (statements[startIndex + 2] is not VariableDeclarationStatement spanDecl
+ || spanDecl.Variables.Count != 1
+ || spanDecl.Variables.FirstOrDefault() is not { } spanVariable
+ || spanVariable.Initializer is not InvocationExpression spanInit
+ || !IsCollectionsMarshalAsSpanCall(spanInit, listVarName))
+ {
+ return false;
+ }
+
+ string spanVarName = spanVariable.Name;
+ var values = new List();
+ var matchedStatements = new List { statements[startIndex], statements[startIndex + 1], statements[startIndex + 2] };
+ string targetMemberName = string.Empty;
+ int i = startIndex + 3;
+ for (; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryMatchTerminalAssignment(statement, listVarName, out targetMemberName))
+ {
+ matchedStatements.Add(statement);
+ break;
+ }
+
+ if (TryCollectSpanAssignmentValue(statement, spanVarName, out var value))
+ {
+ values.Add(value.Clone());
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (TryCollectNestedSpanAssignmentValue(
+ statements,
+ i,
+ boundaryIndex,
+ spanVarName,
+ out var nestedValue,
+ out var nextIndex,
+ out var nestedMatchedStatements))
+ {
+ values.Add(nestedValue.Clone());
+ matchedStatements.AddRange(nestedMatchedStatements);
+ i = nextIndex - 1;
+ continue;
+ }
+
+ if (IsAllowedPreludeNoise(statement))
+ {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (i >= boundaryIndex || string.IsNullOrEmpty(targetMemberName))
+ {
+ return false;
+ }
+
+ var collectionInitializer = new ObjectCreateExpression(listDecl.Type.Clone())
+ {
+ Initializer = new ArrayInitializerExpression(values)
+ };
+ match = new ListPreludeMatch(
+ listVarName,
+ targetMemberName,
+ collectionInitializer,
+ matchedStatements,
+ i + 1
+ );
+ return true;
+ }
+
+ private static bool TryMatchListAddRangeSpreadPrelude(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ out ListPreludeMatch match)
+ {
+ match = default;
+ if (startIndex + 2 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (statements[startIndex] is not VariableDeclarationStatement listDecl
+ || listDecl.Variables.Count != 1
+ || listDecl.Variables.FirstOrDefault() is not { } listVariable
+ || listVariable.Initializer is not ObjectCreateExpression listCreate
+ || !IsGenericCollectionType(listCreate.Type, "List"))
+ {
+ return false;
+ }
+
+ string listVarName = listVariable.Name;
+ if (string.IsNullOrEmpty(listVarName))
+ {
+ return false;
+ }
+
+ var segments = new List();
+ var matchedStatements = new List { statements[startIndex] };
+ string targetMemberName = string.Empty;
+ int i = startIndex + 1;
+ for (; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryMatchTerminalAssignment(statement, listVarName, out targetMemberName))
+ {
+ matchedStatements.Add(statement);
+ break;
+ }
+
+ if (TryMatchCollectionSpreadOperation(statement, listVarName, "AddRange", out var spreadSource))
+ {
+ segments.Add(SpreadSegment.FromRange(spreadSource.Clone()));
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (TryMatchCollectionSpreadOperation(statement, listVarName, "Add", out var addValue))
+ {
+ segments.Add(SpreadSegment.FromValue(addValue.Clone()));
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (IsAllowedPreludeNoise(statement))
+ {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (segments.Count == 0 || i >= boundaryIndex || string.IsNullOrEmpty(targetMemberName))
+ {
+ return false;
+ }
+
+ if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, out var collectionInitializer))
+ {
+ return false;
+ }
+
+ match = new ListPreludeMatch(
+ listVarName,
+ targetMemberName,
+ collectionInitializer,
+ matchedStatements,
+ i + 1
+ );
+ return true;
+ }
+
+ private static bool TryMatchHashSetForeachSpreadPrelude(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ out ListPreludeMatch match)
+ {
+ match = default;
+ if (startIndex + 2 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (statements[startIndex] is not VariableDeclarationStatement setDecl
+ || setDecl.Variables.Count != 1
+ || setDecl.Variables.FirstOrDefault() is not { } setVariable
+ || setVariable.Initializer is not ObjectCreateExpression setCreate
+ || !IsGenericCollectionType(setCreate.Type, "HashSet"))
+ {
+ return false;
+ }
+
+ string setVarName = setVariable.Name;
+ if (string.IsNullOrEmpty(setVarName))
+ {
+ return false;
+ }
+
+ var segments = new List();
+ var matchedStatements = new List { statements[startIndex] };
+ string targetMemberName = string.Empty;
+ int i = startIndex + 1;
+ for (; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryMatchTerminalAssignment(statement, setVarName, out targetMemberName))
+ {
+ matchedStatements.Add(statement);
+ break;
+ }
+
+ if (TryMatchHashSetSpreadForeach(statement, setVarName, out var spreadEnumerable))
+ {
+ segments.Add(SpreadSegment.FromRange(spreadEnumerable.Clone()));
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (TryMatchCollectionSpreadOperation(statement, setVarName, "Add", out var addValue))
+ {
+ segments.Add(SpreadSegment.FromValue(addValue.Clone()));
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (IsAllowedPreludeNoise(statement))
+ {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (segments.Count == 0 || i >= boundaryIndex || string.IsNullOrEmpty(targetMemberName))
+ {
+ return false;
+ }
+
+ if (!TryBuildCollectionFromSpreadSegments(setDecl.Type, segments, out var collectionInitializer))
+ {
+ return false;
+ }
+
+ match = new ListPreludeMatch(
+ setVarName,
+ targetMemberName,
+ collectionInitializer,
+ matchedStatements,
+ i + 1
+ );
+ return true;
+ }
+
+ private static bool TryMatchListSpreadBuilderWrapperAssignment(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ TypeDeclaration currentType,
+ out ListPreludeMatch match)
+ {
+ match = default;
+ if (startIndex + 2 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (statements[startIndex] is not VariableDeclarationStatement listDecl
+ || listDecl.Variables.Count != 1
+ || listDecl.Variables.FirstOrDefault() is not { } listVariable
+ || listVariable.Initializer is not ObjectCreateExpression listCreate
+ || !IsGenericCollectionType(listCreate.Type, "List"))
+ {
+ return false;
+ }
+
+ string listVarName = listVariable.Name;
+ if (string.IsNullOrEmpty(listVarName))
+ {
+ return false;
+ }
+
+ var segments = new List();
+ var matchedStatements = new List { statements[startIndex] };
+ int i = startIndex + 1;
+ for (; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryMatchCollectionSpreadOperation(statement, listVarName, "AddRange", out var spreadSource))
+ {
+ segments.Add(SpreadSegment.FromRange(spreadSource.Clone()));
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (TryMatchCollectionSpreadOperation(statement, listVarName, "Add", out var addValue))
+ {
+ segments.Add(SpreadSegment.FromValue(addValue.Clone()));
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (IsAllowedPreludeNoise(statement))
+ {
+ continue;
+ }
+
+ break;
+ }
+
+ if (segments.Count == 0 || i >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, out var listBuilderInitializer))
+ {
+ return false;
+ }
+
+ if (!TryMatchSimpleAssignment(statements[i], currentType, out var targetMemberName, out var assignmentInitializer))
+ {
+ return false;
+ }
+
+ var replacementMap = new Dictionary(StringComparer.Ordinal)
+ {
+ [listVarName] = listBuilderInitializer
+ };
+ if (!TryRewriteInitializerWithRecoveredLocals(assignmentInitializer, replacementMap, out var rewrittenInitializer))
+ {
+ return false;
+ }
+ rewrittenInitializer = SimplifyRedundantReadOnlyListCollectionWrappers(rewrittenInitializer);
+
+ matchedStatements.Add(statements[i]);
+ match = new ListPreludeMatch(
+ listVarName,
+ targetMemberName,
+ rewrittenInitializer,
+ matchedStatements,
+ i + 1
+ );
+ return true;
+ }
+
+ private static bool IsGenericCollectionType(AstType type, string expectedIdentifier)
+ {
+ return type is SimpleType { Identifier: var identifier } simpleType
+ && string.Equals(identifier, expectedIdentifier, StringComparison.Ordinal)
+ && simpleType.TypeArguments.Count == 1;
+ }
+
+ private static bool TryMatchCollectionSpreadOperation(
+ Statement statement,
+ string collectionVariableName,
+ string methodName,
+ out Expression argument)
+ {
+ argument = Expression.Null;
+ if (statement is not ExpressionStatement { Expression: InvocationExpression invocation }
+ || invocation.Target is not MemberReferenceExpression memberRef
+ || !string.Equals(memberRef.MemberName, methodName, StringComparison.Ordinal)
+ || memberRef.Target is not IdentifierExpression { Identifier: var targetIdentifier }
+ || !string.Equals(targetIdentifier, collectionVariableName, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (invocation.Arguments.Count != 1)
+ {
+ return false;
+ }
+
+ argument = invocation.Arguments.First().Clone();
+ return true;
+ }
+
+ private static bool TryMatchHashSetSpreadForeach(
+ Statement statement,
+ string setVariableName,
+ out Expression spreadEnumerable)
+ {
+ spreadEnumerable = Expression.Null;
+ if (statement is not ForeachStatement foreachStatement
+ || foreachStatement.EmbeddedStatement is not BlockStatement foreachBlock
+ || foreachBlock.Statements.Count != 1
+ || foreachBlock.Statements.FirstOrDefault() is not ExpressionStatement { Expression: InvocationExpression addInvocation }
+ || addInvocation.Target is not MemberReferenceExpression addTarget
+ || !string.Equals(addTarget.MemberName, "Add", StringComparison.Ordinal)
+ || addTarget.Target is not IdentifierExpression { Identifier: var addTargetIdentifier }
+ || !string.Equals(addTargetIdentifier, setVariableName, StringComparison.Ordinal)
+ || addInvocation.Arguments.Count != 1
+ || addInvocation.Arguments.FirstOrDefault() is not IdentifierExpression { Identifier: var addedIdentifier }
+ || foreachStatement.VariableDesignation is not SingleVariableDesignation { Identifier: var loopIdentifier }
+ || !string.Equals(addedIdentifier, loopIdentifier, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ spreadEnumerable = foreachStatement.InExpression.Clone();
+ return true;
+ }
+
+ private static bool TryBuildCollectionFromSpreadSegments(
+ AstType collectionType,
+ IReadOnlyList segments,
+ out Expression initializer)
+ {
+ initializer = Expression.Null;
+ if (segments.Count == 0)
+ {
+ return false;
+ }
+
+ var elements = new List();
+ foreach (var segment in segments)
+ {
+ var element = segment.Expression.Clone();
+ if (segment.IsSpread)
+ {
+ element.AddAnnotation(CollectionExpressionSpreadElementAnnotation.Instance);
+ }
+ elements.Add(element);
+ }
+
+ var collectionExpression = new ArrayInitializerExpression(elements);
+ collectionExpression.AddAnnotation(CollectionExpressionArrayAnnotation.Instance);
+ initializer = new ObjectCreateExpression(collectionType.Clone(), collectionExpression);
+ return true;
+ }
+
+ private static bool IsCollectionsMarshalCall(Statement statement, string methodName, string firstArgIdentifier)
+ {
+ if (statement is not ExpressionStatement { Expression: InvocationExpression invocation }
+ || invocation.Target is not MemberReferenceExpression memberRef
+ || !string.Equals(memberRef.MemberName, methodName, StringComparison.Ordinal)
+ || !IsCollectionsMarshalTarget(memberRef.Target))
+ {
+ return false;
+ }
+
+ if (invocation.Arguments.FirstOrDefault() is not IdentifierExpression { Identifier: var firstArg })
+ {
+ return false;
+ }
+
+ return string.Equals(firstArg, firstArgIdentifier, StringComparison.Ordinal);
+ }
+
+ private static bool IsCollectionsMarshalAsSpanCall(InvocationExpression invocation, string listVarName)
+ {
+ if (invocation.Target is not MemberReferenceExpression memberRef
+ || !string.Equals(memberRef.MemberName, "AsSpan", StringComparison.Ordinal)
+ || !IsCollectionsMarshalTarget(memberRef.Target))
+ {
+ return false;
+ }
+
+ return invocation.Arguments.FirstOrDefault() is IdentifierExpression { Identifier: var firstArg }
+ && string.Equals(firstArg, listVarName, StringComparison.Ordinal);
+ }
+
+ private static bool IsCollectionsMarshalTarget(Expression target)
+ {
+ if (target is IdentifierExpression { Identifier: "CollectionsMarshal" })
+ {
+ return true;
+ }
+
+ if (target is TypeReferenceExpression { Type: SimpleType { Identifier: "CollectionsMarshal" } })
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryCollectSpanAssignmentValue(Statement statement, string spanVarName, out Expression value)
+ {
+ value = Expression.Null;
+ if (statement is not ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment })
+ {
+ return false;
+ }
+
+ if (assignment.Left is IndexerExpression
+ {
+ Target: IdentifierExpression { Identifier: var targetName }
+ }
+ && string.Equals(targetName, spanVarName, StringComparison.Ordinal))
+ {
+ value = assignment.Right;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryCollectNestedSpanAssignmentValue(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ string outerSpanVarName,
+ out Expression value,
+ out int nextIndex,
+ out List matchedStatements)
+ {
+ value = Expression.Null;
+ nextIndex = -1;
+ matchedStatements = [];
+ if (startIndex + 1 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (statements[startIndex] is not VariableDeclarationStatement refSlotDecl
+ || refSlotDecl.Variables.Count != 1
+ || refSlotDecl.Variables.FirstOrDefault() is not { } refSlotVar
+ || refSlotVar.Initializer is not DirectionExpression
+ {
+ FieldDirection: FieldDirection.Ref,
+ Expression: IndexerExpression
+ {
+ Target: IdentifierExpression { Identifier: var slotTargetSpan }
+ }
+ }
+ || !string.Equals(slotTargetSpan, outerSpanVarName, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ string slotVariableName = refSlotVar.Name;
+ if (string.IsNullOrEmpty(slotVariableName))
+ {
+ return false;
+ }
+
+ if (statements[startIndex + 1] is not VariableDeclarationStatement objectDecl
+ || objectDecl.Variables.Count != 1
+ || objectDecl.Variables.FirstOrDefault() is not { } objectVar
+ || objectVar.Initializer is not ObjectCreateExpression objectCreate)
+ {
+ return false;
+ }
+
+ string objectVariableName = objectVar.Name;
+ if (string.IsNullOrEmpty(objectVariableName))
+ {
+ return false;
+ }
+
+ var objectValue = (ObjectCreateExpression)objectCreate.Clone();
+ objectValue.Initializer ??= new ArrayInitializerExpression();
+ var assignedMembers = new HashSet(StringComparer.Ordinal);
+ foreach (var named in objectValue.Initializer.Elements.OfType())
+ {
+ assignedMembers.Add(named.Name);
+ }
+
+ matchedStatements.Add(statements[startIndex]);
+ matchedStatements.Add(statements[startIndex + 1]);
+
+ for (int i = startIndex + 2; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryMatchRefSlotAssignment(statement, slotVariableName, objectVariableName))
+ {
+ matchedStatements.Add(statement);
+ value = objectValue;
+ nextIndex = i + 1;
+ return true;
+ }
+
+ if (TryMatchObjectMemberListPrelude(
+ statements,
+ i,
+ boundaryIndex,
+ objectVariableName,
+ out var listMemberName,
+ out var listMemberValue,
+ out var listMatchedStatements,
+ out var listNextIndex))
+ {
+ if (!assignedMembers.Add(listMemberName))
+ {
+ return false;
+ }
+
+ AddObjectMemberInitializer(objectValue, listMemberName, listMemberValue);
+ matchedStatements.AddRange(listMatchedStatements);
+ i = listNextIndex - 1;
+ continue;
+ }
+
+ if (TryMatchObjectMemberSimpleAssignment(statement, objectVariableName, out var memberName, out var memberValue))
+ {
+ if (!assignedMembers.Add(memberName))
+ {
+ return false;
+ }
+
+ AddObjectMemberInitializer(objectValue, memberName, memberValue);
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (IsAllowedPreludeNoise(statement))
+ {
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
+ private static bool TryMatchObjectMemberListPrelude(
+ IReadOnlyList statements,
+ int startIndex,
+ int boundaryIndex,
+ string objectVariableName,
+ out string memberName,
+ out Expression initializerValue,
+ out List matchedStatements,
+ out int nextIndex)
+ {
+ memberName = string.Empty;
+ initializerValue = Expression.Null;
+ matchedStatements = [];
+ nextIndex = -1;
+ if (startIndex + 3 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (statements[startIndex] is not VariableDeclarationStatement listDecl
+ || listDecl.Variables.Count != 1
+ || listDecl.Variables.FirstOrDefault() is not { } listVariable
+ || listVariable.Initializer is not ObjectCreateExpression)
+ {
+ return false;
+ }
+
+ string listVarName = listVariable.Name;
+ if (string.IsNullOrEmpty(listVarName))
+ {
+ return false;
+ }
+
+ if (!IsCollectionsMarshalCall(statements[startIndex + 1], "SetCount", listVarName))
+ {
+ return false;
+ }
+
+ if (statements[startIndex + 2] is not VariableDeclarationStatement spanDecl
+ || spanDecl.Variables.Count != 1
+ || spanDecl.Variables.FirstOrDefault() is not { } spanVariable
+ || spanVariable.Initializer is not InvocationExpression spanInit
+ || !IsCollectionsMarshalAsSpanCall(spanInit, listVarName))
+ {
+ return false;
+ }
+
+ string spanVarName = spanVariable.Name;
+ var values = new List();
+ matchedStatements.Add(statements[startIndex]);
+ matchedStatements.Add(statements[startIndex + 1]);
+ matchedStatements.Add(statements[startIndex + 2]);
+
+ for (int i = startIndex + 3; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryMatchTerminalObjectMemberAssignment(statement, listVarName, objectVariableName, out memberName))
+ {
+ matchedStatements.Add(statement);
+ initializerValue = new ObjectCreateExpression(listDecl.Type.Clone())
+ {
+ Initializer = new ArrayInitializerExpression(values)
+ };
+ nextIndex = i + 1;
+ return true;
+ }
+
+ if (TryCollectSpanAssignmentValue(statement, spanVarName, out var value))
+ {
+ values.Add(value.Clone());
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ if (IsAllowedPreludeNoise(statement))
+ {
+ matchedStatements.Add(statement);
+ continue;
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
+ private static bool TryMatchObjectMemberSimpleAssignment(
+ Statement statement,
+ string objectVariableName,
+ out string memberName,
+ out Expression memberValue)
+ {
+ memberName = string.Empty;
+ memberValue = Expression.Null;
+ if (statement is not ExpressionStatement
+ {
+ Expression: AssignmentExpression
+ {
+ Operator: AssignmentOperatorType.Assign,
+ Left: MemberReferenceExpression
+ {
+ Target: IdentifierExpression { Identifier: var targetName },
+ MemberName: var assignedMemberName
+ },
+ Right: var right
+ }
+ }
+ || !string.Equals(targetName, objectVariableName, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (!IsAllowedSimpleInitializerExpression(right))
+ {
+ return false;
+ }
+
+ memberName = assignedMemberName;
+ memberValue = right;
+ return true;
+ }
+
+ private static bool TryMatchTerminalObjectMemberAssignment(
+ Statement statement,
+ string listVarName,
+ string objectVariableName,
+ out string memberName)
+ {
+ memberName = string.Empty;
+ if (statement is not ExpressionStatement
+ {
+ Expression: AssignmentExpression
+ {
+ Operator: AssignmentOperatorType.Assign,
+ Left: MemberReferenceExpression
+ {
+ Target: IdentifierExpression { Identifier: var targetName },
+ MemberName: var assignedMemberName
+ },
+ Right: IdentifierExpression { Identifier: var rightName }
+ }
+ }
+ || !string.Equals(targetName, objectVariableName, StringComparison.Ordinal)
+ || !string.Equals(rightName, listVarName, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ memberName = assignedMemberName;
+ return true;
+ }
+
+ private static bool TryMatchRefSlotAssignment(Statement statement, string slotVariableName, string objectVariableName)
+ {
+ if (statement is not ExpressionStatement
+ {
+ Expression: AssignmentExpression
+ {
+ Operator: AssignmentOperatorType.Assign,
+ Left: IdentifierExpression { Identifier: var leftName },
+ Right: IdentifierExpression { Identifier: var rightName }
+ }
+ })
+ {
+ return false;
+ }
+
+ return string.Equals(leftName, slotVariableName, StringComparison.Ordinal)
+ && string.Equals(rightName, objectVariableName, StringComparison.Ordinal);
+ }
+
+ private static void AddObjectMemberInitializer(ObjectCreateExpression objectValue, string memberName, Expression memberValue)
+ {
+ objectValue.Initializer ??= new ArrayInitializerExpression();
+ objectValue.Initializer.Elements.Add(new NamedExpression(memberName, memberValue.Clone()));
+ }
+
+ private static bool IsAllowedPreludeNoise(Statement statement)
+ {
+ // Numeric temp declarations and increments are common in this lowering pattern.
+ if (statement is VariableDeclarationStatement
+ {
+ Variables.Count: 1
+ } variableDecl
+ && variableDecl.Variables.FirstOrDefault() is { Name: var varName }
+ && IsTempCounterName(varName))
+ {
+ return true;
+ }
+
+ if (statement is ExpressionStatement
+ {
+ Expression: UnaryOperatorExpression
+ {
+ Operator: UnaryOperatorType.PostIncrement,
+ Expression: IdentifierExpression { Identifier: var incrementIdentifier }
+ }
+ }
+ && IsTempCounterName(incrementIdentifier))
+ {
+ return true;
+ }
+
+ if (statement is ExpressionStatement
+ {
+ Expression: AssignmentExpression
+ {
+ Operator: AssignmentOperatorType.Assign,
+ Left: IdentifierExpression { Identifier: var assignmentIdentifier }
+ }
+ }
+ && IsTempCounterName(assignmentIdentifier))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsTempCounterName(string identifier)
+ {
+ return identifier.StartsWith("num", StringComparison.Ordinal);
+ }
+
+ private static void CleanupLeadingTempNoise(ConstructorDeclaration constructorDeclaration)
+ {
+ var statements = constructorDeclaration.Body.Statements.ToArray();
+ var prefixNoise = new List();
+
+ foreach (var statement in statements)
+ {
+ if (!IsAllowedPreludeNoise(statement))
+ {
+ break;
+ }
+
+ prefixNoise.Add(statement);
+ }
+
+ if (prefixNoise.Count == 0)
+ {
+ return;
+ }
+
+ var statementsAfterNoise = statements.Skip(prefixNoise.Count).ToArray();
+ foreach (var statement in prefixNoise)
+ {
+ // Declarations are removed only when no longer referenced after the generated prelude.
+ if (statement is VariableDeclarationStatement
+ {
+ Variables.Count: 1
+ } declaration
+ && declaration.Variables.FirstOrDefault() is { Name: var name }
+ && IsTempCounterName(name))
+ {
+ if (!IsIdentifierReferencedOutsidePrefix(name, statementsAfterNoise, constructorDeclaration.Initializer))
+ {
+ statement.Remove();
+ }
+
+ continue;
+ }
+
+ statement.Remove();
+ }
+ }
+
+ private static bool IsIdentifierReferencedOutsidePrefix(
+ string identifier,
+ IReadOnlyList statementsAfterPrefix,
+ ConstructorInitializer constructorInitializer)
+ {
+ foreach (var statement in statementsAfterPrefix)
+ {
+ if (statement.Descendants.OfType()
+ .Any(id => string.Equals(id.Identifier, identifier, StringComparison.Ordinal)))
+ {
+ return true;
+ }
+ }
+
+ if (!constructorInitializer.IsNull
+ && constructorInitializer.Descendants.OfType()
+ .Any(id => string.Equals(id.Identifier, identifier, StringComparison.Ordinal)))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryMatchTerminalAssignment(Statement statement, string listVarName, out string targetMemberName)
+ {
+ targetMemberName = string.Empty;
+ if (statement is not ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment })
+ {
+ return false;
+ }
+
+ if (!TryGetAssignedMemberName(assignment.Left, out targetMemberName))
+ {
+ return false;
+ }
+
+ return assignment.Right is IdentifierExpression { Identifier: var rightName }
+ && string.Equals(rightName, listVarName, StringComparison.Ordinal);
+ }
+
+ private static bool TryBuildConstructorInitializer(
+ ConstructorDeclaration constructorDeclaration,
+ IReadOnlyList statements,
+ int boundaryIndex,
+ Statement boundaryStatement,
+ Dictionary matchedListInitByLocal,
+ out ConstructorInitializer constructorInitializer,
+ out List matchedCtorPreludeStatements)
+ {
+ constructorInitializer = ConstructorInitializer.Null;
+ matchedCtorPreludeStatements = [];
+ if (!TryGetCtorInvocation(boundaryStatement, out var initType, out var invocation))
+ {
+ return false;
+ }
+
+ var newInitializer = new ConstructorInitializer
+ {
+ ConstructorInitializerType = initType
+ };
+
+ foreach (var arg in invocation.Arguments)
+ {
+ switch (arg)
+ {
+ case IdentifierExpression { Identifier: var localName } when matchedListInitByLocal.TryGetValue(localName, out var recoveredExpression):
+ newInitializer.Arguments.Add(recoveredExpression.Clone());
+ break;
+ case IdentifierExpression { Identifier: var localName }:
+ if (constructorDeclaration.Parameters.Any(parameter => parameter.Name == localName))
+ {
+ newInitializer.Arguments.Add(arg.Clone());
+ break;
+ }
+ if (!TryMatchCtorArgumentListPrelude(
+ statements,
+ boundaryIndex,
+ localName,
+ out var ctorArgInitializer,
+ out var matchedStatements))
+ {
+ return false;
+ }
+ newInitializer.Arguments.Add(ctorArgInitializer);
+ matchedCtorPreludeStatements.AddRange(matchedStatements);
+ break;
+ case PrimitiveExpression:
+ case NullReferenceExpression:
+ case ObjectCreateExpression:
+ newInitializer.Arguments.Add(arg.Clone());
+ break;
+ default:
+ return false;
+ }
+ }
+
+ constructorInitializer = newInitializer;
+ return true;
+ }
+
+ private static bool TryMatchCtorArgumentListPrelude(
+ IReadOnlyList statements,
+ int boundaryIndex,
+ string listVarName,
+ out Expression initializer,
+ out List matchedStatements)
+ {
+ initializer = Expression.Null;
+ matchedStatements = [];
+ int startIndex = -1;
+ VariableDeclarationStatement? listDecl = null;
+ for (int i = 0; i < boundaryIndex; i++)
+ {
+ if (statements[i] is VariableDeclarationStatement variableDeclaration
+ && variableDeclaration.Variables.Count == 1
+ && variableDeclaration.Variables.FirstOrDefault() is { } variable
+ && string.Equals(variable.Name, listVarName, StringComparison.Ordinal)
+ && variable.Initializer is ObjectCreateExpression)
+ {
+ startIndex = i;
+ listDecl = variableDeclaration;
+ }
+ }
+
+ if (startIndex < 0 || listDecl == null || startIndex + 2 >= boundaryIndex)
+ {
+ return false;
+ }
+
+ if (!IsCollectionsMarshalCall(statements[startIndex + 1], "SetCount", listVarName))
+ {
+ return false;
+ }
+
+ if (statements[startIndex + 2] is not VariableDeclarationStatement spanDecl
+ || spanDecl.Variables.Count != 1
+ || spanDecl.Variables.FirstOrDefault() is not { } spanVariable
+ || spanVariable.Initializer is not InvocationExpression spanInit
+ || !IsCollectionsMarshalAsSpanCall(spanInit, listVarName))
+ {
+ return false;
+ }
+
+ string spanVarName = spanVariable.Name;
+ var values = new List();
+ for (int i = startIndex + 3; i < boundaryIndex; i++)
+ {
+ var statement = statements[i];
+ if (TryCollectSpanAssignmentValue(statement, spanVarName, out var value))
+ {
+ values.Add(value.Clone());
+ continue;
+ }
+
+ if (!IsAllowedPreludeNoise(statement))
+ {
+ return false;
+ }
+ }
+
+ if (values.Count == 0)
+ {
+ return false;
+ }
+
+ initializer = new ObjectCreateExpression(listDecl.Type.Clone())
+ {
+ Initializer = new ArrayInitializerExpression(values)
+ };
+ for (int i = startIndex; i < boundaryIndex; i++)
+ {
+ matchedStatements.Add(statements[i]);
+ }
+ return true;
+ }
+
+ private static void ReorderRecoveredMembers(
+ TypeDeclaration typeDeclaration,
+ List recoveredMemberOrder,
+ Dictionary memberMap)
+ {
+ var uniqueNames = recoveredMemberOrder.Distinct(StringComparer.Ordinal).ToList();
+ if (uniqueNames.Count < 2)
+ {
+ return;
+ }
+
+ var participatingMembers = uniqueNames
+ .Where(memberMap.ContainsKey)
+ .Select(name => memberMap[name])
+ .Distinct()
+ .ToList();
+ if (participatingMembers.Count < 2)
+ {
+ return;
+ }
+
+ var currentMembers = typeDeclaration.Members.ToArray();
+ var participatingIndices = currentMembers
+ .Select((member, index) => (member, index))
+ .Where(tuple => participatingMembers.Contains(tuple.member))
+ .Select(tuple => tuple.index)
+ .ToArray();
+ if (participatingIndices.Length < 2)
+ {
+ return;
+ }
+ var participatingSet = participatingMembers.ToHashSet();
+ var reorderedParticipants = uniqueNames
+ .Select(name => memberMap.TryGetValue(name, out var member) ? member : null)
+ .Where(member => member != null && participatingSet.Contains(member))
+ .Distinct()
+ .Cast()
+ .ToList();
+ if (reorderedParticipants.Count != participatingMembers.Count)
+ {
+ // Ambiguous mapping; keep declaration order unchanged.
+ return;
+ }
+
+ var rewrittenMembers = currentMembers.ToArray();
+ int nextParticipant = 0;
+ for (int i = 0; i < currentMembers.Length; i++)
+ {
+ if (participatingSet.Contains(currentMembers[i]))
+ {
+ rewrittenMembers[i] = reorderedParticipants[nextParticipant++];
+ }
+ }
+
+ foreach (var member in currentMembers)
+ {
+ member.Remove();
+ }
+
+ foreach (var member in rewrittenMembers)
+ {
+ typeDeclaration.InsertChildBefore(typeDeclaration.RBraceToken, member, Roles.TypeMemberRole);
+ }
+ }
+
+ private readonly record struct ListPreludeMatch(
+ string ListVariableName,
+ string TargetMemberName,
+ Expression InitializerExpression,
+ List MatchedStatements,
+ int NextIndex
+ );
+
+ private readonly record struct ConditionalTempAssignmentMatch(
+ string TargetMemberName,
+ Expression InitializerExpression,
+ List MatchedStatements,
+ int NextIndex
+ );
+
+ private readonly record struct SpreadSegment(bool IsSpread, Expression Expression)
+ {
+ public static SpreadSegment FromRange(Expression expression)
+ {
+ return new SpreadSegment(true, expression);
+ }
+
+ public static SpreadSegment FromValue(Expression expression)
+ {
+ return new SpreadSegment(false, expression);
+ }
+ }
+}
diff --git a/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs b/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs
index 597c1a11..6f5e3fb7 100644
--- a/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs
+++ b/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs
@@ -26,7 +26,7 @@ public static async Task DownloadFileTaskAsync(this HttpClient client, Uri uri,
public static class NugetDetails
{
- public static async Task ResolvePackageAndGetContentHash(string name, string version, CancellationToken cancellationToken)
+ public static async Task ResolvePackageAndGetContentHash(string name, string version, bool checkOnline, CancellationToken cancellationToken)
{
// download the package to the local cache
string? p = null;
@@ -59,10 +59,13 @@ public static class NugetDetails
// either we didn't have a metadata file or it wasn't from nuget.org, so we need to download it again
// save it to a temporary directory so as not to clobber the local cache
var tempDir = Path.Combine(NuGetEnvironment.GetFolderPath(NuGetFolderPath.Temp), name.ToLower(), version.ToLower());
- p = await DownloadPackageFromNugetAsync(name, version, tempDir, cancellationToken);
+ if (checkOnline)
+ {
+ p = await DownloadPackageFromNugetAsync(name, version, tempDir, cancellationToken);
+ }
}
- else
+ else if (checkOnline)
{
p = await DownloadPackageToLocalCache(name, version, cancellationToken);
}
diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs b/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs
new file mode 100644
index 00000000..c516cae3
--- /dev/null
+++ b/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs
@@ -0,0 +1,50 @@
+using ICSharpCode.Decompiler.CSharp.Syntax;
+using ICSharpCode.Decompiler.CSharp.Transforms;
+
+namespace GodotMonoDecomp;
+
+///
+/// Removes erroneous base._002Ector() calls that sometimes appear at the end of
+/// constructor bodies in decompiled output.
+///
+public class RemoveBogusBaseConstructorCalls : DepthFirstAstVisitor, IAstTransform
+{
+ ConstructorDeclaration? currentConstructor;
+
+ public void Run(AstNode rootNode, TransformContext context)
+ {
+ rootNode.AcceptVisitor(this);
+ }
+
+ public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration)
+ {
+ var previousConstructor = currentConstructor;
+ currentConstructor = constructorDeclaration;
+ try
+ {
+ base.VisitConstructorDeclaration(constructorDeclaration);
+ }
+ finally
+ {
+ currentConstructor = previousConstructor;
+ }
+ }
+
+ public override void VisitInvocationExpression(InvocationExpression invocationExpression)
+ {
+ if (currentConstructor?.Body != null
+ && invocationExpression.Arguments.Count == 0
+ && invocationExpression.Target is MemberReferenceExpression memberReference
+ && memberReference.MemberName == "_002Ector"
+ && memberReference.Target is BaseReferenceExpression
+ && invocationExpression.Parent is ExpressionStatement expressionStatement
+ && expressionStatement.Parent == currentConstructor.Body
+ && currentConstructor.Body.Statements.LastOrDefault() == expressionStatement)
+ {
+ expressionStatement.Remove();
+ return;
+ }
+
+ base.VisitInvocationExpression(invocationExpression);
+ }
+}
diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs b/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs
new file mode 100644
index 00000000..0cba4cf8
--- /dev/null
+++ b/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs
@@ -0,0 +1,176 @@
+using ICSharpCode.Decompiler.CSharp;
+using ICSharpCode.Decompiler.CSharp.Syntax;
+using ICSharpCode.Decompiler.CSharp.Transforms;
+using ICSharpCode.Decompiler.Semantics;
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace GodotMonoDecomp;
+
+///
+/// Restores source-level shape for System.Text.Json source-generation context classes.
+///
+public class RemoveJsonSourceGenerationClassBody : DepthFirstAstVisitor, IAstTransform
+{
+ private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute";
+ private const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
+ private const string JsonSourceGeneratorName = "System.Text.Json.SourceGeneration";
+ private const string JsonTypeInfoFullName = "System.Text.Json.Serialization.Metadata.JsonTypeInfo";
+
+ public void Run(AstNode rootNode, TransformContext context)
+ {
+ rootNode.AcceptVisitor(this);
+ }
+
+ public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration)
+ {
+ if (typeDeclaration.ClassType != ClassType.Class)
+ {
+ base.VisitTypeDeclaration(typeDeclaration);
+ return;
+ }
+
+ bool hasJsonSourceGeneratorGeneratedCode = Common.RemoveGeneratedCodeAttributes(
+ typeDeclaration.Attributes,
+ JsonSourceGeneratorName
+ );
+ if (!hasJsonSourceGeneratorGeneratedCode)
+ {
+ base.VisitTypeDeclaration(typeDeclaration);
+ return;
+ }
+ // get all the parameters for the `JsonSerializable` attribute
+ List jsonTypes = typeDeclaration.Attributes
+ .SelectMany(attribute => attribute.Attributes)
+ .Where(attribute => Common.IsAttribute(attribute, JsonSerializableAttributeFullName, "JsonSerializable"))
+ .SelectMany(attribute => attribute.Arguments)
+ .Select(argument =>
+ {
+ if (argument is TypeOfExpression typeOfExpression)
+ {
+ return typeOfExpression.Type.Annotation()?.Type;
+ }
+ return null;
+ })
+ .OfType()
+ .Distinct()
+ .ToList();
+
+ typeDeclaration.Modifiers |= Modifiers.Partial;
+ var members = typeDeclaration.Members.ToArray();
+ int firstJsonTypeInfoFieldIndex = GetFirstIndex(members, member =>
+ member is FieldDeclaration field && IsJsonTypeInfoTypeWithTemplateArgumentsOf(field.ReturnType, jsonTypes)
+ );
+ int firstJsonTypeInfoPropertyIndex = GetFirstIndex(members, member =>
+ member is PropertyDeclaration property && IsJsonTypeInfoTypeWithTemplateArgumentsOf(property.ReturnType, jsonTypes)
+ );
+ int firstJsonTypeInfoMethodIndex = GetFirstIndex(members, member =>
+ member is MethodDeclaration method && IsJsonTypeInfoTypeWithTemplateArgumentsOf(method.ReturnType, jsonTypes)
+ );
+
+ for (int i = 0; i < members.Length; i++)
+ {
+ var member = members[i];
+ if (ShouldPreserveMember(
+ member,
+ i,
+ firstJsonTypeInfoFieldIndex,
+ firstJsonTypeInfoPropertyIndex,
+ firstJsonTypeInfoMethodIndex))
+ {
+ continue;
+ }
+
+ member.Remove();
+ }
+ }
+
+ private static bool ShouldPreserveMember(
+ EntityDeclaration member,
+ int memberIndex,
+ int firstJsonTypeInfoFieldIndex,
+ int firstJsonTypeInfoPropertyIndex,
+ int firstJsonTypeInfoMethodIndex)
+ {
+ // Preserve any leading user member before the generated JsonTypeInfo field section.
+ if (firstJsonTypeInfoFieldIndex >= 0 && memberIndex < firstJsonTypeInfoFieldIndex)
+ {
+ return true;
+ }
+
+ // Preserve leading user properties before generated JsonTypeInfo properties.
+ if (member is PropertyDeclaration
+ && firstJsonTypeInfoPropertyIndex >= 0
+ && memberIndex < firstJsonTypeInfoPropertyIndex)
+ {
+ return true;
+ }
+
+ // Preserve leading user methods before generated JsonTypeInfo-returning methods.
+ if (member is MethodDeclaration
+ && firstJsonTypeInfoMethodIndex >= 0
+ && memberIndex < firstJsonTypeInfoMethodIndex)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static int GetFirstIndex(EntityDeclaration[] members, Func predicate)
+ {
+ for (int i = 0; i < members.Length; i++)
+ {
+ if (predicate(members[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static bool IsJsonTypeInfoType(AstType type)
+ {
+ if (type.IsNull)
+ {
+ return false;
+ }
+
+ if (type.Annotation() is { Type: { } resolvedType })
+ {
+ if (string.Equals(resolvedType.FullName, JsonTypeInfoFullName, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ if (resolvedType.GetDefinition() is ITypeDefinition definition
+ && string.Equals(definition.FullName, JsonTypeInfoFullName, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ // Fallback for cases where type resolution annotations are unavailable.
+ return type.ToString().Contains("JsonTypeInfo", StringComparison.Ordinal);
+ }
+
+ private static bool IsJsonTypeInfoTypeWithTemplateArgumentsOf(AstType type, IEnumerable jsonTypes)
+ {
+ if (IsJsonTypeInfoType(type))
+ {
+ if (type.Annotation() is { Type: { } resolvedType })
+ {
+ if (resolvedType.TypeArguments.All(parameter => jsonTypes.Any(jsonType =>
+ {
+ return jsonType.Equals(parameter.GetDefinition());
+ })))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+}
diff --git a/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs b/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs
new file mode 100644
index 00000000..fe411abc
--- /dev/null
+++ b/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs
@@ -0,0 +1,94 @@
+using ICSharpCode.Decompiler.CSharp;
+using ICSharpCode.Decompiler.CSharp.Syntax;
+using ICSharpCode.Decompiler.CSharp.Transforms;
+using ICSharpCode.Decompiler.Semantics;
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace GodotMonoDecomp;
+
+///
+/// Restores source-level shape for methods generated by the Regex source generator.
+///
+public class RestoreGeneratedRegexMethods : DepthFirstAstVisitor, IAstTransform
+{
+ private const string GeneratedRegexAttributeFullName = "System.Text.RegularExpressions.GeneratedRegexAttribute";
+ private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute";
+ private const string RegexGeneratorName = "System.Text.RegularExpressions.Generator";
+
+ public void Run(AstNode rootNode, TransformContext context)
+ {
+ rootNode.AcceptVisitor(this);
+ }
+
+ public override void VisitMethodDeclaration(MethodDeclaration methodDeclaration)
+ {
+ if (!HasGeneratedRegexAttribute(methodDeclaration))
+ {
+ base.VisitMethodDeclaration(methodDeclaration);
+ return;
+ }
+
+ bool hasRegexGeneratorGeneratedCode = Common.RemoveGeneratedCodeAttributes(methodDeclaration.Attributes, RegexGeneratorName);
+ bool hasGeneratedBodyPattern = HasGeneratedRegexBodyPattern(methodDeclaration);
+ if (!hasGeneratedBodyPattern && !hasRegexGeneratorGeneratedCode)
+ {
+ base.VisitMethodDeclaration(methodDeclaration);
+ return;
+ }
+
+ methodDeclaration.Modifiers |= Modifiers.Partial;
+ methodDeclaration.Body = null;
+ if (methodDeclaration.Parent is TypeDeclaration containingType)
+ {
+ containingType.Modifiers |= Modifiers.Partial;
+ }
+ }
+
+ private static bool HasGeneratedRegexAttribute(MethodDeclaration methodDeclaration)
+ {
+ foreach (var section in methodDeclaration.Attributes)
+ {
+ foreach (var attribute in section.Attributes)
+ {
+ if (Common.IsAttribute(attribute, GeneratedRegexAttributeFullName, "GeneratedRegex"))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static bool RemoveRegexGeneratorGeneratedCodeAttributes(MethodDeclaration methodDeclaration)
+ {
+ return Common.RemoveGeneratedCodeAttributes(methodDeclaration.Attributes, RegexGeneratorName);
+ }
+
+ private static bool HasGeneratedRegexBodyPattern(MethodDeclaration methodDeclaration)
+ {
+ if (methodDeclaration.Body is not { Statements.Count: 1 })
+ {
+ return false;
+ }
+
+ if (methodDeclaration.Body.Statements.First() is not ReturnStatement returnStatement)
+ {
+ return false;
+ }
+
+ if (returnStatement.Expression is not MemberReferenceExpression memberReference)
+ {
+ return false;
+ }
+
+ if (!string.Equals(memberReference.MemberName, "Instance", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ string targetText = memberReference.Target.ToString();
+ return targetText.Contains("RegexGenerator", StringComparison.Ordinal)
+ || targetText.Contains("_003CRegexGenerator", StringComparison.Ordinal);
+ }
+}
diff --git a/godot-mono-decomp/GodotMonoDecompCLI/Program.cs b/godot-mono-decomp/GodotMonoDecompCLI/Program.cs
index c2fd736b..a9e17715 100644
--- a/godot-mono-decomp/GodotMonoDecompCLI/Program.cs
+++ b/godot-mono-decomp/GodotMonoDecompCLI/Program.cs
@@ -1,4 +1,4 @@
-// See https://aka.ms/new-console-template for more information
+// See https://aka.ms/new-console-template for more information
using System.Text;
using GodotMonoDecomp;
@@ -43,6 +43,7 @@ int Main(string[] args)
settings.CreateAdditionalProjectsForProjectReferences = !result.Value.NoCreateAdditionalProjectsForProjectReferences;
settings.VerifyNuGetPackageIsFromNugetOrg = result.Value.VerifyNuGetPackages;
settings.GodotVersionOverride = result.Value.GodotVersion == null ? null : GodotStuff.ParseGodotVersionFromString(result.Value.GodotVersion);
+ settings.EnableCollectionInitializerLifting = !result.Value.DisableCollectionInitializerLifting || result.Value.EnableCollectionInitializerLifting;
// get the current time
var startTime = DateTime.Now;
// call the DecompileProject function
@@ -106,6 +107,12 @@ public class Options
[Option("no-multi-project", Required = false, HelpText = "Whether to create additional projects for project references in main module.")]
public bool NoCreateAdditionalProjectsForProjectReferences { get; set; }
+ [Option("enable-collection-initializer-lifting", Required = false, HelpText = "Enable LiftCollectionInitializers and disable RemoveBogusBaseConstructorCalls.")]
+ public bool EnableCollectionInitializerLifting { get; set; }
+
+ [Option("disable-collection-initializer-lifting", Required = false, HelpText = "Disable LiftCollectionInitializers and run RemoveBogusBaseConstructorCalls instead.")]
+ public bool DisableCollectionInitializerLifting { get; set; }
+
[Option("write-script-info", Required = false, HelpText = "Write script info to a JSON file in the output directory.")]
public bool WriteScriptInfo { get; set; }
// dump strings option
diff --git a/godot-mono-decomp/collection_examples/decompiled/TestCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/decompiled/TestCollectionExpressionInitializers.cs
new file mode 100644
index 00000000..8b6df54c
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/decompiled/TestCollectionExpressionInitializers.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ConsoleApp2;
+
+public static class TestCollectionExpressionInitializers
+{
+ public class TestClassBase
+ {
+ public readonly int parentIntField = 1;
+
+ public readonly List numbers;
+
+ public readonly string secondField;
+
+ public TestClassBase(List numbers)
+ {
+ this.numbers = numbers;
+ secondField = "second string";
+ }
+ }
+
+ public class ElemClass
+ {
+ public int elem;
+ }
+
+ public class TestClass1 : TestClassBase
+ {
+ public readonly int bar = 2;
+
+ public readonly List strings;
+
+ public static readonly List elems;
+
+ public readonly Dictionary dict;
+
+ public int randomField;
+
+ public string anotherRandomField;
+
+ public List fieldInitializedInConstructor;
+
+ // 'set' not in original order; was originally after `IntProp2`
+ public readonly HashSet set;
+
+ public int IntProp1 { get; set; }
+
+ public List ListProp1 { get; set; }
+
+ public int IntProp2 { get; set; }
+
+ public List StringListProp1 { get; set; }
+
+ public TestClass1()
+ {
+ // `strings` initialization
+ int num = 5;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = "one";
+ num2++;
+ span[num2] = "two";
+ num2++;
+ span[num2] = "three";
+ num2++;
+ span[num2] = "four";
+ span[num2 + 1] = "five";
+ strings = list;
+ // `dict` initialization
+ dict = new Dictionary
+ {
+ ["one"] = 1,
+ ["two"] = 2,
+ ["three"] = 3,
+ ["four"] = 4,
+ ["five"] = 5
+ };
+ // `IntProp1` initialization
+ IntProp1 = 1;
+ // `ListProp1` initialization
+ num2 = 5;
+ List list2 = new List(num2);
+ CollectionsMarshal.SetCount(list2, num2);
+ Span span2 = CollectionsMarshal.AsSpan(list2);
+ num = 0;
+ span2[num] = 4;
+ num++;
+ span2[num] = 5;
+ num++;
+ span2[num] = 6;
+ num++;
+ span2[num] = 7;
+ span2[num + 1] = 8;
+ ListProp1 = list2;
+ // `IntProp2` initialization
+ IntProp2 = 2;
+ // `set` initialization
+ // Even though `set` is not listed in the class members in the original order, `set` initialization still happens in original order (i.e. after `IntProp2`)
+ set = new HashSet { 1, 2, 3, 4, 5 };
+ // `StringListProp1` initialization
+ num = 5;
+ List list3 = new List(num);
+ CollectionsMarshal.SetCount(list3, num);
+ Span span3 = CollectionsMarshal.AsSpan(list3);
+ num2 = 0;
+ span3[num2] = "nine";
+ num2++;
+ span3[num2] = "ten";
+ num2++;
+ span3[num2] = "eleven";
+ num2++;
+ span3[num2] = "twelve";
+ span3[num2 + 1] = "thirteen";
+ StringListProp1 = list3;
+ // initialization of the List parameter passed to the base TestClassBase constructor, which uses it to initialize `TestClassBase.numbers`
+ num2 = 5;
+ List list4 = new List(num2);
+ CollectionsMarshal.SetCount(list4, num2);
+ Span span4 = CollectionsMarshal.AsSpan(list4);
+ num = 0;
+ span4[num] = 1;
+ num++;
+ span4[num] = 2;
+ num++;
+ span4[num] = 3;
+ num++;
+ span4[num] = 4;
+ span4[num + 1] = 5;
+ // call to base constructor, indicates that we've reached the end of the originally inlined field/property initializers
+ base._002Ector(list4);
+
+ // Stuff that was in the original constructor body
+ foo();
+ num = 5;
+ List list5 = new List(num);
+ CollectionsMarshal.SetCount(list5, num);
+ Span span5 = CollectionsMarshal.AsSpan(list5);
+ num2 = 0;
+ span5[num2] = "string_1";
+ num2++;
+ span5[num2] = "string_2";
+ num2++;
+ span5[num2] = "string_3";
+ num2++;
+ span5[num2] = "string_4";
+ num2++;
+ span5[num2] = "string_5";
+ fieldInitializedInConstructor = list5;
+ anotherRandomField = "another string";
+ }
+
+ private void foo()
+ {
+ randomField = 1;
+ }
+
+ static TestClass1()
+ {
+ int num = 5;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = new ElemClass
+ {
+ elem = 1
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 2
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 3
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 4
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 5
+ };
+ elems = list;
+ }
+ }
+}
diff --git a/godot-mono-decomp/collection_examples/decompiled/TestCollectionInitWithSpread.cs b/godot-mono-decomp/collection_examples/decompiled/TestCollectionInitWithSpread.cs
new file mode 100644
index 00000000..10bc0152
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/decompiled/TestCollectionInitWithSpread.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+
+namespace ConsoleApp2;
+
+public static class TestCollectionInitWithSpread
+{
+ public class TestStaticHashSetMemberSpreadInit
+ {
+ public static readonly HashSet strings;
+
+ static TestStaticHashSetMemberSpreadInit()
+ {
+ HashSet hashSet = new HashSet();
+ foreach (string item in stringListConst1)
+ {
+ hashSet.Add(item);
+ }
+ foreach (string item2 in stringListConst2)
+ {
+ hashSet.Add(item2);
+ }
+ foreach (string item3 in stringListConst3)
+ {
+ hashSet.Add(item3);
+ }
+ hashSet.Add("sixteen");
+ hashSet.Add("seventeen");
+ hashSet.Add("eighteen");
+ hashSet.Add("nineteen");
+ hashSet.Add("twenty");
+ strings = hashSet;
+ }
+ }
+
+ public class TestStaticListMemberSpreadInit
+ {
+ public static readonly List strings;
+
+ static TestStaticListMemberSpreadInit()
+ {
+ List list = new List();
+ list.AddRange(stringListConst1);
+ list.AddRange(stringListConst2);
+ list.AddRange(stringListConst3);
+ list.Add("sixteen");
+ list.Add("seventeen");
+ list.Add("eighteen");
+ list.Add("nineteen");
+ list.Add("twenty");
+ strings = list;
+ }
+ }
+
+ public class TestStaticReadOnlySetMemberSpreadInit
+ {
+ public static readonly IReadOnlySet strings;
+
+ static TestStaticReadOnlySetMemberSpreadInit()
+ {
+ List list = new List();
+ list.AddRange(stringListConst1);
+ list.AddRange(stringListConst2);
+ list.AddRange(stringListConst3);
+ list.Add("sixteen");
+ list.Add("seventeen");
+ list.Add("eighteen");
+ list.Add("nineteen");
+ list.Add("twenty");
+ strings = new HashSet(new _003C_003Ez__ReadOnlyList(list));
+ }
+ }
+
+ public class TestStaticReadOnlyListMemberSpreadInit
+ {
+ public static readonly IReadOnlyList strings;
+
+ static TestStaticReadOnlyListMemberSpreadInit()
+ {
+ List list = new List();
+ list.AddRange(stringListConst1);
+ list.AddRange(stringListConst2);
+ list.AddRange(stringListConst3);
+ list.Add("sixteen");
+ list.Add("seventeen");
+ list.Add("eighteen");
+ list.Add("nineteen");
+ list.Add("twenty");
+ strings = new _003C_003Ez__ReadOnlyList(list);
+ }
+ }
+
+ private static IEnumerable stringListConst1 => new _003C_003Ez__ReadOnlyArray(new string[5] { "one", "two", "three", "four", "five" });
+
+ private static IEnumerable stringListConst2 => new _003C_003Ez__ReadOnlyArray(new string[5] { "six", "seven", "eight", "nine", "ten" });
+
+ private static IEnumerable stringListConst3 => new _003C_003Ez__ReadOnlyArray(new string[5] { "eleven", "twelve", "thirteen", "fourteen", "fifteen" });
+}
diff --git a/godot-mono-decomp/collection_examples/decompiled/TestCtorBoundaryCoverage.cs b/godot-mono-decomp/collection_examples/decompiled/TestCtorBoundaryCoverage.cs
new file mode 100644
index 00000000..d47fc8ba
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/decompiled/TestCtorBoundaryCoverage.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ConsoleApp2;
+
+public static class TestCtorBoundaryCoverage
+{
+ public class BoundaryBase
+ {
+ public readonly List baseNumbers;
+
+ public BoundaryBase(List baseNumbers)
+ {
+ this.baseNumbers = baseNumbers;
+ }
+ }
+
+ public class TailBoundaryDerived : BoundaryBase
+ {
+ public readonly List names;
+
+ public TailBoundaryDerived()
+ {
+ int num = 3;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = "alpha";
+ num2++;
+ span[num2] = "beta";
+ span[num2 + 1] = "gamma";
+ names = list;
+ num2 = 3;
+ List list2 = new List(num2);
+ CollectionsMarshal.SetCount(list2, num2);
+ Span span2 = CollectionsMarshal.AsSpan(list2);
+ num = 0;
+ span2[num] = 1;
+ num++;
+ span2[num] = 2;
+ span2[num + 1] = 3;
+ base._002Ector(list2);
+ }
+ }
+
+ public class TransitionBoundaryDerived : BoundaryBase
+ {
+ public readonly List values;
+
+ public string data;
+
+ public TransitionBoundaryDerived()
+ {
+ int num = 3;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = 10;
+ num2++;
+ span[num2] = 20;
+ span[num2 + 1] = 30;
+ values = list;
+ num2 = 3;
+ List list2 = new List(num2);
+ CollectionsMarshal.SetCount(list2, num2);
+ Span span2 = CollectionsMarshal.AsSpan(list2);
+ num = 0;
+ span2[num] = 4;
+ num++;
+ span2[num] = 5;
+ span2[num + 1] = 6;
+ base._002Ector(list2);
+ data = ComputeData();
+ }
+
+ private static string ComputeData()
+ {
+ return "from-function";
+ }
+ }
+}
diff --git a/godot-mono-decomp/collection_examples/decompiled/TestFuncInitializer.cs b/godot-mono-decomp/collection_examples/decompiled/TestFuncInitializer.cs
new file mode 100644
index 00000000..bde3fdb3
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/decompiled/TestFuncInitializer.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ConsoleApp2;
+
+public static class TestFuncInitializer
+{
+ public class TestClass1
+ {
+ public readonly List strings;
+
+ private Func filter;
+
+ public TestClass1()
+ {
+ int num = 5;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = "one";
+ num2++;
+ span[num2] = "two";
+ num2++;
+ span[num2] = "three";
+ num2++;
+ span[num2] = "four";
+ span[num2 + 1] = "five";
+ strings = list;
+ filter = (string s) => s.Length > 3;
+ base._002Ector();
+ }
+ }
+}
diff --git a/godot-mono-decomp/collection_examples/decompiled/TestInterleavedStaticCollectionInit.cs b/godot-mono-decomp/collection_examples/decompiled/TestInterleavedStaticCollectionInit.cs
new file mode 100644
index 00000000..1b569820
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/decompiled/TestInterleavedStaticCollectionInit.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ConsoleApp2;
+
+public static class TestInterleavedStaticCollectionInit
+{
+ public class ElemClass
+ {
+ public int elem;
+ }
+
+ public class InterlevedTestClass
+ {
+ public readonly int bar = 2;
+
+ public readonly List strings;
+
+ public List oOOIntListField;
+
+ public static readonly List staticElemClassListField;
+
+ public static readonly List staticStringListField;
+
+ public List ListProp1 { get; set; }
+
+ public List StringListProp1 { get; set; }
+
+ public static List StaticIntProp1 { get; }
+
+ public InterlevedTestClass()
+ {
+ int num = 5;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = "one";
+ num2++;
+ span[num2] = "two";
+ num2++;
+ span[num2] = "three";
+ num2++;
+ span[num2] = "four";
+ span[num2 + 1] = "five";
+ strings = list;
+ num2 = 5;
+ List list2 = new List(num2);
+ CollectionsMarshal.SetCount(list2, num2);
+ Span span2 = CollectionsMarshal.AsSpan(list2);
+ num = 0;
+ span2[num] = 4;
+ num++;
+ span2[num] = 5;
+ num++;
+ span2[num] = 6;
+ num++;
+ span2[num] = 7;
+ span2[num + 1] = 8;
+ ListProp1 = list2;
+ num = 5;
+ List list3 = new List(num);
+ CollectionsMarshal.SetCount(list3, num);
+ Span span3 = CollectionsMarshal.AsSpan(list3);
+ num2 = 0;
+ span3[num2] = 1;
+ num2++;
+ span3[num2] = 2;
+ num2++;
+ span3[num2] = 3;
+ num2++;
+ span3[num2] = 4;
+ span3[num2 + 1] = 5;
+ oOOIntListField = list3;
+ num2 = 5;
+ List list4 = new List(num2);
+ CollectionsMarshal.SetCount(list4, num2);
+ Span span4 = CollectionsMarshal.AsSpan(list4);
+ num = 0;
+ span4[num] = "nine";
+ num++;
+ span4[num] = "ten";
+ num++;
+ span4[num] = "eleven";
+ num++;
+ span4[num] = "twelve";
+ span4[num + 1] = "thirteen";
+ StringListProp1 = list4;
+ base._002Ector();
+ }
+
+ static InterlevedTestClass()
+ {
+ int num = 5;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ span[num2] = new ElemClass
+ {
+ elem = 1
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 2
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 3
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 4
+ };
+ num2++;
+ span[num2] = new ElemClass
+ {
+ elem = 5
+ };
+ staticElemClassListField = list;
+ num2 = 5;
+ List list2 = new List(num2);
+ CollectionsMarshal.SetCount(list2, num2);
+ Span span2 = CollectionsMarshal.AsSpan(list2);
+ num = 0;
+ span2[num] = 1;
+ num++;
+ span2[num] = 2;
+ num++;
+ span2[num] = 3;
+ num++;
+ span2[num] = 4;
+ num++;
+ span2[num] = 5;
+ StaticIntProp1 = list2;
+ num = 5;
+ List list3 = new List(num);
+ CollectionsMarshal.SetCount(list3, num);
+ Span span3 = CollectionsMarshal.AsSpan(list3);
+ num2 = 0;
+ span3[num2] = "one";
+ num2++;
+ span3[num2] = "two";
+ num2++;
+ span3[num2] = "three";
+ num2++;
+ span3[num2] = "four";
+ num2++;
+ span3[num2] = "five";
+ staticStringListField = list3;
+ }
+ }
+}
diff --git a/godot-mono-decomp/collection_examples/decompiled/TestNestedCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/decompiled/TestNestedCollectionExpressionInitializers.cs
new file mode 100644
index 00000000..d197c3d0
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/decompiled/TestNestedCollectionExpressionInitializers.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ConsoleApp2;
+
+public static class TestNestedCollectionExpressionInitializers
+{
+ public class ElemClassWithCollection
+ {
+ public int intField;
+
+ public List intListField;
+
+ public int outOfOrderField;
+
+ public string StringProp { get; set; }
+ }
+
+ public class ParentClassWithCollection
+ {
+ public readonly int bar = 2;
+
+ public List elems;
+
+ public ParentClassWithCollection()
+ {
+ int num = 5;
+ List list = new List(num);
+ CollectionsMarshal.SetCount(list, num);
+ Span span = CollectionsMarshal.AsSpan(list);
+ int num2 = 0;
+ ref ElemClassWithCollection reference = ref span[num2];
+ ElemClassWithCollection obj = new ElemClassWithCollection
+ {
+ intField = 1
+ };
+ int num3 = 5;
+ List list2 = new List(num3);
+ CollectionsMarshal.SetCount(list2, num3);
+ Span span2 = CollectionsMarshal.AsSpan(list2);
+ int num4 = 0;
+ span2[num4] = 1;
+ num4++;
+ span2[num4] = 2;
+ num4++;
+ span2[num4] = 3;
+ num4++;
+ span2[num4] = 4;
+ span2[num4 + 1] = 5;
+ obj.intListField = list2;
+ obj.StringProp = "string_1";
+ obj.outOfOrderField = 2;
+ reference = obj;
+ num2++;
+ ref ElemClassWithCollection reference2 = ref span[num2];
+ ElemClassWithCollection obj2 = new ElemClassWithCollection
+ {
+ intField = 2
+ };
+ num4 = 5;
+ List list3 = new List(num4);
+ CollectionsMarshal.SetCount(list3, num4);
+ Span span3 = CollectionsMarshal.AsSpan(list3);
+ num3 = 0;
+ span3[num3] = 6;
+ num3++;
+ span3[num3] = 7;
+ num3++;
+ span3[num3] = 8;
+ num3++;
+ span3[num3] = 9;
+ span3[num3 + 1] = 10;
+ obj2.intListField = list3;
+ obj2.StringProp = "string_2";
+ obj2.outOfOrderField = 3;
+ reference2 = obj2;
+ num2++;
+ ref ElemClassWithCollection reference3 = ref span[num2];
+ ElemClassWithCollection obj3 = new ElemClassWithCollection
+ {
+ intField = 3
+ };
+ num3 = 5;
+ List list4 = new List(num3);
+ CollectionsMarshal.SetCount(list4, num3);
+ Span span4 = CollectionsMarshal.AsSpan(list4);
+ num4 = 0;
+ span4[num4] = 11;
+ num4++;
+ span4[num4] = 12;
+ num4++;
+ span4[num4] = 13;
+ num4++;
+ span4[num4] = 14;
+ span4[num4 + 1] = 15;
+ obj3.intListField = list4;
+ obj3.StringProp = "string_3";
+ obj3.outOfOrderField = 4;
+ reference3 = obj3;
+ num2++;
+ ref ElemClassWithCollection reference4 = ref span[num2];
+ ElemClassWithCollection obj4 = new ElemClassWithCollection
+ {
+ intField = 4
+ };
+ num4 = 5;
+ List list5 = new List(num4);
+ CollectionsMarshal.SetCount(list5, num4);
+ Span span5 = CollectionsMarshal.AsSpan(list5);
+ num3 = 0;
+ span5[num3] = 16;
+ num3++;
+ span5[num3] = 17;
+ num3++;
+ span5[num3] = 18;
+ num3++;
+ span5[num3] = 19;
+ span5[num3 + 1] = 20;
+ obj4.intListField = list5;
+ obj4.StringProp = "string_4";
+ obj4.outOfOrderField = 5;
+ reference4 = obj4;
+ ref ElemClassWithCollection reference5 = ref span[num2 + 1];
+ ElemClassWithCollection obj5 = new ElemClassWithCollection
+ {
+ intField = 5
+ };
+ num3 = 5;
+ List list6 = new List(num3);
+ CollectionsMarshal.SetCount(list6, num3);
+ Span span6 = CollectionsMarshal.AsSpan(list6);
+ num4 = 0;
+ span6[num4] = 21;
+ num4++;
+ span6[num4] = 22;
+ num4++;
+ span6[num4] = 23;
+ num4++;
+ span6[num4] = 24;
+ span6[num4 + 1] = 25;
+ obj5.intListField = list6;
+ obj5.StringProp = "string_5";
+ obj5.outOfOrderField = 6;
+ reference5 = obj5;
+ elems = list;
+ base._002Ector();
+ }
+ }
+}
diff --git a/godot-mono-decomp/collection_examples/original/TestCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/original/TestCollectionExpressionInitializers.cs
new file mode 100644
index 00000000..fada2d7b
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/original/TestCollectionExpressionInitializers.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+
+namespace ConsoleApp2;
+
+public static class TestCollectionExpressionInitializers
+{
+
+ public class TestClassBase
+ {
+
+ public readonly int parentIntField = 1;
+
+ public readonly List numbers;
+
+ public readonly string secondField;
+
+ public TestClassBase(List numbers)
+ {
+ this.numbers = numbers;
+ this.secondField = "second string";
+ }
+
+ }
+
+ public class ElemClass
+ {
+ public int elem;
+ }
+ public class TestClass1 : TestClassBase
+ {
+
+ public readonly int bar = 2;
+
+ public readonly List strings = ["one", "two", "three", "four", "five"];
+
+ public static readonly List elems = [new ElemClass { elem = 1 }, new ElemClass { elem = 2 }, new ElemClass { elem = 3 }, new ElemClass { elem = 4 }, new ElemClass { elem = 5 }];
+
+ public readonly Dictionary dict = new Dictionary { ["one"] = 1, ["two"] = 2, ["three"] = 3, ["four"] = 4, ["five"] = 5 };
+
+ public int randomField;
+
+ public string anotherRandomField;
+
+ public List fieldInitializedInConstructor;
+
+ public int IntProp1 { get; set; } = 1;
+
+ public List ListProp1 { get; set; } = [4,5,6,7,8];
+
+ public int IntProp2 { get; set; } = 2;
+
+ // field initialized after properties
+ public readonly HashSet set = [1, 2, 3, 4, 5];
+
+ public List StringListProp1 { get; set; } = ["nine", "ten", "eleven", "twelve", "thirteen"];
+
+ public TestClass1() : base([1, 2, 3, 4, 5])
+ {
+ foo();
+ fieldInitializedInConstructor = [
+ "string_1",
+ "string_2",
+ "string_3",
+ "string_4",
+ "string_5"
+ ];
+ anotherRandomField = "another string";
+
+ }
+
+ void foo(){
+ randomField = 1;
+
+ }
+ }
+}
diff --git a/godot-mono-decomp/collection_examples/original/TestCollectionInitWithSpread.cs b/godot-mono-decomp/collection_examples/original/TestCollectionInitWithSpread.cs
new file mode 100644
index 00000000..a6d062b6
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/original/TestCollectionInitWithSpread.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+
+namespace ConsoleApp2;
+
+public static class TestCollectionInitWithSpread
+{
+
+ private static IEnumerable stringListConst1 => [ "one", "two", "three", "four", "five" ];
+
+ private static IEnumerable stringListConst2 => ["six", "seven", "eight", "nine", "ten" ];
+
+ private static IEnumerable stringListConst3 => ["eleven", "twelve", "thirteen", "fourteen", "fifteen"];
+
+
+ public class TestStaticHashSetMemberSpreadInit
+ {
+ public static readonly HashSet strings = [..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"];
+
+ }
+
+
+ public class TestStaticListMemberSpreadInit
+ {
+ public static readonly List strings = [..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"];
+
+ }
+
+
+ public class TestStaticReadOnlySetMemberSpreadInit
+ {
+
+ public static readonly IReadOnlySet strings = new HashSet([..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]);
+
+ }
+ public class TestStaticReadOnlyListMemberSpreadInit
+ {
+ public static readonly IReadOnlyList strings = [..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"];
+
+ }
+
+}
diff --git a/godot-mono-decomp/collection_examples/original/TestCtorBoundaryCoverage.cs b/godot-mono-decomp/collection_examples/original/TestCtorBoundaryCoverage.cs
new file mode 100644
index 00000000..6c15b0f8
--- /dev/null
+++ b/godot-mono-decomp/collection_examples/original/TestCtorBoundaryCoverage.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+
+namespace ConsoleApp2;
+
+public static class TestCtorBoundaryCoverage
+{
+ public class BoundaryBase
+ {
+ public readonly List baseNumbers;
+
+ public BoundaryBase(List baseNumbers)
+ {
+ this.baseNumbers = baseNumbers;
+ }
+ }
+
+ public class TailBoundaryDerived : BoundaryBase
+ {
+ public readonly List