Skip to content

Improve primary-constructor decompilation, collection expressions, and project export - #4049

Open
sonyps5201314 wants to merge 9 commits into
icsharpcode:masterfrom
sonyps5201314:hybrid/copilot-export-fixes
Open

Improve primary-constructor decompilation, collection expressions, and project export#4049
sonyps5201314 wants to merge 9 commits into
icsharpcode:masterfrom
sonyps5201314:hybrid/copilot-export-fixes

Conversation

@sonyps5201314

@sonyps5201314 sonyps5201314 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Supersedes #3598.

Problem

Recent Visual Studio, Copilot, Semantic Kernel, and OpenAI assemblies expose several compiler-generated patterns that are not fully reconstructed by the current decompiler pipeline. The resulting projects may contain incomplete primary constructors, .ctor/_002Ector calls, synthesized record members, raw backing fields, lowered collection helpers, invalid unresolved value-type call targets, inconsistent project references, or incomplete PDB source mapping.

Solution

Primary constructors and constructor initializers

  • Improve detection for classes, structs, records, inherited records, constructor chaining, and complex parameter-dependent member initializers.
  • Preserve base arguments and evaluation order.
  • Recover unresolved base and this constructor calls instead of emitting .ctor or _002Ector.
  • Handle late calls, unresolved argument types, collection temporaries, and spilled in arguments.

C# 12 collection expressions

  • Add collection-expression AST and output support.
  • Recover empty, fixed-element, spread, and multiple-spread expressions for arrays, lists, interfaces, spans, read-only spans, CollectionBuilder types, inline arrays, and CollectionsMarshal lowerings.
  • Support member, return, argument, nested, and constructor-initializer contexts.
  • Avoid unsafe multidimensional and recursive/self-referential rewrites.
  • Add a Use collection expressions option and run the transform only for C# 12 or later.
  • Exclude compiler-generated collection helpers from improved type-inference candidates.

Collection-expression support is included because modern primary constructors frequently contain collection construction in parameter-dependent initializers and base-constructor arguments.

Records, properties, and unresolved call targets

  • Improve inherited-record and synthesized-member detection.
  • Remove compiler-generated PrintMembers, Equals, GetHashCode, Deconstruct, copy constructors, and backing-field artifacts when their compiler-generated shapes are proven.
  • Recover classic auto-properties and C# 14 field-backed properties with unresolved types.
  • Preserve managed-reference receivers for synthetic Ref -> Unknown conversions and avoid invalid (Type)(ref value) or pointer-based call targets.

Project export, assembly resolution, and PDBs

  • Improve SDK target-framework and project-reference generation.
  • Resolve .NETStandard dependencies through DotNetCorePathFinder and select a coherent closest runtime.
  • Preserve explicit references while distinguishing resolution candidates from implicit framework references.
  • Enable nullable annotations, improve multi-project reporting, and improve source-document matching including the global namespace.

Real-world validation

All versions used the same physical DLLs, flattened reference directory, and decompiler settings.

  • Private baseline: fe70c814858488a4f7b34cfbd9747d5c5a66e780
  • Benchmark upstream baseline: 24ec3327e7fa9fcc56159bbbd7cfafdfc3e5ee89
  • Current PR base: e930120ade794c5714e12cc4b9d09e5c8f2f9d98
  • PR head: 793bced7542ac4bce001473fe285b8749926f94e

The upstream commits between the benchmark baseline and current base modify bundle, resource, and LightJson validation. This branch was rebased onto the current base without conflicts.

Lower is better except for recovered collection-expression counts. Syntax counts are Roslyn parse diagnostics.

Dataset A: vs2026

Assemblies: Microsoft.VisualStudio.Extensibility.Copilot, Microsoft.VisualStudio.Extensibility.Copilot.Configuration, Microsoft.VisualStudio.LanguageServices.ExternalAccess.Copilot, Microsoft.VisualStudio.Completions, Microsoft.VisualStudio.Completions.Package, Microsoft.VisualStudio.Copilot, Microsoft.VisualStudio.Copilot.Common, Microsoft.VisualStudio.Copilot.Core, Microsoft.VisualStudio.Copilot.Roslyn, Microsoft.VisualStudio.Copilot.UI, Microsoft.VisualStudio.Copilot.UI.Core, and Microsoft.VisualStudio.Copilot.Vsix.

Metric Private Upstream This PR PR vs upstream
Generated C# files 3,402 3,399 3,398 -1
.ctor / _002Ector artifacts 24 20 0 -20
Raw backing fields 50 0 0 0
Unknown result type comments 248 263 265 +2
[CompilerGenerated] markers 396 1,247 51 -1,196
Detected synthesized record methods 79 29 0 -29
Raw )(ref ...) patterns 29 24 2 -22
Files with syntax errors 257 9 1 -8
Total syntax errors 29,241 24 2 -22
Recovered empty collection expressions 0 0 727 +727
Compiler-helper files 22 19 19 0

The two remaining )(ref ...) occurrences are valid delegate invocations. The two remaining syntax diagnostics are in Document.cs and also occur upstream.

Dataset B: Copilot.Conversations.Service_vs2026

Assemblies: Microsoft.SemanticKernel, Microsoft.SemanticKernel.Abstractions, Microsoft.SemanticKernel.Connectors.AzureOpenAI, Microsoft.SemanticKernel.Connectors.Google, Microsoft.SemanticKernel.Connectors.OpenAI, Microsoft.SemanticKernel.Core, Microsoft.VisualStudio.Copilot, Microsoft.VisualStudio.Copilot.Core, Microsoft.VisualStudio.Copilot.Service, and OpenAI.

Metric Private Upstream This PR PR vs upstream
Generated C# files 5,174 5,174 5,171 -3
.ctor / _002Ector artifacts 36 30 0 -30
Raw backing fields 26 0 0 0
Unknown result type comments 261 116 115 -1
[CompilerGenerated] markers 420 1,308 40 -1,268
Detected synthesized record members 179 108 0 -108
Raw )(ref ...) patterns 105 35 2 -33
Files with syntax errors 322 8 0 -8
Total syntax errors 46,690 33 0 -33
Recovered empty collection expressions 0 0 918 +918
Compiler-helper files 19 19 16 -3

Reproduction material

The benchmark-only material is stored on a separate branch so it does not affect this PR diff:

No Microsoft binaries, complete decompiled source trees, user names, or user-profile extension entries are included.

Reproduction

  1. Build upstream and this PR in Release|Any CPU.
  2. Replace placeholders in the sanitized XML.
  3. Use the same target DLLs and reference directory for both builds.
  4. Export every selected assembly as an SDK-style project with nested namespace directories.
  5. Run the supplied scanner and parse the generated files with Roslyn Preview.

Tests

  • Full Release solution build: 16 projects succeeded, 0 failed.
  • Complete unit-test run: 5,706 total, 5,684 passed, 22 pre-existing ignored/skipped, 0 failed.
  • ICSharpCode.Decompiler.Tests: 4,361 total, 0 failed.
  • Mono.Cecil round-trip inner suite: 218/218 passed.
  • Post-rewrite focused regression selection: 147/147 passed.

Review focus

  • primary-constructor and synthesized-record heuristics;
  • collection-expression lowering and C# 12 gating;
  • constructor initializer evaluation order;
  • unresolved value-type call targets;
  • resolution candidates versus implicit framework references.

The commits are separated by feature and can be split into smaller follow-up PRs if preferred.

  • At least one test covering the code changed


namespace ICSharpCode.Decompiler.CSharp.Transforms
{
public sealed class IntroduceCollectionExpressions : DepthFirstAstVisitor, IAstTransform

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collection expressions should be created in ExpressionBuilder not as a separate transform.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I will not carry this separate transform into the split PRs. Collection expressions are paused; I will open a design issue first and propose creating them in ExpressionBuilder.

return true;
}

void ConvertConstructorInitializerCollections(AstNode invocation)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in the other comment, if collection expressions are handled in the ExpressionBuilder, then this is no longer necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This constructor-specific collection reconstruction is excluded from the split work. If the feature resumes after the design discussion, collection-expression nodes will be created in ExpressionBuilder.

@siegfriedpammer

Copy link
Copy Markdown
Member

Thank you for your pull request. Unfortunately, I don't think we can merge it as one single PR.

Also it would have been a good idea to start an architecture/design session before starting the implementation of a feature as massive and complex as collection expressions. There are a lot of different code patterns that are produced when collection expressions are involved.

As a first step, please split the "primary constructor decompilation" feature out into a separate PR. I will review it separately.

Also let's have a discussion on how to implement collection expressions in an issue first. Since you are using AI, would love to hear about your approach. What is the prompt you use? What are the inputs? Do you write test cases first? How do you decide whether the design proposal by the LLM uses the correct layer?

You see, when I implement a language feature using an LLM I definitely don't let the LLM have the final say on the architecture. I give strict instructions, where the transformation should happen and usually I have to remind the LLM of special cases and edge cases it misses. Using an LLM to implement features and open pull requests does not free you from the responsibility of guiding the LLM. And in order to guide the LLM correctly, you need to actually know the codebase, architecture and the reasons for the design.

I have already commented two hints on the architecture/implementation of collection expresssions, but this is just the tip of the iceberg. Please, let's have a long and detailed discussion first.

@sonyps5201314

Copy link
Copy Markdown
Contributor Author

I think since you have these specific design requirements, it would be better to place these constraints in an AGENT.MD file or a skill directory (rather than in a markdown file that only Claude recognizes). This way, regardless of what AI tools other contributors use, they will automatically adhere to these design conventions—such as requiring unit tests or knowing exactly which layer a modification should be made in.

Also, my PR actually does include unit tests; you can find the details by checking the individual commit history. Additionally, I specifically had the AI generate all possible syntaxes for collection expressions to ensure comprehensive compatibility adaptation.

return null;
}

bool IsTrivialRecordAutoProperty(PropertyDeclaration declaration, IProperty property, IField field)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in what sense are auto properties different in records?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not fundamentally different. This special case tried to ignore outside backing-field references from synthesized record members before those members were removed. That couples two transforms incorrectly, so I am dropping it rather than defending the distinction.

string StrongNameKeyFile { get; }
}

internal interface INullableProjectInfoProvider

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WTF?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; this ad-hoc interface is the wrong abstraction. It existed only to pass the nullable setting without changing the public IProjectInfoProvider contract. I am excluding it from the split PRs and will revisit project export separately through the existing settings/options flow.

Console.WriteLine(value);
}

public void TestUnresolvedStructMemberCalls()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test + fix should be a separate PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the test and fix are now isolated in #4053.


if (!analyzer.Analyze(members))
{
foreach (var constructorDeclaration in members.OfType<ConstructorDeclaration>())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intended as a fallback that lifts a base/this constructor call when field-initializer analysis fails. It is too broad and unrelated at this location, so it is excluded from the primary-constructor split. I will only revisit it as a separate narrowly tested fix.

{
IType targetTypeHint = constrainedTo ?? memberDeclaringType;
if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType, constrainedTo) == StackType.Ref)
if (target is Conv {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not exactly sure what you are trying to do, but there is a UnwrapConv extension method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. The isolated fix in #4053 now uses target.UnwrapConv(ConversionKind.Invalid), guarded specifically to the synthetic Invalid Ref-to-Unknown conversion on an unresolved value-type receiver. The regression test was run red before the implementation.

@siegfriedpammer

Copy link
Copy Markdown
Member

I think since you have these specific design requirements, it would be better to place these constraints in an AGENT.MD file or a skill directory (rather than in a markdown file that only Claude recognizes).

Yes, at some point we might add skills and instructions to automate parts of the language feature development process. We have already added a CLAUDE.md to steer LLMs towards correct unit testing of decompiler changes.

However, we have repeatedly seen even the most advanced "frontier models" of US providers like Fable 5 stumble into the wrong direction architecture-wise and produce unmaintainable code that only fixes one test case, but does not match the overall architecture/design of the decompiler. That's why careful understanding, thinking and planning before prompting is the responsibility of the human engineer. This is something you cannot just add into CLAUDE.md, since it is not an instruction for the LLM: it's the responsibility of the human engineer.

We might add a prompt like:

When a user asks you to implement a C# language feature, refuse to do so, unless they pointed you at the relevant speclets in the csharplang github repository or the csharpstandard repository. Always write a broad test suite first. Always first ask the user about general architectural decisions like ILAst vs C# AST and transformation pipeline order. Never decide on your own.

Fable is great for test case generation though, saves a lot of typing and of course UI/UX development is dead, LLMs are great at that.

you can find the details by checking the individual commit history

Yeah, that exactly IS the problem: In this PR you add at least 3 different decompiler features and hide maybe 2-3 more smaller bug fixes inbetween, that should each be their own PR. I repeat: please split your PR into smaller pieces.

Please start with anything other than "collection expressions" since that will need more thinking and planning (on my part).

Thank you!

@siegfriedpammer

Copy link
Copy Markdown
Member

rather than in a markdown file that only Claude recognizes

Currently, we are primarily using Claude, in my experience from work Github Copilot also honors Claude.md and it became the defacto industry standard.

Moving to AGENTS.MD is on our list for the future.

@sonyps5201314

Copy link
Copy Markdown
Contributor Author

rather than in a markdown file that only Claude recognizes

Currently, we are primarily using Claude, in my experience from work Github Copilot also honors Claude.md and it became the defacto industry standard.

Moving to AGENTS.MD is on our list for the future.

I am currently using codex along with the frontier gpt-5.6-sol model. By default, it doesn't support CLAUDE.md unless the user manually adds project_doc_fallback_filenames = ["CLAUDE.md"] into the config.toml file (I actually just figured this out).

@sonyps5201314

Copy link
Copy Markdown
Contributor Author

Thank you for the detailed feedback. You are right that this PR combines too many independent changes, and my previous reply was too defensive. Sorry about that.

I will keep this branch/PR available as a reference, but I will not treat it as a mergeable unit. I am splitting the work from the current upstream master as follows:

  1. A standalone primary-constructor decompilation PR, without collection expressions, project export/resolver changes, backing-field fixes, or the unresolved value-type call fix.
  2. A standalone PR for the unresolved value-type receiver case covered by the Issue3729 extension. I will rework it around the existing UnwrapConv helper and show the failing test before the fix.
  3. Separate PRs for any remaining record/auto-property and project export/resolver/PDB fixes, only where they can be justified and tested independently.
  4. No collection-expression PR for now. I will open a design issue first, reference the relevant C# specification/speclet, describe the emitted IL patterns, and discuss implementing the reconstruction in ExpressionBuilder rather than as a separate C# AST transform.

For transparency about the AI-assisted process: there was not one carefully designed initial prompt. The prompts evolved from real decompilation failures, emitted IL from multiple Roslyn versions, the existing ILSpy source/tests, and two large real-world assembly sets. Tests were added during implementation, but I did not consistently write the broad test suite first, and I did not start by agreeing on the ILAst/ExpressionBuilder/C# AST layer or by supplying the collection-expression speclet. I allowed the model to infer the layer from the existing pipeline and did not challenge that choice enough. That was the architectural mistake here, and it is my responsibility as the contributor.

For the split work I will follow the repository guidance: spec/design first for language features, red test before implementation, strict pattern matching, full relevant test matrices, and no unrelated fixes hidden in a feature PR.

I will post links to the split PRs here as they are ready. Thank you again for taking the time to point out the architectural and reviewability issues.

@sonyps5201314

Copy link
Copy Markdown
Contributor Author

The first two split PRs are ready:

Both were created from upstream master, with the regression test run red before the implementation and the relevant fixture run green afterwards. The original hybrid/copilot-export-fixes branch remains unchanged as requested and #4049 can remain as the comparison/reference PR.

I will not submit the collection-expression implementation as another split PR. The next step for that work will be a design issue first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants