feat(spec): split a value the way clap splits one - #961
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryAdds delimiter-aware value splitting across the KDL specification, clap bridge, interpreted parser, compiled argv parser, and derive-generated parser.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the eligible follow-up scope. No blocking failure remains. Important Files Changed
Reviews (18): Last reviewed commit: "fix(conformance): carry the delimiter th..." | Re-trigger Greptile |
Instruction countsNothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does. New, nothing to compare against: Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run. Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes. Shadow comparisonParsing
|
|
Both findings were right, and they were the same mistake in two places: treating a word that carries several values as one value. Positional choices ran before the split (@cursor Bugbot, High) — the flag path split first and the positional path did not, so The bridge dropped a delimiter clap would have used (High) — I had gated it on A delimiter is now taken as the statement that several values can land, so it brings This comment was generated by Claude Code. |
5b8c46f to
59ebae4
Compare
c8592da to
e4383ce
Compare
|
Pushed a fix for @cursor Bugbot's
So instead of re-adding a post-binding count, the parser tables gain the delimiter, beside the usage-lib was already counting values rather than words, so its collection loop stopped in the right place but never reported the overshoot. It now does, at the end of an occurrence's run. What I got wrong firstMy initial attempt counted the accumulated list on both sides. The corpus vector Tests
Stubbing Cost and APIThe shadow benchmark is bit-identical at 63,822 instructions and
This comment was generated by Claude Code. |
|
CI was red on The Go parser has no delimiter support at all — not a partial implementation, no mention of it anywhere in So the vector is gone, and My mistake in process, not just in content: I ran This comment was generated by Claude Code. |
Pull Request is not mergeable
Pull Request is not mergeable
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 27f0cdd. Configure here.
|
Rebased onto the updated #941, and through it onto
Worth noting the short-flag assertion a few lines above in that same file, which arrived independently: it refuses a non-ASCII short flag because "the line arrives as UTF-8, where such a character is two bytes or more, and whatever single byte a cast produced would match nothing anybody could type." That is the same reasoning as the delimiter fix in the previous commit, reached separately — good corroboration that ASCII is the right boundary here rather than a convenience.
This comment was generated by Claude Code. |
`--tags a,b,c` as three values. The spec could record a clap *default* split by a
delimiter — `lib/src/spec/arg.rs` has done that since the `--fs-events` fix — and
had no way to say the same thing about a value someone types, so an adopter using
`value_delimiter` lost behaviour rather than spelling.
flag "--tags <tag>" var=#true delimiter=","
Split during the parse, before anything judges what came out, which is the part
that matters: `choices` is asked about each value rather than about the word that
carried them, and `var_min`/`var_max` count the values the user meant rather than
the words they typed. Judging first would reject `--env dev,prod` against a list
that both halves are on.
A delimiter needs somewhere to put what it splits, so it goes with `var`. On a
single-value flag everything after the first separator would be dropped
silently — refused where it is written instead, in all three places a spec can
declare one, and a compile error in the derive, where the field has to be a `Vec`.
The derive splits on the cold path, first in `check` and after the environment
has filled what argv left out, since `TAGS=a,b` is one word too. The binder is
untouched: it collects words and knows nothing about what a value *is*, which is
what keeps the hot path out of this.
The bridge carries it — `Arg::get_value_delimiter` is public, and it is the same
getter the default splitting already used. Only where several values can land,
because clap refuses `value_delimiter` with `num_args(1)` itself and a spec
recording one there would be a spec this crate then declines to parse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…miter
Two from review, both of them the same mistake in two places: treating a word
that carries several values as one value.
**A positional judged the whole word.** The flag path split first and the
positional path did not, so `src:docs` was rejected against a list that both
halves are on, and a bad half was reported as the whole word. Split first now, as
the flag path does, and the collection below reuses what the split produced
rather than doing it again.
**The bridge dropped a delimiter clap would have used.** It was carried only for
`Append`/`Count` or a `num_args` above one, on the reasoning that clap refuses a
delimiter with `num_args(1)`. clap does no such thing: its parser reaches for
`arg.get_value_delimiter()` before it looks at anything else, so `ArgAction::Set`
with `value_delimiter(',')` is one word becoming several — and that is the common
spelling. The generated spec left a CLI whose defaults split and whose typed
values did not.
A delimiter *is* the statement that several values can land, so it brings `var`
with it rather than waiting for one, which also keeps the emitted spec parseable
by the rule that a delimiter needs somewhere to put what it splits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`var_max` is enforced by binding: a variadic stops on its bound and the next field takes the rest, so before delimiters a collection could reach the bound but never pass it. A delimiter breaks that assumption — one word is no longer one value, and `--include a,b,c` is already three on the single word the collection was entitled to take. Binding counted words, so the bound was silently exceeded, for positionals and for variadic flags alike. The parser tables gain the delimiter, beside the `var_max` they already carry and for the same stated reason: it decides *where* a word lands. Binding counts the values a word holds rather than the word, and a word cannot be split between two owners, so passing the bound is an error where reaching it is just a stopping place. usage-lib already counted values but only ever stopped, so it gets the same report at the end of an occurrence's run. Both keep the rule the corpus documents: the bound is on what one occurrence takes, not on the list the occurrences build up, so `--include a,b --include c,d` is two twice and not four. No corpus vector for this yet, deliberately. The corpus is what every implementation answers, and the Go parser has no delimiter support at all — a vector here would fail it for want of the feature rather than for want of this rule. It belongs with whichever change teaches Go to split. The rule is held across the derive and usage-lib by the conformance test in the meantime, which is the agreement this change is about. The derive path is unchanged in cost — the shadow benchmark is bit-identical at 63,822 instructions, mise declaring no delimiters for `values_in` to count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The KDL side accepted any single Unicode scalar while everything below it splits by byte, so the two disagreed in both directions. A separator above U+00FF was dropped on the way into the binding tables and the CLI quietly stopped splitting. Worse, one between U+0080 and U+00FF *fits* in a byte as a scalar while taking two in UTF-8, so matching that byte found the continuation bytes inside unrelated characters: with `delimiter="§"`, binding split `aЧbЧc` into three values and refused it against `var_max=2`, while usage-lib accepted it. Rejecting a command line nobody wrote a separator into is the worse half of the bug, and it arrived with the tables learning the delimiter at all. So the spec enforces what the derive already enforced where it reads the same property, in the same words: one byte, use an ASCII separator. Both conversions into the tables now filter on `is_ascii` rather than on fitting in a `u8`, which is the distinction that was wrong. The clap bridge records only an ASCII delimiter too. clap splits by character and may well have a wider one; the spec keeps `var`, so the values still arrive, and drops only its account of how they were separated — a spec that recorded it could not be written back out, since `to_kdl` would emit what parsing now refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`conformance/src/tables.rs` arrived on main while this branch was open, taking over the table building this branch had been editing in `argv.rs` and constructing each struct field by field — so the `delimiter` added here left four initializers short. Both models want it, and they want it differently: the binding tables take the byte binding counts values by, filtered on `is_ascii` rather than on fitting in a `u8`, and the metadata takes the `char` the spec declared. The short-flag assertion a few lines above already says why that distinction matters — a character that is several bytes in UTF-8 has no single byte that matches anything anyone could type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Stacked on #941. Third item on the "changes what a CLI does" list in PLAN.md.
--tags a,b,cas three values. The spec could already record a clap default split by a delimiter —lib/src/spec/arg.rshas done that since the--fs-eventsfix — and had no way to say the same thing about a value someone types. An adopter usingvalue_delimiterwas losing behaviour rather than spelling.Split before anything judges the result
This is the part that decides whether the feature is worth having.
choicesis asked about each value rather than about the word that carried them, andvar_min/var_maxcount the values the user meant rather than the words they typed:Judging first would reject
dev,prodagainst a list that both halves are on, which is what the first draft of this did.A delimiter needs somewhere to put what it splits
So it goes with
var. On a single-value flag everything after the first separator would be dropped silently — refused where it is written instead, in all three places a spec can declare one (rootarg,cmdblockarg, andflag), and a compile error in the derive, where the field has to be aVec. A non-ASCII delimiter is refused too, since values are split by byte.Where the split runs
The derive splits on the cold path, first in
checkand after the environment has filled what argv left out —TAGS=a,bis one word too. The binder is untouched: it collects words and knows nothing about what a value is, which is what keeps the hot path free of all of this. A split costs one allocation per value, only for a field that asked for one.The bridge carries it
Arg::get_value_delimiteris public — the same getter the default splitting already used. Only where several values can land, because clap refusesvalue_delimiterwithnum_args(1)itself, and a spec recording one on a single-value argument would be a spec this crate then declines to parse.cargo test --all --all-features,mise run lint,mise run renderandmise run gen-shadoware clean.🤖 Generated with Claude Code
Note
Medium Risk
Touches core argv binding and both derive and spec parsers, so miscounting or split order could change CLI behavior; mitigated by extensive tests and alignment with clap semantics.
Overview
Adds
delimiter(spec KDL,#[usage(delimiter = ',')], metadata) so one argv word can represent several values, matching clap’svalue_delimiter.Parsing behavior: Values are split on the delimiter before
choicesandvar_min/var_maxrun, so--env dev,prodis judged per value and limits count intended values, not raw words. The derive path splits in generatedcheckafter env fallback;usage-libdoes the same for spec-driven parse; the argv binder stays word-oriented but usesvalues_inand can returnVarTooManywhen a delimited word exceedsvar_maxin one step.Validation & bridge: Delimiter requires a
Vecfield (compile-time in derive) andvarin the spec; only ASCII separators are stored in binding tables. Clap import setsvarwhen a delimiter is present and round-trips ASCII delimiters in KDL/docs/shadow codegen.Reviewed by Cursor Bugbot for commit abc6187. Bugbot is set up for automated code reviews on this repo. Configure here.