Skip to content

[Version 9.0] Feature support for top level statements - #1454

Draft
BillWagner wants to merge 3 commits into
draft-v9from
v9-toplevel-statements
Draft

[Version 9.0] Feature support for top level statements#1454
BillWagner wants to merge 3 commits into
draft-v9from
v9-toplevel-statements

Conversation

@BillWagner

Copy link
Copy Markdown
Member

This PR contains the work for Top-level statements in C# 9.

The commits from #980 were squashed to the first commit on this branch.

The ANTLR grammar is failing. I admit I'm not sure why. There are no grammar changes in this PR.

@BillWagner
BillWagner force-pushed the v9-toplevel-statements branch from d872f61 to 07946a2 Compare November 6, 2025 20:05
@RexJaeschke RexJaeschke added this to the C# 9.0 milestone Nov 7, 2025
@RexJaeschke RexJaeschke added the Review: pending Proposal is available for review label Nov 7, 2025
@RexJaeschke

RexJaeschke commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

@BillWagner There is one grammar change: the extension of compilation_unit to allow statement_list* before namespace_member_declaration*.

@BillWagner BillWagner changed the title Feature support for top level statements [Version 9.0] Feature support for top level statements Nov 7, 2025
@RexJaeschke RexJaeschke added the type: feature This issue describes a new feature label Nov 11, 2025
@Nigel-Ecma Nigel-Ecma self-assigned this Nov 19, 2025
Comment on lines +774 to +775
- The scope of a name defined by an *extern_alias_directive* ([§14.4](namespaces.md#144-extern-alias-directives)) extends over the *using_directive*s, *global_attributes*, *statement_list*s, and *namespace_member_declaration*s of its immediately containing *compilation_unit* or *namespace_body*. An *extern_alias_directive* does not contribute any new members to the underlying declaration space. In other words, an *extern_alias_directive* is not transitive, but, rather, affects only the *compilation_unit* or *namespace_body* in which it occurs.
- The scope of a name defined or imported by a *using_directive* ([§14.5](namespaces.md#145-using-directives)) extends over the *global_attributes*, *statement_list*s, and *namespace_member_declaration*s of the *compilation_unit* or *namespace_body* in which the *using_directive* occurs. A *using_directive* may make zero or more namespace or type names available within a particular *compilation_unit* or *namespace_body*, but does not contribute any new members to the underlying declaration space. In other words, a *using_directive* is not transitive but rather affects only the *compilation_unit* or *namespace_body* in which it occurs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I just made these two edits (after working on the V10 feature "File-Scoped Namespaces," which further changes these two bullet items). Now that top-level statements (statement_lists) are permitted immediately before namespace_member_declarations, we need to make sure those statements are in the scope of these two directives.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The commit I just pushed fixes compilation_unit to include zero or one statement_list, I missed these additions were plural - they should be singular.

Suggested change
- The scope of a name defined by an *extern_alias_directive* ([§14.4](namespaces.md#144-extern-alias-directives)) extends over the *using_directive*s, *global_attributes*, *statement_list*s, and *namespace_member_declaration*s of its immediately containing *compilation_unit* or *namespace_body*. An *extern_alias_directive* does not contribute any new members to the underlying declaration space. In other words, an *extern_alias_directive* is not transitive, but, rather, affects only the *compilation_unit* or *namespace_body* in which it occurs.
- The scope of a name defined or imported by a *using_directive* ([§14.5](namespaces.md#145-using-directives)) extends over the *global_attributes*, *statement_list*s, and *namespace_member_declaration*s of the *compilation_unit* or *namespace_body* in which the *using_directive* occurs. A *using_directive* may make zero or more namespace or type names available within a particular *compilation_unit* or *namespace_body*, but does not contribute any new members to the underlying declaration space. In other words, a *using_directive* is not transitive but rather affects only the *compilation_unit* or *namespace_body* in which it occurs.
- The scope of a name defined by an *extern_alias_directive* ([§14.4](namespaces.md#144-extern-alias-directives)) extends over the *using_directive*s, *global_attributes*, *statement_list*, and *namespace_member_declaration*s of its immediately containing *compilation_unit* or *namespace_body*. An *extern_alias_directive* does not contribute any new members to the underlying declaration space. In other words, an *extern_alias_directive* is not transitive, but, rather, affects only the *compilation_unit* or *namespace_body* in which it occurs.
- The scope of a name defined or imported by a *using_directive* ([§14.5](namespaces.md#145-using-directives)) extends over the *global_attributes*, *statement_list*, and *namespace_member_declaration*s of the *compilation_unit* or *namespace_body* in which the *using_directive* occurs. A *using_directive* may make zero or more namespace or type names available within a particular *compilation_unit* or *namespace_body*, but does not contribute any new members to the underlying declaration space. In other words, a *using_directive* is not transitive but rather affects only the *compilation_unit* or *namespace_body* in which it occurs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’ll add this to the next commit

@BillWagner

Copy link
Copy Markdown
Member Author

An earlier version of this feature is already present on alpha-v9 from a prior meeting. Edits made to this PR since then are not yet on alpha-v9; they will land at the next propagation. If you need them on alpha-v9 sooner, please open a separate PR targeting alpha-v9.
Thanks!

@jskeet jskeet added the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label Jul 22, 2026
@jskeet

jskeet commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Applied "meeting: discuss" so that we can all perform a first round of review before the next meeting.

Add support for top-level statements

Add support for top-level statements

fix md formatting

tweak generated entry-point signature table

Update basic-concepts.md

Add mention of attribute System.Runtime.CompilerServices.CallerMemberName

mention invoking method in top-level statement situation

fix link
@BillWagner
BillWagner force-pushed the v9-toplevel-statements branch from db48965 to a425ffb Compare July 24, 2026 19:50
- With adding task-valued entry points and now top-level statements some re-org was indicated – the result is shorter, but opinions may vary on the structure…
- Some stuff was removed as being tied to a specific implementation
- Startup and termination are combined under one clause and the process is described as a series of steps which handles both `void`, `int` and `Task` typed entry points.
- With the intro of top-level statements an entry point may no longer be named `Main` – this results in little changes across the Standard where `Main` was referenced as the entry point.
- For the external mechanism there was a possible discrepancy, depending on how you read it, between the Standard wording “an external mechanism may be used to specify which method” and a certain compiler which takes a “/main:type” option – note there former states method and the later type. The description has been re-written to allow a type (implying `Main`) or method (regardless of name) to be specified. This covers a certain compiler and allows others to specify a method if they so choose.
- “execution environment” vs. “host environment” – I think the former tends to be read as the language runtime support (which may include a VM) and the latter as the OS GUI or CLI environment. Neither are defined, the latter is used just 3 times. This has not been changed, but should it be?
- “entry point” has been hyphenated when used as a compound modifier for a noun, e.g. “entry-point method”. English grammarians can decide if that is correct and I used the correct one in each place…
- The initial proposed grammar changes for top-level statements have been fixed, this also required changes to the text.
- A grammar test is included.
- It shall be declared in a non-generic type. If the type declaring the method is a nested type, none of its enclosing types may be generic.
- It may have the `async` modifier provided the method’s return type is `System.Threading.Tasks.Task` or `System.Threading.Tasks.Task<int>`.
- The return type shall be `void`, `int`, `System.Threading.Tasks.Task`, or `System.Threading.Tasks.Task<int>`.
- It may have the `async` modifier only if the method’s return type is `System.Threading.Tasks.Task` or `System.Threading.Tasks.Task<int>`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- It may have the `async` modifier only if the method’s return type is `System.Threading.Tasks.Task` or `System.Threading.Tasks.Task<int>`.
- If the `async` modifier is present, the method’s return type shall be `System.Threading.Tasks.Task` or `System.Threading.Tasks.Task<int>`.

(Just to avoid "may".)

@Nigel-Ecma Nigel-Ecma Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’ll put it in next commit.

- The return type shall be `void`, `int`, `System.Threading.Tasks.Task`, or `System.Threading.Tasks.Task<int>`.
- It may have the `async` modifier only if the method’s return type is `System.Threading.Tasks.Task` or `System.Threading.Tasks.Task<int>`.
- It shall not be a partial method ([§15.6.9](classes.md#1569-partial-methods)) without an implementation.
- The parameter list shall either be empty, or have a single value parameter of type `string[]`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about string?[], string[]? or string?[]? parameter types?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I haven’t checked. However the normal expectation is the entry point is called at startup and is passed the argument array – which is specified to be a non-null array of non-null strings and so an entry-point method can be written with that in mind.

- It shall have the name `Main`.
- It shall be `static`.
- It shall not be generic.
- It shall be declared in a non-generic type. If the type declaring the method is a nested type, none of its enclosing types may be generic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- It shall be declared in a non-generic type. If the type declaring the method is a nested type, none of its enclosing types may be generic.
- It shall be declared in a non-generic type. If the type declaring the method is a nested type, all of its enclosing types shall be non-generic.

(Just to keep things consistent.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’ll put it in next commit.

Otherwise the entry point is selected from the candidates:

- If one of the candidates is specified by an external mechanism (§externally-defined-entry-point) it is selected as the entry point;
- Otherwise, iff one of the candidates is defined by top-level statements (§using-top-level-statements) it is selected as the entry point;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Otherwise, iff one of the candidates is defined by top-level statements (§using-top-level-statements) it is selected as the entry point;
- Otherwise, if exactly one of the candidates is defined by top-level statements (§using-top-level-statements) it is selected as the entry point;

(Just to avoid expanding "iff".)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I knew someone would comment on the iff ;-)

I’ll put it in next commit.


Otherwise the entry point is selected from the candidates:

- If one of the candidates is specified by an external mechanism (§externally-defined-entry-point) it is selected as the entry point;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should sense-check this - is it really okay to have top-level statements and have them ignored due to specifying an entry point type separately? (And do we want to mandate that that's okay?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did test current behaviour of an implementation.

Regardless it does make sense to me:

  • top-level statements are just a shorthand for an entry-point method, they're not otherwise special for a user's perspective.

  • command line arguments are often the way to override some default; here it is either resolving an ambiguity, thus avoiding an error, or overriding the selection made by the resolution rules.

Maybe the question to ask is why do the rules favour top-level statements over Main methods?

The answer may be that as the name used for a top-level statements entry point is implementation defined it is harder to pick use a command line argument – the current rules allow any of the methods to be picked, and if you didn't spot it I wrote the loose enough such that the external mechanism could select a method not automatically recognised as an entry-point. I haven’t tested with the CLI on my system supports it, I know the IDE doesn’t (it provide a menu of recognised entry-points) – but there seems to be no reason to forbid it.


### §entry-point-invocation Entry point invocation

If the entry point declares a parameter, then the implementation shall as the initial value of that parameter provide a non-null reference to a string array. This array shall consist of non-null references to zero or more strings, called ***application parameter***s, which are given implementation-defined values by the host environment prior to application startup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we rename these to "application arguments"? That would be more consistent with parameter/argument elsewhere? (I only noticed due to the note below.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A caller passes arguments, the callee has parameters. So the CLI passes application arguments, the entry-point gets application parameters. I just did a quick check and the C Standard has the names them this way as well. So I think application parameters is correct here. Maybe the Note could be written better?


The application startup and termination process is semantically equivalent to the following steps:

- An application run is started by either:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We've got "application run" twice here, but we haven't defined it. We could just talk about an application being started...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think “run” is used with its common (computing) meaning. In §3 we have:

  • application – assembly with an entry point
  • program – one or more compilation units that are presented to the compiler and are run or executed by an execution environment

that is the first use of “run”, is line 9 of this file. I have just gone with this use of “run”.

Reading the above definitions now I think the Standard might have more issues with “application” (84 occurrences) vs. “program” (186 occurrences) – is this section about application startup or program startup? Maybe we can leave that for another day…

- An application run is started by either:
- Invoking ([§12.8.10](expressions.md#12810-invocation-expressions)) the entry-point method, if its return type is `void` or `int`; or
- Awaiting ([§12.9.9](expressions.md#1299-await-expressions)) the result of invoking the entry-point method, if its return type is a `Task` type.
- In either case if the entry point requires an argument the application parameter array is supplied as its value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- In either case if the entry point requires an argument the application parameter array is supplied as its value.
In either case if the entry point requires an argument the application parameter array is supplied as its value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I actually considered:

The application startup and termination process is semantically equivalent to the following steps:

  • An application run is started by either:
    • Invoking (§12.8.10) the entry-point method, if its return type is void or int; or
    • Awaiting (§12.9.9) the result of invoking the entry-point method, if its return type is a Task type.
  • In either case if the entry point requires an argument the application parameter array is supplied as its value.

You’re suggesting taking it right out of the list:

The application startup and termination process is semantically equivalent to the following steps:

  • An application run is started by either:
    • Invoking (§12.8.10) the entry-point method, if its return type is void or int; or
    • Awaiting (§12.9.9) the result of invoking the entry-point method, if its return type is a Task type.

In either case if the entry point requires an argument the application parameter array is supplied as its value.

I dithered over what is in the commit and the first one above wishing to keep them connected; in yours I think the last bullet/line is too disconnected.

Would you prefer the first over the commit?

- The application is terminated
- If the run results in an `int` value it serves as the termination status code;
- Otherwise, if the run results in no return value the termination status code is `0`;
- Otherwise, if the run terminates due to an exception ([§22.4](exceptions.md#224-how-exceptions-are-handled)), the exit code is implementation-defined. Additionally, the implementation may provide alternative APIs for specifying the exit code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Otherwise, if the run terminates due to an exception ([§22.4](exceptions.md#224-how-exceptions-are-handled)), the exit code is implementation-defined. Additionally, the implementation may provide alternative APIs for specifying the exit code.
- Otherwise, if the run terminates due to an exception ([§22.4](exceptions.md#224-how-exceptions-are-handled)), the exit code is implementation-defined.
Additionally, the implementation may provide alternative APIs for specifying the exit code.

(It feels like that "additionally" isn't really part of the "otherwise".)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The statement appears to allow APIs that replace the three default options, but not how that interacts with entry-point return (e.g. an implementation could effectively return from the entry-point method on calling an “exit(code)” API). Maybe keep it in the list?

  • The application is terminated
    • If the run results in an int value it serves as the termination status code;
    • Otherwise, if the run results in no return value the termination status code is 0;
    • Otherwise, if the run terminates due to an exception (§22.4), the exit code is implementation-defined.
  • Additionally, the implementation may provide alternative APIs for specifying the exit code.


As described in §using-top-level-statements, top-level source tokens are enclosed by the generated entry-point method.

For the purpose of simple-name evaluation, once the global namespace is reached, first, an attempt is made to evaluate the name within the generated entry-point method and only if this attempt fails is the evaluation within the global namespace declaration performed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels odd to me, but it does look correct. I'd expected the code below to compile, but no, it fails with an error of "Cannot use local variable or local function 'SimpleName' declared in a top-level statement in this context."

Progam.cs:

int SimpleName = 10;
OtherClass.OtherMethod(SimpleName);

OtherClasses.cs:

class SimpleName {}

class OtherClass
{
    public static void OtherMethod(int x) => Console.WriteLine(nameof(SimpleName));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(Although using typeof instead of nameof works... I haven't chased down whether that's a contradiction or not; I haven't figured out whether typeof uses simple name evaluation.)

@Nigel-Ecma Nigel-Ecma Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: This wording was in the received text, I just left it. What it does not say is that if the attempt to evaluate it in the generated method succeeds an error must be produced, should it?

I think it is odd as well.

On the one hand it makes no sense to say the top-level statements are wrapped in a method and then insert that method’s body before the global namespace.

However on the other hand those top-level statements visually appear to be at global level and so should be accessible, so recognising them and reporting an error could be deemed better UI.

It is said people get odder as they age, programming languages certainly do.

I would suggest adding a Note to suggest why this is done (either my guess above or some other explanation) @jskeet do you agree? Fancy writing one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@BillWagner to check with Mads and Fred about this. (Perhaps this is a Roslyn-specific value-add?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The reason for this decision is to leave open the design space for top-level members. See dotnet/csharplang#9803 and the proposed feature spec.

By binding to the "global" symbol instead of the symbols in other files, it ensures that the future feature (if implemented) won't break existing code with top-level statements and any associated local functions. The local functions may be promoted to top-level functions, and might be accessible in this case.

@jskeet

jskeet commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Other than the noted oddity, we think this is ready for "final tweaks and final review".

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

Labels

meeting: discuss This issue should be discussed at the next TC49-TG2 meeting Review: pending Proposal is available for review type: feature This issue describes a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants