diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c136f6d..dc0ed49c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.4.12] - 2026-07-09 + +- Tcl 9.1b0 runtime compatibility (first `punk91.exe` build on `tclsfe-x64`; the developed baseline remains Tcl 9.0.2): + - punk::lib 0.3.1: TIP 746 removed `lseq`'s expr-operand behaviour in Tcl 9.1 — `punk::lib::range` (lseq branch) now normalizes `int[+-]int` offset operands itself, so callers like `range 0 [llength $list]-1` keep working (this broke `punk::ansi::grepstr` and with it example-block highlighting and the punk::args examples.test under 9.1). Also aligned the lseq branch with the tcl8 fallback contract: default `by` now infers direction (descending `range 5 1` previously returned empty under tcl9 — silently breaking documented reverse-range callers) and `by 0` returns an empty list (9.1's `lseq ... by 0` changed to return one element). Direct `lseq` expression operands wrapped in `expr` (`lzipn_tcl9b`/`lzipn_tcl9c`/`cols`/`cols2`). `check::has_tclbug_safeinterp_compile` falls back to `interp invokehidden tcl:unsupported:disassemble` — Tcl 9.1 safe interps hide `tcl::unsupported::*` instead of exposing it. + - punk::args 0.3.2: `zero_based_posns` expr-wraps its `count-1` lseq operand (TIP 746). + - New `modules/punk/lib` range.test pins the `range` contract (offset expressions, direction inference, `by 0`) under both 9.0 and 9.1. + ## [0.4.11] - 2026-07-09 - punk::args 0.3.1: fixed `helpers::example` `-title` path calling bare `[a]` (resolvable only where a global `a` alias exists, e.g. punk shells — errored under plain tclsh); found by the new test coverage. diff --git a/punkproject.toml b/punkproject.toml index df96223a..899dddfc 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.4.11" +version = "0.4.12" diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index b3fc4860..441b99c1 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -11590,7 +11590,8 @@ tcl::namespace::eval punk::args::lib { #The internal rep can be an 'arithseries' with no string representation proc zero_based_posns {count} { if {$count < 1} {return} - lseq 0 $count-1 + #expr wrapper required: lseq in Tcl 9.1+ (TIP 746) no longer evaluates expression operands + lseq 0 [expr {$count-1}] } } else { proc zero_based_posns {count} { diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index 25dc5c4e..f74da494 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.3.1 +0.3.2 #First line must be a semantic version number #all other lines are ignored. +#0.3.2 - Tcl 9.1 compatibility (TIP 746 removed expr behaviour from lseq operands): zero_based_posns now wraps its count-1 operand in expr #0.3.1 - fixed helpers::example -title path calling bare [a] (only resolvable where a global 'a' alias exists, e.g. punk shells) - now punk::ansi::a; found by the new examples.test running under plain tclsh #0.3.0 - new: -choicealiases {alias canonical ...} argument option (G-040) - aliases accepted exact under any -choiceprefix/-nocase setting, join the prefix-calculation pool when -choiceprefix is true, and normalize to their canonical choice in parse results; deny applies to the matched name (alias deny requires full alias; canonical reached via alias exempt from the canonical's own deny); usage display folds aliases into the canonical entry's label as an (alias:...) note and includes alias names in display prefix calculation; alias->existing-choice and no-collision validated at definition resolve. new public helper punk::args::choiceword_match - the single choice-word matching implementation extracted from get_dict, also consumed by punk::ns cmd_traverse for doc-lookup parity #0.2.3 - fixed "@opts -any 1" adhoc option passthrough: an option not in the definition crashed get_dict ("can't read argname") at the name-mapping stage instead of passing through - adhoc opts now fall back to their raw supplied name (argstate/arg_checks already carried them); commented out a debug puts stderr (">>>_get_dict_can_assign_value NOT alloc_ok...") that fired on every failed clause type assignment (e.g. any multiform parse miss) diff --git a/src/modules/punk/lib-999999.0a1.0.tm b/src/modules/punk/lib-999999.0a1.0.tm index 6130d0ba..aa93ddb6 100644 --- a/src/modules/punk/lib-999999.0a1.0.tm +++ b/src/modules/punk/lib-999999.0a1.0.tm @@ -320,7 +320,10 @@ tcl::namespace::eval punk::lib::check { interp create x -safe } errMsg]} { x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] + if {[catch {x eval {tcl::unsupported::disassemble proc ::ensembletest}} bytecode_safe]} { + #Tcl 9.1+ removes tcl::unsupported::* from safe interps - disassemble is instead a hidden command (tcl:unsupported:disassemble) + set bytecode_safe [interp invokehidden x tcl:unsupported:disassemble proc ::ensembletest] + } if {$show} { puts safe: puts $bytecode_safe @@ -1311,14 +1314,27 @@ namespace eval punk::lib { #tcl 8.7+ lseq significantly faster, especially for larger ranges #The internal rep can be an 'arithseries' with no string representation #support minimal set from to - proc range {from to {by 1}} { + proc range {from to {by ""}} { #note inconsistency with lseq 1 10 by -9 vs lseq 1 10 by -10 #https://core.tcl-lang.org/tcl/tktview/999b6966b2 + #lseq in Tcl 9.1+ no longer evaluates expression operands such as 4-1 (TIP 746) + #range continues to accept int[+-]int offset forms e.g `range 0 [llength $list]-1` (same contract as the non-lseq fallback below) + if {![string is double -strict $from]} {set from [offset_expr $from]} + if {![string is double -strict $to]} {set to [offset_expr $to]} + if {$by eq ""} { + #direction inferred - lseq produces a descending sequence when from > to (matching the non-lseq fallback below) + return [lseq $from $to] + } + if {![string is double -strict $by]} {set by [offset_expr $by]} + if {$by == 0} { + #documented range behaviour (and lseq behaviour prior to Tcl 9.1): a step of zero produces no results + return [list] + } lseq $from $to by $by } } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 + #lseq prior to Tcl 9.1 (TIP 746) accepted basic expressions e.g 4-2 for both arguments + #range accepts int[+-]int offset forms in all branches e.g we can do range 0 [llength $list]-1 #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. #our range function doesn't support double like lseq does. (deliberate) review proc range {from to {by ""}} { @@ -1595,7 +1611,7 @@ namespace eval punk::lib { foreach len $lens list $args { lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} + lmap c [lseq 0 [expr {$numcolumns-1}]] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} } proc lzipn_tcl9c {args} { #SLOW @@ -1614,7 +1630,7 @@ namespace eval punk::lib { } lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] + set flatlist [lremove $flatlist {*}[lseq 0 to [expr {$flen-1}] by $cols_remaining]] incr cols_remaining -1 } return $zip_l @@ -8613,14 +8629,14 @@ tcl::namespace::eval punk::lib::flatgrid { } proc cols {list numcolumns {blank NULL}} { set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { + foreach colindex [lseq 0 [expr {$numcolumns-1}]] { lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] } return $cols } proc cols2 {list numcolumns {blank NULL}} { set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { + foreach colindex [lseq 0 [expr {$numcolumns-1}]] { lappend cols [col2 $list $numcolumns $colindex $blank] } return $cols diff --git a/src/modules/punk/lib-buildversion.txt b/src/modules/punk/lib-buildversion.txt index f5e59702..ee96c9b4 100644 --- a/src/modules/punk/lib-buildversion.txt +++ b/src/modules/punk/lib-buildversion.txt @@ -1,6 +1,7 @@ -0.3.0 +0.3.1 #First line must be a semantic version number #all other lines are ignored. +#0.3.1 - Tcl 9.1 compatibility (TIP 746 removed expr behaviour from lseq operands): range (lseq branch) now normalizes int[+-]int offset operands itself via offset_expr - callers such as `range 0 [llength $list]-1` keep working; also aligned the lseq branch with the fallback contract: default by now infers direction (range 5 1 -> descending, previously empty under tcl9) and by 0 returns empty (Tcl 9.1 lseq changed by-0 to return one element). lzipn_tcl9b/lzipn_tcl9c/cols/cols2: lseq expression operands wrapped in expr. check::has_tclbug_safeinterp_compile: Tcl 9.1 safe interps hide tcl::unsupported::* - falls back to interp invokehidden tcl:unsupported:disassemble #0.3.0 - new: punk::lib::check::has_libbug_udp_threadexit + libbug_udp_threadexit_applies classifier - version-based detection of tcludp < 1.0.13 on Tcl 9 Windows (per-thread exit handler closes process-global event handles; G-036). has_libbug_* is the new check family for bundled/vendored library bugs, surfaced through 'help tcl' alongside has_tclbug_*; buginfo dicts may carry a full 'url' reference key (non tcl-core trackers) #0.2.1 - call-site update for punk::console 0.6.0 tsv array rename: console -> punk_console (is_raw); no behaviour change #0.2.0 - new: snapshot_package_paths proc returns a script string reproducing tm list, auto_path, and package prefer for use in thread::create init scripts diff --git a/src/tests/core/AGENTS.md b/src/tests/core/AGENTS.md index e4ac4930..53c5eece 100644 --- a/src/tests/core/AGENTS.md +++ b/src/tests/core/AGENTS.md @@ -25,6 +25,7 @@ Selected tests taken from or modeled after Tcl core tests. These tests should pr - ` src/tests/runtests.tcl -report compact -show-passes 0 -include-paths core/**` passes for changes in this subtree. - Compare results with standard `tclsh` and the target Punk executable when compatibility behavior is the subject of the change. +- Known baselines (2026-07-09): under a native `tclsh` (9.0.x/9.1) exactly one failure is expected — `exec-14.3`. Under a built Punk executable (`punk902z`, `punk91`) two additional exec.test failures (`exec-16.2`, `exec-17.1`) and ~22 extra skips appear because the tests re-exec `[interpreter]` (a full punkshell exe whose boot touches stdio) with redirected/closed standard channels; these are punk-exe artifacts, identical on Tcl 9.0.2 and 9.1, not core incompatibilities. ## Child DOX Index diff --git a/src/tests/modules/punk/lib/testsuites/lib/range.test b/src/tests/modules/punk/lib/testsuites/lib/range.test new file mode 100644 index 00000000..2e096cff --- /dev/null +++ b/src/tests/modules/punk/lib/testsuites/lib/range.test @@ -0,0 +1,60 @@ +package require tcltest +package require punk::lib + +namespace eval ::testspace { + namespace import ::tcltest::* + variable common { + set result "" + } + + test range_basic {ascending and descending integer ranges (direction inferred when by not given)}\ + -setup $common -body { + lappend result [punk::lib::range 1 5] + lappend result [punk::lib::range 5 1] + }\ + -cleanup { + }\ + -result [list\ + {1 2 3 4 5}\ + {5 4 3 2 1} + ] + + test range_by_step {explicit by step}\ + -setup $common -body { + lappend result [punk::lib::range 1 7 2] + }\ + -cleanup { + }\ + -result [list\ + {1 3 5 7} + ] + + test range_offset_expr_operands {int[+-]int offset expressions accepted in all operands (lseq in Tcl 9.1+ TIP 746 no longer evaluates these itself)}\ + -setup $common -body { + #common calling pattern e.g punk::ansi::grepstr: range 0 [llength $list]-1 + set l {a b c d} + lappend result [punk::lib::range 0 [llength $l]-1] + lappend result [punk::lib::range 2-1 [llength $l]+1] + lappend result [punk::lib::range 0 8 1+1] + }\ + -cleanup { + }\ + -result [list\ + {0 1 2 3}\ + {1 2 3 4 5}\ + {0 2 4 6 8} + ] + + test range_by_zero {step of zero produces empty list as per lseq}\ + -setup $common -body { + lappend result [llength [punk::lib::range 1 5 0]] + set result + }\ + -cleanup { + }\ + -result [list\ + 0 + ] + +} +tcltest::cleanupTests ;#needed to produce test summary.