diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a5b891..e5d32b11 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/punkproject.toml b/punkproject.toml index 62479bb4..e9c3edbb 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.12.29" +version = "0.12.30" license = "BSD-2-Clause" diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 7d68adf6..9c3d998e 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/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 { diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index f04a9267..d9800fc5 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/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 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). diff --git a/src/tests/modules/punk/args/testsuites/args/allocation.test b/src/tests/modules/punk/args/testsuites/args/allocation.test index 706817f9..6d15eb48 100644 --- a/src/tests/modules/punk/args/testsuites/args/allocation.test +++ b/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