[Version 9.0] Feature support for top level statements - #1454
Conversation
d872f61 to
07946a2
Compare
|
@BillWagner There is one grammar change: the extension of compilation_unit to allow |
| - 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| - 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. |
There was a problem hiding this comment.
I’ll add this to the next commit
b9bed76 to
197acef
Compare
197acef to
ade6460
Compare
ade6460 to
db48965
Compare
|
An earlier version of this feature is already present on |
|
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
db48965 to
a425ffb
Compare
- 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>`. |
There was a problem hiding this comment.
| - 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".)
There was a problem hiding this comment.
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[]`. |
There was a problem hiding this comment.
How about string?[], string[]? or string?[]? parameter types?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
| - 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.)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
| - 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".)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Should we rename these to "application arguments"? That would be more consistent with parameter/argument elsewhere? (I only noticed due to the note below.)
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
We've got "application run" twice here, but we haven't defined it. We could just talk about an application being started...
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
| - 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. |
There was a problem hiding this comment.
I actually considered:
The application startup and termination process is semantically equivalent to the following steps:
- An application run is started by either:
- 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:
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. |
There was a problem hiding this comment.
| - 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".)
There was a problem hiding this comment.
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
intvalue 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. |
There was a problem hiding this comment.
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));
}There was a problem hiding this comment.
(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.)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
@BillWagner to check with Mads and Fred about this. (Perhaps this is a Roslyn-specific value-add?)
There was a problem hiding this comment.
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.
|
Other than the noted oddity, we think this is ready for "final tweaks and final review". |
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.