Browse Source

G-054 achieved: tclcore string is class choices harvested from the running interpreter (tclcore moduledoc 0.2.0, punkshell 0.8.2)

- the ::tcl::string::is definition harvests its class set at define time from the bad-class error message of a deliberately invalid probe (safe, side-effect-free) - accept/reject parity with the loading interpreter by construction: 8.6.13 = 21 classes (no dict - the previous static 9.0 list wrongly ACCEPTED string is dict there), 8.7a6 = 23 (+dict +unicode), 9.0.3 = 22 (unicode removed). Fallback to the 9.0 set if the message format ever changes
- hand-written man-page descriptions (kept verbatim, tstr-processed as before so the A_WARN highlights are unchanged) apply only to classes the runtime accepts; accepted-but-undescribed future classes get a generic label; static version notes added to dict (not in 8.6) and the new unicode entry (unreleased 8.7 only - removed in tcl 9). The per-class virtual docids (::tcl::string::is <class>) follow the harvested set automatically
- new tclcoreparity.test (4 tests, gated on have_tclcoredocs): choices equal the live-harvested set and every documented choice is really accepted; per-class docids exist for every class; error-vs-ok outcome agrees between real string is and parse_status across the 23-shape probe matrix (missing args, trailing flag-like str, option/class prefixes + ambiguity, unknown option/class, -failindex var consumption, per-version dict/unicode presence, divergent-classification shapes); version-note labels conditional on presence. Expectations derived from the LIVE interpreter, never version arithmetic - green on 9.0.3, 8.7a6 and (via a direct tcltest driver, since runtests infrastructure does not run under the plain 8.6 kit) 8.6.13
- full punk/args + punk/ns trees green (219 pass + 1 pre-existing skip) incl the have_tclcoredocs cmdhelp pins

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
4bb7b3d68f
  1. 4
      CHANGELOG.md
  2. 2
      GOALS.md
  3. 2
      punkproject.toml
  4. 99
      src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm
  5. 3
      src/modules/punk/args/moduledoc/tclcore-buildversion.txt
  6. 2
      src/tests/modules/AGENTS.md
  7. 151
      src/tests/modules/punk/args/testsuites/args/tclcoreparity.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.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.

2
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.

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.8.1"
version = "0.8.2"

99
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

3
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)

2
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 {} <prefix>` 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)

151
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.
Loading…
Cancel
Save