ArgParser: add [<ArgumentPrefix "foo">] to namespace a sub-record's args - #598
Merged
Conversation
This was referenced Jul 29, 2026
A field whose type is another argument record contributes that record's
arguments directly, so embedding the same record twice made its arguments
collide and was rejected. [<ArgumentPrefix "foo">] namespaces a field's whole
subtree, so `--blah` becomes `--foo-blah` and
{ [<ArgumentPrefix "src">] Source : Endpoint
[<ArgumentPrefix "dst">] Dest : Endpoint }
is expressible.
An argument's spellings are assembled in exactly one place, and help text, the
--no- variant, the generation-time conflict checks and the erased schema handed
to the runtime all read them from there. Applying the prefix at that point is
therefore the whole implementation, and needs no runtime change: the prefixed
names take part in conflict detection (so a prefix can resolve a collision and
equally create one), and negation wraps the prefix from outside as
`--no-foo-blah`.
The prefix accumulates rather than resetting as the recursion descends, so it
covers the subtree however deep, whether or not intervening records carry
prefixes of their own, and nested prefixes compose from the outside in. A union
case is an alternative rather than a nesting level, so the prefix passes through
each case unchanged.
It must be a string literal (it is combined into names as the parser is
generated, so it must be known then), non-empty, free of '=', and without an
edge dash, since the separating '-' is inserted for us. It is used verbatim: a
hand-written string has none of the constraints an F# identifier does, so unlike
the identifier-derived default there is nothing to normalise. Misplacing it on a
leaf, on a [<PositionalArgs>] field, or on a union case is an error rather than a
silent drop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he dispatch Found in review. The structural branches run before any leaf machinery, so the prefix-on-a-positional-field rejection never saw a field which was both prefixed and record-typed: generation quietly prefixed the subtree and dropped the [<PositionalArgs>]. Check the pairing ahead of the dispatch, which also lets the leaf branch drop to a single message. Also pin down the invariant that the prefixed name reaching the generation-time name checks is the argument's *semantic* spelling and not a rendering of it. The checks compare under the scanner's case-insensitive equality, so an escaped rendering would compare `é` against `É` where `é` and `É` collide, and a schema which fails at every parse would sail through generation. (Emitting a spelling which needs escaping is a separate, pre-existing defect: `[<ArgumentLongForm "back\\tab">]` is already emitted as `"back\tab"` with no prefix in sight, and is read back with a tab in it. Fixing that belongs with the emission sites, and applies equally to the spellings we do not rebuild.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Smaug123
force-pushed
the
argparser-argument-prefix
branch
from
July 29, 2026 06:58
fcd09e6 to
4214ba8
Compare
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.
A field whose type is another argument record contributes that record's arguments directly, so embedding the same record twice made its arguments collide and was rejected outright.
[<ArgumentPrefix "foo">]namespaces a field's whole subtree, so--blahbecomes--foo-blahand this becomes expressible:Why it is this small
An argument's spellings are assembled in exactly one place (
longForms), and help text, the--no-variant, the generation-time conflict checks and the erased schema handed to the runtime all read them from there. Applying the prefix at that point is the whole implementation, and needs no runtime change at all. The prefixed names are what the duplicate-name checks see, so a prefix can resolve a collision and equally create one; and negation wraps from outside as--no-foo-blahbecause the runtime builds the negated token as"--no-" + form.Semantics
[<ArgumentLongForm>]— otherwise two parents embedding one child under different prefixes would still collide on it.--outer-inner-blah).=, and without an edge dash since the separating-is inserted for you. Used verbatim: a hand-written string has none of the constraints an F# identifier does, so unlike the identifier-derived default there is nothing to normalise.[<PositionalArgs>]field, or on a union case is an error with an explanation, not a silent drop.Tests
Generation-time rejections and a property over prefix strings (accepted exactly when well-formed) in
TestArgParserRejection.fs; behaviour in the newTestArgParserPrefix.fs, including:"outer"wrapping"inner"gives exactly the names"outer-inner"gives;Checked by mutation: neutering the prefix application fails the build outright, because
Transfer's twoEndpointcopies collide without it.Known adjacent defect, not fixed here
Emitting a spelling which needs escaping is a pre-existing bug:
[<ArgumentLongForm "back\\tab">]is already emitted as"back\tab"with no prefix in sight, and is read back with a tab in it. The fix belongs with the emission sites and applies equally to spellings we do not rebuild, so it is a separate PR. This PR deliberately keepsArgFormholding the semantic spelling, which is what the case-insensitive name checks must compare — there is a test pinning that, since escaping there would compareéagainstÉwhereéandÉcollide.🤖 Generated with Claude Code