diff --git a/src/modules/punk/lib-999999.0a1.0.tm b/src/modules/punk/lib-999999.0a1.0.tm index cc3a9836..cc6245fa 100644 --- a/src/modules/punk/lib-999999.0a1.0.tm +++ b/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] diff --git a/src/modules/punk/lib-buildversion.txt b/src/modules/punk/lib-buildversion.txt index 15515207..b8c15f8c 100644 --- a/src/modules/punk/lib-buildversion.txt +++ b/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). diff --git a/src/tests/modules/punk/lib/testsuites/lib/compat.test b/src/tests/modules/punk/lib/testsuites/lib/compat.test index 3c48960b..b9eca632 100644 --- a/src/tests/modules/punk/lib/testsuites/lib/compat.test +++ b/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 "" + 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.