Skip to content

Commit d034914

Browse files
Smaug123claude
andcommitted
ArgParser: add [<ArgumentDefaultValue foo>] for constant defaults
Shorthand for the common case where an [<ArgumentDefaultFunction>]'s function would just return a constant. The attribute argument is a SynExpr spliced verbatim into the generated code, exactly as [<ArgumentFlag>]'s value already is; the generated file inherits the source file's `open`s, so an identifier resolves there as it does at the use site. Because `foo` is an ordinary .NET custom-attribute argument, F# itself restricts it to compile-time constants (literals, [<Literal>] bindings, enum cases) on the user's own source line, so the generator's untyped pass need not validate it. A discriminated union case, including a flag DU's, is not such a constant and still needs [<ArgumentDefaultFunction>]. The new attribute joins the existing "at most one default source" check, which until now had no test coverage at all; it is now exercised over every pair drawn from the three default-supplying attributes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ed47edd commit d034914

11 files changed

Lines changed: 479 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
Notable changes are recorded here.
22

3+
# WoofWare.Myriad.Plugins 10.4.1
4+
5+
The `ArgParserGenerator` gains `[<ArgumentDefaultValue foo>]`: shorthand for an `[<ArgumentDefaultFunction>]` whose function just returns a constant.
6+
`foo` is an ordinary .NET attribute argument, so F# restricts it to compile-time constants (literals, `[<Literal>]` bindings, and enum cases); anything else, including a discriminated union case, still needs `[<ArgumentDefaultFunction>]`.
7+
38
# WoofWare.Myriad.Plugins 10.3.1
49

510
The `ArgParserGenerator` now supports positional args together with arbitrary discriminated-union args.

ConsumePlugin/Args.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,22 @@ type ContainsFlagDefaultValue =
196196

197197
static member DefaultDryRun () = DryRunMode.Wet
198198

199+
/// `[<ArgumentDefaultValue>]` is shorthand for an `[<ArgumentDefaultFunction>]` whose function
200+
/// returns a constant. The value is any valid .NET attribute argument: a literal, a `[<Literal>]`
201+
/// binding, or an enum case.
202+
[<ArgParser true>]
203+
type ContainsLiteralDefault =
204+
{
205+
[<ArgumentDefaultValue 3>]
206+
IntVar : Choice<int, int>
207+
[<ArgumentDefaultValue "hello world">]
208+
StringVar : Choice<string, string>
209+
// A non-literal argument requires parens: F#'s attribute syntax otherwise reads the
210+
// identifier as the start of a named argument.
211+
[<ArgumentDefaultValue(Consts.TRUE)>]
212+
BoolVar : Choice<bool, bool>
213+
}
214+
199215
[<ArgParser true>]
200216
type ManyLongForms =
201217
{

ConsumePlugin/GeneratedArgs.fs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4772,6 +4772,229 @@ open System
47724772
open System.IO
47734773
open WoofWare.Myriad.Plugins
47744774

4775+
/// Methods to parse arguments for the type ContainsLiteralDefault
4776+
[<AutoOpen>]
4777+
module ContainsLiteralDefaultArgParse =
4778+
/// Extension methods for argument parsing
4779+
type ContainsLiteralDefault with
4780+
4781+
static member parse'
4782+
(getEnvironmentVariable : string -> string option)
4783+
(args : string list)
4784+
: ContainsLiteralDefault
4785+
=
4786+
let helpText () =
4787+
[
4788+
(sprintf
4789+
"%s %s%s%s"
4790+
(sprintf "--%s" "int-var")
4791+
"int32"
4792+
((3).ToString () |> sprintf " (default value: %s)")
4793+
"")
4794+
4795+
(sprintf
4796+
"%s %s%s%s"
4797+
(sprintf "--%s" "string-var")
4798+
"string"
4799+
(("hello world").ToString () |> sprintf " (default value: %s)")
4800+
"")
4801+
(sprintf
4802+
"%s %s%s%s"
4803+
(sprintf "--%s" "bool-var")
4804+
"bool"
4805+
(((Consts.TRUE)).ToString () |> sprintf " (default value: %s)")
4806+
"")
4807+
]
4808+
|> String.concat "\n"
4809+
4810+
let parser_LeftoverArgs : string ResizeArray = ResizeArray ()
4811+
let mutable arg_0 : Choice<int, int> option = None
4812+
let mutable arg_1 : Choice<string, string> option = None
4813+
let mutable arg_2 : Choice<bool, bool> option = None
4814+
4815+
let parser_schema : ArgParserRuntime_BasicNoPositionals.ErasedSchema =
4816+
{
4817+
Leaves =
4818+
[
4819+
{
4820+
Id = 0
4821+
Forms = [ "int-var" ]
4822+
AcceptsNegation = false
4823+
Arity = ArgParserRuntime_BasicNoPositionals.ErasedArity.One
4824+
Repeatable = false
4825+
Requirement = ArgParserRuntime_BasicNoPositionals.ErasedRequirement.HasDefault
4826+
TypeDescription = ""
4827+
Help = None
4828+
}
4829+
4830+
{
4831+
Id = 1
4832+
Forms = [ "string-var" ]
4833+
AcceptsNegation = false
4834+
Arity = ArgParserRuntime_BasicNoPositionals.ErasedArity.One
4835+
Repeatable = false
4836+
Requirement = ArgParserRuntime_BasicNoPositionals.ErasedRequirement.HasDefault
4837+
TypeDescription = ""
4838+
Help = None
4839+
}
4840+
{
4841+
Id = 2
4842+
Forms = [ "bool-var" ]
4843+
AcceptsNegation = false
4844+
Arity = ArgParserRuntime_BasicNoPositionals.ErasedArity.BoolLike
4845+
Repeatable = false
4846+
Requirement = ArgParserRuntime_BasicNoPositionals.ErasedRequirement.HasDefault
4847+
TypeDescription = ""
4848+
Help = None
4849+
}
4850+
]
4851+
Tree =
4852+
(ArgParserRuntime_BasicNoPositionals.ErasedTree.Product (
4853+
[
4854+
ArgParserRuntime_BasicNoPositionals.ErasedTree.Leaf 0
4855+
ArgParserRuntime_BasicNoPositionals.ErasedTree.Leaf 1
4856+
ArgParserRuntime_BasicNoPositionals.ErasedTree.Leaf 2
4857+
]
4858+
))
4859+
Positionals = List.empty
4860+
}
4861+
4862+
let parser_storeOccurrence
4863+
(occurrence : ArgParserRuntime_BasicNoPositionals.ErasedOccurrence)
4864+
: string option
4865+
=
4866+
match occurrence.LeafId with
4867+
| 0 ->
4868+
match arg_0 with
4869+
| Some _ -> None
4870+
| None ->
4871+
match occurrence.Value with
4872+
| Some value ->
4873+
try
4874+
arg_0 <- Some (Choice1Of2 (value |> (fun x -> System.Int32.Parse x)))
4875+
None
4876+
with _ as exc ->
4877+
(sprintf "%s (at arg %s)" exc.Message occurrence.Source) |> Some
4878+
| None ->
4879+
failwith
4880+
"WoofWare.Myriad internal error in generated parser: arity-one occurrence with no value"
4881+
| 1 ->
4882+
match arg_1 with
4883+
| Some _ -> None
4884+
| None ->
4885+
match occurrence.Value with
4886+
| Some value ->
4887+
try
4888+
arg_1 <- Some (Choice1Of2 (value |> (fun x -> x)))
4889+
None
4890+
with _ as exc ->
4891+
(sprintf "%s (at arg %s)" exc.Message occurrence.Source) |> Some
4892+
| None ->
4893+
failwith
4894+
"WoofWare.Myriad internal error in generated parser: arity-one occurrence with no value"
4895+
| 2 ->
4896+
match arg_2 with
4897+
| Some _ -> None
4898+
| None ->
4899+
match occurrence.Value with
4900+
| Some value ->
4901+
try
4902+
let parsedBool = System.Boolean.Parse value
4903+
let parsedBool = if occurrence.Negated then not parsedBool else parsedBool
4904+
arg_2 <- Some (Choice1Of2 (parsedBool))
4905+
None
4906+
with _ as exc ->
4907+
(sprintf "%s (at arg %s)" exc.Message occurrence.Source) |> Some
4908+
| None ->
4909+
arg_2 <- Some (Choice1Of2 ((if occurrence.Negated then false else true)))
4910+
None
4911+
| _ -> failwith "WoofWare.Myriad internal error in generated parser: unknown argument id"
4912+
4913+
let parser_storePositional (positionalId : int) (value : string) (afterSeparator : bool) : string option =
4914+
failwith "WoofWare.Myriad internal error in generated parser: no positional sink exists"
4915+
4916+
let parser_renderStored (leafId : int) : string =
4917+
match leafId with
4918+
| 0 ->
4919+
match arg_0 with
4920+
| Some (Choice1Of2 x) -> x.ToString ()
4921+
| Some (Choice2Of2 x) -> x.ToString ()
4922+
| None -> "<no value>"
4923+
| 1 ->
4924+
match arg_1 with
4925+
| Some (Choice1Of2 x) -> x.ToString ()
4926+
| Some (Choice2Of2 x) -> x.ToString ()
4927+
| None -> "<no value>"
4928+
| 2 ->
4929+
match arg_2 with
4930+
| Some (Choice1Of2 x) -> x.ToString ()
4931+
| Some (Choice2Of2 x) -> x.ToString ()
4932+
| None -> "<no value>"
4933+
| _ -> "<no value>"
4934+
4935+
let parser_applyDefault (leafId : int) : string option =
4936+
match leafId with
4937+
| 0 ->
4938+
arg_0 <- Some (Choice2Of2 ((3)))
4939+
None
4940+
| 1 ->
4941+
arg_1 <- Some (Choice2Of2 (("hello world")))
4942+
None
4943+
| 2 ->
4944+
arg_2 <- Some (Choice2Of2 (((Consts.TRUE))))
4945+
None
4946+
| _ -> failwith "WoofWare.Myriad internal error in generated parser: unknown defaulted argument id"
4947+
4948+
let parser_callbacks : ArgParserRuntime_BasicNoPositionals.TypedCallbacks =
4949+
{
4950+
StoreOccurrence = parser_storeOccurrence
4951+
StorePositional = parser_storePositional
4952+
HelpText = helpText
4953+
RenderStored = parser_renderStored
4954+
ApplyDefault = parser_applyDefault
4955+
}
4956+
4957+
match
4958+
ArgParserRuntime_BasicNoPositionals.runParse
4959+
(ArgParserRuntime_BasicNoPositionals.WellFormedSchema.checkOrFail parser_schema)
4960+
parser_callbacks
4961+
args
4962+
with
4963+
| ArgParserRuntime_BasicNoPositionals.ParseOutcome.Success parser_selection ->
4964+
{
4965+
BoolVar =
4966+
(match arg_2 with
4967+
| Some x -> x
4968+
| None ->
4969+
failwith
4970+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
4971+
IntVar =
4972+
(match arg_0 with
4973+
| Some x -> x
4974+
| None ->
4975+
failwith
4976+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
4977+
StringVar =
4978+
(match arg_1 with
4979+
| Some x -> x
4980+
| None ->
4981+
failwith
4982+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
4983+
}
4984+
| ArgParserRuntime_BasicNoPositionals.ParseOutcome.HelpRequested ->
4985+
helpText () |> failwithf "Help text requested.\n%s"
4986+
| ArgParserRuntime_BasicNoPositionals.ParseOutcome.Fatal message -> failwith message
4987+
| ArgParserRuntime_BasicNoPositionals.ParseOutcome.Errors errors ->
4988+
errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
4989+
4990+
static member parse (args : string list) : ContainsLiteralDefault =
4991+
ContainsLiteralDefault.parse' (System.Environment.GetEnvironmentVariable >> Option.ofObj) args
4992+
namespace ConsumePlugin
4993+
4994+
open System
4995+
open System.IO
4996+
open WoofWare.Myriad.Plugins
4997+
47754998
/// Methods to parse arguments for the type ManyLongForms
47764999
[<AutoOpen>]
47775000
module ManyLongFormsArgParse =

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ type Foo =
186186
B : Choice<int, int>
187187
[<ArgumentDefaultEnvironmentVariable "MY_ENV_VAR">]
188188
BWithEnv : Choice<int, int>
189+
[<ArgumentDefaultValue 4>]
190+
BWithConstant : Choice<int, int>
189191
[<ArgumentDefaultFunction>]
190192
DryRun : Choice<DryRunMode, DryRunMode>
191193
AnimalPart : AnimalArgs
@@ -225,6 +227,7 @@ and you get back respectively these objects:
225227
A = None
226228
B = Choice2Of2 4
227229
BWithEnv = Choice2Of2 100 // whatever the value of $MY_ENV_VAR was, or a failed parse
230+
BWithConstant = Choice2Of2 4
228231
DryRun = Choice2Of2 DryRunMode.Wet
229232
AnimalPart = AnimalArgs.Fowl { Species = "pheasant" }
230233
}
@@ -234,6 +237,7 @@ and you get back respectively these objects:
234237
A = None
235238
B = Choice2Of2 4
236239
BWithEnv = Choice1Of2 8
240+
BWithConstant = Choice2Of2 4
237241
DryRun = Choice1Of2 DryRunMode.Dry
238242
AnimalPart = AnimalArgs.Fish { Fins = 39 }
239243
}
@@ -242,6 +246,11 @@ and you get back respectively these objects:
242246
Default arguments are handled as `Choice<'a, 'a>`:
243247
you get a `Choice1Of2` if the user provided the input, or a `Choice2Of2` if the parser filled in your specified default value.
244248

249+
`[<ArgumentDefaultValue foo>]` is shorthand for an `[<ArgumentDefaultFunction>]` whose function just returns a constant.
250+
Since `foo` is an ordinary .NET attribute argument, F# restricts it to compile-time constants: literals, `[<Literal>]` bindings, and enum cases.
251+
(A discriminated union case, such as a flag DU's, is not one of those, so those still need `[<ArgumentDefaultFunction>]`.)
252+
Note that F# requires parentheses around an attribute argument which is not a bare literal: `[<ArgumentDefaultValue(Consts.Foo)>]`, but `[<ArgumentDefaultValue 4>]`.
253+
245254
You can control `TimeSpan` and friends with the `[<InvariantCulture>]` and `[<ParseExact @"hh\:mm\:ss">]` attributes.
246255

247256
You can generate extension methods for the type, instead of a module with the type's name, using `[<ArgParser (* isExtensionMethod = *) true>]`.

WoofWare.Myriad.Plugins.Attributes/ArgParserAttributes.fs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ type ArgumentDefaultFunctionAttribute () =
6262
type ArgumentDefaultEnvironmentVariableAttribute (envVar : string) =
6363
inherit Attribute ()
6464

65+
/// Attribute indicating that this field shall have the given constant default value.
66+
/// This is shorthand for `[<ArgumentDefaultFunction>]` in the common case where the default function
67+
/// would simply return a constant.
68+
///
69+
/// This attribute can only be placed on fields of type `Choice<_, _>` where both type parameters
70+
/// are the same.
71+
/// After a successful parse, the value is Choice1Of2 if the user supplied an input,
72+
/// or Choice2Of2 if the input was this constant.
73+
///
74+
/// The value you supply is spliced verbatim into the generated code, so it must have the field's
75+
/// element type: `[<ArgumentDefaultValue 3>]` on a `Choice<int, int>`, but
76+
/// `[<ArgumentDefaultValue 3L>]` on a `Choice<int64, int64>`. A mismatch is a compile error in
77+
/// the generated file rather than at the attribute itself.
78+
///
79+
/// Since this is an ordinary .NET attribute argument, F# restricts you to compile-time constants:
80+
/// literals, `[<Literal>]` values, and enum cases. (Anything else, and in particular a discriminated
81+
/// union case such as a flag DU's, needs `[<ArgumentDefaultFunction>]` instead.) Note that F#'s
82+
/// attribute syntax requires parentheses around an argument which is not a bare literal:
83+
/// `[<ArgumentDefaultValue(Consts.Foo)>]`, but `[<ArgumentDefaultValue 3>]`.
84+
type ArgumentDefaultValueAttribute (defaultValue : obj) =
85+
inherit Attribute ()
86+
6587
/// Attribute indicating that this field or type shall have the given help text, when `--help` is invoked
6688
/// or when a parse error causes us to print help text.
6789
/// When applied to a record type, the help text appears at the top of the help output, before the field descriptions.

WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ WoofWare.Myriad.Plugins.ArgumentDefaultEnvironmentVariableAttribute inherit Syst
77
WoofWare.Myriad.Plugins.ArgumentDefaultEnvironmentVariableAttribute..ctor [constructor]: string
88
WoofWare.Myriad.Plugins.ArgumentDefaultFunctionAttribute inherit System.Attribute
99
WoofWare.Myriad.Plugins.ArgumentDefaultFunctionAttribute..ctor [constructor]: unit
10+
WoofWare.Myriad.Plugins.ArgumentDefaultValueAttribute inherit System.Attribute
11+
WoofWare.Myriad.Plugins.ArgumentDefaultValueAttribute..ctor [constructor]: obj
1012
WoofWare.Myriad.Plugins.ArgumentFlagAttribute inherit System.Attribute
1113
WoofWare.Myriad.Plugins.ArgumentFlagAttribute..ctor [constructor]: bool
1214
WoofWare.Myriad.Plugins.ArgumentHelpTextAttribute inherit System.Attribute

WoofWare.Myriad.Plugins.Attributes/version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "3.8",
2+
"version": "3.9",
33
"publicReleaseRefSpec": [
44
"^refs/heads/main$"
55
],

0 commit comments

Comments
 (0)