Browse Source

punk::args 0.12.3: fix stringstartswith tail-clause reservation ($tp -> $rv); flip pin

The stringstartswith(*) arm of get_dict_can_assign_value''s multi-member
tail-clause reservation walk compared the prefix against $tp (the type
string) instead of $rv (the candidate value). Consequences (pinned pre-fix
in the previous commit):

- a later optional clause mixing literal(...) and stringstartswith(...)
  members never had genuinely matching trailing words reserved - a greedy
  -multiple argument swallowed them and the tail clause was silently lost
- a prefix text that happened to prefix the literal type string (e.g str vs
  stringstartswith(str)) wrongly reserved NON-matching words, raising a
  spurious toomanyarguments overflow

One-token fix; allocation_tailclause_ssw_reservation_GAP flipped to the
correct expectations (matching words reserved; non-matching words left to
the -multiple argument with no error); literal-only and no-match guards
unchanged. Known remaining approximation (pre-existing, noted in the
buildversion changelog): the walk still requires a literal* member in the
tail clause''s typelist to run at all - a stringstartswith-only tail clause
is not reserved.

Verified: full punk/args suite 231 total / 227 passed / 0 failed (4 intended
skips), punk/ns 57/57 under Tcl 9.0.3; allocation.test 10/10 under Tcl 8.7.
punk::args buildversion 0.12.2 -> 0.12.3; project 0.12.30 + CHANGELOG.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 20 hours ago
parent
commit
d50d1dd6c5
  1. 4
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 12
      src/modules/punk/args-999999.0a1.0.tm
  4. 3
      src/modules/punk/args-buildversion.txt
  5. 30
      src/tests/modules/punk/args/testsuites/args/allocation.test

4
CHANGELOG.md

@ -5,6 +5,10 @@ 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.12.30] - 2026-07-14
- punk::args 0.12.3: fixed the stringstartswith tail-clause reservation bug flagged in the 0.12.2 comment review - the multi-member tail-clause walk in get_dict_can_assign_value compared the prefix against the type string instead of the candidate value, so an optional trailing clause like {literal(with) stringstartswith(v)} lost its matching words to a preceding -multiple argument, and certain prefix spellings caused spurious "Received more values than can be assigned" errors. Characterized pre-fix, pin flipped (allocation.test), guards added for the literal-only and no-match cases.
## [0.12.29] - 2026-07-14
- punk::args 0.12.2 comment/documentation hygiene pass (no behaviour change): ~1100 lines of superseded commented-out code removed (old check_clausecolumn, abandoned _check_clausecolumn2 experiment with its error-collection idea preserved as a note for G-072-era rework, pre-parsekey ordering loop, disabled switch arm, debug remnants); MAINTENANCE loop-parity comments upgraded to a dated refactor assessment (shared-helper extraction deferred to ride G-084 under G-046 hot-path constraints); user-visible doc fixes (get_dict return-shape and directive lists corrected, false solo-flag/last-value-only claims removed, two broken doc examples fixed); goal cross-refs (G-053, G-084) annotated at the relevant code sites; a suspected latent bug in the multi-member clause stringstartswith( arm flagged in a comment (matches the type string instead of the value) without changing behaviour.

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.12.29"
version = "0.12.30"
license = "BSD-2-Clause"

12
src/modules/punk/args-999999.0a1.0.tm

@ -7856,13 +7856,13 @@ tcl::namespace::eval punk::args {
}
"stringstartswith(*" {
set pfx [string range $tp 17 end-1]
#2026-07-14 Agent-Generated: suspected bug - this matches
#2026-07-14 Agent-Updated: fixed - this previously matched
#against $tp (the type string) rather than $rv (the value),
#so the branch always takes the success path ("$pfx*" is a
#prefix-match of the type it was cut from). Behaviour
#deliberately left unchanged in this comment-review pass -
#no pins exist; flag for a correctness pass.
if {[string match "$pfx*" $tp]} {
#so matching trailing words were never reserved for the tail
#clause and a prefix text prefixing the literal type string
#wrongly reserved non-matching words (spurious overflow).
#Pinned by allocation_tailclause_ssw_reservation (allocation.test).
if {[string match "$pfx*" $rv]} {
set alloc_ok 1
incr alloc_count
} else {

3
src/modules/punk/args-buildversion.txt

@ -1,6 +1,7 @@
0.12.2
0.12.3
#First line must be a semantic version number
#all other lines are ignored.
#0.12.3 - fixed the stringstartswith(*) arm of get_dict_can_assign_value's multi-member tail-clause reservation walk: it matched against $tp (the type string) instead of $rv (the candidate value), so for a later optional clause mixing literal(...) and stringstartswith(...) members, genuinely matching trailing words were never reserved from a greedy -multiple argument (the optional tail clause was silently lost), while a prefix text that happened to prefix the literal type string (e.g 'str' vs 'stringstartswith(str)') wrongly reserved non-matching words and raised a spurious toomanyarguments overflow. Found flagged-only in the 0.12.2 comment review; characterized pre-fix then pin flipped (allocation.test allocation_tailclause_ssw_reservation + literal-only and no-match guards). Note the walk still only runs when the tail clause's typelist contains a literal* member (the pre-existing acknowledged approximation) - a stringstartswith-only tail clause remains unreserved.
#0.12.2 - comment/documentation hygiene pass (no behaviour change). Removed ~1100 lines of superseded commented-out code: the old-signature private::check_clausecolumn, the abandoned _check_clausecolumn2 'interim version' experiment (its one keeper idea - collect per-clause validation results and report at end instead of raising on first failure - recorded in an in-place note for G-072-era rework), the pre-parsekey opts ordering loop, a disabled XXXliteral* switch arm in get_dict_can_assign_value's multi-member clause walk (open item noted: literalprefix() members unhandled there), and misc debug remnants. Expanded the leaders/values loop MAINTENANCE notes into a real refactor assessment: shared clause-assignment helper extraction is indicated (drift is real - G-082 duplicated its selection block, parsekey idents exist only values-side) but deferred to ride the G-084 leaders/values parsekey parity work under the G-046 hot-path constraints; until then edits to one loop must be mirrored or justified. Documentation corrections: get_dict PUNKARGS/doctools return shape now lists the real result keys (leaders opts values received solos multis id form + formstatus when candidacy ran); directive lists updated beyond @cmd/@leaders/@opts/@values; removed false pre-modern claims ('only supports -flag val pairs, not solo options', 'only the last value is used'); fixed the broken parse -help example (dict exists bracketing) and the module-header dofilestuff example (stray trailing 1 after -type existingfile); assorted typos. Goal cross-references annotated in code comments (G-053 -multiple occurrence ranges, G-084 values/leaders parsekey, corrected test-name refs to parsekey_repeat_ordering). Flagged (comment only, behaviour unchanged, no pins exist): suspected bug in the multi-member clause stringstartswith( arm - it string-matches against $tp (the type) instead of $rv (the value), so it always takes the success path.
#0.12.1 - G-082 single-form parse error selection: a word rejected by an OPTIONAL argument's allocation screen (G-071 restricted-choice screen, or the basic-type screen: int/double/bool/number/dict) that subsequently overflows the argument list now reports the specific rejection - choiceviolation naming the word and the allowed choices (with -badarg/-badval and the same message template as final validation's report for required args), or typemismatch naming the declared type - instead of the generic 'Received more values than can be assigned' toomanyarguments (regression vs 0.5.0 found during G-030 make.tcl dogfooding; the -optional 0 authoring workaround is no longer required). Mechanism: get_dict_can_assign_value returns a 'rejection' record (single-member clauses only; literal-category mismatches deliberately not recorded - overflow stays the clearer report for syntax-word shapes like lseq 'to'), the three allocation call sites (leader/value split trial, leaders loop, values loop) collect first-rejection-per-word into a per-form store, and the two overflow raise sites prefer a recorded rejection for the unassignable word. Genuinely-surplus words (all optionals satisfied) still report toomanyarguments; words consumed by a later slot parse unchanged; multiform selection (rank_form_failures) unchanged - per-form failures just carry the more specific class. New testsuite errorselection.test (characterized pre-fix, pins flipped deliberately).
#0.12.0 - G-074: new punk::args::formcheck - on-demand multiform ambiguity analysis. Static pairwise pass over a definition's resolved FORMS: enumerates the positional word-slot chains each form can present (leaders then values, branching on -optional arguments and ?-wrapped optional clause members, -multiple capped; options excluded - order-free), derives candidate witness arglists for aligned equal-length chains (discriminator words + type-derived witnesses, screened via choiceword_match/type tests so screening can't diverge from parse acceptance), and only reports a form pair after CONFIRMING a witness with a real single-form parse against BOTH forms (parse_status; no user-supplied words, nothing added to define/resolve cost) - so fully discriminated pairs cannot false-alarm and every reported witness is a genuine multipleformmatches arglist; the documented miss direction is witness derivation (exotic types, forms requiring options, enumeration caps). Findings classify as type_weakness (a discriminator slot - literal()/literalprefix()/restricted choices - aligned with a permissive non-validating type: any/none/string/ansistring/globstring/expr/script; the 'lseq 1 count 5' class) vs structural (forms genuinely share an argument shape; the 'after cancel id|script' class). New @form key -overlapallowed <formname-list> sanctions a KNOWN overlap on either pair member: the finding reports with sanctioned 1 and leaves the unsanctioned list (the actionable/gate subset) - parse behaviour is never affected (multipleformmatches still raises); unknown form names rejected at definition resolve. Returns machine-parsable dict (id/form_names/pairs/findings with witness+per-slot relations/unsanctioned/summary) or -return summary for the report text. New testsuite formcheck.test; @form directive doc updated. Proving consumers: tclcore ::lseq (reports range/start_count AND range/count - both real, root cause the expr-typed end slot) and ::after (exactly the sanctioned cancelid/cancelscript documented overlap).

30
src/tests/modules/punk/args/testsuites/args/allocation.test

@ -206,16 +206,14 @@ namespace eval ::testspace {
{items {a b with q99}}
]
#GAP: the stringstartswith(*) arm of the multi-member tail-clause walk matches
#against $tp (the type string) instead of $rv (the candidate value), so:
# - genuinely matching trailing words are NOT reserved (the -multiple argument
# swallows them and the optional tail clause is lost), and
# - a prefix text that happens to prefix the literal type string (e.g 'str'
# prefixing 'stringstartswith(str)') reserves NON-matching words, ending in a
# spurious overflow error.
#Expected post-fix: {a b with v99} -> items {a b} ender {with v99};
# {a b with zzz} (vs stringstartswith(str)) -> items {a b with zzz}
test allocation_tailclause_ssw_reservation_GAP {GAP: stringstartswith tail-clause member compares the type string, not the value}\
#flipped 2026-07-14 (agent) - was allocation_tailclause_ssw_reservation_GAP pinning
#the pre-fix bug (the stringstartswith(*) arm matched against $tp - the type
#string - instead of $rv the candidate value): matching trailing words were lost
#to the -multiple argument ({items {a b with v99}} with ender omitted), and a
#prefix text prefixing the literal type string ('str' vs 'stringstartswith(str)')
#wrongly reserved non-matching words, raising a spurious
#{toomanyarguments 4 index 2} overflow.
test allocation_tailclause_ssw_reservation {stringstartswith tail-clause member reserves matching trailing words and only those}\
-setup {
set result ""
punk::args::define {
@ -232,20 +230,20 @@ namespace eval ::testspace {
}
}\
-body {
#matching trailing words lost to items
#matching trailing words reserved for the tail clause
set argd [punk::args::parse {a b with v99} withid ::testspace::alloc_tail_ssw_v]
lappend result [dict get $argd values]
#non-matching trailing words wrongly reserved -> spurious overflow error
set err [catch {punk::args::parse {a b with zzz} withid ::testspace::alloc_tail_ssw_str} msg opts]
lappend result $err [lrange [dict get $opts -errorcode] 0 2]
#non-matching trailing words left to the -multiple argument (no reservation, no error)
set argd [punk::args::parse {a b with zzz} withid ::testspace::alloc_tail_ssw_str]
lappend result [dict get $argd values]
}\
-cleanup {
punk::args::undefine ::testspace::alloc_tail_ssw_v 1
punk::args::undefine ::testspace::alloc_tail_ssw_str 1
}\
-result [list\
{items {a b with v99}}\
1 {PUNKARGS VALIDATION {toomanyarguments 4 index 2}}
{items {a b} ender {with v99}}\
{items {a b with zzz}}
]
cleanupTests

Loading…
Cancel
Save