Use speakable type for top-level statements#55368
Conversation
| /// The name of a type synthesized for a top-level statements entry point method. | ||
| /// </summary> | ||
| public const string TopLevelStatementsEntryPointTypeName = "<Program>$"; | ||
| public const string TopLevelStatementsEntryPointTypeName = "Program"; |
There was a problem hiding this comment.
FYI @davidwengier @tmat to see if this will break EnC:
roslyn/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs
Lines 5451 to 5452 in b46c7a7
#Resolved
There was a problem hiding this comment.
Also FYI @CyrusNajmabadi, it's likely that generate field/property is going to break in a Program class:
I think the behavior should be that codefix is offered, and if we're in a global statement, then generate partial Program and add the property/field in it.
There was a problem hiding this comment.
Thanks @Youssef1313. EnC itself should be fine, but I would expect test failures which will be easy enough to correct
There was a problem hiding this comment.
I don't think we can continue checking for top-level code container type just by checking the name. We need to check if the Program type is in fact the synthesized implicit type (or if it is partial we need to somehow check if it has a synthesized partial declaration). It would be good to have a compiler API that tells us whether a type is the implicit top-level type.
There might also be some cases to test/consider around partial class Program.
There was a problem hiding this comment.
The distinctive mark of a simple program will be the Program.<Main>$ method (remains unspeakable).
There was a problem hiding this comment.
So we'll need to check if the type contains <Main>$? Seems like there should be API that does that.
There was a problem hiding this comment.
The code was already checking for <Main>$ so it's not really a change.
We can discuss possible public API, but I'm not clear what would the user scenarios be. Do we just care whether there is <Main>$? Do we care whether it's a pure simple program, or whether it has some user-defined members from a partial declaration?
There was a problem hiding this comment.
I see. I guess it's ok then. For some reason I thought we are checking the type.
| await TestMissingAsync(@" | ||
| [|P|] = 10; | ||
|
|
||
| partial class Program | ||
| { | ||
| }"); |
There was a problem hiding this comment.
A more ideal behavior is to generate the property as follows:
P = 10;
partial class Program
{
public int P { get; private set; }
}In case you find it out-of-scope for this PR, we can open a tracking issue for that.
src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs
Outdated
Show resolved
Hide resolved
| var simpleProgramEntryPoints = GetSimpleProgramEntryPoints(); | ||
| foreach (var member in simpleProgramEntryPoints) | ||
| { | ||
| builder.AddNonTypeMember(member, declaredMembersAndInitializers); |
There was a problem hiding this comment.
There was a problem hiding this comment.
I don't think that is possible. GetSimpleProgramEntryPoints is atomic. It will return exactly one set of entry points, then we'll incorporate them into MembersAndInitializers.
Different threads will see the same set of entry points, and there is not other way to get entry point symbols than through GetSimpleProgramEntryPoints.
Does that make sense, or do you still think there might be a way?
src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedSimpleProgramEntryPointSymbol.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedSimpleProgramEntryPointSymbol.cs
Outdated
Show resolved
Hide resolved
Consider removing this override, rather than duplicating the value here, since In reply to: 893005575 In reply to: 893005575 In reply to: 893005575 Refers to: src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedSimpleProgramEntryPointSymbol.cs:146 in 9dc9216. [](commit_id = 9dc9216, deletion_comment = False) |
src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests_CallerArgumentExpression.vb
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs
Show resolved
Hide resolved
|
@dotnet/roslyn-compiler for second review. Thanks |
1 similar comment
|
@dotnet/roslyn-compiler for second review. Thanks |
| : base(tupleData) | ||
| { | ||
| // If we're dealing with a simple program, then we must be in the global namespace | ||
| Debug.Assert(containingSymbol is NamespaceSymbol { IsGlobalNamespace: true } || !declaration.Declarations.Any(d => d.IsSimpleProgram)); |
There was a problem hiding this comment.
Is it true that we can't get here in an error scenario like the following:
namespace NS;
var x = 1;#Resolved
There was a problem hiding this comment.
Correct. I'll add a few tests.
ParseNamespaceBody parses differently in global context (outside of a namespace). See isGlobal local. It recognizes when inside a namespace (either with brace or semi-colon).
| } | ||
|
|
||
| [Fact] | ||
| public void SpeakableEntryPoint_XmlDoc_ProgramIsPartial_NotCommented() |
There was a problem hiding this comment.
nit: It feels like the motivating scenario was typeof(Program).Assembly and reflection. Consider testing this in a scenario where partial class Program is not explicitly declared. #Resolved
There was a problem hiding this comment.
Good idea. Adding a typeof test.
|
|
||
| private sealed class SimpleProgramEntryPointInfo | ||
| { | ||
| public readonly ImmutableArray<SynthesizedSimpleProgramEntryPointSymbol> SimpleProgramEntryPoints; |
There was a problem hiding this comment.
Is it true that in non-error scenarios, there is either 1 element in here, or the SimpleProgramEntryPointInfo itself is null? #Resolved
There was a problem hiding this comment.
It feels like if we expect exactly one of these in a non-error scenario we might put a field on this class for that "single" one, and only use this field for error recovery. However, since it's not likely that very many of these will be created, it doesn't seem like an important change.
There was a problem hiding this comment.
Correct. There's one entry per file with top-level statement.
However, there's code (SynthesizedSimpleProgramEntryPointSymbol? GetSimpleProgramEntryPoint(CSharpCompilation compilation, CompilationUnitSyntax compilationUnit, bool fallbackToMainEntryPoint)) which needs to access all of the entry point symbols. That's why I couldn't store just zero/one here.
| private static bool IsTopLevelMainType([NotNullWhen(true)] this ISymbol? symbol) | ||
| => symbol is INamedTypeSymbol | ||
| } | ||
| && containingType is INamedTypeSymbol |
There was a problem hiding this comment.
nit: this seems like it could be a little more elegant as one big pattern :) #Resolved
| .True("Microsoft.CodeAnalysis.Runtime.Instrumentation.FlushPayload();") | ||
| .True(@"Console.WriteLine(""Test"");"); | ||
| checker.Method(4, 1) | ||
| checker.Method(5, 1) |
There was a problem hiding this comment.
We didn't use to generate a parameterless constructor for the entry-point type (which is a reference type). Now we do.
| Row(2, TableIndex.StandAloneSig, EditAndContinueOperation.Default), | ||
| Row(1, TableIndex.MethodDef, EditAndContinueOperation.Default), | ||
| Row(3, TableIndex.MethodDef, EditAndContinueOperation.Default), | ||
| Row(4, TableIndex.MethodDef, EditAndContinueOperation.Default), |
| Diagnostic(ErrorCode.WRN_MainIgnored, "Main").WithArguments("Program.Main()").WithLocation(14, 23) | ||
| ); | ||
|
|
||
| CompileAndVerify(comp, expectedOutput: "Hi!"); |
| Diagnostic(ErrorCode.WRN_MainIgnored, "Main").WithArguments("Program.Main(string[])").WithLocation(15, 23) | ||
| ); | ||
|
|
||
| CompileAndVerify(comp, expectedOutput: "Hi!"); |
| var text = @" | ||
| using System.Threading.Tasks; | ||
|
|
||
| System.Console.Write(""Hi!""); |
There was a problem hiding this comment.
Why the test scenario is drastically modified? #Pending
There was a problem hiding this comment.
Sure. We can rename the type name
I ran into an implementation problem: when SourceMemberContainerSymbol creates the non-type member symbols, it needs binders to hand them. But when we have top-level statements, some of the binder chains need to contain a binder for the entry point method (because top-level locals are in scope of the rest of the program, even though we'll disallow their usage). But we can't ask for the non-type member symbols to get that entry point method...
This PR breaks this cycle this PR computes the entry point symbols in an earlier phase, and stores them it a dedicated cache.
Fixes #54877
LDM: