Skip to content

Use speakable type for top-level statements#55368

Merged
jcouv merged 8 commits intodotnet:mainfrom
jcouv:top-level
Aug 6, 2021
Merged

Use speakable type for top-level statements#55368
jcouv merged 8 commits intodotnet:mainfrom
jcouv:top-level

Conversation

@jcouv
Copy link
Copy Markdown
Member

@jcouv jcouv commented Aug 3, 2021

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:

@jcouv jcouv self-assigned this Aug 3, 2021
@ghost ghost added the Area-Compilers label Aug 3, 2021
/// 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";
Copy link
Copy Markdown
Member

@Youssef1313 Youssef1313 Aug 3, 2021

Choose a reason for hiding this comment

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

FYI @davidwengier @tmat to see if this will break EnC:

private static bool IsGlobalMain(ISymbol symbol)
=> symbol is IMethodSymbol { Name: WellKnownMemberNames.TopLevelStatementsEntryPointMethodName, ContainingType.Name: WellKnownMemberNames.TopLevelStatementsEntryPointTypeName };

#Resolved

Copy link
Copy Markdown
Member

@Youssef1313 Youssef1313 Aug 3, 2021

Choose a reason for hiding this comment

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

Also FYI @CyrusNajmabadi, it's likely that generate field/property is going to break in a Program class:

internal bool CanGeneratePropertyOrField()
{
return ContainingType is { IsImplicitClass: false, Name: not WellKnownMemberNames.TopLevelStatementsEntryPointTypeName };
}

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.

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.

Thanks @Youssef1313. EnC itself should be fine, but I would expect test failures which will be easy enough to correct

Copy link
Copy Markdown
Member

@tmat tmat Aug 3, 2021

Choose a reason for hiding this comment

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

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.

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 distinctive mark of a simple program will be the Program.<Main>$ method (remains unspeakable).

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.

So we'll need to check if the type contains <Main>$? Seems like there should be API that does that.

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 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?

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.

I see. I guess it's ok then. For some reason I thought we are checking the type.

Comment on lines +2517 to +2522
await TestMissingAsync(@"
[|P|] = 10;

partial class Program
{
}");
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.

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.

@jcouv jcouv marked this pull request as ready for review August 4, 2021 17:14
@jcouv jcouv requested review from a team as code owners August 4, 2021 17:14
@jcouv jcouv added the Feature - Simple Programs Top-level statements label Aug 4, 2021
var simpleProgramEntryPoints = GetSimpleProgramEntryPoints();
foreach (var member in simpleProgramEntryPoints)
{
builder.AddNonTypeMember(member, declaredMembersAndInitializers);
Copy link
Copy Markdown
Contributor

@cston cston Aug 4, 2021

Choose a reason for hiding this comment

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

AddNonTypeMember(member, declaredMembersAndInitializers)

Do we have a race condition where external callers of GetSimpleProgramEntryPoints() might get back an entry point that hasn't been added to MembersAndInitializers yet? If so, what are the implications? #Closed

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.

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?

@cston
Copy link
Copy Markdown
Contributor

cston commented Aug 4, 2021

    public override Accessibility DeclaredAccessibility

Consider removing this override, rather than duplicating the value here, since base.DeclaredAccessibility uses the value set in the .ctor.


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)

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Aug 5, 2021

@dotnet/roslyn-compiler for second review. Thanks

1 similar comment
@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Aug 5, 2021

@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));
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Aug 5, 2021

Choose a reason for hiding this comment

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

Is it true that we can't get here in an error scenario like the following:

namespace NS;

var x = 1;

#Resolved

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.

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()
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Aug 5, 2021

Choose a reason for hiding this comment

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

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

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.

Good idea. Adding a typeof test.

@RikkiGibson RikkiGibson self-assigned this Aug 5, 2021

private sealed class SimpleProgramEntryPointInfo
{
public readonly ImmutableArray<SynthesizedSimpleProgramEntryPointSymbol> SimpleProgramEntryPoints;
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Aug 5, 2021

Choose a reason for hiding this comment

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

Is it true that in non-error scenarios, there is either 1 element in here, or the SimpleProgramEntryPointInfo itself is null? #Resolved

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.

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.

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.

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
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Aug 5, 2021

Choose a reason for hiding this comment

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

nit: this seems like it could be a little more elegant as one big pattern :) #Resolved

@jcouv jcouv merged commit 150b4d1 into dotnet:main Aug 6, 2021
@ghost ghost added this to the Next milestone Aug 6, 2021
@jcouv jcouv deleted the top-level branch August 6, 2021 03:12
.True("Microsoft.CodeAnalysis.Runtime.Instrumentation.FlushPayload();")
.True(@"Console.WriteLine(""Test"");");
checker.Method(4, 1)
checker.Method(5, 1)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Aug 24, 2021

Choose a reason for hiding this comment

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

checker.Method(5, 1)

Do we understand what method is added here? #Resolved

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.

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),
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Aug 24, 2021

Choose a reason for hiding this comment

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

4

Do we understand why this number has changed? #Resolved

Diagnostic(ErrorCode.WRN_MainIgnored, "Main").WithArguments("Program.Main()").WithLocation(14, 23)
);

CompileAndVerify(comp, expectedOutput: "Hi!");
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Aug 24, 2021

Choose a reason for hiding this comment

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

CompileAndVerify(comp, expectedOutput: "Hi!");

It looks like this test was testing interesting success scenario. I think we should keep it as such. The class name isn't important, so renaming it would be more appropriate. #Pending

Diagnostic(ErrorCode.WRN_MainIgnored, "Main").WithArguments("Program.Main(string[])").WithLocation(15, 23)
);

CompileAndVerify(comp, expectedOutput: "Hi!");
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Aug 24, 2021

Choose a reason for hiding this comment

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

CompileAndVerify(comp, expectedOutput: "Hi!");

Same here #Pending

var text = @"
using System.Threading.Tasks;

System.Console.Write(""Hi!"");
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Aug 24, 2021

Choose a reason for hiding this comment

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

Why the test scenario is drastically modified? #Pending

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.

Sure. We can rename the type name

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.

Speakable names for top-level statements

8 participants