Skip to content

Commit 881c04c

Browse files
Smaug123claude
andcommitted
Support discriminated unions of alternative argument sets
An [<ArgParser>] type may now be a discriminated union, each of whose cases holds one record of that case's arguments; and a record field may be union-typed in the same way. Exactly one case's arguments must be supplied: the case whose arguments appear is selected, arguments from two cases are a conflict naming both witnesses, a selected-but- incomplete case reports its own missing arguments, and when nothing selects a case the unique case which needs no arguments (if there is one) is chosen. Argument names must be globally unique across cases (already enforced by the existing collision check), so selection never depends on value conversion and parsing stays linear. The erased runtime already understood Sum schemas (property-tested against the exhaustive expand-every-alternative semantics); this wires the generator up: union lowering in the parse tree, tree-shaped erased schemas, selection-aware assembly (slots of unselected cases are never read), and ParseOutcome.Success now carries the selection. Generation-time checks, exercised in ArgParserConflictTests: - at most one case of a union may be satisfiable with no arguments; - positional args may not appear inside union cases, nor alongside a union arg; - the argument name "help" is reserved (it always meant help at runtime; now generation refuses it too). Help text for union schemas is currently the flat list of every case's arguments; grouping it by case is a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5fd4cf0 commit 881c04c

13 files changed

Lines changed: 2519 additions & 844 deletions

ConsumePlugin/ArgParserConflictTests.fs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,58 @@ type NoConflict =
128128

129129
NormalField : string
130130
}
131+
132+
// ============================================================================
133+
// Discriminated-union and reserved-name conflicts
134+
// ============================================================================
135+
136+
(*
137+
// The argument name "help" is reserved (in any casing): --help always displays help.
138+
[<ArgParser>]
139+
type ReservedHelpName =
140+
{
141+
[<ArgumentLongForm "help">]
142+
Foo : int
143+
}
144+
145+
type AllOptionalA =
146+
{
147+
A : int option
148+
}
149+
150+
type AllOptionalB =
151+
{
152+
B : int option
153+
}
154+
155+
// Two cases which are both satisfiable with no arguments: an empty command line could not
156+
// choose between them, so generation must refuse.
157+
[<ArgParser>]
158+
type AmbiguousEmptyCases =
159+
| CaseA of AllOptionalA
160+
| CaseB of AllOptionalB
161+
162+
type SomePositionals =
163+
{
164+
[<PositionalArgs>]
165+
Rest : string list
166+
}
167+
168+
type NotPositional =
169+
{
170+
C : int
171+
}
172+
173+
type PositionalOrNot =
174+
| Pos of SomePositionals
175+
| NotPos of NotPositional
176+
177+
// Positional args cannot appear inside union cases: the parser could not tell which
178+
// alternative a positional arg belongs to.
179+
[<ArgParser>]
180+
type PositionalInsideUnion =
181+
{
182+
Choice : PositionalOrNot
183+
}
184+
185+
*)

ConsumePlugin/ConsumePlugin.fsproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@
100100
<Compile Include="GeneratedArgParserNegationTests.fs">
101101
<MyriadFile>ArgParserNegationTests.fs</MyriadFile>
102102
</Compile>
103+
<Compile Include="DuArgs.fs" />
104+
<Compile Include="GeneratedDuArgs.fs">
105+
<MyriadFile>DuArgs.fs</MyriadFile>
106+
</Compile>
103107
<Compile Include="Nested/Args.fs" />
104108
<Compile Include="Nested/GeneratedArgs.fs">
105109
<MyriadFile>Nested/Args.fs</MyriadFile>

ConsumePlugin/DuArgs.fs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace ConsumePlugin
2+
3+
open WoofWare.Myriad.Plugins
4+
5+
type FooArgs =
6+
{
7+
[<ArgumentHelpText "The foo argument">]
8+
Foo : int
9+
}
10+
11+
type BarArgs =
12+
{
13+
Bar : int
14+
Baz : int
15+
}
16+
17+
/// The motivating example of discriminated-union argument parsing: the user supplies either
18+
/// `--foo=3`, or both `--bar=8` and `--baz=9`, and the parse tells us which.
19+
[<ArgParser>]
20+
type DuArgs =
21+
| FooCase of FooArgs
22+
| BarCase of BarArgs
23+
24+
type AutoMode =
25+
{
26+
Quiet : bool option
27+
}
28+
29+
type ManualMode =
30+
{
31+
Level : int
32+
}
33+
34+
type Mode =
35+
| Auto of AutoMode
36+
| Manual of ManualMode
37+
38+
/// A union nested inside a record, with a case (Auto) which is satisfiable with no arguments:
39+
/// an empty command line picks it.
40+
[<ArgParser>]
41+
type WithModeArgs =
42+
{
43+
Verbose : bool
44+
Mode : Mode
45+
}

ConsumePlugin/GeneratedArgParserNegationTests.fs

Lines changed: 60 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -770,8 +770,9 @@ module private ArgParserRuntime_BoolNegation =
770770
[<RequireQualifiedAccess>]
771771
type ParseOutcome =
772772
/// Every argument was routed, converted and defaulted without error: the typed layer's
773-
/// slots are fully populated and it may assemble the result.
774-
| Success
773+
/// slots are fully populated and it may assemble the result. The selection records which
774+
/// case was chosen for every discriminated union in the schema.
775+
| Success of selection : Selection
775776
/// A `--help`-shaped token was seen; the typed layer should render help and stop.
776777
| HelpRequested
777778
/// The parse was aborted mid-scan (historically these conditions threw immediately). The
@@ -950,7 +951,7 @@ module private ArgParserRuntime_BoolNegation =
950951
| None -> ()
951952

952953
if errors.Count = 0 then
953-
ParseOutcome.Success
954+
ParseOutcome.Success selection
954955
else
955956
ParseOutcome.Errors (List.ofSeq errors)
956957
namespace ConsumePlugin
@@ -989,8 +990,9 @@ module BoolNegationArgParse =
989990
}
990991
]
991992
Tree =
992-
(ArgParserRuntime_BoolNegation.ErasedTree.Product[ArgParserRuntime_BoolNegation.ErasedTree.Leaf
993-
0])
993+
(ArgParserRuntime_BoolNegation.ErasedTree.Product (
994+
[ ArgParserRuntime_BoolNegation.ErasedTree.Leaf 0 ]
995+
))
994996
Positional = None
995997
}
996998

@@ -1043,16 +1045,14 @@ module BoolNegationArgParse =
10431045
parser_callbacks
10441046
args
10451047
with
1046-
| ArgParserRuntime_BoolNegation.ParseOutcome.Success ->
1047-
let arg_0 =
1048-
match arg_0 with
1049-
| Some x -> x
1050-
| None ->
1051-
failwith
1052-
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse"
1053-
1048+
| ArgParserRuntime_BoolNegation.ParseOutcome.Success parser_selection ->
10541049
{
1055-
EnableFeature = arg_0
1050+
EnableFeature =
1051+
(match arg_0 with
1052+
| Some x -> x
1053+
| None ->
1054+
failwith
1055+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
10561056
}
10571057
| ArgParserRuntime_BoolNegation.ParseOutcome.HelpRequested ->
10581058
helpText () |> failwithf "Help text requested.\n%s"
@@ -1098,8 +1098,9 @@ module FlagNegationArgParse =
10981098
}
10991099
]
11001100
Tree =
1101-
(ArgParserRuntime_BoolNegation.ErasedTree.Product[ArgParserRuntime_BoolNegation.ErasedTree.Leaf
1102-
0])
1101+
(ArgParserRuntime_BoolNegation.ErasedTree.Product (
1102+
[ ArgParserRuntime_BoolNegation.ErasedTree.Leaf 0 ]
1103+
))
11031104
Positional = None
11041105
}
11051106

@@ -1173,16 +1174,14 @@ module FlagNegationArgParse =
11731174
parser_callbacks
11741175
args
11751176
with
1176-
| ArgParserRuntime_BoolNegation.ParseOutcome.Success ->
1177-
let arg_0 =
1178-
match arg_0 with
1179-
| Some x -> x
1180-
| None ->
1181-
failwith
1182-
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse"
1183-
1177+
| ArgParserRuntime_BoolNegation.ParseOutcome.Success parser_selection ->
11841178
{
1185-
DryRun = arg_0
1179+
DryRun =
1180+
(match arg_0 with
1181+
| Some x -> x
1182+
| None ->
1183+
failwith
1184+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
11861185
}
11871186
| ArgParserRuntime_BoolNegation.ParseOutcome.HelpRequested ->
11881187
helpText () |> failwithf "Help text requested.\n%s"
@@ -1236,8 +1235,9 @@ module MultipleFormsNegationArgParse =
12361235
}
12371236
]
12381237
Tree =
1239-
(ArgParserRuntime_BoolNegation.ErasedTree.Product[ArgParserRuntime_BoolNegation.ErasedTree.Leaf
1240-
0])
1238+
(ArgParserRuntime_BoolNegation.ErasedTree.Product (
1239+
[ ArgParserRuntime_BoolNegation.ErasedTree.Leaf 0 ]
1240+
))
12411241
Positional = None
12421242
}
12431243

@@ -1290,16 +1290,14 @@ module MultipleFormsNegationArgParse =
12901290
parser_callbacks
12911291
args
12921292
with
1293-
| ArgParserRuntime_BoolNegation.ParseOutcome.Success ->
1294-
let arg_0 =
1295-
match arg_0 with
1296-
| Some x -> x
1297-
| None ->
1298-
failwith
1299-
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse"
1300-
1293+
| ArgParserRuntime_BoolNegation.ParseOutcome.Success parser_selection ->
13011294
{
1302-
VerboseMode = arg_0
1295+
VerboseMode =
1296+
(match arg_0 with
1297+
| Some x -> x
1298+
| None ->
1299+
failwith
1300+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
13031301
}
13041302
| ArgParserRuntime_BoolNegation.ParseOutcome.HelpRequested ->
13051303
helpText () |> failwithf "Help text requested.\n%s"
@@ -1385,14 +1383,13 @@ module CombinedFeaturesArgParse =
13851383
}
13861384
]
13871385
Tree =
1388-
(ArgParserRuntime_BoolNegation.ErasedTree.Product[ArgParserRuntime_BoolNegation.ErasedTree.Leaf
1389-
0
1390-
1391-
ArgParserRuntime_BoolNegation.ErasedTree.Leaf
1392-
1
1393-
1394-
ArgParserRuntime_BoolNegation.ErasedTree.Leaf
1395-
2])
1386+
(ArgParserRuntime_BoolNegation.ErasedTree.Product (
1387+
[
1388+
ArgParserRuntime_BoolNegation.ErasedTree.Leaf 0
1389+
ArgParserRuntime_BoolNegation.ErasedTree.Leaf 1
1390+
ArgParserRuntime_BoolNegation.ErasedTree.Leaf 2
1391+
]
1392+
))
13961393
Positional = None
13971394
}
13981395

@@ -1489,32 +1486,26 @@ module CombinedFeaturesArgParse =
14891486
parser_callbacks
14901487
args
14911488
with
1492-
| ArgParserRuntime_BoolNegation.ParseOutcome.Success ->
1493-
let arg_0 =
1494-
match arg_0 with
1495-
| Some x -> x
1496-
| None ->
1497-
failwith
1498-
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse"
1499-
1500-
let arg_1 =
1501-
match arg_1 with
1502-
| Some x -> x
1503-
| None ->
1504-
failwith
1505-
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse"
1506-
1507-
let arg_2 =
1508-
match arg_2 with
1509-
| Some x -> x
1510-
| None ->
1511-
failwith
1512-
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse"
1513-
1489+
| ArgParserRuntime_BoolNegation.ParseOutcome.Success parser_selection ->
15141490
{
1515-
Debug = arg_1
1516-
NormalBool = arg_2
1517-
Verbose = arg_0
1491+
Debug =
1492+
(match arg_1 with
1493+
| Some x -> x
1494+
| None ->
1495+
failwith
1496+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
1497+
NormalBool =
1498+
(match arg_2 with
1499+
| Some x -> x
1500+
| None ->
1501+
failwith
1502+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
1503+
Verbose =
1504+
(match arg_0 with
1505+
| Some x -> x
1506+
| None ->
1507+
failwith
1508+
"WoofWare.Myriad internal error in generated parser: required argument missing after successful parse")
15181509
}
15191510
| ArgParserRuntime_BoolNegation.ParseOutcome.HelpRequested ->
15201511
helpText () |> failwithf "Help text requested.\n%s"

0 commit comments

Comments
 (0)