From df74c3b2dbe898ae4615f8f5ef0a9041ddf3eb9d Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sat, 8 Aug 2026 00:14:17 +1000 Subject: [PATCH] punk::args 0.25.0: bounded -multiple ranges participate in leader/value allocation (directed work post-G-053; project 0.62.0) Bounded -multiple occurrence ranges now PARTICIPATE in positional allocation for leaders and values instead of being post-loop enforcement only. Previously greedy collection overran a bounded max unless a type screen happened to stop it - untyped {3 3} followed by a -multiple tail collected 4 words and occurrencecount then reported the overrun the allocation itself created (and typed {3 3} with four consecutive ints failed identically) - and a later REQUIRED ranged argument was starved to a single reserved clause. Three cooperating sites: (a) get_dict_can_assign_value caps a satisfied -multiple argument at its bounded max: the word is yielded (no-consume) so the caller's retreat advances to the next argument - one proc serving both the leaders and values loops. The yield carries a G-082 rejection record of new kind 'occurrence', and both loops' overflow selection sites render it as the pointed occurrence-limit report with the G-053 occurrencecount errorcode (count = the attempted occurrence) instead of the generic toomanyarguments, so genuine over-supply keeps its pointed class - now describing an overflow the allocator refused rather than one it created. (b) Reservations are min-occurrence aware: the allocator's tail_needs and the derived valmin floor (leader/value split + option-scan reservation) reserve min-occurrences x min-clause-length for a required ranged argument - a following required -multiple {3 3} reserves 3 clauses, {2 2} pair clauses reserve 4 words - so earlier greed cannot starve it. An explicit '@values -min' still overrides the derived floor; legacy boolean -multiple (no MULTIPLE_RANGES entry) reserves exactly as before. (c) The greedy leader scan caps a bounded -multiple last leader at max occurrences (via the leader_posn_names_assigned counter), so remaining words flow to the opts/values sections. (The split is a separate pre-loop scan - capping only the loops left over-provided leader words raising toomanyarguments.) First occurrences are never capped (resolve guarantees range max >= 1); {0 1} at-most-once scalars are unaffected (no collection); bounded-variable ranges take greedy-up-to-max deterministically; legacy boolean greed is byte-unchanged (pinned). define -help -multiple documents the allocation semantics. multipleranges.test gains 7 allocation pins: untyped/typed {3 3} cap incl the cap-beats-type-screen four-ints case, greedy-up-to-max {1 2}, required-range and pair-clause reservations, the leaders-side scan cap + split floor, the pointed over-supply report, and a legacy-greed-unchanged guard. src/tests/modules/AGENTS.md index updated. Project 0.61.0 -> 0.62.0 + CHANGELOG (allocation behaviour is user-visible shell parsing). punk::args 0.24.0 -> 0.25.0. Suites: punk/args 408/0; modules tree 1320 pass / 11 constraint-skipped / 0 fail (zig-built tclsh90s 9.0.5); testbody_lint clean; make.tcl projectversion consistency OK. Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 16 ++++ punkproject.toml | 2 +- src/modules/punk/args-999999.0a1.0.tm | 93 +++++++++++++++++-- src/modules/punk/args-buildversion.txt | 3 +- src/tests/modules/AGENTS.md | 2 +- .../args/testsuites/args/multipleranges.test | 79 ++++++++++++++++ 6 files changed, 186 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92bec3ef..e1ad17d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.62.0] - 2026-08-08 + +- `punk::args` bounded `-multiple` occurrence ranges now participate in + positional allocation (directed work, post-G-053): a leader or value + argument that has taken its max occurrences yields further words to later + arguments (an untyped `{3 3}` followed by a `-multiple` tail takes exactly + 3 instead of overrunning and erroring), a later REQUIRED ranged argument + reserves min-occurrences worth of words from earlier greedy collection + (the allocator reservation, the derived valmin floor for the leader/value + split and option scan - an explicit `@values -min` still overrides), and + the greedy leader scan caps a bounded `-multiple` last leader. Genuine + over-supply keeps a pointed report: the overflow site renders the + occurrence limit with the G-053 `occurrencecount` errorcode via a new + G-082 rejection kind. Legacy boolean `-multiple` allocation is unchanged. + (punk::args 0.25.0) + ## [0.61.0] - 2026-08-07 - `punk::args` `-parsekey` completeness (G-084): a VALUE argument's diff --git a/punkproject.toml b/punkproject.toml index d9447b28..f7965b13 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,6 +1,6 @@ [project] name = "punkshell" -version = "0.61.0" +version = "0.62.0" license = "BSD-2-Clause" url = "https://www.gitea1.intx.com.au/jn/punkshell" #packager: declared identity for published artifacts (declarative, not proof - diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 3c6a9aca..b5603c76 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -1040,6 +1040,16 @@ tcl::namespace::eval punk::args { -optional/range-min reconciliation: a non-zero min forces the argument to be present, so it contradicts -optional (which permits absence) - declare -optional 0 for a min >= 1 range. + Bounded ranges participate in positional ALLOCATION for + leaders and values: an argument that has taken its max + occurrences yields further words to later arguments + (greedy-up-to-max - so an untyped {3 3} followed by a + -multiple tail takes exactly 3), and a later REQUIRED + argument with a range minimum reserves min occurrences' + worth of words from earlier greedy collection (an explicit + '@values -min' overrides the derived reservation floor). + A word overflowing a bounded max that fits no later + argument reports the pointed occurrence limit. -multipleunique (only valid if -multiple is true) If true, when multiple values are stored against the same argument name due to -multiple being true, the values must be unique. @@ -8967,6 +8977,28 @@ tcl::namespace::eval punk::args { set thistype [dict get $ARG_INFO $thisname -type] set tailnames [lrange $names $nameidx+1 end] + #G-053 allocation participation (directed work 2026-08-08): a BOUNDED + #-multiple occurrence range caps greedy collection - once this argument has + #taken max occurrences it cannot take another, so return no-consume and let + #the caller's retreat advance to the next argument (the same yield path as a + #failed type screen). Previously ranges were post-loop enforcement only: + #greedy collection overran a bounded max whenever the type screen could not + #stop it, and occurrencecount then reported the overrun allocation itself + #created (untyped {3 3} followed by a -multiple tail collected 4). A first + #occurrence is never capped (resolve guarantees range max >= 1). Serves both + #the leaders and values loops (shared proc). + if {[dict get $ARG_INFO $thisname -multiple] && $thisname in $namesreceived + && [dict exists $formdict MULTIPLE_RANGES $thisname]} { + set mrange [dict get $formdict MULTIPLE_RANGES $thisname] + if {[dict get $mrange maxbounded] + && [llength [lsearch -all -exact $namesreceived $thisname]] >= [dict get $mrange max]} { + #the G-082 rejection record lets an overflow raise report the pointed + #occurrence limit (kind 'occurrence') instead of a generic overflow + set rejection [dict create kind occurrence argname $thisname word [lindex $all_remaining 0] max [dict get $mrange max]] + return [dict create consumed 0 resultlist {} typelist $thistype rejection $rejection] + } + } + #todo - work backwards with any (optional or not) literals at tail that match our values - and remove from assignability. set ridx 0 #puts "-=============- thisname:'$thisname' thistype:'$thistype' tailnames:'$tailnames' all_remaining:'$all_remaining' [info level -2]" @@ -9197,15 +9229,24 @@ tcl::namespace::eval punk::args { set num_remaining [llength $all_remaining] if {[dict get $ARG_INFO $thisname -optional] || ([dict get $ARG_INFO $thisname -multiple] && $thisname in $namesreceived)} { - #2026-07-14 Agent-Updated: occurrence min/max for -multiple is goal G-053 - #(proposed: -multiple accepts a {min max} range alongside the legacy booleans). - #Not implemented - only boolean -multiple semantics apply here. - #thisname already satisfied, or not required + #thisname already satisfied, or not required - reserve words for later + #REQUIRED arguments so greed here cannot starve them. set tail_needs 0 foreach t $tailnames { if {![dict get $ARG_INFO $t -optional]} { set min_clause_length [llength [lsearch -all -not [dict get $ARG_INFO $t -type] {\?*\?}]] - incr tail_needs $min_clause_length + #G-053 allocation participation (directed work 2026-08-08): a + #required argument with a range minimum needs min OCCURRENCES + #reserved, not one - e.g a following required -multiple {3 3} + #reserves 3 clauses' worth of words. (Legacy boolean -multiple + #has no MULTIPLE_RANGES entry; an -optional arg can never carry + #min >= 1 - resolve rejects the contradiction.) + set t_occurrences 1 + if {[dict exists $formdict MULTIPLE_RANGES $t]} { + set t_occurrences [dict get $formdict MULTIPLE_RANGES $t min] + if {$t_occurrences < 1} {set t_occurrences 1} + } + incr tail_needs [expr {$t_occurrences * $min_clause_length}] } } set all_remaining [lrange $all_remaining 0 end-$tail_needs] @@ -10920,7 +10961,17 @@ tcl::namespace::eval punk::args { incr clause_length } } - incr valmin $clause_length + #G-053 allocation participation (directed work 2026-08-08): a required + #value with a range minimum contributes min OCCURRENCES to the floor, + #not one - so the leader/value split and the option-scan reservation + #keep enough words back for e.g a required -multiple {3 3} value. + #(An explicit '@values -min' overrides this whole derivation.) + set v_occurrences 1 + if {[dict exists $MULTIPLE_RANGES $vname]} { + set v_occurrences [dict get $MULTIPLE_RANGES $vname min] + if {$v_occurrences < 1} {set v_occurrences 1} + } + incr valmin [expr {$v_occurrences * $clause_length}] } } else { set valmin $VAL_MIN @@ -11006,6 +11057,19 @@ tcl::namespace::eval punk::args { } else { set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string } + #G-053 allocation participation (directed work 2026-08-08): a + #bounded -multiple range on the (last) leader caps the leader + #scan - once max occurrences are assigned, remaining words belong + #to the opts/values sections rather than overrunning the leader. + if {$is_multiple && $leader_posn_name ne "" + && [dict exists $MULTIPLE_RANGES $leader_posn_name] + && [dict exists $leader_posn_names_assigned $leader_posn_name]} { + set lmr [dict get $MULTIPLE_RANGES $leader_posn_name] + if {[dict get $lmr maxbounded] + && [dict get $leader_posn_names_assigned $leader_posn_name] >= [dict get $lmr max]} { + break + } + } if {$OPT_MAX ne "0" && [string match -* $raw]} { #all_opts includes end_of_opts marker -- if configured - no need to explicitly check for it separately set possible_flagname $raw @@ -12230,6 +12294,14 @@ tcl::namespace::eval punk::args { set rj_name [dict get $rj name] set msg [private::unavailable_choice_msg $rj_argclass $rj_argname $ldr $rj_name [Dict_getdef $argstate $rj_argname -choicelabels {}] [dict get $rj choices]] return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceunavailable $rj_name choices [dict get $rj choices]] -badarg $rj_argname -badval $ldr -argspecs $argspecs]] $msg + } elseif {[dict get $rj kind] eq "occurrence"} { + #G-053 allocation participation: the argument reached its + #bounded -multiple max and this word overflowed - report the + #occurrence limit (same errorcode class as post-loop + #enforcement; count is the attempted occurrence) + set rj_max [dict get $rj max] + set msg "$rj_argclass '$rj_argname' for %caller% accepts at most $rj_max occurrence(s). Received extra word: '$ldr'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list occurrencecount $rj_argname count [expr {$rj_max + 1}] max $rj_max] -badarg $rj_argname -badval $ldr -argspecs $argspecs]] $msg } else { set rj_type [dict get $rj type] set msg "$rj_argclass '$rj_argname' for %caller% requires type '$rj_type'. Received: '$ldr'" @@ -12487,6 +12559,15 @@ tcl::namespace::eval punk::args { set rj_name [dict get $rj name] set msg [private::unavailable_choice_msg $rj_argclass $rj_argname $val $rj_name [Dict_getdef $argstate $rj_argname -choicelabels {}] [dict get $rj choices]] return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceunavailable $rj_name choices [dict get $rj choices]] -badarg $rj_argname -badval $val -argspecs $argspecs]] $msg + } elseif {[dict get $rj kind] eq "occurrence"} { + #G-053 allocation participation: the argument reached its + #bounded -multiple max and this word overflowed - report the + #occurrence limit (same errorcode class as post-loop + #enforcement; count is the attempted occurrence) + #(MAINTENANCE - same selection logic as leaders loop above) + set rj_max [dict get $rj max] + set msg "$rj_argclass '$rj_argname' for %caller% accepts at most $rj_max occurrence(s). Received extra word: '$val'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list occurrencecount $rj_argname count [expr {$rj_max + 1}] max $rj_max] -badarg $rj_argname -badval $val -argspecs $argspecs]] $msg } else { set rj_type [dict get $rj type] set msg "$rj_argclass '$rj_argname' for %caller% requires type '$rj_type'. Received: '$val'" diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index e57f1348..2018424e 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.24.0 +0.25.0 #First line must be a semantic version number #all other lines are ignored. +#0.25.0 - G-053 allocation participation (directed work, post-G-053): bounded -multiple occurrence ranges now PARTICIPATE in positional allocation for leaders and values instead of being post-loop enforcement only. Previously greedy collection overran a bounded max unless a type screen happened to stop it (untyped {3 3} followed by a -multiple tail collected 4 words and occurrencecount then reported the overrun the allocation itself created), and a later REQUIRED ranged argument was starved to a single reserved clause. Three cooperating sites: (a) get_dict_can_assign_value caps a satisfied -multiple argument at its bounded max - it yields the word (no-consume) so the caller's retreat advances to the next argument, serving both the leaders and values loops; the yield carries a G-082 rejection record of new kind 'occurrence', and both loops' overflow selection sites render it as the pointed occurrence-limit report with the G-053 occurrencecount errorcode (count = the attempted occurrence) instead of the generic toomanyarguments - so genuine over-supply keeps its pointed class; (b) reservations are min-occurrence aware - the allocator's tail_needs and the derived valmin floor (leader/value split + option-scan reservation) reserve min-occurrences x min-clause-length for a required ranged argument (e.g a following required -multiple {3 3} reserves 3 clauses; {2 2} pair clauses reserve 4 words); an explicit '@values -min' still overrides the derived floor, and legacy boolean -multiple (no MULTIPLE_RANGES entry) reserves exactly as before; (c) the greedy leader scan caps a bounded -multiple last leader at max occurrences (tracked via leader_posn_names_assigned) so remaining words flow to the opts/values sections. First occurrences are never capped (resolve guarantees range max >= 1); {0 1} at-most-once scalars are unaffected (no collection). define -help -multiple documents the allocation semantics. New multipleranges.test allocation pins: untyped/typed {3 3} cap incl the cap-beats-type-screen 4-ints case, greedy-up-to-max {1 2}, required-range and pair-clause reservations, leaders-side cap + split floor, pointed over-supply report, legacy-greed-unchanged guard. Full punk/args suite 408/0. #0.24.0 - G-084 increment 2 (parsekey completeness, part 2 - the settled cross-member design + integrity closes): -multiple on a member of a shared-parsekey OPTION group is now a DEFINE-TIME error unless ALL members belong to one @opts group marked -parsekeymode error (whose G-083 mutual exclusivity makes per-member collection well-defined; a single -multiple member with its own parsekey is unrestricted). This settles the G-084 cross-member accumulation design decision as rejection rather than accumulate-in-received-order: cross-member collection on one storage key is ambiguous (collect-vs-replace undefined; one member's collected list would silently displace another's). The check runs AFTER the G-053 -multiple canonicalisation so range forms get correct verdicts: {0 1} is replace-shape and allowed on a shared group, collect shapes (boolean 1, max>1, unbounded) are rejected. Value -parsekey integrity settled at define time: duplicate value parsekeys are rejected (trailing values consume positionally so distinct values sharing one result slot silently overwrote each other), and a dash-led value parsekey is rejected (it landed in the options namespace of the result/received dicts and ABORTED parse with an internal error in the option-shaped validation paths). parse_status/parse_report storage-key attribution: parse_status_build now resolves a storage key claimed by exactly ONE argument (a value's -parsekey, an option's declared -parsekey or its '='-suffixed trimmed tail) back to that argument, so argstatus/Source report received with the value-in-effect instead of default/0; shared-parsekey group members stay unresolved (per-member attribution does not survive the storage fold - the remaining recorded G-084 display gap, needs engine-level per-member tracking in the parse result; parse_report's display bridge still shows the group value on each member row). undefine/undefine_deflist now return "" (previously returned the final 'dict unset' expression - the WHOLE cache dict, hundreds of KB, to any caller capturing the value). define -help now documents -parsekey (storage-key rename for options and values, leader rejection, shared groups and -parsekeymode interaction, last-defined-member-wins defaults precedence, the -multiple rule, value parsekey uniqueness/non-dash rules). Internal: dead values-loop identifiers removed (val_ident/val_ident_is_parsekey/values-side seen_pks - assigned per word, never read since the api_valname keying landed), the per-parse parsekey reverse map now iterates only VAL_NAMES (leader map removed entirely - a leader -parsekey is rejected at define time), a dead parsekey-from--default derivation removed in resolve, stale pre-G-084 comments corrected. parsekey.test: parsekey_shared_key_multiple_rejected strengthened (message pins, {0 1}-allowed/{0 4}-rejected range coverage, the -parsekeymode error escape hatch incl collection and optionconflict), new parsekey_value_parsekey_integrity; parsereport.test parsereport_storagekey_attribution + parsestatus.test parsestatus_storagekey_attribution pin the attribution (incl the shared-group default/default remaining-gap row). Full punk/args suite 401/0. #0.23.0 - G-084 increment 1 (parsekey completeness, part 1): value -parsekey is now supported (was: accepted at define time but aborted parse). A value record declaring -parsekey now renames its result `values` slot and its `received` key to the parsekey (e.g `@values {v1 -parsekey renamed -type string}` parses `hello` to `values={renamed hello}`, `received` keyed by `renamed`), and -multiple value members collect under the parsekey (e.g the `variable` setvalues form `?name value...?` pairs collect under `name_value`). The fix keys VAL_DEFAULTS and the values_dict init/storage by parsekey (matching the already-parsekey-keyed VAL_REQUIRED), tracks the parsekey in a new api_valnames_received list (the internal valnames_received stays argname-keyed for the -multiple first/continuation gate and G-053 occurrence enforcement), uses api_valnames_received for the required-satisfaction check and the received dict, and adds a per-form val_pk2name/leader_pk2name reverse map so the valmin clause-length fallback and the post-parse validation loop can resolve a parsekey back to its argname for ARG_INFO/arg_checks lookups. -parsekey on a leader is now REJECTED at define time with a clear message (was: silently ignored) - no live caller uses a leader -parsekey, so the goal's no-silent-ignore contract is met by rejection rather than parallel hot-path surgery; the dead req_name derivation in the leaders resolve branch is removed. The @values directive line still rejects -parsekey (a group-default parsekey is not a feature; per-arg value -parsekey is). The defaulted-members precedence rule for a shared-parsekey group (none received) is now documented as last-defined-member-wins (deterministic in definition order) and the in-code `? review` is removed. The tclcore moduledoc `#todo - fix -parsekey for leaders and values` is resolved (value supported, leader rejected). parsekey.test: the value GAP flipped to parsekey_value_result_key_settled, the leader GAP flipped to parsekey_leader_parsekey_rejected, the @values-line GAP split to parsekey_values_line_rejects_parsekey; the defaults GAP flipped to a settled pin. dev/parsekey-knownbugs.test: the value and leader disabled pins retired (settled in parsekey.test); the cross-member -multiple accumulation pin remains (increment 3). Full punk/args suite 399/0 (1 skipped), punk/ns 125/125, broader punk sweep 989/0. #0.22.0 - G-053 range-valued -multiple (occurrence arity): -multiple now accepts a {min max} range (max -1 = unbounded) alongside the legacy booleans 0/1, so a definition can declare at most once ({0 1}, a second occurrence is a parse error), bounded repetition ({2 4}), or one-or-more ({1 -1}) instead of choosing between silent last-wins (0) and unbounded collection (1). 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). 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). Resolve validation: max must be positive or -1, min <= max, and the -optional/range-min reconciliation - a non-zero min forces presence so -optional set is a contradiction (reject with a clear message; declare -optional 0). 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); over-max is a hard contradiction (fires in both 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 and classified incomplete so a viable form reports incomplete, not invalid). The usage-table Multi column reflects the range (0-1 / 2-4 / 1+ for unbounded-with-floor; the 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 with max>1 ranges unchanged. define -help documents the range form and the -optional/range-min rule. New testsuite multipleranges.test (28 tests: 13 define-time canonicalisation+validation, 12 parse-time enforcement incl parse_status verdicts and legacy-required-still-trailingvaluecount guard, 3 rendering); full punk/args suite 399/0, punk/ns 125/125. diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index b44ed274..0532bd86 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -43,7 +43,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `commandstack/` — commandstack (cooperative command renaming) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0 + the 0.7.0 convenience removal forms + the 0.7.1 reload-contract state guards (G-160 follow-ons, 2026-08-04); usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), multi-renamer stacking with removal in any order (bottom-removal re-linking), builtin renames (next_implementor `original`), remove_rename's three argument forms + unknown-renamer errors, the 0.7.0 convenience removal forms (pop_rename - command form pops topmost-for-renamer and returns the removed record, bare form searches live stacks with a multi-command ambiguity error; remove_renamer - all of a renamer's entries across live stacks with correct re-linking, Rename_stack-parked stacks skipped, removed records returned keyed by command; restore_original - whole-stack unwind to the original returning the record count, deliberately registering stack-evidenced renamers so it survives a known_renamers reset while the renamer-explicit forms keep the gate; all silent with debug off), the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the reload contract (a module re-source preserves ALL state - stacks/token counters/token map/known_renamers/debug each info-exists guarded; delegation and exact-token removal keep working after reload), the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). Green on tclsh90 (9.0.3) and punk86 (8.6) - `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack) - `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW; extended 2026-08-05 by the G-151 pre-modification coverage survey - VIEW -lf 0/1/2 single-line-ization modes + always-on NUL + silent unknown-option tolerance (load-bearing for overtype's '-nul 1' sites) + C1/zero-width visuals, RANGE end-relative/clamping plus the merged-code-stack re-emission and trailing-code-drop semantics a truncate primitive must respect, COUNT-vs-length combining-accent divergence, NEW object basics, and the KNOWN-DEFICIENCY pin that VIEWCODES/VIEWSTYLE raise invalid-command in a plain punk::ansi interp (bare 'a+'/'a' interactive-alias dependency; pinned in a fresh child interp); plus the TRUNCATE primitive pins landed with G-151 (punk::ansi 0.2.0) - fits-unchanged byte identity incl styled fixtures, grapheme-exact capping with marker budget participation and marker-alone truncation, SGR-reset-before-marker on styled prefixes, CJK/combining-cluster grapheme safety, and strict option errors (deliberately unlike VIEW's pinned silent tolerance)), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify), grepstr characterization (`grepstr.test`: return modes incl summarydict (linemap pinned as always-present - the -help says -n-only, reconciliation deferred to the planned hygiene pass), exact highlight SGR wrapping, -n line numbering, invert + empty-highlight strip, -C context/breaks, capture groups, and the tab deficiency: warns once per call on stderr, single-pass tab line survives - the multi-pass mangling is pinned at consumer level in punk/ns corp.test), and untabify characterization (`untabify.test`: -stops int/list/terminal, -with spaces/unicode/custom-pair, multiline, errors, plus the EXPERIMENTAL -plastic elastic-tabstop mode deliberately pinned-as-interim and retained for possible repl editbuf use). Console queries (get_tabstops/get_size + punk::console::tabwidth) are mocked per the overtype renderline.test pattern - they emit live terminal queries that block/error headless. ANSI codes in these tests are literal escape strings so results are colour-state independent -- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms (incl the 2026-08-05 candidacy fencing/fall-through primitive pins ahead of G-168: -regexprefail on optional value slots is form-fatal with no re-landing, unknown-option/dangling-option/option-value-fence failures fall through to sibling forms as successful parses, and the documented per-word limit - a complete option+value+file line double-matches a fenced scriptfile form and a dash-tolerant stdin form), registered-namespace lazy definition loading (`docpackages.test`, G-169 pre-work: inert registration/scan-vs-load split, id_exists never triggers loading, real_id/usage lazy resolution incl tag-prefixed script-level id families, duplicate-definer last-loaded-wins characterization), the G-168 launch-definition model parity (`punkexemodel.test`, punkexe moduledoc 0.7.0, id loading updated for the G-169 handover - the script id resolves via app-punkscript-docs from src/lib (auto_path derived from the test file location) while the moduledoc carries the core-owned ids: script/tclsh selection matrices against the real (script)::punkexe ids with the real-side dispatch/app oracle recorded in goals/archive/G-168, the bare-'-e' viable-incomplete verdict, the ./-e fence message, -encoding fall-through statuses, and the sanctioned complete-'-encoding' multipleformmatches LIMIT pins; the formcheck.test punkexe GAPs flipped to discriminated/sanctioned/selection-sound pins in the same arc), the G-151 landing report (`parsereport.test`, punk::args 0.18.0 parse_report: the canonical flag-like-word-consumed-as-VALUE attribution row, parsed-result vs words+withid entry parity, machine dict shape with declaration-section row order and absent-optional row omission, received xN multiplicity for solos/-multiple opts, type-aware always-marked elision at the default width plus the caller > @cmd -reportvaluewidth > built-in width cascade and never-elided dict returns, VIEW-style single-line-ization of control-bearing values, aliased-optionset storage-key row bridging plus the G-084 storage-key attribution pin (value-parsekey and renamed-option rows attribute received; a shared-parsekey group member row keeps the bridged value with Source default - the recorded remaining gap), bordered-table/tableobject renders, words-form failure errorcode parity with parse, and the registered two-form definition's own render), rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping, and the G-084 unique-storage-key attribution pin - argstatus reports received/value through a value's -parsekey or a renamed option's declared parsekey), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus the G-084 settled pins (2026-08-07): value -parsekey rename support, @values-line -parsekey rejection, leader -parsekey define-time rejection, last-defined-member defaults precedence, cross-member -multiple define-time rejection (allowed only when ALL members share one @opts -parsekeymode error group; a {0 1} range replace-shape stays allowed) and value-parsekey integrity (duplicate/dash-led value parsekeys rejected at define time); `testsuites/dev/parsekey-knownbugs.test` holds no disabled pins (retained as the future punkargsKnownBug home); the parsekey/optname collision GAP is flipped (G-083, 2026-08-07: -parsekey colliding with a distinct defined arg's name is now a resolve error unless that arg shares the parsekey), and the G-083 argument-relations define-time vocabulary is pinned in `relations.test` (per-arg -conflicts target validation, @opts -parsekeymode override|error storage and the requires-group/bad-value/requires-parsekey resolve errors), plus its increment-2 parse-time enforcement (optionconflict raise for -conflicts violations and -parsekeymode error co-occurrence, defaults-never-conflict, cross-group conflicts, parse_status invalid classification), and its increment-3 usage/synopsis rendering and lsearch moduledoc adoption (conflicts-with hints appended to per-arg help rows, -parsekeymode error groups annotated 'mutually exclusive' in group headers with override groups unannotated, synopsis one-line form carrying no conflict detail; and the lsearch tclcore moduledoc modelling -sorted/-glob and -bisect/-all incompatibilities via per-arg -conflicts while -glob/-regexp stay last-wins and the 'punk::args fixes required' caveat is dropped))), the G-053 range-valued -multiple occurrence arity (`multipleranges.test`, punk::args 0.22.0: {min max} range canonicalisation into a per-form MULTIPLE_RANGES companion dict while the stored -multiple boolean keeps its hot-path list-collect meaning so legacy 0/1 stay byte-unchanged, the -optional/range-min reconciliation resolve error, the {0 1}/{2 4}/{1 -1} scalar-vs-list value shapes, parse-time occurrencecount enforcement with min-under-supply suppressed in the G-152 viability probe and classified incomplete (pure exhaustion) while over-max is invalid, the usage-table Multi column showing 0-1/2-4/1+ with the synopsis ?arg? at-most-once ellipsis distinction, and -multipleunique composition), longopt style-distinction and mash-edge characterisation (`longopts.test`, 2026-08-07 gap-fill: the gnu (`--flag=` inline-only; spaced/solo usage = badoptionformat) vs plain (`--flag` spaced-only; inline `=` = invalidoption) vs mixed (`-f|--file|--file=` both forms incl longopt prefix abbreviation) definition-grammar distinctions, first-`=`-only value splitting and empty inline values, the two `=`-member resolve-time definition errors, single-dash `-flag=value` never split at the `=`, and `@opts -any 1` adhoc passthrough where `--flag=value` splits but `-flag=value` stays a whole adhoc flagname expecting a value; the same gap-fill added to `mashopts.test`: unknown-letter-in-mash invalidoption rejection and the `-any` interplay - defined flags still mash while undefined mash-lookalike tokens are adhoc flags, never mash attempts; error-shape pins use the first three -errorcode elements), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix; version-note labels appear when the class is live OR is a declared forward unavailable class - dict labels on every runtime since G-073, unicode keeps the live-only rule; the G-073 forward-class adoption invariants and the USER-SANCTIONED 'di' prefix-strictness exemption (the model is deliberately stricter than real 8.6, full words parity-true); and the G-166 pin that an unavailable class's per-class virtual id LEADS its help with the unavailability statement on runtimes lacking the class while modern runtimes generate it unchanged. All expectations are derived from the running interpreter, green on 8.6/8.7/9.0 through runtests.tcl on each - native Tcl 8.6 has been a supported runner interpreter since 2026-07-21) +- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms (incl the 2026-08-05 candidacy fencing/fall-through primitive pins ahead of G-168: -regexprefail on optional value slots is form-fatal with no re-landing, unknown-option/dangling-option/option-value-fence failures fall through to sibling forms as successful parses, and the documented per-word limit - a complete option+value+file line double-matches a fenced scriptfile form and a dash-tolerant stdin form), registered-namespace lazy definition loading (`docpackages.test`, G-169 pre-work: inert registration/scan-vs-load split, id_exists never triggers loading, real_id/usage lazy resolution incl tag-prefixed script-level id families, duplicate-definer last-loaded-wins characterization), the G-168 launch-definition model parity (`punkexemodel.test`, punkexe moduledoc 0.7.0, id loading updated for the G-169 handover - the script id resolves via app-punkscript-docs from src/lib (auto_path derived from the test file location) while the moduledoc carries the core-owned ids: script/tclsh selection matrices against the real (script)::punkexe ids with the real-side dispatch/app oracle recorded in goals/archive/G-168, the bare-'-e' viable-incomplete verdict, the ./-e fence message, -encoding fall-through statuses, and the sanctioned complete-'-encoding' multipleformmatches LIMIT pins; the formcheck.test punkexe GAPs flipped to discriminated/sanctioned/selection-sound pins in the same arc), the G-151 landing report (`parsereport.test`, punk::args 0.18.0 parse_report: the canonical flag-like-word-consumed-as-VALUE attribution row, parsed-result vs words+withid entry parity, machine dict shape with declaration-section row order and absent-optional row omission, received xN multiplicity for solos/-multiple opts, type-aware always-marked elision at the default width plus the caller > @cmd -reportvaluewidth > built-in width cascade and never-elided dict returns, VIEW-style single-line-ization of control-bearing values, aliased-optionset storage-key row bridging plus the G-084 storage-key attribution pin (value-parsekey and renamed-option rows attribute received; a shared-parsekey group member row keeps the bridged value with Source default - the recorded remaining gap), bordered-table/tableobject renders, words-form failure errorcode parity with parse, and the registered two-form definition's own render), rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping, and the G-084 unique-storage-key attribution pin - argstatus reports received/value through a value's -parsekey or a renamed option's declared parsekey), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus the G-084 settled pins (2026-08-07): value -parsekey rename support, @values-line -parsekey rejection, leader -parsekey define-time rejection, last-defined-member defaults precedence, cross-member -multiple define-time rejection (allowed only when ALL members share one @opts -parsekeymode error group; a {0 1} range replace-shape stays allowed) and value-parsekey integrity (duplicate/dash-led value parsekeys rejected at define time); `testsuites/dev/parsekey-knownbugs.test` holds no disabled pins (retained as the future punkargsKnownBug home); the parsekey/optname collision GAP is flipped (G-083, 2026-08-07: -parsekey colliding with a distinct defined arg's name is now a resolve error unless that arg shares the parsekey), and the G-083 argument-relations define-time vocabulary is pinned in `relations.test` (per-arg -conflicts target validation, @opts -parsekeymode override|error storage and the requires-group/bad-value/requires-parsekey resolve errors), plus its increment-2 parse-time enforcement (optionconflict raise for -conflicts violations and -parsekeymode error co-occurrence, defaults-never-conflict, cross-group conflicts, parse_status invalid classification), and its increment-3 usage/synopsis rendering and lsearch moduledoc adoption (conflicts-with hints appended to per-arg help rows, -parsekeymode error groups annotated 'mutually exclusive' in group headers with override groups unannotated, synopsis one-line form carrying no conflict detail; and the lsearch tclcore moduledoc modelling -sorted/-glob and -bisect/-all incompatibilities via per-arg -conflicts while -glob/-regexp stay last-wins and the 'punk::args fixes required' caveat is dropped))), the G-053 range-valued -multiple occurrence arity (`multipleranges.test`, punk::args 0.22.0: {min max} range canonicalisation into a per-form MULTIPLE_RANGES companion dict while the stored -multiple boolean keeps its hot-path list-collect meaning so legacy 0/1 stay byte-unchanged, the -optional/range-min reconciliation resolve error, the {0 1}/{2 4}/{1 -1} scalar-vs-list value shapes, parse-time occurrencecount enforcement with min-under-supply suppressed in the G-152 viability probe and classified incomplete (pure exhaustion) while over-max is invalid, the usage-table Multi column showing 0-1/2-4/1+ with the synopsis ?arg? at-most-once ellipsis distinction, and -multipleunique composition; plus the allocation-participation pins (directed work 2026-08-08, punk::args 0.25.0): bounded ranges cap greedy leader/value collection at max occurrences (incl the cap-beats-type-screen case), min-occurrence-aware reservations keep a later required ranged argument fed (single and pair clauses, the leaders-side scan cap and split floor), genuine over-supply reports the pointed occurrence limit at the overflow site, legacy boolean greed unchanged), longopt style-distinction and mash-edge characterisation (`longopts.test`, 2026-08-07 gap-fill: the gnu (`--flag=` inline-only; spaced/solo usage = badoptionformat) vs plain (`--flag` spaced-only; inline `=` = invalidoption) vs mixed (`-f|--file|--file=` both forms incl longopt prefix abbreviation) definition-grammar distinctions, first-`=`-only value splitting and empty inline values, the two `=`-member resolve-time definition errors, single-dash `-flag=value` never split at the `=`, and `@opts -any 1` adhoc passthrough where `--flag=value` splits but `-flag=value` stays a whole adhoc flagname expecting a value; the same gap-fill added to `mashopts.test`: unknown-letter-in-mash invalidoption rejection and the `-any` interplay - defined flags still mash while undefined mash-lookalike tokens are adhoc flags, never mash attempts; error-shape pins use the first three -errorcode elements), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix; version-note labels appear when the class is live OR is a declared forward unavailable class - dict labels on every runtime since G-073, unicode keeps the live-only rule; the G-073 forward-class adoption invariants and the USER-SANCTIONED 'di' prefix-strictness exemption (the model is deliberately stricter than real 8.6, full words parity-true); and the G-166 pin that an unavailable class's per-class virtual id LEADS its help with the unavailability statement on runtimes lacking the class while modern runtimes generate it unchanged. All expectations are derived from the running interpreter, green on 8.6/8.7/9.0 through runtests.tcl on each - native Tcl 8.6 has been a supported runner interpreter since 2026-07-21) - `punk/nav/ns/` — punk::nav::ns tests (`testsuites/nav/navns.test`): the n/ n// n/// navigation state machine (ns/ transitions absolute/relative/glob-no-nav, failed-nav state preservation, quad-colon normalization, v-form content selection, ensemble annotation) and the ::punk::nav::ns::ns_current variable contract the repl/codethread/subshell seeding all consume; display content is covered in punk/ns nslist.test - `punk/repl/` — punk::repl tests (`testsuites/repl/`): opunk console backend integration (`consolebackends.test`) and repl current-namespace retention (`nscurrent.test`: real codethread via repl::init driven by synchronous runscript sends - inscope evaluation of ns_current, retention across submissions, n/-navigation retained, auto-create-with-notice for missing namespaces, the 2026-07-14 stray-namespace seeding fix pinned behaviourally plus a source-text guard on repl::start's inline template; the end-to-end piped subshell session is covered at shell level by shell/testsuites/punkexe/shellnavns.test - which found the first-subshell shared-code-interp asymmetry and the piped-inscope gap recorded there) - `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity; plus the G-166 availability axis on the flowunavail/flowunavail_nodoc fixtures - the `unavailable` cmdinfo key present on every result, exact and unique-prefix landings on a `-choiceunavailable` name resolving that name's virtual docid ATTRIBUTED rather than resolving nothing, the choice traverse attributing without addressing when no virtual docid exists, and cmdtype staying unchanged throughout since availability is a second axis, never a cmdtype value), n/ display machinery characterization (`nslist.test`: tier A get_ns_dicts classification buckets as the machine contract for display reworks - incl package tail/prefix derivation, alias edge cases, usageinfo scan-dependence; tier B per-element layout-agnostic marking - underline/underdouble/underdotted namespace package styles, command type tag colours, exported/imported markers, the punkargs doc icon; tier C REWORK-flagged pins of the current hardcoded 2-col/4-col layout and nspath subtables, to flip deliberately with the planned punk-tables/width-responsive rework; plus the flipped nslist_types_default pin - bare nslist without -types displays all member types since punk::ns 0.7.1 fixed the braced-literal -types default), corp proc-retrieval and syntax/untabify interplay (`corp.test`: name edge cases, -ranges/-n line handling, basic-highlight ansistrip equivalence, -untabify spaces/unicode tab-free output, the KNOWN-DEFICIENCY pin for default -untabify none on tabbed bodies - grepstr warns per pass and brace overlays mangle tabbed lines, deterministic under mocked console tabstops - and a ::tcl::CopyDirectory -untabify spaces smoke test; precursor coverage for the planned punk::ns hygiene pass), cmdtrace characterization (`cmdtrace.test`: -pause 0 non-interactive runs, linedict line-mark keys for flat and 2-word-form nested switches as correct-mark guards, and GAP pins for the upstream nested-switch mismark - core.tcl-lang.org tktview 5d5b1052280c976ea3d4, arm bodies whose split-list index lands on a literal switch-command word report container-relative lines; mark tests gated on have_tclcoredocs because cmdtrace's arm-offset correction parses against the ::switch argdoc; plus the fixed-canary asserting punk::lib::check::has_tclbug_nestedswitch_tracelines still reports the bug - a live behavioural probe, so a fixed Tcl release fails the canary first and triggers the documented flip workflow), cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); the flipped G-051 pins for pseudo-command cmdtype (`doconly`) + space-form docid prefixes (real `string is` pins behind the have_tclcoredocs constraint); the G-166 unavailability marking on the helpstrua fixture and on the real `string is dict` subject - the `-return dict` key, the table/string marking with the info scheme suppressed so a valid argument tail no longer renders as a cleanly usable command line, and the `-return text` leading `UNAVAILABLE:` line (the two deliberate key-list flips - `cmdhelp_cmdinfo_result_shape`, `cmdhelp_return_dict_valid` - landed with it); remaining GAP pins for TclOO undocumented-method fallback (G-052) and synopsis marking absence (G-050); plus the G-150 flag-led form-narrowing + selection-soundness pins against punk::auto_exec::hash - bare-parse selection contract (incl the dash-led-name noformmatch cost), no-word/unknown-flag/ambiguity whole-render fallbacks, and hash runtime-behaviour-unchanged), and name/path primitive characterization (`nsprimitives.test`: string pins for nsparts/nsprefix/nstail/nsjoin/nsjoinall incl weird colon-run (`:::`) splitting, the trailing-colon parse ambiguity (`::x:` + `y` joins to the same string as `::x` + `:y` and reparses leading-colon-greedy), and prefix/tail/join round-trip and its absolutizing exceptions (the original twin-divergence pins for nsparts1/nsprefix1/nsprefix_orig/nstail1/nstail_orig served as safe-deletion evidence and were removed with the twins in the punk::ns 0.7.0 hygiene pass - divergence record in this file's git history, commit 0c7168a1); plus nseval fq-requirement/create-on-eval/evaluator-proc caching, the native-vs-punk `p:::x` resolution divergence (native namespace eval reaches child `x`, nseval creates/reaches literal `:x`), nseval_ifexists no-create + error propagation on plain and genuinely weird namespaces, nsexists/nschildren/nstree_raw weird-ns and relative-resolution pins, globmatchns `*`/`**`/`?` semantics (incl `*` matching a single inner colon - a formerly stale 'should be fixed' comment above nsglob_as_re was corrected in the 0.7.0 hygiene pass), and nspath_to_absolute/nspath_here_absolute caller-resolution pins; the nsjoinall error-message wart pin ('nsjoin:' prefix) flipped when punk::ns 0.7.1 fixed it) diff --git a/src/tests/modules/punk/args/testsuites/args/multipleranges.test b/src/tests/modules/punk/args/testsuites/args/multipleranges.test index c3c2cb24..dd6e7634 100644 --- a/src/tests/modules/punk/args/testsuites/args/multipleranges.test +++ b/src/tests/modules/punk/args/testsuites/args/multipleranges.test @@ -351,5 +351,84 @@ namespace eval ::testspace { set result } -result [list 1] + #-------------------------------------------------------------------- + # allocation participation (directed work 2026-08-08, post-G-053) + #-------------------------------------------------------------------- + #added 2026-08-08 (agent) - directed work: bounded -multiple occurrence ranges + #now PARTICIPATE in positional allocation instead of being post-loop + #enforcement only. Previously greedy collection overran a bounded max unless a + #type screen happened to stop it (untyped {3 3} followed by a -multiple tail + #collected 4 and then occurrencecount reported the overrun allocation itself + #created), and a later REQUIRED ranged argument was starved to a single + #reserved clause. Three cooperating sites: the get_dict_can_assign_value cap + #(an argument at its bounded max yields, serving both the leaders and values + #loops), min-occurrence-aware reservations (the allocator's tail_needs and the + #derived valmin floor - an explicit '@values -min' still overrides), and the + #leader-scan cap for a bounded -multiple last leader. + + test multirange_alloc_cap_untyped_value {untyped {3 3} value takes exactly 3 then yields to a following -multiple tail}\ + -setup $common -body { + set argd [punk::args::parse {a 1 2 3 e f} withdef @values v1 {nums -multiple {3 3}} {tail -multiple 1}] + lappend result [dict get $argd values] + set result + } -result [list {v1 a nums {1 2 3} tail {e f}}] + + test multirange_alloc_cap_beats_type_screen {typed {3 3} value with four consecutive ints stops at 3 (cap, not the int screen)}\ + -setup $common -body { + set argd [punk::args::parse {a 1 2 3 4 e} withdef @values v1 {nums -multiple {3 3} -type int} {tail -multiple 1}] + lappend result [dict get $argd values] + set result + } -result [list {v1 a nums {1 2 3} tail {4 e}}] + + test multirange_alloc_cap_bounded_variable {bounded-variable {1 2} takes up to its max (greedy-up-to-max) then yields}\ + -setup $common -body { + set argd [punk::args::parse {1 2 3 4} withdef @values {nums -multiple {1 2}} {tail -multiple 1}] + lappend result [dict get $argd values] + set result + } -result [list {nums {1 2} tail {3 4}}] + + test multirange_alloc_reservation_required_range {a greedy earlier -multiple cannot starve a later required {3 3} (min-occurrence reservation)}\ + -setup $common -body { + set argd [punk::args::parse {x y 1 2 3} withdef @values {head -multiple 1} {nums -multiple {3 3}}] + lappend result [dict get $argd values] + #clause-size 2 with {2 2}: reserves 2 occurrences x 2 words + set argd [punk::args::parse {x a 1 b 2} withdef @values {head -multiple 1} {pairs -multiple {2 2} -type {string int}}] + lappend result [dict get $argd values] + set result + } -result [list\ + {head {x y} nums {1 2 3}}\ + {head x pairs {{a 1} {b 2}}}\ + ] + + test multirange_alloc_leaders_side {leaders side: a bounded {3 3} last leader caps the leader scan and its loop; leader/value split reserves for a required ranged value}\ + -setup $common -body { + #bounded -multiple leader takes exactly 3, remaining words flow to values + set argd [punk::args::parse {1 2 3 e f} withdef {@leaders -min 0 -max -1} {nums -multiple {3 3}} @values {tail -multiple 1}] + lappend result [list [dict get $argd leaders] [dict get $argd values]] + #greedy -multiple leader yields enough words for a required {3 3} value (valmin floor) + set argd [punk::args::parse {x y 1 2 3} withdef {@leaders -min 0 -max -1} {head -multiple 1} @values {nums -multiple {3 3}}] + lappend result [list [dict get $argd leaders] [dict get $argd values]] + set result + } -result [list\ + {{nums {1 2 3}} {tail {e f}}}\ + {{head {x y}} {nums {1 2 3}}}\ + ] + + test multirange_alloc_oversupply_pointed {genuine over-supply reports the pointed occurrence limit at the overflow site (G-082 rejection kind occurrence)}\ + -setup $common -body { + set err [catch {punk::args::parse {1 2 3 4} withdef @values {nums -multiple {3 3}}} msg eopts] + lappend result [list $err [lindex [dict get $eopts -errorcode] 2] [string match "*accepts at most 3 occurrence(s)*" $msg]] + set result + } -result [list\ + {1 {occurrencecount nums count 4 max 3} 1}\ + ] + + test multirange_alloc_legacy_greed_unchanged {legacy boolean -multiple greed is unchanged (all words minus single-clause reservation)}\ + -setup $common -body { + set argd [punk::args::parse {a b c d} withdef @values {head -multiple 1} {tail -type string}] + lappend result [dict get $argd values] + set result + } -result [list {head {a b c} tail d}] + } tcltest::cleanupTests \ No newline at end of file