Support discriminated unions of alternative argument sets - #568
Merged
Conversation
Smaug123
force-pushed
the
argparser-cutover
branch
from
July 15, 2026 08:07
0d98b98 to
5fd4cf0
Compare
Smaug123
force-pushed
the
argparser-du-support
branch
from
July 15, 2026 08:07
6e3455d to
e398701
Compare
Smaug123
force-pushed
the
argparser-cutover
branch
from
July 15, 2026 18:29
5fd4cf0 to
e5740f0
Compare
Smaug123
force-pushed
the
argparser-du-support
branch
from
July 15, 2026 18:29
e398701 to
9d2113e
Compare
Smaug123
force-pushed
the
argparser-cutover
branch
from
July 15, 2026 19:08
e5740f0 to
247fe1f
Compare
Smaug123
force-pushed
the
argparser-du-support
branch
from
July 15, 2026 19:08
9d2113e to
e6e7fe8
Compare
Smaug123
force-pushed
the
argparser-cutover
branch
from
July 15, 2026 19:53
247fe1f to
1ea032c
Compare
Smaug123
force-pushed
the
argparser-du-support
branch
4 times, most recently
from
July 16, 2026 07:39
0e2f8eb to
f77bb29
Compare
Smaug123
force-pushed
the
argparser-du-support
branch
from
July 16, 2026 08:01
f77bb29 to
550182c
Compare
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>
Cross-case name collisions (including case-insensitive ones, the soundness hole for case selection), ambiguous empty cases, positionals inside or alongside a union, and malformed case payloads are now asserted by driving the generator over in-memory source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Help was flattening the parse tree, presenting e.g. --foo, --bar and --baz as one undifferentiated list with nothing to say that the grammar is `--foo` XOR (`--bar` AND `--baz`). Help is now a fold of the parse tree: each union renders as an "exactly one of the following sets of arguments:" header with its alternatives' arguments grouped beneath their case names, indenting two spaces per nesting level. Parsers with no union render byte-identically to before. Also exercise [<ArgumentDefaultFunction>] on a union case's payload record end to end: the generated call resolves against the payload record (fixed one level down the stack), the defaulted case is selectable by an empty command line, and the default does not influence case selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FCS represents `of (FooArgs)` as SynType.Paren, so the by-name lookups for a union case's payload record and for union- or record-typed fields rejected declarations which were valid without the parentheses. Strip optional parens before all three lookups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toParseSpec and unionToParseSpec descend into ambient records and unions by name with no memory of what they were already expanding, so a schema which refers to itself (even indirectly, e.g. a union whose case payload record holds a field of that union) recursed until the Myriad subprocess died with a stack overflow. Thread the chain of types currently being lowered through the recursion and fail with the cycle path on re-entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The by-name lookups which resolve a field type, a union case payload, or a flag DU to a type defined alongside the tagged type compared only the last segment of the reference. A qualified reference to a foreign type whose last segment matches a local type name - e.g. System.Uri alongside a structural union named Uri - was therefore captured by the local type, silently generating code that does not compile (Uri.Case constructions assigned into a System.Uri field). Local types can only be referred to by bare name, so require a single-segment (possibly parenthesized) reference at all four sites; qualified references now fall through to the built-in leaf parsers or to a comprehensible generation error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Smaug123
force-pushed
the
argparser-du-support
branch
from
July 18, 2026 17:37
02c7870 to
0230eab
Compare
Smaug123
commented
Jul 18, 2026
| } | ||
| |> spec.Apply | ||
|
|
||
| if Option.isSome pos && hasSum then |
Owner
Author
There was a problem hiding this comment.
(I think at some point we can relax this.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #567. The feature this whole stack was building toward.
What you can now write
--foo=3parses toFooCase { Foo = 3 };--bar=8 --baz=9toBarCase { Bar = 8; Baz = 9 }.--foo=3 --bar=8fails withArguments select more than one alternative: FooCase (via --foo=3), BarCase (via --bar=8);--bar=8alone fails with BarCase's ownRequired argument '--baz' received no value; supplying nothing fails withNo arguments were supplied to select one of: FooCase, BarCase. Unions compose: a record field may be union-typed (with other fields alongside), payload records may contain nested records, defaults, flags, etc., and when no argument touches a union, the unique case which needs no arguments is selected as the fallback.Semantics
Exactly the shape-first, exactly-one-interpretation model from the design discussion: selection depends only on which argument names appear, never on whether values convert, and argument names are globally unique across cases (the existing collision check already enforces this), so parsing stays linear and a malformed value can never flip the selected case. The erased kernel's
selectwas already property-tested against the exhaustive expand-every-alternative reference semantics in #566; this PR wires the generator to it: union lowering into the parse tree, tree-shaped erased schemas,ParseOutcome.Successcarrying the selection, and selection-aware assembly (the slots of unselected cases are never read).Generation-time checks (all verified to fire, documented in ArgParserConflictTests.fs)
helpis reserved in any casing (it always meant help at runtime; generation now refuses it).Deferred follow-ups
🤖 Generated with Claude Code