From 0f649053ee17a986e9f07cd4a93cc87b183f04bf Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 14 Jul 2026 13:58:44 +1000 Subject: [PATCH] punk::args: characterize stringstartswith tail-clause reservation bug (pre-fix pins) allocation.test gains coverage for the get_dict_can_assign_value multi-member tail-clause reservation walk (a later optional clause mixing literal(...) and stringstartswith(...) members, guarded from a greedy -multiple argument): - guards (must hold pre and post fix): literal-only tail clause reserved correctly; non-matching trailing words correctly left to the -multiple arg - allocation_tailclause_ssw_reservation_GAP pins the current bug: the stringstartswith(*) arm matches against $tp (the type string) instead of $rv (the value), so matching trailing words are swallowed by the -multiple argument (optional tail clause lost), and a prefix text that happens to prefix the type string (str vs stringstartswith(str)) wrongly reserves non-matching words, ending in a spurious toomanyarguments overflow Verified 10/10 against the unfixed module under Tcl 9.0.3. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- .../punk/args/testsuites/args/allocation.test | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/tests/modules/punk/args/testsuites/args/allocation.test b/src/tests/modules/punk/args/testsuites/args/allocation.test index 6eb4d263..706817f9 100644 --- a/src/tests/modules/punk/args/testsuites/args/allocation.test +++ b/src/tests/modules/punk/args/testsuites/args/allocation.test @@ -157,6 +157,97 @@ namespace eval ::testspace { -cleanup $cleanup\ -result [list {1 valid} _default 1] + + #added 2026-07-14 (agent) - tail-clause reservation walk in + #private::get_dict_can_assign_value: a LATER optional clause mixing literal(...) + #and stringstartswith(...) members should have its trailing words reserved from a + #greedy -multiple argument when (and only when) they match. The literal-only + #control and the no-match guard hold today and must keep holding. + test allocation_tailclause_literal_reservation_guard {tail-clause walk reserves trailing words for a literal-only optional clause}\ + -setup { + set result "" + punk::args::define { + @id -id ::testspace::alloc_tail_lit + @values -min 0 -max -1 + items -type string -multiple 1 -optional 1 + ender -type {literal(with) literal(now)} -optional 1 + } + }\ + -body { + set argd [punk::args::parse {a b with now} withid ::testspace::alloc_tail_lit] + lappend result [dict get $argd values] + }\ + -cleanup { + punk::args::undefine ::testspace::alloc_tail_lit 1 + }\ + -result [list\ + {items {a b} ender {with now}} + ] + + test allocation_tailclause_ssw_nomatch_guard {tail-clause walk does not reserve trailing words that do not match the stringstartswith member}\ + -setup { + set result "" + punk::args::define { + @id -id ::testspace::alloc_tail_ssw + @values -min 0 -max -1 + items -type string -multiple 1 -optional 1 + ender -type {literal(with) stringstartswith(v)} -optional 1 + } + }\ + -body { + #q99 does not start with v - items correctly consumes everything + set argd [punk::args::parse {a b with q99} withid ::testspace::alloc_tail_ssw] + lappend result [dict get $argd values] + }\ + -cleanup { + punk::args::undefine ::testspace::alloc_tail_ssw 1 + }\ + -result [list\ + {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}\ + -setup { + set result "" + punk::args::define { + @id -id ::testspace::alloc_tail_ssw_v + @values -min 0 -max -1 + items -type string -multiple 1 -optional 1 + ender -type {literal(with) stringstartswith(v)} -optional 1 + } + punk::args::define { + @id -id ::testspace::alloc_tail_ssw_str + @values -min 0 -max -1 + items -type string -multiple 1 -optional 1 + ender -type {literal(with) stringstartswith(str)} -optional 1 + } + }\ + -body { + #matching trailing words lost to items + 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] + }\ + -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}} + ] + cleanupTests } namespace delete ::testspace