You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

8.6 KiB

G-053 punk::args range-valued -multiple: occurrence arity with strict duplicate handling

Status: achieved 2026-08-08 Scope: src/modules/punk/args-999999.0a1.0.tm (spec compiler, parse, arg_error/synopsis renderers); src/tests/modules/punk/args/testsuites/args/ Goal: -multiple accepts a {min max} occurrence range (mirroring -choicemultiple; max -1 unbounded) alongside the legacy booleans - so a definition can declare "at most once, repeat is an error" ({0 1}) or bounded repetition ({2 4}) instead of choosing between silent last-wins (0) and unbounded collection (1) - with boolean semantics preserved exactly, including the prepend-defaults/last-wins override idiom. Acceptance: parse raises a usage-style arity error naming the argument for occurrences outside a declared range; boolean -multiple 0/1 behaviour is unchanged (full existing punk::args suite passes untouched); the -optional/range-min reconciliation rule is documented and enforced at define time; the usage table Multi column and synopsis reflect declared ranges; -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged; characterization tests cover the new forms and the value-shape rule.

Context

Boolean -multiple conflates three axes:

  1. occurrence arity - how many times the argument may be supplied
  2. overflow policy - what happens beyond the limit: silent replace (legacy -multiple 0 last-wins) or error
  3. value shape - scalar vs list-of-occurrences

The motivating incident (2026-07-10): runtests.tcl's -include-paths was a non-multiple list option, so repeated -include-paths flags silently last-won - quietly narrowing a test run while reporting green, and defeating even a recorded memory note about the gotcha. Repeatable accumulating flags are the dominant convention in shell-facing CLIs (gcc -I, curl -H, rsync --exclude), so this misuse recurs. runtests was fixed by making that option -multiple 1, but the general fix for "repeat should be an error" has no expression today.

A separate -duplicates deny|replace policy flag was considered and rejected: with -multiple 1 a duplicates policy is meaningless (duplicates ARE the collected payload), so the flag's validity would depend on another flag's setting, and it would blur into the existing -multipleunique/-multipleuniqueset territory.

punk::args already has the shape precedent in its own vocabulary: -choicemultiple is a {min max} pair, not a boolean.

Approach

-multiple accepts a boolean (legacy, semantics preserved exactly) or a {min max} range (max -1 = unbounded):

  • -multiple 0 - legacy: single-valued, repeats silently replace (last wins), scalar shape. Unchanged - this preserves the prepend-defaults override idiom punk::args::parse [list -flag default {*}$userargs].
  • -multiple 1 - legacy: unbounded collection, list shape. Unchanged.
  • -multiple {0 1} - at most once; a second occurrence is a parse (arity) error. This is the "duplicates deny" case.
  • -multiple {2 4}, {1 -1} etc - bounded/lower-bounded repetition, collected.

Design decisions to settle (record here when made):

  • Value shape for range forms (settled 2026-08-08): max == 1 forms stay scalar (they are strict single-valued variants of legacy 0); max > 1 or -1 yield the occurrence list. This matches the existing boolean truth-test usage (-multiple true = list-collect), so {0 1} is scalar and {2 4}/{1 -1} are lists. Pinned by the characterization tests.
  • -optional vs range-min reconciliation (settled 2026-08-08): -optional governs presence (the 0..1 of whether the arg appears at all); the range min governs occurrence count when present. -optional 0 + range min>=1 = required (at least min times). Contradictory combinations (range min >= 1 intended as "required" while -optional is also set, or a min that an optional arg can never reach) are rejected at define time with a clear message.
  • Hot-path canonicalization (settled 2026-08-08): the spec compiler canonicalizes -multiple ONCE into internal companion fields while preserving the stored boolean's meaning so every existing truth-test stays correct:
    • stored -multiple boolean = "list-shape collect" (true for legacy 1, {2 4}, {1 -1}, {0 -1}; false for legacy 0, {0 1}, {1 1}) - all existing collect-vs-replace / scalar-vs-list / leader-value-single-multiple truth-tests keep working unchanged.
    • new internal _multiple_min (occurrence floor), _multiple_max (cap, -1 unbounded), _multiple_maxbounded (1 when max is a hard cap that errors on exceed). Legacy 0 -> min 0 max -1 maxbounded 0 (unlimited replace); legacy 1 -> min 0 max -1 maxbounded 0 (unlimited collect); {0 1} -> min 0 max 1 maxbounded 1; {2 4} -> min 2 max 4 maxbounded 1; {1 -1} -> min 1 max -1 maxbounded 0.
    • only two new runtime check sites: a max-occurrence check at the storage sites (opts + leaders + values; count would exceed max -> arity error) and a min-occurrence check at final validation (count < min -> arity error). The boolean hot path is untouched.

Display benefits: the usage table's Multi column and the synopsis ?arg...? rendering gain meaningful bounded-repetition forms (e.g. "0-1", "2-4").

Progress

  • 2026-08-08 G-053 implemented (punk::args 0.22.0, project 0.60.0): -multiple now accepts a {min max} range (max -1 = unbounded) alongside the legacy booleans. The spec compiler canonicalises once at resolve: the stored -multiple becomes the computed boolean (list-shape collect: true for max>1 or -1, false for legacy 0 and max==1) so every existing collect-vs-replace / scalar-vs-list / leader-value-single-multiple hot-path truth-test stays correct, and the range companions (min/max/maxbounded) live in a separate per-form MULTIPLE_RANGES dict - NOT in ARG_INFO, so they do not ride along when ARG_INFO is round-tripped as a spec via resolved_def copyfrom (the first attempt stored them in ARG_INFO and broke 29 tests with an 'unrecognised key _multiple_min' resolve error; the separate-dict fix cleared it). Legacy 0/1 and boolean strings (true/false/yes/no) are coerced to the boolean and stay byte-unchanged (no MULTIPLE_RANGES entry for unlimited cases, so {0 -1} is equivalent to legacy 1). Resolve validation: max positive or -1, min <= max, and the -optional/range-min reconciliation (non-zero min forces presence, contradicts -optional -> reject). Parse enforcement: a new PUNKARGS VALIDATION occurrencecount failure class (payload count min | max ) fires in a single post-loop pass per section (opts/leaders/values) via a private::multiple_range_enforce helper; over-max is a hard contradiction (fires in normal and viability-probe modes, parse_status_classify maps it to invalid), under-min is pure end-of-input exhaustion (SUPPRESSED in the G-152 viability probe via the viabilitycheck arg, classified incomplete so a viable form reports incomplete not invalid). The usage-table Multi column reflects the range (0-1 / 2-4 / 1+; greencheck stays for legacy 1), the string renderer emits MULTI:0-1 etc., and the synopsis distinguishes at-most-once (?arg?, no ellipsis) from repeating (arg...). -multipleunique/-multipleuniqueset compose unchanged. define -help documents the range form and the -optional/range-min rule. New testsuite multipleranges.test (28 tests). Legacy untouched by default confirmed: full punk/args suite 399/0 (3 skipped), punk/ns 125/125.

Notes

  • -multipleunique / -multipleuniqueset remain the uniqueness knobs and only make sense for max > 1; they compose with ranges unchanged.
  • Related: G-045 (authoring ergonomics, achieved 2026-07-12 - see goals/archive/G-045-punkargs-authoring-ergonomics.md), G-046 (parse-time performance - the canonicalization must not regress the hot path).
  • The runtests -include-paths fix (repeatable, accumulate, single-list form still accepted) shipped independently on 2026-07-10 and does not depend on this goal.
  • Archived-goal references in this file: G-046 achieved 2026-07-10 (goals/archive/G-046-punkargs-deferred-help-and-fixes.md).
  • Referenced by G-083 (argument relations - "folding into G-053" considered and rejected there; achieved - see goals/archive/G-083-punkargs-argument-relations.md) and G-084 (-parsekey cross-member -multiple collection rides this goal's occurrence arity) - recorded 2026-07-24 after overlap review.

Follow-ons

Follow-on: G-084 cross-member -multiple collection on a shared parsekey rides this goal's occurrence-arity model - decide accumulate-in-received-order vs error-on-cross-member-combination now that the {min max} vocabulary is landed (see goals/G-084-punkargs-parsekey-completeness.md) => goal G-084