From e827557517f4ed7b4bbaf2d3e1b6e909cc8dd6c6 Mon Sep 17 00:00:00 2001 From: dimension-zero Date: Sat, 23 May 2026 18:26:39 +0100 Subject: [PATCH 1/5] feat(SourceGenerator): Scaffold + [] marker attribute * New project src/Argu.SourceGenerator (netstandard2.0): publishes the [] marker attribute. Today the marker is inert; a follow-up release will ship the actual generator that consumes it to emit a compile-time-built schema (bypassing Argu's reflection path). README.md documents the planned design and roadmap. * New sample samples/Argu.Samples.SourceGenerated: shows the opt-in shape. The sample currently runs through Argu's standard reflection path; once the generator lands, the same SampleArgs annotated with [] will use the generated factory automatically. * Argu.sln: register both new projects. The sample is nested under the existing 'samples' folder; the SourceGenerator library is a sibling of Argu. Strictly additive. No change to the core Argu package's public surface, behaviour, or dependency graph. # Conflicts: # Argu.slnx --- Argu.slnx | 2 + .../Argu.Samples.SourceGenerated.fsproj | 17 +++++++ .../Argu.Samples.SourceGenerated/Program.fs | 32 ++++++++++++++ .../Argu.SourceGenerator.fsproj | 18 ++++++++ src/Argu.SourceGenerator/AttributeMarkers.fs | 21 +++++++++ src/Argu.SourceGenerator/README.md | 44 +++++++++++++++++++ 6 files changed, 134 insertions(+) create mode 100644 samples/Argu.Samples.SourceGenerated/Argu.Samples.SourceGenerated.fsproj create mode 100644 samples/Argu.Samples.SourceGenerated/Program.fs create mode 100644 src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj create mode 100644 src/Argu.SourceGenerator/AttributeMarkers.fs create mode 100644 src/Argu.SourceGenerator/README.md diff --git a/Argu.slnx b/Argu.slnx index 5e6e8a97..aa73b703 100644 --- a/Argu.slnx +++ b/Argu.slnx @@ -40,8 +40,10 @@ + + diff --git a/samples/Argu.Samples.SourceGenerated/Argu.Samples.SourceGenerated.fsproj b/samples/Argu.Samples.SourceGenerated/Argu.Samples.SourceGenerated.fsproj new file mode 100644 index 00000000..b3e0df7c --- /dev/null +++ b/samples/Argu.Samples.SourceGenerated/Argu.Samples.SourceGenerated.fsproj @@ -0,0 +1,17 @@ + + + Exe + net10.0 + false + + + + + + + + + + + + diff --git a/samples/Argu.Samples.SourceGenerated/Program.fs b/samples/Argu.Samples.SourceGenerated/Program.fs new file mode 100644 index 00000000..6d0d9dfd --- /dev/null +++ b/samples/Argu.Samples.SourceGenerated/Program.fs @@ -0,0 +1,32 @@ +module Argu.Samples.SourceGenerated.Program + +open Argu +open Argu.SourceGenerator + +/// CLI template annotated with the source-generator marker. Today +/// nothing is generated; the marker is inert and Argu's reflection +/// path still computes the schema at runtime. When the generator +/// ships, this same template will gain a compile-time-built schema +/// and stop calling Activator.CreateInstance. +[] +type SampleArgs = + | [] Port of int + | Verbose + interface IArgParserTemplate with + member this.Usage = + match this with + | Port _ -> "port to bind to" + | Verbose -> "verbose output" + +[] +let main argv = + let parser = ArgumentParser.Create(programName = "demo") + let results = parser.Parse(argv, ignoreMissing = true, raiseOnUsage = false) + + match results.TryGetResult(Port) with + | Some p -> printfn "port: %d" p + | None -> printfn "port: (unspecified)" + + if results.Contains(Verbose) then printfn "verbose: true" + + 0 diff --git a/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj b/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj new file mode 100644 index 00000000..a540a8a8 --- /dev/null +++ b/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj @@ -0,0 +1,18 @@ + + + netstandard2.0 + true + true + Marker attributes and (planned) source generator for compile-time Argu schema emission. + F#, argument, commandline, parser, source-generator, aot + + + + + + + + + + + diff --git a/src/Argu.SourceGenerator/AttributeMarkers.fs b/src/Argu.SourceGenerator/AttributeMarkers.fs new file mode 100644 index 00000000..820173ba --- /dev/null +++ b/src/Argu.SourceGenerator/AttributeMarkers.fs @@ -0,0 +1,21 @@ +namespace Argu.SourceGenerator + +open System + +/// +/// Marker attribute used by the (planned) Argu source generator. +/// Apply to a CLI template discriminated union to opt that type into +/// compile-time schema generation. The companion generator (shipped +/// separately) reads this attribute and emits an +/// ArgumentParser<'T> factory that does not require +/// reflection at runtime — required for publish-AOT scenarios. +/// +/// +/// This release ships the marker only. The generator that consumes +/// it is intentionally out of scope for the initial scaffold; the +/// marker is published so that downstream code can be annotated +/// ahead of the generator landing. +/// +[] +type ArguGenerateAttribute() = + inherit Attribute() diff --git a/src/Argu.SourceGenerator/README.md b/src/Argu.SourceGenerator/README.md new file mode 100644 index 00000000..7f1bb14b --- /dev/null +++ b/src/Argu.SourceGenerator/README.md @@ -0,0 +1,44 @@ +# Argu.SourceGenerator + +Companion package that establishes the *seam* for compile-time Argu schema generation. The marker attribute ships now; the generator that consumes it is planned in a follow-up release. + +## Why + +Argu's runtime schema computation uses reflection (`FSharpType.GetUnionCases`, `MakeGenericType` + `Activator.CreateInstance`). This is incompatible with publish-AOT, where `Activator.CreateInstance` over a runtime-built generic type cannot be JIT-compiled. The reflection path also slows startup for large CLI templates. + +A source generator, given an annotated DU, can emit: + +- A frozen `UnionArgInfo` record built from compile-time-known metadata +- A `Create`-equivalent factory that skips the reflection walk + +Consumers opt in per template: + +```fsharp +open Argu.SourceGenerator + +[] +type Args = + | [] Port of int + | Verbose + interface IArgParserTemplate with + member this.Usage = ... +``` + +## Status + +| Component | State | +|---|---| +| `[]` marker | Shipped | +| Generator producing schema | Planned | +| Companion `ArgumentParser` factory consuming the schema | Planned | + +## Why a separate package + +Keeping the marker, the generator, and the runtime-bypass factory in a separate NuGet package preserves the core `Argu` package's zero-dependency footprint. Users who don't need AOT see no churn. + +## Roadmap + +1. Marker + scaffold (this PR) +2. F# source generator (Roslyn FSharp.Compiler.Service or external tool like Myriad) emitting a `precomputeSchemaFor<'T>()` function for the simplest schemas (no subcommands, no custom attributes) +3. Companion `ArgumentParser.CreateFromPrecomputed<'T>` overload that bypasses the reflection path +4. Expand generator coverage to subcommands, custom assignment attributes, inheritance and aliasing From 5d2afc8fe93ec5e62960018cfc2e7d7ff10d78ee Mon Sep 17 00:00:00 2001 From: dimension-zero <127850950+dimension-zero@users.noreply.github.com> Date: Sun, 7 Jun 2026 08:39:29 +0100 Subject: [PATCH 2/5] chore(SourceGenerator): packaging hygiene per Copilot review - AttributeMarkers.fs: drop "(shipped separately)" wording, which was inconsistent with the noting the generator is intentionally out of scope for this release. - Argu.SourceGenerator.fsproj: - Set README.md so NuGet clients render the README as the package readme. - Add to match the core Argu package and produce reproducible nupkg metadata. - Change PackagePath="\" to "" for the README pack item to match the existing pattern in src/Argu/Argu.fsproj. --- src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj | 5 ++++- src/Argu.SourceGenerator/AttributeMarkers.fs | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj b/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj index a540a8a8..e34a177b 100644 --- a/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj +++ b/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj @@ -5,14 +5,17 @@ true Marker attributes and (planned) source generator for compile-time Argu schema emission. F#, argument, commandline, parser, source-generator, aot + README.md - + + + diff --git a/src/Argu.SourceGenerator/AttributeMarkers.fs b/src/Argu.SourceGenerator/AttributeMarkers.fs index 820173ba..8d708768 100644 --- a/src/Argu.SourceGenerator/AttributeMarkers.fs +++ b/src/Argu.SourceGenerator/AttributeMarkers.fs @@ -5,10 +5,10 @@ open System /// /// Marker attribute used by the (planned) Argu source generator. /// Apply to a CLI template discriminated union to opt that type into -/// compile-time schema generation. The companion generator (shipped -/// separately) reads this attribute and emits an -/// ArgumentParser<'T> factory that does not require -/// reflection at runtime — required for publish-AOT scenarios. +/// compile-time schema generation. The planned companion generator +/// will read this attribute and emit an ArgumentParser<'T> +/// factory that does not require reflection at runtime — required +/// for publish-AOT scenarios. /// /// /// This release ships the marker only. The generator that consumes From a6194f48ccab51df498382ac22e6a3536f549c32 Mon Sep 17 00:00:00 2001 From: dimension-zero <127850950+dimension-zero@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:52:24 +0100 Subject: [PATCH 3/5] docs(SourceGenerator): Rewrite ArguGenerate doc comment per review Addresses review feedback on the AttributeMarkers.fs doc comment: - now states the attribute's purpose in final form (no "planned" / "future" wording), terse, and calls out the key consideration a user must account for: the union and its Argu attributes must be statically resolvable, since the generator reads the type at compile time. - carries the caveat that the generator does not exist yet and that the marker can be applied now for a no-source-change benefit later. - README intro reworded into plainer English describing what the package does now versus later. --- src/Argu.SourceGenerator/AttributeMarkers.fs | 23 +++++++++++--------- src/Argu.SourceGenerator/README.md | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Argu.SourceGenerator/AttributeMarkers.fs b/src/Argu.SourceGenerator/AttributeMarkers.fs index 8d708768..595b6292 100644 --- a/src/Argu.SourceGenerator/AttributeMarkers.fs +++ b/src/Argu.SourceGenerator/AttributeMarkers.fs @@ -3,18 +3,21 @@ namespace Argu.SourceGenerator open System /// -/// Marker attribute used by the (planned) Argu source generator. -/// Apply to a CLI template discriminated union to opt that type into -/// compile-time schema generation. The planned companion generator -/// will read this attribute and emit an ArgumentParser<'T> -/// factory that does not require reflection at runtime — required -/// for publish-AOT scenarios. +/// Opts an Argu CLI template into compile-time schema generation: its +/// ArgumentParser<'T> is then built without runtime reflection, +/// which is what makes publish-AOT viable and removes the reflection cost +/// from startup. +/// +/// Apply to a discriminated union that implements IArgParserTemplate. +/// The union's cases and their Argu attributes must be resolvable at compile +/// time — the generator reads the type statically, so a template whose shape +/// is built or varied at runtime cannot be generated for. /// /// -/// This release ships the marker only. The generator that consumes -/// it is intentionally out of scope for the initial scaffold; the -/// marker is published so that downstream code can be annotated -/// ahead of the generator landing. +/// The generator that consumes this attribute does not exist yet. This +/// package currently ships the marker alone, so applying it has no build-time +/// effect beyond recording intent. Code annotated now gains reflection-free +/// parsing once the generator lands, with no further source changes. /// [] type ArguGenerateAttribute() = diff --git a/src/Argu.SourceGenerator/README.md b/src/Argu.SourceGenerator/README.md index 7f1bb14b..b6bf67d7 100644 --- a/src/Argu.SourceGenerator/README.md +++ b/src/Argu.SourceGenerator/README.md @@ -1,6 +1,6 @@ # Argu.SourceGenerator -Companion package that establishes the *seam* for compile-time Argu schema generation. The marker attribute ships now; the generator that consumes it is planned in a follow-up release. +Companion package for compile-time Argu schema generation. Right now it ships only the `[]` marker attribute; the generator that consumes it will follow in a later release. ## Why From 18cd08abf7d2e40dacc09b69ee982edf05bf1ed8 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 8 Jun 2026 11:17:27 +0100 Subject: [PATCH 4/5] CL --- RELEASE_NOTES.md | 5 +++-- src/Argu.SourceGenerator/README.md | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d90b7c89..58eaa7ca 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,11 +4,12 @@ * Fix Report all missing args in error message, not just first level [#297](https://github.com/fsprojects/Argu/pull/297) [@dimension-zero](https://github.com/dimension-zero) * Fix Limit min wordwrap column to 20 [#302](https://github.com/fsprojects/Argu/pull/302) [@dimension-zero](https://github.com/dimension-zero) * Fix [misrendering of usage when help blank](https://github.com/fsprojects/Argu/issues/173) [#323](https://github.com/fsprojects/Argu/pull/323) [@DominikL1999](https://github.com/DominikL1999) -* Add `Separator("=", orSpace = true)` and `Separator "="` attribute syntax to replace `Obsoleted` `EqualsAssignment`, `ColonAssignment`, `CustomAssignment`, `EqualsAssignmentOrSpaced`, `ColonAssignmentOrSpaced` and `CustomAssignmentOrSpaced` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero) +* Add `Separator("=", orSpace = true)` and `Separator "="` attribute syntax to replace `Obsolete`d `EqualsAssignment`, `ColonAssignment`, `CustomAssignment`, `EqualsAssignmentOrSpaced`, `ColonAssignmentOrSpaced` and `CustomAssignmentOrSpaced` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero) * Add `Argu.Samples.Introspect` sample [#298](https://github.com/fsprojects/Argu/pull/298) [@dimension-zero](https://github.com/dimension-zero) * Add `ArgumentParser.Parse(ParseConfig)` [#307](https://github.com/fsprojects/Argu/pull/307) [@dimension-zero](https://github.com/dimension-zero) * Add `ArgumentParser.PrintUsage(..., ?UsageStrings)` for localization support [#303](https://github.com/fsprojects/Argu/pull/303) [@dimension-zero](https://github.com/dimension-zero) -* Add AOT annotations on [#314](https://github.com/fsprojects/Argu/pull/314) [@dimension-zero](https://github.com/dimension-zero) +* Add AOT annotations [#314](https://github.com/fsprojects/Argu/pull/314) [@dimension-zero](https://github.com/dimension-zero) +* Add `SourceGenerator.ArguGenerate` marker [#318](https://github.com/fsprojects/Argu/pull/318) [@dimension-zero](https://github.com/dimension-zero) * Obsolete `EqualsAssignmentAttribute`, `ColonAssignmentAttribute`, `CustomAssignmentAttribute`, `EqualsAssignmentOrSpacedAttribute`, `ColonAssignmentOrSpacedAttribute` and `CustomAssignmentOrSpacedAttribute` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero) * Obsolete `PostProcessResult`, `PostProcessResults`, `TryPostProcessResult` [#296](https://github.com/fsprojects/Argu/pull/296) [@dimension-zero](https://github.com/dimension-zero) diff --git a/src/Argu.SourceGenerator/README.md b/src/Argu.SourceGenerator/README.md index b6bf67d7..33622dd9 100644 --- a/src/Argu.SourceGenerator/README.md +++ b/src/Argu.SourceGenerator/README.md @@ -34,11 +34,11 @@ type Args = ## Why a separate package -Keeping the marker, the generator, and the runtime-bypass factory in a separate NuGet package preserves the core `Argu` package's zero-dependency footprint. Users who don't need AOT see no churn. +Keeping the marker, the generator, and the runtime-bypass factory in a separate NuGet package preserves the core `Argu` package's zero-dependency footprint. Users that don't need AOT see no churn. ## Roadmap 1. Marker + scaffold (this PR) -2. F# source generator (Roslyn FSharp.Compiler.Service or external tool like Myriad) emitting a `precomputeSchemaFor<'T>()` function for the simplest schemas (no subcommands, no custom attributes) +2. F# source generator (Roslyn `FSharp.Compiler.Service` or external tool like Myriad) emitting a `precomputeSchemaFor<'T>()` function for the simplest schemas (no subcommands, no custom attributes) 3. Companion `ArgumentParser.CreateFromPrecomputed<'T>` overload that bypasses the reflection path 4. Expand generator coverage to subcommands, custom assignment attributes, inheritance and aliasing From 30c1e6ba57f78007e451c934011b228547564683 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 8 Jun 2026 11:20:31 +0100 Subject: [PATCH 5/5] Collapse folders --- Argu.slnx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Argu.slnx b/Argu.slnx index aa73b703..4c0db3e8 100644 --- a/Argu.slnx +++ b/Argu.slnx @@ -1,7 +1,4 @@ - - - @@ -10,6 +7,7 @@ + @@ -37,13 +35,11 @@ - - - - - + + + - +