Repository navigation
ConditionalAutoOpenAttribute #1473
Description
I propose we add Microsoft.FSharp.Core.ConditionalAutoOpenAttribute(path: string, condition: string). This repeatable assembly-level attribute imports a library module or namespace when the consumer enables its condition.
The motivating case is task {}. The existing FSharp.Core builder uses F# resumable code, which the F# compiler lowers to a task state machine. A runtime-async builder instead lets .NET manage suspension and resumption. I want projects to select that library-defined builder without renaming task or editing each source file.
For example, FSharp.Core could register a module exporting the alternative task builder:
[<assembly: ConditionalAutoOpen(
"Microsoft.FSharp.Control.RuntimeAsync.TaskExpressions",
"runtime-async=on")>]
do ()A consumer enables that import in the project:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Features>$(Features);runtime-async=on</Features>
</PropertyGroup>The command-line equivalent is --langversion:preview --features:runtime-async=on.
The compiler treats feature entries as opaque, ordinal key=value pairs. Repeated --features options and comma-separated entries are accepted. The Features property splits semicolons, commas, and ASCII spaces into ordered --features tokens. This preserves inherited features without an OtherFlags workaround. For duplicate keys, the last value wins. Thus, a later runtime-async=off disables an earlier runtime-async=on. Matching is exact, so ON and true do not match on.
An enabled attribute imports the named module without a source open. The compiler does not recognize task or runtime-async specially. Libraries can use the same mechanism for other literal conditions and modules. The condition is not a Boolean expression language.
For unqualified names, lookup order is local declarations, enabled conditional imports, then ordinary opened imports. A later ordinary open does not undo a conditional import. Distinct conditional providers have equal rank. Ambiguous uses are diagnosed rather than resolved by reference order. Existing extension priority and source order apply within one provider. Fully qualified names remain explicit choices. For example, Microsoft.FSharp.Control.TaskBuilder.task still selects the classic builder.
The separate attribute is safer than adding a condition to AutoOpenAttribute. An older compiler ignores an unknown attribute instead of opening the module unconditionally. Without an active condition, existing lookup is unchanged.
The runtime builder still needs a compatible FSharp.Core, target references, and host. The property does not update packages or enable runtime support. The attribute mechanism itself is not restricted to runtime async or .NET 10. Mixed C# and F# projects can share the Features property through Directory.Build.props, although each compiler assigns its own meaning to entries.
The existing way of approaching this problem in F# is to use a qualified builder, add an open or local alias in each source file, or give the alternative builder another name such as runtimeTask.
Pros and Cons
The advantages of making this adjustment to F# are consumer-controlled activation without per-file source changes, and a generic mechanism that libraries can extend.
The disadvantages of making this adjustment to F# are another implicit import and a configuration dependency across builds and IDEs. Enabling a condition deliberately changes name binding. Competing conditional providers can create ambiguities.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): L
Related suggestions: Global using (open)
Runtime groundwork: dotnet/fsharp#20235. That pull request does not propose this attribute.
No public RFC or implementation pull request exists for this attribute.
Affidavit (please submit!)
Please tick these items by placing a cross in the box:
- This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
- This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
- This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
- I have searched both open and closed suggestions on this site and believe this is not a duplicate
Please tick all that apply:
- This is not a breaking change to the F# language design
- I or my company would be willing to help implement and/or test this
The feature is opt-in, but activation can change unqualified name binding in the consuming project.
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
Activity
Happypig375 commentedon Sep 19, 2026
We could maybe fix some of the inconsistencies with the default functions with this depending on language version like #472.
Tarmil commentedon Sep 19, 2026
Surely the introduction of a compile-time key-value feature system is the big feature being proposed here, and shouldn't be discussed just as a side-show for
ConditionalAutoOpen. It would have many potential uses.T-Gro commentedon Sep 21, 2026
<Features>$(Features);F#++=on</Features>T-Gro commentedon Sep 21, 2026
I would want to manage expectations a little - we do not want to go very wild with applying the feature mechanism.
The primary (and so far only) motivation is FSharp.Core evolution while offering some form of opt-in/opt-out.
For pure compiler features, we do have that with
LangVersion.Any extensibility point like this multiplies testing effort we have to do. We can do that for FSharp.Core (test everything related with "bleeding edge" as well as "backwards compatible" versions of FSharp.Core). We would not be able to do that for any combination with dozens of tunable features.
There are also simpler versions of this proposal:
The sole reason for picking exactly
<Features>runtime-async=on</Features>was to align with C# and offer central Directory.Build.props maintenance.T-Gro commentedon Sep 21, 2026
One thing that is worth discussing are conditional bindings we might bake into the compiler, so that library authors can leverage them in their respective
[< ConditionalAutoOpen>]definitions. A few predefined and compiler-recognized constants:"nullable=on";"project_has_define=NET10_OR_GREATER".Would definitely love to see real-world challenges from library authors and how they could benefit from selectively progressing experience via such feature 👍 .
Tarmil commentedon Sep 21, 2026
Ok I see, I didn't realize that the exact syntax came from C#. I hope this "features" system is well-defined on their side, I have a hard time finding any documentation about it. The only other "feature" I can find mentioned, besides
runtime-async=on, isstrict(with no=on); and only in blogs and StackOverflow, nothing official, so already this looks inconsistent and under-documented...T-Gro commentedon Sep 21, 2026
https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/CommandLine/Feature.cs#L14-L29
It really just is arbitrary strings assigned to arbitrary values.
We do not have to follow it for
<Features>runtime-async=on</Features>, but it is a project setting which already made its way to announcements and blogs.charlesroddie commentedon Sep 23, 2026
This might allow follow-on tidy-up work in the future. The overall theme is that, in some conditions, or as a user preference, certain functions should not be easily usable.
The existing --reflectionfree flag:
These functions should be hidden under
--reflectionfree(autoopened underreflectionfree=offunder this suggestion). Either by extending this feature to be selective about functions, or putting them in a module and not opening the module.If the level of granularity is modules/namespaces this could be implemented by splitting the module containing these into two, autoopening the one with these conditionally, and autoopening the other unconditionally.
Tidy up, resolving continual increases in globally accessible functions
Some functions should be in modules.
E.g. the null/NRT functions (
isNull,isNullV,nonNull,nonNullV,withNull,withNullV,nullV,defaultIfNull,defaultIfNullV,nullArgCheck), which are an extensive pollution of the global namespace. These can be put in modules and hidden under some flag (--modularor--nrtmodulesorcleanfsharpor something). Requires a language suggestion.T-Gro commentedon Sep 23, 2026
Those are all good examples.
The generic theme is around negatively/positively matching an
AutoOpenwith a specific F# compiler CLI flag.[<ConditionalAutoOpen("module or namespace", "exact CLI flag")>][<RetractExistingAutoopen("module or namespace", "exact CLI flag")>](the existing
[<AutoOpen>]where already given should stay in place so that older compilers unaware of new attributes can still work).Some well known CLI flags to make use within FSharp.Core or in 3rd party libraries might be:
--reflectionfree--nullable--features:runtime-async=on--define:NET11_OR_GREATER