Browse Source

punk::lib 0.5.1: compat::lpop builtin-parity fixes, validated by differential matrix

Reviewed against the real 8.7/9 lpop via a 24-case differential (results,
final list state, error messages, errorcodes) - the compat proc was
correct on every success path; all divergences were error-path:
- out-of-range errors said 'tcl_lpop index ...' with no errorcode; now
  the builtin's exact message and {TCL VALUE INDEX OUTOFRANGE}
- variable-access failures named the internal upvar link variable and
  flattened detail (missing array element misreported as 'no such
  variable'; unset var lost its TCL LOOKUP errorcode). Fixed by probing
  with a plain read in the caller's frame BEFORE creating the upvar
  link - through a link, or once a link merely exists, Tcl downgrades
  the error class.
Finding: the assumed 8.6 divergence does not exist - 8.6's core index
parser already accepts TIP-176 M+N arithmetic, so compat::lpop now has
zero known divergence from the builtin (doctools note corrected).

compat.test extended (3 -> 10 tests): a differential parity matrix that
runs wherever the builtin exists (self-updating pin against future core
changes), static pins for values/errors/errorcodes that also run under
8.6 where no builtin exists, an unconditional M+N arithmetic pin, and
an 8.6-only pin that punk::lib installs lpop as the compat alias.
Green under Tcl 9.0.3 (9 pass/1 gated skip) and native 8.6.17
(9 pass/1 gated skip).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
a55344faa1
  1. 17
      src/modules/punk/lib-999999.0a1.0.tm
  2. 3
      src/modules/punk/lib-buildversion.txt
  3. 174
      src/tests/modules/punk/lib/testsuites/lib/compat.test

17
src/modules/punk/lib-999999.0a1.0.tm

@ -613,9 +613,20 @@ tcl::namespace::eval punk::lib::compat {
}
proc lpop {lvar args} {
#*** !doctools
#[call [fun lpop] [arg listvar] [opt {index}]]
#[call [fun lpop] [arg listvar] [opt {index ...}]]
#[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop
#upvar $lvar l
#[para] Index forms come from the running Tcl's core index parser (lindex/lrange/lreplace),
#[para] which accepts the same integer?\[+-]integer? / end?\[+-]integer? grammar on 8.6
#[para] (TIP 176) as 8.7/9. Behaviour - results, final list state, error messages and
#[para] errorcodes - matches the builtin; pinned by the compat testsuite (compat.test).
if {[catch {uplevel 1 [list ::set $lvar]} lreaderr lreadopts]} {
#variable access failed (unset variable, whole-array name, or missing array element).
#Probing with a plain read in the caller's frame - BEFORE creating the upvar link -
#reproduces the builtin's message and errorcode exactly (through a link, or once a link
#exists, the detail flattens: e.g 'no such variable' loses its TCL LOOKUP errorcode and
#a missing array element misreports as 'no such variable').
return -code error -errorcode [dict get $lreadopts -errorcode] $lreaderr
}
upvar 1 $lvar l
if {![llength $args]} {
set args [list end]
@ -628,7 +639,7 @@ tcl::namespace::eval punk::lib::compat {
for {set i 0} {$i < [llength $args]} {incr i} {
set idx [lindex $args $i]
if {![llength [lrange $subl $idx $idx]]} {
error "tcl_lpop index \"$idx\" out of range"
return -code error -errorcode [list TCL VALUE INDEX OUTOFRANGE] "index \"$idx\" out of range"
}
lappend path [lindex $args $i]
set subl [lindex $l {*}$path]

3
src/modules/punk/lib-buildversion.txt

@ -1,6 +1,7 @@
0.5.0
0.5.1
#First line must be a semantic version number
#all other lines are ignored.
#0.5.1 - compat::lpop builtin-parity fixes (validated by a 24-case differential matrix vs the 8.7/9 builtin, pinned in modules/punk/lib compat.test): out-of-range errors now use the builtin's message ('index "X" out of range' - was 'tcl_lpop index ...') and errorcode {TCL VALUE INDEX OUTOFRANGE}; variable-access failures (unset var / whole-array name / missing array element) reproduce the builtin's exact message and errorcode by probing with a plain read in the caller's frame BEFORE creating the upvar link (through a link, or once a link exists, Tcl flattens the detail - 'no such variable' loses its TCL LOOKUP errorcode and a missing array element misreports as 'no such variable'). No success-path changes. Doc note corrected: 8.6's core index parser already accepts TIP-176 M+N arithmetic, so compat::lpop has no known divergence from the builtin.
#0.5.0 - G-076: check::tclbug_console_deadspin_fixed_in set to 9.0.5 - the upstream f10d91c2d3 fix (ticket closed 2026-07-14, merged to core-9-0-branch whose head reports 9.0.5) passed the G-039 kill-procedure re-verification 2026-07-20 on a zig-built core-9-0-branch runtime with the watchdog disarmed (dead console delivered as a readable fileevent; repl exited via the eof path - needs punk::repl 0.5.2's repl_handler guard). Runtimes at or past 9.0.5 now neither warn via 'help tcl' nor arm the repl watchdog; earlier Tcl 9 windows runtimes keep both. Official release not required per the amended G-076 acceptance (self-built from upstream sources qualifies).
#0.5.0 - new check pair punk::lib::check::has_tclbug_nestedswitch_tracelines + tclbug_nestedswitch_tracelines_signature (pure facts-in/verdict-out classifier, G-076 console_deadspin style): live behavioural probe of upstream tcl tktview 5d5b1052280c976ea3d4 (execution-trace line mis-attribution for nested single-block switch arm bodies whose split-list index lands on a literal word of the switch command; punkshell-author-reported; punk-free repro scriptlib/developer/tcl_switch_traceline_repro.tcl, identical 8.6.17/8.7a6/9.0.3). The probe defines a dynamic-body proc, enterstep-traces inner arm 1 (affected index 1 -> reports 3) vs the default arm (index 5, beyond the 4-word command -> reports 2) and matches the characterized wrong/right signature - so detection flips automatically on a fixed Tcl (no version gate; followup steps for that day documented in the proc: re-verify with the repro script, flip the ns/cmdtrace.test GAP pins + canary, update the cmdtrace argdoc caveat, note fixed-in version). Surfaced via 'help tcl' warnings (level medium) alongside the other has_tclbug_* checks; probe is self-cleaning (temp proc/var removed in finally).
#0.4.3 - G-076: buginfo dicts gain an optional mitigated/mitigation axis (mitigated boolean + mitigation text) orthogonal to level - a check whose defect is covered by a shipped punkshell mitigation reports it so 'help tcl' can render the warning subdued while keeping the severity classification. has_tclbug_console_deadspin populates it: mitigated when punk::repl >= 0.5.0 (console liveness watchdog) is available to the runtime (udp-check-style version discovery without loading), with the watchdog scope described in the mitigation text (non-repl console reads remain exposed).

174
src/tests/modules/punk/lib/testsuites/lib/compat.test

@ -40,5 +40,179 @@ namespace eval ::testspace {
{0 1 2 2b 3 4 5}
]
# -------------------------------------------------------------------------
# punk::lib::compat::lpop
# The compat proc must match the 8.7/9 builtin exactly - results, final list
# state, error messages AND errorcodes. Where the builtin exists (Tcl 8.7+)
# a differential matrix pins parity directly; the static pins below also run
# under 8.6 (no builtin) against the known-correct builtin behaviour.
# No known index-form divergence: 8.6's core index parser already accepts the
# integer?[+-]integer? / end?[+-]integer? grammar (TIP 176 landed pre-8.6),
# so M+N arithmetic like 0+1 works via the compat path too (pinned below).
# -------------------------------------------------------------------------
#added 2026-07-21 (agent) - lpop compat validation prior to runtests harness 8.6 bring-across
#builtin lpop present (not the compat alias punk::lib installs on 8.6 - registered as 'lpop', so
#check both alias spellings)
tcltest::testConstraint lpopbuiltin [expr {[info commands ::lpop] ne "" && [interp alias {} ::lpop] eq "" && [interp alias {} lpop] eq ""}]
#run one lpop implementation against a variable prepared by varsetup; capture everything comparable
proc lpop_case {impl varsetup callargs} {
catch {unset ::testspace::tv}
catch {unset ::testspace::arr}
namespace eval ::testspace $varsetup
set code [catch {namespace eval ::testspace [concat $impl $callargs]} res copts]
set state "<none>"
catch {set state $::testspace::tv}
set ec ""
catch {set ec [dict get $copts -errorcode]}
return [list code $code result $res errorcode $ec liststate $state]
}
variable lpop_matrix {
{pop_end {set tv {a b c}} {tv}}
{pop_end_expl {set tv {a b c}} {tv end}}
{pop_0 {set tv {a b c}} {tv 0}}
{pop_mid {set tv {a b c}} {tv 1}}
{pop_end-1 {set tv {a b c}} {tv end-1}}
{single_elem {set tv {a}} {tv}}
{empty_str_elem {set tv {{} b}} {tv 0}}
{nested_1_0 {set tv {{a b} {c d}}} {tv 1 0}}
{nested_1_end {set tv {{a b} {c d}}} {tv 1 end}}
{nested_deep {set tv {{{x y} {z w}} {q r}}} {tv 0 1 0}}
{arith_idx {set tv {a b c}} {tv 0+1}}
{arith_bad {set tv {a b c d}} {tv end-1-1}}
{oob_high {set tv {a b c}} {tv 5}}
{oob_end+1 {set tv {a b c}} {tv end+1}}
{neg_idx {set tv {a b c}} {tv -1}}
{empty_list {set tv {}} {tv}}
{empty_list_0 {set tv {}} {tv 0}}
{bad_idx {set tv {a b c}} {tv foo}}
{nested_oob {set tv {{a b} {c d}}} {tv 1 5}}
{nested_nonlist {set tv {abc def}} {tv 0 1}}
{missing_var {catch {unset tv}} {tv}}
{whole_array {array set tv {x 1}} {tv}}
{missing_arrelem {array set arr {x 1}} {arr(nokey)}}
{nonlist_var {set tv "unmatched \{"} {tv}}
}
test compat_lpop_matrix_parity_vs_builtin {compat lpop matches builtin lpop across the case matrix (result, errorcode, final list state)}\
-constraints lpopbuiltin\
-setup $common -body {
variable lpop_matrix
set mismatches [list]
foreach case $lpop_matrix {
lassign $case label varsetup callargs
set b [lpop_case ::lpop $varsetup $callargs]
set c [lpop_case ::punk::lib::compat::lpop $varsetup $callargs]
if {$b ne $c} {
lappend mismatches [list $label builtin $b compat $c]
}
}
set mismatches
}\
-cleanup {
}\
-result {}
test compat_lpop_basic_values {compat lpop returns popped values and updates the list (static pins - run on 8.6 too)}\
-setup $common -body {
set l {a b c}
lappend result [punk::lib::compat::lpop l] $l
set l {a b c}
lappend result [punk::lib::compat::lpop l 0] $l
set l {{a b} {c d}}
lappend result [punk::lib::compat::lpop l 1 0] $l
set l {a}
lappend result [punk::lib::compat::lpop l] $l
set result
}\
-cleanup {
}\
-result [list\
c {a b}\
a {b c}\
c {{a b} d}\
a {}\
]
test compat_lpop_error_out_of_range {compat lpop out-of-range: builtin message and errorcode (static pins - run on 8.6 too)}\
-setup $common -body {
foreach {init idx} {
{a b c} 5
{a b c} end+1
{a b c} -1
{} end
} {
set l $init
set code [catch {punk::lib::compat::lpop l $idx} msg copts]
lappend result [list $code $msg [dict get $copts -errorcode] $l]
}
set result
}\
-cleanup {
}\
-result [list\
[list 1 {index "5" out of range} {TCL VALUE INDEX OUTOFRANGE} {a b c}]\
[list 1 {index "end+1" out of range} {TCL VALUE INDEX OUTOFRANGE} {a b c}]\
[list 1 {index "-1" out of range} {TCL VALUE INDEX OUTOFRANGE} {a b c}]\
[list 1 {index "end" out of range} {TCL VALUE INDEX OUTOFRANGE} {}]\
]
test compat_lpop_error_variable_access {compat lpop variable-access errors name the caller's variable with builtin errorcodes (static pins - run on 8.6 too)}\
-setup $common -body {
catch {unset novar}
set code [catch {punk::lib::compat::lpop novar} msg copts]
lappend result [list $code $msg [dict get $copts -errorcode]]
catch {unset anarr}
array set anarr {x 1}
set code [catch {punk::lib::compat::lpop anarr} msg copts]
lappend result [list $code $msg [dict get $copts -errorcode]]
set code [catch {punk::lib::compat::lpop anarr(nokey)} msg copts]
lappend result [list $code $msg [dict get $copts -errorcode]]
unset anarr
set result
}\
-cleanup {
}\
-result [list\
[list 1 {can't read "novar": no such variable} {TCL LOOKUP VARNAME novar}]\
[list 1 {can't read "anarr": variable is array} {TCL READ VARNAME}]\
[list 1 {can't read "anarr(nokey)": no such element in array} {TCL READ VARNAME}]\
]
test compat_lpop_error_bad_index {compat lpop bad index reports the core parser message (static pin - run on 8.6 too)}\
-setup $common -body {
set l {a b c}
set code [catch {punk::lib::compat::lpop l foo} msg]
lappend result $code [string match {bad index "foo"*} $msg] $l
set result
}\
-cleanup {
}\
-result {1 1 {a b c}}
test compat_lpop_index_arithmetic {compat lpop supports TIP-176 M+N index arithmetic on all supported Tcl versions (static pin - run on 8.6 too)}\
-setup $common -body {
set l {a b c}
lappend result [punk::lib::compat::lpop l 0+1] $l
set result
}\
-cleanup {
}\
-result {b {a c}}
test compat_lpop_alias_installed_when_no_builtin {8.6 only: punk::lib installs lpop as an alias to the compat proc}\
-constraints !lpopbuiltin\
-setup $common -body {
lappend result [expr {[interp alias {} lpop] ne "" || [interp alias {} ::lpop] ne ""}]
set l {a b c}
lappend result [lpop l] $l
set result
}\
-cleanup {
}\
-result {1 c {a b}}
}
tcltest::cleanupTests ;#needed to produce test summary.

Loading…
Cancel
Save