diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d4c579..60ea2667 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.8.2] - 2026-07-11 + +- G-054 achieved: tclcore moduledoc 0.2.0 — the `string is` class choices shown by `i string is` (and the per-class docids like `i string is digit`) are now harvested from the running interpreter at define time instead of a hand-maintained list, so the documented/parsed class set always matches what the interpreter accepts (previously the static Tcl 9.0 list wrongly accepted `dict` under 8.6 and rejected 8.7's `unicode`). Version notes render on classes that differ across releases; unrecognized future classes get a generic label instead of vanishing. Behavioural parity pinned by `tclcoreparity.test` with expectations derived from the live interpreter — green on Tcl 8.6.13, 8.7a6 and 9.0.3. + ## [0.8.1] - 2026-07-11 - `runtime.cmd` powershell payload: cached `sha1sums.txt` fallback backported from the bash payload - `list -remote` against an unreachable server now warns and compares using the previously fetched copy instead of dying on an unhandled download error, and the `fetch` path's pre-existing silent fall-through to a cached copy now announces itself. Also fixed a latent undefined-variable bug creating the runtime folder in the `list -remote` branch. diff --git a/GOALS.md b/GOALS.md index 14f73cfd..43742f3d 100644 --- a/GOALS.md +++ b/GOALS.md @@ -369,7 +369,7 @@ Detail: goals/G-053-punkargs-multiple-ranges.md Goal: -multiple accepts a {min max} occurrence range (mirroring -choicemultiple; max -1 unbounded) alongside the legacy booleans - so a definition can declare "at most once, repeat is an error" ({0 1}) or bounded repetition ({2 4}) instead of choosing between silent last-wins (0) and unbounded collection (1) - with boolean semantics preserved exactly, including the prepend-defaults/last-wins override idiom. Acceptance: parse raises a usage-style arity error naming the argument for occurrences outside a declared range; boolean -multiple 0/1 behaviour is unchanged (full existing punk::args suite passes untouched); the -optional/range-min reconciliation rule is documented and enforced at define time; the usage table Multi column and synopsis reflect declared ranges; -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged; characterization tests cover the new forms and the value-shape rule. -### G-054 [proposed] tclcore moduledoc: runtime-harvested 'string is' class choices with cross-version behavioural parity pins +### G-054 [achieved 2026-07-11] tclcore moduledoc: runtime-harvested 'string is' class choices with cross-version behavioural parity pins Scope: src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (+ tclcore-buildversion.txt), src/tests/modules/punk/args/testsuites/args/ (new parity test), TEMP_REFERENCE/tcl9 (read-only reference) Goal: the ::tcl::string::is definition's class choices (and the generated per-class virtual docids) are harvested from the running interpreter at define time - static choicelabels applied to classes present, generic label for unrecognized classes, a version note for dict - so accept/reject parity with the running Tcl holds on 8.6 and 9.x without hand-maintained per-version lists. Acceptance: parse/parse_status against ::tcl::string::is and its per-class virtual ids agrees with the real interpreter's error-vs-ok outcome for a pinned probe matrix (missing args, trailing flag-like str word, option/class unique-prefix acceptance and ambiguity rejection, unknown option/class, -failindex var consumption leaving no str, per-version class presence: dict, unicode) on Tcl 9.0.x and 8.6; the rendered choices show only classes the running interp accepts; the parity test derives expectations from the live interpreter (not version arithmetic) and passes under both; existing args/tclcore suites pass; tclcore buildversion bumped with changelog. diff --git a/punkproject.toml b/punkproject.toml index 669a890e..1bff70bb 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.8.1" +version = "0.8.2" diff --git a/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm index 0b889bcf..1a2a85d5 100644 --- a/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm +++ b/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm @@ -9817,41 +9817,37 @@ tcl::namespace::eval punk::args::moduledoc::tclcore { # ============================================================================================================== - punk::args::define [punk::args::lib::tstr -return string { - @id -id ::tcl::string::is - @cmd -name "Built-in: tcl::string::is"\ - -summary\ - "Test character class of string."\ - -help\ - "Returns 1 if string is a valid member of the specified character class, otherwise returns 0. - " - @leaders -min 1 -max 1 - class -type string\ - -choices { - alnum - alpha - ascii - boolean - control - dict - digit - double - entier - false - graph - integer - list - lower - print - punct - space - true - upper - wideinteger - wordchar - xdigit - }\ - -choicelabels { + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + #G-054: the 'string is' class set is harvested from the RUNNING interpreter rather + #than hard-coded. It varies by tcl version (8.6 has no dict class; the unreleased + #8.7 series added unicode, which tcl 9 removed) and a static list breaks + #accept/reject parity between these docs and the interpreter they load into. + #A deliberately invalid probe of the pure builtin (safe, side-effect free) yields + #the authoritative list from its error message: + # bad class "zzz": must be alnum, alpha, ..., or xdigit + set string_is_classes [list] + if {[catch {string is __punk_argdoc_probe__ x} _sis_msg]} { + if {[regexp {must be (.+)$} $_sis_msg -> _sis_csv]} { + foreach _sis_c [split $_sis_csv ,] { + set _sis_c [string trim $_sis_c] + if {[string match "or *" $_sis_c]} { + set _sis_c [string range $_sis_c 3 end] + } + if {$_sis_c ne ""} { + lappend string_is_classes $_sis_c + } + } + } + } + if {![llength $string_is_classes]} { + #harvest failed (unexpected error message format) - fall back to the tcl 9.0 set + set string_is_classes {alnum alpha ascii boolean control dict digit double entier false graph integer list lower print punct space true upper wideinteger wordchar xdigit} + } + set string_is_classes [lsort $string_is_classes] ;#display order (as the previous hand-written list) + #hand-written class descriptions (man-page derived, verbatim) - applied below only for + #classes the running interpreter accepts; accepted classes without an entry get a + #generic label. tstr here resolves the ${$A_WARN}/${$A_RST} highlights as before. + set string_is_class_descriptions [punk::args::lib::tstr -return string { alnum " Any Unicode alphabet or digit character" @@ -9877,7 +9873,9 @@ tcl::namespace::eval punk::args::moduledoc::tclcore { will contain the index of the \"element\" where the dict parsing fails or -1 if - this cannot be determined." + this cannot be determined. + (class not present in + Tcl 8.6)" digit " Any Unicode digit char. Note that this includes @@ -9937,6 +9935,11 @@ tcl::namespace::eval punk::args::moduledoc::tclcore { " Any of the forms allowed for Tcl_GetBoolean where the value is true" + unicode + " Any Unicode character. + (class exists only in the + unreleased Tcl 8.7 series - + removed in Tcl 9)" upper " Any upper case alphabet character in the Unicode @@ -9959,7 +9962,29 @@ tcl::namespace::eval punk::args::moduledoc::tclcore { xdigit " Any hexadecimal digit character ([0-9A-Fa-f])." - }\ + }] + set string_is_choicelabels "" + foreach _sis_c $string_is_classes { + if {[dict exists $string_is_class_descriptions $_sis_c]} { + append string_is_choicelabels [list $_sis_c] " " [list [dict get $string_is_class_descriptions $_sis_c]] \n + } else { + append string_is_choicelabels [list $_sis_c] " " [list " (class accepted by this Tcl\n runtime - not yet described\n in the punk tclcore docs)"] \n + } + } + unset -nocomplain _sis_msg _sis_csv _sis_c + + punk::args::define [punk::args::lib::tstr -return string { + @id -id ::tcl::string::is + @cmd -name "Built-in: tcl::string::is"\ + -summary\ + "Test character class of string."\ + -help\ + "Returns 1 if string is a valid member of the specified character class, otherwise returns 0. + " + @leaders -min 1 -max 1 + class -type string\ + -choices {${$string_is_classes}}\ + -choicelabels {${$string_is_choicelabels}}\ -help\ "character class In the case of boolean, true and false, if the function will return 0, then the diff --git a/src/modules/punk/args/moduledoc/tclcore-buildversion.txt b/src/modules/punk/args/moduledoc/tclcore-buildversion.txt index f47d01c8..d37c03c9 100644 --- a/src/modules/punk/args/moduledoc/tclcore-buildversion.txt +++ b/src/modules/punk/args/moduledoc/tclcore-buildversion.txt @@ -1,3 +1,4 @@ -0.1.0 +0.2.0 #First line must be a semantic version number #all other lines are ignored. +#0.2.0 - G-054: 'string is' class choices (and the generated per-class virtual docids) are harvested from the RUNNING interpreter at define time instead of a hand-maintained list - a deliberately invalid probe of the builtin yields the authoritative class set from its error message (8.6: 21 classes, no dict; unreleased 8.7: +dict +unicode; 9.0: +dict, unicode removed), fixing accept/reject drift such as the doc wrongly accepting 'string is dict' under 8.6. Hand-written man-page descriptions apply only to classes the runtime accepts (generic label for unrecognized future classes); static version notes on dict (not in 8.6) and unicode (unreleased 8.7 only). Parity pinned by tclcoreparity.test with expectations derived from the live interpreter (green on 8.6.13, 8.7a6, 9.0.3) diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index 77cbe73c..c2fba67e 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -39,7 +39,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `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), 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). 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, rendering/indentation characterization, 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), and 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) +- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, 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 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 conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure) - `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity) and 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); remaining GAP pins for pseudo-command cmdtype + space-form docid prefixes (G-051, real `string is` pins behind the have_tclcoredocs constraint), TclOO undocumented-method fallback (G-052), and synopsis marking absence (G-050)) - `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix or via the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl` - staged to the WSL distro's native filesystem, G-059) - `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} ` ifneeded mappings; no-op without a baseline), and the repl command-completeness engine (`commandcomplete.test`: punk::lib::system::incomplete pending-opener stacks - the info-complete quoting quirk progression (`set x "{*}{"` standalone vs in-proc-body), single openers, tabs, escapes, incomplete<->info-complete parity property; pre-repl-refactor characterization, see goals/G-044 detail preserve-list) diff --git a/src/tests/modules/punk/args/testsuites/args/tclcoreparity.test b/src/tests/modules/punk/args/testsuites/args/tclcoreparity.test new file mode 100644 index 00000000..5aef3278 --- /dev/null +++ b/src/tests/modules/punk/args/testsuites/args/tclcoreparity.test @@ -0,0 +1,151 @@ +package require tcltest + +package require punk::args + +#G-054: behavioural parity between the punk::args::moduledoc::tclcore model of +#'string is' and the RUNNING interpreter. The class choices are harvested from the +#runtime at define time (8.6 has no dict class; the unreleased 8.7 series added +#unicode, which tcl 9 removed), so these tests derive their expectations from the +#LIVE interpreter - never from version arithmetic - and pass unchanged on 8.6, 8.7 +#and 9.x. +# +#Parity is asserted on the error-vs-ok OUTCOME of a call shape (real 'string is' +#raising vs punk::args::parse_status ok field) - not on message wording (punkshell +#deliberately uses its own synopsis/message style) and not on failure classification +#(e.g 'string is digit 1 2' is real "bad option" vs model "too many arguments" - +#both errors, divergent blame is accepted; see goals/G-055 fidelity policy). + +namespace eval ::testspace { + namespace import ::tcltest::* + variable common { + set result "" + } + + testConstraint have_tclcoredocs [expr {![catch {package require punk::args::moduledoc::tclcore}]}] + + #harvest the runtime's class list the same way the moduledoc does - from the + #bad-class error message of a deliberately invalid probe + proc live_classes {} { + set classes [list] + if {[catch {string is __punk_parity_probe__ x} msg]} { + if {[regexp {must be (.+)$} $msg -> csv]} { + foreach c [split $csv ,] { + set c [string trim $c] + if {[string match "or *" $c]} { + set c [string range $c 3 end] + } + if {$c ne ""} {lappend classes $c} + } + } + } + return [lsort $classes] + } + #1 if the real 'string is' call shape is accepted (returns 0/1), 0 if it raises + proc real_ok {tail} { + return [expr {![catch {string is {*}$tail} __ignored]}] + } + #1 if the doc model accepts the same tail, 0 if it reports a validation failure + proc doc_ok {tail} { + return [dict get [punk::args::parse_status $tail withid ::tcl::string::is] ok] + } + + test tclcoreparity_stringis_choices_match_runtime {the documented class choices equal the set the running interpreter accepts}\ + -constraints have_tclcoredocs\ + -setup $common -body { + set doc_choices [lsort [dict get [lrange [punk::args::resolved_def -types leaders ::tcl::string::is class] 1 end] -choices]] + lappend result [expr {$doc_choices eq [live_classes]}] + #and every documented choice really is accepted by the interpreter + set rejected [list] + foreach c $doc_choices { + if {![real_ok [list $c 1]]} {lappend rejected $c} + } + lappend result $rejected + }\ + -cleanup { + }\ + -result [list 1 {}] + + test tclcoreparity_stringis_perclass_ids {a per-class virtual docid exists for every harvested class}\ + -constraints have_tclcoredocs\ + -setup $common -body { + set missing [list] + foreach c [live_classes] { + if {![punk::args::id_exists "::tcl::string::is $c"]} {lappend missing $c} + } + lappend result $missing + }\ + -cleanup { + }\ + -result [list {}] + + test tclcoreparity_stringis_probe_matrix {error-vs-ok outcome agrees between the real interpreter and the doc model across the probe matrix}\ + -constraints have_tclcoredocs\ + -setup $common -body { + #matrix per G-054 acceptance: missing args, trailing flag-like str word, + #option/class unique-prefix acceptance and ambiguity rejection, unknown + #option/class, -failindex var consumption leaving no str, per-version class + #presence (dict, unicode), plus middle-word and option-order shapes whose + #CLASSIFICATION diverges but whose outcome must still agree + set matrix [list {*}{ + {} + {digit} + {digit 123} + {digit 12x} + {digit -strict} + {digit -strict -strict} + {digit -failindex} + {digit -failindex pmvar} + {digit -failindex pmvar 12x} + {digit -s 123} + {digit -f pmvar 12x} + {digit -gorp 123} + {tr 1} + {do 123} + {d 123} + {gorp 123} + {digit 1 2} + {-strict digit 1} + {digit -strict -failindex pmvar 123} + {dict x} + {unicode x} + {entier 5} + {boolean -failindex pmvar xyz} + }] + set mismatches [list] + foreach tail $matrix { + set r [real_ok $tail] + set d [doc_ok $tail] + if {$r != $d} { + lappend mismatches [list $tail real $r doc $d] + } + } + lappend result $mismatches + }\ + -cleanup { + catch {unset pmvar} + }\ + -result [list {}] + + test tclcoreparity_stringis_version_notes {version-difference labels appear exactly when the class is present in this runtime}\ + -constraints have_tclcoredocs\ + -setup $common -body { + set labels [dict get [lrange [punk::args::resolved_def -types leaders ::tcl::string::is class] 1 end] -choicelabels] + set classes [live_classes] + if {"dict" in $classes} { + lappend result [string match "*not present in*8.6*" [dict get $labels dict]] + } else { + lappend result [dict exists $labels dict] + } + if {"unicode" in $classes} { + lappend result [string match "*unreleased Tcl 8.7*" [dict get $labels unicode]] + } else { + lappend result [dict exists $labels unicode] + } + #a class always present keeps its man-page description + lappend result [string match "*Unicode digit*" [dict get $labels digit]] + }\ + -cleanup { + }\ + -result [list [expr {"dict" in [live_classes]}] [expr {"unicode" in [live_classes]}] 1] +} +tcltest::cleanupTests ;#needed to produce test summary line.