Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Allow multiple top-level statements per assembly #4163

Open
1 of 4 tasks
Tyrrrz opened this issue Nov 20, 2020 · 3 comments
Open
1 of 4 tasks

Proposal: Allow multiple top-level statements per assembly #4163

Tyrrrz opened this issue Nov 20, 2020 · 3 comments

Comments

@Tyrrrz
Copy link

Tyrrrz commented Nov 20, 2020

Allow multiple top-level statements per assembly

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

Summary

Currently, only one file in an assembly (or project) can have top-level statements, as they were introduced in C# 9 (#2765). This makes sense from a logical perspective, because having multiple entry points would raise ambiguity as to which one should be executed. However, there are certain use cases where this ambiguity can easily be resolved and having multiple top-level programs could be highly beneficial (explained in the next section).

Motivation

Allowing multiple top-level programs could help in some scenarios.

For example, project SmallSharp provides a way to have multiple top-level programs in a project, offering a convenient experience for writing scripts while still benefitting from MSBuild infrastructure and IDEs. It does this by excluding all root *.cs files from compilation (to prevent the compilation error), except the file matching the current launch profile ($(ActiveDebugProfile)). The launch profiles are generated automatically via a source generator, one for each program file in the root directory.

This allows the user to select which program to run in the IDE (by selecting one in the dropdown) or via CLI (via --launch-profile option). While functional, it has a few downsides:

  • Since all files in the root of the project (except current) are excluded from compilation, any shared code must be placed in subdirectories. This makes it a bit ugly to work with.
  • Additionally, because some files are excluded, they don't raise errors during compilation. This effectively means that the compiler validates only the current startup file.

Another use case is my own experimental project Hallstatt. The idea behind it is to offer a way for developers to write tests using top-level statements, avoiding a ton of boilerplate and inherent limitations of methods/attributes. Of course, because currently only one file is allowed to have top-level statements, it severely limits the usability of the library. Additionally, the approach used by SmallSharp is not applicable here, because all of the files containing top-level statements need to be compiled, not just one.

Essentially, in both of these use cases there is no ambiguity problem, because:

  • In SmallSharp, the user chooses what to run
  • In Hallstatt, the test adapter decides what to run via reflection

However, because of the compilation limitation that requires that only one file may contain top-level statements, they both have to rely on ugly workarounds.

Detailed design

The suggestion is to keep the existing behavior which limits top-level statements to one file, but add a compiler option and/or project property to lift it:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>    
    <AllowMultipleTopLevelStatements>true</AllowMultipleTopLevelStatements>
    <StartupFile>FileThatContainsTheTopLevelStatementsThatNeedToRun.cs</StartupFile>
  </PropertyGroup>

</Project>

(property names are up for consideration)

Effectively, this should allow all files that contain top-level statements to compile, each generating a dynamic <$Program> class with a Main method. Another property (StartupFile) will then have to be used to instruct which file to run. If StartupFile is not specified, but AllowMultipleTopLevelStatements is true, a compilation error will need to be raised.

Running dotnet run on this project will run the Main method generated by the top level statements inside FileThatContainsTheTopLevelStatementsThatNeedToRun.cs.

This is quite similar to an already existing -main compiler option (and <StartupObject> property), except that it should take a file path (compilation unit) instead of a class name, because the user doesn't know what is the name of the compiler-generated class.

This would allow SmallSharp to change this property dynamically without having to exclude files from compilation, avoiding the associated drawbacks.

In case with Hallstatt, it would allow the library to use StartupFile to specify its own entry point, while at the same time using reflection to run user-defined top-level statements by searching for Main methods inside compiler-generated <$Program> classes.

Drawbacks

Currently, the compiler-generated class for top-level statements seems to always have the same name. If we introduce the option to allow multiple to be defined, we would have to make sure that the generated names are unique. However, some developers may already have taken dependency on the existing class name through reflection, which may break their code.

Alternatives

Other designs have not been considered.

The impact of not doing this: developers will need to find ways to work around this limitation by hacking with MSBuild targets, ultimately achieving suboptimal and potentially error-prone results.

Unresolved questions

Given that there may be multiple top-level programs, what naming schema should the auto-generated class be using?

Design meetings

@HaloFour
Copy link
Contributor

The C# compiler isn't aware of the project file. It's up to whatever build environment (like MSBuild) to interpret the project file and to pass the appropriate options to the compiler.

If I understand what you're asking for correctly what you want is to be able to have multiple files with top level statements but you'd only ever compile one of them at a time. That feels like something that should be handled by the build tool by only including the specific source file that contains the top level statements that you want to have compiled.

@Tyrrrz
Copy link
Author

Tyrrrz commented Nov 20, 2020

The C# compiler isn't aware of the project file. It's up to whatever build environment (like MSBuild) to interpret the project file and to pass the appropriate options to the compiler.

I understand that. I envision that the project property would map to the corresponding compiler option, similar to how -main and <StartupObject> do.

If I understand what you're asking for correctly what you want is to be able to have multiple files with top level statements but you'd only ever compile one of them at a time. That feels like something that should be handled by the build tool by only including the specific source file that contains the top level statements that you want to have compiled.

Not exactly. I want all of them compiled, but only one of them (or some other file) to be considered as entry point. This is analogous to the -main compiler option, except that this option would specify a file name instead of a class name.

@kyleburnsdev
Copy link

One thought to potentially help with the generation of unique names while not breaking existing dependencies on the <$Program> class name would be to have the class compiled from StartupFile remain <$Program> while all others are derived from their file name.

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

No branches or pull requests

3 participants