From d4519f41d3779e54df1431e25c9083194ba63ece Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sun, 12 Jul 2026 23:00:36 +1000 Subject: [PATCH] G-071 achieved: punk::args 0.9.0 allocation choice screen - lseq-class optional-element arglists parse in-form (project 0.12.11) Root cause: get_dict_can_assign_value blanket-satisfied the member check for any argument carrying choices, so an optional restricted-choice value (the lseq '..'/'to' noise word) consumed any word whenever arity permitted, starving later elements/clauses and failing validation with a confusing trailing-choices error ('lseq 0 10 2' shape). Fix: an allocation choice screen using choiceword_match (the shared G-040 implementation - exact/alias/prefix/nocase semantics; -choicemultiple words screened per list member within min/max), applied only where allocation has an alternative: -optional arguments and further occurrences of -multiple arguments. Required arguments are deliberately not screened - the word must fill them and validation's choiceviolation reporting stays informative (the existing parsestatus/cmdhelp/choices pins forced and now document both refinements). -choicerestricted 0 behaviour unchanged. Results: the allocation.test GAPs flipped ('1 2 3' and '1 2 by 3' parse with correct clause shapes {{} 3}/{by 3}; '1 2 3 4' gets the plain excess-values rejection instead of blaming the unrelated optional); prefix normalization survives the screen; all noise-word variants and if-shape guards unchanged. lseq moduledoc matrix parses per lseq.n under -form range, and 'i lseq 0 10 2' works end-to-end. Remaining lseq divergences trace to G-041 form selection or the -type expr laxity recorded in G-055 - not allocation. Also corrected an increment-1 mis-premise: parse_status ALREADY accepts -form before the withid/withdef tail per its documented synopsis - the probe had passed it trailing. GAP replaced by parsestatus_form_option_order (correct-order acceptance incl. the form key, trailing rejection). No code change was needed for that acceptance clause. Verification: punk::args suite 193 pass / 1 pre-existing skip / 0 fail; punk::ns 53/53; full source-tree suite 813 pass / 13 skip / 1 fail = exec-14.3 only (known baseline) - zero regressions (tclsh 9.0.3; lseq verification in punk902z src, Tcl 9.0.2). Flip edits: G-071 index entry moved to GOALS-archive.md (achieved 2026-07-12), detail Status flipped with the acceptance review and before/after record; reference sweep updates G-041 (prerequisite fixed, parse_status correction) and G-072 Notes. The detail-file move to goals/archive/ follows as a pure rename; goals_lint transiently unclean between the two commits. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 4 + GOALS-archive.md | 4 + GOALS.md | 4 - goals/G-041-punkargs-form-matching.md | 17 +++- goals/G-071-punkargs-optional-allocation.md | 59 ++++++++++-- goals/G-072-punkargs-compound-clause-types.md | 10 +- punkproject.toml | 2 +- src/modules/punk/args-999999.0a1.0.tm | 91 +++++++++++++++++-- src/modules/punk/args-buildversion.txt | 3 +- .../punk/args/testsuites/args/allocation.test | 66 +++++++++----- 10 files changed, 205 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f711a05c..939d0d8c 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.11] - 2026-07-12 + +- G-071 achieved: punk::args 0.9.0 allocation choice screen - optional arguments with restricted choice sets no longer greedily consume non-choice words at allocation time, so noise-word grammars parse correctly with the noise word omitted. 'i lseq 0 10 2' (and the '0 10 by 2' / '1 5 by 0' shapes) now parse and render correctly against the tclcore moduledoc; genuinely invalid arglists report a plain excess-values error instead of blaming an unrelated optional argument. Correction recorded: parse_status already accepted -form before the withid/withdef tail per its documented synopsis - the earlier 'missing -form' finding was an argument-order mistake, now pinned by test. + ## [0.12.10] - 2026-07-12 - punk::args 0.8.3 (G-071 increment): silenced four more unconditional debug lines that printed to stderr during normal argument parsing whenever an optional element was skipped (visible when using 'i'/help display against commands modelled with noise-word arguments such as if and lseq). New allocation characterization testsuite pins the optional-element mis-allocation ('lseq 0 10 2'-class failures) as GAP tests ahead of the allocator fix. diff --git a/GOALS-archive.md b/GOALS-archive.md index 22abaed9..00f8e37a 100644 --- a/GOALS-archive.md +++ b/GOALS-archive.md @@ -73,3 +73,7 @@ Acceptance: a documented probe helper yields a wsl_linux_available constraint wh ### G-062 [achieved 2026-07-11] Canonical project license: BSD-2-Clause LICENSE file with SPDX-identified references → detail: goals/archive/G-062-project-license-file.md Scope: LICENSE.txt (new, repo root), README.md, punkproject.toml ([project] license field), AGENTS.md (Repo-wide Notes license mention) Acceptance: LICENSE.txt exists at the repo root containing the standard BSD-2-Clause text with a real copyright line; README.md names BSD-2-Clause and points at LICENSE.txt; root AGENTS.md Repo-wide Notes names the license precisely; punkproject.toml [project] carries license = "BSD-2-Clause"; a repo-sweep for top-level license claims finds none contradicting it (sweep result recorded here). + +### G-071 [achieved 2026-07-12] punk::args value-allocation correctness for optional elements (lseq-class arglists) + parse_status -form → detail: goals/archive/G-071-punkargs-optional-allocation.md +Scope: src/modules/punk/args-999999.0a1.0.tm (get_dict value allocation, private::get_dict_can_assign_value, parse_status), src/tests/modules/punk/args/testsuites/args/ (new allocation characterization suite), src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::lseq as the proving consumer) +Acceptance: a new allocation characterization suite drives the lseq range matrix under explicit -form range - '0 10', '0 10 2', '0 10 by 2', '0 to 10 2', '0 .. 10 by 2', '1 5 by 0' parse per the lseq.n grammar (the three currently-failing cases fixed) and '0 10 2 4' still fails - plus reduced fixtures isolating the shape (optional choice value between required values + trailing optional-member clause) independent of the moduledoc; the ::if noise-word cases (mid-clause ?literal(then)?, clause-leading ?literal(else)?) keep passing - no regression to optional clause members generally; a genuinely invalid arglist's error names the failing element (the '..|to' misblame case pinned fixed); punk::args::parse_status accepts -form (single form name/index at minimum, consistent with parse) with its status structure reporting the form used; full punk::args and punk::ns suites pass with no expectations weakened; before/after results for the probe matrix recorded in this file. diff --git a/GOALS.md b/GOALS.md index 50461599..5a6c399a 100644 --- a/GOALS.md +++ b/GOALS.md @@ -286,10 +286,6 @@ Detail: goals/G-069-splitter-tclparser-lint.md Scope: src/modules/punk/lib-999999.0a1.0.tm (tclparser_tcl stub + dispatch; new module if size warrants - decided in the work), src/tests/modules/punk/lib/ (parity + fallback suites), TEMP_REFERENCE/ (tclparser reference source, user-provided, read-only) Detail: goals/G-070-pure-tcl-tclparser.md -### G-071 [active] punk::args value-allocation correctness for optional elements (lseq-class arglists) + parse_status -form -Scope: src/modules/punk/args-999999.0a1.0.tm (get_dict value allocation, private::get_dict_can_assign_value, parse_status), src/tests/modules/punk/args/testsuites/args/ (new allocation characterization suite), src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::lseq as the proving consumer) -Detail: goals/G-071-punkargs-optional-allocation.md - ### G-072 [proposed] punk::args compound clause types: named alternates with per-element typing and per-alternate arity (try-class handlers) Scope: src/modules/punk/args-999999.0a1.0.tm (type-expression parsing, clause allocation, synopsis/help renderers), src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::try as proving consumer; ::if/::switch as touched), src/tests/modules/punk/args/testsuites/args/ Detail: goals/G-072-punkargs-compound-clause-types.md diff --git a/goals/G-041-punkargs-form-matching.md b/goals/G-041-punkargs-form-matching.md index c6f51f54..3f7d3fa6 100644 --- a/goals/G-041-punkargs-form-matching.md +++ b/goals/G-041-punkargs-form-matching.md @@ -97,15 +97,22 @@ when shaping step 1. optional-member clause. lseq range-form arglists WITHOUT the ../to noise word but WITH a step ('0 10 2', '0 10 by 2', '1 5 by 0') fail IN the range form (the error blames '..|to'), while '0 to 10 2' and '0 .. 10 by 2' parse fine. No other - lseq form accepts those arglists, so form auto-selection alone cannot make - 'i lseq 0 10 2' work - the allocator fix must precede or accompany this goal - (flagged as a candidate goal 2026-07-12). ::if's noise-word clauses + lseq form accepts those arglists, so form auto-selection alone could not make + 'i lseq 0 10 2' work. The allocator prerequisite became G-071 and was FIXED + the same day (achieved 2026-07-12, punk::args 0.9.0 allocation choice screen - + see goals/archive/G-071-punkargs-optional-allocation.md): the lseq matrix now + parses in-form, leaving form auto-selection as this goal's remaining gap for + the 'lseq 3' / switch-block-form cases. ::if's noise-word clauses ('?literal(then)?' mid-clause, '?literal(else)?' clause-leading) parse correctly in the same probes, so the failure is specific to the optional standalone value + trailing optional-member clause combination, not optional clause members generally. - - punk::args::parse_status does not accept -form - needed for per-form probing, - for this goal's candidacy API, and for the G-044 partial-arglist consumer. + - CORRECTED (G-071 closeout): parse_status DOES accept -form - positioned + before the withid/withdef tail per its documented synopsis; the probe had + passed it trailing. Behaviour pinned in allocation.test + (parsestatus_form_option_order). The candidacy API and the G-044 consumer + can build on the existing surface (list-of-forms support remains this + goal's -form list item, as for parse). - Over-acceptance divergences (model valid where the real command errors) recorded for the modelability list in G-055's detail file - not this goal's scope. - Incidental fix during probing: another unconditional debug puts on the clause diff --git a/goals/G-071-punkargs-optional-allocation.md b/goals/G-071-punkargs-optional-allocation.md index 6780f6ef..52a1b450 100644 --- a/goals/G-071-punkargs-optional-allocation.md +++ b/goals/G-071-punkargs-optional-allocation.md @@ -1,6 +1,6 @@ # G-071 punk::args value-allocation correctness for optional elements (lseq-class arglists) + parse_status -form -Status: active +Status: achieved 2026-07-12 Scope: src/modules/punk/args-999999.0a1.0.tm (get_dict value allocation, private::get_dict_can_assign_value, parse_status), src/tests/modules/punk/args/testsuites/args/ (new allocation characterization suite), src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::lseq as the proving consumer) Goal: argument lists that are valid for a single form parse correctly when optional standalone values and optional-member clauses are skipped or filled in any documented combination - specifically the lseq range shape (an optional single-word choice value between required values, followed by a trailing optional-member clause), where today the allocator force-feeds a word to the skippable optional and fails ('lseq 0 10 2', '0 10 by 2', '1 5 by 0' all fail in-form while '0 to 10 2' parses) - and parse failures blame the genuinely failing element rather than an unrelated optional; parse_status gains -form so per-form status probing works. Acceptance: a new allocation characterization suite drives the lseq range matrix under explicit -form range - '0 10', '0 10 2', '0 10 by 2', '0 to 10 2', '0 .. 10 by 2', '1 5 by 0' parse per the lseq.n grammar (the three currently-failing cases fixed) and '0 10 2 4' still fails - plus reduced fixtures isolating the shape (optional choice value between required values + trailing optional-member clause) independent of the moduledoc; the ::if noise-word cases (mid-clause ?literal(then)?, clause-leading ?literal(else)?) keep passing - no regression to optional clause members generally; a genuinely invalid arglist's error names the failing element (the '..|to' misblame case pinned fixed); punk::args::parse_status accepts -form (single form name/index at minimum, consistent with parse) with its status structure reporting the form used; full punk::args and punk::ns suites pass with no expectations weakened; before/after results for the probe matrix recorded in this file. @@ -68,13 +68,56 @@ needs it first. - Verified (tclsh 9.0.3): full punk::args suite 193 pass / 1 pre-existing skip / 0 fail. -Remaining for acceptance: -- allocator fix: optional-element rejection yields the word to subsequent - elements/clauses when a consistent allocation exists (the three GAPs flip) -- blame quality: misblame GAP flips to naming the genuinely failing element -- parse_status -form support (its GAP flips) -- lseq moduledoc matrix verified under -form range post-fix; before/after - results recorded here; punk::ns suite regression pass +### 2026-07-12 increment 2: allocation choice screen (punk::args 0.9.0) - acceptance complete + +- Root cause: get_dict_can_assign_value blanket-satisfied the member check for any + argument with choices ("each tp in the clause is just for validating a value + outside the choice-list when -choicerestricted 0" - applied to restricted sets + too), so an optional choice value consumed any word whenever arity permitted. + Arity always permitted here because (a) the tail-reservation scan cannot see the + by-step clause (its lsearch for literal* misses the optional-wrapped + ?literalprefix(by)?), and (b) tail_needs counts only required tail elements. +- Fix: a choiceword_match allocation screen (the shared G-040 implementation, so + allocation acceptance cannot diverge from parse acceptance - exact/alias/prefix/ + nocase; -choicemultiple words screened per list member within min/max), applied + ONLY where allocation has an alternative: -optional arguments and further + occurrences of -multiple arguments. Two refinements forced by the existing + suites during implementation, both kept as design decisions: + - REQUIRED arguments are not screened - the word must fill them regardless, and + screening only masked validation's informative choiceviolation as a + missingrequired* (caught by parsestatus.test/cmdhelp.test pins). + - -choicemultiple list-valued words are screened member-wise (caught by + choices.test choicemultiple pins). + The screen compares raw words (no ansistrip): an ansi-wrapped choice word on an + optional argument would be skipped where validation would accept it - accepted + edge, recorded in the code comment. +- Correction of an increment-1 mis-premise: parse_status ALREADY accepts -form - + positioned before the withid/withdef tail per its documented synopsis; the + probing failure was a trailing -form (argument-order mistake). The GAP was + replaced by parsestatus_form_option_order pinning correct-order acceptance + (with the status structure's form key) and trailing-order rejection. The + acceptance clause "parse_status accepts -form ... reporting the form used" was + therefore already satisfied; verified with a multiform fixture (form=beta + reported). The Goal line's "parse_status gains -form" framing was inaccurate - + no code change was needed or made for it. +- Before/after (reduced fixture, tclsh 9.0.3): '1 2 3' {0 invalid sep} -> + {1 valid {start end {by step}}} with clause {{} 3}; '1 2 by 3' + {0 incomplete end} -> {1 valid ...} with clause {by 3}; '1 2 3 4' + {0 invalid sep} -> {0 invalid {}} (same excess-values rejection as the + noise-word twin); prefix semantics preserved ('1 2 b 3' -> clause {by 3}, + '1 t 2 3' -> sep normalized to 'to'); all noise-word variants and all if-shape + guards unchanged. +- lseq moduledoc matrix (punk902z src, Tcl 9.0.2): explicit -form range now + parses '0 10 2', '0 10 by 2', '1 5 by 0' per lseq.n (previously in-form + failures); count and start_count forms unchanged; default-form parsing of + '0 10 2' now valid end-to-end (the original 'i lseq 0 10 2' complaint). + Remaining real-vs-model divergences all trace to G-041 form selection or the + -type expr non-validation recorded in G-055's notes - not allocation. +- Verified (tclsh 9.0.3): punk::args suite 193 pass / 1 pre-existing skip / 0 + fail (allocation GAPs flipped; the two required-choice-arg pins and the + choicemultiple pins unweakened); punk::ns suite 53/53; full source-tree suite + 827 total, 813 pass, 13 skip, 1 fail = exec-14.3 only (known pre-existing + core-test baseline) - zero regressions; make.tcl modules builds clean. ## Notes diff --git a/goals/G-072-punkargs-compound-clause-types.md b/goals/G-072-punkargs-compound-clause-types.md index 4709dd02..99fca05d 100644 --- a/goals/G-072-punkargs-compound-clause-types.md +++ b/goals/G-072-punkargs-compound-clause-types.md @@ -56,10 +56,12 @@ mechanism. Brief examination at drafting time: - Related: G-041 (form selection and clause allocation interact; its candidacy machinery should not need changes from this goal's types), G-071 (allocation - correctness for optional elements - sequence that first: compound alternates - ride on the same allocator), G-055 (modelability findings list; its parity - workflow verifies whatever this goal makes expressible), G-053 (occurrence - arity - adjacent clause machinery). + correctness for optional elements, achieved 2026-07-12 - see + goals/archive/G-071-punkargs-optional-allocation.md; its choiceword_match + allocation screen and allocation.test fixtures are the base this goal's + alternates ride on), G-055 (modelability findings list; its parity workflow + verifies whatever this goal makes expressible), G-053 (occurrence arity - + adjacent clause machinery). - Display cost matters: the ::try notes warn bracketed alternate forms "get unwieldy in synopsis listings" - synopsis rendering of compound types is part of the mechanism's acceptance, not an afterthought. diff --git a/punkproject.toml b/punkproject.toml index aa86043d..5f2a1f6f 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.12.10" +version = "0.12.11" 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 50ebe452..1936670e 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -7184,6 +7184,49 @@ tcl::namespace::eval punk::args { set tpidx 0 set newtypelist $thistype set has_choices [expr {[tcl::dict::exists $ARG_INFO $thisname -choices] || [tcl::dict::exists $ARG_INFO $thisname -choicegroups]}] + set choicescreen_applies 0 + if {$has_choices} { + #G-071: with a restricted choice set (the default) allocation SCREENS the + #candidate word against the choices rather than blanket-accepting - but + #ONLY where allocation has an alternative: optional arguments (and extra + #occurrences of -multiple arguments). Previously any word was consumed + #here whenever arity permitted, so an optional choice value (e.g the lseq + #'..'/'to' noise word) greedily took a word that belonged to later + #elements/clauses and the parse failed with a confusing trailing-choices + #error ('lseq 0 10 2' shape). The screen uses choiceword_match - the + #shared G-040 implementation - so allocation acceptance cannot diverge + #from parse acceptance (exact/alias/prefix/nocase semantics included); + #-choicemultiple words are screened per list member. For a REQUIRED + #argument the word must fill it regardless - rejecting here would only + #mask final validation's informative choiceviolation as a + #missingrequired* error, so required arguments are not screened. + #Final validation remains authoritative; the screen compares raw words + #(no ansistrip) - an ansi-wrapped choice word on an optional argument + #would be skipped here where validation would accept it (accepted edge - + #allocation errs toward yielding words onward). With -choicerestricted 0 + #any word remains allocatable (each tp in the clause then only validates + #values outside the choice-list). + if {[Dict_getdef $ARG_INFO $thisname -choicerestricted 1] + && ([tcl::dict::get $ARG_INFO $thisname -optional] + || ([tcl::dict::get $ARG_INFO $thisname -multiple] && $thisname in $namesreceived))} { + set choicescreen_applies 1 + set cw_allchoices [list] + if {[tcl::dict::exists $ARG_INFO $thisname -choices]} { + set cw_allchoices [tcl::dict::get $ARG_INFO $thisname -choices] + } + if {[tcl::dict::exists $ARG_INFO $thisname -choicegroups]} { + tcl::dict::for {_grp grpmembers} [tcl::dict::get $ARG_INFO $thisname -choicegroups] { + lappend cw_allchoices {*}$grpmembers + } + } + set cw_nocase [Dict_getdef $ARG_INFO $thisname -nocase 0] + set cw_prefix [Dict_getdef $ARG_INFO $thisname -choiceprefix 1] + set cw_aliases [Dict_getdef $ARG_INFO $thisname -choicealiases {}] + set cw_deny [Dict_getdef $ARG_INFO $thisname -choiceprefixdenylist {}] + set cw_reserve [Dict_getdef $ARG_INFO $thisname -choiceprefixreservelist {}] + lassign [Dict_getdef $ARG_INFO $thisname -choicemultiple {1 1}] cw_cmmin cw_cmmax + } + } foreach tp $thistype { #usual case is a single tp (basic length-1 clause) - but tp may commonly have alternates eg int|literal(xxx) set v [lindex $all_remaining $alloc_count] @@ -7195,21 +7238,53 @@ tcl::namespace::eval punk::args { set tp [string trim $tp ?] set member_satisfied 0 + set member_choicechecked 0 if {$has_choices} { - #each tp in the clause is just for validating a value outside the choice-list when -choicerestricted 0 - set member_satisfied 1 + if {$choicescreen_applies} { + #G-071 allocation screen (see block above the loop) + set member_choicechecked 1 + if {$cw_cmmax == 1} { + set cwm [choiceword_match $v $cw_nocase $cw_allchoices $cw_aliases $cw_prefix $cw_deny $cw_reserve] + set member_satisfied [tcl::dict::get $cwm matched] + } else { + #-choicemultiple: the word is itself a list of choices - screen + #each member; count within the declared min/max (max -1 = no limit) + if {[catch {llength $v} v_len]} { + set member_satisfied 0 + } elseif {$v_len < $cw_cmmin || ($cw_cmmax > 0 && $v_len > $cw_cmmax)} { + set member_satisfied 0 + } else { + set member_satisfied 1 + foreach v_member $v { + set cwm [choiceword_match $v_member $cw_nocase $cw_allchoices $cw_aliases $cw_prefix $cw_deny $cw_reserve] + if {![tcl::dict::get $cwm matched]} { + set member_satisfied 0 + break + } + } + } + } + } else { + #required argument with restricted choices (word must fill it - + #validation reports choice violations), or -choicerestricted 0 + #(each tp in the clause only validates values outside the + #choice-list) + set member_satisfied 1 + } } - if {!$member_satisfied} { + #category lists must exist even when the choice screen decided the member + #(the !member_satisfied blocks below consult their lengths) + set ctg_literals [list] + set ctg_literalprefixes [list] + set ctg_stringstartswith [list] + set ctg_stringendswith [list] + set ctg_other [list] + if {!$member_satisfied && !$member_choicechecked} { #----------------------------------------------------------------------------------- #first build category lists of any literal,literalprefix,stringstartwith,other # - set ctg_literals [list] - set ctg_literalprefixes [list] - set ctg_stringstartswith [list] - set ctg_stringendswith [list] - set ctg_other [list] #foreach tp_alternative [split $tp |] {} foreach tp_alternative [private::split_type_expression $tp] { #JJJJ diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index 7813356d..3b51edbc 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.8.3 +0.9.0 #First line must be a semantic version number #all other lines are ignored. +#0.9.0 - G-071: allocation choice screen - get_dict_can_assign_value no longer blanket-accepts any word for an argument with a RESTRICTED choice set; where allocation has an alternative (the argument is -optional, or a further occurrence of a -multiple argument) the candidate word is screened with choiceword_match (the shared G-040 implementation: exact/alias/prefix/nocase semantics; -choicemultiple words screened per list member within min/max), and a non-matching word yields onward to later elements/clauses instead of being consumed and failing validation. Fixes the lseq-class in-form failures: an optional choice noise word between required values plus a trailing optional-member clause ('lseq 0 10 2', '0 10 by 2', '1 5 by 0' now parse; noise-word variants and prefix normalization unchanged; genuinely invalid arglists now get the plain excess-values rejection instead of blaming the unrelated optional). REQUIRED arguments are deliberately not screened - the word must fill them and validation's choiceviolation reporting stays informative. -choicerestricted 0 behaviour unchanged. allocation.test GAPs flipped; parse_status -form documented-order behaviour pinned (a prior mis-premise: -form was already supported before the withid/withdef tail per its synopsis). #0.8.3 - G-071: silenced four more unconditional debug puts on get_dict's allocation retreat paths ("get_dict cannot assign val:... (111)/(222)/(333)/(444)") - these fired on stderr during NORMAL successful parses whenever an optional element's candidate word was rejected and allocation retreated (e.g. every noise-word skip when parsing if/lseq-modelled commands), polluting interactive output. Same class as the 0.2.3/0.8.2 finds. The retreat logic they marked is the target of the G-071 allocator-correctness work; behaviour unchanged, output only. New allocation.test characterization suite (reduced lseq-range/if-shape fixtures): current mis-allocation and misblame pinned as GAP tests (flip with G-071), noise-word variants and if-shape guards pinned as regression guards, parse_status -form absence pinned as GAP. #0.8.2 - fixed stray debug output: an unconditional 'puts' in private::get_dict_can_assign_value ("checking tp '' against value ''") fired on multi-element clause type checks (e.g. parsing 'try ... trap ... on ...' style arglists against the tclcore moduledoc), polluting interactive output - now commented like its 0.2.3 companion. Found during G-041 prework probing (real-vs-model divergence sweep of if/switch/try/lseq). #0.8.1 - G-045 documentation-only: the container quoting rules pinned by defquoting.test promoted into the define -help documentation (-help key section): braced values fully literal (backslash sequences survive as typed) with only tstr placeholders and their backslash escape special; double-quoted values get Tcl backslash semantics at record parse (\n -> newline, doubled backslash collapses) while $ and [] stay literal with no variable/command substitution outside tstr placeholders; the backslash-escaped placeholder idiom renders a literal placeholder. diff --git a/src/tests/modules/punk/args/testsuites/args/allocation.test b/src/tests/modules/punk/args/testsuites/args/allocation.test index 0ec13478..6eb4d263 100644 --- a/src/tests/modules/punk/args/testsuites/args/allocation.test +++ b/src/tests/modules/punk/args/testsuites/args/allocation.test @@ -9,12 +9,14 @@ package require punk::args #clauses) from the tclcore moduledoc, so the pins do not depend on moduledoc #availability or vintage. # -#GAP-labelled tests pin the CURRENT broken allocation and flip when G-071 lands: -#with the optional 'sep' noise word absent, the allocator force-assigns the word -#after start to 'sep' (whose -choices then reject it) instead of yielding it to -#later elements/clauses - so '1 2 3' and '1 2 by 3' fail in-form while every -#variant carrying the noise word parses. '1 2 3 4' is genuinely invalid but the -#blame lands on 'sep' rather than the failing clause (misblame pin). +#History: added with GAP pins encoding the pre-G-071 broken allocation (the +#allocator blanket-accepted any word for a restricted-choice value at allocation +#time, so the optional 'sep' noise word greedily consumed the word after start +#and '1 2 3' / '1 2 by 3' failed in-form with a trailing-choices error). +#G-071 added a choiceword_match allocation screen for restricted choice sets +#(punk::args 0.9.0): a non-choice word is no longer allocatable to 'sep', so it +#yields onward to end and the by-step clause - the GAPs below flipped to the +#correct outcomes the same day they were added. namespace eval ::testspace { namespace import ::tcltest::* @@ -70,31 +72,41 @@ namespace eval ::testspace { {1 valid {start sep end {by step}}}\ ] - #added 2026-07-12 (agent, G-071) - test allocation_range_optskip_bare_step_GAP {GAP: sep absent + bare step ('1 2 3') fails - word 2 force-fed to the skippable optional 'sep' instead of yielding to end + clause}\ + #added 2026-07-12 (agent, G-071) - was allocation_range_optskip_bare_step_GAP + #(pinned {0 invalid sep}); flipped by the G-071 allocation choice screen + test allocation_range_optskip_bare_step {sep absent + bare step ('1 2 3'): non-choice word yields past the optional 'sep' to end + clause}\ -setup $common -body { - pstat {1 2 3} ::testspace::alloc_range + lappend result [pstat {1 2 3} ::testspace::alloc_range] + #the clause's optional literalprefix member is omitted (empty) + lappend result [dict get [dict get [punk::args::parse {1 2 3} withid ::testspace::alloc_range] values] {by step}] }\ -cleanup $cleanup\ - -result {0 invalid sep} + -result [list {1 valid {start end {by step}}} {{} 3}] - #added 2026-07-12 (agent, G-071) - test allocation_range_optskip_by_clause_GAP {GAP: sep absent + by-clause ('1 2 by 3') fails as incomplete blaming 'end'}\ + #added 2026-07-12 (agent, G-071) - was allocation_range_optskip_by_clause_GAP + #(pinned {0 incomplete end}); flipped by the G-071 allocation choice screen + test allocation_range_optskip_by_clause {sep absent + by-clause ('1 2 by 3'): allocation yields correctly and the clause takes both members}\ -setup $common -body { - pstat {1 2 by 3} ::testspace::alloc_range + lappend result [pstat {1 2 by 3} ::testspace::alloc_range] + lappend result [dict get [dict get [punk::args::parse {1 2 by 3} withid ::testspace::alloc_range] values] {by step}] + #prefix semantics survive the allocation screen: 'b' prefixes the clause + #literal, 't' prefixes the sep choice (normalized to 'to') + lappend result [dict get [dict get [punk::args::parse {1 2 b 3} withid ::testspace::alloc_range] values] {by step}] + lappend result [dict get [dict get [punk::args::parse {1 t 2 3} withid ::testspace::alloc_range] values] sep] }\ -cleanup $cleanup\ - -result {0 incomplete end} + -result [list {1 valid {start end {by step}}} {by 3} {by 3} to] - #added 2026-07-12 (agent, G-071) - '1 2 3 4' must STAY invalid when the - #allocator is fixed (per lseq.n no form accepts it) but the blame should move - #off the unrelated optional 'sep' to the genuinely failing element - test allocation_range_excess_invalid_misblame_GAP {GAP: genuinely invalid '1 2 3 4' is rejected (correct) but blames the unrelated optional 'sep' (misblame)}\ + #added 2026-07-12 (agent, G-071) - was allocation_range_excess_invalid_misblame_GAP + #(pinned {0 invalid sep}); the arglist stays invalid (per lseq.n no form accepts + #it) but the blame no longer lands on the unrelated optional 'sep' - it is now + #the same excess-values rejection as the noise-word twin '1 to 2 3 4' + test allocation_range_excess_invalid {genuinely invalid '1 2 3 4' is rejected as excess values without blaming 'sep'}\ -setup $common -body { pstat {1 2 3 4} ::testspace::alloc_range }\ -cleanup $cleanup\ - -result {0 invalid sep} + -result {0 invalid {}} #added 2026-07-12 (agent, G-071) - guard: with the noise word present the #equivalent excess arglist is rejected without touching 'sep' @@ -130,14 +142,20 @@ namespace eval ::testspace { -cleanup $cleanup\ -result [list {1 valid} {1 valid} {1 valid} {1 valid} {1 valid} {1 valid} {1 valid} {0 invalid}] - #added 2026-07-12 (agent, G-071) - test parsestatus_form_option_GAP {GAP: parse_status does not accept -form - per-form status probing raises instead of parsing}\ + #added 2026-07-12 (agent, G-071) - characterization correcting an initial + #mis-premise: parse_status DOES accept -form, positioned per its documented + #synopsis (options BEFORE the withid/withdef tail); a trailing -form is + #rejected like any misplaced option + test parsestatus_form_option_order {parse_status -form works before the withid tail and is rejected trailing}\ -setup $common -body { - set rc [catch {punk::args::parse_status {1 to 2} withid ::testspace::alloc_range -form 0} r] - list $rc [string match "Bad number of values*" $r] + set st [punk::args::parse_status {1 to 2} -form 0 withid ::testspace::alloc_range] + lappend result [list [dict get $st ok] [dict get $st status]] + #the status structure reports the form used + lappend result [dict get $st form] + lappend result [catch {punk::args::parse_status {1 to 2} withid ::testspace::alloc_range -form 0}] }\ -cleanup $cleanup\ - -result {1 1} + -result [list {1 valid} _default 1] cleanupTests }