diff --git a/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/bootsupport/modules/punk/libunknown-0.1.tm index a88c1272..eca0df03 100644 --- a/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -100,6 +100,42 @@ tcl::namespace::eval ::punk::libunknown { } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::zipfs_tm_UnknownHandler + @cmd -name punk::libunknown::zipfs_tm_UnknownHandler\ + -summary\ + "Custom package unknown handler for tcl::tm::list module search paths."\ + -help\ + "Replaces Tcl's built-in ::tcl::tm::UnknownHandler as the first entry in the + package unknown handler chain. Called when package require cannot find a + satisfying version among already-registered ifneeded scripts. + + Key differences from Tcl's standard tm handler: + - Uses an epoch-based cache to avoid re-scanning directories that have already + been searched. The epoch is incremented when tcl::tm::paths changes (via a + trace), invalidating stale cache entries. + - Registers ifneeded scripts for ALL .tm files found in each scanned directory, + not just the requested package — so sibling packages are discovered as a + side effect. + - For static (zipfs) paths, skips re-scanning a directory if the requested + package was not found there in a previous scan within the same epoch. + - Falls through to the next handler (e.g zipfs_tclPkgUnknown) if no satisfying + candidate is found in the tm paths. + + The \$original parameter is the next handler in the package unknown chain, + called as a fallback when the tm search does not satisfy the request." + @leaders + original -type any -help\ + "Next handler in the package unknown chain (list or empty). Called as fallback if the tm search does not satisfy the request." + name -type string -help\ + "Package name being searched for." + @values -min 0 -max -1 + args -type any -optional 1 -multiple 1 -help\ + "Version requirements passed through from package require." + }] + } #will use standard mechanism for non zipfs paths in the tm list. proc zipfs_tm_UnknownHandler {original name args} { #------------------------------ @@ -441,6 +477,40 @@ tcl::namespace::eval ::punk::libunknown { uplevel 1 $original [::linsert $args 0 $name] } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::zipfs_tclPkgUnknown + @cmd -name punk::libunknown::zipfs_tclPkgUnknown\ + -summary\ + "Custom package unknown handler for auto_path/pkgIndex.tcl package search."\ + -help\ + "Replaces Tcl's built-in ::tclPkgUnknown as the second entry in the package + unknown handler chain. Called when zipfs_tm_UnknownHandler could not satisfy + the package request from the tm paths. + + Key differences from Tcl's standard tclPkgUnknown: + - Uses an epoch-based cache to avoid re-sourcing pkgIndex.tcl files that have + already been processed in the current epoch. + - Processes auto_path entries from end to front (same as tclPkgUnknown), but + tracks which packages and versions were added or changed by each pkgIndex.tcl + so that ifneeded scripts from earlier (higher-priority) paths can be reverted + if they were overridden by later (lower-priority) paths. + - The refresh/reversion mechanism ensures that path ordering priority is + respected even though auto_path is processed in reverse. + - Handles auto_path expansion: if a pkgIndex.tcl appends to auto_path during + sourcing, the new entries are reordered to follow the longest common prefix + rather than simply appending to the end. + - For static (zipfs) paths, skips re-sourcing pkgIndex.tcl files if the + requested package was not provided by that path in a previous scan." + @leaders + name -type string -help\ + "Package name being searched for." + @values -min 0 -max -1 + args -type any -optional 1 -multiple 1 -help\ + "Version requirements passed through from package require." + }] + } proc zipfs_tclPkgUnknown {name args} { #puts "-> zipfs_tclPkgUnknown $name $args EXPERIMENTAL" @@ -1173,6 +1243,39 @@ tcl::namespace::eval ::punk::libunknown { } return $versionlist } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::init + @cmd -name punk::libunknown::init\ + -summary\ + "Install punk::libunknown's custom package unknown handlers and epoch system in the current interp."\ + -help\ + "Renames the builtin ::package to ::package:: and installs a custom ::package proc + that intercepts 'forget' (controlled forgetting) and adds an 'epoch' subcommand + (for introspection). Sets package unknown to a two-handler chain: + zipfs_tm_UnknownHandler (tm paths) and zipfs_tclPkgUnknown (auto_path/pkgIndex). + + Installs traces on ::auto_path and ::tcl::tm::paths that increment the epoch + when these change, invalidating stale scan cache entries. + + Epoch sharing: if \$::punk::libunknown::epoch is already set before init is called + (e.g copied from a parent interp via punk::lib::interp_sync_package_paths -libunknown 1), + init accepts it as a pre-provided record and uses the parent's scan cache instead + of building one from scratch. Packages that were already known before the epoch was + set are recorded as 'untracked' — full scans are performed for them on first request. + + This proc should be called as early as possible in a new interp or thread, before + any package require calls that would trigger scans, to minimize the number of + untracked packages and maximize cache effectiveness. + + See also: punk::lib::interp_sync_package_paths, punk::lib::snapshot_package_paths" + @opts + -caller -type string -default "" -help\ + "Informational label identifying the calling context (e.g 'main.tcl', 'repl::init init_script'). + Used only for debug logging — does not affect behaviour." + }] + } proc init {args} { variable last_auto_path set last_auto_path [set ::auto_path] @@ -1503,6 +1606,33 @@ tcl::namespace::eval ::punk::libunknown { package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown} } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::package_query + @cmd -name punk::libunknown::package_query\ + -summary\ + "Diagnostic tool showing epoch tracking and scan cache state for a named package."\ + -help\ + "Returns a text report showing what punk::libunknown knows about the given + package from its epoch-based scan cache. The report includes: + + - Whether the package is in the tm or pkg 'untracked' sets (packages that were + already known before libunknown::init was called, meaning the epoch data is + not conclusive for them). + - A table of auto_path directories (pkg epoch) showing which packages were found + in each, with version numbers and epoch stamps for the requested package. + - A table of tcl::tm::list directories (tm epoch) showing which packages were + found in each, with version numbers for the requested package. + + This is primarily a developer diagnostic for understanding why a particular + package version was (or was not) loaded, and which search path provided it. + Requires punk::ansi and textblock for the table formatting." + @values -min 1 -max 1 + pkgname -type string -help\ + "Name of the package to inspect." + }] + } proc package_query {pkgname} { variable epoch diff --git a/src/bootsupport/modules/zzzload-0.1.0.tm b/src/bootsupport/modules/zzzload-0.1.0.tm index f397f22e..81c375a5 100644 --- a/src/bootsupport/modules/zzzload-0.1.0.tm +++ b/src/bootsupport/modules/zzzload-0.1.0.tm @@ -63,9 +63,15 @@ namespace eval zzzload { return $ver } if {$loader_tid eq ""} { - set loader_tid [thread::create -joinable -preserved] - #todo - set tcl::tm::list and ::auto_path in the loader thread to match the main thread. - #(startup process may have modified these paths) + #propagate tm list, auto_path, and package prefer to the loader thread + #so it can find the same packages as the parent interp. + if {![catch {package require punk::lib}]} { + set init_script [punk::lib::snapshot_package_paths] + set loader_tid [thread::create -joinable -preserved $init_script] + } else { + #punk::lib not available — fall back to bare thread + set loader_tid [thread::create -joinable -preserved] + } } if {![tsv::exists zzzload_pkg $pkgname]} { #puts stderr "zzzload pkg_require $pkgname" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm index a88c1272..eca0df03 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -100,6 +100,42 @@ tcl::namespace::eval ::punk::libunknown { } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::zipfs_tm_UnknownHandler + @cmd -name punk::libunknown::zipfs_tm_UnknownHandler\ + -summary\ + "Custom package unknown handler for tcl::tm::list module search paths."\ + -help\ + "Replaces Tcl's built-in ::tcl::tm::UnknownHandler as the first entry in the + package unknown handler chain. Called when package require cannot find a + satisfying version among already-registered ifneeded scripts. + + Key differences from Tcl's standard tm handler: + - Uses an epoch-based cache to avoid re-scanning directories that have already + been searched. The epoch is incremented when tcl::tm::paths changes (via a + trace), invalidating stale cache entries. + - Registers ifneeded scripts for ALL .tm files found in each scanned directory, + not just the requested package — so sibling packages are discovered as a + side effect. + - For static (zipfs) paths, skips re-scanning a directory if the requested + package was not found there in a previous scan within the same epoch. + - Falls through to the next handler (e.g zipfs_tclPkgUnknown) if no satisfying + candidate is found in the tm paths. + + The \$original parameter is the next handler in the package unknown chain, + called as a fallback when the tm search does not satisfy the request." + @leaders + original -type any -help\ + "Next handler in the package unknown chain (list or empty). Called as fallback if the tm search does not satisfy the request." + name -type string -help\ + "Package name being searched for." + @values -min 0 -max -1 + args -type any -optional 1 -multiple 1 -help\ + "Version requirements passed through from package require." + }] + } #will use standard mechanism for non zipfs paths in the tm list. proc zipfs_tm_UnknownHandler {original name args} { #------------------------------ @@ -441,6 +477,40 @@ tcl::namespace::eval ::punk::libunknown { uplevel 1 $original [::linsert $args 0 $name] } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::zipfs_tclPkgUnknown + @cmd -name punk::libunknown::zipfs_tclPkgUnknown\ + -summary\ + "Custom package unknown handler for auto_path/pkgIndex.tcl package search."\ + -help\ + "Replaces Tcl's built-in ::tclPkgUnknown as the second entry in the package + unknown handler chain. Called when zipfs_tm_UnknownHandler could not satisfy + the package request from the tm paths. + + Key differences from Tcl's standard tclPkgUnknown: + - Uses an epoch-based cache to avoid re-sourcing pkgIndex.tcl files that have + already been processed in the current epoch. + - Processes auto_path entries from end to front (same as tclPkgUnknown), but + tracks which packages and versions were added or changed by each pkgIndex.tcl + so that ifneeded scripts from earlier (higher-priority) paths can be reverted + if they were overridden by later (lower-priority) paths. + - The refresh/reversion mechanism ensures that path ordering priority is + respected even though auto_path is processed in reverse. + - Handles auto_path expansion: if a pkgIndex.tcl appends to auto_path during + sourcing, the new entries are reordered to follow the longest common prefix + rather than simply appending to the end. + - For static (zipfs) paths, skips re-sourcing pkgIndex.tcl files if the + requested package was not provided by that path in a previous scan." + @leaders + name -type string -help\ + "Package name being searched for." + @values -min 0 -max -1 + args -type any -optional 1 -multiple 1 -help\ + "Version requirements passed through from package require." + }] + } proc zipfs_tclPkgUnknown {name args} { #puts "-> zipfs_tclPkgUnknown $name $args EXPERIMENTAL" @@ -1173,6 +1243,39 @@ tcl::namespace::eval ::punk::libunknown { } return $versionlist } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::init + @cmd -name punk::libunknown::init\ + -summary\ + "Install punk::libunknown's custom package unknown handlers and epoch system in the current interp."\ + -help\ + "Renames the builtin ::package to ::package:: and installs a custom ::package proc + that intercepts 'forget' (controlled forgetting) and adds an 'epoch' subcommand + (for introspection). Sets package unknown to a two-handler chain: + zipfs_tm_UnknownHandler (tm paths) and zipfs_tclPkgUnknown (auto_path/pkgIndex). + + Installs traces on ::auto_path and ::tcl::tm::paths that increment the epoch + when these change, invalidating stale scan cache entries. + + Epoch sharing: if \$::punk::libunknown::epoch is already set before init is called + (e.g copied from a parent interp via punk::lib::interp_sync_package_paths -libunknown 1), + init accepts it as a pre-provided record and uses the parent's scan cache instead + of building one from scratch. Packages that were already known before the epoch was + set are recorded as 'untracked' — full scans are performed for them on first request. + + This proc should be called as early as possible in a new interp or thread, before + any package require calls that would trigger scans, to minimize the number of + untracked packages and maximize cache effectiveness. + + See also: punk::lib::interp_sync_package_paths, punk::lib::snapshot_package_paths" + @opts + -caller -type string -default "" -help\ + "Informational label identifying the calling context (e.g 'main.tcl', 'repl::init init_script'). + Used only for debug logging — does not affect behaviour." + }] + } proc init {args} { variable last_auto_path set last_auto_path [set ::auto_path] @@ -1503,6 +1606,33 @@ tcl::namespace::eval ::punk::libunknown { package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown} } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::package_query + @cmd -name punk::libunknown::package_query\ + -summary\ + "Diagnostic tool showing epoch tracking and scan cache state for a named package."\ + -help\ + "Returns a text report showing what punk::libunknown knows about the given + package from its epoch-based scan cache. The report includes: + + - Whether the package is in the tm or pkg 'untracked' sets (packages that were + already known before libunknown::init was called, meaning the epoch data is + not conclusive for them). + - A table of auto_path directories (pkg epoch) showing which packages were found + in each, with version numbers and epoch stamps for the requested package. + - A table of tcl::tm::list directories (tm epoch) showing which packages were + found in each, with version numbers for the requested package. + + This is primarily a developer diagnostic for understanding why a particular + package version was (or was not) loaded, and which search path provided it. + Requires punk::ansi and textblock for the table formatting." + @values -min 1 -max 1 + pkgname -type string -help\ + "Name of the package to inspect." + }] + } proc package_query {pkgname} { variable epoch diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zzzload-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zzzload-0.1.0.tm index f397f22e..81c375a5 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zzzload-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zzzload-0.1.0.tm @@ -63,9 +63,15 @@ namespace eval zzzload { return $ver } if {$loader_tid eq ""} { - set loader_tid [thread::create -joinable -preserved] - #todo - set tcl::tm::list and ::auto_path in the loader thread to match the main thread. - #(startup process may have modified these paths) + #propagate tm list, auto_path, and package prefer to the loader thread + #so it can find the same packages as the parent interp. + if {![catch {package require punk::lib}]} { + set init_script [punk::lib::snapshot_package_paths] + set loader_tid [thread::create -joinable -preserved $init_script] + } else { + #punk::lib not available — fall back to bare thread + set loader_tid [thread::create -joinable -preserved] + } } if {![tsv::exists zzzload_pkg $pkgname]} { #puts stderr "zzzload pkg_require $pkgname" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm index a88c1272..eca0df03 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -100,6 +100,42 @@ tcl::namespace::eval ::punk::libunknown { } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::zipfs_tm_UnknownHandler + @cmd -name punk::libunknown::zipfs_tm_UnknownHandler\ + -summary\ + "Custom package unknown handler for tcl::tm::list module search paths."\ + -help\ + "Replaces Tcl's built-in ::tcl::tm::UnknownHandler as the first entry in the + package unknown handler chain. Called when package require cannot find a + satisfying version among already-registered ifneeded scripts. + + Key differences from Tcl's standard tm handler: + - Uses an epoch-based cache to avoid re-scanning directories that have already + been searched. The epoch is incremented when tcl::tm::paths changes (via a + trace), invalidating stale cache entries. + - Registers ifneeded scripts for ALL .tm files found in each scanned directory, + not just the requested package — so sibling packages are discovered as a + side effect. + - For static (zipfs) paths, skips re-scanning a directory if the requested + package was not found there in a previous scan within the same epoch. + - Falls through to the next handler (e.g zipfs_tclPkgUnknown) if no satisfying + candidate is found in the tm paths. + + The \$original parameter is the next handler in the package unknown chain, + called as a fallback when the tm search does not satisfy the request." + @leaders + original -type any -help\ + "Next handler in the package unknown chain (list or empty). Called as fallback if the tm search does not satisfy the request." + name -type string -help\ + "Package name being searched for." + @values -min 0 -max -1 + args -type any -optional 1 -multiple 1 -help\ + "Version requirements passed through from package require." + }] + } #will use standard mechanism for non zipfs paths in the tm list. proc zipfs_tm_UnknownHandler {original name args} { #------------------------------ @@ -441,6 +477,40 @@ tcl::namespace::eval ::punk::libunknown { uplevel 1 $original [::linsert $args 0 $name] } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::zipfs_tclPkgUnknown + @cmd -name punk::libunknown::zipfs_tclPkgUnknown\ + -summary\ + "Custom package unknown handler for auto_path/pkgIndex.tcl package search."\ + -help\ + "Replaces Tcl's built-in ::tclPkgUnknown as the second entry in the package + unknown handler chain. Called when zipfs_tm_UnknownHandler could not satisfy + the package request from the tm paths. + + Key differences from Tcl's standard tclPkgUnknown: + - Uses an epoch-based cache to avoid re-sourcing pkgIndex.tcl files that have + already been processed in the current epoch. + - Processes auto_path entries from end to front (same as tclPkgUnknown), but + tracks which packages and versions were added or changed by each pkgIndex.tcl + so that ifneeded scripts from earlier (higher-priority) paths can be reverted + if they were overridden by later (lower-priority) paths. + - The refresh/reversion mechanism ensures that path ordering priority is + respected even though auto_path is processed in reverse. + - Handles auto_path expansion: if a pkgIndex.tcl appends to auto_path during + sourcing, the new entries are reordered to follow the longest common prefix + rather than simply appending to the end. + - For static (zipfs) paths, skips re-sourcing pkgIndex.tcl files if the + requested package was not provided by that path in a previous scan." + @leaders + name -type string -help\ + "Package name being searched for." + @values -min 0 -max -1 + args -type any -optional 1 -multiple 1 -help\ + "Version requirements passed through from package require." + }] + } proc zipfs_tclPkgUnknown {name args} { #puts "-> zipfs_tclPkgUnknown $name $args EXPERIMENTAL" @@ -1173,6 +1243,39 @@ tcl::namespace::eval ::punk::libunknown { } return $versionlist } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::init + @cmd -name punk::libunknown::init\ + -summary\ + "Install punk::libunknown's custom package unknown handlers and epoch system in the current interp."\ + -help\ + "Renames the builtin ::package to ::package:: and installs a custom ::package proc + that intercepts 'forget' (controlled forgetting) and adds an 'epoch' subcommand + (for introspection). Sets package unknown to a two-handler chain: + zipfs_tm_UnknownHandler (tm paths) and zipfs_tclPkgUnknown (auto_path/pkgIndex). + + Installs traces on ::auto_path and ::tcl::tm::paths that increment the epoch + when these change, invalidating stale scan cache entries. + + Epoch sharing: if \$::punk::libunknown::epoch is already set before init is called + (e.g copied from a parent interp via punk::lib::interp_sync_package_paths -libunknown 1), + init accepts it as a pre-provided record and uses the parent's scan cache instead + of building one from scratch. Packages that were already known before the epoch was + set are recorded as 'untracked' — full scans are performed for them on first request. + + This proc should be called as early as possible in a new interp or thread, before + any package require calls that would trigger scans, to minimize the number of + untracked packages and maximize cache effectiveness. + + See also: punk::lib::interp_sync_package_paths, punk::lib::snapshot_package_paths" + @opts + -caller -type string -default "" -help\ + "Informational label identifying the calling context (e.g 'main.tcl', 'repl::init init_script'). + Used only for debug logging — does not affect behaviour." + }] + } proc init {args} { variable last_auto_path set last_auto_path [set ::auto_path] @@ -1503,6 +1606,33 @@ tcl::namespace::eval ::punk::libunknown { package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown} } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::libunknown::package_query + @cmd -name punk::libunknown::package_query\ + -summary\ + "Diagnostic tool showing epoch tracking and scan cache state for a named package."\ + -help\ + "Returns a text report showing what punk::libunknown knows about the given + package from its epoch-based scan cache. The report includes: + + - Whether the package is in the tm or pkg 'untracked' sets (packages that were + already known before libunknown::init was called, meaning the epoch data is + not conclusive for them). + - A table of auto_path directories (pkg epoch) showing which packages were found + in each, with version numbers and epoch stamps for the requested package. + - A table of tcl::tm::list directories (tm epoch) showing which packages were + found in each, with version numbers for the requested package. + + This is primarily a developer diagnostic for understanding why a particular + package version was (or was not) loaded, and which search path provided it. + Requires punk::ansi and textblock for the table formatting." + @values -min 1 -max 1 + pkgname -type string -help\ + "Name of the package to inspect." + }] + } proc package_query {pkgname} { variable epoch diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zzzload-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zzzload-0.1.0.tm index f397f22e..81c375a5 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zzzload-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zzzload-0.1.0.tm @@ -63,9 +63,15 @@ namespace eval zzzload { return $ver } if {$loader_tid eq ""} { - set loader_tid [thread::create -joinable -preserved] - #todo - set tcl::tm::list and ::auto_path in the loader thread to match the main thread. - #(startup process may have modified these paths) + #propagate tm list, auto_path, and package prefer to the loader thread + #so it can find the same packages as the parent interp. + if {![catch {package require punk::lib}]} { + set init_script [punk::lib::snapshot_package_paths] + set loader_tid [thread::create -joinable -preserved $init_script] + } else { + #punk::lib not available — fall back to bare thread + set loader_tid [thread::create -joinable -preserved] + } } if {![tsv::exists zzzload_pkg $pkgname]} { #puts stderr "zzzload pkg_require $pkgname"