You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Revisits #13880, which was closed in Oct 2022 with "we shouldn't do it". This proposal takes that
decision seriously and is written specifically to answer the objections raised there — in particular @dsyme's point that FSharp.Compiler.Service should keep a very low dependency base and stay
fully-F#-implemented for Fable. The design below adds no dependency to FSharp.Compiler.Service at all.
If the answer is still "no", the compatibility inventory in section 3 should at least be useful on its own.
FSI-specific options in src/Compiler/Interactive/fsi.fs
~20
opts* help strings in FSComp.txt
95
fsi* help strings in FSIstrings.txt
56
localised .xlf languages
13
test files under tests/.../CompilerOptions/
111
Why this is worth revisiting
The parser is ~250 lines of bespoke string slicing (parseOption, getSwitchOpt, getSwitch, PostProcessCompilerArgs, GetAbbrevFlagSet). Its quirks are load-bearing and under-specified,
and they leak: #10819, #18983 and #19214 are all defects in this layer. The -- handling in fsi.fs in particular needs a manual argv split before PostProcessCompilerArgs to avoid
rewriting user script args. There is no completion support, and the help renderer
(getCompilerOption, fixed 42-column flag field) is hand-written.
The part that constrains everything
ParseCompilerOptions is not just an fsc/fsi entry-point concern. It is also how the
language service interprets project otherOptions:
#13880 was closed with four objections. Where they stand in 2026:
Objection (2022)
Status now
"Whatever else we use will have some weird compat problems"
Still the main risk — but now measurable rather than hypothetical. Section 3 is the actual measurement; only one syntax form is genuinely unsupported.
"What is there works, is stable"
Mostly true, but #10819 / #18983 / #19214 are open/recent defects in exactly this layer.
"The Fable version of the F# compiler would need an implementation of that library"
Fully addressed by the design in section 4 — FSharp.Compiler.Service takes no new dependency; the adapter lives in the fsc/fsi executables only.
"Structural advantage in low dependency base for FCS"
Same as above — FCS dependency base is unchanged.
Also new: System.CommandLine reached 2.0.0 GA in Oct 2025 after years in beta (the churn
that made it unattractive in 2022 is over), and @baronfel's 2022 point about standardising on it
across the .NET stack still holds.
3. Compatibility surface (measured, not assumed)
I built a probe against System.CommandLine 2.0.11 (highest stable) targeting net472 and
modern .NET, and exercised the syntax forms the F# compiler accepts today:
Current syntax
Example
System.CommandLine 2.0.11
--flag:value
--define:FOO
✅ works natively
--flag=value
--define=FOO
✅
--flag value
--warn 3
✅
short alias + colon
-d:FOO
✅ via registered alias
slash prefix
/define:FOO, /optimize
✅ via explicitly registered alias
response files
@args.rsp
✅ built in
-- passthrough
fsi a.fsx -- x y
✅
+/- switch suffix
--optimize+, --debug-
❌ not supported
unknown token
--bogus
⚠️ behaviour differs (see below)
Two real gaps:
+/- suffixes (26 flags).--optimize+ is not parsed; it falls through as a positional
argument. --optimize:true / --optimize:falsedo work, so this is solvable with a small
argv pre-normalisation pass (--opt+ → --opt:true, --opt- → --opt:false) driven by the
known set of switch flags. This replaces PostProcessCompilerArgs/GetAbbrevFlagSet rather
than adding to them.
Unknown-token handling. Today unrecognised options are an error; with a ZeroOrMore
positional argument for source files they are silently collected. Needs an explicit validator
so fsc --bogus a.fs still fails, while a.fs is still accepted as a file.
Everything else — including the colon separator, which was my main worry — already works.
Not yet measured, and needed before committing: startup cost. fsc/fsi startup is
performance-sensitive and there is R2R/MIBC work in the build (fscProject/fsc.fsproj); loading
and constructing a ~185-option System.CommandLine tree must be benchmarked against the current
parser before this is accepted.
4. Variant A — plain System.CommandLine
Key design point: keep CompilerOptions.fs as the single source of truth and add the System.CommandLine layer only in the executables.
FSharp.Compiler.Service already grants InternalsVisibleTo to fsc, fscAnyCpu, fscArm64, fsi, fsiAnyCpu, fsiArm64
(FSharp.Compiler.Service.fsproj:85-90),
so the exes can consume the internal CompilerOption model directly.
FSharp.Compiler.Service (netstandard2.0) ← no new dependency, Fable unaffected
CompilerOptions.fs
CompilerOption / OptionSpec model ← unchanged, still the source of truth
ParseCompilerOptions ← retained for the language service
│
│ (internal model, via IVT)
▼
fsc.exe / fsi.exe ← System.CommandLine lives ONLY here
CompilerOptionsToSystemCommandLine.fs
CompilerOption list -> RootCommand
argv normalisation (+/- suffixes)
The adapter is mechanical: each OptionSpec case maps to an Option<T> whose action invokes the
existing setter closure, so TcConfigBuilder mutation stays exactly where it is today.
OptionSpec
maps to
OptionUnit / OptionSet / OptionClear
Option<bool> (no arg)
OptionString / OptionInt / OptionFloat
Option<string/int/float>
OptionStringList / OptionIntList
Option<T[]>, AllowMultipleArgumentsPerToken
OptionSwitch
Option<bool> + argv normalisation for +/-
OptionStringListSwitch / OptionIntListSwitch
Option<T[]> + same normalisation
OptionRest
trailing Argument<string[]>
OptionConsoleOnly
System.CommandLine help/version actions
OptionGeneral (1 use, FSI script.fsx arg1 …)
custom Action on the root command
Pros: no third-party dependency; Microsoft-supported; matches @baronfel's and @vzarytovskii's stated preference in #13880; completions and a maintained help renderer come free;
FCS/Fable untouched.
Cons: the C#-shaped API is verbose from F# (Option<T> construction, ParseResult.GetValue);
help output changes, so both .bsl baselines need rebaselining and the localised .xlf strings
need re-review against System.CommandLine's own localised chrome ("Usage:", "Options:").
5. Variant B — FSharp.SystemCommandLine
FSharp.SystemCommandLine (MIT,
Jordan Marr) is a thin F# wrapper over System.CommandLine, offering a CE-based API:
openFSharp.SystemCommandLineopenFSharp.SystemCommandLine.Inputletdefine= option<string array>"--define"|> alias "-d"|> alias "/define"|> def [||]letoptimize= option<bool>"--optimize"|> alias "/optimize"|> def falseletfiles= argument<string array>"files"|> arity ArgumentArity.ZeroOrMore
rootCommand argv {
description "F# Compiler"
inputs (define, optimize, files)
setAction (fun(d,o,f)->...)}
Version facts (verified against nuget.org):
Latest stable 2.2.0, targeting netstandard2.0 and net6.0 — so net472 is covered.
Declares System.CommandLine >= 2.0.0; I verified it builds and runs correctly against
2.0.11, the highest stable System.CommandLine, on both net472 and modern .NET. So
"use the newest possible System.CommandLine" is compatible with this wrapper today.
System.CommandLine 3.0.0 is preview-only; a major-version bump against a wrapper compiled
for 2.x should be assumed binary-breaking until proven otherwise.
Pros: substantially less ceremony; strongly-typed tupled inputs; idiomatic F#.
Cons — and these are decisive against using it inside this repo:
The CE inputs API is arity-limited (tuples). It is designed for tools with a handful of
options, not ~185, so the compiler would end up bypassing the ergonomic layer anyway and
driving System.CommandLine directly — losing the benefit that motivates the dependency.
Two dependencies to service instead of one, including one outside Microsoft's support boundary.
Recommendation: Variant A. Variant B is documented here because it was explicitly asked
about, and it remains an excellent choice for F# tooling in general — just not for the compiler
itself, where the option count defeats its ergonomics and the third-party dependency is the very
thing #13880 objected to.
6. Suggested phasing
Each phase is independently shippable and revertible.
Extract the adapterCompilerOption list -> RootCommand plus argv normalisation, in src/fsc/src/fsi only. No behaviour change yet; parse with both parsers and assert the
resulting TcConfigBuilder state matches, across the test corpus.
Switch fsi.exe to the new front end first — smaller surface, and the FSI-specific
options (--use, --load, --exec, --gui, --readline, --fsi-server*, script
passthrough) are where the known bugs are. Rebaseline expected-help-output.bsl.
Switch fsc.exe. Rebaseline compiler_help_output.bsl; re-review the 13 .xlf locales
for the help chrome that System.CommandLine now owns.
Leave the language-service path on ParseCompilerOptions — or migrate it separately and
deliberately, since it is IDE-visible and has no CLI-compat requirement to honour.
Is the startup-time cost of building a ~185-option command tree acceptable for fsc? (Must be
benchmarked in phase 2; a negative result should kill the proposal.)
Is changing --help output acceptable, given it is baselined and localised into 13 languages?
Should the language-service path (ApplyCommandLineArgs) ever move, or stay on the current
parser permanently?
I'm willing to do the work, starting with phase 1 (characterisation tests), which has standalone
value regardless of the outcome. But given #13880 I'd like a maintainer signal before writing the
adapter — happy to have this closed again if the trade-off hasn't changed.
1. Current state
Command-line parsing for both
fscandfsilives insrc/Compiler/Driver/CompilerOptions.fs,built on a hand-rolled model:
Measured inventory on current
main:CompilerOptiondefinitions inCompilerOptions.fsOptionSwitch(the+/-suffix forms)OptionConsoleOnly(--help,--version, …)OptionGeneral(fully custom matcher)src/Compiler/Interactive/fsi.fsopts*help strings inFSComp.txtfsi*help strings inFSIstrings.txt.xlflanguagestests/.../CompilerOptions/Why this is worth revisiting
The parser is ~250 lines of bespoke string slicing (
parseOption,getSwitchOpt,getSwitch,PostProcessCompilerArgs,GetAbbrevFlagSet). Its quirks are load-bearing and under-specified,and they leak: #10819, #18983 and #19214 are all defects in this layer. The
--handling infsi.fsin particular needs a manual argv split beforePostProcessCompilerArgsto avoidrewriting user script args. There is no completion support, and the help renderer
(
getCompilerOption, fixed 42-column flag field) is hand-written.The part that constrains everything
ParseCompilerOptionsis not just anfsc/fsientry-point concern. It is also how thelanguage service interprets project
otherOptions:src/Compiler/Driver/fsc.fs:270src/Compiler/Interactive/fsi.fs:1265src/Compiler/Service/BackgroundCompiler.fs:1305src/Compiler/Service/FSharpCheckerResults.fs:4098src/Compiler/Service/TransparentCompiler.fs:497src/Compiler/Service/IncrementalBuild.fs:1504(viaApplyCommandLineArgs)src/Compiler/Service/service.fs:615(viaApplyCommandLineArgs)Any change in parsing semantics is therefore an IDE-behaviour change, not only a CLI change.
2. What changed since #13880
#13880 was closed with four objections. Where they stand in 2026:
FSharp.Compiler.Servicetakes no new dependency; the adapter lives in thefsc/fsiexecutables only.Also new:
System.CommandLinereached 2.0.0 GA in Oct 2025 after years in beta (the churnthat made it unattractive in 2022 is over), and @baronfel's 2022 point about standardising on it
across the .NET stack still holds.
3. Compatibility surface (measured, not assumed)
I built a probe against
System.CommandLine2.0.11 (highest stable) targetingnet472andmodern .NET, and exercised the syntax forms the F# compiler accepts today:
System.CommandLine2.0.11--flag:value--define:FOO--flag=value--define=FOO--flag value--warn 3-d:FOO/define:FOO,/optimize@args.rsp--passthroughfsi a.fsx -- x y+/-switch suffix--optimize+,--debug---bogusTwo real gaps:
+/-suffixes (26 flags).--optimize+is not parsed; it falls through as a positionalargument.
--optimize:true/--optimize:falsedo work, so this is solvable with a smallargv pre-normalisation pass (
--opt+→--opt:true,--opt-→--opt:false) driven by theknown set of switch flags. This replaces
PostProcessCompilerArgs/GetAbbrevFlagSetratherthan adding to them.
ZeroOrMorepositional argument for source files they are silently collected. Needs an explicit validator
so
fsc --bogus a.fsstill fails, whilea.fsis still accepted as a file.Everything else — including the colon separator, which was my main worry — already works.
Not yet measured, and needed before committing: startup cost.
fsc/fsistartup isperformance-sensitive and there is R2R/MIBC work in the build (
fscProject/fsc.fsproj); loadingand constructing a ~185-option
System.CommandLinetree must be benchmarked against the currentparser before this is accepted.
4. Variant A — plain
System.CommandLineKey design point: keep
CompilerOptions.fsas the single source of truth and add theSystem.CommandLinelayer only in the executables.FSharp.Compiler.Servicealready grantsInternalsVisibleTotofsc,fscAnyCpu,fscArm64,fsi,fsiAnyCpu,fsiArm64(
FSharp.Compiler.Service.fsproj:85-90),so the exes can consume the internal
CompilerOptionmodel directly.The adapter is mechanical: each
OptionSpeccase maps to anOption<T>whose action invokes theexisting setter closure, so
TcConfigBuildermutation stays exactly where it is today.OptionSpecOptionUnit/OptionSet/OptionClearOption<bool>(no arg)OptionString/OptionInt/OptionFloatOption<string/int/float>OptionStringList/OptionIntListOption<T[]>,AllowMultipleArgumentsPerTokenOptionSwitchOption<bool>+ argv normalisation for+/-OptionStringListSwitch/OptionIntListSwitchOption<T[]>+ same normalisationOptionRestArgument<string[]>OptionConsoleOnlySystem.CommandLinehelp/version actionsOptionGeneral(1 use, FSIscript.fsx arg1 …)Actionon the root commandPros: no third-party dependency; Microsoft-supported; matches @baronfel's and
@vzarytovskii's stated preference in #13880; completions and a maintained help renderer come free;
FCS/Fable untouched.
Cons: the C#-shaped API is verbose from F# (
Option<T>construction,ParseResult.GetValue);help output changes, so both
.bslbaselines need rebaselining and the localised.xlfstringsneed re-review against
System.CommandLine's own localised chrome ("Usage:", "Options:").5. Variant B —
FSharp.SystemCommandLineFSharp.SystemCommandLine(MIT,Jordan Marr) is a thin F# wrapper over
System.CommandLine, offering a CE-based API:Version facts (verified against nuget.org):
netstandard2.0andnet6.0— sonet472is covered.System.CommandLine >= 2.0.0; I verified it builds and runs correctly against2.0.11, the highest stable
System.CommandLine, on bothnet472and modern .NET. So"use the newest possible
System.CommandLine" is compatible with this wrapper today.System.CommandLine3.0.0 is preview-only; a major-version bump against a wrapper compiledfor 2.x should be assumed binary-breaking until proven otherwise.
Pros: substantially less ceremony; strongly-typed tupled inputs; idiomatic F#.
Cons — and these are decisive against using it inside this repo:
on it puts the F# compiler's CLI on a third-party release cadence — exactly what
@vzarytovskii argued against in Investigate the options of migrating to some library for parsing cmd line args #13880 ("I'd vote for not using anything 3rd-party").
inputsAPI is arity-limited (tuples). It is designed for tools with a handful ofoptions, not ~185, so the compiler would end up bypassing the ergonomic layer anyway and
driving
System.CommandLinedirectly — losing the benefit that motivates the dependency.Recommendation: Variant A. Variant B is documented here because it was explicitly asked
about, and it remains an excellent choice for F# tooling in general — just not for the compiler
itself, where the option count defeats its ergonomics and the third-party dependency is the very
thing #13880 objected to.
6. Suggested phasing
Each phase is independently shippable and revertible.
form in section 3 × the option kinds, including the ugly ones (
-d 5space-separatedabbreviation,
/flag,--flag+,@rsp,--infsi). This is the real deliverable even ifthe migration is declined — it is what makes fsi.CommandLineArgs different behavior on -d/-r/-I args, ignores --. #10819/dotnet fsi command line: -- has no effect #18983/fsi incorrect handling of some script arguments #19214-class bugs regression-proof.
CompilerOption list -> RootCommandplus argv normalisation, insrc/fsc/src/fsionly. No behaviour change yet; parse with both parsers and assert theresulting
TcConfigBuilderstate matches, across the test corpus.fsi.exeto the new front end first — smaller surface, and the FSI-specificoptions (
--use,--load,--exec,--gui,--readline,--fsi-server*, scriptpassthrough) are where the known bugs are. Rebaseline
expected-help-output.bsl.fsc.exe. Rebaselinecompiler_help_output.bsl; re-review the 13.xlflocalesfor the help chrome that
System.CommandLinenow owns.ParseCompilerOptions— or migrate it separately anddeliberately, since it is IDE-visible and has no CLI-compat requirement to honour.
dotnet fsi/dotnet fscshell completions, now essentially free.7. Open questions
fsc? (Must bebenchmarked in phase 2; a negative result should kill the proposal.)
--helpoutput acceptable, given it is baselined and localised into 13 languages?ApplyCommandLineArgs) ever move, or stay on the currentparser permanently?
System.CommandLineto the executables genuinely resolve the Fable concern fromInvestigate the options of migrating to some library for parsing cmd line args #13880, or does Fable also consume the
fscentry point?I'm willing to do the work, starting with phase 1 (characterisation tests), which has standalone
value regardless of the outcome. But given #13880 I'd like a maintainer signal before writing the
adapter — happy to have this closed again if the trade-off hasn't changed.