commandstack 0.6.0:
- tokenid unique+monotonic per (renamer,command): counter increments the
real namespace variable (was an apply-local copy stuck at 1). Same-
renamer re-renames chain through the full stack, are removable by
exact token, and a third rename no longer collides on the parked name.
- get_next_command resolves via a maintained token->implementation dict
(O(1) dispatch, no lsearch stack scan; parked stacks keep dispatching).
- get_IMPLEMENTOR: ::tcl::info::cmdtype qualified in guard and call -
the builtin classification branch is reachable (undetermined on 8.6).
- debug validates its argument with a clean error naming on_off.
- Channel discipline: informational warnings debug-gated on stderr,
stray bare 'puts stderr' removed, errors stay errors.
- Records carry trailing did_rename 0|1 (leading token/renamer key order
contract unchanged); no-rename returns {implementation {} did_rename 0}.
- rename_command errors when -renamer appears in a non-leading position.
- get_stack raw-key-first (Rename_stack-parked stacks retrievable);
Rename_stack returns 1/0; Delete_stack errors while records are live.
- New delegation helper commandstack::next (PUNKARGS-documented, in help).
- known_renamers defaults reconciled to the vendored packages' actual
renamer strings (packagetrace packagesuppress).
punk::packagepreference 0.2.1: install re-install guard reads record key
'renamer' (was 'rename' - a second install threw instead of returning 0).
Tests: the five _GAP_ defect pins flipped to fixed-behaviour pins; new
pins for did_rename/key-order + token-map consistency, silent cycle with
debug off, -renamer misplacement, commandstack::next delegation incl
caller context, Rename_stack/get_stack parking, Delete_stack guard;
new packagepreference installguard.test (double-install returns 0,
uninstall/re-install cycle with fresh tokenid). commandstack suite 33/33,
packagepreference 6/6 - green on tclsh90 (9.0.3) and punk86 (8.6).
Live punk91 src smoke: packagepreference/auto_execok/cd renames working.
Claude-Session: https://claude.ai/code/session_01TNn3C58Cpekt3CsZNLCqvR
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
error "commandstack::rename_command -renamer is recognised only as the leading argument. usage: rename_command ?-renamer <string>? command procargs procbody"
set command [uplevel 1 [list namespace which $command]]
if {$command eq ""} {
#review
puts stderr "commandstack::rename_command no rename performed for command '$command' by '$renamer'. command '$command' not found in calling context. Ensure command name is fully qualified or that command exists."
#add something to stack?
return [dict create implementation ""]
if {$debug} {
puts stderr "commandstack::rename_command no rename performed for command '[lindex $arglist 0]' by '$renamer'. command not found in calling context. Ensure command name is fully qualified or that command exists."
set tokenid [dict get $renamer_command_tokens [list $renamer $command]]
#e.g packageTrace and packageSuppress packages use this convention.
set nextinfo [uplevel 1 [list\
apply {{command renamer procbody} {
apply {{command renamer procbody tokenid} {
#todo - munge dash so we can make names in renamed_commands separable
# {- _dash_} ?
set mungedcommand [string map {:: _ns_} $command]
set mungedrenamer [string map {:: _ns_} $renamer]
set tokenid [lindex [dict incr renamer_command_tokens [list $renamer $command]] 1]
set next_target ::commandstack::renamed_commands::${mungedcommand}-original-$mungedrenamer-$tokenid ;#default is to assume we are the only one playing around with it, but we'll check for known associates too.
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command with same procbody - Aborting rename."
puts stderr [::commandstack::show_stack $command]
if {$::commandstack::debug} {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command with same procbody - Aborting rename."
puts stderr [::commandstack::show_stack $command]
}
} else {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command - but appears to be with new code - proceeding."
puts stdout "----------"
puts stdout "$current_code"
puts stdout "----------"
puts stdout "$new_code"
puts stdout "----------"
if {$::commandstack::debug} {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command - but appears to be with new code - proceeding."
puts stderr "----------"
puts stderr "$current_code"
puts stderr "----------"
puts stderr "$new_code"
puts stderr "----------"
}
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
}
} else {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command, but is not immediate predecessor - proceeding anyway... (untested)"
puts stderr
if {$::commandstack::debug} {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command, but is not immediate predecessor - proceeding anyway... (untested)"
}
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
}
@ -468,7 +551,9 @@ namespace eval commandstack {
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
} else {
puts stderr "(commandstack::rename_command) Warning - pkg:'$next_implementor' has renamed the '$command' command. Attempting to cooperate. (untested)"
if {$::commandstack::debug} {
puts stderr "(commandstack::rename_command) Warning - pkg:'$next_implementor' has renamed the '$command' command. Attempting to cooperate. (untested)"
}
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
dict set token_implementations [dict get $nextinfo token] [dict get $nextinfo next_target]
return $new_record
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::next
@cmd -name "commandstack::next" -&
-summary -&
"Delegate from an override body to the next implementation on the stack." -&
-help -&
{Convenience delegation helper for proc bodies installed by
rename_command. Called directly from an override body it
resolves the body's pre-set COMMANDSTACKNEXT variable and
invokes that implementation with the given arguments at the
override's caller level - exactly equivalent to the manual
convention:
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
Returns the implementation's result. An error is raised when
called from a frame without a COMMANDSTACKNEXT variable (i.e
from anywhere other than directly inside an installed
override body). The COMMANDSTACKNEXT variables remain the
primitive interface - existing consumers need not change.}
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed through to the next implementation."
}]
}
proc next {args} {
#call directly from a rename_command-installed override body only:
#COMMANDSTACKNEXT is set at the top of such bodies by the injected header
upvar 1 COMMANDSTACKNEXT COMMANDSTACKNEXT
if {![info exists COMMANDSTACKNEXT]} {
error "(commandstack::next) ERROR: no COMMANDSTACKNEXT variable in the calling frame. commandstack::next must be called directly from a proc body installed by commandstack::rename_command"
}
#uplevel 2 = the override's caller frame - same frame the manual
#'uplevel 1 [list $COMMANDSTACKNEXT {*}$args]' convention evaluates in
uplevel 2 [list $COMMANDSTACKNEXT {*}$args]
}
#todo - concept of 'pop' for renamer. Remove topmost entry specific to the renamer
#todo - removal by token to allow renamer to have multiple entries for one command but to remove one that is not the topmost
#todo - removal of all entries pertaining to a particular renamer
@ -588,6 +710,8 @@ namespace eval commandstack {
set command [uplevel 1 [list namespace which $command]]
variable all_stacks
variable known_renamers
variable token_implementations
variable debug
if {$renamer ni $known_renamers} {
error "(commandstack::remove_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers' for command '$command'. Ensure remove_rename called from same context as rename_command was, or explicitly supply exact token or {<command> <renamer>}"
}
@ -614,7 +738,11 @@ namespace eval commandstack {
set rewrite_record [lindex $stack $rewrite_posn]
if {[dict get $rewrite_record next_implementor] ne $renamer} {
puts stderr "(commandstack::remove_rename) WARNING: next record on the commandstack didn't record '$renamer' as the next_implementor - not deleting implementation [dict get $rewrite_record implementation]"
#anomalous stack state (external interference or historical duplicate
#tokens) - conservatively leave the parked implementation in place
if {$debug} {
puts stderr "(commandstack::remove_rename) WARNING: next record on the commandstack didn't record '$renamer' as the next_implementor - not deleting implementation [dict get $rewrite_record implementation]"
}
} else {
uplevel #0 [list rename [dict get $rewrite_record implementation] ""]
}
@ -623,9 +751,12 @@ namespace eval commandstack {
dict set rewrite_record implementation [dict get $doomed_record implementation]
lset stack $rewrite_posn $rewrite_record
dict set all_stacks $command $stack
#re-point the rewritten record's token at its new implementation
dict set token_implementations [dict get $rewrite_record token] [dict get $doomed_record implementation]
}
set stack [lreplace $stack $doomed_posn $doomed_posn]
dict set all_stacks $command $stack
dict unset token_implementations [dict get $doomed_record token]
"Discard a command's rename-stack records (maintenance - unexported)." -&
-help -&
{Removes the command's entry from the stacks dict WITHOUT
undoing any renames - the renamed commands themselves are
left in place. Always returns 1, whether or not a stack
existed. CAUTION: if overrides installed by rename_command
are still live, deleting their stack breaks the
COMMANDSTACKNEXT lookup - get_next_command then resolves to
the (overridden) command itself and the next call recurses
until the interp recursion limit. Not exported - intended for
maintenance/experimentation only (under review).}
{Removes the command's entry from the stacks dict. An error
is raised when the stack still holds rename records - live
overrides installed by rename_command must be removed with
remove_rename first (deleting the records of a live override
historically broke the COMMANDSTACKNEXT lookup so the next
call recursed to the interp limit). An empty stack entry
(the residue after the last remove_rename) is deleted and 1
is returned; a command with no stack entry also returns 1.
Not exported - intended for maintenance/experimentation only
(under review).}
@values -min 1 -max 1
command -type string -help -&
"Fully qualified command name key (no resolution is performed)."
"Stacks-dict key - fully qualified command name or parked
stack name (no resolution is performed)."
}]
}
proc Delete_stack {command} {
variable all_stacks
if {[dict exists $all_stacks $command]} {
set stack [dict get $all_stacks $command]
if {[llength $stack]} {
#records represent live renames - deleting them would break the
#COMMANDSTACKNEXT delegation of the installed overrides (the next
#call would recurse to the interp limit)
error "(commandstack::Delete_stack) ERROR: stack for '$command' still holds [llength $stack] live rename record(s) - remove them with commandstack::remove_rename first"
#0.2.1 - G-160: install's already-installed guard reads record key 'renamer' (the key that exists) - the historical 'rename' read made a second install throw a dict error instead of silently returning 0 (latent in kits, which install once per interp)
#0.2.0 - G-058 static-vs-bundled policy: on require of a package in the runtime static baseline (::punkboot::static_packages), ensure the static ifneeded mapping exists and trigger the package unknown index scan so bundled vfs/module copies register BEFORE resolution - version comparison then applies (highest wins; identical versions resolve to the bundled copy). Static REGISTRATIONS (empty-filename info loaded entries) are now excluded from the loaded-shared-object same-version forcing, which would otherwise pin resolution to the static version even when a bundled copy is genuinely newer.
#0.1.1 - moduledoc auto-load success notice moved from stdout to stderr (stdout must stay clean for script/exec contexts e.g the punk 'script' subcommand; failure branch already used stderr)
#overrides from two different renamers stack bottom-first; the second record
#identifies the first renamer via the injected IMPLEMENTOR magic comment and
#parks its proc at the dash-form name; calls run top-down through the chain.
@ -337,11 +385,12 @@ namespace eval ::testspace {
}
} -result {1 1 1 1}
#known_renamers ships preseeded with ::packagetrace and ::packageSuppress (the
#original cooperating packages - note the vendored packages currently register
#as plain-word 'packagetrace'/'packagesuppress', so these defaults do not match
#them); new renamers are appended by rename_command.
test commandstack_known_renamers_defaults {known_renamers preseeded with ::packagetrace ::packageSuppress; rename_command appends new renamers}\
#known_renamers ships preseeded with the strings the vendored cooperating
#packages actually pass as -renamer: plain-word packagetrace/packagesuppress
#(G-160 reconciliation - the historical ::packagetrace/::packageSuppress
#defaults matched no actual registration); new renamers are appended by
#rename_command.
test commandstack_known_renamers_defaults {known_renamers preseeded with the vendored packages' actual renamer strings; rename_command appends new renamers}\
#KNOWN DEFECT (pinned): the per-renamer token counter increments an apply-local
#renamer_command_tokens variable, so tokenid never advances past 1. A
#same-renamer re-rename with NEW code proceeds (warning captured) and stacks a
#second record - but with a DUPLICATE token {::tgt ::csA 1}. get_next_command
#finds the FIRST matching record, so the new override delegates straight to the
#ORIGINAL, silently bypassing the renamer's still-stacked first override.
test commandstack_GAP_samerenamer_new_body_duplicate_token_bypass {same-renamer new-body re-rename stacks a duplicate token and dispatch bypasses the first override}\
#G-160 FIXED-BEHAVIOUR PIN (flipped from _GAP_ duplicate-token bypass): the
#tokenid counter increments the real namespace variable, so a same-renamer
#re-rename with NEW code gets a DISTINCT token {::tgt ::csA 2}, delegates to
#that renamer's PREVIOUS override (full chain, not the original), and the
#first override remains individually removable by its exact token - after
#which dispatch re-links to the original.
test commandstack_samerenamer_new_body_distinct_token_chain {same-renamer new-body re-rename gets a distinct token, chains through the first override, exact-token removal re-links}\
#KNOWN DEFECT (pinned): because tokenid is stuck at 1, a THIRD same-renamer
#rename computes the same renamed_commands target name as the second and the
#underlying [rename] errors ('command already exists'). The failure is at least
#clean: it occurs before the live command is touched - the stack keeps its 2
#records and the command remains callable.
test commandstack_GAP_third_samerenamer_rename_name_collision {third same-renamer rename collides on the parked-implementation name and errors cleanly}\
set chained [::tgt x]
commandstack::remove_rename [dict get $r1 token]
list [llength $stack] [dict get $r1 token] [dict get $r2 token] \
[dict get $r2 implementation] $chained [::tgt x] \
#G-160 FIXED-BEHAVIOUR PIN (flipped from _GAP_ parked-name collision): with
#unique tokenids a THIRD same-renamer rename computes a fresh parked name and
#succeeds - the call chains through all three overrides to the original.
test commandstack_third_samerenamer_rename_succeeds {third same-renamer rename succeeds with a fresh token and the call chains through all three overrides}\
#KNOWN DEFECT (pinned): a renamer re-renaming after ANOTHER renamer took the top
#proceeds with the 'not immediate predecessor' warning (and a stray bare
#'puts stderr' that writes the literal word 'stderr' to stdout - asserted via
#the shim's {stdout stderr} log entry), stacking a third record - but its
#duplicate token again resolves to the renamer's FIRST record, so dispatch
#bypasses both intermediate overrides.
test commandstack_GAP_rerename_after_other_renamer_bypasses_chain {re-rename after another renamer warns, stacks a third record, and dispatch bypasses both intermediate overrides}\
#the commandstack::next delegation helper: called directly from an installed
#override body it delegates to the body's COMMANDSTACKNEXT at the override's
#caller level - result AND caller-context identical to the manual
#'uplevel 1 [list $COMMANDSTACKNEXT {*}$args]' convention (the base proc
#reports its caller's namespace to prove the frame); called from anywhere
#else it errors cleanly.
test commandstack_next_helper_delegation {commandstack::next delegates like the manual uplevel convention incl caller context; errors outside an override body}\
set logged [putslog_matches "*1st detected rename of command '::tgt'*"]
set d [commandstack::debug 0]
set e [catch {commandstack::debug notabool}]
list $a $b $c $logged $d $e
set e [catch {commandstack::debug notabool} emsg]
list $a $b $c $logged $d $e \
[string match "*on_off*'notabool'*not a boolean*" $emsg]
}
} -result {0 1 1 1 0 1}
} -result {0 1 1 1 0 1 1}
#Delete_stack always returns 1 (stack present or not) and does NOT undo renames.
#KNOWN FOOT-GUN (pinned): deleting the stack under a live override breaks
#COMMANDSTACKNEXT resolution - get_next_command falls back to the command name
#itself, so the next call recurses to the interp limit.
test commandstack_GAP_delete_stack_live_rename_recursion {Delete_stack under a live rename leaves the override calling itself - recursion limit error}\
#G-160 FIXED-BEHAVIOUR PIN (flipped from _GAP_ self-delegation recursion):
#Delete_stack ERRORS while the stack still holds live rename records (the
#override keeps working - no recursion), still returns 1 for a never-stacked
#command, and deletes the empty stack entry left after the last remove_rename.
test commandstack_delete_stack_guard {Delete_stack errors under live renames leaving the override working; empty and missing stacks return 1}\
set rec [commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set d1 [commandstack::Delete_stack ::tgt]
}]
set c1 [catch {commandstack::Delete_stack ::tgt} msg1]
set still_works [::tgt x]
set d2 [commandstack::Delete_stack ::never_stacked]
set code [catch {::tgt x} msg]
list $d1 $d2 $code [string match "*too many nested evaluations*" $msg]
commandstack::remove_rename [dict get $rec token]
set empty_entry_exists [dict exists [commandstack::get_stack] ::tgt]
set d3 [commandstack::Delete_stack ::tgt]
list $c1 [string match "*live rename record*remove_rename first*" $msg1] \
$still_works $d2 $empty_entry_exists $d3 \
[dict exists [commandstack::get_stack] ::tgt]
}
} -result {1 1 1 1}
#Rename_stack re-keys stack records without touching commands; the parked key is
#invisible to get_stack <name> (namespace which resolution fails for a
#non-command name) but present in the no-arg dict; renaming onto an existing
#stack key errors.
test commandstack_rename_stack_parks_records {Rename_stack parks records under a new key - visible only via the no-arg dict; existing key errors}\
} -result {1 1 {base x} 1 1 1 0}
#G-160: Rename_stack re-keys stack records without touching commands and
#returns 1 (moved) / 0 (no stack at oldname) instead of leaking the whole
#stacks dict; get_stack retrieves the parked records by raw key; delegation
#of the live override keeps working while parked (token map resolution);
#renaming onto an existing stack key errors.
test commandstack_rename_stack_parks_records {Rename_stack returns 1/0, parked records retrievable via get_stack, live dispatch survives parking; existing key errors}\
#first install returns 1 and stacks one punk::packagepreference record; the
#SECOND install takes the already-installed guard (which now reads the
#record key that exists - renamer) and returns 0 silently without stacking
#anything; ::package keeps working through the override.
test packagepreference_double_install_returns_0 {second install returns 0 silently via the renamer-key guard, stack keeps one record, ::package still works}\
-constraints ppsrc -body {
pp_probe {
set i1 [punk::packagepreference::install]
set i2 [punk::packagepreference::install]
set stack [commandstack::get_stack ::package]
list $i1 $i2 [llength $stack] [dict get [lindex $stack 0] renamer] \
[expr {[package provide Tcl] ne ""}] [llength $::PUTS_LOG]