diff --git a/Argu.slnx b/Argu.slnx index 5e6e8a97..4c0db3e8 100644 --- a/Argu.slnx +++ b/Argu.slnx @@ -1,7 +1,4 @@ - - - @@ -10,6 +7,7 @@ + @@ -37,11 +35,11 @@ - - - - + + + - + + 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/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..e34a177b --- /dev/null +++ b/src/Argu.SourceGenerator/Argu.SourceGenerator.fsproj @@ -0,0 +1,21 @@ + + + netstandard2.0 + true + 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 new file mode 100644 index 00000000..595b6292 --- /dev/null +++ b/src/Argu.SourceGenerator/AttributeMarkers.fs @@ -0,0 +1,24 @@ +namespace Argu.SourceGenerator + +open System + +/// +/// 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. +/// +/// +/// 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() = + inherit Attribute() diff --git a/src/Argu.SourceGenerator/README.md b/src/Argu.SourceGenerator/README.md new file mode 100644 index 00000000..33622dd9 --- /dev/null +++ b/src/Argu.SourceGenerator/README.md @@ -0,0 +1,44 @@ +# Argu.SourceGenerator + +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 + +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 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) +3. Companion `ArgumentParser.CreateFromPrecomputed<'T>` overload that bypasses the reflection path +4. Expand generator coverage to subcommands, custom assignment attributes, inheritance and aliasing