Skip to content

Fix #3954: name the entry point of a top-level program 'Main' - #3955

Merged
siegfriedpammer merged 1 commit into
masterfrom
fix-3954-entrypoint-name
Aug 5, 2026
Merged

Fix #3954: name the entry point of a top-level program 'Main'#3955
siegfriedpammer merged 1 commit into
masterfrom
fix-3954-entrypoint-name

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Aug 5, 2026

Copy link
Copy Markdown
Member

Fixes #3954.

Roslyn compiles top-level statements into a synthesized Program class whose entry point is called <Main>$ — a name that cannot be declared in C#. The decompiled output kept it verbatim (escaped to _003CMain_003E_0024 when exporting a project), and since C# accepts only a method called Main as an entry point, the exported executable did not compile:

CSC : error CS5001: Program does not contain a static 'Main' method suitable for an entry point

Change: CSharpDecompiler.DoDecompile(IMethod) gives that method the name Main, next to the existing explicit-interface-implementation rename. The new IsEntryPoint helper resolves the entry point from CorHeader.EntryPointTokenOrRelativeVirtualAddress via MetadataTokenHelpers.EntityHandleOrNil and requires a MethodDefinition handle — per ECMA-335 II.25.3.3 that field is a MethodDef or a File token, the latter when the entry point lives in another module of a multi-module assembly. Per the decision recorded in #829 top-level statements are not reconstructed — this is just the level of support the output needs in order to compile.

Async top-level programs need the name in a different place. They compile to <Main>$ holding the statements plus a <Main> wrapper that only awaits it. The wrapper carries the .entrypoint marker, but it is hidden from the output (MemberIsHidden via AsyncAwaitDecompiler.IsCompilerGeneratedMainMethod), so renaming strictly the method marked .entrypoint would leave every await-using program emitting <Main>$ and still not compiling. IsEntryPoint therefore also maps the hidden wrapper to the method it awaits, guarded by settings.AsyncAwait — the same condition under which the wrapper is hidden — so the two can never both end up named Main. A classic static async Task Main is unaffected: its wrapper is hidden and its own name is already speakable.

Not a regression: the same <Main>$ output comes out of release/10.1, v9.1, and the NuGet ilspycmd 8.2.0 (2023) and 7.2.1 (2022) — the case has simply never been handled since top-level statements arrived in C# 9.

Tests: Ugly/TopLevelProgram and Ugly/TopLevelProgramAsync — the input is valid C#, and the .Expected.cs records the output we intentionally do not reproduce faithfully, which is what the Ugly suite is for. They use Run rather than RunForLibrary, because top-level statements require an executable and the entry-point token has to be set. All 8 configurations are red before the change (emitting _003CMain_003E_0024) and green after; full decompiler suite 3340 tests, 0 failures.

Beyond the fixtures, dotnet new console and an await-using variant were decompiled with the rebuilt ilspycmd: both exported projects now compile and run, where the first previously failed with CS5001.

🤖 Generated with Claude Code

Roslyn compiles top-level statements into a synthesized Program class
whose entry point is called '<Main>$', a name that cannot be declared
in C#. Decompiled output kept it verbatim (escaped to
_003CMain_003E_0024 when exporting a project), and since C# accepts
only a method called 'Main' as an entry point, the exported executable
did not compile (CS5001).

Give that method the name 'Main'. Per the decision recorded in #829 we
do not reconstruct top-level statements, so this is the level of
support the output needs to compile.

An async top-level program needs the name in a different place: it
compiles to '<Main>$' holding the statements plus a '<Main>' entry
point that only awaits it. That wrapper carries the .entrypoint marker
but is hidden from the output, so the name goes to the method it
awaits instead - unless AsyncAwait is off, when the wrapper is
emitted and keeps the name itself.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the fix-3954-entrypoint-name branch from 03fd48e to a18654a Compare August 5, 2026 10:12
@siegfriedpammer
siegfriedpammer merged commit 17e6649 into master Aug 5, 2026
15 checks passed
@siegfriedpammer
siegfriedpammer deleted the fix-3954-entrypoint-name branch August 5, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for top-level statements

1 participant