diff --git a/src/AGENTS.md b/src/AGENTS.md
index b43f58c8..2a8612ba 100644
--- a/src/AGENTS.md
+++ b/src/AGENTS.md
@@ -68,7 +68,12 @@ Recovery after a wrong path guess:
- Use `punk make.tcl project` or `punk902z make.tcl project` inside Punk shell when building binaries through Punk.
- Binary images are platform-specific; build on each target platform rather than expecting a cross-platform flag.
- Remove `_build/` artifacts only when a clean/resync is needed, then rerun the relevant `make.tcl` command. Avoid partial cleans that break boot modules.
-- TODO (build maintenance): built module trees accumulate superseded intermediate versions (e.g `modules/punk/console-0.1.2.tm` and `-0.1.3.tm` alongside `-0.1.4.tm`), and `vfscommonupdate`/`bootsupport` propagate them into `_vfscommon.vfs`, bootsupport snapshots and shipped binaries. A prune step for superseded intermediates should run before payloads are wrapped. Constraint: `.punkcheck` state must stay consistent with the pruning — either record the deletions through punkcheck's event mechanism or prune the now-extraneous install records from the affected `.punkcheck` files (root `modules/`, `src/bootsupport`, project-layout bootsupport copies); silent file deletion with dangling punkcheck records would corrupt change detection. Version resolution always selects the highest version, so accumulation is size/cruft hygiene rather than a correctness issue.
+- Superseded module intermediates are pruned automatically so payload wrapping inherits clean trees:
+ - `punk::mix::cli::lib::prune_superseded_target_modules` runs inside `build_modules_from_source_to_base` after each module install/skip (covers root `modules/`, `modules_tcl8/`, `modules_tcl9/`), and from `make.tcl bootsupport` for non-glob `include_modules.config` entries (glob entries may intentionally track multiple versions and are never pruned).
+ - `punk::mix::cli::lib::prune_sourcevanished_targets` mirror-prunes recorded `.tm` targets whose recorded source files no longer exist: project-layout bootsupport copies (installer `punkboot-bootsupport`) and root `modules*/` vendormodule copies (installer `make.tcl`).
+ - Every deletion is recorded as a punkcheck DELETE event in the owning `.punkcheck`, so change detection stays consistent. Files without a qualifying punkcheck install record (e.g placed manually) are never deleted — they are reported to stderr and must be removed by hand if unwanted.
+ - Install records carry virtual `module_name`/`module_version` SOURCE entries (punkcheck `targetset_addsource_virtual`, punkcheck >= 0.3.0) identifying which product of a source fileset a target is — needed because successive versions are typically built from the same physical source files. Prune trusts this identity; for legacy records it falls back to requiring the candidate's recorded sources to share the keep-version's source folder(s) and module-name prefix.
+ - The make.tcl call sites are guarded with `info commands` checks: after changing this tooling itself, the first `make.tcl modules` + `make.tcl bootsupport` pass builds/propagates the new tooling (prune skipped with a stderr note) and the next pass prunes.
- Use `tcl::tm::path add
` to surface project modules when writing focused tooling.
- Review VS Code Tcl lint diagnostics before submitting new agent-produced Tcl code, but do not use lint tooling to reformat existing code.
- When touching VFS payloads, describe regeneration steps in durable docs if the workflow changes.
diff --git a/src/bootsupport/AGENTS.md b/src/bootsupport/AGENTS.md
index a32841cb..0ec12731 100644
--- a/src/bootsupport/AGENTS.md
+++ b/src/bootsupport/AGENTS.md
@@ -9,6 +9,7 @@ Modules and libraries required during the build/bootstrap/make process before th
- Agents should not directly modify files in this tree unless the task specifically targets boot behaviour.
- Bootsupport modules are snapshots that may lag behind or diverge from the corresponding `src/modules/` versions intentionally.
- The systematic update workflow is `modules/include_modules.config` (entries are ` ` pairs; base `src/vendormodules` for vendored modules, base `modules` for the project's *built* root modules - build with `make.tcl modules` first) followed by `tclsh src/make.tcl bootsupport`, which also propagates snapshots into the project-layout bootsupport trees.
+- For non-glob config entries only the latest version is tracked: `make.tcl bootsupport` prunes punkcheck-recorded superseded older `.tm` versions from the bootsupport folders (recorded as DELETE events in `src/bootsupport/.punkcheck`), and mirror-prunes project-layout copies whose recorded sources vanished. Use a glob entry to intentionally keep multiple versions. Files without punkcheck install records are never pruned automatically.
- `dev lib.copyasmodule` is a manual convenience for copying a module (or a single-file pkgIndex.tcl library, converting it to .tm form) from a running punk shell - see `modules/README.md`; it is not the primary update mechanism.
## Local Contracts
diff --git a/src/bootsupport/modules/fauxlink-0.1.1.tm b/src/bootsupport/modules/fauxlink-0.1.1.tm
index 2fc9c5fb..970e47da 100644
--- a/src/bootsupport/modules/fauxlink-0.1.1.tm
+++ b/src/bootsupport/modules/fauxlink-0.1.1.tm
@@ -205,7 +205,7 @@ namespace eval fauxlink {
# %2F "/"
# %2f "/"
# %7f (del)
- #we exclude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
+ #we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
#
set decode_map [dict merge $decode_map [dict create\
%09 \t\
diff --git a/src/bootsupport/modules/include_modules.config b/src/bootsupport/modules/include_modules.config
index 72b5b2de..49d0fee9 100644
--- a/src/bootsupport/modules/include_modules.config
+++ b/src/bootsupport/modules/include_modules.config
@@ -7,7 +7,16 @@
#The original idea was that bootsupport could be a subset - but in practice we seem to need pretty much everything?
#we still get the advantage that the bootsupport modules can be updated independently (less frequently - after testing)
-#each entry - base module
+#each entry - base modulematch
+# base - path relative to projectroot containing built .tm modules (e.g src/vendormodules, or 'modules' for the project's built root modules)
+# modulematch - namespaced module name; :: qualifiers map to subfolders (e.g test::tomlish -> test/tomlish-*.tm). Surrounding ':' are trimmed.
+# plain name (no dash) - matches -*.tm
+# name with dash e.g name-0.1 - version-line pin, matches -0.1*.tm
+# Both of the above are 'non-glob' entries: make.tcl bootsupport copies only the LATEST matching version and prunes
+# punkcheck-recorded superseded older versions from the bootsupport folder (recorded as DELETE events in src/bootsupport/.punkcheck).
+# any glob chars * ? [ ] - ALL matching files are copied and nothing is pruned - use a glob entry to intentionally keep multiple versions.
+# Files without punkcheck install records are never pruned automatically.
+# (glob detection: [regexp {[*?\[\]]} $modulematch] in make.tcl bootsupport_localupdate; see also src/bootsupport/AGENTS.md)
set bootsupport_modules [list\
src/vendormodules commandstack\
src/vendormodules cksum\
@@ -18,7 +27,6 @@ set bootsupport_modules [list\
src/vendormodules md5\
src/vendormodules metaface\
src/vendormodules modpod\
- src/vendormodules overtype\
src/vendormodules packagetest\
src/vendormodules pattern\
src/vendormodules patterncmd\
diff --git a/src/bootsupport/modules/metaface-1.2.8.tm b/src/bootsupport/modules/metaface-1.2.8.tm
deleted file mode 100644
index cc55ada8..00000000
--- a/src/bootsupport/modules/metaface-1.2.8.tm
+++ /dev/null
@@ -1,6365 +0,0 @@
-
-package provide metaface [namespace eval metaface {
- variable version
- set version 1.2.8
-}]
-
-# 2025-09-29 - update outdated 'trace vinfo' to 'trace info variable' - both work for 8.6, but vinfo deprecated for tcl 9+
-# 2023-07 - add .. MetaMethods
-
-
-#example datastructure:
-#$_ID_
-#{
-#i
-# {
-# this
-# {
-# {16 ::p::16 item ::>x {}}
-# }
-# role2
-# {
-# {17 ::p::17 item ::>y {}}
-# {18 ::p::18 item ::>z {}}
-# }
-# }
-#context {}
-#}
-
-#$MAP
-#invocantdata {16 ::p::16 item ::>x {}}
-#interfaces {level0
-# {
-# api0 {stack {123 999}}
-# api1 {stack {333}}
-# }
-# level0_default api0
-# level1
-# {
-# }
-# level1_default {}
-# }
-#patterndata {patterndefaultmethod {}}
-
-
-namespace eval ::p::predator {}
-#temporary alternative to ::p::internals namespace.
-# - place predator functions here until ready to replace internals.
-
-
-namespace eval ::p::snap {
- variable id 0 ;#ever-increasing non-reused snapshot-id to identify ::p::snapshot namespaces used to allow overlay-rollbacks.
-}
-
-
-
-
-# not called directly. Retrieved using 'info body ::p::predator::getprop_template'
-#review - why use a proc instead of storing it as a string?
-proc ::p::predator::getprop_template {_ID_ args} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {"%varspace%" eq ""} {
- set ns ::p::${OID}
- } else {
- if {[string match "::*" "%varspace%"]} {
- set ns "%varspace%"
- } else {
- set ns ::p::${OID}::%varspace%
- }
- }
-
-
- if {[llength $args]} {
- #lassign [lindex $invocant 0] OID alias itemCmd cmd
- if {[array exists ${ns}::o_%prop%]} {
- #return [set ${ns}::o_%prop%($args)]
- if {[llength $args] == 1} {
- return [set ::p::${OID}::o_%prop%([lindex $args 0])]
- } else {
- return [lindex [set ::p::${OID}::o_%prop%([lindex $args 0])] {*}[lrange $args 1 end]]
- }
- } else {
- set val [set ${ns}::o_%prop%]
-
- set rType [expr {[scan [namespace tail $val] >%s rType] ? {object} : {unknown}}]
- if {$rType eq "object"} {
- #return [$val . item {*}$args]
- return [$val {*}$args]
- } else {
- #treat as list?
- return [lindex $val $args]
- }
- }
- } else {
- return [set ${ns}::o_%prop%]
- }
-}
-
-
-proc ::p::predator::getprop_template_immediate {_ID_ args} {
- if {[llength $args]} {
- if {[array exists %ns%::o_%prop%]} {
- return [set %ns%::o_%prop%($args)]
- } else {
- set val [set %ns%::o_%prop%]
- set rType [expr {[scan [namespace tail $val] >%s rType] ? {object} : {unknown}}]
- if {$rType eq "object"} {
- #return [$val . item {*}$args]
- #don't assume defaultmethod named 'item'!
- return [$val {*}$args]
- } else {
- #treat as list?
- return [lindex $val $args]
- }
- }
- } else {
- return [set %ns%::o_%prop%]
- }
-}
-
-
-
-
-
-
-
-
-proc ::p::predator::getprop_array {_ID_ prop args} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
-
- #upvar 0 ::p::${OID}::o_${prop} prop
- #1st try: assume array
- if {[catch {array get ::p::${OID}::o_${prop}} result]} {
- #treat as list (why?)
- #!review
- if {[info exists ::p::${OID}::o_${prop}]} {
- array set temp [::list]
- set i 0
- foreach element ::p::${OID}::o_${prop} {
- set temp($i) $element
- incr i
- }
- set result [array get temp]
- } else {
- error "unable to retrieve [set ::p::${OID}::o_${prop}] contents in 'array get' format"
- }
- }
- return $result
-}
-
-proc ::p::predator::setprop_template {prop _ID_ args} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {"%varspace%" eq ""} {
- set ns ::p::${OID}
- } else {
- if {[string match "::*" "%varspace%"]} {
- set ns "%varspace%"
- } else {
- set ns ::p::${OID}::%varspace%
- }
- }
-
-
- if {[llength $args] == 1} {
- #return [set ::p::${OID}::o_%prop% [lindex $args 0]]
- return [set ${ns}::o_%prop% [lindex $args 0]]
-
- } else {
- if {[array exists ${ns}::o_%prop%] || ![info exists ${ns}::o_%prop%]} {
- #treat attempt to perform indexed write to nonexistant var, same as indexed write to array
-
- #2 args - single index followed by a value
- if {[llength $args] == 2} {
- return [set ${ns}::o_%prop%([lindex $args 0]) [lindex $args 1]]
- } else {
- #multiple indices
- #return [set ::p::${OID}::o_%prop%([lrange $args 0 end-1]) [lindex $args end]]
- return [lset ${ns}::o_%prop%([lindex $args 0]) {*}[lrange $args 1 end-1] [lindex $args end] ]
- }
- } else {
- #treat as list
- return [lset ${ns}::o_%prop% [lrange $args 0 end-1] [lindex $args end]]
- }
- }
-}
-
-#--------------------------------------
-#property read & write traces
-#--------------------------------------
-
-
-proc ::p::predator::propref_trace_read {get_cmd _ID_ refname prop indices vtraced idx op} {
-
- #puts stderr "\t-->propref_trace_read get_cmd:'$get_cmd' refname:'$refname' prop:'$prop' indices:'$indices' $vtraced idx:'$idx' "
-
- #set cmd ::p::${OID}::(GET)$prop ;#this is an interp alias to the head of the implementation command-chain.
-
- if {[llength $idx]} {
- if {[llength $idx] == 1} {
- set ${refname}($idx) [$get_cmd $_ID_ {*}$indices $idx]
- } else {
- lset ${refname}([lindex $idx 0]) [lrange $idx 1 end] [$get_cmd $_ID_ {*}$indices {*}$idx]
- }
- return ;#return value ignored - in a trace we can only return the value by setting the traced variable to a value
- } else {
- if {![info exists $refname]} {
- set $refname [$get_cmd $_ID_ {*}$indices]
- } else {
- set newval [$get_cmd $_ID_ {*}$indices]
- if {[set $refname] ne $newval} {
- set $refname $newval
- }
- }
- return
- }
-}
-
-
-
-
-proc ::p::predator::propref_trace_write {_ID_ OID full_varspace refname vname idx op} {
- #note 'vname' may be upvar-ed local - we need the fully qualified name so must use passed in $refname
- #puts stdout "\t-->propref_trace_write $OID ref:'$refname' var:'$vname' idx:'$idx'"
-
-
- #derive the name of the write command from the ref var.
- set indices [lassign [split [namespace tail $refname] +] prop]
-
-
- #assert - we will never have both a list in indices and an idx value
- if {[llength $indices] && ($idx ne "")} {
- #since Tcl has no nested arrays - we can't write to an idx within something like ${prop}+x
- #review - are there any datastructures which would/should allow this?
- #this assertion is really just here as a sanity check for now
- error "propref_trace_write unexpected values. Didn't expect a refname of the form ${prop}+* as well as an idx value"
- }
-
- #upvar #0 ::p::${OID}::_meta::map MAP
- #puts "-->propref_trace_write map: $MAP"
-
- #temporarily deactivate refsync trace
- #puts stderr -->1>--removing_trace_o_${field}
-### trace remove variable ::p::${OID}::o_${prop} [::list write] [::list ::p::predator::propvar_write_TraceHandler $OID $prop]
-
- #we need to catch, and re-raise any error that we may receive when writing the property
- # because we have to reinstate the propvar_write_TraceHandler after the call.
- #(e.g there may be a propertywrite handler that deliberately raises an error)
-
- set excludesync_refs $refname
- set cmd ::p::${OID}::(SET)$prop
-
-
- set f_error 0
- if {[catch {
-
- if {![llength $indices]} {
- if {[string length $idx]} {
- $cmd $_ID_ $idx [set ${refname}($idx)]
- #::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop}($idx) [list]
- ### ::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop} [list $idx]
-
- } else {
- $cmd $_ID_ [set $refname]
- ### ::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop} [list]
- }
- } else {
- #puts " ++>> cmd:$cmd indices:'$indices' refname:'$refname'\n"
- $cmd $_ID_ {*}$indices [set $refname]
- ### ::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop} $indices
- }
-
- } result]} {
- set f_error 1
- }
-
-
-
-
- #::p::predator::propvar_write_TraceHandler $OID $prop ::p::${OID}::o_${prop} $indices write
- #reactivate refsync trace
- #puts stderr "****** reactivating refsync trace on o_$field"
- #puts stderr -->2>--reactivating_trace_o_${field}
- ### trace add variable ::p::${OID}::o_${prop} [::list write] [::list ::p::predator::propvar_write_TraceHandler $OID $prop]
-
-
- if {$f_error} {
- #!todo - review error & 'return' functions for proper way to throw error, preserving callstack info for debugging.
- # ? return -code error $errMsg ? -errorinfo
-
- #!quick n dirty
- #error $errorMsg
- return -code error -errorinfo $::errorInfo $result
- } else {
- return $result
- }
-}
-
-
-
-
-
-proc ::p::predator::propref_trace_array {_ID_ OID refname vref idx op} {
- #puts stderr "\t-->propref_trace_array OID:$OID refname:'$refname' var:'$vref' index:'$idx' operation:'$op'"
- #NOTE - do not rely on $vref !!!! (can be upvared - so could be anything. e.g during 'parray' calls it is set to 'array')
-
- set indices [lassign [split [namespace tail $refname] +] prop] ;#make sure 'prop' is set
-
- #set updated_value [::p::predator::getprop_array $prop $_ID_]
- #puts stderr "-->array_Trace updated_value:$updated_value"
- if {[catch {array set $refname [::p::predator::getprop_array $_ID_ $prop ]} errm]} {
- puts stderr "-->propref_trace_array error $errm"
- array set $refname {}
- }
-
- #return value ignored for
-}
-
-
-#--------------------------------------
-#
-proc ::p::predator::object_array_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd
-
-
- #don't rely on variable name passed by trace - may have been 'upvar'ed
- set refvar ::p::${OID}::_ref::__OBJECT
-
- #puts "+=====>object_array_trace $map '$vref' '$idx' '$op' refvar: $refvar"
-
- set iflist [dict get $MAP interfaces level0]
-
- set plist [list]
-
- #!todo - get propertylist from cache on object(?)
- foreach IFID [lreverse $iflist] {
- dict for {prop pdef} [set ::p::${IFID}::_iface::o_properties] {
- #lassign $pdef v
- if {[catch {lappend plist $prop [set ::p::${OID}::o_${prop}]}]} {
- if {[array exists ::p::${OID}::o_${prop}]} {
- lappend plist $prop [array get ::p::${OID}::o_${prop}]
- } else {
- #ignore - array only represents properties that have been set.
- #error "property $v is not set"
- #!todo - unset corresponding items in $refvar if needed?
- }
- }
- }
- }
- array set $refvar $plist
-}
-
-
-proc ::p::predator::object_read_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd
- #don't rely on variable name passed by trace.
- set refvar ::p::${OID}::_ref::__OBJECT
-
- #puts "\n\n+=====>object_read_trace map:'$MAP' '$vref' '$idx' '$op' refvar: $refvar\n\n"
-
- #!todo? - build a list of all interface properties (cache it on object??)
- set iflist [dict get $MAP interfaces level0]
- set IID ""
- foreach id [lreverse $iflist] {
- if {$idx in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set IID $id
- break
- }
- }
-
- if {[string length $IID]} {
- #property
- if {[catch {set ${refvar}($idx) [::p::${id}::_iface::(GET)$idx $_ID_]} errmsg]} {
- puts stderr "\twarning: ::p::${id}::_iface::(GET)$idx retrieval failed (array?) errmsg:$errmsg"
- }
- } else {
- #method
- error "property '$idx' not found"
- }
-}
-
-
-proc ::p::predator::object_unset_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd
-
- #!todo - ???
-
- if {![llength [info commands ::p::${OID}::$idx]]} {
- error "no such method or property: '$idx'"
- } else {
- #!todo? - build a list of all interface properties (cache it on object??)
- set iflist [dict get $MAP interfaces level0]
- set found 0
- foreach id [lreverse $iflist] {
- if {$idx in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set found 1
- break
- }
- }
-
- if {$found} {
- unset ::p::${OID}::o_$idx
- } else {
- puts stderr "\tWARNING: UNIMPLEMENTED CASE! (unset) object_unset_trace id:$OID objectcmd:[lindex [dict get $MAP invocantdata] 3] var:$vref prop:$idx"
- }
- }
-}
-
-
-proc ::p::predator::object_write_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd
- #don't rely on variable name passed by trace.
- set refvar ::p::${OID}::_ref::__OBJECT
- #puts "+=====>object_write_trace $MAP '$vref' '$idx' '$op' refvar: $refvar"
-
-
- if {![llength [info commands ::p::${OID}::$idx]]} {
- #!todo - create new property in interface upon attempt to write to non-existant?
- # - or should we require some different kind of object-reference for that?
- array unset $refvar $idx ;#make sure 'array names' on the ref doesn't include this $idx
- error "no such method or property: '$idx'"
- } else {
- #!todo? - build a list of all interface properties (cache it on object??)
- set iflist [dict get $MAP interfaces level0]
- set IID ""
- foreach id [lreverse $iflist] {
- if {$idx in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set IID $id
- break
- }
- }
-
- #$IID is now topmost interface in default iStack which has this property
-
- if {[string length $IID]} {
- #write to defined property
-
- ::p::${IID}::_iface::(SET)$idx $_ID_ [set ${refvar}($idx)]
- } else {
- #!todo - allow write of method body back to underlying object?
- #attempted write to 'method' ..undo(?)
- array unset $refvar $idx ;#make sure 'array names' on the ref doesn't include this $idx
- error "cannot write to method '$idx'"
- #for now - disallow
- }
- }
-
-}
-
-
-
-proc ::p::predator::propref_trace_unset {_ID_ OID refname vref idx op} {
- #note 'vref' may be upvar-ed local - we need the fully qualified name so must use passed in $refname
-
- set refindices [lassign [split [namespace tail $refname] +] prop]
- #derive the name of any potential PropertyUnset command from the refname. i.e (UNSET)$prop
- #if there is no PropertyUnset command - we unset the underlying variable directly
-
- trace remove variable ::p::${OID}::o_${prop} [::list unset] [::list ::p::predator::propvar_unset_TraceHandler $OID $prop]
-
-
- if {[catch {
-
- #assert if refname is complex (prop+idx etc), we will not get a reference trace with an $idx value
- #i.e
- if {[llength $refindices] && [string length $idx]} {
- puts stderr "\t !!!!! unexpected call to propref_trace_unset oid:'$OID' refname:'$refname' vref:'$vref' idx:'$idx' op:'$op'"
- error "unexpected call to propref_trace_unset"
- }
-
-
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set iflist [dict get $MAP interfaces level0]
- #find topmost interface containing this $prop
- set IID ""
- foreach id [lreverse $iflist] {
- if {$prop in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set IID $id
- break
- }
- }
- if {![string length $IID]} {
- error "propref_trace_unset failed to find property '$prop' on objectid $OID ([lindex [dict get $_ID_ i this] 0 3])"
- }
-
-
-
- if {[string length $idx]} {
- #eval "$_alias ${unset_}$field $idx"
- #what happens to $refindices???
-
-
- #!todo varspace
-
- if {![llength $refindices]} {
- #puts stdout "\t 1a@@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
-
- if {![llength [info commands ::p::${IID}::_iface::(UNSET)$prop]]} {
- unset ::p::${OID}::o_${prop}($idx)
- } else {
- ::p::${IID}::_iface::(UNSET)$prop $_ID_ $idx
- }
-
-
- #manually call refsync, passing it this refvar as an exclusion
- ::p::predator::refsyncvar_unset_manualupdate $OID $refname $prop ::p::${OID}::o_${prop} $idx
- } else {
- #assert - won't get here
- error 1a
-
- }
-
- } else {
- if {[llength $refindices]} {
- #error 2a
- #puts stdout "\t 2a@@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
-
- if {![llength [info commands ::p::${IID}::_iface::(UNSET)$prop]]} {
- #review - what about list-type property?
- #if {[array exists ::p::${OID}::o_${prop}]} ???
- unset ::p::${OID}::o_${prop}($refindices)
- } else {
- ::p::${IID}::_iface::(UNSET)$prop $_ID_ $refindices
- }
-
-
-
- #manually call refsync, passing it this refvar as an exclusion
- ::p::predator::refsyncvar_unset_manualupdate $OID $refname $prop ::p::${OID}::o_${prop} $refindices
-
-
- } else {
- #puts stdout "\t 2b@@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
-
- #ref is not of form prop+x etc and no idx in the trace - this is a plain unset
- if {![llength [info commands ::p::${IID}::_iface::(UNSET)$prop]]} {
- unset ::p::${OID}::o_${prop}
- } else {
- ::p::${IID}::_iface::(UNSET)$prop $_ID_ ""
- }
- #manually call refsync, passing it this refvar as an exclusion
- ::p::predator::refsyncvar_unset_manualupdate $OID $refname $prop ::p::${OID}::o_${prop} {}
-
- }
- }
-
-
- } errM]} {
- #set ::LAST_UNSET_ERROR "$errM\n[set ::errorInfo]"
- set ruler [string repeat - 80]
- puts stderr "\t$ruler"
- puts stdout "\t @@@@ERROR propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
- puts stderr "\t$ruler"
- puts stderr $errM
- puts stderr "\t$ruler"
-
- } else {
- #puts stdout "\t @@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
- #puts stderr "*@*@*@*@ end propref_trace_unset - no error"
- }
-
- trace add variable ::p::${OID}::o_${prop} [::list unset] [::list ::p::predator::propvar_unset_TraceHandler $OID $prop]
-
-
-}
-
-
-
-
-proc ::p::predator::refsyncvar_unset_manualupdate {OID triggeringRef prop vtraced vidx} {
- #Do not use 'info exists' (avoid triggering read trace) - use info vars
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- #puts " **> lappending '::p::REF::${OID}::$prop'"
- lappend refvars ::p::${OID}::_ref::$prop
- }
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*]
-
- if {[string length $triggeringRef]} {
- set idx [lsearch -exact $refvars $triggeringRef]
- if {$idx >= 0} {
- set refvars [lreplace $refvars[set refvars {}] $idx $idx] ;#note inline K combinator [set refvars {}]
- }
- }
- if {![llength $refvars]} {
- #puts stderr " %%%%%%%%%% no refvars for propvar_unset_TraceHandler to update - short circuiting . $OID $triggeringRef $prop $vtraced $vidx"
- return
- }
-
-
- #*usually* triggeringRef is not in the reflist because the triggeringRef is being unset
- # - but this is not the case when we do an array unset of an element using a reference to the whole array e.g "array unset [>obj . arr .] b"
- if {([string length $triggeringRef]) && ($triggeringRef in $refvars)} {
- #puts stderr "\t@@@@@@@@@@ propvar_unset_TraceHandler unexpected situation. triggeringRef $triggeringRef in refvars:$refvars during unset ???"
- puts stderr "\t@@@@@ propvar_unset_TraceHandler triggeringRef $triggeringRef is in refvars list - probably a call of form 'array unset \[>obj .arr .\] someindex'"
- }
-
-
- puts stderr "\t refsyncvar_unset_manualupdate OID:'$OID' triggeringRef:'$triggeringRef' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx' "
-
-
-
- upvar $vtraced SYNCVARIABLE
-
-
- #We are only interested in suppressing the 'setGet_TraceHandler' traces on refvars
- array set traces [::list]
-
- #puts stderr "*-*-*-*-*-* refvars \n- [join $refvars "\n- "]"
-
-
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
- if {$ops in {read write unset array}} {
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- }
- }
- }
- }
-
-
- if {[array exists SYNCVARIABLE]} {
- #underlying variable is an array - we are presumably unsetting just an element
- set vtracedIsArray 1
- } else {
- #!? maybe the var was an array - but it's been unset?
- set vtracedIsArray 0
- }
-
- #puts stderr "--------------------------------------------------\n\n"
- #some things we don't want to repeat for each refvar in case there are lots of them..
-
- #set triggeringRefIdx $vidx
-
- if {[string match "${prop}+*" [namespace tail $triggeringRef]]} {
- set triggering_indices [lrange [split [namespace tail $triggeringRef] +] 1 end]
- } else {
- set triggering_indices [list]
- }
-
-
- #puts stderr ">>>...----refsync-trace = $vtraced $op refvars:$refvars"
- #puts stderr ">>> [trace info variable $vtraced]"
- #puts "--- unset branch refvar:$refvar"
-
- if {[llength $vidx]} {
- #trace called with an index - must be an array
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
-
- if {[string match "${prop}+*" $reftail]} {
- #!todo - add test
- if {$vidx eq [lrange [split $reftail +] 1 end]} {
- #unset if indices match
- error "untested, possibly unused branch spuds1"
- #puts "1111111111111111111111111"
- unset $refvar
- }
- } else {
- #test exists - #!todo - document which one
-
- #see if we succeeded in unsetting this element in the underlying variables
- #(may have been blocked by a PropertyUnset body)
- set element_exists [uplevel 1 [::list info exists ${vtraced}($vidx)]]
- #puts "JJJJJJ vtraced:$vtraced vidx:$vidx element_exists:$element_exists"
- if {$element_exists} {
- #do nothing it wasn't actually unset
- } else {
- #puts "JJJJJ unsetting ${refvar}($vidx)"
- unset ${refvar}($vidx)
- }
- }
- }
-
- } else {
-
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
-
- if {[string match "${prop}+*" $reftail]} {
- #check indices of triggering refvar match this refvars indices
-
- if {$reftail eq [namespace tail $triggeringRef]} {
- #!todo - add test
- error "untested, possibly unused branch spuds2"
- #puts "222222222222222222"
- unset $refvar
- } else {
-
- #error "untested - branch spuds2a"
-
- }
-
- } else {
- #!todo -add test
- #reference is directly to property var
- error "untested, possibly unused branch spuds3"
- #theoretically no other non-indexed ref.. so $triggeringRefIdx must contain non-zero-len string?
- puts "\t33333333333333333333"
-
- if {[string length $triggeringRefIdx]} {
- unset $refvar($triggeringRefIdx)
- }
- }
- }
-
- }
-
- #!todo - understand.
- #puts stderr "\n*****\n propvar_unset_TraceHandler $refvar unset $prop $args \n*****\n"
- #catch {unset $refvar} ;#oops - Tcl_EventuallyFree called twice - abnormal program termination (tcl8.4?)
-
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names traces] {
- foreach tinfo $traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing setGet trace '$ops' on variable $rv"
- trace add variable $rv $ops $cmd
- }
- }
-
-
-
-
-
-}
-
-
-proc ::p::predator::propvar_unset_TraceHandler {OID prop vtraced vidx op} {
-
- upvar $vtraced SYNCVARIABLE
-
- set refvars [::list]
- #Do not use 'info exists' (avoid triggering read trace) - use info vars
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- lappend refvars ::p::${OID}::_ref::$prop
- }
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*]
-
- #short_circuit breaks unset traces for array elements (why?)
-
-
- if {![llength $refvars]} {
- #puts stderr "\t%%%%%%%%%% no refvars for propvar_unset_TraceHandler to update - short circuiting . OID:'$OID' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx'"
- return
- } else {
- puts stderr "\t****** [llength $refvars] refvars for propvar_unset_TraceHandler to update. OID:'$OID' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx'"
- }
-
- if {[catch {
-
-
-
- #We are only interested in suppressing the 'setGet_TraceHandler' traces on refvars
- array set traces [::list]
-
- #puts stderr "*-*-*-*-*-* refvars \n- [join $refvars "\n- "]"
-
-
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
- if {$ops in {read write unset array}} {
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- }
- }
- }
- }
-
-
-
-
- if {[array exists SYNCVARIABLE]} {
-
- #underlying variable is an array - we are presumably unsetting just an element
- set vtracedIsArray 1
- } else {
- #!? maybe the var was an array - but it's been unset?
- set vtracedIsArray 0
- }
-
- #puts stderr "--------------------------------------------------\n\n"
- #some things we don't want to repeat for each refvar in case there are lots of them..
- set triggeringRefIdx $vidx
-
-
- #puts stderr ">>>...----refsync-trace = $vtraced $op refvars:$refvars"
- #puts stderr ">>> [trace info variable $vtraced]"
- #puts "--- unset branch refvar:$refvar"
-
-
- if {[llength $vidx]} {
- #trace called with an index - must be an array
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
-
- if {[string match "${prop}+*" $reftail]} {
- #!todo - add test
- if {$vidx eq [lrange [split $reftail +] 1 end]} {
- #unset if indices match
- error "untested, possibly unused branch spuds1"
- #puts "1111111111111111111111111"
- unset $refvar
- }
- } else {
- #test exists - #!todo - document which one
-
- #see if we succeeded in unsetting this element in the underlying variables
- #(may have been blocked by a PropertyUnset body)
- set element_exists [uplevel 1 [::list info exists ${vtraced}($vidx)]]
- #puts "JJJJJJ vtraced:$vtraced vidx:$vidx element_exists:$element_exists"
- if {$element_exists} {
- #do nothing it wasn't actually unset
- } else {
- #puts "JJJJJ unsetting ${refvar}($vidx)"
- unset ${refvar}($vidx)
- }
- }
- }
-
-
- } else {
-
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
- unset $refvar
-
- }
-
- }
-
-
- #!todo - understand.
- #puts stderr "\n*****\n propvar_unset_TraceHandler $refvar unset $prop $args \n*****\n"
- #catch {unset $refvar} ;#oops - Tcl_EventuallyFree called twice - abnormal program termination (tcl8.4?)
-
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names traces] {
- foreach tinfo $traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing setGet trace '$ops' on variable $rv"
- trace add variable $rv $ops $cmd
- }
- }
-
- } errM]} {
- set ruler [string repeat * 80]
- puts stderr "\t$ruler"
- puts stderr "\t>>>>>>>$ propvar_unset_TraceHandler OID:'$OID' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx' $op"
- puts stderr "\t$ruler"
- puts stderr $::errorInfo
- puts stderr "\t$ruler"
-
- }
-
-}
-
-proc ::p::predator::refsyncvar_write_manualupdate {OID triggeringRef prop vtraced indices} {
- error hmmmmm
- upvar $vtraced SYNCVARIABLE
- #puts stderr "\t>>>>>>>$ refsyncvar_write_manualupdate $OID '$triggeringRef' '$prop' vtraced:'$vtraced' indices:'$indices' "
- set refvars [::list]
-
- #avoid info exists ::p::${OID}::_ref::$prop (info exists triggers read unnecessary read trace )
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- lappend refvars ::p::${OID}::_ref::$prop ;#this is the basic unindexed reference we normally get when getting a standard property ref (e.g set ref [>obj . prop .])
- }
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*] ;#add any indexed references
- #assert triggeringRef is in the list
- if {([string length $triggeringRef]) && ($triggeringRef ni $refvars)} {
- error "@@@@@@@@@@ refsyncvar_write_manualupdate unexpected situation. triggeringRef $triggeringRef ni refvars:$refvars"
- }
- set refposn [lsearch -exact $refvars $triggeringRef]
- #assert - due to test above, we know $triggeringRef is in the list so refposn > 0
- set refvars [lreplace $refvars[set refvars {}] $refposn $refposn] ;#note inline K combinator [set refvars {}]
- if {![llength $refvars]} {
- #puts stderr " %%%%%%%%%% no refvars for refsyncvar_write_manualupdate to update - short circuiting . OID:$OID prop:$prop"
- return [list refs_updates [list]]
- }
-
- #suppress the propref_trace_* traces on all refvars
- array set traces [::list]
- array set external_traces [::list] ;#e.g application/3rd party traces on "">obj . prop ."
- #we do not support tracing of modifications to refs which occur from inside the pattern system. ie we disable them during refsync
- #todo - after finished refsyncing - consider manually firing the external_traces in such a way that writes/unsets raise an error?
- #(since an external trace should not be able to affect a change which occured from inside the object - but can affect values from application writes/unsets to the ref)
-
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
-
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- } else {
- #all other traces are 'external'
- lappend external_traces($rv) $tinfo
- #trace remove variable $rv $ops $cmd
- }
-
- }
- }
- #--------------------------------------------------------------------------------------------------------------------------
- if {([array exists SYNCVARIABLE]) || (![info exists SYNCVARIABLE])} {
- if {![info exists SYNCVARIABLE]} {
- error "WARNING: REVIEW why does $vartraced not exist here?"
- }
- #either the underlying variable is an array
- # OR - underlying variable doesn't exist - so we treat the property as an array because of the indexed access pattern
- set treat_vtraced_as_array 1
- } else {
- set treat_vtraced_as_array 0
- }
-
- set refs_updated [list]
- set refs_deleted [list] ;#unset due to index no longer being relevant
- if {$treat_vtraced_as_array} {
- foreach refvar $refvars {
- #puts stdout "\n\n \tarrayvariable:'$vtraced' examining REFVAR:'$refvar'"
- set refvar_tail [namespace tail $refvar]
- if {[string match "${prop}+*" $refvar_tail]} {
- #refvar to update is curried e.g ::p::${OID}::_ref::${prop}+x+y
- set ref_indices [lrange [split $refvar_tail +] 1 end]
- if {[llength $indices]} {
- if {[llength $indices] == 1} {
- if {[lindex $ref_indices 0] eq [lindex $indices 0]} {
- #error "untested xxx-a"
- set ${refvar} [set SYNCVARIABLE([lindex $indices 0])]
- lappend refs_updated $refvar
- } else {
- #test exists
- #error "xxx-ok single index"
- #updating a different part of the property - nothing to do
- }
- } else {
- #nested index
- if {[lindex $ref_indices 0] eq [lindex $indices 0]} {
- if {[llength $ref_indices] == 1} {
- #error "untested xxx-b1"
- set ${refvar} [lindex [set SYNCVARIABLE([lindex $indices 0])] [lrange $indices 1 end] ]
- } else {
- #assert llength $ref_indices > 1
- #NOTE - we cannot test index equivalence reliably/simply just by comparing indices
- #compare by value
-
- if {![catch {lindex [set SYNCVARIABLE([lindex $indices 0])] [lrange $indices 1 end]} possiblyNewVal]} {
- #puts stderr "\tYYYYYYYYY $refvar:'[set $refvar]'' / possiblyNewVal:'$possiblyNewVal'"
- if {[set $refvar] ne $possiblyNewVal} {
- set $refvar $possiblyNewVal
- }
- } else {
- #fail to retrieve underlying value corrsponding to these $indices
- unset $refvar
- }
- }
- } else {
- #test exists
- #error "untested xxx-ok deepindex"
- #updating a different part of the property - nothing to do
- }
- }
- } else {
- error "untested xxx-c"
-
- }
-
- } else {
- #refvar to update is plain e.g ::p::${OID}::_ref::${prop}
- if {[llength $indices]} {
- if {[llength $indices] == 1} {
- set ${refvar}([lindex $indices 0]) [set SYNCVARIABLE([lindex $indices 0])]
- } else {
- lset ${refvar}([lindex $indices 0]) {*}[lrange $indices 1 end] [lindex [set SYNCVARIABLE([lindex $indices 0])] {*}[lrange $indices 1 end]]
- }
- lappend refs_updated $refvar
- } else {
- error "untested yyy"
- set $refvar $SYNCVARIABLE
- }
- }
- }
- } else {
- #vtraced non array, but could be an array element e.g ::p::${OID}::_ref::ARR(x)
- #
- foreach refvar $refvars {
- #puts stdout "\n\n \tsimplevariable:'$vtraced' examining REFVAR:'$refvar'"
- set refvar_tail [namespace tail $refvar]
- if {[string match "${prop}+*" $refvar_tail]} {
- #refvar to update is curried e.g ::p::${OID}::_ref::${prop}+x+y
- set ref_indices [lrange [split $refvar_tail +] 1 end]
-
- if {[llength $indices]} {
- #see if this update would affect this curried ref
- #1st see if we can short-circuit our comparison based on numeric-indices
- if {[string is digit -strict [join [concat $ref_indices $indices] ""]]} {
- #both sets of indices are purely numeric (no end end-1 etc)
- set rlen [llength $ref_indices]
- set ilen [llength $indices]
- set minlen [expr {min($rlen,$ilen)}]
- set matched_firstfew_indices 1 ;#assume the best
- for {set i 0} {$i < $minlen} {incr i} {
- if {[lindex $ref_indices $i] ne [lindex $indices $i]} {
- break ;#
- }
- }
- if {!$matched_firstfew_indices} {
- #update of this refvar not required
- #puts stderr "\t@@@1 SKIPPING refvar $refvar - indices don't match $ref_indices vs $indices"
- break ;#break to next refvar in the foreach loop
- }
- }
- #failed to short-circuit
-
- #just do a simple value comparison - some optimisations are possible, but perhaps unnecessary here
- set newval [lindex $SYNCVARIABLE $ref_indices]
- if {[set $refvar] ne $newval} {
- set $refvar $newval
- lappend refs_updated $refvar
- }
-
- } else {
- #we must be updating the entire variable - so this curried ref will either need to be updated or unset
- set newval [lindex $SYNCVARIABLE $ref_indices]
- if {[set ${refvar}] ne $newval} {
- set ${refvar} $newval
- lappend refs_updated $refvar
- }
- }
- } else {
- #refvar to update is plain e.g ::p::${OID}::_ref::${prop}
- if {[llength $indices]} {
- #error "untested zzz-a"
- set newval [lindex $SYNCVARIABLE $indices]
- if {[lindex [set $refvar] $indices] ne $newval} {
- lset ${refvar} $indices $newval
- lappend refs_updated $refvar
- }
- } else {
- if {[set ${refvar}] ne $SYNCVARIABLE} {
- set ${refvar} $SYNCVARIABLE
- lappend refs_updated $refvar
- }
- }
-
- }
-
- }
- }
- #--------------------------------------------------------------------------------------------------------------------------
-
- #!todo - manually fire $external_traces as appropriate - but somehow raise error if attempt to write/unset
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names traces] {
- if {$rv ni $refs_deleted} {
- foreach tinfo $traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing trace '$ops' on variable $rv cmd:$cmd"
- trace add variable $rv $ops $cmd
- }
- }
- }
- foreach rv [array names external_traces] {
- if {$rv ni $refs_deleted} {
- foreach tinfo $external_traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #trace add variable $rv $ops $cmd
- }
- }
- }
-
-
- return [list updated_refs $refs_updated]
-}
-
-#purpose: update all relevant references when context variable changed directly
-proc ::p::predator::propvar_write_TraceHandler {OID prop vtraced vidx op} {
- #note that $vtraced may have been upvared in calling scope - so could have any name! only use it for getting/setting values - don't rely on it's name in any other way.
- #we upvar it here instead of using uplevel - as presumably upvar is more efficient (don't have to wory about whether uplevelled script is bytecompiled etc) and also makes code simpler
-
- upvar $vtraced SYNCVARIABLE
- #puts stderr "\t>>>>>>>$ propvar_write_TraceHandler OID:$OID propertyname:'$prop' vtraced:'$vtraced' index:'$vidx' operation:$op"
- #set t_info [trace info variable $vtraced]
- #foreach t_spec $t_info {
- # set t_ops [lindex $t_spec 0]
- # if {$op in $t_ops} {
- # puts stderr "\t!!!!!!!! propvar_write_Tracehandler [lindex $t_spec 1]"
- # }
- #}
-
- #puts stderr -*-*-[info vars ::p::_ref::${OID}::[lindex $prop 0]+*]-*-*-
- #vtype = array | array-item | list | simple
-
- set refvars [::list]
-
- ############################
- #!!!NOTE!!! do not call 'info exists' on a propref here as it will trigger a read trace -which then pulls in the value from the (GET)prop function etc!!!
- #This would be extra cpu work - and sets the propref prematurely (breaking proper property-trace functionality plus vwaits on proprefs)
- #The alternative 'info vars' does not trigger traces
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- #puts " **> lappending '::p::REF::${OID}::$prop'"
- lappend refvars ::p::${OID}::_ref::$prop ;#this is the basic unindexed reference we normally get when getting a standard property ref (e.g set ref [>obj . prop .])
- }
- ############################
-
- #lappend refvars ::p::${OID}::_ref::$prop ;#this is the basic unindexed reference we normally get when getting a standard property ref (e.g set ref [>obj . prop .])
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*] ;#add any indexed references
-
-
- if {![llength $refvars]} {
- #puts stderr "\t%%%%%%%%%% no refvars for propvar_write_TraceHandler to update - short circuiting . OID:$OID prop:$prop"
- return
- }
-
-
- #puts stderr "*-*-*-*-*-* refvars \n- [join $refvars "\n- "]"
-
- #We are only interested in suppressing the pattern library's 'propref_trace_*' traces and 3rd party 'read' traces on refvars
- array set predator_traces [::list]
- #maintain two lists of external traces - as we need to temporarily deactivate all non-pattern read traces even if they are part of a more comprehensive trace..
- #ie for something like 'trace add variable someref {write read array} somefunc'
- # we need to remove and immediately reinstall it as a {write array} trace - and at the end of this procedure - reinstall it as the original {write read array} trace
- array set external_read_traces [::list] ;#pure read traces the library user may have added
- array set external_readetc_traces [::list] ;#read + something else traces the library user may have added
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
- #if {$ops in {read write unset array}} {}
-
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend predator_traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- } else {
- #other traces
- # puts "##trace $tinfo"
- if {"read" in $ops} {
- if {[llength $ops] == 1} {
- #pure read -
- lappend external_read_traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- } else {
- #mixed operation trace - remove and reinstall without the 'read'
- lappend external_readetc_traces($rv) $tinfo
- set other_ops [lsearch -all -inline -not $ops "read"]
- trace remove variable $rv $ops $cmd
- #reinstall trace for non-read operations only
- trace add variable $rv $other_ops $cmd
- }
- }
- }
- }
- }
-
-
- if {([array exists SYNCVARIABLE]) || (![info exists SYNCVARIABLE])} {
- #either the underlying variable is an array
- # OR - underlying variable doesn't exist - so we treat the property as an array because of the indexed access pattern
- set vtracedIsArray 1
- } else {
- set vtracedIsArray 0
- }
-
- #puts stderr "--------------------------------------------------\n\n"
-
- #puts stderr ">>>...----refsync-trace = $vtraced $op refvars:$refvars"
- #puts stderr ">>> [trace info variable $vtraced]"
- #puts "**write*********** propvar_write_TraceHandler $prop $vtraced $vidx $op"
- #puts "**write*********** refvars: $refvars"
-
- #!todo? unroll foreach into multiple foreaches within ifs?
- #foreach refvar $refvars {}
-
-
- #puts stdout "propvar_write_TraceHandler examining REFVAR $refvar"
- if {[string length $vidx]} {
- #indexable
- if {$vtracedIsArray} {
-
- foreach refvar $refvars {
- #puts stderr " - - a refvar $refvar vidx: $vidx"
- set tail [namespace tail $refvar]
- if {[string match "${prop}+*" $tail]} {
- #refvar is curried
- #only set if vidx matches curried index
- #!todo -review
- set idx [lrange [split $tail +] 1 end]
- if {$idx eq $vidx} {
- set newval [set SYNCVARIABLE($vidx)]
- if {[set $refvar] ne $newval} {
- set ${refvar} $newval
- }
- #puts stderr "=a.1=> updated $refvar"
- }
- } else {
- #refvar is simple
- set newval [set SYNCVARIABLE($vidx)]
- if {![info exists ${refvar}($vidx)]} {
- #new key for this array
- #puts stderr "\npropvar_write_TraceHandler------ about to call 'array set $refvar [::list $vidx [set SYNCVARIABLE($vidx)] ]' "
- array set ${refvar} [::list $vidx [set SYNCVARIABLE($vidx)] ]
- } else {
- set oldval [set ${refvar}($vidx)]
- if {$oldval ne $newval} {
- #puts stderr "\npropvar_write_TraceHandler------ about to call 'array set $refvar [::list $vidx [set SYNCVARIABLE($vidx)] ]' "
- array set ${refvar} [::list $vidx [set SYNCVARIABLE($vidx)] ]
- }
- }
- #puts stderr "=a.2=> updated ${refvar} $vidx"
- }
- }
-
- } else {
-
- foreach refvar $refvars {
- upvar $refvar internal_property_reference
- #puts stderr " - - b vidx: $vidx"
-
- #!? could be object not list??
- #!!but what is the difference between an object, and a list of object names which happens to only contain one object??
- #For predictability - we probably need to autodetect type on 1st write to o_prop either list, array or object (and maintain after unset operations)
- #There would still be an edge case of an initial write of a list of objects of length 1.
- if {([llength [set $SYNCVARIABLE]] ==1) && ([string range [set $SYNCVARIABLE] 0 0] eq ">")} {
- error "untested review!"
- #the o_prop is object-shaped
- #assumes object has a defaultmethod which accepts indices
- set newval [[set $SYNCVARIABLE] {*}$vidx]
-
- } else {
- set newval [lindex $SYNCVARIABLE {*}$vidx]
- #if {[set $refvar] ne $newval} {
- # set $refvar $newval
- #}
- if {$internal_property_reference ne $newval} {
- set internal_property_reference $newval
- }
-
- }
- #puts stderr "=b=> updated $refvar"
- }
-
- }
-
- } else {
- #no vidx
-
- if {$vtracedIsArray} {
-
- foreach refvar $refvars {
- set targetref_tail [namespace tail $refvar]
- set targetref_is_indexed [string match "${prop}+*" $targetref_tail]
-
-
- #puts stderr " - - c traced: $vtraced refvar:$refvar triggeringRef: $triggeringRef"
- if {$targetref_is_indexed} {
- #curried array item ref of the form ${prop}+x or ${prop}+x+y etc
-
- #unindexed write on a property that is acting as an array..
-
- #case a) If the underlying variable is actually an array - it will error upon attempt to write it like this - that's ok.
-
- #case b) If the underlying variable doesn't exist - perhaps a PropertyWrite will accept the unindexed write (e.g by asigning a default for the missing index).
- # we can't know here how this write affects other indexed traces on this property... hence we warn but do nothing.
- puts stderr "\tc.1 WARNING: write to property without 'array set'. op:'$op' refvar:'$refvar' prop:'$prop' \n\traw: propvar_write_TraceHandler $OID $prop $vtraced $vidx $op"
- } else {
- #How do we know what to write to array ref?
- puts stderr "\tc.2 WARNING: unimplemented/unused?"
- #error no_tests_for_branch
-
- #warning - this would trigger 3rd party unset traces which is undesirable for what is really a 'bookkeeping' operation
- #if this branch is actually useful - we probably need to step through the array and unset and set elements as appropriate
- array unset ${refvar}
- array set ${refvar} [array get SYNCVARIABLE]
- }
- }
-
- } else {
- foreach refvar $refvars {
- #puts stderr "\t\t_________________[namespace current]"
- set targetref_tail [namespace tail $refvar]
- upvar $refvar INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail
- set targetref_is_indexed [string match "${prop}+*" $targetref_tail]
-
- if {$targetref_is_indexed} {
- #puts "XXXXXXXXX vtraced:$vtraced"
- #reference curried with index(es)
- #we only set indexed refs if value has changed
- # - this not required to be consistent with standard list-containing variable traces,
- # as normally list elements can't be traced seperately anyway.
- #
-
- #only bother checking a ref if no setVia index
- # i.e some operation on entire variable so need to test synchronisation for each element-ref
- set targetref_indices [lrange [split $targetref_tail +] 1 end]
- set possiblyNewVal [lindex $SYNCVARIABLE {*}$targetref_indices]
- #puts stderr "YYYYYYYYY \[set \$refvar\]: [set $refvar] / possiblyNewVal: $possiblyNewVal"
- if {[set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail] ne $possiblyNewVal} {
- set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail $possiblyNewVal
- #puts stderr "=d1=> updated $refvar -> [uplevel 1 "lindex \[set $vtraced] $idx"]"
- }
-
- } else {
- #for consistency with standard traces on a list-containing variable, we perform the set even if the list value has not changed!
-
- #puts stderr "- d2 set"
- #puts "refvar: [set $refvar]"
- #puts "SYNCVARIABLE: $SYNCVARIABLE"
-
- #if {[set $refvar] ne $SYNCVARIABLE} {
- # set $refvar $SYNCVARIABLE
- #}
- if {[set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail] ne $SYNCVARIABLE} {
- set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail $SYNCVARIABLE
- }
-
- }
- }
-
- }
-
- }
-
-
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names predator_traces] {
- foreach tinfo $predator_traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing trace '$ops' on variable $rv cmd:$cmd"
- trace add variable $rv $ops $cmd
- }
- }
-
- foreach rv [array names external_traces] {
- foreach tinfo $external_traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing trace '$ops' on variable $rv cmd:$cmd"
- trace add variable $rv $ops $cmd
- }
- }
-
-
-
-}
-
-# end propvar_write_TraceHandler
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-#
-
-#returns 0 if method implementation not present for interface
-proc ::p::predator::method_chainhead {iid method} {
- #Interface proc
- # examine the existing command-chain
- set candidates [info commands ::p::${iid}::_iface::$method.*] ;#rough grab (info commands only allows basic pattern globbing - not a regex)
- set cmdchain [list]
-
- set re [string map [list %m% [string map {( \\( ) \\) . \\.} $method]] {^%m%.([0-9]+)$}]
- set maxversion 0
- #loop and test because it is possible there are unrelated commands (having a matching prefix with . character) which were caught in the glob.
- foreach test [lsort -dictionary $candidates] {
- set c [namespace tail $test]
- if {[regexp $re $c _match version]} {
- lappend cmdchain $c
- if {$version > $maxversion} {
- set maxversion $version
- }
- }
- }
- return $maxversion
-}
-
-
-
-
-
-#this returns a script that upvars vars for all interfaces on the calling object -
-# - must be called at runtime from a method
-proc ::p::predator::upvar_all {_ID_} {
- #::set OID [lindex $_ID_ 0 0]
- ::set OID [::lindex [::dict get $_ID_ i this] 0 0]
- ::set decl {}
- #[set ::p::${OID}::_meta::map]
- #[dict get [lindex [dict get $_ID_ i this] 0 1] map]
-
- ::upvar #0 ::p::${OID}::_meta::map MAP
- #puts stdout "\n\n -->-->-->--> _meta::map '$MAP' <-<-<-\n\n"
- #set iflist [::lindex [dict get [lindex [dict get $_ID_ i this] 0 1] map] 1 0]
-
- ::foreach ifid [dict get $MAP interfaces level0] {
- if {[::dict size [::set ::p::${ifid}::_iface::o_variables]]} {
- ::array unset nsvars
- ::array set nsvars [::list]
- ::dict for {vname vinfo} [::set ::p::${ifid}::_iface::o_variables] {
- ::set varspace [::dict get $vinfo varspace]
- ::lappend nsvars($varspace) $vname
- }
- #nsvars now contains vars grouped by varspace.
-
- ::foreach varspace [::array names nsvars] {
- if {$varspace eq ""} {
- ::set ns ::p::${OID}
- } else {
- if {[::string match "::*" $varspace]} {
- ::set ns $varspace
- } else {
- ::set ns ::p::${OID}::$varspace
- }
- }
-
- ::append decl "namespace upvar $ns "
- ::foreach vname [::set nsvars($varspace)] {
- ::append decl "$vname $vname "
- }
- ::append decl " ;\n"
- }
- ::array unset nsvars
- }
- }
- ::return $decl
-}
-
-#we need to use eval because it is potentially a multiline script returned by upvar_all (so can't just use {*} operator)
-proc ::p::predator::runtime_vardecls {} {
- set result "::eval \[::p::predator::upvar_all \$_ID_\]"
- #set result "::apply { {_ID_} ::p::predator::upvar_all } \$_ID_"
-
- #set result "::apply \[::list {} \[::p::predator::upvar_all \$_ID_\] \[namespace current\]\]"
- #set result "::interp eval {} \[::p::predator::upvar_all \$_ID_\]"
- #puts stdout "\t>>>[info level -1]\n\t>>>>>>>>>>>>>>>>>>>>> '$result'"
- return $result
-}
-
-
-
-
-
-
-#OBSOLETE!(?) - todo - move stuff out of here.
-proc ::p::predator::compile_interface {IFID caller_ID_} {
- upvar 0 ::p::${IFID}:: IFACE
-
- #namespace eval ::p::${IFID} {
- # namespace ensemble create
- #}
-
- #'namespace upvar' - from tip.tcl.tk #250: Efficient Access to Namespace Variables
-
- namespace upvar ::p::${IFID}::_iface o_propertyunset_handlers o_propertyunset_handlers o_variables o_variables o_properties o_properties o_methods o_methods o_unknown o_unknown o_varspace o_varspace o_varspaces o_varspaces
-
- #set varDecls {}
- #if {[llength $o_variables]} {
- # #puts "*********!!!! $vlist"
- # append varDecls "namespace upvar ::p::\[lindex \$_ID_ 0 0 \] "
- # foreach vdef $o_variables {
- # append varDecls "[lindex $vdef 0] [lindex $vdef 0] "
- # }
- # append varDecls \n
- #}
-
- #runtime gathering of vars from other interfaces.
- #append varDecls [runtime_vardecls]
-
- set varDecls [runtime_vardecls]
-
-
-
- #implement methods
-
- #!todo - avoid globs on iface array? maintain list of methods in another slot?
- #foreach {n mname} [array get IFACE m-1,name,*] {}
-
-
- #namespace eval ::p::${IFID}::_iface "namespace export {*}$o_methods" ;#make methods available as interface ensemble.
-
-
-
- #implement property getters/setters/unsetters
- #'setter' overrides
- #pw short for propertywrite
- foreach {n property} [array get IFACE pw,name,*] {
- if {[string length $property]} {
- #set property [lindex [split $n ,] end]
-
- #!todo - next_script
- #set next [::p::next_script "\[set ::p::\${_ID_}::(self)]" $IFID $property]
-
- set maxversion [::p::predator::method_chainhead $IFID (SET)$property]
- set chainhead [expr {$maxversion + 1}]
- set THISNAME (SET)$property.$chainhead ;#first version will be (SET)$property.1
-
- set next [::p::predator::next_script $IFID (SET)$property $THISNAME $caller_ID_] ;#?! caller_ID_ ??
-
- set body $IFACE(pw,body,$property)
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t\timplicit vardecls used for propertywrite $property on interface $IFID ##### \n $body"
- }
-
- #set body [string map [::list @this@ "\[lindex \${_ID_} 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
-
- set maxversion [::p::predator::method_chainhead $IFID $property]
- set headid [expr {$maxversion + 1}]
-
- proc ::p::${IFID}::_iface::(SET)$property.$headid [concat _ID_ $IFACE(pw,arg,$property)] $body
-
- interp alias {} ::p::${IFID}::_iface::(SET)$property {} ::p::${IFID}::_iface::(SET)$property.$headid
-
- #proc ::p::${IFID}::___system___write_$property [concat _ID_ $IFACE(pw,arg,$property)] $body
- }
- }
- #'unset' overrides
-
- dict for {property handler_info} $o_propertyunset_handlers {
-
- set body [dict get $handler_info body]
- set arraykeypattern [dict get $handler_info arraykeypattern] ;#array element pattern for unsetting individual elements in an array
-
- set maxversion [::p::predator::method_chainhead $IFID (UNSET)$property]
- set headid [expr {$maxversion + 1}]
-
- set THISNAME (UNSET)$property.$headid
-
- set next [::p::predator::next_script $IFID (UNSET)$property $THISNAME $caller_ID_] ;#?! caller_ID_ ???
-
-
-
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t\timplicit vardecls used for property unset $property on interface $IFID ##### \n $body"
-
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
-
-
- #implement
- #always take arraykeypattern argument even though usually empty string (only used for unsetting individual array elements)
- if {[string trim $arraykeypattern] eq ""} {
- set arraykeypattern "_dontcare_"
- }
- proc ::p::${IFID}::_iface::(UNSET)$property.$headid [concat _ID_ $arraykeypattern] $body
-
-
- #chainhead pointer
- interp alias {} ::p::${IFID}::_iface::(UNSET)$property {} ::p::${IFID}::_iface::(UNSET)$property.$headid
- }
-
-
-
- interp alias {} ::p::${IFID}::(VIOLATE) {} ::p::internals::(VIOLATE)
-
- #the usual case will have no destructor - so use info exists to check.
-
- if {[info exists ::p::${IFID}::_iface::o_destructor_body]} {
- #!todo - chained destructors (support @next@).
- #set next [::p::next_script_destructor "\[lindex \$_ID_ 0 1\]" $IFID]
- set next NEXT
-
- set body [set ::p::${IFID}::_iface::o_destructor_body]
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t\t**********************implicit vardecls used for destructor on interface $IFID ##### \n $body"
- }
- #set body [::p::fixed_var_statements \n@IMPLICITDECLS@\n$body]
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- proc ::p::${IFID}::___system___destructor _ID_ $body
- }
-
-
- if {[info exists o_unknown]} {
- #use 'apply' somehow?
- interp alias {} ::p::${IFID}::_iface::(UNKNOWN) {} ::p::${IFID}::_iface::$o_unknown
-
- #namespace eval ::p::${IFID}::_iface [list namespace unknown $o_unknown]
- }
-
-
- return
-}
-
-
-
-
-
-
-
-#'info args' - assuming arbitrary chain of 'interp aliases'
-proc ::p::predator::command_info_args {cmd} {
- if {[llength [set next [interp alias {} $cmd]]]} {
- set curriedargs [lrange $next 1 end]
-
- if {[catch {set arglist [info args [lindex $next 0]]}]} {
- set arglist [command_info_args [lindex $next 0]]
- }
- #trim curriedargs
- return [lrange $arglist [llength $curriedargs] end]
- } else {
- info args $cmd
- }
-}
-
-
-proc ::p::predator::do_next {_ID_ IFID mname nextArgs args} {
- if {[llength $args]} {
- tailcall ::p::${IFID}::_iface::$mname $_ID_ {*}$args
- } else {
- if {[llength $nextArgs] > 1} {
- set argVals [::list]
- set i 0
- foreach arg [lrange $nextArgs 1 end] {
- upvar 1 $arg $i
- if {$arg eq "args"} {
- #need to check if 'args' is actually available in caller
- if {[info exists $i]} {
- set argVals [concat $argVals [set $i]]
- }
- } else {
- lappend argVals [set $i]
- }
- }
- tailcall ::p::${IFID}::_iface::$mname $_ID_ {*}$argVals
- } else {
- tailcall ::p::${IFID}::_iface::$mname $_ID_
- }
- }
-}
-
-#----------------------------------------------------------------------------------------------
-proc ::p::predator::next_script {IFID method caller caller_ID_} {
-
- if {$caller eq "(CONSTRUCTOR).1"} {
- return [string map [list %cID% [list $caller_ID_] %ifid% $IFID %m% $method] {::p::predator::do_next_pattern_if $_ID_ %cID% %ifid% %m%}]
- } elseif {$caller eq "$method.1"} {
- #delegate to next interface lower down the stack which has a member named $method
- return [string map [list %ifid% $IFID %m% $method] {::p::predator::do_next_if $_ID_ %ifid% %m%}]
- } elseif {[string match "(GET)*.2" $caller]} {
- # .1 is the getprop procedure, .2 is the bottom-most PropertyRead.
-
- #jmn
- set prop [string trimright $caller 1234567890]
- set prop [string range $prop 5 end-1] ;#string leading (GET) and trailing .
-
- if {$prop in [dict keys [set ::p::${IFID}::_iface::o_properties]]} {
- #return [string map [list %ifid% $IFID %p% $prop ] {::p::%ifid%::_iface::(GET)%p%.1 $_ID_}]
- return [string map [list %ifid% $IFID %m% (GET)$prop.1 %nargs% [list]] {::p::predator::do_next $_ID_ %ifid% %m% [list %nargs%]}]
- } else {
- #we can actually have a property read without a property or a method of that name - but it could also match the name of a method.
- # (in which case it could return a different value depending on whether called via set [>obj . something .] vs >obj . something)
- return [string map [list %ifid% $IFID %m% $method] {::p::predator::do_next_if $_ID_ %ifid% %m%}]
- }
- } elseif {[string match "(SET)*.2" $caller]} {
- return [string map [list %ifid% $IFID %m% $method] {::p::predator::do_next_if $_ID_ %ifid% %m%}]
- } else {
- #this branch will also handle (SET)*.x and (GET)*.x where x >2
-
- #puts stdout "............next_script IFID:$IFID method:$method caller:$caller"
- set callerid [string range $caller [string length "$method."] end]
- set nextid [expr {$callerid - 1}]
-
- if {[catch {set nextArgs [info args ::p::${IFID}::_iface::$method.$nextid]} errMsg]} {
- #not a proc directly on this interface - presumably an alias made by something like linkcopy_interface.
- #puts ">>>>>>>>::p::predator::next_script IFID:$IFID caller:$caller aaaa@ $method.$nextid"
- set nextArgs [command_info_args ::p::${IFID}::_iface::$method.$nextid]
- }
-
- return [string map [list %ifid% $IFID %m% $method.$nextid %nargs% $nextArgs] {::p::predator::do_next $_ID_ %ifid% %m% [list %nargs%]}]
- }
-}
-
-proc ::p::predator::do_next_if {_ID_ IFID method args} {
- #puts "<>(::p::predator::do_next_if)<> '$_ID_' '$IFID' '$method' '$args' ((("
-
- #set invocants [dict get $_ID_ i]
- #set this_invocantdata [lindex [dict get $invocants this] 0]
- #lassign $this_invocantdata OID this_info
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set interfaces [dict get $MAP interfaces level0]
- set patterninterfaces [dict get $MAP interfaces level1]
-
- set L0_posn [lsearch $interfaces $IFID]
- if {$L0_posn == -1} {
- error "(::p::predator::do_next_if) called with interface not present at level0 for this object"
- } elseif {$L0_posn > 0} {
- #set ifid_next [lindex $interfaces $L0_posn-1] ;#1 lower in the iStack
- set lower_interfaces [lrange $interfaces 0 $L0_posn-1]
-
- foreach if_sub [lreverse $lower_interfaces] {
- if {[string match "(GET)*" $method]} {
- #do not test o_properties here! We need to call even if there is no underlying property on this interface
- #(PropertyRead without Property is legal. It results in dispatch to subsequent interface rather than property variable for this interface)
- # relevant test: higher_order_propertyread_chaining
- return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
- } elseif {[string match "(SET)*" $method]} {
- #must be called even if there is no matching $method in o_properties
- return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
- } elseif {[string match "(UNSET)*" $method]} {
- #review untested
- #error "do_next_if (UNSET) untested"
- #puts stderr "<>(::p::predator::do_next_if)<> (UNSET) called - dispatching to ::p::${if_sub}::_iface::$method with args:'$args'"
- return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
-
- } elseif {$method in [dict keys [set ::p::${if_sub}::_iface::o_methods]]} {
- if {[llength $args]} {
- #puts stdout "<>(::p::predator::do_next_if)<> - - - calling ::p::${if_sub}::_iface::$method on sub interface $if_sub with $args"
-
- #return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
- #tailcall ::p::${if_sub}::_iface::$method $_ID_ {*}$args
-
- #!todo - handle case where llength $args is less than number of args for subinterface command
- #i.e remaining args will need to be upvared to get values from calling scope (auto-set any values not explicitly set)
-
- #handle case where next interface has different arguments (masking of sub interfaces in the stack with function with different arity/signature)
- set head [interp alias {} ::p::${if_sub}::_iface::$method]
- set nextArgs [info args $head] ;#!todo - fix... head not necessarily a proc
- set argx [list]
- foreach a $nextArgs {
- lappend argx "\$a"
- }
-
- #todo - handle func a b args called with func "x" ie short on named vars so b needs to be upvared
-
- if {([llength $args] == [llength $nextArgs]) || ([lindex $nextArgs end] eq "args")} {
- tailcall apply [list $nextArgs [list ::p::${if_sub}::_iface::$method {*}$argx ]] $_ID_ {*}$args
- } else {
- #todo - upvars required for tail end of arglist
- tailcall apply [list $nextArgs [list ::p::${if_sub}::_iface::$method {*}$argx ]] $_ID_ {*}$args
- }
-
- } else {
- #auto-set: upvar vars from calling scope
- #!todo - robustify? alias not necessarily matching command name..
- set head [interp alias {} ::p::${if_sub}::_iface::$method]
-
-
- set nextArgs [info args $head] ;#!todo - fix... head not necessarily a proc
- if {[llength $nextArgs] > 1} {
- set argVals [::list]
- set i 0
- foreach arg [lrange $nextArgs 1 end] {
- upvar 1 $arg $i
- if {$arg eq "args"} {
- #need to check if 'args' is actually available in caller
- if {[info exists $i]} {
- set argVals [concat $argVals [set $i]]
- }
- } else {
- lappend argVals [set $i]
- }
- }
- #return [$head $_ID_ {*}$argVals]
- tailcall $head $_ID_ {*}$argVals
- } else {
- #return [$head $_ID_]
- tailcall $head $_ID_
- }
- }
- } elseif {$method eq "(CONSTRUCTOR)"} {
- #chained constructors will only get args if the @next@ caller explicitly provided them.
- puts stdout "!!!<>(::p::predator::do_next_if)<> CONSTRUCTOR CHAINED CALL via do_next_if _ID_:$_ID_ IFID:$IFID method:$method args:$args!!!"
- #return [::p::${if_sub}::_iface::(CONSTRUCTOR) $_ID_ {*}$args]
- xtailcall ::p::${if_sub}::_iface::(CONSTRUCTOR) $_ID_ {*}$args
- }
- }
- #no interfaces in the iStack contained a matching method.
- return
- } else {
- #no further interfaces in this iStack
- return
- }
-}
-
-
-#only really makes sense for (CONSTRUCTOR) calls.
-#_ID_ is the invocant data for the target. caller_ID_ is the invocant data for the calling(creating,cloning etc) pattern/class.
-proc ::p::predator::do_next_pattern_if {_ID_ caller_ID_ IFID method args} {
- #puts ")))) do_next_pattern_if _ID_:'$_ID_' IFID:'$IFID' method:'$method' args:'$args' ((("
-
- #set invocants [dict get $_ID_ i]
- #set this_invocant [lindex [dict get $invocants this] 0]
- #lassign $this_invocant OID this_info
- #set OID [lindex [dict get $invocants this] 0 0]
- #upvar #0 ::p::${OID}::_meta::map map
- #lassign [lindex $map 0] OID alias itemCmd cmd
-
-
- set caller_OID [lindex [dict get $caller_ID_ i this] 0 0]
- upvar #0 ::p::${caller_OID}::_meta::map callermap
-
- #set interfaces [lindex $map 1 0]
- set patterninterfaces [dict get $callermap interfaces level1]
-
- set L0_posn [lsearch $patterninterfaces $IFID]
- if {$L0_posn == -1} {
- error "do_next_pattern_if called with interface not present at level1 for this object"
- } elseif {$L0_posn > 0} {
-
- set lower_interfaces [lrange $patterninterfaces 0 $L0_posn-1]
-
- foreach if_sub [lreverse $lower_interfaces] {
- if {$method eq "(CONSTRUCTOR)"} {
- #chained constructors will only get args if the @next@ caller explicitly provided them.
- #puts stdout "!!! CONSTRUCTOR CHAINED CALL via do_next_pattern_if _ID_:$_ID_ IFID:$IFID method:$method args:$args!!!"
- tailcall ::p::${if_sub}::_iface::(CONSTRUCTOR) $_ID_ {*}$args
- }
- }
- #no interfaces in the iStack contained a matching method.
- return
- } else {
- #no further interfaces in this iStack
- return
- }
-}
-
-
-
-
-
-#------------------------------------------------------------------------------------------------
-
-
-
-
-
-#-------------------------------------------------------------------------------------
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-
-
-#!todo - can we just call new_object somehow to create this?
-
- #until we have a version of Tcl that doesn't have 'creative writing' scope issues -
- # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword.
- # (see http://mini.net/tcl/1030 'Dangers of creative writing')
-namespace eval ::p::-1 {
- #namespace ensemble create
-
- namespace eval _ref {}
- namespace eval _meta {}
-
- namespace eval _iface {
- variable o_usedby
- variable o_open
- variable o_constructor
- variable o_variables
- variable o_properties
- variable o_methods
- variable o_definition
- variable o_varspace
- variable o_varspaces
-
- array set o_usedby [list i0 1] ;#!todo - review
- #'usedby' array the metaface is an exception. All objects use it - so we should list none of them rather than pointless updating of this value?
-
- set o_open 1
- set o_constructor [list]
- set o_variables [list]
- set o_properties [dict create]
- set o_methods [dict create]
- array set o_definition [list]
- set o_varspace ""
- set o_varspaces [list]
- }
-}
-
-
-#
-
-#interp alias {} ::p::internals::>metaface {} ::p::internals::predator [list [list -1 ::p::internals::>metaface item {}] {{} {}}]
-interp alias {} ::p::internals::>metaface {} ::p::internals::predator [list i [list this [list [list -1 ::p::internals::>metaface item {}]]] context {}]
-
-
-upvar #0 ::p::-1::_iface::o_definition def
-
-
-#! concatenate -> compose ??
-dict set ::p::-1::_iface::o_methods Concatenate {arglist {target args}}
-proc ::p::-1::Concatenate {_ID_ target args} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- if {![string match "::*" $target]} {
- if {[set ns [uplevel 1 {namespace current}]] eq "::"} {
- set target ::$target
- } else {
- set target ${ns}::$target
- }
- }
- #add > character if not already present
- set target [namespace qualifiers $target]::>[string trimleft [namespace tail $target] >]
- set _target [string map {::> ::} $target]
-
- set ns [namespace qualifiers $target]
- if {$ns eq ""} {
- set ns "::"
- } else {
- namespace eval $ns {}
- }
-
- if {![llength [info commands $target]]} {
- #degenerate case - target does not exist
- #Probably just 1st of a set of Concatenate calls - so simply delegate to 'Clone'
- #review - should be 'Copy' so it has object state from namespaces and variables?
- return [::p::-1::Clone $_ID_ $target {*}$args]
-
- #set TARGETMAP [::p::predator::new_object $target]
- #lassign [lindex $TARGETMAP 0] target_ID target_cmd itemCmd
-
- } else {
- #set TARGETMAP [lindex [interp alias {} [namespace origin $target]] 1]
- set TARGETMAP [$target --]
-
- lassign [dict get $TARGETMAP invocantdata] target_ID target_cmd itemCmd
-
- #Merge lastmodified(?) level0 and level1 interfaces.
-
- }
-
- return $target
-}
-
-
-
-#Object's Base-Interface proc with itself as curried invocant.
-#interp alias {} ::p::-1::Create {} ::p::-1::_iface::Create $invocant
-#namespace eval ::p::-1 {namespace export Create}
-dict set ::p::-1::_iface::o_methods Define {arglist definitions}
-#define objects in one step
-proc ::p::-1::Define {_ID_ definitions} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias default_method cmd
- set interfaces [dict get $MAP interfaces level0] ;#level-0 interfaces
- set patterns [dict get $MAP interfaces level1] ;#level-1 interfaces
-
- #!todo - change these to dicts; key=interface stack name value= a list of interfaces in the stack
- #set IFID0 [lindex $interfaces 0]
- #set IFID1 [lindex $patterns 0] ;#1st pattern
-
- #set IFID_TOP [lindex $interfaces end]
- set IFID_TOP [::p::predator::get_possibly_new_open_interface $OID]
-
- #set ns ::p::${OID}
-
- #set script [string map [list %definitions% $definitions] {
- # if {[lindex [namespace path] 0] ne "::p::-1"} {
- # namespace path [list ::p::-1 {*}[namespace path]]
- # }
- # %definitions%
- # namespace path [lrange [namespace path] 1 end]
- #
- #}]
-
- set script [string map [list %id% $_ID_ %definitions% $definitions] {
- set ::p::-1::temp_unknown [namespace unknown]
-
- namespace unknown [list ::apply {{funcname args} {::p::predator::redirect $funcname [list %id%] {*}$args}}]
-
- #namespace unknown [list ::apply { {funcname args} {if {![llength [info commands ::p::-1::$funcname]]} {::unknown $funcname {*}$args } else {::p::-1::$funcname [list %id%] {*}$args} }} ]
-
- %definitions%
-
-
- namespace unknown ${::p::-1::temp_unknown}
- return
- }]
-
-
-
- #uplevel 1 $script ;#this would run the script in the global namespace
- #run script in the namespace of the open interface, this allows creating of private helper procs
- #namespace inscope ::p::${IFID_TOP}::_iface $script ;#do not use tailcall here! Define belongs on the callstack
- #namespace inscope ::p::${OID} $script
- namespace eval ::p::${OID} $script
- #return $cmd
-}
-
-
-proc ::p::predator::redirect {func args} {
- #todo - review tailcall - tests?
- if {![llength [info commands ::p::-1::$func]]} {
- #error "invalid command name \"$func\""
- tailcall uplevel 1 [list ::unknown $func {*}$args]
- } else {
- tailcall uplevel 1 [list ::p::-1::$func {*}$args]
- }
-}
-
-
-#'immediate' constructor - this is really like a (VIOLATE) call.. todo - review.
-dict set ::p::-1::_iface::o_methods Construct {arglist {argpairs body args}}
-proc ::p::-1::Construct {_ID_ argpairs body args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set interfaces [dict get $MAP interfaces level0]
- set iid_top [lindex $interfaces end]
- namespace upvar ::p::${iid_top}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- set ARGSETTER {}
- foreach {argname argval} $argpairs {
- append ARGSETTER "set $argname $argval\n"
- }
- #$_self (VIOLATE) $ARGSETTER$body
-
- set body $ARGSETTER\n$body
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls\n[dict get $processed body]
- # puts stderr "\t runtime_vardecls in Construct $varDecls"
- }
-
- set next "\[error {next not implemented}\]"
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]"] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- #namespace eval ::p::${iid_top} $body
-
- #return [apply [list {_ID_ args} $body ::p::${iid_top}::_iface] $_ID_]
- #does this handle Varspace before constructor?
- return [apply [list {_ID_ args} $body ::p::${OID} ] $_ID_ {*}$args]
-}
-
-
-
-
-
-#hacked optimized version of ::p::-1::Create for creating ::p::ifaces::>* objects
-namespace eval ::p::3 {}
-proc ::p::3::_create {child {OID "-2"}} {
- #puts stderr "::p::3::_create $child $OID"
- set _child [string map {::> ::} $child]
- if {$OID eq "-2"} {
- #set childmapdata [::p::internals::new_object $child]
- #set child_ID [lindex [dict get $childmapdata invocantdata] 0 ]
- set child_ID [lindex [dict get [::p::internals::new_object $child] invocantdata] 0]
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
- } else {
- set child_ID $OID
- #set _childmap [::p::internals::new_object $child "" $child_ID]
- ::p::internals::new_object $child "" $child_ID
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
- }
-
- #--------------
-
- set oldinterfaces [dict get $CHILDMAP interfaces]
- dict set oldinterfaces level0 [list 2]
- set modifiedinterfaces $oldinterfaces
- dict set CHILDMAP interfaces $modifiedinterfaces
-
- #--------------
-
-
-
-
- #puts stderr ">>>> creating alias for ::p::$child_ID"
- #puts stderr ">>>::p::3::_create $child $OID >>>[interp alias {} ::p::$child_ID]"
-
- #interp alias ::p::$child_ID already exists at this point - so calling here will do nothing!
- #interp alias {} ::p::$child_ID {} ::p::internals::predator [dict create i [dict create this [list [list $child_ID {} ]]]]
- #puts stderr ">>>[interp alias {} ::p::$child_ID]"
-
-
-
- #---------------
- namespace upvar ::p::2::_iface o_methods o_methods o_properties o_properties
- foreach method [dict keys $o_methods] {
- #todo - change from interp alias to context proc
- interp alias {} ::p::${child_ID}::$method {} ::p::2::_iface::$method
- }
- #namespace eval ::p::${child_ID} [list namespace export {*}$o_methods]
- #implement property even if interface already compiled because we need to create defaults for each new child obj.
- # also need to add alias on base interface
- #make sure we are only implementing properties from the current CREATOR
- dict for {prop pdef} $o_properties {
- #lassign $pdef prop default
- interp alias {} ::p::${child_ID}::$prop {} ::p::2::_iface::(GET)$prop
- interp alias {} ::p::${child_ID}::(GET)$prop {} ::p::2::_iface::(GET)$prop
-
- }
- ::p::2::_iface::(CONSTRUCTOR) [dict create i [dict create this [list [dict get $CHILDMAP invocantdata]]] context {}]
- #---------------
- #namespace eval ::p::${child_ID} "namespace ensemble create -command $_child"
- return $child
-}
-
-#configure -prop1 val1 -prop2 val2 ...
-dict set ::p::-1::_iface::o_methods Configure {arglist args}
-proc ::p::-1::Configure {_ID_ args} {
-
- #!todo - add tests.
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd this
-
- if {![expr {([llength $args] % 2) == 0}]} {
- error "expected even number of Configure args e.g '-property1 value1 -property2 value2'"
- }
-
- #Do a separate loop to check all the arguments before we run the property setting loop
- set properties_to_configure [list]
- foreach {argprop val} $args {
- if {!([string range $argprop 0 0] eq "-") || ([string length $argprop] < 2)} {
- error "expected Configure args in the form: '-property1 value1 -property2 value2'"
- }
- lappend properties_to_configure [string range $argprop 1 end]
- }
-
- #gather all valid property names for all level0 interfaces in the relevant interface stack
- set valid_property_names [list]
- set iflist [dict get $MAP interfaces level0]
- foreach id [lreverse $iflist] {
- set interface_property_names [dict keys [set ::p::${id}::_iface::o_properties]]
- foreach if_prop $interface_property_names {
- if {$if_prop ni $valid_property_names} {
- lappend valid_property_names $if_prop
- }
- }
- }
-
- foreach argprop $properties_to_configure {
- if {$argprop ni $valid_property_names} {
- error "Configure failed - no changes made. Unable to find property '$argprop' on object $this OID:'$OID' valid properties: $valid_property_names"
- }
- }
-
- set top_IID [lindex $iflist end]
- #args ok - go ahead and set all properties
- foreach {prop val} $args {
- set property [string range $prop 1 end]
- #------------
- #don't use property ref unnecessarily - leaves property refs hanging around which traces need to update
- #ie don't do this here: set [$this . $property .] $val
- #-------------
- ::p::${top_IID}::_iface::(SET)$property $_ID_ $val ;#equivalent to [$this . (SET)$property $val]
- }
- return
-}
-
-
-
-
-
-
-dict set ::p::-1::_iface::o_methods AddPatternInterface {arglist iid}
-proc ::p::-1::AddPatternInterface {_ID_ iid} {
- #puts stderr "!!!!!!!!!!!!!!! ::p::-1::AddPatternInterface $_ID_ $iid"
- if {![string is integer -strict $iid]} {
- error "adding interface by name not yet supported. Please use integer id"
- }
-
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- #lassign [lindex $invocant 0] OID alias itemCmd cmd
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set existing_ifaces [dict get $MAP interfaces level1] ;#pattern interfaces
-
-
-
- #it is theoretically possible to have the same interface present multiple times in an iStack.
- # #!todo -review why/whether this is useful. should we disallow it and treat as an error?
-
- lappend existing_ifaces $iid
- #lset map {1 1} $existing_ifaces
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 $existing_ifaces
- dict set MAP interfaces $extracted_sub_dict
-
- #lset invocant {1 1} $existing_ifaces
-
-}
-
-
-#!todo - update usedby ??
-dict set ::p::-1::_iface::o_methods AddInterface {arglist iid}
-proc ::p::-1::AddInterface {_ID_ iid} {
- #puts stderr "::p::-1::AddInterface _ID_:$_ID_ iid:$iid"
- if {![string is integer -strict $iid]} {
- error "adding interface by name not yet supported. Please use integer id"
- }
-
-
- lassign [dict get $_ID_ i this] list_of_invocants_for_role_this ;#Although there is normally only 1 'this' element - it is a 'role' and the structure is nonetheless a list.
- set this_invocant [lindex $list_of_invocants_for_role_this 0]
-
- lassign $this_invocant OID _etc
-
- upvar #0 ::p::${OID}::_meta::map MAP
- set existing_ifaces [dict get $MAP interfaces level0]
-
- lappend existing_ifaces $iid
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 $existing_ifaces
- dict set MAP interfaces $extracted_sub_dict
- return [dict get $extracted_sub_dict level0]
-}
-
-
-
-# The 'Create' method on the meta-interface has 2 variants (CreateNew & CreateOverlay) provided to enhance code clarity for the application using the pattern module.
-# The 'Create' method could be used in all instances - but 'CreateNew' is designed for the case where the target/child object does not yet exist
-# and 'CreateOverlay' for the case where the target/child object already exists.
-# If the application writer follows the convention of using 'CreateNew' & 'CreateOverlay' instead of 'Create' - it should be more obvious where a particular object first comes into existence,
-# and it should reduce errors where the author was expecting to overlay an existing object, but accidentally created a new object.
-# 'CreateNew' will raise an error if the target already exists
-# 'CreateOverlay' will raise an error if the target object does not exist.
-# 'Create' will work in either case. Creating the target if necessary.
-
-
-#simple form:
-# >somepattern .. Create >child
-#simple form with arguments to the constructor:
-# >somepattern .. Create >child arg1 arg2 etc
-#complex form - specify more info about the target (dict keyed on childobject name):
-# >somepattern .. Create {>child {-id 1}}
-#or
-# >somepattern .. Create [list >child {-id 1 -somethingelse etc} >child2 {}]
-#complex form - with arguments to the contructor:
-# >somepattern .. Create [list >child {-id 1}] arg1 arg2 etc
-dict set ::p::-1::_iface::o_methods Create {arglist {target_spec args}}
-proc ::p::-1::Create {_ID_ target_spec args} {
- #$args are passed to constructor
- if {[llength $target_spec] ==1} {
- set child $target_spec
- set targets [list $child {}]
- } else {
- set targets $target_spec
- }
-
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- set invocants [dict get $_ID_ i]
- set invocant_roles [dict keys $invocants] ;#usually the only invocant role present will be 'this' (single dispatch case)
-
- foreach {child target_spec_dict} $targets {
- #puts ">>>::p::-1::Create $_ID_ $child $args <<<"
-
-
-
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
-
-
- #puts ">>Create _ID_:$_ID_ child:$child args:$args map:$map OID:$OID"
-
- #child should already be fully ns qualified (?)
- #ensure it is has a pattern-object marker >
- #puts stderr ".... $child (nsqual: [namespace qualifiers $child])"
-
-
- lassign [dict get $MAP invocantdata] OID alias parent_defaultmethod cmd
- set interfaces [dict get $MAP interfaces level0] ;#level-0 interfaces
- set patterns [dict get $MAP interfaces level1] ;#level-1 interfaces
- #puts "parent: $OID -> child:$child Patterns $patterns"
-
- #todo - change to dict of interface stacks
- set IFID0 [lindex $interfaces 0]
- set IFID1 [lindex $patterns 0] ;#1st pattern
-
- #upvar ::p::${OID}:: INFO
-
- if {![string match {::*} $child]} {
- if {[set ns [uplevel 1 {namespace current}]] eq "::"} {
- set child ::$child
- } else {
- set child ${ns}::$child
- }
- }
-
-
- #add > character if not already present
- set child [namespace qualifiers $child]::>[string trimleft [namespace tail $child] >]
- set _child [string map {::> ::} $child]
-
- set ns [namespace qualifiers $child]
- if {$ns eq ""} {
- set ns "::"
- } else {
- namespace eval $ns {}
- }
-
-
- #maintain a record of interfaces created so that we can clean-up if we get an error during any of the Constructor calls.
- set new_interfaces [list]
-
- if {![llength $patterns]} {
- ##puts stderr "===> WARNING: no level-1 interfaces (patterns) on object $cmd when creating $child"
- #lappend patterns [::p::internals::new_interface $OID]
-
- #lset invocant {1 1} $patterns
- ##update our command because we changed the interface list.
- #set IFID1 [lindex $patterns 0]
-
- #set patterns [list [::p::internals::new_interface $OID]]
-
- #set patterns [list [::p::internals::new_interface]]
-
- #set patterns [list [set iid [expr {$::p::ID + 1}]]] ;#PREDICT the next object's id
- #set patterns [list [set iid [incr ::p::ID]]]
- set patterns [list [set iid [::p::get_new_object_id]]]
-
- #---------
- #set iface [::p::>interface .. Create ::p::ifaces::>$iid]
- #::p::-1::Create [list {caller ::p::3}] ::p::ifaces::>$iid
-
- #lappend new_interfaces [::p::3::_create ::p::ifaces::>$iid] ;#interface creation
- lappend new_interfaces [::p::3::_create ::p::ifaces::>$iid $iid]
-
- #---------
-
- #puts "??> p::>interface .. Create ::p::ifaces::>$iid"
- #puts "??> [::p::ifaces::>$iid --]"
- #set [$iface . UsedBy .]
- }
- set parent_patterndefaultmethod [dict get $MAP patterndata patterndefaultmethod]
-
- #if {![llength [info commands $child]]} {}
-
- if {[namespace which $child] eq ""} {
- #normal case - target/child does not exist
- set is_new_object 1
-
- if {[dict exists $target_spec_dict -id]} {
- set childmapdata [::p::internals::new_object $child "" [dict get $target_spec_dict -id]]
- } else {
- set childmapdata [::p::internals::new_object $child]
- }
- lassign [dict get $childmapdata invocantdata] child_ID child_alias child_defaultmethod
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
-
- #child initially uses parent's level1 interface as it's level0 interface
- # child has no level1 interface until PatternMethods or PatternProperties are added
- # (or applied via clone; or via create with a parent with level2 interface)
- #set child_IFID $IFID1
-
- #lset CHILDMAP {1 0} [list $IFID1]
- #lset CHILDMAP {1 0} $patterns
-
- set extracted_sub_dict [dict get $CHILDMAP interfaces]
- dict set extracted_sub_dict level0 $patterns
- dict set CHILDMAP interfaces $extracted_sub_dict
-
- #why write back when upvared???
- #review
- set ::p::${child_ID}::_meta::map $CHILDMAP
-
- #::p::predator::remap $CHILDMAP
-
- #interp alias {} $child {} ::p::internals::predator $CHILDMAP
-
- #set child_IFID $IFID1
-
- #upvar ::p::${child_ID}:: child_INFO
-
- #!todo review
- #set n ::p::${child_ID}
- #if {![info exists ${n}::-->PATTERN_ANCHOR]} {
- # #puts stdout "### target:'$child' Creating ${n}::-->PATTERN_ANCHOR (unset trace to delete namespace '$n'"
- # #!todo - keep an eye on tip.tcl.tk #140 - 'Tracing Namespace Modification' - may be able to do away with this hack
- # set ${n}::-->PATTERN_ANCHOR "objects within this namespace will be deleted when this var is unset"
- # trace add variable ${n}::-->PATTERN_ANCHOR {unset} [list ::p::meta::clear_ns $n]
- #}
-
- set ifaces_added $patterns
-
- } else {
- #overlay/mixin case - target/child already exists
- set is_new_object 0
-
- #set CHILDMAP [lindex [interp alias {} [namespace origin $child]] 1]
- set childmapdata [$child --]
-
-
- #puts stderr " *** $cmd .. Create -> target $child already exists!!!"
- #puts " **** CHILDMAP: $CHILDMAP"
- #puts " ****"
-
- #puts stderr " ---> Properties: [$child .. Properties . names]"
- #puts stderr " ---> Methods: [$child .. Properties . names]"
-
- lassign [dict get $childmapdata invocantdata] child_ID child_alias child_default child_cmd
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
-
- #set child_IFID [lindex $CHILDMAP 1 0 end]
- #if {$child_IFID != [set child_IFID [::p::internals::expand_interface $child_IFID]]} {
- # lset CHILDMAP {1 0} [concat [lindex $CHILDMAP 1 0] $child_IFID]
- # interp alias {} $child_cmd {} ::p::internals::predator $CHILDMAP
- #}
- ##!todo? - merge only 'open' parent interfaces onto 'open' target interfaces
- #::p::merge_interface $IFID1 $child_IFID
-
-
- set existing_interfaces [dict get $CHILDMAP interfaces level0]
- set ifaces_added [list]
- foreach p $patterns {
- if {$p ni $existing_interfaces} {
- lappend ifaces_added $p
- }
- }
-
- if {[llength $ifaces_added]} {
- #lset CHILDMAP {1 0} [concat [lindex $CHILDMAP 1 0] $ifaces_added]
- set extracted_sub_dict [dict get $CHILDMAP interfaces]
- dict set extracted_sub_dict level0 [concat $existing_interfaces $ifaces_added]
- dict set CHILDMAP interfaces $extracted_sub_dict
- #set ::p::${child_ID}::_meta::map $CHILDMAP ;#why?
- #::p::predator::remap $CHILDMAP
- }
- }
-
- #do not overwrite the child's defaultmethod value if the parent_patterndefaultmethod is empty
- if {$parent_patterndefaultmethod ne ""} {
- set child_defaultmethod $parent_patterndefaultmethod
- set CHILD_INVOCANTDATA [dict get $CHILDMAP invocantdata]
- lset CHILD_INVOCANTDATA 2 $child_defaultmethod
- dict set CHILDMAP invocantdata $CHILD_INVOCANTDATA
- #update the child's _ID_
- interp alias {} $child_alias {} ;#first we must delete it
- interp alias {} $child_alias {} ::p::internals::predator [list i [list this [list $CHILD_INVOCANTDATA] ] context {}]
-
- #! object_command was initially created as the renamed alias - so we have to do it again
- rename $child_alias $child
- trace add command $child rename [list $child .. Rename]
- }
- #!todo - review - dont we already have interp alias entries for every method/prop?
- #namespace eval ::p::${child_ID} "namespace ensemble create -command $_child"
-
-
-
-
-
- set constructor_failure 0 ;#flag to indicate abortion due to error during a constructor call.
-
-
-
- #------------------------------------------------------------------------------------
- #create snapshot of the object-namespaces variables to allow object state to be rolledback if any Constructor calls fail.
- # - All variables under the namespace - not just those declared as Variables or Properties
- # - use a namespace. For the usual case of success, we just namespace delete, and remove the COW traces.
- # - presumably this snapshot should be reasonably efficient even if variables hold large amounts of data, as Tcl implements Copy-On-Write.
-
- #NOTE - do not use the objectID as the sole identifier for the snapshot namespace.
- # - there may be multiple active snapshots for a single object if it overlays itself during a constructor,
- # and it may be that a failure of an inner overlay is deliberately caught and not considered reason to raise an error for the initial constructor call.
- # - we will use an ever-increasing snapshotid to form part of ns_snap
- set ns_snap "::p::snap::[incr ::p::snap::id]_$child_ID" ;#unique snapshot namespace for this call to Create.
-
- #!todo - this should look at child namespaces (recursively?)
- #!todo - this should examine any namespaces implied by the default 'varspace' value for all interfaces.
- # (some of these namespaces might not be descendants of the object's ::p::${child_ID} namespace)
-
- namespace eval $ns_snap {}
- foreach vname [info vars ::p::${child_ID}::*] {
- set shortname [namespace tail $vname]
- if {[array exists $vname]} {
- array set ${ns_snap}::${shortname} [array get $vname]
- } elseif {[info exists $vname]} {
- set ${ns_snap}::${shortname} [set $vname]
- } else {
- #variable exists without value (e.g created by 'variable' command)
- namespace eval $ns_snap [list variable $shortname] ;#create the variable without value, such that it is present, but does not 'info exist'
- }
- }
- #------------------------------------------------------------------------------------
-
-
-
-
-
-
-
-
-
- #puts "====>>> ifaces_added $ifaces_added"
- set idx 0
- set idx_count [llength $ifaces_added]
- set highest_constructor_IFID ""
- foreach IFID $ifaces_added {
- incr idx
- #puts "--> adding iface $IFID "
- namespace upvar ::p::${IFID}::_iface o_usedby o_usedby o_open o_open o_methods o_methods o_properties o_properties o_variables o_variables o_unknown o_unknown o_varspace o_varspace o_varspaces o_varspaces
-
- if {[llength $o_varspaces]} {
- foreach vs $o_varspaces {
- #ensure all varspaces for the interface exists so that the 'namespace upvar' entries in methods etc will work.
- if {[string match "::*" $vs]} {
- namespace eval $vs {} ;#an absolute path to a namespace which may not be under the object's namespace at all.
- } else {
- namespace eval ::p::${child_ID}::$vs {}
- }
- }
- }
-
- if {$IFID != 2} {
- #>ifinfo interface always has id 2 and is used by all interfaces - no need to add everything to its usedby list.
- if {![info exists o_usedby(i$child_ID)]} {
- set o_usedby(i$child_ID) $child_alias
- }
-
- #compile and close the interface only if it is shared
- if {$o_open} {
- ::p::predator::compile_interface $IFID $_ID_ ;#params: IFID , caller_ID_
- set o_open 0
- }
- }
-
-
- package require struct::set
-
- set propcmds [list]
- foreach cmd [info commands ::p::${IFID}::_iface::(GET)*] {
- set cmd [namespace tail $cmd]
- #may contain multiple results for same prop e.g (GET)x.3
- set cmd [string trimright $cmd 0123456789]
- set cmd [string trimright $cmd .] ;#do separately in case cmd name also contains numerals
- lappend propcmds [string range $cmd 5 end] ;#don't worry about dupes here.
- }
- set propcmds [struct::set union $propcmds] ;#a way to get rid of dupes.
- #$propcmds now holds all Properties as well as PropertyReads with no corresponding Property on this interface.
- foreach property $propcmds {
- #puts "\n\n ::p::${child_ID}::$property --->>>>>>>>>>>> ::p::${IFID}::_iface::(GET)$property \n"
- interp alias {} ::p::${child_ID}::(GET)$property {} ::p::${IFID}::_iface::(GET)$property ;#used by property reference traces
- interp alias {} ::p::${child_ID}::$property {} ::p::${IFID}::_iface::(GET)$property
- }
-
- set propcmds [list]
- foreach cmd [info commands ::p::${IFID}::_iface::(SET)*] {
- set cmd [namespace tail $cmd]
- #may contain multiple results for same prop e.g (GET)x.3
- set cmd [string trimright $cmd 0123456789]
- set cmd [string trimright $cmd .] ;#do separately in case cmd name also contains numerals
- lappend propcmds [string range $cmd 5 end] ;#don't worry about dupes here.
- }
- set propcmds [struct::set union $propcmds] ;#a way to get rid of dupes.
- #$propcmds now holds all Properties as well as PropertyReads with no corresponding Property on this interface.
- foreach property $propcmds {
- interp alias {} ::p::${child_ID}::(SET)$property {} ::p::${IFID}::_iface::(SET)$property ;#used by property reference traces
- }
-
-
- foreach method [dict keys $o_methods] {
- set arglist [dict get $o_methods $method arglist]
- set argvals ""
- foreach argspec $arglist {
- if {[llength $argspec] == 2} {
- set a [lindex $argspec 0]
- } else {
- set a $argspec
- }
-
- if {$a eq "args"} {
- append argvals " \{*\}\$args"
- } else {
- append argvals " \$$a"
- }
- }
- set argvals [string trimleft $argvals]
-
- #interp alias {} ::p::${child_ID}::$method {} ::p::${IFID}::_iface::$method
-
- #this proc directly on the object is not *just* a forwarding proc
- # - it provides a context in which the 'uplevel 1' from the running interface proc runs
- #This (in 2018) is faster than a namespace alias forward to an interface proc which used apply to run in the dynamically calculated namespace (it seems the dynamic namespace stopped it from byte-compiling?)
-
- #proc calls the method in the interface - which is an interp alias to the head of the implementation chain
-
- proc ::p::${child_ID}::$method [list _ID_ {*}$arglist] [subst {
- ::p::${IFID}::_iface::$method \$_ID_ $argvals
- }]
-
- #proc ::p::${child_ID}::$method [list _ID_ {*}$arglist] [string map [list @m@ $method @ID@ $IFID @argvals@ $argvals] {
- # ::p::@ID@::_iface::@m@ $_ID_ @argvals@
- #}]
-
- }
-
- #namespace eval ::p::${child_ID} [list namespace export {*}$o_methods]
-
- #implement property even if interface already compiled because we need to create defaults for each new child obj.
- # also need to add alias on base interface
- #make sure we are only implementing properties from the current CREATOR
- dict for {prop pdef} $o_properties {
- set varspace [dict get $pdef varspace]
- if {![string length $varspace]} {
- set ns ::p::${child_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${child_ID}::$varspace
- }
- }
- if {[dict exists $pdef default]} {
- if {![info exists ${ns}::o_$prop]} {
- #apply CREATORS defaults - don't trash existing state for matching property (only apply if var unset)
- set ${ns}::o_$prop [dict get $pdef default]
- }
- }
- #! May be replaced by a method with the same name
- if {$prop ni [dict keys $o_methods]} {
- interp alias {} ::p::${child_ID}::$prop {} ::p::${IFID}::_iface::(GET)$prop
- }
- interp alias {} ::p::${child_ID}::(GET)$prop {} ::p::${IFID}::_iface::(GET)$prop
- interp alias {} ::p::${child_ID}::(SET)$prop {} ::p::${IFID}::_iface::(SET)$prop
- }
-
-
-
- #variables
- #foreach vdef $o_variables {
- # if {[llength $vdef] == 2} {
- # #there is a default value defined.
- # lassign $vdef v default
- # if {![info exists ::p::${child_ID}::$v]} {
- # set ::p::${child_ID}::$v $default
- # }
- # }
- #}
- dict for {vname vdef} $o_variables {
- if {[dict exists $vdef default]} {
- #there is a default value defined.
- set varspace [dict get $vdef varspace]
- if {$varspace eq ""} {
- set ns ::p::${child_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${child_ID}::$varspace
- }
- }
- set ${ns}::$vname [dict get $vdef default]
- }
- }
-
- #!todo - review. Write tests for cases of multiple constructors!
-
- #We don't want to the run constructor for each added interface with the same set of args!
- #run for last one - rely on constructor authors to use @next@ properly?
- if {[llength [set ::p::${IFID}::_iface::o_constructor]]} {
- set highest_constructor_IFID $IFID
- }
-
- if {$idx == $idx_count} {
- #we are processing the last interface that was added - now run the latest constructor found
- if {$highest_constructor_IFID ne ""} {
- #at least one interface has a constructor
- if {[llength [set ::p::${highest_constructor_IFID}::_iface::o_constructor]]} {
- #puts ">>!! running constructor ifid:$highest_constructor_IFID child: $CHILDMAP"
- if {[catch {::p::${highest_constructor_IFID}::_iface::(CONSTRUCTOR) [dict create i [dict create this [list [dict get $CHILDMAP invocantdata] ] ]] {*}$args} constructor_error]} {
- set constructor_failure 1
- set constructor_errorInfo $::errorInfo ;#cache it immediately.
- break
- }
- }
- }
- }
-
- if {[info exists o_unknown]} {
- interp alias {} ::p::${IFID}::_iface::(UNKNOWN) {} ::p::${IFID}::_iface::$o_unknown
- interp alias {} ::p::${child_ID}::(UNKNOWN) {} ::p::${child_ID}::$o_unknown
-
- #interp alias {} ::p::${IFID}::_iface::(UNKNOWN) {} ::p::${child_ID}::$o_unknown
- #namespace eval ::p::${IFID}::_iface [list namespace unknown $o_unknown]
- #namespace eval ::p::${child_ID} [list namespace unknown $o_unknown]
- }
- }
-
- if {$constructor_failure} {
- if {$is_new_object} {
- #is Destroy enough to ensure that no new interfaces or objects were left dangling?
- $child .. Destroy
- } else {
- #object needs to be returned to a sensible state..
- #attempt to rollback all interface additions and object state changes!
- puts "!!!!!!!!!!!!!!!!>>>constructor rollback object $child_ID \n\n\n\n"
- #remove variables from the object's namespace - which don't exist in the snapshot.
- set snap_vars [info vars ${ns_snap}::*]
- puts "ns_snap '$ns_snap' vars'${snap_vars}'"
- foreach vname [info vars ::p::${child_ID}::*] {
- set shortname [namespace tail $vname]
- if {"${ns_snap}::$shortname" ni "$snap_vars"} {
- #puts "--- >>>>> unsetting $shortname "
- unset -nocomplain $vname
- }
- }
-
- #restore variables from snapshot - but try to do so with minimal writes (don't want to trigger any unnecessary traces)
- #values of vars may also have Changed
- #todo - consider traces? what is the correct behaviour?
- # - some application traces may have fired before the constructor error occurred.
- # Should the rollback now also trigger traces?
- #probably yes.
-
- #we need to test both source and dest var for arrayness - as the failed constructor could have changed the variable type, not just the value
- foreach vname $snap_vars {
- #puts stdout "@@@@@@@@@@@ restoring $vname"
- #flush stdout
-
-
- set shortname [namespace tail $vname]
- set target ::p::${child_ID}::$shortname
- if {$target in [info vars ::p::${child_ID}::*]} {
- set present 1 ;#variable exists in one of 3 forms; array, simple, or 'declared only'
- } else {
- set present 0
- }
-
- if {[array exists $vname]} {
- #restore 'array' variable
- if {!$present} {
- array set $target [array get $vname]
- } else {
- if {[array exists $target]} {
- #unset superfluous elements
- foreach key [array names $target] {
- if {$key ni [array names $vname]} {
- array unset $target $key
- }
- }
- #.. and write only elements that have changed.
- foreach key [array names $vname] {
- if {[set ${target}($key)] ne [set ${vname}($key)]} {
- set ${target}($key) [set ${vname}($key)]
- }
- }
- } else {
- #target has been changed to a simple variable - unset it and recreate the array.
- unset $target
- array set $target [array get $vname]
- }
- }
- } elseif {[info exists $vname]} {
- #restore 'simple' variable
- if {!$present} {
- set $target [set $vname]
- } else {
- if {[array exists $target]} {
- #target has been changed to array - unset it and recreate the simple variable.
- unset $target
- set $target [set $vname]
- } else {
- if {[set $target] ne [set $vname]} {
- set $target [set $vname]
- }
- }
- }
- } else {
- #restore 'declared' variable
- if {[array exists $target] || [info exists $target]} {
- unset -nocomplain $target
- }
- namespace eval ::p::${child_ID} [list variable $shortname]
- }
- }
- }
- namespace delete $ns_snap
- return -code error -errorinfo "oid:${child_ID} constructor_failure for IFID:${IFID}\n$constructor_errorInfo" $constructor_error
- }
- namespace delete $ns_snap
-
- }
-
- return $child
-}
-
-dict set ::p::-1::_iface::o_methods Clone {arglist {clone args}}
-#A cloned individual doesn't have the scars of its parent. i.e values (state) not *copied*
-# (new 'clean' object with same structure. values as set by constructor or *specified by defaults*)
-# Also: Any 'open' interfaces on the parent become closed on clone!
-proc ::p::-1::Clone {_ID_ clone args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set invocants [dict get $_ID_ i]
- lassign [dict get $MAP invocantdata] OID alias parent_defaultmethod cmd
-
- set _cmd [string map {::> ::} $cmd]
- set tail [namespace tail $_cmd]
-
-
- #obsolete?
- ##set IFID0 [lindex $map 1 0 end]
- #set IFID0 [lindex [dict get $MAP interfaces level0] end]
- ##set IFID1 [lindex $map 1 1 end]
- #set IFID1 [lindex [dict get $MAP interfaces level1] end]
-
-
- if {![string match "::*" $clone]} {
- if {[set ns [uplevel 1 {namespace current}]] eq "::"} {
- set clone ::$clone
- } else {
- set clone ${ns}::$clone
- }
- }
-
-
- set clone [namespace qualifiers $clone]::>[string trimleft [namespace tail $clone] >]
- set _clone [string map {::> ::} $clone]
-
-
- set cTail [namespace tail $_clone]
-
- set ns [namespace qualifiers $clone]
- if {$ns eq ""} {
- set ns "::"
- }
-
- namespace eval $ns {}
-
-
- #if {![llength [info commands $clone]]} {}
- if {[namespace which $clone] eq ""} {
- set clonemapdata [::p::internals::new_object $clone]
- } else {
- #overlay/mixin case - target/clone already exists
- #set CLONEMAP [lindex [interp alias {} [namespace origin $clone]] 1]
- set clonemapdata [$clone --]
- }
- set clone_ID [lindex [dict get $clonemapdata invocantdata] 0]
-
- upvar #0 ::p::${clone_ID}::_meta::map CLONEMAP
-
-
- #copy patterndata element of MAP straight across
- dict set CLONEMAP patterndata [dict get $MAP patterndata]
- set CLONE_INVOCANTDATA [dict get $CLONEMAP invocantdata]
- lset CLONE_INVOCANTDATA 2 $parent_defaultmethod
- dict set CLONEMAP invocantdata $CLONE_INVOCANTDATA
- lassign $CLONE_INVOCANTDATA clone_ID clone_alias clone_defaultmethod clone
-
- #update the clone's _ID_
- interp alias {} $clone_alias {} ;#first we must delete it
- interp alias {} $clone_alias {} ::p::internals::predator [list i [list this [list $CLONE_INVOCANTDATA] ] context {}]
-
- #! object_command was initially created as the renamed alias - so we have to do it again
- rename $clone_alias $clone
- trace add command $clone rename [list $clone .. Rename]
-
-
-
-
- #obsolete?
- #upvar ::p::${clone_ID}:: clone_INFO
- #upvar ::p::${IFID0}:: IFACE ;#same interface on predecessor(self) and clone.
- #upvar ::p::${OID}:: INFO
-
-
- array set clone_INFO [array get INFO]
-
- array set ::p::${clone_ID}::_iface::o_usedby [list] ;#'usedby'
-
-
- #!review!
- #if {![catch {set itemCmd $IFACE(m-1,name,item)}]} {
- #puts "***************"
- #puts "clone"
- #parray IFINFO
- #puts "***************"
- #}
-
- #we need the parent(s) in order to 'clone'??? - probably, as the defs are usually there unless the object was created with ad-hoc methods/props directly from ::>pattern
-
-
- #clone's interface maps must be a superset of original's
- foreach lev {0 1} {
- #set parent_ifaces [lindex $map 1 $lev]
- set parent_ifaces [dict get $MAP interfaces level$lev]
-
- #set existing_ifaces [lindex $CLONEMAP 1 $lev]
- set existing_ifaces [dict get $CLONEMAP interfaces level$lev]
-
- set added_ifaces_$lev [list]
- foreach ifid $parent_ifaces {
- if {$ifid ni $existing_ifaces} {
-
- #interface must not remain extensible after cloning.
- if {[set ::p::${ifid}::_iface::o_open]} {
- ::p::predator::compile_interface $ifid $_ID_
- set ::p::${ifid}::_iface::o_open 0
- }
-
-
-
- lappend added_ifaces_$lev $ifid
- #clone 'uses' all it's predecessor's interfaces, so update each interface's 'usedby' list.
- set ::p::${ifid}::_iface::o_usedby(i$clone_ID) $clone
- }
- }
- set extracted_sub_dict [dict get $CLONEMAP interfaces]
- dict set extracted_sub_dict level$lev [concat $existing_ifaces [set added_ifaces_$lev]]
- dict set CLONEMAP interfaces $extracted_sub_dict
- #lset CLONEMAP 1 $lev [concat $existing_ifaces [set added_ifaces_$lev]]
- }
-
- #interp alias {} ::p::${IFID0}::(VIOLATE) {} ::p::internals::(VIOLATE)
-
-
- #foreach *added* level0 interface..
- foreach ifid $added_ifaces_0 {
- namespace upvar ::p::${ifid}::_iface o_methods o_methods o_properties o_properties o_variables o_variables o_constructor o_constructor o_unknown o_unknown
-
-
- dict for {prop pdef} $o_properties {
- #lassign $pdef prop default
- if {[dict exists $pdef default]} {
- set varspace [dict get $pdef varspace]
- if {$varspace eq ""} {
- set ns ::p::${clone_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${clone_ID}::$varspace
- }
- }
-
- if {![info exists ${ns}::o_$prop]} {
- #apply CREATORS defaults - don't trash existing state for matching property (only apply if var unset)
- set ${ns}::o_$prop [dict get $pdef default]
- }
- }
-
- #! May be replaced by method of same name
- if {[namespace which ::p::${clone_ID}::$prop] eq ""} {
- interp alias {} ::p::${clone_ID}::$prop {} ::p::${ifid}::_iface::(GET)$prop
- }
- interp alias {} ::p::${clone_ID}::(GET)$prop {} ::p::${ifid}::_iface::(GET)$prop
- interp alias {} ::p::${clone_ID}::(SET)$prop {} ::p::${ifid}::_iface::(SET)$prop
- }
-
- #variables
- dict for {vname vdef} $o_variables {
- if {[dict exists $vdef default]} {
- set varspace [dict get $vdef varspace]
- if {$varspace eq ""} {
- set ns ::p::${clone_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${clone_ID}::$varspace
- }
- }
- if {![info exists ${ns}::$vname]} {
- set ::p::${clone_ID}::$vname [dict get $vdef default]
- }
- }
- }
-
-
- #update the clone object's base interface to reflect the new methods.
- #upvar 0 ::p::${ifid}:: IFACE
- #set methods [list]
- #foreach {key mname} [array get IFACE m-1,name,*] {
- # set method [lindex [split $key ,] end]
- # interp alias {} ::p::${clone_ID}::$method {} ::p::${ifid}::_iface::$method $CLONEMAP
- # lappend methods $method
- #}
- #namespace eval ::p::${clone_ID} [list namespace export {*}$methods]
-
-
- foreach method [dict keys $o_methods] {
-
- set arglist [dict get $o_methods $method arglist]
- set argvals ""
- foreach argspec $arglist {
- if {[llength $argspec] == 2} {
- set a [lindex $argspec 0]
- } else {
- set a $argspec
- }
-
- if {$a eq "args"} {
- append argvals " \{*\}\$args"
- } else {
- append argvals " \$$a"
- }
- }
- set argvals [string trimleft $argvals]
- #interp alias {} ::p::${clone_ID}::$method {} ::p::${ifid}::_iface::$method
-
- #this proc directly on the object is not *just* a forwarding proc
- # - it provides a context in which the 'uplevel 1' from the running interface proc runs
- #This (in 2018) is faster than a namespace alias forward to an interface proc which used apply to run in the dynamically calculated namespace (it seems the dynamic namespace stopped it from byte-compiling?)
-
- #proc calls the method in the interface - which is an interp alias to the head of the implementation chain
- proc ::p::${clone_ID}::$method [list _ID_ {*}$arglist] [subst {
- ::p::${ifid}::_iface::$method \$_ID_ $argvals
- }]
-
- }
- #namespace eval ::p::${clone_ID} [list namespace export {*}$o_methods]
-
-
- if {[info exists o_unknown]} {
- #interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${clone_ID}::$o_unknown
- interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${IID}::_iface::$o_unknown
- interp alias {} ::p::${clone_ID}::(UNKNOWN) {} ::p::${clone_ID}::$o_unknown
-
- #namespace eval ::p::${IID}::_iface [list namespace unknown $o_unknown]
- #namespace eval ::p::${clone_ID} [list namespace unknown $o_unknown]
-
- }
-
-
- #2021
- #Consider >parent with constructor that sets height
- #.eg >parent .. Constructor height {
- # set o_height $height
- #}
- #>parent .. Create >child 5
- # - >child has height 5
- # now when we peform a clone operation - it is the >parent's constructor that will run.
- # A clone will get default property and var values - but not other variable values unless the constructor sets them.
- #>child .. Clone >fakesibling 6
- # - >sibling has height 6
- # Consider if >child had it's own constructor created with .. Construct prior to the clone operation.
- # The >child's constructor didn't run - even though we created a >fakesibling - because the paren'ts one ran instead.
- # If we now add a constructor to >fakesibling - and put @next@ for constructor chaining...
- # when we now do >sibling .. Create >grandchild
- # - The constructor on >sibling runs first but chains to >child - the cloner aunt/uncle of the >grandchild
- # (while the calling order can't be changed - the positioning of @next@ tag in the contructor can allow code to run before and/or after the chained constructors and chaining can be disabled by providing a constructor without this tag.)
- # However - the args supplied in the >clone operation don't get either constructor running on the >grandchild
- #(though other arguments can be manually passed)
- # #!review - does this make sense? What if we add
- #
- #constructor for each interface called after properties initialised.
- #run each interface's constructor against child object, using the args passed into this clone method.
- if {[llength [set constructordef [set o_constructor]]]} {
- #error
- puts "!!!!!> running constructor for ifid:$ifid on clone:$clone_ID"
- ::p::${ifid}::_iface::(CONSTRUCTOR) [dict create i [dict create this [list [dict get $CLONEMAP invocantdata]] ]] {*}$args
-
- }
-
- }
-
-
- return $clone
-
-}
-
-
-
-interp alias {} ::p::-1::constructor {} ::p::-1::Constructor ;#for Define compatibility (snit?)
-dict set ::p::-1::_iface::o_methods Constructor {arglist {arglist body}}
-proc ::p::-1::Constructor {_ID_ arglist body} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- #lassign [lindex $invocant 0 ] OID alias itemCmd cmd
-
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end] ;#!todo - choose 'open' interface to expand.
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- #set iid_top [::p::get_new_object_id]
-
- #the >interface constructor takes a list of IDs for o_usedby
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top [list $OID]]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat $patterns $iid_top]
-
- #::p::predator::remap $invocant
- }
- set IID $iid_top
-
- namespace upvar ::p::${IID}::_iface o_open o_open o_constructor o_constructor o_varspace o_varspace o_varspaces o_varspaces
-
-
- # examine the existing command-chain
- set maxversion [::p::predator::method_chainhead $IID (CONSTRUCTOR)]
- set headid [expr {$maxversion + 1}]
- set THISNAME (CONSTRUCTOR).$headid ;#first version will be $method.1
-
- set next [::p::predator::next_script $IID (CONSTRUCTOR) $THISNAME $_ID_]
-
- #set varspaces [::pattern::varspace_list]
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
-
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls]
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t runtime_vardecls in Constructor $varDecls"
- }
-
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- #puts stderr ----
- #puts stderr $body
- #puts stderr ----
-
- proc ::p::${IID}::_iface::(CONSTRUCTOR).$headid [concat _ID_ $arglist] $body
- interp alias {} ::p::${IID}::_iface::(CONSTRUCTOR) {} ::p::${IID}::_iface::(CONSTRUCTOR).$headid
-
-
-
- set o_constructor [list $arglist $body]
- set o_open 1
-
- return
-}
-
-
-
-dict set ::p::-1::_iface::o_methods UsedBy {arglist {}}
-proc ::p::-1::UsedBy {_ID_} {
- return [array get ::p::[lindex [dict get $_ID_ i this] 0 0]::_iface::o_usedby]
-}
-
-
-dict set ::p::-1::_iface::o_methods Ready {arglist {}}
-proc ::p::-1::Ready {_ID_} {
- return [expr {![set ::p::[lindex [dict get $_ID_ i this] 0 0]::_iface::o_open]}]
-}
-
-
-
-dict set ::p::-1::_iface::o_methods Destroy {arglist {{force 1}}}
-
-#'force' 1 indicates object command & variable will also be removed.
-#'force' 0 is used when the containing namespace is being destroyed anyway - so no need to destroy cmd & var.
-#this is necessary for versions of Tcl that have problems with 'unset' being called multiple times. (e.g Tcl 8.5a4)
-#
-proc ::p::-1::Destroy {_ID_ {force 1}} {
- #puts stdout "\t\tDestroy called with _ID_:$_ID_ force:$force caller:[info level 1]"
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
-
- if {$OID eq "null"} {
- puts stderr "warning - review code. Destroy called on object with null OID. _ID_:$_ID_"
- return
- }
-
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
-
- #puts ">>>>>Explicit Destroy $cmd [clock format [clock seconds] -format %H:%M:%S] info-level-1'[info level 1]'<<<<<" ;flush stdout
-
- #explicit Destroy - remove traces
- #puts ">>TRACES: [trace info variable $cmd]"
- #foreach tinfo [trace info variable $cmd] {
- # trace remove variable $cmd {*}$tinfo
- #}
- #foreach tinfo [trace info command $cmd] {
- # trace remove command $cmd {*}$tinfo
- #}
-
-
- set _cmd [string map {::> ::} $cmd]
-
- #set ifaces [lindex $map 1]
- set iface_stacks [dict get $MAP interfaces level0]
- #set patterns [lindex $map 2]
- set pattern_stacks [dict get $MAP interfaces level1]
-
-
-
- set ifaces $iface_stacks
-
-
- set patterns $pattern_stacks
-
-
- #set i 0
- #foreach iflist $ifaces {
- # set IFID$i [lindex $iflist 0]
- # incr i
- #}
-
-
- set IFTOP [lindex $ifaces end]
-
- set DESTRUCTOR ::p::${IFTOP}::___system___destructor
- #may be a proc, or may be an alias
- if {[namespace which $DESTRUCTOR] ne ""} {
- set temp_ID_ [dict create i [dict create this [list [dict get $MAP invocantdata]]] context {}]
-
- if {[catch {$DESTRUCTOR $temp_ID_} prob]} {
- #!todo - ensure correct calling order of interfaces referencing the destructor proc
-
-
- #!todo - emit destructor errors somewhere - logger?
- #puts stderr "underlying proc already removed??? ---> $prob"
- #puts stderr "--------Destructor Error on interface $IFID0 of Object $OID-------------"
- #puts stderr $::errorInfo
- #puts stderr "---------------------"
- }
- }
-
-
- #remove ourself from each interfaces list of referencers
- #puts stderr "--- $ifaces"
-
- foreach var {ifaces patterns} {
-
- foreach i [set $var] {
-
- if {[string length $i]} {
- if {$i == 2} {
- #skip the >ifinfo interface which doesn't maintain a usedby list anyway.
- continue
- }
-
- if {[catch {
-
- upvar #0 ::p::${i}::_iface::o_usedby usedby
-
- array unset usedby i$OID
-
- #puts "\n***>>***"
- #puts "IFACE: $i usedby: $usedby"
- #puts "***>>***\n"
-
- #remove interface if no more referencers
- if {![array size usedby]} {
- #puts " **************** DESTROYING unused interface $i *****"
- #catch {namespace delete ::p::$i}
-
- #we happen to know where 'interface' object commands are kept:
-
- ::p::ifaces::>$i .. Destroy
-
- }
-
- } errMsg]} {
- #warning
- puts stderr "warning: error during destruction of object:$OID (removing usedby reference for interface $i) ([lindex [dict get $MAP invocantdata] 3]) \n $errMsg"
- }
- }
-
- }
-
- }
-
- set ns ::p::${OID}
- #puts "-- destroying objects below namespace:'$ns'"
- ::p::internals::DestroyObjectsBelowNamespace $ns
- #puts "--.destroyed objects below '$ns'"
-
-
- #set ns ::p::${OID}::_sub
- #call .. Destroy on each thing that looks like a pattern object anywhere below our 'user-area' namespace
- #( ::p::OBJECT::$OID )
- #puts "\n******** [clock format [clock seconds] -format %H:%M:%S] destroyingobjectsbelownamespace ns: $ns *****\n"
- #::p::internals::DestroyObjectsBelowNamespace $ns
-
- #same for _meta objects (e.g Methods,Properties collections)
- #set ns ::p::${OID}::_meta
- #::p::internals::DestroyObjectsBelowNamespace $ns
-
-
-
- #foreach obj [info commands ${ns}::>*] {
- # #Assume it's one of ours, and ask it to die.
- # catch {::p::meta::Destroy $obj}
- # #catch {$cmd .. Destroy}
- #}
- #just in case the user created subnamespaces.. kill objects there too.
- #foreach sub [namespace children $ns] {
- # ::p::internals::DestroyObjectsBelowNamespace $sub
- #}
-
-
- #!todo - fix. info vars on the namespace is not enough to detect references which were never set to a value!
- #use info commands ::p::${OID}::_ref::* to find all references - including variables never set
- #remove variable traces on REF vars
- #foreach rv [info vars ::p::${OID}::_ref::*] {
- # foreach tinfo [trace info variable $rv] {
- # #puts "-->removing traces on $rv: $tinfo"
- # trace remove variable $rv {*}$tinfo
- # }
- #}
-
- #!todo - write tests
- #refs create aliases and variables at the same place
- #- but variable may not exist if it was never set e.g if it was only used with info exists
- foreach rv [info commands ::p::${OID}::_ref::*] {
- foreach tinfo [trace info variable $rv] {
- #puts "-->removing traces on $rv: $tinfo"
- trace remove variable $rv {*}$tinfo
- }
- }
-
-
-
-
-
-
-
- #if {[catch {namespace delete $nsMeta} msg]} {
- # puts stderr "-----&&&&&&&&&&&&&& ERROR deleting NS $nsMeta : $msg "
- #} else {
- # #puts stderr "------ -- -- -- -- deleted $nsMeta "
- #}
-
-
- #!todo - remove
- #temp
- #catch {interp alias "" ::>$OID ""}
-
- if {$force} {
- #rename $cmd {}
-
- #removing the alias will remove the command - even if it's been renamed
- interp alias {} $alias {}
-
- #if {[catch {rename $_cmd {} } why]} {
- # #!todo - work out why some objects don't have matching command.
- # #puts stderr "\t rename $_cmd {} failed"
- #} else {
- # puts stderr "\t rename $_cmd {} SUCCEEDED!!!!!!!!!!"
- #}
-
- }
-
- set refns ::p::${OID}::_ref
- #puts "[clock format [clock seconds] -format %H:%M:%S] - tidying up namespace $refns"
- #puts "- children: [llength [namespace children $refns]]"
- #puts "- vars : [llength [info vars ${refns}::*]]"
- #puts "- commands: [llength [info commands ${refns}::*]]"
- #puts "- procs : [llength [info procs ${refns}::*]]"
- #puts "- aliases : [llength [lsearch -all -inline [interp aliases {}] ${refns}::*]]"
- #puts "- matching command: [llength [info commands ${refns}]]"
- #puts "[clock format [clock seconds] -format %H:%M:%S] - tidyup DONE $refns"
-
-
- #foreach v [info vars ${refns}::*] {
- # unset $v
- #}
- #foreach p [info procs ${refns}::*] {
- # rename $p {}
- #}
- #foreach a [lsearch -all -inline [interp aliases {}] ${refns}::*] {
- # interp alias {} $a {}
- #}
-
-
- #set ts1 [clock seconds]
- #puts "[clock format $ts1 -format %H:%M:%S] $cmd about to delete $refns."
- #puts "- children: [llength [namespace children $refns]]"
- #puts "- vars : [llength [info vars ${refns}::*]]"
-
- #puts "- commands: [llength [info commands ${refns}::*]]"
- #puts "- procs : [llength [info procs ${refns}::*]]"
- #puts "- aliases : [llength [lsearch -all -inline [interp aliases {}] ${refns}::*]]"
- #puts "- exact command: [info commands ${refns}]"
-
-
-
-
- #puts "--delete ::p::${OID}::_ref"
- if {[namespace exists ::p::${OID}::_ref]} {
- #could just catch.. but would rather know if there's some other weird reason the namespace can't be deleted.
- namespace delete ::p::${OID}::_ref::
- }
- set ts2 [clock seconds]
- #puts "[clock format $ts2 -format %H:%M:%S] $cmd deleted $refns. ELAPSED: [expr {$ts2 - $ts1}]"
-
-
- #delete namespace where instance variables reside
- #catch {namespace delete ::p::$OID}
- namespace delete ::p::$OID
-
- #puts "...... destroyed $cmd [clock format [clock seconds] -format %H:%M:%S] <<<<<" ;flush stdout
- return
-}
-
-
-interp alias {} ::p::-1::destructor {} ::p::-1::Destructor ;#for Define compatibility
-
-
-dict set ::p::-1::_iface::o_methods Destructor {arglist {args}}
-#!todo - destructor arguments? e.g to be able to mark for destruction on next sweep of some collector as opposed to immediate destruction?
-#install a Destructor on the invocant's open level1 interface.
-proc ::p::-1::Destructor {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- #lassign [lindex $map 0] OID alias itemCmd cmd
-
- set patterns [dict get $MAP interfaces level1]
-
- if {[llength $args] > 2} {
- error "too many arguments to 'Destructor' - expected at most 2 (arglist body)"
- }
-
- set existing_IID [lindex $patterns end] ;#!todo - get 'open' interface.
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- error "NOT TESTED"
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $patterns $posn $posn] $IID]
-
- #::p::predator::remap $invocant
- }
-
-
- set ::p::${IID}::_iface::o_destructor_body [lindex $args end]
-
- if {[llength $args] > 1} {
- #!todo - allow destructor args(?)
- set arglist [lindex $args 0]
- } else {
- set arglist [list]
- }
-
- set ::p::${IID}::_iface::o_destructor_args $arglist
-
- return
-}
-
-
-
-
-
-interp alias {} ::p::-1::method {} ::p::-1::PatternMethod ;#for Define compatibility (with snit)
-
-
-dict set ::p::-1::_iface::o_methods PatternMethod {arglist {method arglist body}}
-proc ::p::-1::PatternMethod {_ID_ method arglist body} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- lassign [dict get $MAP invocantdata] OID alias default_method object_command _wrapped
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end] ;#!todo - get 'open' interface.
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
- set IID $iid_top
-
-
- namespace upvar ::p::${IID}::_iface o_methods o_methods o_definition o_definition o_varspace o_varspace o_varspaces o_varspaces
-
-
- # examine the existing command-chain
- set maxversion [::p::predator::method_chainhead $IID $method]
- set headid [expr {$maxversion + 1}]
- set THISNAME $method.$headid ;#first version will be $method.1
-
- set next [::p::predator::next_script $IID $method $THISNAME $_ID_]
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- #puts stdout "!!!>!>>>>>$THISNAME VarDecls: $varDecls"
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t object $OID runtime_vardecls in PatternMethod $method $varDecls"
- }
-
-
- set body [::p::predator::wrap_script_in_apply_object_namespace $o_varspace $body[set body {}] $arglist]
-
- #set body [string map [::list @this@ "\[lindex \${_ID_} 0 3]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata\] 3\]" @next@ $next] $body[set body {}]\n]
- #puts "\t\t--------------------"
- #puts "\n"
- #puts $body
- #puts "\n"
- #puts "\t\t--------------------"
- proc ::p::${IID}::_iface::$THISNAME [concat _ID_ $arglist] $body
-
-
-
- #pointer from method-name to head of the interface's command-chain
- interp alias {} ::p::${IID}::_iface::$method {} ::p::${IID}::_iface::$THISNAME
-
-
-
- if {$method in [dict keys $o_methods]} {
- #error "patternmethod '$method' already present in interface $IID"
- set msg "WARNING: patternmethod '$method' already exists on objectid $OID ($object_command). Replacing previous version. (no chaining support here yet...)"
- if {[string match "*@next@*" $body]} {
- append msg "\n EXTRA-WARNING: method contains @next@"
- }
-
- puts stdout $msg
- } else {
- dict set o_methods $method [list arglist $arglist]
- }
-
- #::p::-1::update_invocant_aliases $_ID_
- return
-}
-
-#MultiMethod
-#invocant_signature records the rolenames and aritys as a dispatch signature to support multimethods which act on any number of invocants
-# e.g1 $obj .. MultiMethod add {these 2} $arglist $body
-# e.g2 $obj .. MultiMethod add {these n} $arglist $body
-#
-# e.g3 $collidabletemplate .. MultiMethod collision {vehicles 2 cameras 0..n} $arglist $body
-#
-# for e.g3 - all vehicles & cameras involved would need to have the interface containing the method named 'collision', with the matching invocant_signature.
-# (it is possible for the object, or even the same interface to contain another method named 'collision' with a different signature)
-# !todo - review rules for when invocants participating in a multimethod with a particular signature, have different implementations (method from different interfaces)
-# - can we avoid the overhead of checking for this at dispatch-time, and simply use which ever implementation we first encounter?
-# - should we warn about or enforce a same-implementation rule for all multimethod conflicts found at the time an object-conglomeration is formed?
-# - should there be before and after hooks for all invocants involved in a multimethod so they can each add behaviour independent of the shared multimethod code?
-# (and how would we define the call order? - presumably as it appears in the conglomerate)
-# (or could that be done with a more general method-wrapping mechanism?)
-#...should multimethods use some sort of event mechanism, and/or message-passing system?
-#
-dict set ::p::-1::_iface::o_methods MultiMethod {arglist {method invocant_signature arglist body args}}
-proc ::p::-1::MultiMethod {_ID_ method invocant_signature arglist body args} {
- set invocants [dict get $_ID_ i]
-
- error "not implemented"
-}
-
-dict set ::p::-1::_iface::o_methods DefaultMethod {arglist {{methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"}}}
-# we could use . to indicate no methodname - as this is one of a few highly confusing names for a method (also for example .. , # -- )
-#we can create a method named "." by using the argprotect operator --
-# e.g >x .. Method -- . {args} $body
-#It can then be called like so: >x . .
-#This is not guaranteed to work and is not in the test suite
-#for now we'll just use a highly unlikely string to indicate no argument was supplied
-proc ::p::-1::DefaultMethod {_ID_ {methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"} } {
- set non_argument_magicstring "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_method object_command _wrapped
- if {$methodname eq $non_argument_magicstring} {
- return $default_method
- } else {
- set extracted_value [dict get $MAP invocantdata]
- lset extracted_value 2 $methodname
- dict set MAP invocantdata $extracted_value ;#write modified value back
- #update the object's command alias to match
- interp alias {} $alias {} ;#first we must delete it
- interp alias {} $alias {} ::p::internals::predator [list i [list this [list $extracted_value ] ] context {}]
-
- #! $object_command was initially created as the renamed alias - so we have to do it again
- rename $alias $object_command
- trace add command $object_command rename [list $object_command .. Rename]
- return $methodname
- }
-}
-
-dict set ::p::-1::_iface::o_methods PatternDefaultMethod {arglist {{methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"}}}
-proc ::p::-1::PatternDefaultMethod {_ID_ {methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"} } {
- set non_argument_magicstring "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set extracted_patterndata [dict get $MAP patterndata]
- set pattern_default_method [dict get $extracted_patterndata patterndefaultmethod]
- if {$methodname eq $non_argument_magicstring} {
- return $pattern_default_method
- } else {
- dict set extracted_patterndata patterndefaultmethod $methodname
- dict set MAP patterndata $extracted_patterndata
- return $methodname
- }
-}
-
-
-dict set ::p::-1::_iface::o_methods Method {arglist {method arglist bodydef args}}
-proc ::p::-1::Method {_ID_ method arglist bodydef args} {
- set invocants [dict get $_ID_ i]
-
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
- set invocant_signature [list] ;
- ;# we sort when calculating the sig.. so a different key order will produce the same signature - !todo - this is probably desirable but review anyway.
- foreach role [lsort [dict keys $invocants]] {
- lappend invocant_signature $role [llength [dict get $invocants $role]]
- }
- #note: it's expected that by far the most common 'invocant signature' will be {this 1} - which corresponds to a standard method dispatch on a single invocant object - the 'subject' (aka 'this')
-
-
-
- lassign [dict get $MAP invocantdata] OID alias default_method object_command
- set interfaces [dict get $MAP interfaces level0]
-
-
-
- #################################################################################
- if 0 {
- set iid_top [lindex $interfaces end] ;#!todo - get 'open' interface
- set prev_open [set ::p::${iid_top}::_iface::o_open]
-
- set iface ::p::ifaces::>$iid_top
-
- set f_new 0
- if {![string length $iid_top]} {
- set f_new 1
- } else {
- if {[$iface . isClosed]} {
- set f_new 1
- }
- }
- if {$f_new} {
- #create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat $interfaces $iid_top]
- dict set MAP interfaces $extracted_sub_dict
-
- }
- set IID $iid_top
-
- }
- #################################################################################
-
- set IID [::p::predator::get_possibly_new_open_interface $OID]
-
- #upvar 0 ::p::${IID}:: IFACE
-
- namespace upvar ::p::${IID}::_iface o_methods o_methods o_definition o_definition o_varspace o_varspace o_varspaces o_varspaces
-
-
- #Interface proc
- # examine the existing command-chain
- set maxversion [::p::predator::method_chainhead $IID $method]
- set headid [expr {$maxversion + 1}]
- set THISNAME $method.$headid ;#first version will be $method.1
-
- if {$method ni [dict keys $o_methods]} {
- dict set o_methods $method [list arglist $arglist]
- }
-
- #next_script will call to lower interface in iStack if we are $method.1
- set next [::p::predator::next_script $IID $method $THISNAME $_ID_] ;#last parameter is caller_ID_
- #puts ">!>>$THISNAME>>>>> next: '$next'<<<<<<"
-
-
- #implement
- #-----------------------------------
- set processed [dict create {*}[::p::predator::expand_var_statements $bodydef $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- set varDecls ""
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls\n[dict get $processed body]
- }
-
-
- set body [::p::predator::wrap_script_in_apply_object_namespace $o_varspace $body $arglist]
-
-
-
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- #if {[string length $varDecls]} {
- # puts stdout "\t---------------------------------------------------------------"
- # puts stdout "\t----- efficiency warning - implicit var declarations used -----"
- # puts stdout "\t-------- $object_command .. Method $method $arglist ---------"
- # puts stdout "\t[string map [list \n \t\t\n] $body]"
- # puts stdout "\t--------------------------"
- #}
- #invocants are stored as a nested dict in the Invocant Data parameter (_ID_) under the key 'i', and then the invocant_role
- # while 'dict get $_ID_ i this' should always return a single invocant, all roles theoretically return a list of invocants fulfilling that position.
- #(as specified by the @ operator during object conglomeration)
- #set body [string map [::list @this@ "\[dict get \$_ID_ i this \]" @next@ $next] $body\n]
-
- #puts stdout "\t\t----------------------------"
- #puts stdout "$body"
- #puts stdout "\t\t----------------------------"
-
- proc ::p::${IID}::_iface::$THISNAME [concat _ID_ $arglist] $body
-
- #-----------------------------------
-
- #pointer from method-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::$method {} ::p::${IID}::_iface::$THISNAME
-
-
- #point to the interface command only. The dispatcher will supply the invocant data
- #interp alias {} ::p::${OID}::$method {} ::p::${IID}::_iface::$method
- set argvals ""
- foreach argspec $arglist {
- if {[llength $argspec] == 2} {
- set a [lindex $argspec 0]
- } else {
- set a $argspec
- }
- if {$a eq "args"} {
- append argvals " \{*\}\$args"
- } else {
- append argvals " \$$a"
- }
- }
- set argvals [string trimleft $argvals]
- #this proc directly on the object is not *just* a forwarding proc
- # - it provides a context in which the 'uplevel 1' from the running interface proc runs
- #This (in 2018) is faster than a namespace alias forward to an interface proc which used apply to run in the dynamically calculated namespace (it seems the dynamic namespace stopped it from byte-compiling?)
-
- #we point to the method of the same name in the interface - which is an interp alias to the head of the implementation chain
-
- proc ::p::${OID}::$method [list _ID_ {*}$arglist] [subst {
- ::p::${IID}::_iface::$method \$_ID_ $argvals
- }]
-
- if 0 {
- if {[llength $argvals]} {
- proc ::p::${OID}::$method [list _ID_ {*}$arglist] [string map [list @ID@ [list $_ID_] @iid@ $IID @m@ $method @argl@ $arglist @argv@ $argvals] {
- apply {{_ID_ @argl@} {::p::@iid@::_iface::@m@ $_ID_ @argl@}} @ID@ @argv@
- }]
- } else {
-
- proc ::p::${OID}::$method [list _ID_ {*}$arglist] [string map [list @ID@ [list $_ID_] @iid@ $IID @m@ $method @argl@ $arglist] {
- apply [list {_ID_ @argl@} {::p::@iid@::_iface::@m@ $_ID_ @argl@} [namespace current]] @ID@
- }]
-
- }
- }
-
-
- #proc ::p::${OID}::$method [list _ID_ {*}$arglist] [subst {
- # ::p::${IID}::_iface::$method \$_ID_ $argvals
- #}]
-
- #todo - for o_varspaces
- #install ::p::${OID}::${varspace}::$method with interp alias from ::p::${OID}::$method
- #- this should work correctly with the 'uplevel 1' procs in the interfaces
-
-
- if {[string length $o_varspace]} {
- if {[string match "::*" $o_varspace]} {
- namespace eval $o_varspace {}
- } else {
- namespace eval ::p::${OID}::$o_varspace {}
- }
- }
-
-
- #if the metainfo collection exists, update it. Don't worry if nonexistant as it will be created if needed.
- set colMethods ::p::${OID}::_meta::>colMethods
-
- if {[namespace which $colMethods] ne ""} {
- if {![$colMethods . hasKey $method]} {
- $colMethods . add [::p::internals::predator $_ID_ . $method .] $method
- }
- }
-
- #::p::-1::update_invocant_aliases $_ID_
- return
- #::>pattern .. Create [::>pattern .. Namespace]::>method_???
- #return $method_object
-}
-
-
-dict set ::p::-1::_iface::o_methods V {arglist {{glob *}}}
-proc ::p::-1::V {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
-
-
- set vlist [list]
- foreach IID $ifaces {
- dict for {vname vdef} [set ::p::${IID}::_iface::o_variables] {
- if {[string match $glob $vname]} {
- lappend vlist $vname
- }
- }
- }
- return $vlist
-}
-
-#experiment from http://wiki.tcl.tk/4884
-proc p::predator::pipeline {args} {
- set lambda {return -level 0}
- foreach arg $args {
- set lambda [list apply [dict get {
- toupper {{lambda input} {string toupper [{*}$lambda $input]}}
- tolower {{lambda input} {string tolower [{*}$lambda $input]}}
- totitle {{lambda input} {string totitle [{*}$lambda $input]}}
- prefix {{lambda pre input} {string cat $pre [{*}$lambda $input]}}
- suffix {{lambda suf input} {string cat [{*}$lambda $input] $suf}}
- } [lindex $arg 0]] $lambda[set lambda {}] {*}[lrange $arg 1 end]]
- }
- return $lambda
-}
-
-proc ::p::predator::get_apply_arg_0_oid {} {
- set apply_args [lrange [info level 0] 2 end]
- puts stderr ">>>>> apply_args:'$apply_args'<<<<"
- set invocant [lindex $apply_args 0]
- return [lindex [dict get $invocant i this] 0 0]
-}
-proc ::p::predator::get_oid {} {
- #puts stderr "---->> [info level 1] <<-----"
- set _ID_ [lindex [info level 1] 1] ;#something like ::p::17::_iface::method.1 {i {this { {16 ::p::16 item ::>thing {} } } }} arg1 arg2
- tailcall lindex [dict get $_ID_ i this] 0 0
-}
-
-#todo - make sure this is called for all script installations - e.g propertyread etc etc
-#Add tests to check code runs in correct namespace
-#review - how does 'Varspace' command affect this?
-proc ::p::predator::wrap_script_in_apply_object_namespace {varspace body arglist} {
- #use 'lindex $a 0' to make sure we only get the variable name. (arglist may have defaultvalues)
- set arglist_apply ""
- append arglist_apply "\$_ID_ "
- foreach a $arglist {
- if {$a eq "args"} {
- append arglist_apply "{*}\$args"
- } else {
- append arglist_apply "\$[lindex $a 0] "
- }
- }
- #!todo - allow fully qualified varspaces
- if {[string length $varspace]} {
- if {[string match ::* $varspace]} {
- return "tailcall apply \[list \[list _ID_ $arglist\] \{$body\} $varspace \] $arglist_apply"
- } else {
- #return "uplevel 1 \[list apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@::$varspace \] $arglist_apply \]\n"
- return "tailcall apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@::$varspace \] $arglist_apply"
- }
- } else {
- #return "uplevel 1 \[list apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply \]\n"
- #return "tailcall try \[list apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply \]"
-
- set script "tailcall apply \[list \{_ID_"
-
- if {[llength $arglist]} {
- append script " $arglist"
- }
- append script "\} \{"
- append script $body
- append script "\} ::p::@OID@\] "
- append script $arglist_apply
- #puts stderr "\n88888888888888888888888888\n\t$script\n"
- #puts stderr "\n77777777777777777777777777\n\ttailcall apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply"
- #return $script
-
-
- #-----------------------------------------------------------------------------
- # 2018 candidates
- #
- #return "tailcall apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply" ;#ok - but doesn't seem to be bytecompiled
- #return "tailcall apply \[list {_ID_ $arglist} {$body} ::p::@OID@ \] $arglist_apply" ;#ok - but doesn't seem to be bytecompiled
-
- #this has problems with @next@ arguments! (also script variables will possibly interfere with each other)
- #faster though.
- #return "uplevel 1 \{$body\}"
- return "uplevel 1 [list $body]"
- #-----------------------------------------------------------------------------
-
- #set script "apply \[list \[list _ID_ $arglist\] \{$body\}\] $arglist_apply"
- #return "uplevel 1 \{$script\}"
-
- #return "puts stderr --\[info locals\]-- ;apply \[list {_ID_ $arglist} {$body} ::p::\[p::predator::get_oid\] \] $arglist_apply" ;#fail
- #return "apply \[list {_ID_ $arglist} {$body} ::p::\[p::predator::get_oid\] \] $arglist_apply" ;#fail
-
-
-
- #return "tailcall apply { {_ID_ $arglist} {$body} ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\] } $arglist_apply" ;#wrong
-
- #return "tailcall apply \[list {_ID_ $arglist} {apply { {_ID_ $arglist} {$body}} $arglist_apply } ::p::@OID@ \] $arglist_apply" ;#wrong ns
-
-
- #experiment with different dispatch mechanism (interp alias with 'namespace inscope')
- #-----------
- #return "apply { {_ID_ $arglist} {$body}} $arglist_apply"
-
-
- #return "uplevel 1 \{$body\}" ;#do nothing
-
- #----------
-
- #return "tailcall namespace inscope ::p::@OID@ \{apply \{\{_ID_ $arglist\} \{$body\}\}\} $arglist_apply" ;#wrong! doesn't evaluate in the correct namespace (wrong _ID_ ??)
-
- #return "tailcall apply \{\{_ID_ $arglist\} \{namespace inscope ::p::@OID@ \{$body\}\} \} $arglist_apply" ;#wrong - _ID_ now not available in $body
-
- #return "tailcall apply \{\{ns _ID_ $arglist\} \{ apply \[list {_ID_ $arglist} \{$body\} \$ns \] $arglist_apply \} \} ::p::@OID@ $arglist_apply" ;#no quicker
-
- #return "tailcall "
-
- }
-}
-
-
-#Handle 'var' and 'varspace' declarations in method/constructor/destructor/propertyread etc bodies.
-#expand 'var' statements inline in method bodies
-#The presence of a var statement in any code-branch will cause the processor to NOT insert the implicit default var statements.
-#
-#concept of 'varspace' to allow separation and/or sharing of contexts for cooperating interfaces
-#WARNING: within methods etc, varspace statements affect all following var statements.. i.e varspace not affected by runtime code-branches!
-# e.g if 1 {varspace x} else {varspace y} will always leave 'varspace y' in effect for following statements.
-#Think of var & varspace statments as a form of compile-time 'macro'
-#
-#caters for 2-element lists as arguments to var statement to allow 'aliasing'
-#e.g var o_thing {o_data mydata}
-# this will upvar o_thing as o_thing & o_data as mydata
-#
-proc ::p::predator::expand_var_statements {rawbody {varspace ""}} {
- set body {}
-
- #keep count of any explicit var statments per varspace in 'numDeclared' array
- # don't initialise numDeclared. We use numDeclared keys to see which varspaces have var statements.
-
- #default varspace is ""
- #varspace should only have leading :: if it is an absolute namespace path.
-
-
- foreach ln [split $rawbody \n] {
- set trimline [string trim $ln]
-
- if {$trimline eq "var"} {
- #plain var statement alone indicates we don't have any explicit declarations in this branch
- # and we don't want implicit declarations for the current varspace either.
- #!todo - implement test
-
- incr numDeclared($varspace)
-
- #may be further var statements e.g - in other code branches
- #return [list body $rawbody varspaces_with_explicit_vars 1]
- } elseif {([string range $trimline 0 2] eq "var") && ([string is space [string index $trimline 3]])} {
-
- #append body " upvar #0 "
- #append body " namespace upvar ::p::\[lindex \$_ID_ 0 0 \]${varspace} "
- #append body " namespace upvar ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]${varspace} "
-
- if {$varspace eq ""} {
- append body " namespace upvar ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\] "
- } else {
- if {[string match "::*" $varspace]} {
- append body " namespace upvar $varspace "
- } else {
- append body " namespace upvar ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::${varspace} "
- }
- }
-
- #any whitespace before or betw var names doesn't matter - about to use as list.
- foreach varspec [string range $trimline 4 end] {
- lassign [concat $varspec $varspec] var alias ;#var == alias if varspec only 1 element.
- ##append body "::p::\[lindex \$_ID_ 0 0 \]::${varspace}$var $alias "
- #append body "::p::\[lindex \$_ID_ 0 0 \]${varspace}$var $alias "
-
- append body "$var $alias "
-
- }
- append body \n
-
- incr numDeclared($varspace)
- } elseif {([string range $trimline 0 7] eq "varspace") && ([string is space -strict [string index $trimline 8]])} {
- # 2021 REVIEW - why do we even need 'varspace x' commands in bodies? - just use 'namespace eval x' ???
- #it is assumed there is a single word following the 'varspace' keyword.
- set varspace [string trim [string range $trimline 9 end]]
-
- if {$varspace in [list {{}} {""}]} {
- set varspace ""
- }
- if {[string length $varspace]} {
- #set varspace ::${varspace}::
- #no need to initialize numDeclared($varspace) incr will work anyway.
- #if {![info exists numDeclared($varspace)]} {
- # set numDeclared($varspace) 0
- #}
-
- if {[string match "::*" $varspace]} {
- append body "namespace eval $varspace {} \n"
- } else {
- append body "namespace eval ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::$varspace {} \n"
- }
-
- #puts "!!!! here~! namespace eval ::p::\[lindex \$_ID_ 0 0\]$varspace {} "
- #append body "namespace eval ::p::\[lindex \$_ID_ 0 0\]$varspace {} \n"
- #append body "namespace eval ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]$varspace {} \n"
-
- #append body "puts \"varspace: created ns ::p::\[lindex \$_ID_ 0 0\]$varspace \"\n"
- }
- #!review - why? why do we need the magic 'default' name instead of just using the empty string?
- #if varspace argument was empty string - leave it alone
- } else {
- append body $ln\n
- }
- }
-
-
-
- set varspaces [array names numDeclared]
- return [list body $body varspaces_with_explicit_vars $varspaces]
-}
-
-
-
-
-#Interface Variables
-dict set ::p::-1::_iface::o_methods IV {arglist {{glob *}}}
-proc ::p::-1::IV {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
-
- #!todo - test
- #return [dict keys ::p::${OID}::_iface::o_variables $glob]
-
- set members [list]
- foreach vname [dict keys [set ::p::${OID}::_iface::o_variables]] {
- if {[string match $glob $vname]} {
- lappend members $vname
- }
- }
- return $members
-}
-
-dict set ::p::-1::_iface::o_methods MetaMethods {arglist {{glob *}}}
-proc ::p::-1::MetaMethods {_ID_ {glob *}} {
- upvar ::p::-1::_iface::o_methods metaface_methods
- set metamethod_names [lsort [dict keys $metaface_methods]]
- if {$glob ne "*"} {
- set metamethod_names [lsearch -all -inline $metamethod_names $glob]
- }
- return $metamethod_names
-}
-
-
-dict set ::p::-1::_iface::o_methods Methods {arglist {{idx ""}}}
-proc ::p::-1::Methods {_ID_ {idx ""}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set map [dict get $this_info map]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set col ::p::${OID}::_meta::>colMethods
-
- if {[namespace which $col] eq ""} {
- patternlib::>collection .. Create $col
- foreach IID $ifaces {
- foreach m [dict keys [set ::p::${IID}::_iface::o_methods]] {
- if {![$col . hasIndex $m]} {
- #todo - create some sort of lazy-evaluating method object?
- #set arglist [dict get [set ::p::${IID}::iface::o_methods] $m arglist]
- $col . add [::p::internals::predator $_ID_ . $m .] $m
- }
- }
- }
- }
- if {[string length $idx]} {
- return [$col . item $idx]
- } else {
- return $col
- }
-}
-
-dict set ::p::-1::_iface::o_methods M {arglist {{glob *}}}
-proc ::p::-1::M {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set map [dict get $this_info map]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
- set members [list]
- foreach IID $ifaces {
- lappend members {*}[dict keys [set ::p::${IID}::_iface::o_methods] $glob]
- }
- return $members
-}
-
-#PatternMethods
-dict set ::p::-1::_iface::o_methods PM {arglist {{glob *}}}
-proc ::p::-1::PM {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- #lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level1] ;#level 1 interfaces
- set members [list]
- foreach IID $ifaces {
- lappend members {*}[dict keys [set ::p::${IID}::_iface::o_methods] $glob]
- }
- return [lsort $members]
-}
-
-
-#review
-#Interface Methods
-dict set ::p::-1::_iface::o_methods IM {arglist {{glob *}}}
-proc ::p::-1::IM {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set map [dict get $this_info map]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- return [dict keys [set ::p::${OID}::_iface::o_methods] $glob]
-
-}
-
-
-
-dict set ::p::-1::_iface::o_methods InterfaceStacks {arglist {}}
-proc ::p::-1::InterfaceStacks {_ID_} {
- upvar #0 ::p::[lindex [dict get $_ID_ i this] 0 0]::_meta::map MAP
- return [dict get $MAP interfaces level0]
-}
-
-
-dict set ::p::-1::_iface::o_methods PatternStacks {arglist {}}
-proc ::p::-1::PatternStacks {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- return [dict get $MAP interfaces level1]
-}
-
-
-#!todo fix. need to account for references which were never set to a value
-dict set ::p::-1::_iface::o_methods DeletePropertyReferences {arglist {}}
-proc ::p::-1::DeletePropertyReferences {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- set cleared_references [list]
- set refvars [info vars ::p::${OID}::_ref::*]
- #unsetting vars will clear traces anyway - but we wish to avoid triggering the 'unset' traces - so we will explicitly remove all traces 1st.
- foreach rv $refvars {
- foreach tinfo [trace info variable $rv] {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- trace remove variable $rv $ops $cmd
- }
- unset $rv
- lappend cleared_references $rv
- }
-
-
- return [list deleted_property_references $cleared_references]
-}
-
-dict set ::p::-1::_iface::o_methods DeleteMethodReferences {arglist {}}
-proc ::p::-1::DeleteMethodReferences {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- set cleared_references [list]
-
- set iflist [dict get $MAP interfaces level0]
- set iflist_reverse [lreferse $iflist]
- #set iflist [dict get $MAP interfaces level0]
-
-
- set refcommands [info commands ::p::${OID}::_ref::*]
- foreach c $refcommands {
- set reftail [namespace tail $c]
- set field [lindex [split $c +] 0]
- set field_is_a_method 0
- foreach IFID $iflist_reverse {
- if {$field in [dict keys [set ::p::${IFID}::_iface::o_methods]]} {
- set field_is_a_method 1
- break
- }
- }
- if {$field_is_a_method} {
- #what if it's also a property?
- interp alias {} $c {}
- lappend cleared_references $c
- }
- }
-
-
- return [list deleted_method_references $cleared_references]
-}
-
-
-dict set ::p::-1::_iface::o_methods DeleteReferences {arglist {}}
-proc ::p::-1::DeleteReferences {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_method this
-
- set result [dict create]
- dict set result {*}[$this .. DeletePropertyReferences]
- dict set result {*}[$this .. DeleteMethodReferences]
-
- return $result
-}
-
-##
-#Digest
-#
-#!todo - review
-# -> a variable containing empty string is the same as a non existant variable as far as digest is concerned.. is that bad? (probably!)
-#
-#!todo - write tests - check that digest changes when properties of contained objects change value
-#
-#!todo - include method/property/interfaces in digest calc, or provide a separate more comprehensive digest method?
-#
-dict set ::p::-1::_iface::o_methods Digest {arglist {args}}
-proc ::p::-1::Digest {_ID_ args} {
- set invocants [dict get $_ID_ i]
- # md5 c-version is faster than md4 tcl version... and more likely to be required in the interp for some other purpose anyway.
- #set this_invocant [lindex [dict get $invocants this] 0]
- #lassign $this_invocant OID _etc
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] _OID alias default_method this
-
-
- set interface_ids [dict get $MAP interfaces level0]
- set IFID0 [lindex $interface_ids end]
-
- set known_flags {-recursive -algorithm -a -indent}
- set defaults {-recursive 1 -algorithm md5 -indent ""}
- if {[dict exists $args -a] && ![dict exists $args -algorithm]} {
- dict set args -algorithm [dict get $args -a]
- }
-
- set opts [dict merge $defaults $args]
- foreach key [dict keys $opts] {
- if {$key ni $known_flags} {
- error "unknown option $key. Expected only: $known_flags"
- }
- }
-
- set known_algos {"" raw RAW none NONE md5 MD5 sha256 SHA256}
- if {[dict get $opts -algorithm] ni $known_algos} {
- error "call to Digest with unknown -algorithm [dict get $opts -algorithm]. Expected one of: $known_algos"
- }
- set algo [string tolower [dict get $opts -algorithm]]
-
- # append comma for each var so that all changes in adjacent vars detectable.
- # i.e set x 34; set y 5
- # must be distinguishable from:
- # set x 3; set y 45
-
- if {[dict get $opts -indent] ne ""} {
- set state ""
- set indent "[dict get $opts -indent]"
- } else {
- set state "---\n"
- set indent " "
- }
- append state "${indent}object_command: $this\n"
- set indent "${indent} "
-
- #append state "[lindex [interp alias {} $alias] 1]\n" ;#at the very least, include the object's interface state.
- append state "${indent}interfaces: [dict get $MAP interfaces]\n";#at the very least, include the object's interface state.
-
-
- #!todo - recurse into 'varspaces'
- set varspaces_found [list]
- append state "${indent}interfaces:\n"
- foreach IID $interface_ids {
- append state "${indent} - interface: $IID\n"
- namespace upvar ::p::${IID}::_iface o_varspace local_o_varspace o_varspaces local_o_varspaces
- append state "${indent} varspaces:\n"
- foreach vs $local_o_varspaces {
- if {$vs ni $varspaces_found} {
- lappend varspaces_found $vs
- append state "${indent} - varspace: $vs\n"
- }
- }
- }
-
- append state "${indent}vars:\n"
- foreach var [info vars ::p::${OID}::*] {
- append state "${indent} - [namespace tail $var] : \""
- if {[catch {append state "[set $var]"}]} {
- append state "[array get $var]"
- }
- append state "\"\n"
- }
-
- if {[dict get $opts -recursive]} {
- append state "${indent}sub-objects:\n"
- set subargs $args
- dict set subargs -indent "$indent "
- foreach obj [info commands ::p::${OID}::>*] {
- append state "[$obj .. Digest {*}$subargs]\n"
- }
-
- append state "${indent}sub-namespaces:\n"
- set subargs $args
- dict set subargs -indent "$indent "
- foreach ns [namespace children ::p::${OID}] {
- append state "${indent} - namespace: $ns\n"
- foreach obj [info commands ${ns}::>*] {
- append state "[$obj .. Digest {*}$subargs]\n"
- }
- }
- }
-
- if {$algo in {"" raw none}} {
- return $state
- } else {
- if {$algo eq "md5"} {
- package require md5
- return [::md5::md5 -hex $state]
- } elseif {$algo eq "sha256"} {
- package require sha256
- return [::sha2::sha256 -hex $state]
- } elseif {$algo eq "blowfish"} {
- package require patterncipher
- patterncipher::>blowfish .. Create >b1
- set [>b1 . key .] 12341234
- >b1 . encrypt $state -final 1
- set result [>b1 . ciphertext]
- >b1 .. Destroy
-
- } elseif {$algo eq "blowfish-binary"} {
-
- } else {
- error "can't get here"
- }
-
- }
-}
-
-
-dict set ::p::-1::_iface::o_methods Variable {arglist {varname args}}
-proc ::p::-1::Variable {_ID_ varname args} {
- set invocants [dict get $_ID_ i]
-
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- #this interface itself is always a co-invocant
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set interfaces [dict get $MAP interfaces level0]
-
- #set existing_IID [lindex $map 1 0 end]
- set existing_IID [lindex $interfaces end]
-
- set prev_openstate [set ::p::${existing_IID}::_iface::o_open]
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #IID changed
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
-
- #update original object command
- set ::p::${IID}::_iface::o_open 0
- } else {
- set ::p::${IID}::_iface::o_open $prev_openstate
- }
-
- set varspace [set ::p::${IID}::_iface::o_varspace] ;#varspace at the time this Variable was added (may differ from default for interface)
-
- if {[llength $args]} {
- #!assume var not already present on interface - it is an error to define twice (?)
- #lappend ::p::${IID}::_iface::o_variables [list $varname [lindex $args 0]]
- dict set ::p::${IID}::_iface::o_variables $varname [list default [lindex $args 0] varspace $varspace]
-
-
- #Implement if there is a default
- #!todo - correct behaviour when overlaying on existing object with existing var of this name?
- #if {[string length $varspace]} {
- # set ::p::${OID}::${varspace}::$varname [lindex $args 0]
- #} else {
- set ::p::${OID}::$varname [lindex $args 0]
- #}
- } else {
- #lappend ::p::${IID}::_iface::o_variables [list $varname]
- dict set ::p::${IID}::_iface::o_variables $varname [list varspace $varspace]
- }
-
- #varspace '_iface'
-
- return
-}
-
-
-#interp alias {} ::p::-1::variable {} ::p::-1::PatternVariable ;#for Define compatibility
-
-dict set ::p::-1::_iface::o_methods PatternVariable {arglist {varname args}}
-proc ::p::-1::PatternVariable {_ID_ varname args} {
- set invocants [dict get $_ID_ i]
-
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- ##this interface itself is always a co-invocant
- #lassign [lindex $invocant 0 ] OID alias itemCmd cmd
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end] ;#!todo - get 'open' interface.
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat $patterns $iid_top]
- }
- set IID $iid_top
-
- set varspace [set ::p::${IID}::_iface::o_varspace] ;#record varspace against each variable, because default varspace for interface can be modified.
-
-
- if {[llength $args]} {
- #lappend ::p::${IID}::_iface::o_variables [list $varname [lindex $args 0]]
- dict set ::p::${IID}::_iface::o_variables $varname [list default [lindex $args 0] varspace $varspace]
- } else {
- dict set ::p::${IID}::_iface::o_variables $varname [list varspace $varspace]
- }
-
- return
-}
-
-dict set ::p::-1::_iface::o_methods Varspaces {arglist args}
-proc ::p::-1::Varspaces {_ID_ args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- if {![llength $args]} {
- #query
- set iid_top [lindex [dict get $MAP interfaces level0] end]
- set iface ::p::ifaces::>$iid_top
- if {![string length $iid_top]} {
- error "Cannot query Varspaces because no top level interface on object:[lindex [dict get $MAP invocantdata] 3] "
- } elseif {[$iface . isClosed]} {
- error "Cannot query Varspaces because top level interface (id:$iid_top) is closed on object:[lindex [dict get $MAP invocantdata] 3] "
- }
- return [set ::p::${iid_top}::_iface::o_varspaces]
- }
- set IID [::p::predator::get_possibly_new_open_interface $OID]
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_varspaces o_varspaces
-
- set varspaces $args
- foreach vs $varspaces {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- if {[string match ::* $vs]} {
- namespace eval $vs {}
- } else {
- namespace eval ::p::${OID}::$vs {}
- }
- lappend o_varspaces $vs
- }
- }
- return $o_varspaces
-}
-
-#set or query Varspace. Error to query a closed interface, but if interface closed when writing, itwill create a new open interface
-dict set ::p::-1::_iface::o_methods Varspace {arglist args}
-# set the default varspace for the interface, so that new methods/properties refer to it.
-# varspace may be switched in between various additions of methods/properties so that different methods/properties are using different varspaces.
-proc ::p::-1::Varspace {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- if {![llength $args]} {
- #query
- set iid_top [lindex [dict get $MAP interfaces level0] end]
- set iface ::p::ifaces::>$iid_top
- if {![string length $iid_top]} {
- error "Cannot query Varspace because no top level interface on object:[lindex [dict get $MAP invocantdata] 3] "
- } elseif {[$iface . isClosed]} {
- error "Cannot query Varspace because top level interface (id:$iid_top) is closed on object:[lindex [dict get $MAP invocantdata] 3] "
- }
- return [set ::p::${iid_top}::_iface::o_varspace]
- }
- set varspace [lindex $args 0]
-
- #set interfaces [dict get $MAP interfaces level0]
- #set iid_top [lindex $interfaces end]
-
- set IID [::p::predator::get_possibly_new_open_interface $OID]
-
-
- #namespace upvar ::p::${IID}::_iface o_variables o_variables o_properties o_properties o_methods o_methods o_varspace o_varspace
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_varspaces o_varspaces
-
- if {[string length $varspace]} {
- #ensure namespace exists !? do after list test?
- if {[string match ::* $varspace]} {
- namespace eval $varspace {}
- } else {
- namespace eval ::p::${OID}::$varspace {}
- }
- if {$varspace ni $o_varspaces} {
- lappend o_varspaces $varspace
- }
- }
- set o_varspace $varspace
-}
-
-
-proc ::p::predator::get_possibly_new_open_interface {OID} {
- #we need to re-upvar MAP rather than using a parameter - as we need to write back to it
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0]
- set iid_top [lindex $interfaces end]
-
-
- set iface ::p::ifaces::>$iid_top
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- #puts stderr ">>>>creating new interface $iid_top"
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat $interfaces $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
-
- return $iid_top
-}
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternVarspace {arglist {varspace args}}
-# set the default varspace for the interface, so that new methods/properties refer to it.
-# varspace may be switched in between various additions of methods/properties so that different methods/properties are using different varspaces.
-proc ::p::-1::PatternVarspace {_ID_ varspace args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end]
-
- set iface ::p::ifaces::>$iid_top
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
- set IID $iid_top
-
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_varspaces o_varspaces
- if {[string length $varspace]} {
- if {$varspace ni $o_varspaces} {
- lappend o_varspaces $varspace
- }
- }
- #o_varspace is the currently active varspace
- set o_varspace $varspace
-}
-###################################################################################################################################################
-
-#get varspace and default from highest interface - return all interface ids which define it
-dict set ::p::-1::_iface::o_methods GetPropertyInfo {arglist {{propnamepattern *}}}
-proc ::p::-1::GetPropertyInfo {_ID_ {propnamepattern *}} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0]
-
- array set propinfo {}
- set found_property_names [list]
- #start at the lowest and work up (normal storage order of $interfaces)
- foreach iid $interfaces {
- set propinfodict [set ::p::${iid}::_iface::o_properties]
- set matching_propnames [dict keys $propinfodict $propnamepattern]
- foreach propname $matching_propnames {
- if {$propname ni $found_property_names} {
- lappend found_property_names $propname
- }
- lappend propinfo($propname,interfaces) $iid
- ;#These 2 values for this $propname are overwritten for each iid in the outer loop - we are only interested in the last one
- if {[dict exists $propinfodict $propname default]} {
- set propinfo($propname,default) [dict get $propinfodict $propname default]
- }
- set propinfo($propname,varspace) [dict get $propinfodict $propname varspace]
- }
- }
-
- set resultdict [dict create]
- foreach propname $found_property_names {
- set fields [list varspace $propinfo($propname,varspace)]
- if {[array exists propinfo($propname,default)]} {
- lappend fields default [set propinfo($propname,default)]
- }
- lappend fields interfaces $propinfo($propname,interfaces)
- dict set resultdict $propname $fields
- }
- return $resultdict
-}
-
-
-dict set ::p::-1::_iface::o_methods GetTopPattern {arglist args}
-proc ::p::-1::GetTopPattern {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set interfaces [dict get $MAP interfaces level1]
- set iid_top [lindex $interfaces end]
- if {![string length $iid_top]} {
- lassign [dict get $MAP invocantdata] OID _alias _default_method object_command
- error "No installed level1 interfaces (patterns) for object $object_command"
- }
- return ::p::ifaces::>$iid_top
-}
-
-
-
-dict set ::p::-1::_iface::o_methods GetTopInterface {arglist args}
-proc ::p::-1::GetTopInterface {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set iid_top [lindex [dict get $MAP interfaces level0] end]
- if {![string length $iid_top]} {
- lassign [dict get $MAP invocantdata] OID _alias _default_method object_command
- error "No installed level0 interfaces for object $object_command"
- }
- return ::p::ifaces::>$iid_top
-}
-
-
-dict set ::p::-1::_iface::o_methods GetExpandableInterface {arglist args}
-proc ::p::-1::GetExpandableInterface {_ID_ args} {
-
-}
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods Property {arglist {property args}}
-proc ::p::-1::Property {_ID_ property args} {
- #puts stderr "::p::-1::Property called with _ID_: '$_ID_' property:$property args:$args"
- #set invocants [dict get $_ID_ i]
- #set invocant_roles [dict keys $invocants]
- if {[llength $args] > 1} {
- error ".. Property expects 1 or 2 arguments only. (>object .. Property propertyname ?default?)"
- }
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set interfaces [dict get $MAP interfaces level0]
- set iid_top [lindex $interfaces end]
-
- set prev_openstate [set ::p::${iid_top}::_iface::o_open]
-
- set iface ::p::ifaces::>$iid_top
-
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat $interfaces $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
- set IID $iid_top
-
-
- namespace upvar ::p::${IID}::_iface o_variables o_variables o_properties o_properties o_methods o_methods o_varspace o_varspace
-
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.1
-
-
- if {$headid == 1} {
- #implementation
- #interp alias {} ::p::${IID}::_iface::(GET)$property.1 {} ::p::predator::getprop $property
-
- #if {$o_varspace eq ""} {
- # set ns ::p::${OID}
- #} else {
- # if {[string match "::*" $o_varspace]} {
- # set ns $o_varspace
- # } else {
- # set ns ::p::${OID}::$o_varspace
- # }
- #}
- #proc ::p::${IID}::_iface::(GET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace %ns% $ns] [info body ::p::predator::getprop_template_immediate]]
-
- proc ::p::${IID}::_iface::(GET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace ] [info body ::p::predator::getprop_template]]
-
-
- #interp alias {} ::p::${IID}::_iface::(SET)$property.1 {} ::p::predator::setprop $property
- proc ::p::${IID}::_iface::(SET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace] [info body ::p::predator::setprop_template]]
-
-
- #chainhead pointers
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.1
- interp alias {} ::p::${IID}::_iface::(SET)$property {} ::p::${IID}::_iface::(SET)$property.1
-
-
- }
-
- if {($property ni [dict keys $o_methods])} {
- interp alias {} ::p::${IID}::_iface::$property {} ::p::${IID}::_iface::(GET)$property
- }
-
-
-
- #installation on object
-
- #namespace eval ::p::${OID} [list namespace export $property]
-
-
-
- #obsolete?
- #if {$property ni [P $_ID_]} {
- #only link objects (GET)/(SET) for this property if property not present on any of our other interfaces
- #interp alias {} ::p::${OID}::(GET)$property {} ::p::${IID}::_iface::(GET)$property $invocant
- #interp alias {} ::p::${OID}::(SET)$property {} ::p::${IID}::_iface::(SET)$property $invocant
- #}
-
- #link main (GET)/(SET) to this interface
- interp alias {} ::p::${OID}::(GET)$property {} ::p::${IID}::_iface::(GET)$property
- interp alias {} ::p::${OID}::(SET)$property {} ::p::${IID}::_iface::(SET)$property
-
- #Only install property if no method of same name already installed here.
- #(Method takes precedence over property because property always accessible via 'set' reference)
- #convenience pointer to chainhead pointer.
- if {$property ni [M $_ID_]} {
- interp alias {} ::p::${OID}::$property {} ::p::${IID}::_iface::(GET)$property
- } else {
- #property with same name as method - we need to make sure the refMisuse_traceHandler is fixed
-
-
- }
-
-
- set varspace [set ::p::${IID}::_iface::o_varspace]
-
-
-
- #Install the matching Variable
- #!todo - which should take preference if Variable also given a default?
- #if {[set posn [lsearch -index 0 $o_variables o_$property]] >= 0} {
- # set o_variables [lreplace $o_variables $posn $posn o_$property]
- #} else {
- # lappend o_variables [list o_$property]
- #}
- dict set o_variables o_$property [list varspace $varspace]
-
-
-
-
- if {[llength $args]} {
- #should store default once only!
- #set IFINFO(v,default,o_$property) $default
-
- set default [lindex $args end]
-
- dict set o_properties $property [list default $default varspace $varspace]
-
- #if {[set posn [lsearch -index 0 $o_properties $property]] >= 0} {
- # set o_properties [lreplace $o_properties $posn $posn [list $property $default]]
- #} else {
- # lappend o_properties [list $property $default]
- #}
-
- if {$varspace eq ""} {
- set ns ::p::${OID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${OID}::$o_varspace
- }
- }
-
- set ${ns}::o_$property $default
- #set ::p::${OID}::o_$property $default
- } else {
-
- #if {[set posn [lsearch -index 0 $o_properties $property]] >= 0} {
- # set o_properties [lreplace $o_properties $posn $posn [list $property]]
- #} else {
- # lappend o_properties [list $property]
- #}
- dict set o_properties $property [list varspace $varspace]
-
-
- #variable ::p::${OID}::o_$property
- }
-
-
-
-
-
- #if the metainfo collection exists, update it. Don't worry if nonexistant as it will be created if needed.
- #!todo - mark interface dirty (not ready?) instead? - would need all colProperties methods to respect dirty flag & synchronize as needed. (object filter?)
- #catch {::p::OBJECT::${OID}::colProperties add [::p::internals::predator $invocant . $property .] $property}
-
- set colProperties ::p::${OID}::_meta::>colProperties
- if {[namespace which $colProperties] ne ""} {
- if {![$colProperties . hasKey $property]} {
- $colProperties . add [::p::internals::predator $_ID_ . $property .] $property
- }
- }
-
- return
-}
-###################################################################################################################################################
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-interp alias {} ::p::-1::option {} ::p::-1::PatternProperty ;#for Define compatibility
-dict set ::p::-1::_iface::o_methods PatternProperty {arglist {property args}}
-proc ::p::-1::PatternProperty {_ID_ property args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end]
-
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat $patterns $iid_top]
- }
- set IID $iid_top
-
- namespace upvar ::p::${IID}::_iface o_properties o_properties o_variables o_variables o_varspace o_varspace
-
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.1
-
-
-
- if {$headid == 1} {
- #implementation
- #interp alias {} ::p::${IID}::_iface::(GET)$property.1 {} ::p::predator::getprop $property
- proc ::p::${IID}::_iface::(GET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace] [info body ::p::predator::getprop_template]]
- #interp alias {} ::p::${IID}::_iface::(SET)$property.1 {} ::p::predator::setprop $property
- proc ::p::${IID}::_iface::(SET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace] [info body ::p::predator::setprop_template]]
-
-
- #chainhead pointers
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.1
- interp alias {} ::p::${IID}::_iface::(SET)$property {} ::p::${IID}::_iface::(SET)$property.1
-
- }
-
- if {($property ni [dict keys [set ::p::${IID}::_iface::o_methods]])} {
- interp alias {} ::p::${IID}::_iface::$property {} ::p::${IID}::_iface::(GET)$property
- }
-
- set varspace [set ::p::${IID}::_iface::o_varspace]
-
- #Install the matching Variable
- #!todo - which should take preference if Variable also given a default?
- #if {[set posn [lsearch -index 0 $o_variables o_$property]] >= 0} {
- # set o_variables [lreplace $o_variables $posn $posn o_$property]
- #} else {
- # lappend o_variables [list o_$property]
- #}
- dict set o_variables o_$property [list varspace $varspace]
-
- set argc [llength $args]
-
- if {$argc} {
- if {$argc == 1} {
- set default [lindex $args 0]
- dict set o_properties $property [list default $default varspace $varspace]
- } else {
- #if more than one arg - treat as a dict of options.
- if {[dict exists $args -default]} {
- set default [dict get $args -default]
- dict set o_properties $property [list default $default varspace $varspace]
- } else {
- #no default value
- dict set o_properties $property [list varspace $varspace]
- }
- }
- #! only set default for property... not underlying variable.
- #lappend ::p::${IID}::_iface::o_variables [list o_$property [lindex $args 0]]
- } else {
- dict set o_properties $property [list varspace $varspace]
- }
- return
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternPropertyRead {arglist {property args}}
-proc ::p::-1::PatternPropertyRead {_ID_ property args} {
- set invocants [dict get $_ID_ i]
-
- set this_invocant [lindex [dict get $_ID_ i this] 0] ;#assume only one 'this'
- set OID [lindex $this_invocant 0]
- #set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias defaut_command cmd
-
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end]
-
- set idxlist [::list]
- if {[llength $args] == 1} {
- set body [lindex $args 0]
- } elseif {[llength $args] == 2} {
- lassign $args idxlist body
- } else {
- error "wrong # args: should be \"property body\" or \"property idxlist body\""
- }
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $patterns $posn $posn] $IID]
-
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
-
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- if {$headid == 1} {
- set headid 2 ;#reserve 1 for the getprop of the underlying property
- }
-
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.1
- set next [::p::predator::next_script $IID (GET)$property $THISNAME $_ID_] ;#last parameter is caller_ID_
-
-
- #implement
- #-----------------------------------
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
-
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- #implementation
- if {![llength $idxlist]} {
- proc ::p::${IID}::_iface::(GET)$property.$headid {_ID_ args} $body
- } else {
- #what are we trying to achieve here? ..
- proc ::p::${IID}::_iface::(GET)$property.$headid [linsert $idxlist 0 _ID_] $body
- }
-
-
- #-----------------------------------
-
-
- #adjust chain-head pointer to point to new head.
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.$headid
-
- return
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PropertyRead {arglist {property args}}
-proc ::p::-1::PropertyRead {_ID_ property args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- #assert $OID ne "null" - dispatcher won't call PropertyRead on a non-object(?) (presumably the call would be to 'Method' instead)
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end]
-
-
- set idxlist [::list]
- if {[llength $args] == 1} {
- set body [lindex $args 0]
- } elseif {[llength $args] == 2} {
- lassign $args idxlist body
- } else {
- error "wrong # args: should be \"property body\" or \"property idxlist body\""
- }
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
-
- set ::p::${IID}::_iface::o_open 0
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- #array set ::p::${IID}:: [::list pr,body,$property $body pr,arg,$property $idxlist pr,name,$property $property pr,iface,$property $cmd]
-
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- if {$headid == 1} {
- set headid 2
- }
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.2 - even if corresponding property is missing (we reserve $property.1 for the property itself)
-
- set next [::p::predator::next_script $IID (GET)$property $THISNAME $_ID_]
-
- #implement
- #-----------------------------------
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- proc ::p::${IID}::_iface::$THISNAME [concat _ID_ $idxlist] $body
-
- #-----------------------------------
-
-
-
- #pointer from prop-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.$headid
-
-
- interp alias {} ::p::${OID}::(GET)$property {} ::p::${IID}::_iface::(GET)$property ;#the reference traces will call this one - in case there is both a property and a method with this name.
- if {$property ni [M $_ID_]} {
- interp alias {} ::p::${OID}::$property {} ::p::${IID}::_iface::(GET)$property
- }
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PropertyWrite {arglist {property argname body}}
-proc ::p::-1::PropertyWrite {_ID_ property argname body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end] ;#!todo - get 'open' interface.
-
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
- set ::p::${IID}::_iface::o_open 0
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- #pw short for propertywrite
- #array set ::p::${IID}:: [::list pw,body,$property $body pw,arg,$property $argname pw,name,$property $property pw,iface,$property $cmd]
- array set ::p::${IID}:: [::list pw,body,$property $body pw,arg,$property $argname pw,name,$property $property]
-
-
- set maxversion [::p::predator::method_chainhead $IID (SET)$property]
- set headid [expr {$maxversion + 1}]
-
- set THISNAME (SET)$property.$headid
-
- set next [::p::predator::next_script $IID (SET)$property $THISNAME $_ID_]
-
- #implement
- #-----------------------------------
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- proc ::p::${IID}::_iface::$THISNAME [list _ID_ $argname] $body
-
- #-----------------------------------
-
-
-
- #pointer from method-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::(SET)$property {} ::p::${IID}::_iface::(SET)$property.$headid
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternPropertyWrite {arglist {property argname body}}
-proc ::p::-1::PatternPropertyWrite {_ID_ property argname body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
-
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end] ;#!todo - get 'open' interface.
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set existing_ifaces [lindex $map 1 1]
- set posn [lsearch $existing_ifaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $existing_ifaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $existing_ifaces $posn $posn] $IID]
-
- #set ::p::${IID}::_iface::o_open 0
- } else {
- }
-
- #pw short for propertywrite
- array set ::p::${IID}:: [::list pw,body,$property $body pw,arg,$property $argname pw,name,$property $property pw,iface,$property $cmd]
-
-
- return
-
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PropertyUnset {arglist {property arraykeypattern body}}
-proc ::p::-1::PropertyUnset {_ID_ property arraykeypattern body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end] ;#!todo - choose 'open' interface to expand.
-
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace o_propertyunset_handlers propertyunset_handlers
- #upvar ::p::${IID}::_iface::o_propertyunset_handlers propertyunset_handlers
- dict set propertyunset_handlers $property [list body $body arraykeypattern $arraykeypattern]
-
- set maxversion [::p::predator::method_chainhead $IID (UNSET)$property]
- set headid [expr {$maxversion + 1}]
-
- set THISNAME (UNSET)$property.$headid
-
- set next [::p::predator::next_script $IID (UNSET)$property $THISNAME $_ID_]
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- #note $arraykeypattern actually contains the name of the argument
- if {[string trim $arraykeypattern] eq ""} {
- set arraykeypattern _dontcare_ ;#
- }
- proc ::p::${IID}::_iface::(UNSET)$property.$headid [list _ID_ $arraykeypattern] $body
-
-#-----------------------------------
-
-
-#pointer from method-name to head of override-chain
-interp alias {} ::p::${IID}::_iface::(UNSET)$property {} ::p::${IID}::_iface::(UNSET)$property.$headid
-
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternPropertyUnset {arglist {property arraykeypattern body}}
-proc ::p::-1::PatternPropertyUnset {_ID_ property arraykeypattern body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
-
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end] ;#!todo - choose 'open' interface to expand.
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #set ::p::${IID}::_iface::o_open 0
- }
-
-
- upvar ::p::${IID}::_iface::o_propertyunset_handlers propertyunset_handlers
- dict set propertyunset_handlers $property [list body $body arraykeypattern $arraykeypattern]
-
- return
-}
-###################################################################################################################################################
-
-
-
-#lappend ::p::-1::_iface::o_methods Implements
-#!todo - some way to force overriding of any abstract (empty) methods from the source object
-#e.g leave interface open and raise an error when closing it if there are unoverridden methods?
-
-
-
-
-
-#implementation reuse - sugar for >object .. Clone >target
-dict set ::p::-1::_iface::o_methods Extends {arglist {pattern}}
-proc ::p::-1::Extends {_ID_ pattern} {
- if {!([string range [namespace tail $pattern] 0 0] eq ">")} {
- error "'Extends' expected a pattern object"
- }
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd object_command
-
-
- tailcall $pattern .. Clone $object_command
-}
-#implementation reuse - sugar for >pattern .. Create >target
-dict set ::p::-1::_iface::o_methods PatternExtends {arglist {pattern}}
-proc ::p::-1::PatternExtends {_ID_ pattern} {
- if {!([string range [namespace tail $pattern] 0 0] eq ">")} {
- error "'PatternExtends' expected a pattern object"
- }
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd object_command
-
-
- tailcall $pattern .. Create $object_command
-}
-
-
-dict set ::p::-1::_iface::o_methods Extend {arglist {{idx ""}}}
-proc ::p::-1::Extend {_ID_ {idx ""}} {
- puts stderr "Extend is DEPRECATED - use Expand instead"
- tailcall ::p::-1::Expand $_ID_ $idx
-}
-
-#set the topmost interface on the iStack to be 'open'
-dict set ::p::-1::_iface::o_methods Expand {arglist {{idx ""}}}
-proc ::p::-1::Expand {_ID_ {idx ""}} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0] ;#level 0 interfaces
- set iid_top [lindex $interfaces end]
- set iface ::p::ifaces::>$iid_top
-
- if {![string length $iid_top]} {
- #no existing interface - create a new one
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [list $iid_top]
- dict set MAP interfaces $extracted_sub_dict ;#write new interface into map
- $iface . open
- return $iid_top
- } else {
- if {[$iface . isOpen]} {
- #already open..
- #assume ready to expand.. shared or not!
- return $iid_top
- }
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- if {[$iface . refCount] > 1} {
- if {$iid_top != [set IID [::p::internals::expand_interface $iid_top ]]} {
- #!warning! not exercised by test suites!
-
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${iid_top}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- #remove existing interface & add
- set posn [lsearch $interfaces $iid_top]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
-
- set iid_top $IID
- set iface ::p::ifaces::>$iid_top
- }
- }
- }
-
- $iface . open
- return $iid_top
-}
-
-dict set ::p::-1::_iface::o_methods PatternExtend {arglist {{idx ""}}}
-proc ::p::-1::PatternExtend {_ID_ {idx ""}} {
- puts stderr "PatternExtend is DEPRECATED - use PatternExpand instead"
- tailcall ::p::-1::PatternExpand $_ID_ $idx
-}
-
-
-
-#set the topmost interface on the pStack to be 'open' if it's not shared
-# if shared - 'copylink' to new interface before opening for extension
-dict set ::p::-1::_iface::o_methods PatternExpand {arglist {{idx ""}}}
-proc ::p::-1::PatternExpand {_ID_ {idx ""}} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- #puts stderr "no tests written for PatternExpand "
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- set ifaces [dict get $MAP interfaces level1] ;#level 1 interfaces
- set iid_top [lindex $ifaces end]
- set iface ::p::ifaces::>$iid_top
-
- if {![string length $iid_top]} {
- #no existing interface - create a new one
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [list $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [list $iid_top]
- $iface . open
- return $iid_top
- } else {
- if {[$iface . isOpen]} {
- #already open..
- #assume ready to expand.. shared or not!
- return $iid_top
- }
-
- if {[$iface . refCount] > 1} {
- if {$iid_top != [set IID [::p::internals::expand_interface $iid_top]]} {
- #!WARNING! not exercised by test suite!
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${iid_top}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $ifaces $iid_top]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $ifaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $ifaces $posn $posn] $IID]
-
- set iid_top $IID
- set iface ::p::ifaces::>$iid_top
- }
- }
- }
-
- $iface . open
- return $iid_top
-}
-
-
-
-
-
-dict set ::p::-1::_iface::o_methods Properties {arglist {{idx ""}}}
-proc ::p::-1::Properties {_ID_ {idx ""}} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set col ::p::${OID}::_meta::>colProperties
-
- if {[namespace which $col] eq ""} {
- patternlib::>collection .. Create $col
- foreach IID $ifaces {
- dict for {prop pdef} [set ::p::${IID}::_iface::o_properties] {
- if {![$col . hasIndex $prop]} {
- $col . add [::p::internals::predator $_ID_ . $prop .] $prop
- }
- }
- }
- }
-
- if {[string length $idx]} {
- return [$col . item $idx]
- } else {
- return $col
- }
-}
-
-dict set ::p::-1::_iface::o_methods P {arglist {{glob *}}}
-proc ::p::-1::P {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set members [list]
- foreach IID $interfaces {
- lappend members {*}[dict keys [set ::p::${IID}::_iface::o_properties] $glob]
- }
- return [lsort $members]
-}
-
-#PatternProperties
-dict set ::p::-1::_iface::o_methods PP {arglist {{glob *}}}
-proc ::p::-1::PP {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level1] ;#level 1 interfaces
-
- set members [list]
- foreach IID $interfaces {
- lappend members {*}[dict keys [set ::p::${IID}::_iface::o_properties] $glob]
- }
- return [lsort $members]
-}
-
-
-
-#Interface Properties
-dict set ::p::-1::_iface::o_methods IP {arglist {{glob *}}}
-proc ::p::-1::IP {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
- set members [list]
-
- foreach m [dict keys [set ::p::${OID}::_iface::o_properties]] {
- if {[string match $glob [lindex $m 0]]} {
- lappend members [lindex $m 0]
- }
- }
- return $members
-}
-
-
-#used by rename.test - theoretically should be on a separate interface!
-dict set ::p::-1::_iface::o_methods CheckInvocants {arglist {args}}
-proc ::p::-1::CheckInvocants {_ID_ args} {
- #check all invocants in the _ID_ are consistent with data stored in their MAP variable
- set status "ok" ;#default to optimistic assumption
- set problems [list]
-
- set invocant_dict [dict get $_ID_ i]
- set invocant_roles [dict keys $invocant_dict]
-
- foreach role $invocant_roles {
- set invocant_list [dict get $invocant_dict $role]
- foreach aliased_invocantdata $invocant_list {
- set OID [lindex $aliased_invocantdata 0]
- set map_invocantdata [dict get [set ::p::${OID}::_meta::map] invocantdata]
- #we use lrange to make sure the lists are in canonical form
- if {[lrange $map_invocantdata 0 end] ne [lrange $aliased_invocantdata 0 end]} {
- set status "not-ok"
- lappend problems [list type "invocant_data_mismatch" invocant_role $role oid $OID command_invocantdata $aliased_invocantdata map_invocantdata $map_invocantdata]
- }
- }
-
- }
-
-
- set result [dict create]
- dict set result status $status
- dict set result problems $problems
-
- return $result
-}
-
-
-#get or set t
-dict set ::p::-1::_iface::o_methods Namespace {arglist {args}}
-proc ::p::-1::Namespace {_ID_ args} {
- #set invocants [dict get $_ID_ i]
- #set this_invocant [lindex [dict get $invocants this] 0]
- #lassign $this_invocant OID this_info
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set IID [lindex [dict get $MAP interfaces level0] end]
-
- namespace upvar ::p::${IID}::_iface o_varspace active_varspace
-
- if {[string length $active_varspace]} {
- set ns ::p::${OID}::$active_varspace
- } else {
- set ns ::p::${OID}
- }
-
- #!todo - review.. 'eval' & 'code' subcommands make it too easy to violate the object?
- # - should .. Namespace be usable at all from outside the object?
-
-
- if {[llength $args]} {
- #special case some of the namespace subcommands.
-
- #delete
- if {[string match "d*" [lindex $args 0]]} {
- error "Don't destroy an object's namespace like this. Use '>object .. Destroy' to remove an object."
- }
- #upvar,ensemble,which,code,origin,expor,import,forget
- if {[string range [lindex $args 0] 0 1] in [list "up" "en" "wh" "co" "or" "ex" "im" "fo"]} {
- return [namespace eval $ns [list namespace {*}$args]]
- }
- #current
- if {[string match "cu*" [lindex $args 0]]} {
- return $ns
- }
-
- #children,eval,exists,inscope,parent,qualifiers,tail
- return [namespace {*}[linsert $args 1 $ns]]
- } else {
- return $ns
- }
-}
-
-
-
-
-
-
-
-
-
-
-dict set ::p::-1::_iface::o_methods PatternUnknown {arglist {args}}
-proc ::p::-1::PatternUnknown {_ID_ args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end] ;#!todo - choose 'open' interface to expand.
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $patterns $posn $posn] $IID]
- #::p::predator::remap $invocant
- }
-
- set handlermethod [lindex $args 0]
-
-
- if {[llength $args]} {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- return
- } else {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- }
-
-}
-
-
-
-dict set ::p::-1::_iface::o_methods Unknown {arglist {args}}
-proc ::p::-1::Unknown {_ID_ args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end] ;#!todo - choose 'open' interface to expand.
-
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
- set ::p::${IID}::_iface::o_open 0
- } else {
- set ::p::${IID}::_iface::o_open $prev_open
- }
-
- set handlermethod [lindex $args 0]
-
- if {[llength $args]} {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- #set ::p::${IID}::(unknown) $handlermethod
-
-
- #interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${OID}::$handlermethod
- interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${IID}::_iface::$handlermethod
- interp alias {} ::p::${OID}::(UNKNOWN) {} ::p::${OID}::$handlermethod
-
- #namespace eval ::p::${IID}::_iface [list namespace unknown $handlermethod]
- #namespace eval ::p::${OID} [list namespace unknown $handlermethod]
-
- return
- } else {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- }
-
-}
-
-
-#useful on commandline - can just uparrow and add to it to become ' .. As varname' instead of editing start and end of commandline to make it 'set varname []'
-# should also work for non-object results
-dict set ::p::-1::_iface::o_methods As {arglist {varname}}
-proc ::p::-1::As {_ID_ varname} {
- set invocants [dict get $_ID_ i]
- #puts stdout "invocants: $invocants"
- #!todo - handle multiple invocants with other roles, not just 'this'
-
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- tailcall set $varname $cmd
- } else {
- #puts stdout "info level 1 [info level 1]"
- set role_members [dict get $_ID_ i this]
- if {[llength $role_members] == 1} {
- set member [lindex $role_members 0]
- lassign $member _OID namespace default_method stackvalue _wrapped
- tailcall set $varname $stackvalue
- } else {
- #multiple invocants - return all results as a list
- set resultlist [list]
- foreach member $role_members {
- lassign $member _OID namespace default_method stackvalue _wrapped
- lappend resultlist $stackvalue
- }
- tailcall set $varname $resultlist
- }
- }
-}
-
-#!todo - AsFileStream ??
-dict set ::p::-1::_iface::o_methods AsFile {arglist {filename args}}
-proc ::p::-1::AsFile {_ID_ filename args} {
- dict set default -force 0
- dict set default -dumpmethod ".. Digest -algorithm raw" ;#how to serialize/persist an object
- set opts [dict merge $default $args]
- set force [dict get $opts -force]
- set dumpmethod [dict get $opts -dumpmethod]
-
-
- if {[file pathtype $filename] eq "relative"} {
- set filename [pwd]/$filename
- }
- set filedir [file dirname $filename]
- if {![sf::file_writable $filedir]} {
- error "(method AsFile) ERROR folder $filedir is not writable"
- }
- if {[file exists $filename]} {
- if {!$force} {
- error "(method AsFile) ERROR file $filename already exists. Use -force 1 to overwrite"
- }
- if {![sf::file_writable $filename]} {
- error "(method AsFile) ERROR file $filename is not writable - check permissions"
- }
- }
- set fd [open $filename w]
- fconfigure $fd -translation binary
-
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- #tailcall set $varname $cmd
- set object_data [$cmd {*}$dumpmethod]
- puts -nonewline $fd $object_data
- close $fd
- return [list status 1 bytes [string length $object_data] filename $filename]
- } else {
- #puts stdout "info level 1 [info level 1]"
- set role_members [dict get $_ID_ i this]
- if {[llength $role_members] == 1} {
- set member [lindex $role_members 0]
- lassign $member _OID namespace default_method stackvalue _wrapped
- puts -nonewline $fd $stackvalue
- close $fd
- #tailcall set $varname $stackvalue
- return [list status 1 bytes [string length $stackvalue] filename $filename]
- } else {
- #multiple invocants - return all results as a list
- set resultlist [list]
- foreach member $role_members {
- lassign $member _OID namespace default_method stackvalue _wrapped
- lappend resultlist $stackvalue
- }
- puts -nonewline $fd $resultset
- close $fd
- return [list status 1 bytes [string length $resultset] filename $filename]
- #tailcall set $varname $resultlist
- }
- }
-
-}
-
-
-
-dict set ::p::-1::_iface::o_methods Object {arglist {}}
-proc ::p::-1::Object {_ID_} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- set result [string map [list ::> ::] $cmd]
- if {![catch {info level -1} prev_level]} {
- set called_by "(called by: $prev_level)"
- } else {
- set called_by "(called by: interp?)"
-
- }
-
- puts stdout "\n\nWARNING: '.. Object' calls are now obsolete. Please adjust your code. $called_by ( [info level 1])\n\n"
- puts stdout " (returning $result)"
-
- return $result
-}
-
-#todo: make equivalent to >pattern = cmdname, >pattern . x = cmdname , >pattern # apiname = cmdname
-dict set ::p::-1::_iface::o_methods MakeAlias {arglist {cmdname}}
-proc ::p::-1::MakeAlias {_ID_cmdname } {
- set OID [::p::obj_get_this_oid $_ID_]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- error "concept probably won't work - try making dispatcher understand trailing '= cmdname' "
-}
-dict set ::p::-1::_iface::o_methods ID {arglist {}}
-proc ::p::-1::ID {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- return $OID
-}
-
-dict set ::p::-1::_iface::o_methods IFINFO {arglist {}}
-proc ::p::-1::IFINFO {_ID_} {
- puts stderr "--_ID_: $_ID_--"
- set OID [::p::obj_get_this_oid $_ID_]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- puts stderr "-- MAP: $MAP--"
-
- set interfaces [dict get $MAP interfaces level0]
- set IFID [lindex $interfaces 0]
-
- if {![llength $interfaces]} {
- puts stderr "No interfaces present at level 0"
- } else {
- foreach IFID $interfaces {
- set iface ::p::ifaces::>$IFID
- puts stderr "$iface : [$iface --]"
- puts stderr "\tis open: [set ::p::${IFID}::_iface::o_open]"
- set variables [set ::p::${IFID}::_iface::o_variables]
- puts stderr "\tvariables: $variables"
- }
- }
-
-}
-
-
-
-
-dict set ::p::-1::_iface::o_methods INVOCANTDATA {arglist {}}
-proc ::p::-1::INVOCANTDATA {_ID_} {
- #same as a call to: >object ..
- return $_ID_
-}
-
-#obsolete?
-dict set ::p::-1::_iface::o_methods UPDATEDINVOCANTDATA {arglist {}}
-proc ::p::-1::UPDATEDINVOCANTDATA {_ID_} {
- #package require dictutils
- set updated_ID_ $_ID_
- array set updated_roles [list]
-
- set invocants [dict get $_ID_ i]
- set invocant_roles [dict keys $invocants]
- foreach role $invocant_roles {
-
- set role_members [dict get $invocants $role]
- foreach member [dict get $invocants $role] {
- #each member is a 2-element list consisting of the OID and a dictionary
- #each member is a 5-element list
- #set OID [lindex $member 0]
- #set object_dict [lindex $member 1]
- lassign $member OID alias itemcmd cmd wrapped
-
- set MAP [set ::p::${OID}::_meta::map]
- #if {[dictutils::equal {apply {{key v1 v2} {expr {$v1 eq $v2}}}} $mapvalue [dict get $object_dict map]]} {}
-
- if {[dict get $MAP invocantdata] eq $member}
- #same - nothing to do
-
- } else {
- package require overtype
- puts stderr "---------------------------------------------------------"
- puts stderr "UPDATEDINVOCANTDATA WARNING: invocantdata in _ID_ not equal to invocantdata in _meta::map - returning updated version"
- set col1 [string repeat " " [expr {[string length [dict get $MAP invocantdata]] + 2}]]
- puts stderr "[overtype::left $col1 {_ID_ map value}]: $member"
- puts stderr "[overtype::left $col1 ::p::${OID}::_meta::map]: [dict get $MAP invocantdata]"
- puts stderr "---------------------------------------------------------"
- #take _meta::map version
- lappend updated_roles($role) [dict get $MAP invocantdata]
- }
-
- }
-
- #overwrite changed roles only
- foreach role [array names updated_roles] {
- dict set updated_ID_ i $role [set updated_roles($role)]
- }
-
- return $updated_ID_
-}
-
-
-
-dict set ::p::-1::_iface::o_methods INFO {arglist {}}
-proc ::p::-1::INFO {_ID_} {
- set result ""
- append result "_ID_: $_ID_\n"
-
- set invocants [dict get $_ID_ i]
- set invocant_roles [dict keys $invocants]
- append result "invocant roles: $invocant_roles\n"
- set total_invocants 0
- foreach key $invocant_roles {
- incr total_invocants [llength [dict get $invocants $key]]
- }
-
- append result "invocants: ($total_invocants invocant(s) in [llength $invocant_roles] role(s)) \n"
- foreach key $invocant_roles {
- append result "\t-------------------------------\n"
- append result "\trole: $key\n"
- set role_members [dict get $invocants $key] ;#usually the role 'this' will have 1 member - but roles can have any number of invocants
- append result "\t Raw data for this role: $role_members\n"
- append result "\t Number of invocants in this role: [llength $role_members]\n"
- foreach member $role_members {
- #set OID [lindex [dict get $invocants $key] 0 0]
- set OID [lindex $member 0]
- append result "\t\tOID: $OID\n"
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- append result "\t\tmap:\n"
- foreach key [dict keys $MAP] {
- append result "\t\t\t$key\n"
- append result "\t\t\t\t [dict get $MAP $key]\n"
- append result "\t\t\t----\n"
- }
- lassign [dict get $MAP invocantdata] _OID namespace default_method cmd _wrapped
- append result "\t\tNamespace: $namespace\n"
- append result "\t\tDefault method: $default_method\n"
- append result "\t\tCommand: $cmd\n"
- append result "\t\tCommand Alias: [::pattern::which_alias $cmd]\n"
- append result "\t\tLevel0 interfaces: [dict get $MAP interfaces level0]\n"
- append result "\t\tLevel1 interfaces: [dict get $MAP interfaces level1]\n"
- } else {
- lassign $member _OID namespace default_method stackvalue _wrapped
- append result "\t\t last item on the predator stack is a value not an object"
- append result "\t\t Value is: $stackvalue"
-
- }
- }
- append result "\n"
- append result "\t-------------------------------\n"
- }
-
-
-
- return $result
-}
-
-
-
-
-dict set ::p::-1::_iface::o_methods Rename {arglist {args}}
-proc ::p::-1::Rename {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- if {![llength $args]} {
- error "Rename expected \$newname argument"
- }
-
- #Rename operates only on the 'this' invocant? What if there is more than one 'this'? should we raise an error if there is anything other than a single invocant?
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
-
- #puts ">>.>> Rename. _ID_: $_ID_"
-
- if {[catch {
-
- if {([llength $args] == 3) && [lindex $args 2] eq "rename"} {
-
- #appears to be a 'trace command rename' firing
- #puts "\t>>>> rename trace fired $MAP $args <<<"
-
- lassign $args oldcmd newcmd
- set extracted_invocantdata [dict get $MAP invocantdata]
- lset extracted_invocantdata 3 $newcmd
- dict set MAP invocantdata $extracted_invocantdata
-
-
- lassign $extracted_invocantdata _oid alias _default_method object_command _wrapped
-
- #Write the same info into the _ID_ value of the alias
- interp alias {} $alias {} ;#first we must delete it
- interp alias {} $alias {} ::p::internals::predator [list i [list this [list $extracted_invocantdata ] ] context {}]
-
-
-
- #! $object_command was initially created as the renamed alias - so we have to do it again
- uplevel 1 [list rename $alias $object_command]
- trace add command $object_command rename [list $object_command .. Rename]
-
- } elseif {[llength $args] == 1} {
- #let the rename trace fire and we will be called again to do the remap!
- uplevel 1 [list rename [lindex [dict get $MAP invocantdata] 3] [lindex $args 0]]
- } else {
- error "Rename expected \$newname argument ."
- }
-
- } errM]} {
- puts stderr "\t@@@@@@ rename error"
- set ruler "\t[string repeat - 80]"
- puts stderr $ruler
- puts stderr $errM
- puts stderr $ruler
-
- }
-
- return
-
-
-}
-
-proc ::p::obj_get_invocants {_ID_} {
- return [dict get $_ID_ i]
-}
-#The invocant role 'this' is special and should always have only one member.
-# dict get $_ID_ i XXX will always return a list of invocants that are playing role XXX
-proc ::p::obj_get_this_oid {_ID_} {
- return [lindex [dict get $_ID_ i this] 0 0]
-}
-proc ::p::obj_get_this_ns {_ID_} {
- return [lindex [dict get $_ID_ i this] 0 1]
-}
-
-proc ::p::obj_get_this_cmd {_ID_} {
- return [lindex [dict get $_ID_ i this] 0 3]
-}
-proc ::p::obj_get_this_data {_ID_} {
- lassign [dict get [set ::p::[lindex [dict get $_ID_ i this] 0 0]::_meta::map] invocantdata] OID ns _unknown cmd
- #set this_invocant_data {*}[dict get $_ID_ i this]
- return [list oid $OID ns $ns cmd $cmd]
-}
-proc ::p::map {OID varname} {
- tailcall upvar #0 ::p::${OID}::_meta::map $varname
-}
-
-
-
diff --git a/src/bootsupport/modules/natsort-0.1.1.6.tm b/src/bootsupport/modules/natsort-0.1.1.6.tm
deleted file mode 100644
index d16fa718..00000000
--- a/src/bootsupport/modules/natsort-0.1.1.6.tm
+++ /dev/null
@@ -1,1982 +0,0 @@
-#! /usr/bin/env tclsh
-
-
-#todo - remove flagfilter - use punk::args?
-package require flagfilter
-namespace import ::flagfilter::check_flags
-
-namespace eval natsort {
- #REVIEW - determine and document the purpose of scriptdir being added to tm path
- proc scriptdir {} {
- set possibly_linked_script [file dirname [file normalize [file join [info script] __dummy__]]]
- if {[file isdirectory $possibly_linked_script]} {
- return $possibly_linked_script
- } else {
- return [file dirname $possibly_linked_script]
- }
- }
- if {![interp issafe]} {
- set sdir [scriptdir]
- #puts stderr "natsort tcl::tm::add $sdir"
- if {$sdir ni [tcl::tm::list]} {
- catch {tcl::tm::add $sdir}
- }
- }
-}
-
-
-namespace eval natsort {
- variable stacktrace_on 0
-
- proc do_error {msg {then error}} {
- #note we exit or error out even if debug selected - as every do_error call is meant to interrupt code processing at the site of call
- #this is not just a 'logging' call even though it has log-like descriptors
- lassign $then type code
- if {$code eq ""} {
- set code 1
- }
- set type [string tolower $type]
- set levels [list debug info notice warn error critical]
- if {$type in [concat $levels exit]} {
- puts stderr "|$type> $msg"
- } else {
- puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '"
- }
- flush stderr
- if {$::tcl_interactive} {
- #may not always be desirable - but assumed to be more useful not to exit despite request, to aid in debugging
- if {[string tolower $type] eq "exit"} {
- puts stderr " (exit suppressed due to tcl_interactive - raising error instead)"
- if {![string is digit -strict $code]} {
- puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '"
- }
- flush stderr
- }
- return -code error $msg
- } else {
- if {$type ne "exit"} {
- return -code error $msg
- } else {
- if {[string is digit -strict $code]} {
- exit $code
- } else {
- puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '"
- return -code error $msg
- }
- }
- }
- }
-
-
-
-
-
-
- variable debug 0
- variable testlist
- set testlist {
- 00.test-firstposition.txt
- 0001.blah.txt
- 1.test-sorts-after-all-leadingzero-number-one-equivs.txt
- 1010.thousand-and-ten.second.txt
- 01010.thousand-and-ten.first.txt
- 0001.aaa.txt
- 001.zzz.txt
- 08.octal.txt-last-octal
- 008.another-octal-first-octal.txt
- 08.again-second-octal.txt
- 001.a.txt
- 0010.reconfig.txt
- 010.etc.txt
- 005.etc.01.txt
- 005.Etc.02.txt
- 005.123.abc.txt
- 200.somewhere.txt
- 2zzzz.before-somewhere.txt
- 00222-after-somewhere.txt
- 005.00010.abc.txt
- 005.a3423bc.00010.abc.txt
- 005.001.abc.txt
- 005.etc.1010.txt
- 005.etc.010.txt
- 005.etc.10.txt
- " 005.etc.10.txt"
- 005.etc.001.txt
- 20.somewhere.txt
- 4611686018427387904999999999-bignum.txt
- 4611686018427387903-bigishnum.txt
- 9223372036854775807-bigint.txt
- etca-a
- etc-a
- etc2-a
- a0001blah.txt
- a010.txt
- winlike-sort-difference-0.1.txt
- winlike-sort-difference-0.1.1.txt
- a1.txt
- b1-a0001blah.txt
- b1-a010.txt
- b1-a1.txt
- -a1.txt
- --a1.txt
- --a10.txt
- 2.high-two.yml
- 02.higher-two.yml
- reconfig.txt
- _common.stuff.txt
- CASETEST.txt
- casetest.txt
- something.txt
- some~thing.txt
- someathing.txt
- someThing.txt
- thing.txt
- thing_revised.txt
- thing-revised.txt
- "thing revised.txt"
- "spacetest.txt"
- " spacetest.txt"
- " spacetest.txt"
- "spacetest2.txt"
- "spacetest 2.txt"
- "spacetest02.txt"
- name.txt
- name2.txt
- "name .txt"
- "name2 .txt"
- blah.txt
- combined.txt
- a001.txt
- .test
- .ssh
- "Feb 10.txt"
- "Feb 8.txt"
- 1ab23v23v3r89ad8a8a8a9d.txt
- "Folder (10)/file.tar.gz"
- "Folder/file.tar.gz"
- "Folder (1)/file (1).tar.gz"
- "Folder (1)/file.tar.gz"
- "Folder (01)/file.tar.gz"
- "Folder1/file.tar.gz"
- "Folder(1)/file.tar.gz"
-
- }
- lappend testlist "Some file.txt"
- lappend testlist " Some extra file1.txt"
- lappend testlist " Some extra file01.txt"
- lappend testlist " some extra file1.txt"
- lappend testlist " Some extra file003.txt"
- lappend testlist " Some file.txt"
- lappend testlist "Some extra file02.txt"
- lappend testlist "Program Files (x86)"
- lappend testlist "01999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999991-bigger-pathologically-bignum.txt"
- lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt"
- lappend testlist "29999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-smaller-pathologically-bignum.txt"
- lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt with (more 1.txt"
- lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt with (more 01.txt"
- lappend testlist "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1.pathological-num-nonnum-swapping-leadzero-should-be-first.txt"
- lappend testlist "a1a1a1a1a1a1a1a1a1a1a1a01a1a1a1a1a1a1a1a1a1a1a1a1.pathological-num-nonnum-swapping-leadzero-should-be-first.txt"
- lappend testlist "b1b1b1b1.txt"
- lappend testlist "b1b01z1z1.txt"
- lappend testlist "c1c111c1.txt"
- lappend testlist "c1c1c1c1.txt"
-
- namespace eval overtype {
- proc right {args} {
- # @d !todo - implement overflow, length checks etc
-
- if {[llength $args] < 2} {
- error {usage: ?-overflow [1|0]? undertext overtext}
- }
- foreach {undertext overtext} [lrange $args end-1 end] break
-
- set opt(-overflow) 0
- array set opt [lrange $args 0 end-2]
-
-
- set olen [string length $overtext]
- set ulen [string length $undertext]
-
- if {$opt(-overflow)} {
- return [string range $undertext 0 end-$olen]$overtext
- } else {
- if {$olen > $ulen} {
- set diff [expr {$olen - $ulen}]
- return [string range $undertext 0 end-$olen][string range $overtext 0 end-$diff]
- } else {
- return [string range $undertext 0 end-$olen]$overtext
- }
- }
- }
- proc left {args} {
- # @c overtype starting at left (overstrike)
- # @c can/should we use something like this?: 'format "%-*s" $len $overtext
-
- if {[llength $args] < 2} {
- error {usage: ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext}
- }
- foreach {undertext overtext} [lrange $args end-1 end] break
-
- set opt(-ellipsis) 0
- set opt(-ellipsistext) {...}
- set opt(-overflow) 0
- array set opt [lrange $args 0 end-2]
-
-
- set len [string length $undertext]
- set overlen [string length $overtext]
- set diff [expr {$overlen - $len}]
-
- #puts stdout "====================>overtype: datalen:$len overlen:$overlen diff:$diff"
- #puts stdout "====================>overtype: data: $overtext"
- if {$diff > 0} {
- if {$opt(-overflow)} {
- return $overtext
- } else {
- if {$opt(-ellipsis)} {
- return [overtype::left [string range $overtext 0 [expr {$len -1}]] $opt(-ellipsistext)]
- } else {
- return [string range $overtext 0 [expr {$len -1}]]
- }
- }
- } else {
- return "$overtext[string range $undertext $overlen end]"
- }
- }
-
- }
-
- #considered using hex to make large numbers more compact for viewing in debug output - but it's not that much shorter and probably obscures more than it helps.
- proc hex2dec {largeHex} {
- #todo - use punk::lib::hex2dec - (scan supports ll so can do larger hex values directly)
- set res 0
- set largeHex [string map {_ {}} $largeHex]
- if {[string length $largeHex] <=7} {
- #scan can process up to FFFFFFF and does so quickly
- return [scan $largeHex %x]
- }
- foreach hexDigit [split $largeHex {}] {
- set new 0x$hexDigit
- set res [expr {16*$res + $new}]
- }
- return $res
- }
- proc dec2hex {decimalNumber} {
- format %4.4llX $decimalNumber
- }
-
- #punk::lib::trimzero
- proc trimzero {number} {
- set trimmed [string trimleft $number 0]
- if {[string length $trimmed] == 0} {
- set trimmed 0
- }
- return $trimmed
- }
- #todo - consider human numeric split
- #e.g consider SI suffixes k|KMGTPEZY in that order
-
- #in this context, for natural sorting - numeric segments don't contain underscores or other punctuation such as . - + etc.
- #review - what about unicode equivalents such as wide numerals \UFF10 to \UFF19? unicode normalization?
- proc split_numeric_segments {name} {
- set segments [list]
- while {[string length $name]} {
- if {[scan $name {%[0-9]%n} chunk len] == 2} {
- lappend segments $chunk
- set name [string range $name $len end]
- }
- if {[scan $name {%[^0-9]%n} chunk len] == 2} {
- lappend segments $chunk
- set name [string range $name $len end]
- }
- }
- return $segments
- }
-
- proc padleft {str count {ch " "}} {
- set val [string repeat $ch $count]
- append val $str
- set diff [expr {max(0,$count - [string length $str])}]
- set offset [expr {max(0,$count - $diff)}]
- set val [string range $val $offset end]
- }
-
-
- # Sqlite may have limited collation sequences available in default builds.
- # with custom builds - there may be others such as 'natsort' - see https://sqlite.org/forum/forumpost/e4dc6f3331
- # This is of limited use with the few builtin collations available in 2023 ie binary,nocase & rtrim
- # but may provide a quicker,flexible sort option, especially if/when more collation sequences are added to sqlite
- # There are also prebuilt packages such as sqlite3-icu which allows things like "SELECT icu_load_collation('en_AU', 'australian');"
- proc sort_sqlite {stringlist args} {
- package require sqlite3
-
-
- set args [check_flags -caller natsort_sqlite -defaults [list -db :memory: -collate nocase -winlike 0 -topchars "\uFFFF" -debug 0 -splitchars [list / . - _] -extras {all}] -values $args]
- set db [string trim [dict get $args -db]]
- set collate [string trim [dict get $args -collate]]
- set debug [string trim [dict get $args -debug]]
- set topchars [string trim [dict get $args -topchars]]
-
- set topdot [expr {"." in $topchars}]
- set topunderscore [expr {"_" in $topchars}]
-
-
- sqlite3 db_sort_basic $db
- set orderedlist [list]
- db_sort_basic eval [string map [list %collate% $collate] {create table sqlitesort(index0 text COLLATE %collate%, name text COLLATE %collate%)}]
- foreach nm $stringlist {
- set segments [split_numeric_segments $nm]
- set index ""
- set s 0
- foreach seg $segments {
- if {($s == 0) && ![string length [string trim $seg]]} {
- #don't index leading space
- } elseif {($s == 0) && ($topunderscore) && [string match _* [string trim $seg]]} {
- append index "[padleft "0" 5]-d -100 topunderscore "
- append index [string trim $seg]
- } elseif {($s == 0) && ($topdot) && [string match .* [string trim $seg]]} {
- append index "[padleft "0" 5]-d -50 topdot "
- append index [string trim $seg]
- } else {
- if {[string is digit [string trim $seg]]} {
- set basenum [trimzero [string trim $seg]]
- set lengthindex "[padleft [string length $basenum] 5]-d"
- append index "$lengthindex "
- #append index [padleft $basenum 40]
- append index $basenum
- } else {
- append index [string trim $seg]
- }
- }
- incr s
- }
- puts stdout ">>$index"
- db_sort_basic eval {insert into sqlitesort values($index,$nm)}
- }
- db_sort_basic eval [string map [list %collate% $collate] {select name from sqlitesort order by index0 COLLATE %collate% ASC, name COLLATE %collate% ASC }] {
- lappend orderedlist $name
- }
- db_sort_basic close
- return $orderedlist
- }
-
- proc get_leading_char_count {str char} {
- #todo - something more elegant? regex?
- set count 0
- foreach c [split $str "" ] {
- if {$c eq $char} {
- incr count
- } else {
- break
- }
- }
- return $count
- }
- proc stacktrace {} {
- set stack "Stack trace:\n"
- for {set i 1} {$i < [info level]} {incr i} {
- set lvl [info level -$i]
- set pname [lindex $lvl 0]
- append stack [string repeat " " $i]$pname
-
- if {![catch {info args $pname} pargs]} {
- foreach value [lrange $lvl 1 end] arg $pargs {
-
- if {$value eq ""} {
- if {$arg != 0} {
- info default $pname $arg value
- }
- }
- append stack " $arg='$value'"
- }
- } else {
- append stack " !unknown vars for $pname"
- }
-
- append stack \n
- }
- return $stack
- }
-
- proc get_char_count {str char} {
- #faster than lsearch on split for str of a few K
- expr {[tcl::string::length $str]-[tcl::string::length [tcl::string::map "$char {}" $str]]}
- }
-
- proc build_key {chunk splitchars topdict tagconfig debug} {
- variable stacktrace_on
- if {$stacktrace_on} {
- puts stderr "+++>[stacktrace]"
- }
-
- set index_map [list - "" _ ""]
- #e.g - need to maintain the order
- #a b.txt
- #a book.txt
- #ab.txt
- #abacus.txt
-
-
- set original_splitchars [dict get $tagconfig original_splitchars]
-
- # tag_dashes test moved from loop - review
- set tag_dashes 0
- if {![string length [dict get $tagconfig last_part_text_tag]]} {
- #winlike
- set tag_dashes 1
- }
- if {("-" ni $original_splitchars)} {
- set tag_dashes 1
- }
- if {$debug >= 3} {
- puts stdout "START build_key chunk : $chunk"
- puts stdout "START build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes"
- }
-
-
- ## index_map will have no effect if we've already split on the char anyway(?)
- #foreach m [dict keys $index_map] {
- # if {$m in $original_splitchars} {
- # dict unset index_map $m
- # }
- #}
-
- #if {![string length $chunk]} return
-
- set result ""
- if {![llength $splitchars]} {
- #no more structural splits - but we need to examine numeric/non-numeric segments at the lowest level.
- # we are at a leaf in the recursive split hierarchy
-
- set s "" ;#we never actually split on "" (unless that was put in splitchars.. but it probably shouldn't be)
- set parts [list $chunk] ;#important to treat as list or leading/trailing whitespace lost
-
-
- } else {
- set s [lindex $splitchars 0]
- if {"spudbucket$s" in "[split $chunk {}]"} {
- error "dead-branch spudbucket"
- set partindex [build_key $chunk [lrange $splitchars 1 end] $topdict $tagconfig $debug]
- if {[dict get $tagconfig showsplits]} {
- set pfx "(1${s}=)" ;# = sorts before _
- set partindex ${pfx}$partindex
- }
-
- return $partindex
- } else {
- set parts_below_index ""
-
- if {$s ni [split $chunk ""]} {
- #$s can be an empty string
- set parts [list $chunk]
- } else {
- set parts [split $chunk $s] ;#whitespace preserved - even if splitting on s that is not in string.
- }
- #assert - we have a splitchar $s that is in the chunk - so at least one part
- if {(![string length $s] || [llength $parts] == 0)} {
- error "buld_key assertion false empty split char and/or no parts"
- }
-
- set pnum 1 ;# 1 based for clarity of reading index in debug output
- set subpart_count [llength $parts]
-
- set sub_splits [lrange $splitchars 1 end] ;#pass same splitchars to each subpart
- foreach p $parts {
- set partindex [build_key $p $sub_splits $topdict $tagconfig $debug]
- set lastpart [expr {$pnum == $subpart_count}]
-
-
- #######################
- set showsplits [dict get $tagconfig showsplits]
- #split prefixing experiment - maybe not suitable for general use - as it affects sort order
- #note that pfx must be consistent until last one, no matter how many partnumbers there are in total.
- # we don't want to influence sort order before reaching end.
- #e.g for:
- #(1.=)...
- #(1._)...(2._)...(3.=)
- #(1._)...(2.=)
- #Note that this is probably more suitable for highly structure dependant sorts where the results are maybe less.. natural.
- if {$showsplits} {
- if {$lastpart} {
- set pfx "(${pnum}${s}_"
- #set pfx "(${pnum}${s}=)" ;# = sorts before _
- } else {
- set pfx "(${pnum}${s}_"
- }
- append parts_below_index $pfx
- }
- #######################
-
- if {$lastpart} {
- if {[string length $p] && [string is digit $p]} {
- set last_part_tag "<22${s}>"
- } else {
- set last_part_tag "<33${s}>"
- }
-
- set last_part_text_tag [dict get $tagconfig last_part_text_tag]
- #for -winlike 1 there is no tag configured. Windows explorer likes to put things in the order:
- # module-0.1.1.tm
- # module-0.1.1.2.tm
- # module-0.1.tm
- # arguably -winlike 0 is more natural/human
- # module-0.1.tm
- # module-0.1.1.tm
- # module-0.1.1.2.tm
-
- if {[string length $last_part_text_tag]} {
- #replace only the first text-tag (<30>) from the subpart_index
- if {[string match "<30?>*" $partindex]} {
- #give textual string index a specific tag for last part in split only. e.g <130> for lower than integers
- set partindex "<130>[string range $partindex 5 end]"
- }
- #append parts_below_index $last_part_tag
- }
- #set partindex $last_part_tag$partindex
-
-
- }
- append parts_below_index $partindex
-
-
-
- if {$showsplits} {
- if {$lastpart} {
- set suffix "${pnum}${s}=)" ;# = sorts before _
- } else {
- set suffix "${pnum}${s}_)"
- }
- append parts_below_index $suffix
- }
-
-
- incr pnum
- }
- append parts_below_index "" ;# don't add anything at the tail that may perturb sort order
-
- if {$debug >= 3} {
- set pad [string repeat " " 20]
- puts stdout "END build_key chunk : $chunk "
- puts stdout "END build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes"
- puts stdout "END build_key ret below_index: $parts_below_index"
- }
- return $parts_below_index
-
-
- }
- }
-
-
-
- #puts stdout ">>>chunk:'$chunk'<<< split-on:$s parts: '$parts' splitchars: $splitchars -topdict:$topdict"
-
-
-
-
-
- #if {$chunk eq ""} {
- # puts "___________________________________________!!!____"
- #}
- #puts stdout "-->chunk:$chunk $s parts:$parts"
-
- #puts stdout "---chunk:'$chunk' part:'$part' parts:'$parts' s:'$s'"
-
-
-
-
- set segments [split_numeric_segments $chunk] ;#!
- set stringindex ""
- set segnum 0
- foreach seg $segments {
- #puts stdout "=================---->seg:$seg segments:$segments"
- #-strict ?
- if {[string length $seg] && [string is digit $seg]} {
- set basenum [trimzero [string trim $seg]]
- set lengthindex "[padleft [string length $basenum] 4]d"
- #append stringindex "<20>$lengthindex $basenum $seg"
- } else {
- set c1 [string range $seg 0 0]
- #puts stdout "==============> c1'$c1' topdict: $topdict stringindex:$stringindex"
-
- if {$c1 in [dict keys $topdict]} {
- set tag [dict get $topdict $c1]
- #append stringindex "${tag}$c1"
- #set seg [string range $seg 1 end]
- }
- #textindex
- set leader "<30>"
- set idx $seg
- set idx [string trim $idx]
- set idx [string tolower $idx]
- set idx [string map $index_map $idx]
-
-
-
-
-
- #set the X-c count to match the length of the index - not the raw data
- set lengthindex "[padleft [string length $idx] 4]c"
-
- #append stringindex "${leader}$idx $lengthindex $texttail"
- }
- }
-
- if {[llength $parts] != 1} {
- error "build_key assertion fail llength parts != 1 parts:$parts"
- }
-
- set segtail_clearance_buffer " " ;#space to clear other split indicators if using showsplits
- set segtail $segtail_clearance_buffer
- append segtail "\["
- set grouping ""
- set pnum 0
- foreach p $parts {
- set sublen_list [list]
- set subsegments [split_numeric_segments $p]
- set i 0
-
- set partsorter ""
- foreach sub $subsegments {
- ##don't trim spaces here - it would be inconsistent. Some subs are pure whitespace - others have internal whitespace. e.g "a4 400b a b2" becomes "a 4 { } 400 {b a b} 2"
- #mapping away all whitespace would be consistent, but not necessarily desirable. If it's in the index_map it'll happen anyway - so we don't do it manually here except for evaluating conditions.
- set test_trim [string trim $sub]
- set str $sub
- set str [string tolower $str]
- set str [string map $index_map $str]
- if {[string length $test_trim] && [string is digit $test_trim]} {
- append partsorter [trimzero $str]
- } else {
- append partsorter "$str"
- }
- append partsorter
- }
-
-
- foreach sub $subsegments {
-
- if {[string length $sub] && [string is digit $sub]} {
- set basenum [trimzero [string trim $sub]]
- set subequivs $basenum
- set lengthindex "[padleft [string length $subequivs] 4]d "
- set idx "$lengthindex [padleft $basenum 10]" ;#todo - cycle through data and determine longest
- set tail [overtype::left [string repeat " " 10] $sub]
- #set tail ""
- } else {
- set idx ""
-
-
- set lookahead [lindex $subsegments $i+1]
- if {![string length $lookahead]} {
- set zeronum "[padleft 0 4]d0"
- } else {
- set zeronum ""
- }
- set subequivs $sub
- #set subequivs [string trim $subequivs]
- set subequivs [string tolower $subequivs]
- set subequivs [string map $index_map $subequivs]
-
- append idx $subequivs
- append idx $zeronum
-
- set idx $subequivs
-
-
- #
-
- set ch "-"
- if {$tag_dashes} {
- #puts stdout "____TAG DASHES"
- #winlike
- set numleading [get_leading_char_count $seg $ch]
- if {$numleading > 0} {
- set texttail "<31-leading[padleft $numleading 4]$ch>"
- } else {
- set texttail "<30>"
- }
- set numothers [expr {[get_char_count $seg $ch] - $numleading}]
- if {$debug >= 2} {
- puts stdout "____dashcount: [get_char_count $seg $ch] numothers: $numothers"
- }
- if {$numothers > 0} {
- append texttail "<31-others[padleft $numothers 4]$ch>"
- } else {
- append textail "<30>"
- }
- } else {
- set texttail "<30>"
- }
-
-
-
-
- #set idx $partsorter
- set tail ""
- #set tail [string tolower $sub] ;#raw
- #set tail $partsorter
- #append tail ":[string tolower $p]" ;#we need the whole part - even though it makes the index much larger. !todo - tagconfig switch to toggle case sensitive sorting
- }
-
- append grouping "$idx $tail|$s"
- incr i
- }
-
-
-
-
-
- if {$p eq ""} {
- # no subsegments..
- set zeronum "[padleft 0 4]d0"
- #append grouping "\u000$zerotail"
- append grouping ".$zeronum"
- }
-
- #append grouping |
- #append grouping $s
- #foreach len $sublen_list {
- # append segtail "<[padleft $len 3]>"
- #}
- incr pnum
- }
- set grouping [string trimright $grouping $s]
- append grouping "[padleft [llength $parts] 4]"
- append segtail $grouping
-
-
- #append segtail " <[padleft [llength $parts] 4]>"
-
- append segtail "\]"
-
-
- #if {[string length $seg] && [string is digit $seg]} {
- # append segtail "<20>"
- #} else {
- # append segtail "<30>"
- #}
- append stringindex $segtail
-
- incr segnum
-
-
-
-
- lappend indices $stringindex
-
- if {[llength $indices] > 1} {
- puts stderr "INDICES [llength $indices]: $stringindex"
- error "build_key assertion error deadconcept indices"
- }
-
- #topchar handling on splitter characters
- #set c1 [string range $chunk 0 0]
- if {$s in [dict keys $topdict]} {
- set tag [dict get $topdict $s]
- set joiner [string map [list ">" "$s>"] ${tag}]
- #we have split on this character $s so if the first part is empty string then $s was a leading character
- # we need to bring a tag out front for this, or it will be dominated by the leading sections-remaing tag
- # (since the empty string produces no tag of it's own - ?)
- if {[string length [lindex $parts 0]] == 0} {
- set prefix ${joiner}
- } else {
- set prefix ""
- }
- } else {
- #use standard character-data positioning tag if no override from topdict
- set joiner "<30J>$s"
- set prefix ""
- }
-
-
- set contentindex $prefix[join $indices $joiner]
- if {[string length $s]} {
- set split_indicator ""
- } else {
- set split_indicator ""
-
- }
- if {![string length $s]} {
- set s ~
- }
-
- #return "[overtype::left [string repeat { } 80] $contentindex][overtype::left [string repeat { } 10] [list $s $chunk]]"
- #return $contentindex$split_indicator
- #return [overtype::left [string repeat - 40] $contentindex]
-
- if {$debug >= 3} {
- puts stdout "END build_key chunk : $chunk"
- puts stdout "END build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes"
- puts stdout "END build_key ret contentidx : $contentindex"
- }
- return $contentindex
- }
-
- #----------------------------------------
- #line-processors - data always last argument - opts can be empty string
- #all processor should accept empty opts and ignore opts if they don't use them
- proc _lineinput_as_tcl1 {opts line} {
- set out ""
- foreach i $line {
- append out "$i "
- }
- set out [string range $out 0 end-1]
- return $out
- }
- #should be equivalent to above
- proc _lineinput_as_tcl {opts line} {
- return [concat {*}$line]
- }
- #will put extra tcl quoting if it was already tcl-shaped e.g text "a" -> {"a"}
- proc _lineoutput_as_tcl {opts line} {
- return [regexp -inline -all {\S+} $line]
- }
-
- proc _lineinput_as_raw {opts line} {
- return $line
- }
- proc _lineoutput_as_raw {opts line} {
- return $line
- }
-
- #words is opposite of tcl
- proc _lineinput_as_words {opts line} {
- #wordlike_parts
- return [regexp -inline -all {\S+} $line]
- }
- proc _lineoutput_as_words {opts line} {
- return [concat {*}$line]
- }
-
- #opts same as tcllib csv::split - except without the 'line' element
- #?-alternate? ?sepChar? ?delChar?
- proc _lineinput_as_csv {opts line} {
- package require csv
- if {[lindex $opts 0] eq "-alternate"} {
- return [csv::split -alternate $line {*}[lrange $opts 1 end]]
- } else {
- return [csv::split $line {*}$opts]
- }
- }
- #opts same as tcllib csv::join
- #?sepChar? ?delChar? ?delMode?
- proc _lineoutput_as_csv {opts line} {
- package require csv
- return [csv::join $line {*}$opts]
- }
- #----------------------------------------
- variable sort_flagspecs
- set sort_flagspecs [dict create {*}{
- -caller natsort::sort
- -return supplied|defaults
- } -defaults [list -collate nocase {*}{
- -winlike 0
- -splits "\uFFFF"
- -topchars {. _}
- -showsplits 1
- -sortmethod ascii
- -collate "\uFFFF"
- -inputformat raw
- -inputformatapply {index data}
- -inputformatoptions ""
- -outputformat raw
- -outputformatoptions ""
- -cols "\uFFFF"
- -debug 0
- -db ""
- -stacktrace 0
- -splits "\uFFFF"
- -showsplits 0
- }] {*}{
- -required {all}
- -extras {none}
- -commandprocessors {}
- }]
-
- proc sort {stringlist args} {
- #puts stdout "natsort::sort args: $args"
- variable debug
- variable sort_flagspecs
- if {![llength $stringlist]} return
- if {[llength $stringlist] == 1} {
- if {"-inputformat" ni $args && "-outputformat" ni $args} {
- return $stringlist
- }
- }
-
- #allow pass through of the check_flags flag -debugargs so it can be set by the caller
- set debugargs 0
- if {[set posn [lsearch $args -debugargs]] >=0} {
- if {$posn == [llength $args]-1} {
- #-debugargs at tail of list
- set debugargs 1
- } else {
- set debugargs [lindex $args $posn+1]
- }
- }
-
- #-return flagged|defaults doesn't work Review.
- #flagfilter global processor/allocator not working 2023-08
-
- set opts [check_flags {*}$sort_flagspecs -debugargs $debugargs -values $args]
-
- #we can only shortcircuit input list of single element at this point if there aren't non-default -inputformat or -outputformat transformations
- if {[llength $stringlist] == 1} {
- set is_basic 1
- foreach fname [list -inputformat -outputformat] {
- if {[dict get $sort_flagspecs -defaults $fname] ne [dict get $opts $fname]} {
- set is_basic 0
- break
- }
- }
- if {$is_basic} {
- return $stringlist
- }
- }
-
-
- set winlike [dict get $opts -winlike]
- set topchars [dict get $opts -topchars]
- set cols [dict get $opts -cols]
- set debug [dict get $opts -debug]
- set stacktrace [dict get $opts -stacktrace]
- set showsplits [dict get $opts -showsplits]
- set splits [dict get $opts -splits]
- set sortmethod [dict get $opts -sortmethod]
- set opt_collate [dict get $opts -collate]
- set opt_inputformat [dict get $opts -inputformat]
- set opt_inputformatapply [dict get $opts -inputformatapply]
- set opt_inputformatoptions [dict get $opts -inputformatoptions]
- set opt_outputformat [dict get $opts -outputformat]
- set opt_outputformatoptions [dict get $opts -outputformatoptions]
-
- if {$debug} {
- #dict unset opts -showsplits
- #dict unset opts -splits
- puts stdout "natsort::sort processed_args: $opts"
- if {$debug == 1} {
- puts stdout "natsort::sort - try also -debug 2, -debug 3"
- }
- }
-
- #set sortmethod "-dictionary" ;# sorts a2b before a001b - possibly other strangenesses that are hard to reason about
- switch -- $sortmethod {
- dictionary - ascii {
- set sortmethod "-$sortmethod"
- # -ascii is default for tcl lsort.
- }
- default {
- set sortmethod "-ascii"
- }
- }
-
- set allowed_collations [list nocase]
- if {$opt_collate ne "\uFFFF"} {
- if {$opt_collate ni $allowed_collations} {
- error "natsort::sort unknown value for -collate option. Only acceptable value(s): $allowed_collations"
- }
- set nocaseopt "-$opt_collate"
- } else {
- set nocaseopt ""
- }
- set allowed_inputformats [list tcl raw csv words]
- switch -- $opt_inputformat {
- tcl - raw - csv - words {}
- default {
- error "natsort::sort unknown value for -inputformat option. Only acceptable value(s): $allowed_inputformats"
- }
- }
- set allowed_outputformats [list tcl raw csv words]
- switch -- $opt_outputformat {
- tcl - raw - csv - words {}
- default {
- error "natsort::sort unknown value for -outputformat option. Only acceptable value(s): $allowed_outputformats"
- }
- }
-
- #
- set winsplits [list / . _]
- set commonsplits [list / . _ -]
- #set commonsplits [list]
-
- set tagconfig [dict create]
- dict set tagconfig last_part_text_tag "<19>"
- if {$winlike} {
- set splitchars $winsplits
- #windows explorer sorts leading spaces at the top - which doesn't seem very helpful for keeping things together - but the explorer doesn't seem able to create leading spaces anyway.
- set wintop [list "(" ")" { } {.} {_}] ;#windows specific order
- foreach t $topchars {
- if {$t ni $wintop} {
- lappend wintop $t
- }
- }
- set topchars $wintop
- dict set tagconfig last_part_text_tag ""
- } else {
- set splitchars $commonsplits
- }
- if {$splits ne "\uFFFF"} {
- set splitchars $splits
- }
- dict set tagconfig original_splitchars $splitchars
- dict set tagconfig showsplits $showsplits
-
- #create topdict
- set i 0
- set topdict [dict create]
- foreach c $topchars {
- incr i ;#start at 01 so that 00 reserved for final-split tag (allows x-0.1.txt to sort above x-0.1.1.txt by default. Use tagconfig to change, or choose -winlike 1 for explorer-like sorting)
- dict set topdict $c "<0$i>"
- }
- set keylist [list]
-
- switch -- $opt_inputformat {
- tcl {
- set lineinput_transform [list _lineinput_as_tcl $opt_inputformatoptions]
- }
- csv {
- set lineinput_transform [list _lineinput_as_csv $opt_inputformatoptions]
- }
- raw {
- set lineinput_transform [list _lineinput_as_raw $opt_inputformatoptions]
- }
- words {
- set lineinput_transform [list _lineinput_as_words $opt_inputformatoptions]
- }
- }
- switch -- $opt_outputformat {
- tcl {
- set lineoutput_transform [list _lineoutput_as_tcl $opt_outputformatoptions]
- }
- csv {
- set lineoutput_transform [list _lineoutput_as_csv $opt_outputformatoptions]
- }
- raw {
- set lineoutput_transform [list _lineoutput_as_raw $opt_outputformatoptions]
- }
- words {
- set lineoutput_transform [list _lineoutput_as_words $opt_outputformatoptions]
- }
- }
-
- if {("data" in $opt_inputformatapply) || ("index" in $opt_inputformatapply)} {
- if {$opt_inputformat eq "raw"} {
- set tf_stringlist $stringlist
- } else {
- set tf_stringlist [list]
- foreach v $stringlist {
- lappend tf_stringlist [{*}$lineinput_transform $v]
- }
- }
- if {"data" in $opt_inputformatapply} {
- set tf_data_stringlist $tf_stringlist
- } else {
- set tf_data_stringlist $stringlist
- }
- if {"index" in $opt_inputformatapply} {
- set tf_index_stringlist $tf_stringlist
- } else {
- set tf_index_stringlist $stringlist
- }
- } else {
- set tf_data_stringlist $stringlist
- set tf_index_stringlist $stringlist
- }
-
-
-
- if {$stacktrace} {
- puts stdout [natsort::stacktrace]
- set natsort::stacktrace_on 1
- }
- if {$cols eq "\uFFFF"} {
- set colkeys [lmap v $stringlist {}]
- } else {
- set colkeys [list]
- foreach v $tf_index_stringlist {
- set lineparts $v
- set k [list]
- foreach c $cols {
- lappend k [lindex $lineparts $c]
- }
- lappend colkeys [join $k "_"] ;#use a common-split char - Review
- }
- }
- #puts stdout "colkeys: $colkeys"
-
- if {$opt_inputformat eq "raw"} {
- #no inputformat was applied - can just use stringlist
- foreach value $stringlist ck $colkeys {
- set contentindex [build_key $value $splitchars $topdict $tagconfig $debug]
- set colindex [build_key $ck $splitchars $topdict $tagconfig $debug]
- lappend keylist ${colindex}-${contentindex}-$value ;#note: entire raw value used for final sort disambiguation (can be whitespace that was ignored in indexing)
- }
- } else {
- foreach keyinput $tf_index_stringlist datavalue $tf_data_stringlist ck $colkeys {
- #data may or may not have been transformed
- #column index may or may not have been built with transformed data
-
- set contentindex [build_key $keyinput $splitchars $topdict $tagconfig $debug]
- set colindex [build_key $ck $splitchars $topdict $tagconfig $debug]
- lappend keylist ${colindex}-${contentindex}-$datavalue ;#note: entire value used for final sort disambiguation (can be whitespace that was ignored in indexing)
- }
- }
- #puts stderr "keylist: $keylist"
-
- ###################################################################################################
- # Use the generated keylist to do the actual sorting
- # select either the transformed or raw data as the corresponding output
- ###################################################################################################
- if {[string length $nocaseopt]} {
- set sortcommand [list lsort $sortmethod $nocaseopt -indices $keylist]
- } else {
- set sortcommand [list lsort $sortmethod -indices $keylist]
- }
- if {$opt_outputformat eq "raw"} {
- #raw output means no further transformations - it doesn't mean there wasn't a transform applied on the input side
- #use the tf_data_stringlist in the output - which will be the same as the input stringlist if no input transform applied for data.
- #(Also - it may or may not have been *sorted* on transformed data depending on whether 'index' was in $opt_inputformatapply)
- foreach idx [{*}$sortcommand] {
- lappend result [lindex $tf_data_stringlist $idx]
- }
- } else {
- #we need to apply an output format
- #The data may or may not have been transformed at input
- foreach idx [{*}$sortcommand] {
- lappend result [{*}$lineoutput_transform [lindex $tf_data_stringlist $idx]]
- }
- }
- ###################################################################################################
-
-
-
-
-
- if {$debug >= 2} {
- set screen_width 250
- set max_val 0
- set max_idx 0
- ##### calculate colum widths
- foreach i [{*}$sortcommand] {
- set len_val [string length [lindex $stringlist $i]]
- if {$len_val > $max_val} {
- set max_val $len_val
- }
- set len_idx [string length [lindex $keylist $i]]
- if {$len_idx > $max_idx} {
- set max_idx $len_idx
- }
- }
- ####
- set l_width [expr {$max_val + 1}]
- set leftcol [string repeat " " $l_width]
- set r_width [expr {$screen_width - $l_width - 1}]
- set rightcol [string repeat " " $r_width]
- set str [overtype::left $leftcol RAW]
- puts stdout " $str Index with possibly transformed data at tail"
- foreach i [{*}$sortcommand] {
- #puts stdout "|d> [overtype::left $leftcol [lindex $stringlist $i] ] [lindex $keylist $i]"
- set index [lindex $keylist $i]
- set len_idx [string length $index]
- set rowcount [expr {$len_idx / $r_width}]
- if {($len_idx % $r_width) > 0} {
- incr rowcount
- }
- set rows [list]
- for {set r 0} {$r < $rowcount} {incr r} {
- lappend rows [string range $index 0 $r_width-$r]
- set index [string range $index $r_width end]
- }
-
- set r 0
- foreach idxpart $rows {
- if {$r == 0} {
- #use the untransformed stringlist
- set str [overtype::left $leftcol [lindex $stringlist $i]]
- } else {
- set str [overtype::left $leftcol ...]]
- }
- puts stdout " $str $idxpart"
- incr r
- }
- #puts stdout "|> '[lindex $stringlist $i]'"
- #puts stdout "|> [lindex $keylist $i]"
- }
-
- puts stdout "|debug> topdict: $topdict"
- puts stdout "|debug> splitchars: $splitchars"
- }
- return $result
- }
-
-
-
- #Note that although leading whitespace isn't a commonly used feature of filesystem names - it's possible at least on FreeBSD,windows and linux so we should try to handle it sensibly.
- proc sort_experiment {stringlist args} {
- package require sqlite3
-
- variable debug
- set args [check_flags -caller natsort::sort {*}{
- } -defaults [dict create -db :memory: -collate nocase -nullvalue "->NULL<" -winlike 0 -topchars [list] -debug 0] {*}{
- } -extras {all} {*}{
- } -values $args {*}{
- }
- ]
- set db [string trim [dict get $args -db]]
- set collate [string trim [dict get $args -collate]]
- set winlike [string trim [dict get $args -winlike]]
- set debug [string trim [dict get $args -debug]]
- set nullvalue [string trim [dict get $args -nullvalue]]
-
-
- set topchars [string trim [dict get $args -topchars]]
-
- set topdot [expr {"." in $topchars}]
- set topunderscore [expr {"_" in $topchars}]
-
-
- sqlite3 db_natsort2 $db
- #--
- #our table must handle the name with the greatest number of numeric/non-numeric splits.
- #This means a single list member with pathological naming e.g a1a1a1a1a1a1a1a1a1a1a1.txt could greatly extend the number of columns and indices and affect performance.
- #review: could be optimised to aggregate the tail into a single index, as the the extra columns won't assist in ordering, but make the table and query bigger.
- # we should probably determine the longest common sequence of splits in the input list and add only one more index for the segment after that.
- set maxsegments 0
- #--
- set prefix "idx"
-
- #note - there will be more columns in the sorting table than segments.
- # (a segment equals one of the numeric & non-numeric string portions returned from 'split_numeric_sgements')
- #---------------------------
- # consider
- # a123b.v1.2.txt
- # a123b.v1.3beta1.txt
- # these have the following segments:
- # a 123 b.v 1 . 2 .txt
- # a 123 b.v 1 . 3 beta 1 .txt
- #---------------------------
- # The first string has 7 segments (numbered 0 to 6)
- # the second string has 9 segments
- #
- # for example when the data has any elements in a segment position that are numeric (e.g 0001 123)
- # - then an index column with numeric equivalents will be created (e.g 0001 becomes 1), and any non-numeric values in that column will get mapped to a negative value (for special cases) or a high value such as NULL (with NULLS LAST sql support)
- #
- # when a segment
-
- #cycle through all strings - we cannot build tabledef as we go because the column type depends on whether all segments for a particular column are text vs int-equivalent.
- array set segmentinfo {}
- foreach nm $stringlist {
- set segments [split_numeric_segments $nm]
- if {![string length [string trim [lindex $segments 0]]]} {
- if {[string is digit [string trim [lindex $segments 1]]]} {
- #name is whitespace followed by a digit - special case - ignore the whitespace for numbers only. (whitespace still goes through to name column though)
- set segments [lrange $segments 1 end]
- }
- }
-
-
- set c 0 ;#start of index columns
- if {[llength $segments] > $maxsegments} {
- set maxsegments [llength $segments]
- }
- foreach seg $segments {
- set seg [string trim $seg]
- set column_exists [info exists segmentinfo($c,type)]
- if {[string is digit $seg]} {
- if {$column_exists} {
- #override it (may currently be text or int)
- set segmentinfo($c,type) "int"
- } else {
- #new column
- set segmentinfo($c,name) ${prefix}$c
- set segmentinfo($c,type) "int"
- }
- } else {
- #text never overrides int
- if {!$column_exists} {
- set segmentinfo($c,name) ${prefix}$c
- set segmentinfo($c,type) "text"
- }
- }
- incr c
- }
- }
- if {$debug} {
- puts stdout "Largest number of num/non-num segments in data: $maxsegments"
- #parray segmentinfo
- }
-
- #
- set tabledef ""
- set ordered_column_names [list]
- set ordered_segmentinfo_tags [lsort -dictionary [array names segmentinfo *]]
- foreach k $ordered_segmentinfo_tags {
- lassign [split $k ,] c tag
- if {$tag eq "type"} {
- set type [set segmentinfo($k)]
- if {$type eq "int"} {
- append tabledef "$segmentinfo($c,name) int,"
- } else {
- append tabledef "$segmentinfo($c,name) text COLLATE $collate,"
- }
- append tabledef "raw$c text COLLATE $collate,"
- lappend ordered_column_names $segmentinfo($c,name)
- lappend ordered_column_names raw$c ;#additional index column not in segmentinfo
- }
- if {$tag eq "name"} {
- #lappend ordered_column_names $segmentinfo($k)
- }
- }
- append tabledef "name text"
-
- #puts stdout "tabledef:$tabledef"
-
-
- db_natsort2 eval [string map [list %tabledef% $tabledef] {create table natsort(%tabledef%)}]
-
-
- foreach nm $stringlist {
- array unset intdata
- array set intdata {}
- array set rawdata {}
- #init array and build sql values string
- set sql_insert "insert into natsort values("
- for {set i 0} {$i < $maxsegments} {incr i} {
- set intdata($i) ""
- set rawdata($i) ""
- append sql_insert "\$intdata($i),\$rawdata($i),"
- }
- append sql_insert "\$nm" ;#don't manipulate name value in any way - e.g must leave all whitespace as the sort must return exactly the same elements as in the original list.
- append sql_insert ")"
-
- set segments [split_numeric_segments $nm]
- if {![string length [string trim [lindex $segments 0]]]} {
- if {[string is digit [string trim [lindex $segments 1]]]} {
- #name is whitespace followed by a digit - special case - ignore the whitespace for numbers only. (whitespace still goes through to name column though)
- set segments [lrange $segments 1 end]
- }
- }
- set values ""
- set c 0
- foreach seg $segments {
- if {[set segmentinfo($c,type)] eq "int"} {
- if {[string is digit [string trim $seg]]} {
- set intdata($c) [trimzero [string trim $seg]]
- } else {
- catch {unset intdata($c)} ;#set NULL - sorts last
- if {($c == 0) && ($topunderscore) && [string match _* [string trim $seg]]} {
- set intdata($c) -100
- }
- if {($c == 0) && ($topdot) && [string match .* [string trim $seg]]} {
- set intdata($c) -50
- }
- }
- set rawdata($c) [string trim $seg]
- } else {
- #pure text column
- #set intdata($c) [string trim $seg] ;#ignore leading/trailing whitespace - we sort first on trimmed version, then refine with the sort on rawdata index
- #catch {unset indata($c)}
- set indata($c) [string trim $seg]
- set rawdata($c) $seg
- }
- #set rawdata($c) [string trim $seg]#
- #set rawdata($c) $seg
- incr c
- }
- db_natsort2 eval $sql_insert
- }
-
- set orderedlist [list]
-
- if {$debug} {
- db_natsort2 eval {select * from pragma_table_info('natsort')} rowdata {
- parray rowdata
- }
- }
- set orderby "order by "
-
- foreach cname $ordered_column_names {
- if {[string match "idx*" $cname]} {
- append orderby "$cname ASC NULLS LAST,"
- } else {
- append orderby "$cname ASC,"
- }
- }
- append orderby " name ASC"
- #append orderby " NULLS LAST" ;#??
-
- #e.g "order by idx0 ASC, raw0 ASC, idx1 ASC .... name ASC"
- if {$debug} {
- puts stdout "orderby clause: $orderby"
- }
- db_natsort2 eval [string map [list %orderby% $orderby] {select * from natsort %orderby%}] rowdata {
- set line "- "
- #parray rowdata
- set columnnames $rowdata(*)
- #puts stdout "columnnames: $columnnames"
- #[lsort -dictionary [array names rowdata]
- append line "$rowdata(name) \n"
- foreach nm $columnnames {
- if {$nm ne "name"} {
- append line "$nm: $rowdata($nm) "
- }
- }
- #puts stdout $line
- #puts stdout "$rowdata(name)"
- lappend orderedlist $rowdata(name)
- }
-
- db_natsort2 close
- return $orderedlist
- }
-}
-
-
-#application section e.g this file might be linked from /usr/local/bin/natsort
-namespace eval natsort {
- namespace import ::flagfilter::check_flags
-
- proc called_directly_namematch {} {
- global argv0
- if {[info script] eq ""} {
- return 0
- }
- #see https://wiki.tcl-lang.org/page/main+script
- #trailing ... let's us resolve symlinks in last component of the path (could be something else like ___ but ... seems unlikely to collide with anything in the filesystem)
- if {[info exists argv0]
- &&
- [file dirname [file normalize [file join [info script] ...]]]
- eq
- [file dirname [file normalize [file join $argv0 ...]]]
- } {
- return 1
- } else {
- #puts stdout "norm info script: [file dirname [file normalize [file join [info script] ...]]]"
- #puts stdout "norm argv0 : [file dirname [file normalize [file join $argv0 ...]]]"
- return 0
- }
- }
- #Review issues around comparing names vs using inodes (esp with respect to samba shares)
- proc called_directly_inodematch {} {
- global argv0
-
- if {[info exists argv0]
- && [file exists [info script]] && [file exists $argv0]} {
- file stat $argv0 argv0Info
- file stat [info script] scriptInfo
- if {$argv0Info(ino) == 0 || $scriptInfo(ino) == 0 || $argv0Info(dev) == 0 || $scriptInfo(dev) == 0} {
- #vfs?
- #e.g //zipfs:/
- return 0
- }
- return [expr {$argv0Info(dev) == $scriptInfo(dev)
- && $argv0Info(ino) == $scriptInfo(ino)}]
- } else {
- return 0
- }
- }
-
- if {![interp issafe]} {
- set is_namematch [called_directly_namematch]
- set is_inodematch [called_directly_inodematch]
- ####
- #review - reliability of mechanisms to determine direct calls
- # we don't want application being called when being used as a library, but we need it to run if called directly or from symlinks etc
- #-- choose a policy and leave the others commented.
- #set is_called_directly $is_namematch
- #set is_called_directly $is_inodematch
-
- #puts "NATSORT: called_directly_namematch - $is_namematch"
- #puts "NATSORT: called_directly_inodematch - $is_inodematch"
- #flush stdout
-
- set is_called_directly [expr {$is_namematch || $is_inodematch}]
- #set is_called_directly [expr {$is_namematch && $is_inodematch}]
- ###
-
-
- #puts stdout "called_directly_name: [called_directly_namematch] called_directly_inode: [called_directly_inodematch]"
- } else {
- #safe interp
- set is_called_directly 0
- }
-
-
-
- proc test_pass_fail_message {pass {additional ""}} {
- variable test_fail_msg
- variable test_pass_msg
- if {$pass} {
- puts stderr $test_pass_msg
- } else {
- puts stderr $test_fail_msg
- }
- puts stderr $additional
- }
-
- variable test_fail_msg "XXXXXXXXXXXX FAIL XXXXXXXXXXXXX"
- variable test_pass_msg "------------ PASS -------------"
- proc test_sort_1 {args} {
- package require struct::list
- puts stderr "---$args"
- set args [check_flags -caller natsort:test_sort_1 -defaults [list -collate nocase -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] -values $args]
-
- puts stderr "test_sort_1 got args: $args"
-
- set unsorted_input {
- 2.2.2
- 2.2.2.2
- 1a.1.1
- 1a.2.1.1
- 1.12.1
- 1.2.1.1
- 1.02.1.1
- 1.002b.1.1
- 1.1.1.2
- 1.1.1.1
- }
- set input {
-1.1.1
-1.1.1.2
-1.002b.1.1
-1.02.1.1
-1.2.1.1
-1.12.1
-1a.1.1
-1a.2.1.1
-2.2.2
-2.2.2.2
- }
-
- set sorted [natsort::sort $input {*}$args]
- set is_match [struct::list equal $input $sorted]
-
- set msg "windows-explorer order"
-
- test_pass_fail_message $is_match $msg
- puts stdout [string repeat - 40]
- puts stdout INPUT
- puts stdout [string repeat - 40]
- foreach item $input {
- puts stdout $item
- }
- puts stdout [string repeat - 40]
- puts stdout OUTPUT
- puts stdout [string repeat - 40]
- foreach item $sorted {
- puts stdout $item
- }
- test_pass_fail_message $is_match $msg
- return [expr {!$is_match}]
- }
- proc test_sort_showsplits {args} {
- package require struct::list
-
- set args [check_flags -caller natsort:test_sort_1 {*}{
- } -defaults [list -collate nocase -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 1 ] {*}{
- } -extras {all} {*}{
- } -values $args {*}{
- }
- ]
-
- set input1 {
- a-b.txt
- a.b.c.txt
- b.c-txt
- }
-
-
- set input2 {
- a.b.c.txt
- a-b.txt
- b.c-text
- }
-
- foreach {msg testlist } [list "custom-order" $input1 "windows-explorer (should work with -winlike 1)" $input2] {
- set sorted [natsort::sort $testlist {*}$args]
- set is_match [struct::list equal $testlist $sorted]
-
- test_pass_fail_message $is_match $msg
- puts stderr "INPUT"
- puts stderr "[string repeat - 40]"
- foreach item $testlist {
- puts stdout $item
- }
- puts stderr "[string repeat - 40]"
- puts stderr "OUTPUT"
- puts stderr "[string repeat - 40]"
- foreach item $sorted {
- puts stdout $item
- }
-
- test_pass_fail_message $is_match $msg
- }
-
- #return [expr {!$is_match}]
-
- }
-
- #tcl proc dispatch order - non flag items up front
- #trailing flags are paired even if supplied as solo flags e.g -l becomes -l 1
- proc commandline_ls {args} {
- set operands [list]
- set posn 0
- foreach a $args {
- if {![string match -* $a]} {
- lappend operands $a
- } else {
- set flag1_posn $posn
- break
- }
- incr posn
- }
- set args [lrange $args $flag1_posn end]
-
-
- set debug 0
- set posn [lsearch $args -debug]
- if {$posn > 0} {
- if {[lindex $args $posn+1]} {
- set debug [lindex $args $posn+1]
- }
- }
- if {$debug} {
- puts stderr "|debug>commandline_ls got $args"
- }
-
- #if first operand not supplied - replace it with current working dir
- if {[lindex $operands 0] eq "\uFFFF"} {
- lset operands 0 [pwd]
- }
-
- set targets [list]
- foreach op $operands {
- if {$op ne "\uFFFF"} {
- set opchars [split [file tail $op] ""]
- if {"?" in $opchars || "*" in $opchars} {
- lappend targets $op
- } else {
- #actual file or dir
- set targetitem $op
- set targetitem [file normalize $op]
- if {![file exists $targetitem]} {
- if {$debug} {
- puts stderr "|debug>commandline_ls Unable to access path '$targetitem'"
- }
- }
- lappend targets $targetitem
- if {$debug} {
- puts stderr "|debug>commandline_ls listing for $targetitem"
- }
- }
- }
- }
- set args [check_flags -caller commandline_ls {*}{
- -return flagged|defaults
- -debugargs 0
- } -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 -algorithm sort] {*}{
- -required {all}
- -extras {all}
- -soloflags {-v -l}
- -commandprocessors {}
- } -values $args {*}{
- }]
- if {$debug} {
- puts stderr "|debug>args: $args"
- }
-
-
- set algorithm [dict get $args -algorithm]
- dict unset args -algorithm
-
- set allfolders [list]
- set allfiles [list]
- foreach item $targets {
- if {[file exists $item]} {
- if {[file type $item] eq "directory"} {
- set dotfolders [glob -nocomplain -directory $item -type {d} -tail .*]
- set folders [glob -nocomplain -directory $item -type {d} -tail *]
- set allfolders [concat $allfolders $dotfolders $folders]
-
- set dotfiles [glob -nocomplain -directory $item -type {f} -tail .*]
- set files [glob -nocomplain -directory $item -type {f} -tail *]
- set allfiles [concat $allfiles $dotfiles $files]
- } else {
- #file (or link?)
- set files [glob -nocomplain -directory [file dirname $item] -tail [file tail $item]]
- set allfiles [concat $allfiles $files]
- }
- } else {
- set folders [glob -nocomplain -directory $item -type {d} -tail [file tail $item]]
- set allfolders [concat $allfolders $folders]
- set files [glob -nocomplain -directory [file dirname $item] -tail [file tail $item]]
- set allfiles [concat $allfiles $files]
- }
- }
-
-
- set sorted_folders [natsort::sort $allfolders {*}$args]
- set sorted_files [natsort::sort $allfiles {*}$args]
-
- foreach fold $sorted_folders {
- puts stdout $fold
- }
- foreach file $sorted_files {
- puts stdout $file
- }
-
- return "-- ok printed to stdout [llength $sorted_folders] folders and [llength $sorted_files] files --"
- }
-
- package require argp
- argp::registerArgs commandline_test {
- { -showsplits boolean 0}
- { -stacktrace boolean 0}
- { -debug boolean 0}
- { -winlike boolean 0}
- { -db string ":memory:"}
- { -collate string "nocase"}
- { -algorithm string "sort"}
- { -topchars string "\uFFFF"}
- { -testlist string {10 1 30 3}}
- }
- argp::setArgsNeeded commandline_test {-stacktrace}
- proc commandline_test {test args} {
- variable testlist
- puts stdout "commandline_test got $args"
- argp::parseArgs opts
- puts stdout "commandline_test got [array get opts]"
- set args [check_flags -caller natsort_commandline {*}{
- } -return flagged|defaults {*}{
- } -defaults [list -db :memory: -collate nocase -testlist $testlist -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] {*}{
- } -values $args {*}{
- }
- ]
-
- if {[string tolower $test] in [list "1" "true"]} {
- set test "sort"
- } else {
- if {![llength [info commands $test]]} {
- error "test $test not found"
- }
- }
- dict unset args -test
- set stacktrace [dict get $args -stacktrace]
- # dict unset args -stacktrace
-
- set argtestlist [dict get $args -testlist]
- dict unset args -testlist
-
-
- set debug [dict get $args -debug]
-
- set collate [dict get $args -collate]
- set db [dict get $args -db]
- set winlike [dict get $args -winlike]
- set topchars [dict get $args -topchars]
-
-
- puts stderr "|test>-----start natsort::$test--- input list size : [llength $argtestlist]"
- #set resultlist [$test $argtestlist -db $db -collate $collate -topchars $topchars -winlike $winlike]
- set resultlist [$test $argtestlist {*}$args]
- foreach nm $resultlist {
- puts stdout $nm
- }
- puts stdout "|test>-----end natsort::$test--- sorted list size: [llength $resultlist]"
- return "test end"
- }
- proc commandline_runtests {runtests args} {
- set argvals [check_flags {*}{
- } -caller commandline_runtests {*}{
- } -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits "\uFFFF" -runtests 1] {*}{
- } -values $args {*}{
- }
- ]
-
- puts stderr "runtests args: $argvals"
-
- #set runtests [dict get $argvals -runtests]
- dict unset argvals -runtests
- dict unset argvals -algorithm
-
- puts stderr "runtests args: $argvals"
- #exit 0
-
- set test_prefix "::natsort::test_sort_"
-
- if {$runtests eq "1"} {
- set runtests "*"
- }
-
-
- set testcommands [info commands ${test_prefix}${runtests}]
- if {![llength $testcommands]} {
- puts stderr "No test commands matched -runtests argument '$runtests'"
- puts stderr "Use 1 to run all tests"
- set alltests [info commands ${test_prefix}*]
- puts stderr "Valid tests are:"
-
- set prefixlen [string length $test_prefix]
- foreach t $alltests {
- set shortname [string range $t $prefixlen end]
- puts stderr "$t = -runtests $shortname"
- }
-
- } else {
- foreach cmd $testcommands {
- puts stderr [string repeat - 40]
- puts stderr "calling $cmd with args: '$argvals'"
- puts stderr [string repeat - 40]
- $cmd {*}$argvals
- }
- }
- exit 0
- }
- proc help {args} {
- puts stdout "natsort::help got '$args'"
- return "Help not implemented"
- }
- proc natsort_pipe {args} {
- #PIPELINE to take input list on stdin and write sorted list to stdout
- #strip - from arglist
- #set args [check_flags -caller natsort_pipeline {*}{
- # } -return all {*}{
- # } -defaults [list -db :memory: -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] {*}{
- # } -values $args {*}{
- # }
- #]
-
-
- set debug [dict get $args -debug]
- if {$debug} {
- puts stderr "|debug> natsort_pipe got args:'$args'"
- }
- set algorithm [dict get $args -algorithm]
- dict unset args -algorithm
-
- set proclist [info commands ::natsort::sort*]
- set algos [list]
- foreach p $proclist {
- lappend algos [namespace tail $p]
- }
- if {$algorithm ni [list {*}$proclist {*}$algos]} {
- do_error "valid sort mechanisms: $algos" 2
- }
-
- set input_list [list]
- while {![eof stdin]} {
- if {[gets stdin line] > 0} {
- lappend input_list $line
- } else {
- if {[eof stdin]} {
-
- } else {
- after 10
- }
- }
- }
-
- if {$debug} {
- puts stderr "|debug> received [llength $input_list] list elements"
- }
-
- set resultlist [$algorithm $input_list {*}$args]
- if {$debug} {
- puts stderr "|debug> returning [llength $resultlist] list elements"
- }
- foreach r $resultlist {
- puts stdout $r
- }
- #exit 0
-
- }
- if {($is_called_directly)} {
- set cmdprocessors {
- {helpfinal {match "^help$" dispatch natsort::help}}
- {helpfinal {sub -topic default "NONE"}}
- }
- #set args [check_flags {*}{
- # -caller test1
- # -debugargs 2
- # -return arglist
- # } -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] {*}{
- # -required {none}
- # -extras {all}
- # } -commandprocessors $cmdprocessors {*}{
- # } -values $::argv {*}{
- #}]
- interp alias {} do_filter {} ::flagfilter::check_flags
-
- #mashopts are generally single-letter opts that can be run together e.g -l -d as -ld
- set cmdprocessors {
- {helpcmd {match "^help$" dispatch natsort::help singleopts {-v}}}
- {helpcmd {sub -operand default \uFFFF singleopts {-l}}}
- {lscmd {match "^ls$" dispatch natsort::commandline_ls dispatchtype tcl dispatchglobal 1 mashopts {-l -a} singleopts {-l -a} pairopts {} longopts {--color=always}}}
- {lscmd {sub dir default "\uFFFF"}}
- {lscmd {sub dir2 default "\uFFFF"}}
- {lscmd {sub dir3 default "\uFFFF"}}
- {lscmd {sub dir4 default "\uFFFF"}}
- {lscmd {sub dir5 default "\uFFFF"}}
- {lscmd {sub dir6 default "\uFFFF"}}
- {runtests {match "^-tests$" dispatch natsort::commandline_runtests singleopts {-l}}}
- {runtests {sub testname default "1" singleopts {-l}}}
- {pipecmd {match "^-$" dispatch natsort::natsort_pipe dispatchtype tcl}}
- }
- set arglist [do_filter {*}{
- -debugargs 0
- -debugargsonerror 2
- -caller cline_dispatch1
- -return all
- -soloflags {-v -x}
- } -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] {*}{
- -required {all}
- -extras {all}
- } -commandprocessors $cmdprocessors {*}{
- } -values $::argv {*}{
- }]
-
-
- #mashopts are generally single-letter opts that can be run together e.g -l -d as -ld
- set cmdprocessors {
- {testcmd {match "^test$" dispatch natsort::commandline_test singleopts {-l}}}
- {testcmd {sub testname default "1" singleopts {-l}}}
- }
- set arglist [check_flags {*}{
- -debugargs 0
- -caller cline_dispatch2
- -return all
- -soloflags {-v -l}
- } -defaults [list -collate nocase -algorithm sort -testlist "1 2 3 10" -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] {*}{
- -required {all}
- -extras {all}
- } -commandprocessors $cmdprocessors {*}{
- } -values $::argv {*}{
- }
- ]
-
-
-
-
- #set cmdprocessors [list]
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors $cmdprocessors -values $::argv ]
-
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {-cmd {-cmd -cmdarg1 -default "."} {-cmd -cmdarg2 -default j}} -values $::argv ]
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {{-cmd -default help} {-cmd -cmdarg1 -default "."} {-cmd -cmdarg2 -default j}} -values $::argv ]
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {ls {ls lsdir -default "\uFFFF"}} -values $::argv ]
-
- puts stderr "natsort directcall exit"
- flush stderr
- exit 0
-
- if {$::argc} {
-
- }
- }
-}
-
-
-package provide natsort [namespace eval natsort {
- variable version
- set version 0.1.1.6
-}]
-
-
diff --git a/src/bootsupport/modules/oolib-0.1.2.tm b/src/bootsupport/modules/oolib-0.1.2.tm
deleted file mode 100644
index 858c61cd..00000000
--- a/src/bootsupport/modules/oolib-0.1.2.tm
+++ /dev/null
@@ -1,201 +0,0 @@
-#JMN - api should be kept in sync with package patternlib where possible
-#
-package provide oolib [namespace eval oolib {
- variable version
- set version 0.1.2
-}]
-
-namespace eval oolib {
- oo::class create collection {
- variable o_data ;#dict
- #variable o_alias
- constructor {} {
- set o_data [dict create]
- }
- method info {} {
- return [dict info $o_data]
- }
- method count {} {
- return [dict size $o_data]
- }
- method isEmpty {} {
- expr {[dict size $o_data] == 0}
- }
- method names {{globOrIdx {}}} {
- if {[llength $globOrIdx]} {
- if {[string is integer -strict $globOrIdx]} {
- set idx $globOrIdx
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- if {[catch {lindex [dict keys $o_data] $idx} result]} {
- error "[self object] no such index : '$idx'"
- } else {
- return $result
- }
- } else {
- #glob
- return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx]
- }
- } else {
- return [dict keys $o_data]
- }
- }
- #like names but without globbing
- method keys {} {
- dict keys $o_data
- }
- method key {{posn 0}} {
- if {$posn < 0} {
- set posn "end-[expr {abs($posn + 1)}]"
- }
- if {[catch {lindex [dict keys $o_data] $posn} result]} {
- error "[self object] no such index : '$posn'"
- } else {
- return $result
- }
- }
- method hasKey {key} {
- dict exists $o_data $key
- }
- method get {} {
- return $o_data
- }
- method items {} {
- return [dict values $o_data]
- }
- method item {key} {
- if {[string is integer -strict $key]} {
- if {$key >= 0} {
- set valposn [expr {(2*$key) +1}]
- return [lindex $o_data $valposn]
- } else {
- set key "end-[expr {abs($key + 1)}]"
- return [lindex $o_data $key]
- #return [lindex [dict keys $o_data] $key]
- }
- }
- if {[dict exists $o_data $key]} {
- return [dict get $o_data $key]
- }
- }
- #inverse lookup
- method itemKeys {value} {
- set value_indices [lsearch -all [dict values $o_data] $value]
- set keylist [list]
- foreach i $value_indices {
- set idx [expr {(($i + 1) *2) -2}]
- lappend keylist [lindex $o_data $idx]
- }
- return $keylist
- }
- method search {value args} {
- set matches [lsearch {*}$args [dict values $o_data] $value]
- if {"-inline" in $args} {
- return $matches
- } else {
- set keylist [list]
- foreach i $matches {
- set idx [expr {(($i + 1) *2) -2}]
- lappend keylist [lindex $o_data $idx]
- }
- return $keylist
- }
- }
- #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists?
- #review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well?
- #method alias {newAlias existingKeyOrAlias} {
- # if {[string is integer -strict $newAlias]} {
- # error "[self object] collection key alias cannot be integer"
- # }
- # if {[string length $existingKeyOrAlias]} {
- # set o_alias($newAlias) $existingKeyOrAlias
- # } else {
- # unset o_alias($newAlias)
- # }
- #}
- #method aliases {{key ""}} {
- # if {[string length $key]} {
- # set result [list]
- # foreach {n v} [array get o_alias] {
- # if {$v eq $key} {
- # lappend result $n $v
- # }
- # }
- # return $result
- # } else {
- # return [array get o_alias]
- # }
- #}
- ##if the supplied index is an alias, return the underlying key; else return the index supplied.
- #method realKey {idx} {
- # if {[catch {set o_alias($idx)} key]} {
- # return $idx
- # } else {
- # return $key
- # }
- #}
- method add {value key} {
- if {[string is integer -strict $key]} {
- error "[self object] collection key must not be an integer. Use another structure if integer keys required"
- }
- if {[dict exists $o_data $key]} {
- error "[self object] col_processors object error: key '$key' already exists in collection"
- }
- dict set o_data $key $value
- return [expr {[dict size $o_data] - 1}] ;#return index of item
- }
- method remove {idx {endRange ""}} {
- if {[string length $endRange]} {
- error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time"
- }
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx+1)}]"
- }
- set key [lindex [dict keys $o_data] $idx]
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- if {$posn < 0} {
- error "[self object] no such index: '$idx' in this collection"
- }
- }
- dict unset o_data $key
- return
- }
- method clear {} {
- set o_data [dict create]
- return
- }
- method reverse_the_collection {} {
- #named slightly obtusely because reversing the data when there may be references held is a potential source of bugs
- #the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy.
- #todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations.
- set dictnew [dict create]
- foreach k [lreverse [dict keys $o_data]] {
- dict set dictnew $k [dict get $o_data $k]
- }
- set o_data $dictnew
- return
- }
- #review - cmd as list vs cmd as script?
- method map {cmd} {
- set seed [list]
- dict for {k v} $o_data {
- lappend seed [uplevel #0 [list {*}$cmd $v]]
- }
- return $seed
- }
- method objectmap {cmd} {
- set seed [list]
- dict for {k v} $o_data {
- lappend seed [uplevel #0 [list $v {*}$cmd]]
- }
- return $seed
- }
- }
-
-}
-
diff --git a/src/bootsupport/modules/opunk/console-0.1.0.tm b/src/bootsupport/modules/opunk/console-0.1.0.tm
deleted file mode 100644
index bf940c11..00000000
--- a/src/bootsupport/modules/opunk/console-0.1.0.tm
+++ /dev/null
@@ -1,361 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt
-# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) 2026
-#
-# @@ Meta Begin
-# Application opunk::console 0.1.0
-# Meta platform tcl
-# Meta license BSD
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-package require Tcl 8.6-
-
-package require voo
-package require punk::args
-
-#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
-#belong to one responding endpoint, together with settled capability facts about that endpoint.
-#
-#Motivation (see punk::console get_size / get_ansi_response_payload history):
-#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
-#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
-#replies land in some other process's input. A console object makes the pairing an explicit,
-#construction-time property instead of a per-query-site guess.
-#
-#Design notes:
-# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
-# are anchored at well-known namespace variables (::opunk::console::instances::) and the value
-# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
-# the lowercase ::opunk::console wrapper procs.
-# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so channel
-# environments that respond like terminals but aren't platform consoles can subclass
-# (-extends ::opunk::Console) and override the capability/size methods, with existing holders of
-# console values dispatching correctly.
-# - This first cut is standalone and passive: capability detection is heuristic (channel options,
-# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
-# query probing arrives with punk::console integration (which will delegate here rather than the
-# class depending on punk::console).
-
-tcl::namespace::eval ::opunk::console {
- variable PUNKARGS
-
- #well-known cooperative store for bytes consumed by probe reads on pipe-like channels
- #dict keyed by channel name -> list of chunks. Readers taking over a channel should consume
- #and clear any entry for that channel first. (analogous to punk::console::input_chunks_waiting)
- variable waiting_chunks
- if {![info exists waiting_chunks]} {
- set waiting_chunks [dict create]
- }
-}
-
-voo::class ::opunk::Console -virtual {
- private {
- #o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
- #and matches tclOO-style implementations elsewhere for easier code movement).
- #Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
- string_t o_in stdin
- string_t o_out stdout
- string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
- list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
- dict_t o_default_size [dict create columns 80 rows 24]
- }
- public {
- #settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
- #public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
- #::opunk::console can settle it without reaching into private my.* accessors.
- int_t o_can_respond -1
-
- method in {} {
- my.get.o_in $this
- }
- method out {} {
- my.get.o_out $this
- }
- method channels {} {
- list [my.get.o_in $this] [my.get.o_out $this]
- }
- method terminal_class {} {
- my.get.o_terminal_class $this
- }
- method default_size {} {
- my.get.o_default_size $this
- }
-
- #Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
- #A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
- #get a non-blocking 1-byte probe. A probed byte is preserved in ::opunk::console::waiting_chunks
- #for cooperating readers. Console/tty channels are never probed (must not consume a keystroke).
- method at_eof {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set in [my.get.o_in $this]
- if {[catch {chan eof $in} is_eof]} {
- return 1 ;#closed/invalid channel - unusable
- }
- if {$is_eof} {
- return 1
- }
- if {[catch {chan configure $in} conf]} {
- return 1
- }
- if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
- return 0
- }
- if {[catch {
- set prior_blocking [dict get $conf -blocking]
- chan configure $in -blocking 0
- set probe [read $in 1]
- chan configure $in -blocking $prior_blocking
- }]} {
- return 1
- }
- if {$probe ne ""} {
- dict lappend ::opunk::console::waiting_chunks $in $probe
- }
- return [chan eof $in]
- }
-
- #Heuristic: could the input channel be connected to a terminal able to answer queries?
- #Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
- #terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
- #negatives there at the cost of false positives for genuinely piped-but-open input.
- method is_console_or_tty {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set in [my.get.o_in $this]
- if {[catch {chan configure $in} conf]} {
- return 0
- }
- if {[dict exists $conf -inputmode]} {
- return 1
- }
- if {[dict exists $conf -mode]} {
- return 1
- }
- if {[at_eof $this]} {
- return 0
- }
- if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
- return 1
- }
- return 0
- }
-
- #Response capability: settled value if known, else computed heuristically (without settling -
- #this method is pure; use ::opunk::console::can_respond to settle an anchored instance).
- #TODO: active probe query (device attributes with timeout) when punk::console query machinery
- #is integrated - heuristics can then be replaced by one settling probe at first use.
- method can_respond {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set settled [get.o_can_respond $this]
- if {$settled != -1} {
- return $settled
- }
- return [is_console_or_tty $this]
- }
-
- #Size of the console: chan -winsize on the output channel when available (fast, no query),
- #else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
- #this standalone class - they arrive via punk::console integration and subclasses.
- method size {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set out [my.get.o_out $this]
- if {![catch {chan configure $out} oconf]} {
- if {[dict exists $oconf -winsize]} {
- lassign [dict get $oconf -winsize] cols lines
- if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
- return [dict create columns $cols rows $lines]
- }
- }
- }
- return [my.get.o_default_size $this]
- }
- }
-
- constructor {in out} {
- #new() supplies the class defaults including the virtual class tag at slot 0
- set obj [new()]
- my.set.o_in obj $in
- my.set.o_out obj $out
- return $obj
- }
-}
-
-#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
-namespace eval ::opunk::Console {
- namespace ensemble create
-}
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Anchored instances - the 'consoles are entities' layer
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-tcl::namespace::eval ::opunk::console {
- tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
-
- namespace eval instances {
- #one variable per anchored console instance - the canonical location of each console object.
- #Access via the sibling procs; methods needing to mutate an instance go through these anchors.
- }
-
- proc create {name in out} {
- if {![string is wordchar -strict $name]} {
- error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
- }
- set [namespace current]::instances::$name [::opunk::Console::new $in $out]
- return [namespace current]::instances::$name
- }
- proc instancevar {name} {
- set v [namespace current]::instances::$name
- if {![info exists $v]} {
- error "opunk::console: no anchored console instance named '$name' (known: [names])"
- }
- return $v
- }
- proc instance {name} {
- set [instancevar $name]
- }
- proc names {} {
- set result [list]
- foreach v [info vars [namespace current]::instances::*] {
- lappend result [namespace tail $v]
- }
- return $result
- }
- proc forget {name} {
- unset [instancevar $name]
- return
- }
-
- #settle-and-cache response capability on the anchored instance.
- #The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
- proc can_respond {name} {
- upvar #0 [instancevar $name] obj
- set settled [::opunk::Console::get.o_can_respond $obj]
- if {$settled != -1} {
- return $settled
- }
- set r [::opunk::Console::can_respond $obj]
- ::opunk::Console::set.o_can_respond obj $r
- return $r
- }
- proc size {name} {
- ::opunk::Console::size [instance $name]
- }
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-# == === === === === === === === === === === === === === ===
-# Sample 'about' function with punk::args documentation
-# == === === === === === === === === === === === === === ===
-tcl::namespace::eval ::opunk::console {
- variable PUNKARGS
- variable PUNKARGS_aliases
-
- lappend PUNKARGS [list {
- @id -id "(package)opunk::console"
- @package -name "opunk::console" -help\
- "voo class ::opunk::Console representing a console as an in/out channel pair with settled
- response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
- }]
-
- namespace eval argdoc {
- proc package_name {} {
- return opunk::console
- }
- proc about_topics {} {
- set topic_funs [info commands [namespace current]::get_topic_*]
- set about_topics [list]
- foreach f $topic_funs {
- set tail [namespace tail $f]
- lappend about_topics [string range $tail [string length get_topic_] end]
- }
- return [lsort $about_topics]
- }
- proc default_topics {} {return [list Description *]}
-
- proc get_topic_Description {} {
- punk::args::lib::tstr [string trim {
- package opunk::console
- voo class for consoles as channel-pair objects with anchored canonical instances.
- } \n]
- }
- proc get_topic_License {} {
- return "BSD"
- }
- proc get_topic_Version {} {
- return "$::opunk::console::version"
- }
- proc get_topic_Contributors {} {
- set authors {{"Julian Noble" "julian@precisium.com.au"}}
- set contributors ""
- foreach a $authors {
- append contributors $a \n
- }
- if {[string index $contributors end] eq "\n"} {
- set contributors [string range $contributors 0 end-1]
- }
- return $contributors
- }
- proc get_topic_classes {} {
- punk::args::lib::tstr -return string {
- opunk::Console "virtual class for consoles as in/out channel pairs"
- }
- }
- }
-
- set overrides [dict create]
- dict set overrides @id -id "::opunk::console::about"
- dict set overrides @cmd -name "opunk::console::about"
- dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
- About opunk::console
- }] \n]
- dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
- dict set overrides topic -choicerestricted 1
- dict set overrides topic -default [opunk::console::argdoc::default_topics]
- set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
- lappend PUNKARGS [list $newdef]
- proc about {args} {
- package require punk::args
- set argd [punk::args::parse $args withid ::opunk::console::about]
- lassign [dict values $argd] _leaders opts values _received
- punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
- }
-}
-# end of sample 'about' function
-# == === === === === === === === === === === === === === ===
-
-
-# -----------------------------------------------------------------------------
-# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
-# -----------------------------------------------------------------------------
-namespace eval ::punk::args::register {
- #use fully qualified so 8.6 doesn't find existing var in global namespace
- lappend ::punk::args::register::NAMESPACES ::opunk::console ::opunk::Console
-}
-# -----------------------------------------------------------------------------
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide opunk::console [tcl::namespace::eval ::opunk::console {
- variable pkg opunk::console
- variable version
- set version 0.1.0
-}]
-return
diff --git a/src/bootsupport/modules/opunk/console-0.1.1.tm b/src/bootsupport/modules/opunk/console-0.1.1.tm
deleted file mode 100644
index a71562df..00000000
--- a/src/bootsupport/modules/opunk/console-0.1.1.tm
+++ /dev/null
@@ -1,361 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt
-# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) 2026
-#
-# @@ Meta Begin
-# Application opunk::console 0.1.1
-# Meta platform tcl
-# Meta license BSD
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-package require Tcl 8.6-
-
-package require voo
-package require punk::args
-
-#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
-#belong to one responding endpoint, together with settled capability facts about that endpoint.
-#
-#Motivation (see punk::console get_size / get_ansi_response_payload history):
-#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
-#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
-#replies land in some other process's input. A console object makes the pairing an explicit,
-#construction-time property instead of a per-query-site guess.
-#
-#Design notes:
-# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
-# are anchored at well-known namespace variables (::opunk::console::instances::) and the value
-# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
-# the lowercase ::opunk::console wrapper procs.
-# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so channel
-# environments that respond like terminals but aren't platform consoles can subclass
-# (-extends ::opunk::Console) and override the capability/size methods, with existing holders of
-# console values dispatching correctly.
-# - This first cut is standalone and passive: capability detection is heuristic (channel options,
-# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
-# query probing arrives with punk::console integration (which will delegate here rather than the
-# class depending on punk::console).
-
-tcl::namespace::eval ::opunk::console {
- variable PUNKARGS
-
- #well-known cooperative store for bytes consumed by probe reads on pipe-like channels
- #dict keyed by channel name -> list of chunks. Readers taking over a channel should consume
- #and clear any entry for that channel first. (analogous to punk::console::input_chunks_waiting)
- variable waiting_chunks
- if {![info exists waiting_chunks]} {
- set waiting_chunks [dict create]
- }
-}
-
-voo::class ::opunk::Console -virtual {
- private {
- #o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
- #and matches tclOO-style implementations elsewhere for easier code movement).
- #Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
- string_t o_in stdin
- string_t o_out stdout
- string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
- list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
- dict_t o_default_size [dict create columns 80 rows 24]
- }
- public {
- #settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
- #public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
- #::opunk::console can settle it without reaching into private my.* accessors.
- int_t o_can_respond -1
-
- method in {} {
- my.get.o_in $this
- }
- method out {} {
- my.get.o_out $this
- }
- method channels {} {
- list [my.get.o_in $this] [my.get.o_out $this]
- }
- method terminal_class {} {
- my.get.o_terminal_class $this
- }
- method default_size {} {
- my.get.o_default_size $this
- }
-
- #Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
- #A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
- #get a non-blocking 1-byte probe. A probed byte is preserved in ::opunk::console::waiting_chunks
- #for cooperating readers. Console/tty channels are never probed (must not consume a keystroke).
- method at_eof {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set in [my.get.o_in $this]
- if {[catch {chan eof $in} is_eof]} {
- return 1 ;#closed/invalid channel - unusable
- }
- if {$is_eof} {
- return 1
- }
- if {[catch {chan configure $in} conf]} {
- return 1
- }
- if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
- return 0
- }
- if {[catch {
- set prior_blocking [dict get $conf -blocking]
- chan configure $in -blocking 0
- set probe [read $in 1]
- chan configure $in -blocking $prior_blocking
- }]} {
- return 1
- }
- if {$probe ne ""} {
- dict lappend ::opunk::console::waiting_chunks $in $probe
- }
- return [chan eof $in]
- }
-
- #Heuristic: could the input channel be connected to a terminal able to answer queries?
- #Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
- #terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
- #negatives there at the cost of false positives for genuinely piped-but-open input.
- method is_console_or_tty {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set in [my.get.o_in $this]
- if {[catch {chan configure $in} conf]} {
- return 0
- }
- if {[dict exists $conf -inputmode]} {
- return 1
- }
- if {[dict exists $conf -mode]} {
- return 1
- }
- if {[at_eof $this]} {
- return 0
- }
- if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
- return 1
- }
- return 0
- }
-
- #Response capability: settled value if known, else computed heuristically (without settling -
- #this method is pure; use ::opunk::console::can_respond to settle an anchored instance
- #heuristically, or punk::console::settle_can_respond for active-probe settling - the
- #probe machinery lives in punk::console so this class stays free of that dependency).
- method can_respond {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set settled [get.o_can_respond $this]
- if {$settled != -1} {
- return $settled
- }
- return [is_console_or_tty $this]
- }
-
- #Size of the console: chan -winsize on the output channel when available (fast, no query),
- #else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
- #this standalone class - they arrive via punk::console integration and subclasses.
- method size {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set out [my.get.o_out $this]
- if {![catch {chan configure $out} oconf]} {
- if {[dict exists $oconf -winsize]} {
- lassign [dict get $oconf -winsize] cols lines
- if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
- return [dict create columns $cols rows $lines]
- }
- }
- }
- return [my.get.o_default_size $this]
- }
- }
-
- constructor {in out} {
- #new() supplies the class defaults including the virtual class tag at slot 0
- set obj [new()]
- my.set.o_in obj $in
- my.set.o_out obj $out
- return $obj
- }
-}
-
-#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
-namespace eval ::opunk::Console {
- namespace ensemble create
-}
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Anchored instances - the 'consoles are entities' layer
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-tcl::namespace::eval ::opunk::console {
- tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
-
- namespace eval instances {
- #one variable per anchored console instance - the canonical location of each console object.
- #Access via the sibling procs; methods needing to mutate an instance go through these anchors.
- }
-
- proc create {name in out} {
- if {![string is wordchar -strict $name]} {
- error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
- }
- set [namespace current]::instances::$name [::opunk::Console::new $in $out]
- return [namespace current]::instances::$name
- }
- proc instancevar {name} {
- set v [namespace current]::instances::$name
- if {![info exists $v]} {
- error "opunk::console: no anchored console instance named '$name' (known: [names])"
- }
- return $v
- }
- proc instance {name} {
- set [instancevar $name]
- }
- proc names {} {
- set result [list]
- foreach v [info vars [namespace current]::instances::*] {
- lappend result [namespace tail $v]
- }
- return $result
- }
- proc forget {name} {
- unset [instancevar $name]
- return
- }
-
- #settle-and-cache response capability on the anchored instance.
- #The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
- proc can_respond {name} {
- upvar #0 [instancevar $name] obj
- set settled [::opunk::Console::get.o_can_respond $obj]
- if {$settled != -1} {
- return $settled
- }
- set r [::opunk::Console::can_respond $obj]
- ::opunk::Console::set.o_can_respond obj $r
- return $r
- }
- proc size {name} {
- ::opunk::Console::size [instance $name]
- }
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-# == === === === === === === === === === === === === === ===
-# Sample 'about' function with punk::args documentation
-# == === === === === === === === === === === === === === ===
-tcl::namespace::eval ::opunk::console {
- variable PUNKARGS
- variable PUNKARGS_aliases
-
- lappend PUNKARGS [list {
- @id -id "(package)opunk::console"
- @package -name "opunk::console" -help\
- "voo class ::opunk::Console representing a console as an in/out channel pair with settled
- response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
- }]
-
- namespace eval argdoc {
- proc package_name {} {
- return opunk::console
- }
- proc about_topics {} {
- set topic_funs [info commands [namespace current]::get_topic_*]
- set about_topics [list]
- foreach f $topic_funs {
- set tail [namespace tail $f]
- lappend about_topics [string range $tail [string length get_topic_] end]
- }
- return [lsort $about_topics]
- }
- proc default_topics {} {return [list Description *]}
-
- proc get_topic_Description {} {
- punk::args::lib::tstr [string trim {
- package opunk::console
- voo class for consoles as channel-pair objects with anchored canonical instances.
- } \n]
- }
- proc get_topic_License {} {
- return "BSD"
- }
- proc get_topic_Version {} {
- return "$::opunk::console::version"
- }
- proc get_topic_Contributors {} {
- set authors {{"Julian Noble" "julian@precisium.com.au"}}
- set contributors ""
- foreach a $authors {
- append contributors $a \n
- }
- if {[string index $contributors end] eq "\n"} {
- set contributors [string range $contributors 0 end-1]
- }
- return $contributors
- }
- proc get_topic_classes {} {
- punk::args::lib::tstr -return string {
- opunk::Console "virtual class for consoles as in/out channel pairs"
- }
- }
- }
-
- set overrides [dict create]
- dict set overrides @id -id "::opunk::console::about"
- dict set overrides @cmd -name "opunk::console::about"
- dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
- About opunk::console
- }] \n]
- dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
- dict set overrides topic -choicerestricted 1
- dict set overrides topic -default [opunk::console::argdoc::default_topics]
- set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
- lappend PUNKARGS [list $newdef]
- proc about {args} {
- package require punk::args
- set argd [punk::args::parse $args withid ::opunk::console::about]
- lassign [dict values $argd] _leaders opts values _received
- punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
- }
-}
-# end of sample 'about' function
-# == === === === === === === === === === === === === === ===
-
-
-# -----------------------------------------------------------------------------
-# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
-# -----------------------------------------------------------------------------
-namespace eval ::punk::args::register {
- #use fully qualified so 8.6 doesn't find existing var in global namespace
- lappend ::punk::args::register::NAMESPACES ::opunk::console ::opunk::Console
-}
-# -----------------------------------------------------------------------------
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide opunk::console [tcl::namespace::eval ::opunk::console {
- variable pkg opunk::console
- variable version
- set version 0.1.1
-}]
-return
diff --git a/src/bootsupport/modules/opunk/console-0.2.0.tm b/src/bootsupport/modules/opunk/console-0.2.0.tm
deleted file mode 100644
index 2432c1c7..00000000
--- a/src/bootsupport/modules/opunk/console-0.2.0.tm
+++ /dev/null
@@ -1,372 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt
-# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) 2026
-#
-# @@ Meta Begin
-# Application opunk::console 0.2.0
-# Meta platform tcl
-# Meta license BSD
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-package require Tcl 8.6-
-
-package require voo
-package require punk::args
-
-#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
-#belong to one responding endpoint, together with settled capability facts about that endpoint.
-#
-#Motivation (see punk::console get_size / get_ansi_response_payload history):
-#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
-#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
-#replies land in some other process's input. A console object makes the pairing an explicit,
-#construction-time property instead of a per-query-site guess.
-#
-#Design notes:
-# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
-# are anchored at well-known namespace variables (::opunk::console::instances::) and the value
-# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
-# the lowercase ::opunk::console wrapper procs.
-# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so channel
-# environments that respond like terminals but aren't platform consoles can subclass
-# (-extends ::opunk::Console) and override the capability/size methods, with existing holders of
-# console values dispatching correctly.
-# - This first cut is standalone and passive: capability detection is heuristic (channel options,
-# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
-# query probing arrives with punk::console integration (which will delegate here rather than the
-# class depending on punk::console).
-
-tcl::namespace::eval ::opunk::console {
- variable PUNKARGS
-
- #well-known cooperative store for bytes consumed by probe reads on pipe-like channels:
- #an array keyed by channel name -> list of chunks. Readers taking over a channel should consume
- #and clear any entry for that channel first.
- #The store is PLUGGABLE via waiting_chunks_arrayvar: environments with an established cooperative
- #store (e.g punk::console::input_chunks_waiting, which the punk repl reader consults) redirect it
- #so probe-consumed bytes are never invisible to the active reader. The class itself stays free of
- #any punk::console dependency - redirection is performed by the integrating layer.
- variable waiting_chunks
- if {![array exists waiting_chunks]} {
- array set waiting_chunks {}
- }
- variable waiting_chunks_arrayvar
- if {![info exists waiting_chunks_arrayvar]} {
- set waiting_chunks_arrayvar ::opunk::console::waiting_chunks
- }
-}
-
-voo::class ::opunk::Console -virtual {
- private {
- #o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
- #and matches tclOO-style implementations elsewhere for easier code movement).
- #Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
- string_t o_in stdin
- string_t o_out stdout
- string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
- list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
- dict_t o_default_size [dict create columns 80 rows 24]
- }
- public {
- #settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
- #public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
- #::opunk::console can settle it without reaching into private my.* accessors.
- int_t o_can_respond -1
-
- method in {} {
- my.get.o_in $this
- }
- method out {} {
- my.get.o_out $this
- }
- method channels {} {
- list [my.get.o_in $this] [my.get.o_out $this]
- }
- method terminal_class {} {
- my.get.o_terminal_class $this
- }
- method default_size {} {
- my.get.o_default_size $this
- }
-
- #Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
- #A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
- #get a non-blocking 1-byte probe. A probed byte is preserved in the cooperative waiting store
- #(array named by ::opunk::console::waiting_chunks_arrayvar - redirectable by the integrating
- #layer to e.g punk::console::input_chunks_waiting so active readers can find it).
- #Console/tty channels are never probed (must not consume a keystroke).
- method at_eof {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set in [my.get.o_in $this]
- if {[catch {chan eof $in} is_eof]} {
- return 1 ;#closed/invalid channel - unusable
- }
- if {$is_eof} {
- return 1
- }
- if {[catch {chan configure $in} conf]} {
- return 1
- }
- if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
- return 0
- }
- if {[catch {
- set prior_blocking [dict get $conf -blocking]
- chan configure $in -blocking 0
- set probe [read $in 1]
- chan configure $in -blocking $prior_blocking
- }]} {
- return 1
- }
- if {$probe ne ""} {
- upvar #0 $::opunk::console::waiting_chunks_arrayvar wchunks
- lappend wchunks($in) $probe
- }
- return [chan eof $in]
- }
-
- #Heuristic: could the input channel be connected to a terminal able to answer queries?
- #Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
- #terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
- #negatives there at the cost of false positives for genuinely piped-but-open input.
- method is_console_or_tty {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set in [my.get.o_in $this]
- if {[catch {chan configure $in} conf]} {
- return 0
- }
- if {[dict exists $conf -inputmode]} {
- return 1
- }
- if {[dict exists $conf -mode]} {
- return 1
- }
- if {[at_eof $this]} {
- return 0
- }
- if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
- return 1
- }
- return 0
- }
-
- #Response capability: settled value if known, else computed heuristically (without settling -
- #this method is pure; use ::opunk::console::can_respond to settle an anchored instance
- #heuristically, or punk::console::settle_can_respond for active-probe settling - the
- #probe machinery lives in punk::console so this class stays free of that dependency).
- method can_respond {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set settled [get.o_can_respond $this]
- if {$settled != -1} {
- return $settled
- }
- return [is_console_or_tty $this]
- }
-
- #Size of the console: chan -winsize on the output channel when available (fast, no query),
- #else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
- #this standalone class - they arrive via punk::console integration and subclasses.
- method size {} -virtual {
- if {[llength $this] < 7} {
- error "invalid object $this"
- }
- set out [my.get.o_out $this]
- if {![catch {chan configure $out} oconf]} {
- if {[dict exists $oconf -winsize]} {
- lassign [dict get $oconf -winsize] cols lines
- if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
- return [dict create columns $cols rows $lines]
- }
- }
- }
- return [my.get.o_default_size $this]
- }
- }
-
- constructor {in out} {
- #new() supplies the class defaults including the virtual class tag at slot 0
- set obj [new()]
- my.set.o_in obj $in
- my.set.o_out obj $out
- return $obj
- }
-}
-
-#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
-namespace eval ::opunk::Console {
- namespace ensemble create
-}
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Anchored instances - the 'consoles are entities' layer
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-tcl::namespace::eval ::opunk::console {
- tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
-
- namespace eval instances {
- #one variable per anchored console instance - the canonical location of each console object.
- #Access via the sibling procs; methods needing to mutate an instance go through these anchors.
- }
-
- proc create {name in out} {
- if {![string is wordchar -strict $name]} {
- error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
- }
- set [namespace current]::instances::$name [::opunk::Console::new $in $out]
- return [namespace current]::instances::$name
- }
- proc instancevar {name} {
- set v [namespace current]::instances::$name
- if {![info exists $v]} {
- error "opunk::console: no anchored console instance named '$name' (known: [names])"
- }
- return $v
- }
- proc instance {name} {
- set [instancevar $name]
- }
- proc names {} {
- set result [list]
- foreach v [info vars [namespace current]::instances::*] {
- lappend result [namespace tail $v]
- }
- return $result
- }
- proc forget {name} {
- unset [instancevar $name]
- return
- }
-
- #settle-and-cache response capability on the anchored instance.
- #The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
- proc can_respond {name} {
- upvar #0 [instancevar $name] obj
- set settled [::opunk::Console::get.o_can_respond $obj]
- if {$settled != -1} {
- return $settled
- }
- set r [::opunk::Console::can_respond $obj]
- ::opunk::Console::set.o_can_respond obj $r
- return $r
- }
- proc size {name} {
- ::opunk::Console::size [instance $name]
- }
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-# == === === === === === === === === === === === === === ===
-# Sample 'about' function with punk::args documentation
-# == === === === === === === === === === === === === === ===
-tcl::namespace::eval ::opunk::console {
- variable PUNKARGS
- variable PUNKARGS_aliases
-
- lappend PUNKARGS [list {
- @id -id "(package)opunk::console"
- @package -name "opunk::console" -help\
- "voo class ::opunk::Console representing a console as an in/out channel pair with settled
- response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
- }]
-
- namespace eval argdoc {
- proc package_name {} {
- return opunk::console
- }
- proc about_topics {} {
- set topic_funs [info commands [namespace current]::get_topic_*]
- set about_topics [list]
- foreach f $topic_funs {
- set tail [namespace tail $f]
- lappend about_topics [string range $tail [string length get_topic_] end]
- }
- return [lsort $about_topics]
- }
- proc default_topics {} {return [list Description *]}
-
- proc get_topic_Description {} {
- punk::args::lib::tstr [string trim {
- package opunk::console
- voo class for consoles as channel-pair objects with anchored canonical instances.
- } \n]
- }
- proc get_topic_License {} {
- return "BSD"
- }
- proc get_topic_Version {} {
- return "$::opunk::console::version"
- }
- proc get_topic_Contributors {} {
- set authors {{"Julian Noble" "julian@precisium.com.au"}}
- set contributors ""
- foreach a $authors {
- append contributors $a \n
- }
- if {[string index $contributors end] eq "\n"} {
- set contributors [string range $contributors 0 end-1]
- }
- return $contributors
- }
- proc get_topic_classes {} {
- punk::args::lib::tstr -return string {
- opunk::Console "virtual class for consoles as in/out channel pairs"
- }
- }
- }
-
- set overrides [dict create]
- dict set overrides @id -id "::opunk::console::about"
- dict set overrides @cmd -name "opunk::console::about"
- dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
- About opunk::console
- }] \n]
- dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
- dict set overrides topic -choicerestricted 1
- dict set overrides topic -default [opunk::console::argdoc::default_topics]
- set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
- lappend PUNKARGS [list $newdef]
- proc about {args} {
- package require punk::args
- set argd [punk::args::parse $args withid ::opunk::console::about]
- lassign [dict values $argd] _leaders opts values _received
- punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
- }
-}
-# end of sample 'about' function
-# == === === === === === === === === === === === === === ===
-
-
-# -----------------------------------------------------------------------------
-# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
-# -----------------------------------------------------------------------------
-namespace eval ::punk::args::register {
- #use fully qualified so 8.6 doesn't find existing var in global namespace
- lappend ::punk::args::register::NAMESPACES ::opunk::console ::opunk::Console
-}
-# -----------------------------------------------------------------------------
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide opunk::console [tcl::namespace::eval ::opunk::console {
- variable pkg opunk::console
- variable version
- set version 0.2.0
-}]
-return
diff --git a/src/bootsupport/modules/packagetest-0.1.7.tm b/src/bootsupport/modules/packagetest-0.1.7.tm
deleted file mode 100644
index 658d45a4..00000000
Binary files a/src/bootsupport/modules/packagetest-0.1.7.tm and /dev/null differ
diff --git a/src/bootsupport/modules/punk-0.1.tm b/src/bootsupport/modules/punk-0.1.tm
deleted file mode 100644
index 91ead8b0..00000000
--- a/src/bootsupport/modules/punk-0.1.tm
+++ /dev/null
@@ -1,9286 +0,0 @@
-#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk.
-#Built on Tcl of course - because it's the most powerful piece of under-appreciated and alternate-thinking engineering you can plug into.
-
-
-namespace eval punk {
- proc lazyload {pkg} {
- package require zzzload
- if {[package provide $pkg] eq ""} {
- zzzload::pkg_require $pkg
- }
- }
- #lazyload twapi ?
-
- catch {package require vfs} ;#attempt load now so we can use faster 'package provide' to test existence later
-
- variable can_exec_windowsapp
- set can_exec_windowsapp unknown ;#don't spend a potential X00ms testing until needed
- variable windowsappdir
- set windowsappdir ""
- variable cmdexedir
- set cmdexedir ""
-
- proc sync_package_paths_script {} {
- #the tcl::tm namespace doesn't exist until one of the tcl::tm commands
- #is run. (they are loaded via ::auto_index triggering load of tm.tcl)
- #we call tcl::tm::list to trigger the initial set of tm paths before
- #we can override it, otherwise our changes will be lost
- #REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc
- list apply {{ap tmlist} {
- set ::auto_path $ap
- tcl::tm::list
- set ::tcl::tm::paths $tmlist
- }} $::auto_path [tcl::tm::list]
- }
-
-
-
- proc ::punk::auto_execok_original name [info body ::auto_execok]
- variable better_autoexec
-
-
- #use this var via better_autoexec only
- set better_autoexec 1
- proc ::punk::auto_execok_windows name {
- ::punk::auto_execok_better $name
- }
-
- set has_commandstack [expr {![catch {package require commandstack}]}]
- if {$has_commandstack} {
- if {[catch {
- package require punk::packagepreference
- } errM]} {
- catch {puts stderr "Failed to load punk::packagepreference"}
- }
- catch punk::packagepreference::install
- } else {
- #
- }
-
- if {![interp issafe] && $::tcl_platform(platform) eq "windows"} {
-
- #return the raw command string from the registry for the association of the given extension, without processing the placeholders such as %1 %SystemRoot% etc.
- #This is because we want to process these ourselves to be able to return a proper list of command and arguments.
- #Note that the resulting string can't be directly treated as a tcl list because it has double quoted segments with characters that are literals (not escaped)
- #Accessing it directly as a list will cause tcl to interpret the backslashes as escapes and lose the literal meaning values such as the path.
- proc extension_open_association {ext} {
- #look up assoc and ftype to find associated file type and application.
- #Note that the windows 'ftype' and 'assoc' commands only examine the system's data - not the user-specific data which may override the system data.
- #to get the user-specific associations we need to read the registry keys.
-
- #extensions in the registry seem to be stored lower case wnd with a leading dot.
- set lext [string tolower $ext]
- package require registry
- set user_assoc_path [join [list HKEY_CURRENT_USER Software Microsoft Windows CurrentVersion Explorer FileExts $lext UserChoice] "\\"]
-
- #The UserChoice subkey under this key is where the user-specific association is stored when the user has made a choice. It contains a Progid value which points to the associated file type.
- #It can return something like "Python.File" or "Applications\python.exe" or even tcl_auto_file (for .tcl files associated with tclsh by a tcl installer)
-
- #The UserChoice key may not exist if the user hasn't made an explicit choice, in which case we fall back to the system association which is stored under HKEY_CLASSES_ROOT\$ext (which also contains user-specific overrides but is more cumbersome to query for them)
- if {![catch {registry get $user_assoc_path Progid} user_choice]} {
- if {$user_choice ne ""} {
- #examples such as Applications\python.exe and tcl_auto_file are found under HKEY_CURRENT_USER\Software\Classes
- #they then have a sub-path shell\open\command which contains the command to open files of that type, which is what we need to extract the associated application.
- #it is faster to query for the key directly within a catch than to retrieve keys as lists and iterate through them to find the one we want.
- if {![catch {registry get [join [list HKEY_CURRENT_USER Software Classes $user_choice shell open command] "\\"] ""} raw_assoc]} {
- #The command string can contain placeholders like "%1" for the file name, so we need to extract just the executable path.
- #e.g .py -> "c:\Program Files\Python\python.exe" "%1"
- #e.g .rb -> "C:\tools\ruby31\bin\ruby.exe" "%1" %*
- # e.g .vbs -> "%SystemRoot%\System32\WScript.exe" "%1" %*
- #we need to process this without Tcl interpreting the backslashes as escapes.
- #we will use double quotes to determine which entries belong together as a list item for the resulting list of command and arguments.
- #set assoc [string map [list \\ \\\\] $raw_assoc]
- set assoc $raw_assoc
- } else {
- #e.g Python.File
- if {![catch {registry get [join [list HKEY_LOCAL_MACHINE SOFTWARE Classes $user_choice shell open command] "\\"] ""} raw_assoc]} {
- #set assoc [string map [list \\ \\\\] $raw_assoc]
- set assoc $raw_assoc
- } else {
- set assoc ""
- }
- }
-
- } else {
- #review - is it possible for Progid to be empty string? If so we should probably fall back to system association instead of returning no association.
- #alternatively it could mean the user has explicitly chosen to have no association for this file type - in which case returning an empty association may be the correct behaviour.
- set assoc ""
- }
- } else {
- #fall back to system association and ftype
- if {![catch {registry get [join [list HKEY_LOCAL_MACHINE SOFTWARE Classes $lext] "\\"] ""} ftype]} {
- #ftype is the file type associated with the extension, e.g "Python.File"
- #we then need to look up the command associated with this file type under the same path but with the file type instead of the extension and with the additional sub-path shell\open\command
- if {![catch {registry get [join [list HKEY_LOCAL_MACHINE SOFTWARE Classes $ftype shell open command] "\\"] ""} raw_assoc]} {
- #set assoc [string map [list \\ \\\\] $raw_assoc]
- set assoc $raw_assoc
- } else {
- set assoc ""
- }
- } else {
- set assoc ""
- }
- }
- return $assoc
- }
-
-
- }
-
- if {![interp issafe] && $has_commandstack && $::tcl_platform(platform) eq "windows"} {
-
- #still a caching version of auto_execok - but with proper(fixed) search order
-
- #set b [info body ::auto_execok]
- #proc ::auto_execok_original name $b
-
- proc better_autoexec {{onoff ""}} {
- variable better_autoexec
- if {$onoff eq ""} {
- return $better_autoexec
- }
- if {![string is boolean -strict $onoff]} {
- error "better_autoexec argument 'onoff' must be a boolean, received: $onoff"
- }
- if {$onoff && ($onoff != $better_autoexec)} {
- puts "Turning on better_autoexec - search PATH first then extension"
- set better_autoexec 1
- proc ::punk::auto_execok_windows name {
- ::punk::auto_execok_better $name
- }
- punk::auto_exec::rehash
- } elseif {!$onoff && ($onoff != $better_autoexec)} {
- puts "Turning off better_autoexec - search extension then PATH"
- set better_autoexec 0
- proc ::punk::auto_execok_windows name {
- ::punk::auto_execok_original $name
- }
- punk::auto_exec::rehash
- } else {
- puts "no change"
- }
- }
- #better_autoexec $better_autoexec ;#init to default
-
-
- proc auto_execok_better name {
- #review - we have a gneral problem of auto_exec caching negative results for relative paths.
- #A failed resolution of a relative path should not generate an entry in ::auto_execs.
- #This happens in plain tclsh - so we need to determine where in Tcl this happens and fix it there.
- #Simply returning an empty string here will still result in a negative cache entry.
- #we want to cache negative results for absolute paths or plain filenames with no file-separator.
- #e.g ./doesntexist.exe should not be cached as not found, but should be re-resolved every time. (cwd dependent)
- #e.g doesntexist.exe should be cached as not found, because it will always be not found until it appears in the PATH.
- #i.e it is required to prefix with ./ to exec a file in the current directory. (similar to unix shells)
-
-
- global auto_execs env tcl_platform
- #for now at least, auto_execok_better is windows-specific.
- package require punk::auto_exec
-
- if {[info exists auto_execs($name)]} {
- return $auto_execs($name)
- }
- #puts stdout "[a+ red]...[a]"
- set auto_execs($name) ""
-
- set shellBuiltins [list {*}{
- assoc cls copy date del dir echo erase exit ftype
- md mkdir mklink move rd ren rename rmdir start time type ver vol
- }]
- if {[info exists env(PATHEXT)]} {
- # Add an initial ; to have the {} extension check first.
- set execExtensions [split ";$env(PATHEXT)" ";"]
- } else {
- set execExtensions [list {} .com .exe .bat .cmd]
- }
-
- if {[string tolower $name] in $shellBuiltins} {
- # When this is command.com for some reason on Win2K, Tcl won't
- # exec it unless the case is right, which this corrects. COMSPEC
- # may not point to a real file, so do the check.
- set cmd $env(COMSPEC)
- if {[file exists $cmd]} {
- set cmd [file attributes $cmd -shortname]
- }
- return [set auto_execs($name) [list $cmd /c $name]]
- }
-
- if {[llength [file split $name]] != 1} {
- #-----------------------------------------------------
- #has a path component - could be relative or absolute.
- #-----------------------------------------------------
- if {[file pathtype $name] eq "relative"} {
- #don't cache negative result for any relative paths - as they may become valid if the file appears in the relative location, or if the user changes directory and the same relative path points to a different file.
- #our only way to do this is by cooperating with the unknown handler.
- set auto_execs($name) "for_unknown_handler by punk::auto_exec relative_path - file existence should be re-checked at call time"
- return $auto_execs($name)
- }
-
- if {[string tolower [file extension $name]] eq ".lnk"} {
- #special case .lnk
- #todo - consider working directory or other properties of link before launching?
- package require punk::winlnk
- if {![catch {punk::winlnk::target $name} linktarget]} {
- if {$linktarget ne ""} {
- set target $linktarget
- } else {
- return ""
- }
- } else {
- set target $name
- }
- } else {
- set target $name
- }
- #always store $name as the key when setting auto_execs.
- foreach ext $execExtensions {
- set file ${target}${ext}
- #first execExtension is empty string - ensure we test actual file as given before we try appending extensions.
- if {$ext eq ""} {
- set test_ext [file extension $file]
- } else {
- set test_ext $ext
- }
- if {[file exists $file] && ![file isdirectory $file]} {
- #look up assoc and ftype to find associated file type and application.
- #Note that the windows 'ftype' and 'assoc' commands only examine the system's data - not the user-specific data which may override the system data.
- #set assoc [extension_open_association $ext]
- set associnfo [punk::auto_exec::shell_open_command $test_ext]
- set valuetype [dict get $associnfo type]
- set assoc [dict get $associnfo value]
- set windows_file_type [dict get $associnfo filetype]
- if {$assoc eq ""} {
- return [set auto_execs($name) [list $file]]
- } else {
- if {[file pathtype $target] eq "relative" && $windows_file_type eq "InternetShortcut"} {
- #special case InternetShortcut - cannot accept relative path - so we can't cache it in auto_execs if we used a relative path to launch
- #if we return an empty string - the auto_exec will fail to launch this every time.
- #The best we can do is return a token for the 'unknown' process to detect and re-resolve the path every time.
- #This requires cooperation from 'unknown' which may not be configured to handle this token if the default 'punk' version isn't installed.
-
- #we can't resolve using absolute path here - because we don't want to lock in a specific file for a relative path.
- #e.g ::auto_execs(./link.url) = some.exe c:/desktop/link.url
- #this would be wrong if the user changed directory and tried to run ./link.url again on a different file with the same name
- # - as the cached path would no longer be correct.
- return [set auto_execs($name) "for_unknown_handler by punk::auto_exec absolute_path required"]
- }
- puts stderr "auto_execok_better: (review required) assoc $assoc for file $file ext $test_ext"
- set run [punk::auto_exec::shell_command_as_tcl_list -type $valuetype $assoc $file] ;# -workingdir [pwd] vs path of script?
- return [set auto_execs($name) $run]
- #return [set auto_execs($name) [list $file]]
- }
- }
- }
- #cache negative result for absolute paths - as they will always point to the same location, so if they don't exist now, they won't exist later.
- set auto_execs($name) ""
- return ""
- }
-
- #change1
- #set path "[file dirname [info nameofexecutable]];.;"
- set path "[file dirname [info nameofexecutable]];"
-
-
- # ------------------------
- #Note that unlike an ordinary Tcl array - the linked ::env behaves differently.
- #e.g parray ::env Path will not find ::env(PATH) and yet 'info exists env(Path)' returns true.
- #similarly 'set ::env(Path) ?newval?' or any case variation can set/get the value of ::env(PATH)
- #Windows environment variables are case-insensitive.
-
- #No evidence has been seen that any version of windows; current or historic since NT; can allow differently cased versions
- # of an environment variable to exist concurrently in the same environment.
- #for this reason we should be able to just use PATH.
- #
- if {[info exists env(PATH)]} {
- append path ";$env(PATH)"
- }
- # ------------------------
-
- if {[info exists env(SystemRoot)]} {
- set windir $env(SystemRoot)
- } elseif {[info exists env(WINDIR)]} {
- set windir $env(WINDIR)
- }
- if {[info exists windir]} {
- append path "$windir/system32;$windir/system;$windir;"
- }
-
- #change2
- if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} {
- set lookfor [list $name]
- } else {
- set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}]
- }
- #puts "-->$lookfor"
-
-
- foreach dir [split $path {;}] {
- set dir [string trim $dir {\\}] ;#trailing slash will result in a tail such as "/python.exe"
- #set dir [file normalize $dir]
- # Skip already checked directories
- if {[info exists checked($dir)] || ($dir eq "")} {
- continue
- }
- set checked($dir) {}
-
- #surprisingly fast
- #set matches [glob -nocomplain -dir $dir -types f -tails {*}$lookfor]
- ##puts "--dir $dir matches:$matches"
- #if {[llength $matches]} {
- # set file [file join $dir [lindex $matches 0]]
- # #puts "--match0:[lindex $matches 0] file:$file"
- # return [set auto_execs($name) [list $file]]
- #}
-
- #what if it's a link?
- #foreach match [glob -nocomplain -dir $dir -types f -tail {*}$lookfor] {
- # set file [file join $dir $match]
- # if {[file exists $file]} {
- # return [set auto_execs($name) [list $file]]
- # }
- #}
-
- #safest? could be a link?
- foreach match [glob -nocomplain -dir $dir -tail -- {*}$lookfor] {
- set file [file join $dir $match]
- if {[file exists $file] && ![file isdirectory $file]} {
- #set assoc [extension_open_association [file extension $file]]
- #todo - cache this lookup for each extension we encounter? maybe not, as the user might like changes reflected between runs. review.
- if {"windows" ne $::tcl_platform(platform)} {
- return [set auto_execs($name) [list $file]]
- }
-
- set associnfo [punk::auto_exec::shell_open_command [file extension $file]]
- set assoc [dict get $associnfo value]
- set type [dict get $associnfo type]
- if {$assoc eq ""} {
- return [set auto_execs($name) [list $file]]
- } else {
- puts stderr "auto_execok_better: assoc $assoc for file $file with type $type"
- #return [set auto_execs($name) [list $file]]
- #review - our stored auto_execs doesn't have any way to capture the full assoc info such as how subsequent arguments should be processed.
- #This may need handling in our Tcl shell 'unknown' function when calls are actually made to these commands
- #- we may need to re-process the assoc info at that point to determine how to combine all arguments with the calling specification in the assoc string.
- #The workingdir for the command may also need to be determined at that point - should it be the dir of the script being called, or the current dir of the shell?
-
- #The main point of Tcl's auto_execs is to avoid scanning the PATH entries every time a command is called,
- #but we may want to keep some of the assoc info available for processing at call time.
- set run [punk::auto_exec::shell_command_as_tcl_list -type $type $assoc $file] ;# -workingdir [pwd] vs path of script?
- return [set auto_execs($name) $run]
- }
- }
- }
- }
-
- #foreach ext $execExtensions {
- #unset -nocomplain checked
- #foreach dir [split $path {;}] {
- # # Skip already checked directories
- # if {[info exists checked($dir)] || ($dir eq "")} {
- # continue
- # }
- # set checked($dir) {}
- # set file [file join $dir ${name}${ext}]
- # if {[file exists $file] && ![file isdirectory $file]} {
- # return [set auto_execs($name) [list $file]]
- # }
- #}
- #}
- return ""
- }
-
-
-
- #review - what if punk package reloaded - but ::auto_execs has updated path for winget.exe?
- #what if we create another interp and use the same ::auto_execs? The appdir won't be detected.
- #TODO - see if there is a proper windows way to determine where the 'reparse point' apps are installed
-
-
-
- #winget is installed on all modern windows (but not on windows sandbox!) and is an example of the problem this addresses
- #we target apps with same location
-
- #the main purpose of this override is to support windows app executables (installed as 'reparse points')
- #for Tcl versions prior to the 2025-01 fix by APN https://core.tcl-lang.org/tcl/tktview/4f0b5767ac
- #versions prior to this will use cmd.exe to resolve the links
- set stackrecord [commandstack::rename_command -renamer ::punk auto_execok name {
- #set windowsappdir "%appdir%"
- upvar ::punk::can_exec_windowsapp can_exec_windowsapp
- upvar ::punk::windowsappdir windowsappdir
- upvar ::punk::cmdexedir cmdexedir
-
- if {$windowsappdir eq ""} {
- #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points'
- #Tcl (2025) can't exec when given a path to these 0KB files
- #This path is probably something like C:/Users/username/AppData/Local/Microsoft/WindowsApps
- if {!([info exists ::env(LOCALAPPDATA)] &&
- [file exists [set testapp [file join $::env(LOCALAPPDATA) "Microsoft" "WindowsApps" "winget.exe"]]])} {
- #should be unlikely to get here - unless LOCALAPPDATA missing (or winget.exe missing e.g windows sandbox)
- set windowsappdir [file dirname [lindex [::punk::auto_execok_windows winget.exe] 0]]
- catch {puts stderr "(resolved winget by search)"}
- } else {
- set windowsappdir [file dirname $testapp]
- }
- }
-
- #set default_auto [$COMMANDSTACKNEXT $name]
- set default_auto [::punk::auto_execok_windows $name]
- #if {$name ni {cmd cmd.exe}} {
- # unset -nocomplain ::auto_execs
- #}
-
- if {$default_auto eq ""} {
- return
- }
- set namedir [file dirname [lindex $default_auto 0]]
-
- if {$namedir eq $windowsappdir} {
- if {$can_exec_windowsapp eq "unknown"} {
- if {[catch {exec [file join $windowsappdir winget.exe] --version}]} {
- set can_exec_windowsapp 0
- } else {
- set can_exec_windowsapp 1
- }
- }
- if {$can_exec_windowsapp} {
- return [file join $windowsappdir $name]
- }
- if {$cmdexedir eq ""} {
- #cmd.exe very unlikely to move
- set cmdexedir [file dirname [lindex [::punk::auto_execok_windows cmd.exe] 0]]
- #auto_reset never seems to exist as a command - because auto_reset deletes all commands in the ::auto_index
- #anyway.. it has other side effects (affects auto_load)
- }
- return "[file join $cmdexedir cmd.exe] /c $name"
- }
- return $default_auto
- }]
-
-
- }
-
-}
-
-
-
-#repltelemetry cooperation with other packages such as shellrun
-#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists
-namespace eval punk {
- variable repltelemetry_emmitters
- #don't stomp.. even if something created this namespace in advance and is 'cooperating' a bit early
- if {![info exists repltelemetry_emitters]} {
- set repltelemetry_emmitters [list]
- }
-}
-
-namespace eval punk::pipecmds {
- #where to install proc/compilation artifacts for pieplines
- namespace export *
-}
-namespace eval punk::pipecmds::split_patterns {}
-namespace eval punk::pipecmds::split_rhs {}
-namespace eval punk::pipecmds::var_classify {}
-namespace eval punk::pipecmds::destructure {}
-namespace eval punk::pipecmds::insertion {}
-
-
-#globals... some minimal global var pollution
-#punk's official silly test dictionary
-set punk_testd [dict create \
- a0 a0val \
- b0 [dict create \
- a1 b0a1val \
- b1 b0b1val \
- c1 b0c1val \
- d1 b0d1val \
- ] \
- c0 [dict create] \
- d0 [dict create \
- a1 [dict create \
- a2 d0a1a2val \
- b2 d0a1b2val \
- c2 d0a1c2val \
- ] \
- b1 [dict create \
- a2 [dict create \
- a3 d0b1a2a3val \
- b3 d0b1a2b3val \
- ] \
- b2 [dict create \
- a3 d0b1b2a3val \
- bananas "in pyjamas" \
- c3 [dict create \
- po "in { }" \
- b4 ""\
- c4 "can go boom" \
- ] \
- d3 [dict create \
- a4 "-paper -cuts" \
- ] \
- e3 [dict create] \
- ] \
- ] \
- ] \
- e0 "multi\nline"\
- ]
-#test dict 2 - uniform structure and some keys with common prefixes for glob matching
-set punk_testd2 [dict create {*}{
- } a0 [dict create {*}{
- b1 {a b c}
- b2 {a b c d}
- x1 {x y z 1 2}
- y2 {X Y Z 1 2}
- z1 {k1 v1 k2 v2 k3 v3}
- }] {*}{
- } a1 [dict create {*}{
- b1 {a b c}
- b2 {a b c d}
- x1 {x y z 1 2}
- y2 {X Y Z 1 2}
- z1 {k1 v1 k2 v2 k3 v3}
- }] {*}{
- } b1 [dict create {*}{
- b1 {a b c}
- b2 {a b c d}
- x1 {x y z 1 2}
- y2 {X Y Z 1 2}
- z1 {k1 v1 k2 v2 k3 v3}
- }] {*}{
- }
-]
-
-#impolitely cooperative with punk repl - todo - tone it down.
-#namespace eval ::punk::repl::codethread {
-# variable running 0
-#}
-package require punk::lib ;# subdependency punk::args
-package require punk::ansi
-if {![llength [info commands ::ansistring]]} {
- namespace import punk::ansi::ansistring
-}
-#require aliascore after punk::lib & punk::ansi are loaded
-#package require punk::aliascore ;#mostly punk::lib aliases
-#punk::aliascore::init -force 1
-
-package require punk::repl::codethread
-package require punk::config
-#package require textblock
-catch {package require punk::console} ;#requires Thread - will not work in safe interps.
-package require punk::ns
-package require punk::winpath ;# for windows paths - but has functions that can be called on unix systems
-package require punk::repo
-package require punk::du
-package require punk::mix::base
-package require base64
-
-package require punk::pipe
-
-namespace eval punk {
- # -- --- ---
- #namespace import ::control::assert ;#according to tcllib doc - assert can be enabled/disabled per namespace
- # using control::control assert enabled within a namespace for which ::control::assert wasn't imported can produce surprising results.
- #e.g setting to zero may keep asserts enabled - (e.g if the assert command is still available due to namespace path etc) - but.. querying the enabled status may show zero even in the parent namespace where asserts also still work.
- #package require control
- #control::control assert enabled 1
-
- #We will use punk::assertion instead
-
- package require punk::assertion
- if {[catch {namespace import ::punk::assertion::assert} errM]} {
- catch {
- puts stderr "punk error importing punk::assertion::assert\n$errM"
- puts stderr "punk::a* commands:[info commands ::punk::a*]"
- }
- }
- punk::assertion::active on
- # -- --- ---
-
- interp alias {} purelist {} lreplace x 0 0 ;#required by pipe system
- if {[catch {
- package require pattern
- } errpkg]} {
- catch {puts stderr "Failed to load package pattern error: $errpkg"}
- }
- package require shellfilter
- package require punkapp
- package require funcl
-
- package require struct::list
- package require fileutil
- #package require punk::lib
-
- #NOTE - always call debug.xxx with braced message instead of double-quoted (unless specifically intending to do double-subtition)
- #(or $ within values will be substituted, causing an extra error message if the var doesn't exist - which it quite possibly doesn't)
- package require debug
-
- debug define punk.unknown
- debug define punk.pipe
- debug define punk.pipe.var
- debug define punk.pipe.args
- debug define punk.pipe.rep ;#string/list representation with tcl::unsupported::representation
- debug define punk.pipe.compile ;#info about when we compile pipeline components into procs etc
-
-
- #-----------------------------------
- # todo - load initial debug state from config
- debug off punk.unknown
- debug level punk.unknown 1
- debug off punk.pipe
- debug level punk.pipe 4
- debug off punk.pipe.var
- debug level punk.pipe.var 4
- debug off punk.pipe.args
- debug level punk.pipe.args 3
- debug off punk.pipe.rep 2
- debug off punk.pipe.compile
- debug level punk.pipe.compile 2
-
-
- debug header "dbg> "
-
-
- variable last_run_display [list]
-
-
- #variable re_headvar1 {([a-zA-Z:@.(),]+?)(?![^(]*\))(,.*)*$}
-
-
-
- #-----------------------------------------------------------------------------------
- #strlen is important for testing issues with string representationa and shimmering.
- #This specific implementation with append (as at 2023-09) is designed to ensure the original str representation isn't changed
- #It may need to be reviewed with different Tcl versions in case the append empty string is 'optimised/tuned' in some way that affects the behaviour
- proc strlen {str} {
- append str2 $str {}
- string length $str2
- }
- #-----------------------------------------------------------------------------------
-
- #get a copy of the item without affecting internal rep
- proc valcopy {obj} {
- append obj2 $obj {}
- }
-
-
- proc set_valcopy {varname obj} {
- #maintenance: also punk::lib::set_valcopy
- #e.g used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val]
- append obj2 $obj {}
- uplevel 1 [list set $varname $obj2]
- }
-
- interp alias "" strlen "" ::punk::strlen
- interp alias "" str_len "" ::punk::strlen
- interp alias "" valcopy "" ::punk::valcopy
- #proc ::strlen {str} {
- # string length [append str2 $str {}]
- #}
- #proc ::valcopy {obj} {
- # append obj2 $obj {}
- #}
-
- #-----------------------------------------------------------------------------------
- #order of arguments designed for pipelining
- #review - 'piper_' prefix is a naming convention for functions that are ordered for tail-argument pipelining
- #piper_ function names should read intuitively when used in a pipeline with tail argument supplied by the pipeline - but may seem reversed when using standalone.
- proc piper_append {new base} {
- append base $new
- }
- interp alias "" piper_append "" ::punk::piper_append
- proc piper_prepend {new base} {
- append new $base
- }
- interp alias "" piper_prepend "" ::punk::piper_prepend
-
- proc ::punk::K {x y} { return $x}
-
- #----------------------
- #todo - fix overtype
- #create test
- #overtype::renderline -insert_mode 0 -transparent 1 [a+ green]-----[a] " [a+ underline]x[a]"
- #----------------------
-
-
- proc stacktrace {} {
- set stack "Stack trace:\n"
- for {set i 1} {$i < [info level]} {incr i} {
- set lvl [info level -$i]
- set pname [lindex $lvl 0]
- append stack [string repeat " " $i]$pname
-
- if {![catch {info args $pname} pargs]} {
- foreach value [lrange $lvl 1 end] arg $pargs {
-
- if {$value eq ""} {
- if {$arg != 0} {
- info default $pname $arg value
- }
- }
- append stack " $arg='$value'"
- }
- } else {
- append stack " !unknown vars for $pname"
- }
-
- append stack \n
- }
- return $stack
- }
-
- namespace eval argdoc {
- punk::args::define {
- @id -id ::punk::get_runchunk
- @cmd -name "punk::get_runchunk" -help\
- "experimental"
- @opts
- -1 -optional 1 -type none
- -2 -optional 1 -type none
- @values -min 0 -max 0
- }
- }
- #get last command result that was run through the repl
- proc ::punk::get_runchunk {args} {
- #set argd [punk::args::parse $args withdef {
- # @id -id ::punk::get_runchunk
- # @cmd -name "punk::get_runchunk" -help\
- # "experimental"
- # @opts
- # -1 -optional 1 -type none
- # -2 -optional 1 -type none
- # @values -min 0 -max 0
- #}]
- #todo - make this command run without truncating previous runchunks
- set runchunks [tsv::array names repl runchunks-*]
-
- set sortlist [list]
- foreach cname $runchunks {
- set num [lindex [split $cname -] 1]
- lappend sortlist [list $num $cname]
- }
- set sorted [lsort -index 0 -integer $sortlist]
- set chunkname [lindex $sorted end-1 1]
- set runlist [tsv::get repl $chunkname]
- #puts stderr "--$runlist"
- if {![llength $runlist]} {
- return ""
- } else {
- return [lindex [lsearch -inline -index 0 $runlist result] 1]
- }
- }
- interp alias {} _ {} ::punk::get_runchunk
-
-
- proc ::punk::var {varname {= _=.=_} args} {
- upvar $varname the_var
- switch -exact -- ${=} {
- = {
- if {[llength $args] > 1} {
- set the_var $args
- } else {
- set the_var [lindex $args 0]
- }
- }
- .= {
- if {[llength $args] > 1} {
- set the_var [uplevel 1 $args]
- } else {
- set the_var [uplevel 1 [lindex $args 0]]
- }
- }
- _=.=_ {
- set the_var
- }
- default {
- set the_var [list ${=} {*}$args]
- }
- }
- }
- proc src {args} {
- #based on wiki.. https://wiki.tcl-lang.org/page/source+with+args
- #added support for ?-encoding name? and other options of Tcl source command under assumption they come pairs before the filename
- # review? seems unlikely source command will ever accept solo options. It would make complete disambiguation impossible when passing additional args as we are doing here.
- set cmdargs [list]
- set scriptargs [list]
- set inopts 0
- set i 0
- foreach a $args {
- if {$i eq [llength $args]-1} {
- #reached end without finding end of opts
- #must be file - even if it does match -* ?
- break
- }
- if {!$inopts} {
- if {[string match -* $a]} {
- set inopts 1
- } else {
- #leave loop at first nonoption - i should be index of file
- break
- }
- } else {
- #leave for next iteration to check
- set inopts 0
- }
- incr i
- }
- set cmdargs [lrange $args 0 $i]
- set scriptargs [lrange $args $i+1 end]
- set argv $::argv
- set argc $::argc
- set ::argv $scriptargs
- set ::argc [llength $scriptargs]
- set code [catch {uplevel [list source {*}$cmdargs]} return]
- set ::argv $argv
- set ::argc $argc
- return -code $code $return
- }
-
-
-
-
- proc varinfo {vname {flag ""}} {
- upvar $vname v
- if {[array exists $vname]} {
- error "can't read \"$vname\": variable is array"
- }
- if {[catch {set v} err]} {
- error "can't read \"$vname\": no such variable"
- }
- set inf [shellfilter::list_element_info [list $v]]
- set inf [dict get $inf 0]
- if {$flag eq "-v"} {
- return $inf
- }
-
- set output [dict create]
- dict set output wouldbrace [dict get $inf wouldbrace]
- dict set output wouldescape [dict get $inf wouldescape]
- dict set output head_tail_names [dict get $inf head_tail_names]
- dict set output len [dict get $inf len]
- return $output
- }
-
- #review - extending core commands could be a bit intrusive...although it can make sense in a pipeline.
- #e.g contrived pipeline example to only allow setting existing keys
- ## .= @head.= list {a aaa b bbb c ccc} |d,dkeys@keys> |> &true.= {is_list_all_in_list $nkeys $dkeys} |> {dict modify d {*}$new} |> &true.= {is_list_all_ni_list $nkeys $dkeys} |> {dict modify d {*}$new} -1} {
- #lassign [punk::lib::string_splitbefore $token $first_term] v k
- set v [string range $token 0 $first_term-1]
- set k [string range $token $first_term end] ;#key section includes the terminal char
- lappend varlist [list $v $k]
- } else {
- lappend varlist [list $token ""]
- }
- set token ""
- set token_index -1 ;#reduce by 1 because , not included in next token
- set first_term -1
- } else {
- if {$first_term == -1} {
- if {$c in $var_terminals} {
- set first_term $token_index
- }
- }
- append token $c
- if {$c eq "("} {
- set in_brackets 1
- }
- }
- }
- incr token_index
- }
- if {[string length $token]} {
- if {$first_term > -1} {
- set v [string range $token 0 $first_term-1]
- set k [string range $token $first_term end] ;#key section includes the terminal char
- lappend varlist [list $v $k]
- } else {
- lappend varlist [list $token ""]
- }
- }
- return $varlist
- }
-
- proc fp_restructure {selector data} {
- if {$selector eq ""} {
- fun=.= {val $input} and always break
- set lhs ""
- set rhs ""
- #todo - check performance impact of catches around list and dict operations - consider single catch around destructure and less specific match error info?
- foreach index $subindices {
- set subpath [join [lrange $subindices 0 $i_keyindex] /]
- set lhs $subpath
- set assigned ""
- set get_not 0
- set already_assigned 0
- set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning.
- #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values.
- #todo - see if 'string is list' improved in tcl9 vs catch {llength $list}
- switch -exact -- $index {
- # {
- set active_key_type "list"
- if {![catch {llength $leveldata} assigned]} {
- set already_assigned 1
- } else {
- set action ?mismatch-not-a-list
- break
- }
- }
- ## {
- set active_key_type "dict"
- if {![catch {dict size $leveldata} assigned]} {
- set already_assigned 1
- } else {
- set action ?mismatch-not-a-dict
- break
- }
- }
- #? {
- #review - compare to %# ?????
- #seems to be unimplemented ?
- set assigned [string length $leveldata]
- set already_assigned 1
- }
- @ {
- upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position
- set active_key_type "list"
- #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey
- #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3
- #while x@,y@.= is reasonably handy - especially for args e.g $len} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- set assigned [lindex $leveldata $index]
- set already_assigned 1
- }
- @@ - @?@ - @??@ {
- set active_key_type "dict"
-
- #NOTE: it may at first seem pointless to use @@/key, since we have to know the key - but this can be used to match 'key' only at the first position in .= list key {x y} key2 etc
- #x@@ = a {x y}
- #x@@/@0 = a
- #x@@/@1 = x y
- #x@@/a = a {x y}
- # but.. as the @@ is stateful - it generally isn't very useful for multiple operations on the same pair within the pattern group.
- # (note that ?@ forms a different subpath - so can be used to test match prior to @@ without affecting the index)
- # It is analogous to v1@,v2@ for lists.
- # @pairs is more useful for repeated operations
-
- #
- #set subpath [join [lrange $subindices 0 $i_keyindex] /]
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- set next_this_level [incr v_dict_idx($subpath)]
- set keyindex [expr {$next_this_level -1}]
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- if {$index eq "@?@"} {
- set assigned [dict get $leveldata $k]
- } else {
- set assigned [list $k [dict get $leveldata $k]]
- }
- } else {
- if {$index eq "@@"} {
- set action ?mismatch-dict-index-out-of-range
- break
- } else {
- set assigned [list]
- }
- }
- set already_assigned 1
- }
- default {
- switch -glob -- $index {
- @@* {
- set active_key_type "dict"
- set key [string range $index 2 end]
- #dict exists test is safe - no need for catch
- if {[dict exists $leveldata $key]} {
- set assigned [dict get $leveldata $key]
- } else {
- set action ?mismatch-dict-key-not-found
- break
- }
- set already_assigned 1
- }
- {@\?@*} {
- set active_key_type "dict"
- set key [string range $index 3 end]
- #dict exists test is safe - no need for catch
- if {[dict exists $leveldata $key]} {
- set assigned [dict get $leveldata $key]
- } else {
- set assigned [list]
- }
- set already_assigned 1
- }
- {@\?\?@*} {
- set active_key_type "dict"
- set key [string range $index 4 end]
- #dict exists test is safe - no need for catch
- if {[dict exists $leveldata $key]} {
- set assigned [list $key [dict get $leveldata $key]]
- } else {
- set assigned [list]
- }
- set already_assigned 1
- }
- @* {
- set active_key_type "list"
- set do_bounds_check 1
- set index [string trimleft $index @]
- }
- default {
- #
- }
- }
-
- if {!$already_assigned} {
- if {[string match "not-*" $index] && $active_key_type in [list "" "list"]} {
- #e.g not-0-end-1 not-end-4-end-2
- set get_not 1
- #cherry-pick some easy cases, and either assign, or re-map to corresponding index
- switch -- $index {
- not-tail {
- set active_key_type "list"
- set assigned [lindex $leveldata 0]; set already_assigned 1
- }
- not-head {
- set active_key_type "list"
- #set selector "tail"; set get_not 0
- set assigned [lrange $leveldata 1 end]; set already_assigned 1
- }
- not-end {
- set active_key_type "list"
- set assigned [lrange $leveldata 0 end-1]; set already_assigned 1
- }
- default {
- #trim off the not- and let the remaining index handle based on get_not being 1
- set index [string range $index 4 end]
- }
- }
- }
- }
- }
- }
-
- if {!$already_assigned} {
-
- #keyword 'pipesyntax' at beginning of error message
- set listmsg "pipesyntax Unable to interpret subindex $index\n"
- append listmsg "selector: '$selector'\n"
- append listmsg "@ must be followed by a selector (possibly compound separated by forward slashes) suitable for lindex or lrange commands, or a not-x expression\n"
- append listmsg "Additional accepted keywords include: head tail\n"
- append listmsg "Use var@@key to treat value as a dict and retrieve element at key"
-
-
- #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against
- #need to set a corresponding action
- if {$active_key_type in [list "" "list"]} {
- set active_key_type "list"
- #for pattern matching purposes - head/tail not valid on empty lists (similar to elixir)
- if {$index eq "0"} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lindex $leveldata 0]
- } elseif {$index eq "head"} {
- #NOTE: /@head and /head both do bounds check. This is intentional
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- if {$len == 0} {
- set action ?mismatch-list-index-out-of-range-empty
- break
- }
- #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax
- set assigned [lindex $leveldata 0]
- } elseif {$index eq "end"} {
- # @end /end
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- if {$do_bounds_check && $len < 1} {
- set action ?mismatch-list-index-out-of-range
- }
- set assigned [lindex $leveldata end]
- } elseif {$index eq "tail"} {
- #NOTE: /@tail and /tail both do bounds check. This is intentional.
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list
- #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems.
- #In this way tail is different to @1-end
- if {$len == 0} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero.
- } elseif {$index eq "anyhead"} {
- # @anyhead
- #allow returning of head or nothing if empty list
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lindex $leveldata 0]
- } elseif {$index eq "anytail"} {
- # @anytail
- #allow returning of tail or nothing if empty list
- #anytail will return empty both for empty list, or single element list - but potentially useful in combination with anyhead.
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lrange $leveldata 1 end]
- } elseif {$index eq "init"} {
- # @init
- #all but last element - same as haskell 'init'
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lrange $leveldata 0 end-1]
- } elseif {$index eq "list"} {
- # @list
- #allow returning of entire list even if empty
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned $leveldata
- } elseif {$index eq "raw"} {
- #no list checking..
- set assigned $leveldata
- } elseif {$index eq "keys"} {
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- set assigned [dict keys $leveldata]
- } elseif {$index eq "values"} {
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- set assigned [dict values $leveldata]
- } elseif {$index eq "pairs"} {
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- #set assigned [dict values $leveldata]
- set pairs [list]
- tcl::dict::for {k v} $leveldata {lappend pairs [list $k $v]}
- set assigned [lindex [list $pairs [unset pairs]] 0]
- } elseif {[string is integer -strict $index]} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- # only check if @ was directly in original index section
- if {$do_bounds_check && ($index+1 > $len || $index < 0)} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- if {$get_not} {
- #already handled not-0
- set assigned [lreplace $leveldata $index $index]
- } else {
- set assigned [lindex $leveldata $index]
- }
- } elseif {[string first "end" $index] >=0} {
- if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- #leave the - from the end- as part of the offset
- set offset [expr $endspec] ;#don't brace! (consider: set x --34;puts expr $j;puts expr {$j} )
- if {$do_bounds_check && ($offset > 0 || abs($offset) >= $len)} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- if {$get_not} {
- set assigned [lreplace $leveldata $index $index]
- } else {
- set assigned [lindex $leveldata $index]
- }
- } elseif {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $index _ start end]} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- if {$do_bounds_check && [string is integer -strict $start]} {
- if {$start+1 > $len || $start < 0} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- } elseif {$start eq "end"} {
- #ok
- } elseif {$do_bounds_check} {
- set startoffset [string range $start 3 end] ;#include the - from end-
- set startoffset [expr $startoffset] ;#don't brace!
- if {$startoffset > 0 || abs($startoffset) >= $len} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- }
- if {$do_bounds_check && [string is integer -strict $end]} {
- if {$end+1 > $len || $end < 0} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- } elseif {$end eq "end"} {
- #ok
- } elseif {$do_bounds_check} {
- set endoffset [string range $end 3 end] ;#include the - from end-
- set endoffset [expr $endoffset] ;#don't brace!
- if {$endoffset > 0 || abs($endoffset) >= $len} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- }
- if {$get_not} {
- set assigned [lreplace $leveldata $start $end]
- } else {
- set assigned [lrange $leveldata $start $end]
- }
- } else {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- } elseif {[string first - $index] > 0} {
- puts "====> index:$index leveldata:$leveldata"
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- #handle pure int-int ranges separately
- set testindex [string map [list - "" + ""] $index]
- if {[string is digit -strict $testindex]} {
- #don't worry about leading - negative value for indices not valid anyway
- set parts [split $index -]
- if {[llength $parts] != 2} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- lassign $parts start end
- if {$start+1 > $len || $end+1 > $len} {
- set action ?mismatch-not-a-list
- break
- }
- if {$get_not} {
- set assigned [lreplace $leveldata $start $end]
- } else {
- set assigned [lrange $leveldata $start $end]
- }
- } else {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
-
- } else {
- #keyword 'pipesyntax' at beginning of error message
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- } else {
- #treat as dict key
- set active_key_type "dict"
- if {[dict exists $leveldata $index]} {
- set assigned [dict get $leveldata $index]
- } else {
- set action ?mismatch-dict-key-not-found
- break
- }
-
- }
- }
- set leveldata $assigned
- set rhs $leveldata
- #don't break on empty data - operations such as # and ## can return 0
- #if {![llength $leveldata]} {
- # break
- #}
- incr i_keyindex
- }
- #puts stdout "----> destructure rep leveldata: [rep $leveldata]"
- #puts stdout ">> destructure returning: [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]"
-
- #maintain key order - caller unpacks using lassign
- return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]
-
- }
- #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script
- proc destructure_func {selector data} {
- #puts stderr ".d."
- set selector [string trim $selector /]
- #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position
- #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position
-
- #map some problematic things out of the way in a manner that maintains some transparency
- #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]}
- #The selector forms part of the proc name
- #review - compare with pipecmd_namemapping
- set selector_safe [string map [list {*}{
- ?
- *
- \\
- {"}
- {$}
- "\x1b\["
- "\x1b\]"
- {[}
- {]}
- ::
- {;}
- " "
- \t
- \n
- \r
- }] $selector]
-
- set cmdname ::punk::pipecmds::destructure::_$selector_safe
- if {[info commands $cmdname] ne ""} {
- return [$cmdname $data] ;# note upvar 2 for stateful v_list_idx to be resolved in _multi_bind_result context
- }
-
- set leveldata $data
- set body [destructure_func_build_procbody $cmdname $selector $data]
-
- puts stdout ----
- puts stderr "proc $cmdname {leveldata} {"
- puts stderr $body
- puts stderr "}"
- puts stdout ---
- proc $cmdname {leveldata} $body
- #eval $script ;#create the proc
- debug.punk.pipe.compile {proc $cmdname} 4
- #return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]
- #use return - script has upvar 2 for v_list_idx to be resolved in _multi_bind_result context
- return [$cmdname $data]
- }
-
- #Builds a *basic* function to do the destructuring.
- #This is simply a set of steps to destructure each level of the data based on the hierarchical selector.
- #It just uses intermediate variables and adds some comments to the code to show the indices used at each point.
- #This may be useful in the long run as a debug/fallback mechanism - but ideally we should be building a more efficient script.
- proc destructure_func_build_procbody {cmdname selector data} {
- set script ""
- #place selector in comment in script only - if there is an error in selector we pick it up when building the script.
- #The script itself should only be returning errors in its action key of the result dictionary
- append script \n [string map [list $selector] {# set selector {}}]
- set subindices [split $selector /]
- append script \n [string map [list [list $subindices]] {# set subindices }]
- set action ?match ;#default assumption. Alternatively set to ?mismatch or ?mismatch- and always break
- append script \n {set action ?match}
- #append script \n {set assigned ""} ;#review
- set active_key_type ""
- append script \n {# set active_key_type ""}
- set lhs ""
- #append script \n [tstr {set lhs ${{$lhs}}}]
- append script \n {set lhs ""}
- set rhs ""
- append script \n {set rhs ""}
-
- set INDEX_OPERATIONS {} ;#caps to make clear in templates that this is substituted from script building scope
-
- #maintain key order - caller unpacks using lassign
- set returnline {dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs}
- set return_template {return [tcl::dict::create -assigned $leveldata -action $action -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- #set tpl_return_mismatch {return [dict create -assigned $leveldata -action ${$MISMATCH} -lhs $lhs -rhs $rhs -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch {return [dict create -assigned $leveldata -action ${$MISMATCH} -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_not_a_list {return [dict create -assigned $leveldata -action ?mismatch-not-a-list -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_list_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-list-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_list_index_out_of_range_empty {return [dict create -assigned $leveldata -action ?mismatch-list-index-out-of-range-empty -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_not_a_dict {return [dict create -assigned $leveldata -action ?mismatch-not-a-dict -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- #dict 'index' when using stateful @@ etc to iterate over dict instead of by key
- set tpl_return_mismatch_dict_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-dict-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_dict_key_not_found {return [dict create -assigned $leveldata -action ?mismatch-dict-key-not-found -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
-
-
- if {![string length $selector]} {
- #just return $leveldata
- set script {
- dict create -assigned $leveldata -action ?match -lhs "" -rhs $leveldata
- }
- return $script
- }
-
- if {[string is digit -strict [join $subindices ""]]} {
- #review tip 551 (underscores in numerical literals) (tcl9+)
- #puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
- #pure numeric keylist - put straight to lindex
- #
- #NOTE: this direct access e.g v/0/1/2 doesn't check out of bounds which is at odds with list access containing @
- #We will leave this as a syntax for different (more performant) behaviour
- #- it's potentially a little confusing - but it would be a shame not to have the possibility to take advantage of the lindex deep indexing capability in pattern matching.
- #TODO - review and/or document
- #
- #Todo - add a handler for v/n/n/n/n/# to allow unchecked counting at depth too.
- #(or more generally - loop until we hit another type of subindex)
-
- #set assigned [lindex $leveldata {*}$subindices]
- if {[llength $subindices] == 1} {
- append script \n "# index_operation listindex" \n
- lappend INDEX_OPERATIONS listindex
- } else {
- append script \n "# index_operation listindex-nested" \n
- lappend INDEX_OPERATIONS listindex-nested
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {lindex $leveldata ${$subindices}} leveldata]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
- # -- --- ---
- #append script \n $returnline \n
- append script [tstr -return string $return_template]
- return $script
- # -- --- ---
- }
- if {[string match @@* $selector]} {
- #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc
- set rawkeylist [split $selector /] ;#first key retains @@ - may be just '@@'
- set keypath [string range $selector 2 end]
- set keylist [split $keypath /]
- lappend INDEX_OPERATIONS dict_path
- if {([lindex $rawkeylist 0] ne "@@") && ([lsearch $keylist @*] == -1) && ([lsearch $keylist #*] == -1) && ([lsearch $keylist %*] == -1)} {
- #pure keylist for dict - process in one go
- #dict exists will return 0 if not a valid dict.
- # is equivalent to {*}keylist when substituted
- append script \n [tstr -return string -allowcommands {
- if {[dict exists $leveldata ${$keylist}]} {
- set leveldata [dict get $leveldata ${$keylist}]
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- append script [tstr -return string $return_template]
- return $script
- # -- --- ---
- }
- #else
- #compound keylist e.g x@@data/@0/1 or x@@/a (combined dict/list access)
- #process level by level
- }
-
-
-
- set i_keyindex 0
- append script \n {set i_keyindex 0}
- #todo - check performance impact of catches around list and dict operations - consider single catch around destructure and less specific match error info?
- foreach index $subindices {
- #set index_operation "unspecified"
- set level_script_complete 0 ;#instead of break - as we can't use data to determine break when building script
- set SUBPATH [join [lrange $subindices 0 $i_keyindex] /]
- append script \n "# ------- START index:$index subpath:$SUBPATH ------"
- set lhs $index
- append script \n "set lhs {$index}"
-
- set assigned ""
- append script \n {set assigned ""}
-
- #got_not shouldn't need to be in script
- set get_not 0
- if {[tcl::string::index $index 0] eq "!"} {
- append script \n {#get_not is true e.g !0-end-1 !end-4-end-2 !0 !@0 !@@key}
- set index [tcl::string::range $index 1 end]
- set get_not 1
- }
-
- # do_bounds_check shouldn't need to be in script
- set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning.
- #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values.
- #append script \n {set do_boundscheck 0}
- switch -exact -- $index {
- # - @# {
- #list length
- set active_key_type "list"
- if {$get_not} {
- lappend INDEX_OPERATIONS not-list
- append script \n {# set active_key_type "list" index_operation: not-list}
- append script \n {
- if {[catch {llength $leveldata}]} {
- #not a list - not-length is true
- set assigned 1
- } else {
- #is a list - not-length is false
- set assigned 0
- }
- }
- } else {
- lappend INDEX_OPERATIONS list-length
- append script \n {# set active_key_type "list" index_operation: list-length}
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} assigned]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
- }
- set level_script_complete 1
- }
- ## {
- #dict size
- set active_key_type "dict"
- if {$get_not} {
- lappend INDEX_OPERATIONS not-dict
- append script \n {# set active_key_type "dict" index_operation: not-dict}
- append script \n {
- if {[catch {dict size $leveldata}]} {
- set assigned 1 ;#not a dict - not-size is true
- } else {
- set assigned 0 ;#is a dict - not-size is false
- }
- }
- } else {
- lappend INDEX_OPERATIONS dict-size
- append script \n {# set active_key_type "dict" index_operation: dict-size}
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} assigned]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- }
- set level_script_complete 1
- }
- %# {
- set active_key_type "string"
- if {$get_not} {
- error "!%# not string length is not supported"
- }
- #string length - REVIEW -
- lappend INDEX_OPERATIONS string-length
- append script \n {# set active_key_type "" index_operation: string-length}
- append script \n {set assigned [string length $leveldata]}
- set level_script_complete 1
- }
- %%# {
- #experimental
- set active_key_type "string"
- if {$get_not} {
- error "!%%# not string length is not supported"
- }
- #string length - REVIEW -
- lappend INDEX_OPERATIONS ansistring-length
- append script \n {# set active_key_type "" index_operation: ansistring-length}
- append script \n {set assigned [ansistring length $leveldata]}
- set level_script_complete 1
- }
- %str - %string {
- set active_key_type "string"
- if {$get_not} {
- error "!%str - not string-get is not supported"
- }
- lappend INDEX_OPERATIONS string-get
- append script \n {# set active_key_type "" index_operation: string-get}
- append script \n {set assigned $leveldata}
- set level_script_complete 1
-
- #todo - %lpad- %lpadstr- %join- etc as in punk::lib::showdict
- #review - merge code shared with showdict for these operations
- }
- %sp {
- #experimental
- set active_key_type "string"
- if {$get_not} {
- error "!%sp - not string-space is not supported"
- }
- lappend INDEX_OPERATIONS string-space
- append script \n {# set active_key_type "" index_operation: string-space}
- append script \n {set assigned " "}
- set level_script_complete 1
- }
- %empty {
- #experimental
- set active_key_type "string"
- if {$get_not} {
- error "!%empty - not string-empty is not supported"
- }
- lappend INDEX_OPERATIONS string-empty
- append script \n {# set active_key_type "" index_operation: string-empty}
- append script \n {set assigned ""}
- set level_script_complete 1
- }
- @words {
- set active_key_type "string"
- if {$get_not} {
- error "!%words - not list-words-from-string is not supported"
- }
- lappend INDEX_OPERATIONS list-words-from-string
- append script \n {# set active_key_type "" index_operation: list-words-from-string}
- append script \n {set assigned [regexp -inline -all {\S+} $leveldata]}
- set level_script_complete 1
- }
- @chars {
- #experimental - leading character based on result not input(?)
- #input type is string - but output is list
- set active_key_type "list"
- if {$get_not} {
- error "!%chars - not list-chars-from-string is not supported"
- }
- lappend INDEX_OPERATIONS list-from_chars
- append script \n {# set active_key_type "" index_operation: list-chars-from-string}
- append script \n {set assigned [split $leveldata ""]}
- set level_script_complete 1
- }
- @join {
- #experimental - flatten one level of list
- #join without arg - output is list
- set active_key_type "string"
- if {$get_not} {
- error "!@join - not list-join-list is not supported"
- }
- lappend INDEX_OPERATIONS list-join-list
- append script \n {# set active_key_type "" index_operation: list-join-list}
- append script \n {set assigned [join $leveldata]}
- set level_script_complete 1
- }
- %join {
- #experimental
- #input type is list - but output is string
- set active_key_type "string"
- if {$get_not} {
- error "!%join - not string-join-list is not supported"
- }
- lappend INDEX_OPERATIONS string-join-list
- append script \n {# set active_key_type "" index_operation: string-join-list}
- append script \n {set assigned [join $leveldata ""]}
- set level_script_complete 1
- }
- %ansiview {
- #review - implemented differently in showdict.
- #(showdict uses ansistring VIEW -lf 1 )
- set active_key_type "string"
- if {$get_not} {
- error "!%# not string-ansiview is not supported"
- }
- lappend INDEX_OPERATIONS string-ansiview
- append script \n {# set active_key_type "" index_operation: string-ansiview}
- append script \n {set assigned [ansistring VIEW $leveldata]}
- set level_script_complete 1
- }
- %ansiviewstyle {
- set active_key_type "string"
- if {$get_not} {
- error "!%# not string-ansiviewstyle is not supported"
- }
- lappend INDEX_OPERATIONS string-ansiviewstyle
- append script \n {# set active_key_type "" index_operation: string-ansiviewstyle}
- append script \n {set assigned [ansistring VIEWSTYLE $leveldata]}
- set level_script_complete 1
- }
- @ {
- #as this is a stateful list next index operation - we use not (!@) to mean there is no element at the next index (instead of returning the complement ie all elements except next)
- #This is in contrast to other not operations on indices e.g /!2 which returns all elements except that at index 2
-
-
- #append script \n {puts stderr [uplevel 1 [list info vars]]}
-
- #NOTE:
- #v_list_idx in context of _multi_bind_result
- #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run)
- append script \n {upvar 2 v_list_idx v_list_idx}
-
- set active_key_type "list"
- append script \n {# set active_key_type "list" index_operation: list-get-next}
- #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey
- #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3
- #while x@,y@.= is reasonably handy - especially for args e.g $len} {
- set assigned 1
- } else {
- set assigned 0
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS get-next
- append script \n [tstr -return string -allowcommands {
- set index [expr {[incr v_list_idx(@)]-1}]
-
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$index+1 > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- } else {
- set assigned [lindex $leveldata $index]
- }
- }]
- }
- set level_script_complete 1
- }
- @* {
- set active_key_type "list"
- if {$get_not} {
- lappend INDEX_OPERATIONS list-is-empty
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- set assigned 1 ;#list is empty
- } else {
- set assigned 0
- }
- }]
- } else {
- lappend INDEX_OPERATIONS list-get-all
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- set assigned [lrange $leveldata 0 end]
- }
- }]
- }
- set level_script_complete 1
- }
- @@ {
- #stateful: tracking of index using v_dict_idx
- set active_key_type "dict"
- lappend INDEX_OPERATIONS get-next-value
- append script \n {# set active_key_type "dict" index_operation: get-next-value}
- append script \n {upvar v_dict_idx v_dict_idx} ;#review!
-
- #NOTE: it may at first seem pointless to use @@/key, since we have to know the key - but this can be used to match 'key' only at the first position in .= list key {x y} key2 etc
- #x@@ = a {x y}
- #x@@/@0 = a
- #x@@/@1 = x y
- #x@@/a = a {x y}
- # but.. as the @@ is stateful - it generally isn't very useful for multiple operations on the same pair within the pattern group.
- # (note that @@ @?@ @??@ form different subpaths - so the ? & ?? versions can be used to test match prior to @@ without affecting the index)
- #review - might be more useful if they shared an index ?
- # It is analogous to v1@,v2@ for lists.
- # @pairs is more useful for repeated operations
-
-
- set indent " "
- set assignment_script [string map [list \r\n "\r\n$indent" \n "\n$indent" ] {
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- set assigned [list $k [dict get $leveldata $k]]
- } else {
- ${[tstr -ret string $tpl_return_mismatch_dict_index_out_of_range]}
- }
- }]
-
- set assignment_script [tstr -ret string -allowcommands $assignment_script]
-
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set next_this_level [incr v_dict_idx(${$SUBPATH})]
- set keyindex [expr {$next_this_level -1}]
- ${$assignment_script}
- }
- }]
- set level_script_complete 1
- }
- @?@ {
- #stateful: tracking of index using v_dict_idx
- set active_key_type "dict"
- lappend INDEX_OPERATIONS get?-next-value
- append script \n {# set active_key_type "dict" index_operation: get?-next-value}
- append script \n {upvar v_dict_idx v_dict_idx} ;#review!
- set indent " "
- set assignment_script [string map [list \r\n "\r\n$indent" \n "\n$indent" ] {
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- set assigned [dict get $leveldata $k]
- } else {
- set assigned [list]
- }
- }]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set next_this_level [incr v_dict_idx(${$SUBPATH})]
- set keyindex [expr {$next_this_level -1}]
- ${$assignment_script}
- }
- }]
- set level_script_complete 1
- }
- @??@ {
- set active_key_type "dict"
- lappend INDEX_OPERATIONS get?-next-pair
- append script \n {# set active_key_type "dict" index_operation: get?-next-pair}
- append script \n {upvar v_dict_idx v_dict_idx} ;#review!
- set indent " "
- set assignment_script [string map [list \r\n "\r\n$indent" \n "\n$indent" ] {
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- set assigned [list $k [dict get $leveldata $k]]
- } else {
- set assigned [list]
- }
- }]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set next_this_level [incr v_dict_idx(${$SUBPATH})]
- set keyindex [expr {$next_this_level -1}]
- ${$assignment_script}
- }
- }]
- set level_script_complete 1
- }
- @vv@ - @VV@ - @kk@ - @KK@ {
- error "unsupported index $index"
- }
- default {
-
- #assert rules for values within @@
- #glob search is done only if there is at least one * within @@
- #if there is at least one ? within @@ - then a non match will not raise an error (quiet)
-
- #single or no char between @@:
- #lookup/search is based on key - return is values
-
- #double char within @@:
- #anything with a dot returns k v pairs e.g @k.@ @v.@ @..@
- #anything that is a duplicate returns k v pairs e.g @kk@ @vv@ @**@
- #anything with a letter and a star returns the type of the letter, and the search is based on the position of the star where posn 1 is for key, posn 2 is for value
- #e.g @k*@ returns keys - search on values
- #e.g @*k@ returns keys - search on keys
- #e.g @v*@ returns values - search on values
- #e.g @*v@ returns values - search on keys
-
- switch -glob -- $index {
- @@* {
- #exact key match - return value
- #noisy get value - complain if key non-existent
- #doesn't complain if not a dict - because we use 'tcl::dict::exists' which will return false without error even if the value isn't dict-shaped
- set active_key_type "dict"
- set key [string range $index 2 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey-get-value-not
- #review - dict remove allows silent call if key doesn't exist - but we are enforcing existence here
- #this seems reasonable given we have an explicit @?@ syntax (nocomplain equivalent) and there could be a legitimate case for wanting a non-match if trying to return the complement of a non-existent key
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey-get-value-not
- if {[dict exists $leveldata ${$key}]} {
- set assigned [dict values [dict remove $leveldata ${$key}]]
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactkey-get-value
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict index_operation: exactkey-get-value"
- if {[dict exists $leveldata ${$key}]} {
- set assigned [dict get $leveldata ${$key}]
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
- set level_script_complete 1
- }
- {@\?@*} {
- #exact key match - quiet get value
- #silent empty result if non-existent key - silence when non-existent key also if using not-@?@badkey which will just return whole dict
- #note - dict remove will raise error on non-dict-shaped value whilst dict exists will not
- set active_key_type "dict"
- set key [string range $index 3 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey?-get-value-not
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey?-get-value-not
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set assigned [dict values [dict remove $leveldata ${$key}]]
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactkey?-get-value
- #dict exists test is safe - no need for catch
- append script \n [string map [list $key] {
- # set active_key_type "dict" index_operation: exactkey?-get-value
- if {[dict exists $leveldata ]} {
- set assigned [dict get $leveldata ]
- } else {
- set assigned [dict create]
- }
- }]
- }
- set level_script_complete 1
- }
- {@\?\?@*} {
- #quiet get pairs
- #this is silent too.. so how do we do a checked return of dict key+val?
- set active_key_type "dict"
- set key [string range $index 4 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey?-get-pair-not
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey?-get-pair-not
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set assigned [dict remove $leveldata ${$key}]
- }]
- } else {
- lappend INDEX_OPERATIONS exactkey?-get-pair
- append script \n [string map [list $key] {
- # set active_key_type "dict" index_operation: exactkey?-get-pair
- if {[dict exists $leveldata ]} {
- set assigned [dict create [dict get $leveldata ]]
- } else {
- set assigned [dict create]
- }
- }]
- }
- set level_script_complete 1
- }
- @..@* - @kk@* - @KK@* {
- #noisy get pairs by key
- set active_key_type "dict"
- set key [string range $index 4 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey-get-pairs-not
- #review - dict remove allows silent call if key doesn't exist - but we are enforcing existence here
- #this seems reasonable given we have an explicit @?@ syntax (nocomplain equivalent) and there could be a legitimate case for wanting a non-match if trying to return the complement of a non-existent key
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey-get-pairs-not
- if {[dict exists $leveldata ${$key}]} {
- set assigned [tcl::dict::remove $leveldata ${$key}]
- } else {
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactkey-get-pairs
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict index_operation: exactkey-get-pairs"
- if {[dict exists $leveldata ${$key}]} {
- tcl::dict::set assigned ${$key} [tcl::dict::get $leveldata ${$key}]
- } else {
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
- set level_script_complete 1
-
- }
- @vv@* - @VV@* {
- #noisy(?) get pairs by exact value
- #return mismatch on non-match even when not- specified
- set active_key_type "dict"
- set keyglob [string range $index 4 end]
- set active_key_type "dict"
- set key [string range $index 4 end]
- if {$get_not} {
- #review - for consistency we are reporting a mismatch when the antikey being looked up doesn't exist
- #The utility of this is debatable
- lappend INDEX_OPERATIONS exactvalue-get-pairs-not
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactvalue-get-pairs-not
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set nonmatches [dict create]
- tcl::dict::for {k v} $leveldata {
- if {![string equal ${$key} $v]} {
- dict set nonmatches $k $v
- }
- }
-
- if {[dict size $nonmatches] < [dict size $leveldata]} {
- #our key matched something
- set assigned $nonmatches
- } else {
- #our key didn't match anything - don't return the nonmatches
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactvalue-get-pairs
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict index_operation: exactvalue-get-pairs-not"
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set matches [list]
- tcl::dict::for {k v} $leveldata {
- if {[string equal ${$key} $v]} {
- lappend matches $k $v
- }
- }
- if {[llength $matches]} {
- set assigned $matches
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
- set level_script_complete 1
-
- }
- {@\*@*} - {@\*v@*} - {@\*V@*} {
- #dict key glob - return values only
- set active_key_type "dict"
- if {[string match {@\*@*} $index]} {
- set keyglob [string range $index 3 end]
- } else {
- #vV
- set keyglob [string range $index 4 end]
- }
- #if $keyglob eq "" - needs to query for dict key that is empty string.
- if {$get_not} {
- lappend INDEX_OPERATIONS globkey-get-values-not
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- # set active_key_type "dict" index_operation: globkey-get-values-not
- set matched [dict keys $leveldata {${$keyglob}}]
- set assigned [dict values [dict remove $leveldata {*}$matched]]
- }]
-
- } else {
- lappend INDEX_OPERATIONS globkey-get-values
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: globkey-get-values
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set matched [dict keys $leveldata {${$keyglob}}]
- set assigned [list]
- foreach m $matched {
- lappend assigned [dict get $leveldata $m]
- }
- }]
- }
- set level_script_complete 1
-
- }
- {@\*.@*} {
- #dict key glob - return pairs
- set active_key_type "dict"
- set keyglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globkey-get-pairs-not
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operation: globkey-get-pairs-not
- set matched [dict keys $leveldata {}]
- set assigned [dict remove $leveldata {*}$matched]
- }]
-
- } else {
- lappend INDEX_OPERATIONS globkey-get-pairs
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operations: globkey-get-pairs
- set matched [dict keys $leveldata {}]
- set assigned [dict create]
- foreach m $matched {
- dict set assigned $m [dict get $leveldata $m]
- }
- }]
- }
- set level_script_complete 1
- }
- {@\*k@*} - {@\*K@*} {
- #dict key glob - return keys
- set active_key_type "dict"
- set keyglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globkey-get-keys-not
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operation: globkey-get-keys-not
- set matched [dict keys $leveldata {}]
- set assigned [dict keys [dict remove $leveldata {*}$matched]]
- }]
-
- } else {
- lappend INDEX_OPERATIONS globkey-get-keys
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operation: globkey-get-keys
- set assigned [dict keys $leveldata {}]
- }]
- }
- set level_script_complete 1
- }
- {@k\*@*} - {@K\*@*} {
- #dict value glob - return keys
- set active_key_type "dict"
- set valglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globvalue-get-keys-not
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-keys-not
- set assigned [list]
- tcl::dict::for {k v} $leveldata {
- if {![string match {} $v]} {
- lappend assigned $k
- }
- }
- }]
- } else {
- lappend INDEX_OPERATIONS globvalue-get-keys
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-keys
- set assigned [list]
- tcl::dict::for {k v} $leveldata {
- if {[string match {} $v]} {
- lappend assigned $k
- }
- }
- }]
- }
- set level_script_complete 1
- }
- {@.\*@*} {
- #dict value glob - return pairs
- set active_key_type "dict"
- set valglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globvalue-get-pairs-not
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-pairs-not
- set assigned [dict create]
- tcl::dict::for {k v} $leveldata {
- if {![string match {} $v]} {
- dict set assigned $k $v
- }
- }
- }]
- } else {
- lappend INDEX_OPERATIONS globvalue-get-pairs
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-pairs
- set assigned [dict create]
- tcl::dict::for {k v} $leveldata {
- if {[string match {} $v]} {
- dict set assigned $k $v
- }
- }
- }]
- }
- set level_script_complete 1
- }
- {@V\*@*} - {@v\*@*} {
- #dict value glob - return values
- set active_key_type dict
- set valglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globvalue-get-values-not
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" ;# index_operation: globvalue-get-values-not
- set assigned [list]
- tcl::dict::for {k v} $leveldata {
- if {![string match {} $v]} {
- lappend assigned $v
- }
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS globvalue-get-values
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" ;#index_operation: globvalue-get-value
- set assigned [dict values $leveldata ]
- }]
- }
- set level_script_complete 1
-
- }
- {@\*\*@*} {
- #dict val/key glob return pairs)
- set active_key_type "dict"
- set keyvalglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globkeyvalue-get-pairs-not
- error "globkeyvalue-get-pairs-not todo"
- } else {
- lappend INDEX_OPERATIONS globkeyvalue-get-pairs
- append script \n [string map [list $keyvalglob] {
- # set active_key_type "dict" ;# index_operation: globkeyvalue-get-pairs-not
- set assigned [dict create]
- tcl::dict::for {k v} $leveldata {
- if {[string match {} $k] || [string match {} $v]} {
- dict set assigned $k $v
- }
- }
- }]
- }
- set level_script_complete 1
- puts stderr "globkeyvalue-get-pairs review"
- }
- @* {
- set active_key_type "list"
- set do_bounds_check 1
-
- set index [string trimleft $index @]
- append script \n [string map [list $index] {
- # set active_key_type "list" index_operation: ?
- set index
- }]
- }
- %split-* {
- #split on one or more chars - review
- #set hidekey 1
- #lassign [split $key -] _ splitchars
- #set thisval [split $dval $splitchars]
- set active_key_type "string"
- set splitchars [string range $index 7 end]
- append script \n [string map [list $splitchars] {
- # set active_key_type "string" index_operation: split-string
- #e.g supports %split-"\\n"= "l1\n\nl3" -> {l1 "" l3}
- set splitchars ""
- set assigned [split $leveldata $splitchars]
- }]
- set level_script_complete 1
-
- #todo %splitat- %splitn- ??
- }
- %lpad-* {
- #moved from punk::lib::showdict patterns.
- #set hidekey 1
- #lassign [split $key -] _ extra
- #set width [expr {[textblock::width $dval] + $extra}]
- #set thisval [textblock::pad $dval -which left -width $width]
- set active_key_type "string"
- set extra [string range $index 6 end]
- append script \n [string map [list $extra] {
- # set active_key_type "string" index_operation: lpad-string
- set extra ""
- set width [expr {[textblock::width $leveldata] + $extra}]
- set assigned [textblock::pad $leveldata -which left -width $width]
- }]
- set level_script_complete 1
- }
- %* {
- #see above re %lpad- etc and synchronizing with showdict
- set active_key_type "string"
- set do_bounds_check 0
- set index [string range $index 1 end]
- append script \n [string map [list $index] {
- # set active_key_type "string" index_operation: ?
- set index
- }]
- }
- default {
- puts "destructure_func_build_body unmatched index $index"
- }
- }
- }
- }
-
- if {!$level_script_complete} {
-
-
- #keyword 'pipesyntax' at beginning of error message
- set listmsg "pipesyntax Unable to interpret subindex $index\n"
- append listmsg "selector: '$selector'\n"
- append listmsg "@ must be followed by a selector (possibly compound separated by forward slashes) suitable for lindex or lrange commands, or a not-x expression\n"
- append listmsg "Additional accepted keywords include: head tail\n"
- append listmsg "Use var@@key to treat value as a dict and retrieve element at key"
-
- #append script \n [string map [list $listmsg] {set listmsg ""}]
-
-
-
- #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against
- #need to set a corresponding action
- if {$active_key_type in [list "" "list"]} {
- set active_key_type "list"
- append script \n {# set active_key_type "list"}
- #for pattern matching purposes - head/tail not valid on empty lists (similar to elixir)
- switch -exact -- $index {
- 0 {
- if {$get_not} {
- append script \n "# index_operation listindex-int-not" \n
- lappend INDEX_OPERATIONS listindex-zero-not
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- } else {
- lappend INDEX_OPERATIONS listindex-zero
- set assignment_script {set assigned [lindex $leveldata 0]}
- if {$do_bounds_check} {
- append script \n "# index_operation listindex-int (bounds checked)" \n
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {[llength $leveldata] == 0} {
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- ${$assignment_script}
- }
- }]
- } else {
- append script \n "# index_operation listindex-int" \n
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- }
- }
- head {
- #NOTE: /@head and /head both do bounds check. This is intentional
- if {$get_not} {
- append script \n "# index_operation listindex-head-not" \n
- lappend INDEX_OPERATIONS listindex-head-not
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- } else {
- append script \n "# index_operation listindex-head" \n
- lappend INDEX_OPERATIONS listindex-head
- set assignment_script {set assigned [lindex $leveldata 0]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- #set action ?mismatch-list-index-out-of-range-empty
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax
- ${$assignment_script}
- }
- }]
- }
- end {
- if {$get_not} {
- append script \n "# index_operation listindex-end-not" \n
- lappend INDEX_OPERATIONS listindex-end-not
- #on single element list Tcl's lrange will do what we want here and return nothing
- set assignment_script {set assigned [lrange $leveldata 0 end-1]}
- } else {
- append script \n "# index_operation listindex-end" \n
- lappend INDEX_OPERATIONS listindex-end
- set assignment_script {set assigned [lindex $leveldata end]}
- }
- if {$do_bounds_check} {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- ${$assignment_script}
- }
- }]
- } else {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- }
- tail {
- #NOTE: /@tail and /tail both do bounds check. This is intentional.
- #
- #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list
- #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems.
- #In this way tail is different to @1-end
- if {$get_not} {
- append script \n "# index_operation listindex-tail-not" \n
- lappend INDEX_OPERATIONS listindex-tail-not
- set assignment_script {set assigned [lindex $leveldata 0]}
- } else {
- append script \n "# index_operation listindex-tail" \n
- lappend INDEX_OPERATIONS listindex-tail
- set assignment_script {set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- anyhead {
- #allow returning of head or nothing if empty list
- if {$get_not} {
- append script \n "# index_operation listindex-anyhead-not" \n
- lappend INDEX_OPERATIONS listindex-anyhead-not
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- } else {
- append script \n "# index_operation listindex-anyhead" \n
- lappend INDEX_OPERATIONS listindex-anyhead
- set assignment_script {set assigned [lindex $leveldata 0]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- anytail {
- #allow returning of tail or nothing if empty list
- #anytail will return empty both for empty list, or single element list - but potentially useful in combination with anyhead.
- if {$get_not} {
- append script \n "# index_operation listindex-anytail-not" \n
- lappend INDEX_OPERATIONS listindex-anytail-not
- set assignment_script {set assigned [lindex $leveldata 0]}
- } else {
- append script \n "# index_operation listindex-anytail" \n
- lappend INDEX_OPERATIONS listindex-anytail
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- init {
- #all but last element - same as haskell 'init'
- #counterintuitively, get-notinit can therefore return first element if it is a single element list
- #does bounds_check for get-not@init make sense here? maybe - review
- if {$get_not} {
- append script \n "# index_operation listindex-init-not" \n
- lappend INDEX_OPERATIONS listindex-init-not
- set assignment_script {set assigned [lindex $leveldata end]}
- } else {
- append script \n "# index_operation listindex-init" \n
- lappend INDEX_OPERATIONS listindex-init
- set assignment_script {set assigned [lrange $leveldata 0 end-1]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- list {
- #get_not?
- #allow returning of entire list even if empty
- if {$get_not} {
- lappend INDEX_OPERATIONS list-getall-not
- set assignment_script {set assigned {}}
- } else {
- lappend INDEX_OPERATIONS list-getall
- set assignment_script {set assigned $leveldata}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- raw {
- #get_not - return nothing??
- #no list checking..
- if {$get_not} {
- lappend INDEX_OPERATIONS getraw-not
- append script \n {set assigned {}}
- } else {
- lappend INDEX_OPERATIONS getraw
- append script \n {set assigned $leveldata}
- }
- }
- keys {
- #@get_not??
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {$get_not} {
- lappend INDEX_OPERATIONS list-getkeys-not
- set assignment_script {set assigned [dict values $leveldata]} ;#not-keys is equivalent to values
- } else {
- lappend INDEX_OPERATIONS list-getkeys
- set assignment_script {set assigned [dict keys $leveldata]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- values {
- #get_not ??
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {$get_not} {
- lappend INDEX_OPERATIONS list-getvalues-not
- set assignment_script {set assigned [dict keys $leveldata]} ;#not-values is equivalent to keys
- } else {
- lappend INDEX_OPERATIONS list-getvalues
- set assignment_script {set assigned [dict values $leveldata]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- pairs {
- #get_not ??
- if {$get_not} {
- #review - return empty list instead like not-list and not-raw?
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector not-pairs_not_supported]
- } else {
- lappend INDEX_OPERATIONS list-getpairs
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set pairs [list]
- tcl::dict::for {k v} $leveldata {lappend pairs [list $k $v]}
- set assigned [lindex [list $pairs [unset pairs]] 0]
- }
- }]
- }
- default {
- if {[regexp {[?*]} $index]} {
- if {$get_not} {
- lappend INDEX_OPERATIONS listsearch-not
- set assign_script [string map [list $index] {
- set assigned [lsearch -all -inline -not $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS listsearch
- set assign_script [string map [list $index] {
- set assigned [lsearch -all -inline $leveldata ]
- }]
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- } elseif {[string is integer -strict $index]} {
- if {$get_not} {
- lappend INDEX_OPERATIONS listindex-not
- set assign_script [string map [list $index] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS listindex
- set assign_script [string map [list $index] {set assigned [lindex $leveldata ]}]
- }
-
- if {$do_bounds_check} {
- if {$index < 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector index_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- set max [expr {$index + 1}]
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- # bounds_check due to @ directly specified in original index section
- if {${$max} > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- } else {
- ${$assign_script}
- }
- }
- }]
- } else {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- }
- } elseif {[punk::lib::is_indexset $index]} {
- #review - a basic math statement such as 5-1 is also a valid member of an indexset
- #see punk::lib::is_indexset and punk::lib::indexset_resolve
- #single element of an indexset - e.g @..3 or @1..5 or @..end or @.. or @end..0 or @end-5..8 etc
- set is_range [expr {[string first ".." $index] >= 0}]
- if {$get_not} {
- if {$is_range} {
- lappend INDEX_OPERATIONS list-range-not
- } else {
- lappend INDEX_OPERATIONS listindex-not
- }
- set assign_script {
- set assigned [lremove $assigned {*}[punk::lib::indexset_resolve [llength $leveldata] ]]
- }
- } else {
- if {$is_range} {
- lappend INDEX_OPERATIONS list-range
- #todo - if we know it's a contiguous range, we could use lrange here instead of lindex
- #we would also need to detect if it's a reverse range such as @5..1 and handle that correctly
- #- lrange doesn't support reverse ranges, but we could resolve the indexset to a list of indices
- #and then use lindex with that list of indices to get the correct result.
- #we don't always know at this point if the range is in reverse or not because we don't know the size of the list until
- #runtime - so we will handle both cases in the same way for now.
- #e.g for index 5..end-6 - this could be forward or reverse depending on the length of the list.
- set assign_script {
- set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] ] {lindex $leveldata $i}]
- }
- } else {
- lappend INDEX_OPERATIONS listindex
- set assign_script {
- set assigned [lindex $leveldata [punk::lib::indexset_resolve [llength $leveldata] ]]
- }
- }
- }
-
- if {$do_bounds_check} {
- #bounds check each element of the resolved indexset - if any are out of bounds, return mismatch-list-index-out-of-range
- if {$is_range} {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- lassign [split ..] idx1 _ idx2
- set v2 [punk::lib::lindex_resolve_basic $len $idx2]
- if {isinf($v2)} {
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- set v1 [punk::lib::lindex_resolve_basic $len $idx1]
- if {isinf($v1)} {
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- ${$assign_script}
- }
- }]
- } else {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- set v1 [punk::lib::lindex_resolve_basic $len ]
- if {isinf($v1)} {
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- ${$assign_script}
- }
- }]
- }
- } else {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- }
- set script [string map [list $index] $script]
- } elseif {[string first "end" $index] >=0} {
- #review - obsoleted by indexset syntax. prune branch?
- puts stderr "index with end detected - review if this branch still reachable - prune? $index"
- if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {
-
- if {$get_not} {
- lappend INDEX_OPERATIONS listindex-endoffset-not
- set assign_script [string map [list $index] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS listindex-endoffset
- set assign_script [string map [list $index ] {set assigned [lindex $leveldata ]}]
- }
-
- if {$do_bounds_check} {
- #tstr won't add braces - so the ${$endspec} value inserted in the expr will remain unbraced as required in this case.
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- } else {
- #bounds-check is true
- #leave the - from the end- as part of the offset
- set offset [expr ${$endspec}] ;#don't brace!
- if {($offset > 0 || abs($offset) >= $len)} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- } else {
- ${$assign_script}
- }
- }
- }]
- } else {
- append script \n [tstr -ret string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- }
-
- } elseif {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $index _ start end]} {
- #review - obsoleted by indexset syntax. prune branch?
- puts stderr "index with range and end detected - review if this branch still reachable - prune? $index"
- if {$get_not} {
- lappend INDEX_OPERATIONS list-range-not
- set assign_script [string map [list $start $end ] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS list-range
- set assign_script [string map [list $start $end] {set assigned [lrange $leveldata ]}]
- }
-
- append script \n [tstr -ret string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
-
- if {$do_bounds_check} {
- if {[string is integer -strict $start]} {
- if {$start < 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector start_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- append script \n [tstr -return string -allowcommands {
- set start ${$start}
- if {$start+1 > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- } elseif {$start eq "end"} {
- #noop
- } else {
- set startoffset [string range $start 3 end] ;#include the - from end-
- set startoffset [expr $startoffset] ;#don't brace!
- if {$startoffset > 0} {
- #e.g end+1
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on]
-
- }
- append script \n [tstr -return string -allowcommands {
- set startoffset ${$startoffset}
- if {abs($startoffset) >= $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- }
- if {[string is integer -strict $end]} {
- if {$end < 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- append script \n [tstr -return string -allowcommands {
- set end ${$end}
- if {$end+1 > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- } elseif {$end eq "end"} {
- #noop
- } else {
- set endoffset [string range $end 3 end] ;#include the - from end-
-
- set endoffset [expr $endoffset] ;#don't brace!
- if {$endoffset > 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- append script \n [tstr -return string -allowcommands {
- set endoffset ${$endoffset}
- if {abs($endoffset) >= $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- }
- }
-
- append script \n [string map [list $assign_script] {
- if {![string match ?mismatch-* $action]} {
-
- }
- }]
-
- } else {
- #fail now - no need for script
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- } elseif {[string first - $index] > 0} {
- puts stderr "index with - detected - review if this branch still reachable - prune? $index"
- #review - we changed to detect indexset above.
- #syntax @m-n should be deprecated in favour of @m..n
- #todo - check if this branch still reachable - prune?
- #e.g @1-3 gets here
- #JMN
- if {$get_not} {
- lappend INDEX_OPERATIONS list-range-not
- } else {
- lappend INDEX_OPERATIONS list-range
- }
-
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
-
- #handle pure int-int ranges separately
- set testindex [string map [list - "" + ""] $index]
- if {[string is digit -strict $testindex]} {
- #don't worry about leading - negative value for indices not valid anyway
- set parts [split $index -]
- if {[llength $parts] != 2} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- lassign $parts start end
-
- #review - Tcl lrange just returns nothing silently.
- #if we don't intend to implement reverse indexing - we should probably not emit an error
- if {$start > $end} {
- puts stderr "pipesyntax for selector $selector error - reverse index unimplemented"
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- if {$do_bounds_check} {
- #append script [string map [list $start $end] {
- # set start
- # set end
- # if {$start+1 > $len || $end+1 > $len} {
- # set action ?mismatch-list-index-out-of-range
- # }
- #}]
- #set eplusone [expr {$end+1}]
- append script [tstr -return string -allowcommands {
- if {$len < ${[expr {$end+1}]}} {
- set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- }
-
-
- if {$get_not} {
- set assign_script [string map [list $start $end] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- set assign_script [string map [list $start $end] {set assigned [lrange $leveldata ]}]
- }
-
-
- } else {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
-
- append script \n [string map [list $assign_script] {
- if {![string match ?mismatch-* $action]} {
-
- }
- }]
-
- } else {
- #keyword 'pipesyntax' at beginning of error message
- #pipesyntax error - no need to even build script - can fail now
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- }
- }
- } elseif {$active_key_type eq "string"} {
- #changed to indexset notation m..n allowing eg 2..end-1 etc.
- #if {[string match *-* $index]} {}
-
- if {[punk::lib::is_indexset $index]} {
- #review - we are assuming a single element indexset here - ie no comma separated sets.
-
- #todo - support $get_not
- #todo - consider bounds_check for string indices.
- # - Tcl doesn't do bounds checking for string index, but we need to consider in the context of pattern-matching
- # whether we want to support syntaxes for with and without bounds checking on string indices.
-
- set is_range [expr {[string first ".." $index] >= 0}]
- if {$is_range} {
- lappend INDEX_OPERATIONS string-range
- #review - not efficient for contiguous monotonically increasing ranges
- #because we are retrievinng each character individually and concatenating
- #- but it is more flexible because it also supports reverse ranges and could support non-contiguous ranges such as @0,2,4..6
- set assign_script {
- set assigned [join [lmap i [punk::lib::indexset_resolve [string length $leveldata] ] {string index $leveldata $i}] ""]
- }
- } else {
- lappend INDEX_OPERATIONS string-index
- set assign_script {
- set assigned [string index $leveldata [punk::lib::indexset_resolve [string length $leveldata] ]]
- }
- }
-
- #set assign_script {
- # set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] ] {lindex $leveldata $i}]
- #}
-
- #todo - consider where/if we can support 'ansistring INDEX' for ANSI strings.
- #if so - it shouldn't overload the % operator we currently use for string access.
- append script \n [tstr -return string -allowcommands {
- if {$leveldata eq ""} {
- set assigned ""
- } else {
- ${$assign_script}
- }
- }]
- set script [string map [list $index] $script]
-
-
- #set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$}
- ##todo - support more complex indices: 0-end-1 etc
-
- #lassign [split $index -] a b
- #append script \n [tstr -return string -allowcommands {
- # # set active_key_type "string"
- # set assigned [string range $leveldata ${$a} ${$b}]
- #}]
-
- } else {
- if {$index eq "*"} {
- #equivalent to indexset ".."
- lappend INDEX_OPERATIONS string-all
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "string"
- set assigned $leveldata
- }]
- } elseif {[regexp {[?*]} $index]} {
- lappend INDEX_OPERATIONS string-globmatch
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "string"
- if {[string match $index $leveldata]} {
- set assigned $leveldata
- } else {
- set assigned ""
- }
- }]
- } else {
- lappend INDEX_OPERATIONS string-index
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "string"
- set assigned [string index $leveldata ${$index}]
- }]
- }
- }
-
- } else {
- #treat as dict key
- if {$get_not} {
- #dict remove can accept non-existent keys.. review do we require not-@?@key to get silence?
- append script \n [tstr -return string {
- set assigned [dict remove $leveldata ${$index}]
- }]
- } else {
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict"
- if {[dict exists $leveldata {${$index}}]} {
- set assigned [dict get $leveldata {${$index}}]
- } else {
- set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
-
- }
-
-
- } ;# end if $level_script_complete
-
-
- append script \n {
- set leveldata $assigned
- }
- incr i_keyindex
- append script \n "# ------- END index $index ------"
- } ;# end foreach
-
-
-
- #puts stdout "----> destructure rep leveldata: [rep $leveldata]"
- #puts stdout ">> destructure returning: [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]"
-
- #maintain key order - caller unpacks using lassign
- #append script \n {dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs}
- append script \n [tstr -return string $return_template] \n
- return $script
- }
-
-
-
-
- #called from match_assign/know_dot_assign for lhs of assignment - uplevel 2 to caller's level
- #called from match_assign/know_dot_assign for rhs pipelined vars - uplevel 1 to write vars only in 'apply' scope
- #return a dict with keys result, setvars, unsetvars
- #TODO - implement cross-binding (as opposed to overwrite/reassignment) when a var appears multiple times in a pattern/multivar
- #e.g x@0,x@1 will only match if value at positions 0 & 1 is the same (a form of auto-pinning?)
- #e.g x,x@0 will only match a single element list
- #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline)
- # non-blocking or - p1||p2 if p1 matches - return p1 and continue pipeline - else match p2 and continue pipeline
- proc _multi_bind_result {multivar data args} {
- #puts stdout "---- _multi_bind_result multivar:'$multivar' data:'$data' options:'$args'"
- #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1
- if {![string length $multivar]} {
- #treat the absence of a pattern as a match to anything
- #JMN2 - changed to list based destructuring
- return [dict create ismatch 1 result $data setvars {} script {}]
- #return [dict create ismatch 1 result [list $data] setvars {} script {}]
- }
- set returndict [dict create ismatch 0 result "" setvars {}]
- set script ""
-
- set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0]
- set opts [dict merge $defaults $args]
- set unset [dict get $opts -unset]
- set lvlup [dict get $opts -levelup]
- set get_mismatchinfo [dict get $opts -mismatchinfo]
-
-
-
- #first classify into var_returntype of either "pipeline" or "segment"
- #segment returntype is indicated by leading %
-
- set varinfo [punk::pipe::lib::_var_classify $multivar]
- set var_names [dict get $varinfo var_names]
- set var_class [dict get $varinfo var_class]
- set varspecs_trimmed [dict get $varinfo varspecs_trimmed]
-
- set var_actions [list]
- set expected_values [list]
- #e.g {a = abc} {b set ""}
- foreach classinfo $var_class vname $var_names {
- lassign [lindex $classinfo 0] v
- lappend var_actions [list $v "" ""] ;#varactions keeps original lhs - not trimmed version
- lappend expected_values [list var $vname spec $v info - lhs - rhs -] ;#code looks for 'info -' to see if changed from default
- }
-
- #puts stdout "var_actions: $var_actions"
- #puts stdout "expected_values: $expected_values"
-
-
- #puts stdout "\n var_class: $var_class\n"
- # e.g {{x {}} 0} {{y @0} 0} {{'ok @0} 1} {{^v @@key} 2}
-
- #set varspecs_trimmed [lmap varinfo $var_class {expr {([lindex $varinfo 1] > 0) ? [list [string range [lindex $varinfo 0 0] 1 end] [lindex $varinfo 0 1]] : [lindex $varinfo 0]}}]
- #puts stdout "\n varspecs_trimmed: $varspecs_trimmed\n"
-
-
- #var names (possibly empty portion to the left of )
- #debug.punk.pipe.var "varnames: $var_names" 4
-
- set v_list_idx(@) 0 ;#for spec with single @ only
- set v_dict_idx(@@) 0 ;#for spec with @@ only
-
- #jn
-
- #member lists of returndict which will be appended to in the initial value-retrieving loop
- set returndict_setvars [dict get $returndict setvars]
-
- set assigned_values [list]
-
-
- #varname action value - where value is value to be set if action is set
- #actions:
- # "" unconfigured - assert none remain unconfigured at end
- # noop no-change
- # matchvar-set name is a var to be matched
- # matchatom-set names is an atom to be matched
- # matchglob-set
- # set
- # question mark versions are temporary - awaiting a check of action vs var_class
- # e.g ?set may be changed to matchvar or matchatom or set
-
-
- debug.punk.pipe.var {initial map expected_values: $expected_values} 5
-
- set returnval ""
- set i 0
- #assertion i incremented at each continue and at each end of loop - at end i == list length + 1
- #always use 'assigned' var in each loop
- # (for consistency and to assist with returnval)
- # ^var means a pinned variable - compare value of $var to rhs - don't assign
- #
- # In this loop we don't set variables - but assign an action entry in var_actions - all with leading question mark.
- # as well as adding the data values to the var_actions list
- #
- # TODO! we may (commonly) encounter same vkey in the pattern - no need to reparse and re-fetch from data!
- set vkeys_seen [list]
- foreach v_and_key $varspecs_trimmed {
- set vspec [join $v_and_key ""]
- lassign $v_and_key v vkey
-
- set assigned ""
- #The binding spec begins at first @ or # or /
-
- #set firstq [string first "'" $vspec]
- #set v [lindex $var_names $i]
- #if v contains any * and/or ? - then it is a glob match - not a varname
-
- lassign [destructure_func $vkey $data] _assigned assigned _action matchaction _lhs lhs _rhs rhs
- if {$matchaction eq "?match"} {
- set matchaction "?set"
- }
- lset var_actions $i 1 $matchaction
- lset var_actions $i 2 $assigned
-
- #update the setvars/unsetvars elements
- if {[string length $v]} {
- dict set returndict_setvars $v $assigned
- }
-
- #JMN2
- #special case expansion for empty varspec (e.g , or ,,)
- #if {$vspec eq ""} {
- # lappend assigned_values {*}$assigned
- #} else {
- lappend assigned_values $assigned
- #}
- incr i
- }
-
- #todo - fix! this isn't the actual tclvars that were set!
- dict set returndict setvars $returndict_setvars
-
- #assigned_values is the ordered list of source elements in the data (rhs) as extracted by each position-spec
- #For booleans the final val may later be normalised to 0 or 1
-
-
- #assertion all var_actions were set with leading question mark
- #perform assignments only if matched ok
-
-
- #0 - novar
- #1 - atom '
- #2 - pin ^
- #3 - boolean &
- #4 - integer
- #5 - double
- #6 - var
- #7 - glob (no classifier and contains * or ?)
- if 0 {
- debug.punk.pipe.var {VAR_CLASS: $var_class} 5
- debug.punk.pipe.var {VARACTIONS: $var_actions} 5
- debug.punk.pipe.var {VARSPECS_TRIMMED: $varspecs_trimmed} 5
-
- debug.punk.pipe.var {atoms: [lsearch -all -inline -index 1 $var_class 1]} 5
- debug.punk.pipe.var {pins: [lsearch -all -inline -index 1 $var_class 2]} 5
- debug.punk.pipe.var {bools: [lsearch -all -inline -index 1 $var_class 3]} 5
- debug.punk.pipe.var {ints: [lsearch -all -inline -index 1 $var_class 4]} 5
- debug.punk.pipe.var {doubles: [lsearch -all -inline -index 1 $var_class 5]} 5
- debug.punk.pipe.var {vars: [lsearch -all -inline -index 1 $var_class 6]} 5
- debug.punk.pipe.var {globs: [lsearch -all -inline -index 1 $var_class 7]} 5
- }
-
- set match_state [lrepeat [llength $var_names] ?]
- unset -nocomplain v
- unset -nocomplain nm
- set mismatched [list]
- set i 0
- #todo - stop at first mismatch - for pattern matching (especially pipecase - we don't want to waste time reading vars if we already have a mismatch earlier in the pattern)
- foreach va $var_actions {
- #val comes from -assigned
- lassign $va lhsspec act val ;#lhsspec is the full value source for LHS ie the full atom/number/varspec e.g for pattern ^var@@key/@0 it is "^var"
- set varname [lindex $var_names $i]
-
- if {[string match "?mismatch*" $act]} {
- #already determined a mismatch - e.g list or dict key not present
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info mismatch lhs ? rhs $val]
- break
- }
-
-
- set class_key [lindex $var_class $i 1]
- lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan
- foreach ck $class_key {
- switch -- $ck {
- 1 {set isatom 1}
- 2 {set ispin 1}
- 3 {set isbool 1}
- 4 {set isint 1}
- 5 {set isdouble 1}
- 6 {set isvar 1}
- 7 {set isglob 1}
- 8 {set isnumeric 1}
- 9 {set isgreaterthan 1}
- 10 {set islessthan 1}
- }
- }
-
-
- #set isatom [expr {$class_key == 1}]
- #set ispin [expr {2 in $class_key}]
- #set isbool [expr {3 in $class_key}]
- #set isint [expr {4 in $class_key}]
- #set isdouble [expr {5 in $class_key}]
- #set isvar [expr {$class_key == 6}]
- #set isglob [expr {7 in $class_key}]
- #set isnumeric [expr {8 in $class_key}] ;#force numeric comparison (only if # classifier present)
- ##marking numbers with pin ^ has undetermined meaning. Perhaps force expr matching only?
- #set isgreaterthan [expr {9 in $class_key}]
- #set islessthan [expr {10 in $class_key}]
-
-
-
- if {$isatom} {
- #puts stdout "==>isatom $lhsspec"
- set lhs [string range $lhsspec 1 end]
- if {[string index $lhs end] eq "'"} {
- set lhs [string range $lhs 0 end-1]
- }
- lset var_actions $i 1 matchatom-set
- if {$lhs eq $val} {
- lset match_state $i 1
- lset expected_values $i [list var $varname spec $lhsspec info match lhs $lhs rhs $val]
- incr i
- continue
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info strings-not-equal lhs $lhs rhs $val]
- break
- }
- }
-
-
-
-
- # - should set expected_values in each branch where match_state is not set to 1
- # - setting expected_values when match_state is set to 0 is ok except for performance
-
-
- #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or
- #ispin may reclassify as isint,isdouble based on contained value (as they don't have their own classifier char and are unambiguous and require special handling)
- if {$ispin} {
- #puts stdout "==>ispin $lhsspec"
- if {$act in [list "?set" "?matchvar-set"]} {
- lset var_actions $i 1 matchvar-set
- #attempt to read
- upvar $lvlup $varname the_var
- #if {![catch {uplevel $lvlup [list ::set $varname]} existingval]} {}
- if {![catch {set the_var} existingval]} {
-
- if {$isbool} {
- #isbool due to 2nd classifier i.e ^&
- lset expected_values $i [list var $varname spec $lhsspec info test-lhs-bool lhs $existingval rhs $val]
- #normalise to LHS!
- lset assigned_values $i $existingval
- } elseif {$isglob} {
- #isglob due to 2nd classifier ^*
- lset expected_values $i [list var $varname spec $lhsspec info test-lhs-glob lhs $existingval rhs $val]
- } elseif {$isnumeric} {
- #flagged as numeric by user using ^# classifiers
- set testexistingval [join [scan $existingval %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, internal decimal points and sci notation (but not leading .)
- if {[string is integer -strict $testexistingval]} {
- set isint 1
- lset assigned_values $i $existingval
- lset expected_values $i [list var $varname spec $lhsspec info test-lhs-int lhs $existingval rhs $val]
- } elseif {[string is double $existingval] || [string is double -strict $testexistingval]} {
- #test existingval in case something like .5 (which scan will have missed - producing empty testexistingval)
- set isdouble 1
- #doubles comparisons use float_almost_equal - so lhs can differ from rhs - for pins we always want to return the normalised lhs ie exactly what is in the var
- lset assigned_values $i $existingval
-
- lset expected_values $i [list var $varname spec $lhsspec info test-lhs-double lhs $existingval rhs $val]
- } else {
- #user's variable doesn't seem to have a numeric value
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info mismatch-lhs-not-numeric lhs $existingval rhs $val]
- break
- }
-
- } else {
- #standard pin - single classifier ^var
- lset match_state $i [expr {$existingval eq $val}]
- if {![lindex $match_state $i]} {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "string-compare-not-equal" lhs $existingval rhs $val]
- break
- } else {
- lset expected_values $i [list var $varname spec $lhsspec info "string-compare-equal" lhs $existingval rhs $val]
- }
- }
-
- } else {
- #puts stdout "pinned var $varname result:$result vs val:$val"
- #failure is *probably* because var is unset - but could be a read-only var due to read-trace or it could be nonexistant namespace
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info failread-$varname lhs ? rhs $val]
- break
- }
- }
- }
-
-
-
- if {$isint} {
- #note - we can have classified (above) a value such as 08 on lhs as integer - even though expr and string is integer don't do so.
- #expected_values $i [list var $varname spec $lhsspec info match-lhs-int lhs $existingval rhs $val]
-
- if {$ispin} {
- set existing_expected [lindex $expected_values $i]
- set lhs [dict get $existing_expected lhs]
- } else {
- set lhs $lhsspec ;#literal integer in the pattern
- }
- if {$isgreaterthan || $islessthan} {
- set lhs [string range $lhsspec 0 end-1]
- set testlhs $lhs
- }
- if {[string index $lhs 0] eq "."} {
- set testlhs $lhs
- } else {
- set testlhs [join [scan $lhs %lld%s] ""]
- }
- if {[string index $val 0] eq "."} {
- set testval $val
- } else {
- set testval [join [scan $val %lld%s] ""] ;# handles octals (leading zeros) and bignums (not leading .)
- }
- if {[string is integer -strict $testval]} {
- if {$isgreaterthan} {
- #puts "lhsspec: $lhsspec testlhs: $testlhs testval: $testval"
- if {$testlhs <= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-greater-than-int-int" lhs $lhs rhs $val]
- break
- }
- } elseif {$islessthan} {
- if {$testlhs >= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-less-than-int-int" lhs $lhs rhs $val]
- break
- }
- } else {
- if {$testlhs == $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-mismatch-int-int" lhs $lhs rhs $val]
- break
- }
- }
- } elseif {[string is double -strict $testval]} {
- #dragons. (and shimmering)
- if {[string first "e" $val] != -1} {
- #scientific notation - let expr compare
- if {$isgreaterhthan} {
- if {$testlhs <= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-greater-than-int-sci" lhs $lhs rhs $val]
- break
- }
- } elseif {$islessthan} {
- if {$testlhs >= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-less-than-int-sci" lhs $lhs rhs $val]
- break
- }
- } else {
- if {$testlhs == $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-mismatch-int-sci" lhs $lhs rhs $val]
- break
- }
- }
- } elseif {[string is digit -strict [string trim $val -]] } {
- #probably a wideint or bignum with no decimal point
- #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side .
- #if we use float_almost_equal they may compare equal. on the other hand expr also does apparently inconsistent thins with comparing integer-like bignums vs similar sized nums with .x at the end.
- #2 values further apart can compare equal while int-like ones closer together can compare different.
- #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in.
- #This is basically what we're doing here but with an arguably better (for some purposes!) float comparison.
- #string comparison can presumably always be used as an alternative.
- #
- #let expr compare
- if {$isgreaterthan} {
- if {$testlhs <= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-greater-than-int-puredigits" lhs $lhs rhs $val]
- break
- }
- } elseif {$islessthan} {
- if {$testlhs >= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-less-than-int-puredigits" lhs $lhs rhs $val]
- break
- }
- } else {
- if {$testlhs == $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-mismatch-int-puredigits" lhs $lhs rhs $val]
- break
- }
- }
- } else {
- if {[punk::pipe::float_almost_equal $testlhs $testval]} {
- lset match_state $i 1
- } else {
- if {$isgreaterthan} {
- if {$testlhs <= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-greater-than-int-float" lhs $lhs rhs $val]
- break
- }
- } elseif {$islessthan} {
- if {$testlhs >= $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-not-less-than-int-float" lhs $lhs rhs $val]
- break
- }
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "float_almost_equal-mismatch-int-float" lhs $lhs rhs $val]
- break
- }
- }
- }
- } else {
- #e.g rhs not a number..
- if {$testlhs == $testval} {
- lset match_state $i 1
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "expr-mismatch-unknown-rhstestval-$testval" lhs $lhs rhs $val]
- break
- }
- }
- } elseif {$isdouble} {
- #dragons (and shimmering)
- #
- #
- if {$ispin} {
- set existing_expected [lindex $expected_values $i]
- set lhs [dict get $existing_expected lhs]
- } else {
- set lhs $lhsspec ;#literal integer in the pattern
- }
- if {$isgreaterthan || $islessthan} {
- error "+/- not yet supported for lhs float"
- set lhs [string range $lhsspec 0 end-1]
- set testlhs $lhs
- }
- if {[string index $val 0] eq "."} {
- set testval $val ;#not something with some number of leading zeros
- } else {
- set testval [join [scan $val %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, internal decimal points and sci notation (but not leading .)
- }
- #expr handles leading 08.1 0009.1 etc without triggering octal
- #so we don't need to scan lhs
- if {[string first "e" $lhs] >= 0 || [string first "e" $testval] >= 0} {
- if {$lhs == $testval} {
- lset match_state $i 1
- lset expected_values $i [list var $varname spec $lhsspec info match-expr-sci lhs $lhs rhs $val]
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info mismatch-expr-sci lhs $lhs rhs $val]
- break
- }
- } elseif {[string is digit -strict [string trim $lhs -]] && [string is digit -strict [string trim $val -]]} {
- #both look like big whole numbers.. let expr compare using it's bignum capability
- if {$lhs == $testval} {
- lset match_state $i 1
- lset expected_values $i [list var $varname spec $lhsspec info match-expr-pure-digits lhs $lhs rhs $val]
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info mismatch-expr-pure-digits lhs $lhs rhs $val]
- break
- }
- } else {
- #float_almost_equal will disagree with expr based on scale.. just enough to allow for example [expr 0.2 + 0.1] to equal 0.3 - whereas expr will declare a mismatch
- if {[punk::pipe::float_almost_equal $lhs $testval]} {
- lset match_state $i 1
- lset expected_values $i [list var $varname spec $lhsspec info match-float-almost-equal lhs $lhs rhs $val]
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info mismatch-float-almost-equal lhs $lhs rhs $val]
- break
- }
- }
- } elseif {$isbool} {
- #Note - cross binding of booleans deliberately doesn't compare actual underlying values - only that truthiness or falsiness matches.
- #e.g &x/0,&x/1,&x/2= {1 2 yes}
- # all resolve to true so the cross-binding is ok.
- # Also - the setting of the variable x is normalized to 1 or 0 only. (true & false would perhaps be nicer - but 1 & 0 are theoretically more efficient for later comparisons as they can have a pure int rep?.)
- # todo - consider the booleanString rep. Can/should we return true & false instead and maintain efficiency w.r.t shimmering?
- #
- #punk::pipe::boolean_equal $a $b
- set extra_match_info "" ;# possible crossbind indication
- set is_literal_boolean 0
- if {$ispin} {
- #for a pinned boolean - the most useful return is the value in the pinned var rather than the rhs. This is not entirely consistent .. e.g pinned numbers will return rhs !review!
- #As an additional pattern can always retrieve the raw value - pinned vars returning themselves (normalisation use-case ) seems the most consistent overall, and the most useful
- set existing_expected [lindex $expected_values $i]
- set lhs [dict get $existing_expected lhs]
- } else {
- set lhs [string range $lhsspec 1 end] ;# - strip off & classifier prefix
-
- if {![string length $lhs]} {
- #empty varname - ok
- if {[string is boolean -strict $val] || [string is double -strict $val]} {
- lset match_state $i 1
- lset var_actions $i 1 "return-normalised-value"
- lset assigned_values $i [expr {bool($val)}]
- lset expected_values $i [list var $varname spec $lhsspec info "return-boolean-rhs-normalised" lhs - rhs $val]
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "mismatch-boolean-rhs" lhs - rhs $val]
- break
- }
- } elseif {$lhs in [list 0 1]} {
- #0 & 1 are the only literal numbers that satisfy Tcl's 'string is boolean' test.
- set is_literal_boolean 1
- } elseif {[string index $lhs 0] eq "'" && [string index $lhs end] eq "'"} {
- #literal boolean (&'yes',&'false',&'1',&'0' etc) in the pattern
- #we won't waste any cycles doing an extra validity test here - it will fail in the comparison below if not a string understood by Tcl to represent a boolean.
- set is_literal_boolean 1
- set lhs [string range $lhs 1 end-1] ;#strip off squotes
- } else {
- #todo - a standard variable name checking function for consistency.. for now we'll rule out numbers here to help avoid mistakes.
- set tclvar $lhs
- if {[string is double $tclvar]} {
- error "pipesyntax invalid variable name '$tclvar' for boolean in pattern. (subset of legal tcl vars allowed in pattern context)" "_multi_bind_result $multivar $data $args" [list pipesyntax patternvariable invalid_boolean $tclvar]
- #proc _multi_bind_result {multivar data args}
- }
- #treat as variable - need to check cross-binding within this pattern group
- set first_bound [lsearch -index 0 $var_actions $lhsspec]
- if {$first_bound == $i} {
- #test only rhs (val) for boolean-ness - but boolean-ness as boolean_almost_equal understands it. (e.g floats allowed)
- if {[string is boolean -strict $val] || [string is double -strict $val]} {
- lset match_state $i 1
- lset var_actions $i 1 [string range $act 1 end] ;# should now be the value "set". We only need this on the first_bound
- #review - consider what happens if boolean is leftmost pattern - underlying value vs normalised value to continue in pipeline
- #Passing underlying value is inconsistent with what goes in the tclvar - so we need to update the returnval
- #puts stderr "==========[lindex $assigned_values $i]"
- lset var_actions $i 2 [expr {bool($val)}] ;#normalise to 1 or 0
- lset assigned_values $i [lindex $var_actions $i 2]
- #puts stderr "==========[lindex $assigned_values $i]"
- lset expected_values $i [list var $varname spec $lhsspec info "match-boolean-rhs-any-lhs" lhs - rhs $val] ;#retain underlying val in expected_values for diagnostics.
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "mismatch-boolean-rhs-any-lhs" lhs - rhs $val]
- break
- }
- } else {
- set expectedinfo [lindex $expected_values $first_bound]
- set expected_earlier [dict get $expectedinfo rhs]
- set extra_match_info "-crossbind-first"
- set lhs $expected_earlier
- }
- }
- }
-
-
- #may have already matched above..(for variable)
- if {[lindex $match_state $i] != 1} {
- if {![catch {punk::pipe::boolean_almost_equal $lhs $val} ismatch]} {
- if {$ismatch} {
- lset match_state $i 1
- lset expected_values $i [list var $varname spec $lhsspec info match-boolean-almost-equal$extra_match_info lhs $lhs rhs $val]
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info mismatch-boolean-almost-equal$extra_match_info lhs $lhs rhs $val]
- break
- }
- } else {
- #we should only error from boolean_equal if passed something Tcl doesn't recognise as a boolean
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info badvalue-boolean$extra_match_info lhs $lhs rhs $val]
- break
- }
- }
-
- } elseif {$isglob} {
- if {$ispin} {
- set existing_expected [lindex $expected_values $i]
- set lhs [dict get $existing_expected lhs]
- } else {
- set lhs $lhsspec ;#literal glob in the pattern - no classifier prefix
- }
- if {[string match $lhs $val]} {
- lset match_state $i 1
- lset expected_values $i [list var $varname spec $lhsspec info "match-glob" lhs $lhs rhs $val]
- } else {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "mismatch-glob" lhs $lhs rhs $val]
- break
- }
-
- } elseif {$ispin} {
- #handled above.. leave case in place so we don't run else for pins
-
- } else {
- #puts stdout "==> $lhsspec"
- #NOTE - pinned var of same name is independent!
- #ie ^x shouldn't look at earlier x bindings in same pattern
- #unpinned non-atoms
- #cross-binding. Within this multivar pattern group only (use pin ^ for binding to result from a previous pattern)
- #
- switch -- $varname {
- "" {
- #don't attempt cross-bind on empty-varname
- lset match_state $i 1
- #don't change var_action $i 1 to set
- lset expected_values $i [list var $varname spec $lhsspec info "match-no-lhs-var" lhs - rhs $val]
- }
- "_" {
- #don't cross-bind on the special 'don't-care' varname
- lset match_state $i 1
- lset var_actions $i 1 [string range $act 1 end] ;# ?set -> set
- lset expected_values $i [list var $varname spec $lhsspec info "match-any-lhs-dontcare-var" lhs - rhs $val]
- }
- default {
- set first_bound [lsearch -index 0 $var_actions $varname]
- #assertion first_bound >=0, we will always find something - usually self
- if {$first_bound == $i} {
- lset match_state $i 1
- lset var_actions $i 1 [string range $act 1 end] ;# ?set -> set
- lset expected_values $i [list var $varname spec $lhsspec info "match-any-lhs" lhs - rhs $val]
- } else {
- assert {$first_bound < $i} assertion_fail: _multi_bind_result condition: [list $first_bound < $i]
- set expectedinfo [lindex $expected_values $first_bound]
- set expected_earlier [dict get $expectedinfo rhs]
- if {$expected_earlier ne $val} {
- lset match_state $i 0
- lset expected_values $i [list var $varname spec $lhsspec info "mismatch-crossbind-first" lhs $expected_earlier rhs $val]
- break
- } else {
- lset match_state $i 1
- #don't convert ?set to set - or var setter will write for each crossbound instance. Possibly no big deal for performance - but could trigger unnecessary write traces for example
- #lset var_actions $i 1 [string range $act 1 end]
- lset expected_values $i [list var $varname spec $lhsspec info "match-crossbind-first" lhs $expected_earlier rhs $val]
- }
- }
- }
- }
- }
-
- incr i
- }
-
- #JMN2 - review
- #set returnval [lindex $assigned_values 0]
- if {[llength $assigned_values] == 1} {
- set returnval [join $assigned_values]
- } else {
- set returnval $assigned_values
- }
- #puts stdout "----> > rep returnval: [rep $returnval]"
-
-
-
-
-
- #--------------------------------------------------------------------------
- #Variable assignments (set) should only occur down here, and only if we have a match
- #--------------------------------------------------------------------------
- set match_count_needed [llength $var_actions]
- #set match_count [expr [join $match_state +]] ;#expr must be unbraced here
- set matches [lsearch -all -inline $match_state 1] ;#default value for each match_state entry is "?"
- set match_count [llength $matches]
-
-
- debug.punk.pipe.var {MATCH_STATE: $match_state count_needed: $match_count_needed vs match_count: $match_count} 4
- debug.punk.pipe.var {VARACTIONS2: $var_actions} 5
- debug.punk.pipe.var {EXPECTED : $expected_values} 4
-
- #set match_count [>f . foldl 0 [>f . sum .] $match_state] ;#ok method.. but slow compared to expr with join
- if {$match_count == $match_count_needed} {
- #do assignments
- for {set i 0} {$i < [llength $var_actions]} {incr i} {
- if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} {
- #isvar
- if {[lindex $var_actions $i 1] eq "set"} {
- upvar $lvlup $varname the_var
- set the_var [lindex $var_actions $i 2]
- }
- }
- }
- dict set returndict ismatch 1
- #set i 0
- #foreach va $var_actions {
- # #set isvar [expr {[lindex $var_class $i 1] == 6}]
- # if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} {
- # #isvar
- # lassign $va lhsspec act val
- # upvar $lvlup $varname the_var
- # if {$act eq "set"} {
- # set the_var $val
- # }
- # #if {[lindex $var_actions $i 1] eq "set"} {
- # # set the_var $val
- # #}
- # }
- # incr i
- #}
- } else {
- #todo - some way to restrict mismatch info to simple "mismatch" and avoid overhead of verbose message
- #e.g for within pipeswitch block where mismatches are expected and the reasons are less important than moving on quickly
- set vidx 0
- #set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}]
- set mismatches [lmap m $match_state v $var_names {expr {$m == 0 ? [list mismatch $v] : [list match $v]}}]
- set var_display_names [list]
- foreach v $var_names {
- if {$v eq ""} {
- lappend var_display_names {{}}
- } else {
- lappend var_display_names $v
- }
- }
- #REVIEW 2025
- #set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}]
- set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0 ? $v : [expr {$m eq "?" ? "?[string repeat { } [expr {[string length $v] -1}]]" : [string repeat " " [string length $v]] }]}}]
- set msg "\n"
- append msg "Unmatched\n"
- append msg "Cannot match right hand side to pattern $multivar\n"
- append msg "vars/atoms/etc: $var_names\n"
- append msg "mismatches: [join $mismatches_display { } ]\n"
- set i 0
- #0 - novar
- #1 - atom '
- #2 - pin ^
- #3 - boolean &
- #4 - integer
- #5 - double
- #6 - var
- #7 - glob (no classifier and contains * or ?)
- foreach mismatchinfo $mismatches {
- lassign $mismatchinfo status varname
- if {$status eq "mismatch"} {
- # varname can be empty string
- set varclass [lindex $var_class $i 1]
- set val [lindex $var_actions $i 2]
- set e [dict get [lindex $expected_values $i] lhs]
- set type ""
- if {2 in $varclass} {
- append type "pinned "
- }
-
- if {$varclass == 1} {
- set type "atom"
- } elseif {$varclass == 2} {
- set type "pinned var"
- } elseif {3 in $varclass} {
- append type "boolean"
- } elseif {4 in $varclass} {
- append type "int"
- } elseif {5 in $varclass} {
- append type "double"
- } elseif {$varclass == 6} {
- set type "var"
- } elseif {7 in $varclass} {
- append type "glob"
- } elseif {8 in $varclass} {
- append type "numeric"
- }
- if {$type eq ""} {
- set type ""
- }
-
- set lhs_tag "- [dict get [lindex $expected_values $i] info]"
- set mmaction [lindex $var_actions $i 1] ;#e.g ?mismatch-dict-index-out-of-range
- set tag "?mismatch-"
- if {[string match $tag* $mmaction]} {
- set mismatch_reason [string range $mmaction [string length $tag] end]
- } else {
- set mismatch_reason $mmaction
- }
- append msg " $type: '$varname' $mismatch_reason $lhs_tag LHS: '$e' vs RHS: '$val'\n"
- }
- incr i
- }
- #error $msg
- dict unset returndict result
- #structured error return - used by pipeswitch/pipecase - matching on "binding mismatch*"
- dict set returndict mismatch [dict create binding mismatch varnames $var_names matchinfo $mismatches display $msg data $data]
- return $returndict
- }
-
- if {![llength $var_names]} {
- #var_name entries can be blank - but it will still be a list
- #JMN2
- #dict set returndict result [list $data]
- dict set returndict result $data
- } else {
- assert {$i == [llength $var_names]} assertion_fail _multi_bind_result condition {$i == [llength $var_names]}
- dict set returndict result $returnval
- }
- return $returndict
- }
-
- ########################################################
- # dragons.
- # using an error as out-of-band way to signal mismatch is the easiest.
- # It comes at some cost (2us 2023?) to trigger catches. (which is small relative to uncompiled pipeline costs in initial version - but per pattern mismatch will add up)
- # The alternative of attempting to tailcall return the mismatch as data - is *hard* if not impossible to get right.
- # We need to be able to match on things like {error {mismatch etc}} - without it then itself being interpreted as a mismatch!
- # A proper solution may involve a callback? tailcall some_mismatch_func?
- # There may be a monad-like boxing we could do.. to keep it in data e.g {internalresult match } {internalresult mismatch } and be careful to not let boxed data escape ??
- # make sure there is good test coverage before experimenting with this
- proc _handle_bind_result {d} {
- #set match_caller [info level 2]
- #debug.punk.pipe {_handle_bind_result match_caller: $match_caller} 9
- if {![dict exists $d result]} {
- #uplevel 1 [list error [dict get $d mismatch]]
- #error [dict get $d mismatch]
- return -code error -errorcode [list binding mismatch varnames [dict get $d mismatch varnames]] [dict get $d mismatch]
- } else {
- return [dict get $d result]
- }
- }
- # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch
- proc _handle_bind_result_experimental1 {d} {
- #set match_caller [info level 2]
- #debug.punk.pipe {_handle_bind_result match_caller: $match_caller} 9
- if {![dict exists $d result]} {
- tailcall return [dict get $d mismatch]
- } else {
- return [dict get $d result]
- }
- }
- ########################################################
-
- #timings very similar. listset3 closest in performance to pipeset. review - test on different tcl versions.
- #Unfortunately all these variations seem around 10x slower than 'set list {a b c}' or 'set list [list a b c]'
- #there seems to be no builtin for list setting with args syntax. lappend is close but we would need to catch unset the var first.
- #proc listset1 {listvarname args} {
- # tailcall set $listvarname $args
- #}
- #interp alias {} listset2 {} apply {{vname args} {tailcall set $vname $args}}
- #interp alias {} listset3 {} apply {{vname args} {upvar $vname v; set v $args}}
- proc pipeset {pipevarname args} {
- upvar $pipevarname the_pipe
- set the_pipe $args
- }
-
- #pipealias should capture the namespace context of the pipeline so that commands are resolved in the namespace in which the pipealias is created
- proc pipealias {targetcmd args} {
- set cmdcopy [punk::valcopy $args]
- set nscaller [uplevel 1 [list ::tcl::namespace::current]]
- tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller]
- }
- proc pipealias_extract {targetcmd} {
- set applybody [lindex [interp alias "" $targetcmd] 1 1]
- #strip off trailing " {*}$args"
- return [lrange [string range $applybody 0 end-9] 0 end]
- }
- #although the pipealias2 'concat' alias is cleaner in that the original pipeline can be extracted using list commands - it runs much slower
- proc pipealias2 {targetcmd args} {
- set cmdcopy [punk::valcopy $args]
- set nscaller [uplevel 1 [list ::tcl::namespace::current]]
- tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller]
- }
-
-
- #same as used in unknown func for initial launch
- #variable re_assign {^([^\r\n=\{]*)=(.*)}
- #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)}
- variable re_assign {^([^ \t\r\n=\{]*)=(.*)}
- variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)}
- #match_assign is tailcalled from unknown - uplevel 1 gets to caller level
- proc match_assign {scopepattern equalsrhs args} {
- #review - :: is legal in atoms!
- if {[string match "*::*" $scopepattern]} {
- error "match_assign scopepattern '$scopepattern' contains namespace separator '::' - invalid."
- }
- #puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args"
- set fulltail $args
- set cmdns ::punk::pipecmds
- set namemapping [punk::pipe::lib::pipecmd_namemapping $equalsrhs]
-
- #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW
- #(we need for example x*= to be available as is via namespace path mechanism (from punk::pipecmds namespace))
-
- set pipecmd ${cmdns}::$scopepattern=$namemapping
-
- #pipecmd could have glob chars - test $pipecmd in the list - not just that info commands returns results.
- if {$pipecmd in [info commands $pipecmd]} {
- #puts "==nscaller: '[uplevel 1 [list namespace current]]'"
- #uplevel 1 [list ::namespace import $pipecmd]
- set existing_path [uplevel 1 [list ::tcl::namespace::path]]
- if {$cmdns ni $existing_path} {
- uplevel 1 [list ::tcl::namespace::path [concat $existing_path $cmdns]]
- }
- tailcall $pipecmd {*}$args
- }
-
-
- #NOTE:
- #we need to ensure for case:
- #= x=y
- #that the second arg is treated as a raw value - never a pipeline command
-
- #equalsrhs is set if there is a segment-insertion-pattern *directly* after the =
- #debug.punk.pipe {match_assign '$multivar' '$equalsrhs' '$fulltail'} 4
- #can match pattern on lhs with a value where pattern is a minilang that can refer to atoms (simple non-whitespace strings), numbers, or varnames (possibly pinned) as well as a trailing spec for position within the data.
-
- # allow x=insertionpattern to begin a pipeline e.g x= |> string tolower ? or x=1 a b c <| X to produce a X b c
- #
- #to assign an entire pipeline to a var - use pipeset varname instead.
-
- # in our script's handling of args:
- #avoid use of regexp match on each element - or we will unnecessarily force string reps on lists
- #same with lsearch with a string pattern -
- #wouldn't matter for small lists - but we need to be able to handle large ones efficiently without unneccessary string reps
- set script [string map [list $scopepattern $equalsrhs] {
- #script built by punk::match_assign
- if {[llength $args]} {
- #scan for existence of any pipe operator (|*> or <*|) only - we don't need position
- #all pipe operators must be a single element
- #we don't first check llength args == 1 because for example:
- # x= <|
- # x= |>
- #both leave x empty. To assign a pipelike value to x we would have to do: x= <| |> (equiv: set x |>)
- foreach a $args {
- if {![catch {llength $a} sublen]} {
- #don't enforce sublen == 1. Legal to have whitespace including newlines {| x >}
- if {[string match |*> $a] || [string match <*| $a]} {
- tailcall punk::pipeline = "" "" {*}$args
- }
- }
- }
- if {[llength $args] == 1} {
- set segmenttail [lindex $args 0]
- } else {
- error "pipedata = must take a single argument. Got [llength $args] args: '$args'" "match_assign $args" [list pipedata segment too_many_elements segment_type =]
- }
- } else {
- #set segmenttail [purelist]
- set segmenttail [lreplace x 0 0]
- }
- }]
-
-
-
-
- if {[string length $equalsrhs]} {
- # as we aren't in a pipleine - there is no data to insert - we proably still need to run _split_equalsrhs to verify the syntax.
- # review - consider way to turn it off as optimisation for non-pipelined assignment - but generally standard Tcl set could be used for that purpose.
- # We are probably only here if testing in the repl - in which case the error messages are important.
- set var_index_position_list [punk::pipe::lib::_split_equalsrhs $equalsrhs]
- #we may have an insertion-spec that inserts a literal atom e.g to wrap in "ok"
- # x='ok'>0/0 data
- # => {ok data}
- # we won't examine for vars as there is no pipeline - ignore
- # also ignore trailing * (indicator for variable data to be expanded or not - ie {*})
- # we will differentiate between / and @ in the same way that general pattern matching works.
- # /x will simply call linsert without reference to length of list
- # @x will check for out of bounds
- #
- # !TODO - sort by position lowest to highest? or just require user to order the pattern correctly?
-
-
-
- foreach v_pos $var_index_position_list {
- lassign $v_pos v indexspec positionspec
- #e.g =v1/1>0 A pattern predator system)
- #
- #todo - review
- #
- #
- #for now - the script only needs to handle the case of a single segment pipeline (no |> <|)
-
-
- #temp - needs_insertion
- #we can safely output no script for variable insertions for now - because if there was data available,
- #we would have to be in a pipeline - in which case the script above would have delegated all our operations anyway.
- #tag: positionspechandler
- if {([string index $v 0] eq "'" && [string index $v end] eq "'") || [string is integer -strict $v]} {
- #(for now)don't allow indexspec on a literal value baked into the pipeline - it doesn't really make sense
- #- unless the pipeline construction has been parameterised somehow e.g "=${something}/0"
- #review
- if {[string length $indexspec]} {
- error "pipesyntax literal value $v - index specification not allowed (match_assign)1" "match_assign $scopepattern $equalsrhs $args" [list pipesyntax index_on_literal]
- }
- if {[string index $v 0] eq "'" && [string index $v end] eq "'"} {
- set datasource [string range $v 1 end-1]
- } elseif {[string is integer -strict $v]} {
- set datasource $v
- }
- append script [string map [list $datasource] {
- set insertion_data "" ;#atom could have whitespace
- }]
-
- set needs_insertion 1
- } elseif {$v eq ""} {
- #default variable is 'data'
- set needs_insertion 0
- } else {
- append script [string map [list $v] {
- #uplevel?
- #set insertion_data [set ]
- }]
- set needs_insertion 0
- }
- if {$needs_insertion} {
- set script2 [punk::list_insertion_script $positionspec segmenttail ]
- set script2 [string map [list "\$insertion_data" ] $script2]
- append script $script2
- }
-
-
- }
-
-
- }
-
- if {![string length $scopepattern]} {
- append script {
- return $segmenttail
- }
- } else {
- append script [string map [list $scopepattern] {
- #we still need to bind whether list is empty or not to allow any patternmatch to succeed/fail
- set d [punk::_multi_bind_result {} $segmenttail]
- #return [punk::_handle_bind_result $d]
- #maintenance: inlined
- if {![dict exists $d result]} {
- #uplevel 1 [list error [dict get $d mismatch]]
- #error [dict get $d mismatch]
- return -code error -level 1 -errorcode [list binding mismatch] [dict get $d mismatch]
- } else {
- return [dict get $d result]
- }
- }]
- }
-
- debug.punk.pipe.compile {match_assign creating proc $pipecmd} 2
- uplevel 1 [list ::proc $pipecmd args $script]
- set existing_path [uplevel 1 [list ::tcl::namespace::path]]
- if {$cmdns ni $existing_path} {
- uplevel 1 [list ::tcl::namespace::path [concat $existing_path $cmdns]]
- }
- tailcall $pipecmd {*}$args
- }
-
- #return a script for inserting data into listvar
- #review - needs updating for list-return semantics of patterns?
- proc list_insertion_script {keyspec listvar {data }} {
- set positionspec [string trimright $keyspec "*"]
- set do_expand [expr {[string index $keyspec end] eq "*"}]
- if {$do_expand} {
- set exp {{*}}
- } else {
- set exp ""
- }
- #NOTE: linsert and lreplace can take multiple values at tail ie expanded data
-
- set ptype [string index $positionspec 0]
- if {$ptype in [list @ /]} {
- set index [string range $positionspec 1 end]
- } else {
- #the / is optional (default) at first position - and we have already discarded the ">"
- set ptype "/"
- set index $positionspec
- }
- #puts stderr ">> >> $index"
- set script ""
- set isint [string is integer -strict $index]
- if {$index eq "."} {
- #do nothing - this char signifies no insertion
- } elseif {$isint || [regexp {^(end|end[-+]{1,2}[0-9]+)$} $index]} {
- if {$ptype eq "@"} {
- #compare position to *possibly updated* list - note use of $index > $datalen rather than $index+1 > $datalen - (we allow 'insertion' at end of list by numeric index)
- if {$isint} {
- append script [string map [list $listvar $index] {
- if {( > [llength $])} {
- #not a pipesyntax error
- error "pipedata insertionpattern index out of bounds. index: vs len: [llength $] use /x instead of @x to avoid check (list_insertion_script)" "list_insertion_script $keyspec" [list pipedata insertionpattern index_out_f_bounds]
- }
- }]
- }
- #todo check end-x bounds?
- }
- #todo - change to ledit
- #consider also $[set {}] instead of using unset
- #see https://wiki.tcl-lang.org/page/K regarding Unsharing Objects
- if {$isint} {
- append script [string map [list $listvar $index $exp $data] {
- set [linsert [lindex [list $ [unset ]] 0] ]
- }]
- } else {
- append script [string map [list $listvar $index $exp $data] {
- #use inline K to make sure the list is unshared (optimize for larger lists)
- set [linsert [lindex [list $ [unset ]] 0] ]
- }]
-
- }
- } elseif {[string first / $index] < 0 && [string first - $index] > 0} {
- if {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $index _ start end]} {
- #also - range checks for @ which must go into script !!!
- append script [string map [list $listvar $start $end $exp $data] {
- set [lreplace [lindex [list $ [unset ]] 0] ]
- }]
- } else {
- error "pipesyntax error in segment insertionpattern - positionspec:'$keyspec' unable to interpret position spec (list_insertion_script)2" "list_insertion_script $keyspec" [list pipedata insertionpattern_invalid]
- }
- } elseif {[string first / $index] >= 0} {
- #nested insertion e.g /0/1/2 /0/1-1
- set parts [split $index /]
- set last [lindex $parts end]
- if {[string first - $last] >=0} {
- lassign [split $last -] a b
- if {![regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $last _ a b]} {
- error "pipesyntax error in segment insertionpattern - positionspec:'$keyspec' unable to interpret position spec (list_insertion_script)3" "list_insertion_script $keyspec" [list pipesyntax insertionpattern_invalid]
- }
- if {$a eq $b} {
- if {!$do_expand} {
- #we can do an lset
- set lsetkeys [list {*}[lrange $parts 0 end-1] $a]
- append script [string map [list $listvar $lsetkeys $data] {
- lset
- }]
- } else {
- #we need to lreplace the containing item
- append script [string map [list $listvar [lrange $parts 0 end-1] $a $data] {
- set target [lindex $ ]
- lset target {*}
- lset $target
- }]
- }
- } else {
- #we need to lreplace a range at the target level
- append script [string map [list $listvar [lrange $parts 0 end-1] $a $b $exp $data] {
- set target [lindex $ ]
- set target [lreplace $target ]
- lset $target
- }]
- }
- } else {
- #last element has no -, so we are inserting at the final position - not replacing
- append script [string map [list $listvar [lrange $parts 0 end-1] $last $exp $data] {
- set target [lindex $ ]
- #set target [linsert $target ]
- ledit target -1
- lset $target
- }]
- }
-
-
- } else {
- error "pipesyntax error in segment - positionspec:'$keyspec' unable to interpret position spec (list_insertion_script)4" "list_insertion_script $keyspec" [list pipesyntax insertionpattern_invalid]
- }
- return $script
- }
-
-
-
-
- proc _is_math_func_prefix {e1} {
- #also catch starting brackets.. e.g "(min(4,$x) "
- if {[regexp {^[^[:alnum:]]*([[:alnum:]]*).*} $e1 _ word]} {
- #possible math func
- if {$word in [info functions]} {
- return true
- }
- }
- return false
- }
-
- #todo - option to disable these traces which provide clarifying errors (performance hit?)
- proc pipeline_args_read_trace_error {args} {
- error "The pipelined data doesn't appear to be a valid Tcl list\nModify input, or use \$data or another variable name instead of \$args." "pipeline_args_read_trace_error $args" [list pipedata args_unavailable_data_not_a_list]
- }
-
-
- #NOTE: the whole idea of scanning for %x% is a lot of work(performance penalty)
- #consider single placeholder e.g "_" as only positional indicator - for $data only - and require braced script with $var for more complex requirements
- #possibly also *_ for expanded _ ?
- #This would simplify code a lot - but also quite possible to collide with user data.
- #Perhaps not a big deal as unbraced segments between |> are mainly(?) a convenience for readability/repl etc.
- # (but importantly (at pipeline start anyway) unbraced segments are a mechanism to inject data from calling scope or from pipeline args <|)
- #
- #detect and retrieve %xxx% elements from item without affecting list/string rep
- #commas, @, ', ^ and whitespace not part of a valid tag (allows some substitution within varspecs)
- #%% is not a valid tag
- #(as opposed to using regexp matching which causes string reps)
- proc get_tags {item} {
- set chars [split $item {}]
- set terminal_chars [list , @ ' ^ " " \t \n \r]
- #note % is both terminal and initiating - so for the algorithm we don't include it in the list of terminal_chars
- set nonterminal [lmap v $chars {expr {$v ni $terminal_chars}}]
- set percents [lmap v $chars {expr {$v eq "%"}}]
- #useful for test/debug
- #puts "CHARS : $chars"
- #puts "NONTERMINAL: $nonterminal"
- #puts "PERCENTS : $percents"
- set sequences [list]
- set in_sequence 0
- set start -1
- set end -1
- set i 0
- #todo - some more functional way of zipping/comparing these lists?
- set s_length 0 ;#sequence length including % symbols - minimum for tag therefore 2
- foreach n $nonterminal p $percents {
- if {!$in_sequence} {
- if {$n & $p} {
- set s_length 1
- set in_sequence 1
- set start $i
- set end $i
- } else {
- set s_length 0
- }
- } else {
- if {$n ^ $p} {
- incr s_length
- incr end
- } else {
- if {$n & $p} {
- if {$s_length == 1} {
- # % followed dirctly by % - false start
- #start again from second %
- set s_length 1
- set in_sequence 1
- set start $i
- set end $i
- } else {
- incr end
- lappend sequences [list $start $end]
- set in_sequence 0
- set s_length 0
- set start -1; set end -1
- }
- } else {
- #terminated - not a tag
- set in_sequence 0
- set s_length 0
- set start -1; set end -1
- }
- }
- }
- incr i
- }
-
- set tags [list]
- foreach s $sequences {
- lassign $s start end
- set parts [lrange $chars $start $end]
- lappend tags [join $parts ""]
- }
- return $tags
- }
-
- #show underlying rep of list and first level
- proc rep_listname {lname} {
- upvar $lname l
- set output "$lname list rep: [rep $l]\n"
- foreach item $l {
- append output "-rep $item\n"
- append output " [rep $item]\n"
- }
- return $output
- }
-
-
- # --
- #consider possible tilde templating version ~= vs .=
- #support ~ and ~* placeholders only.
- #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~*
- #The ~ being mapped to $data in the pipeline.
- #This is more readable and simpler for beginners - although it doesn't handle more advanced insertion requirements.
- #possibility to mix as we can already with .= and =
- #e.g
- #x.= list aa b c |> ~= lmap v ~ {string length $v} |> .=>* tcl::mathfunc::max
- # --
- proc pipeline {segment_op initial_returnvarspec equalsrhs args} {
- set fulltail $args
- #unset args ;#leave args in place for error diagnostics
- debug.punk.pipe {call pipeline: op:'$segment_op' '$initial_returnvarspec' '$equalsrhs' '$fulltail'} 4
- #debug.punk.pipe.rep {[rep_listname fulltail]} 6
-
-
- #---------------------------------------------------------------------
- # test if we have an initial x.=y.= or x.= y.=
-
- #nextail is tail for possible recursion based on first argument in the segment
- #set nexttail [lassign $fulltail next1] ;#tail head
-
- set next1 [lindex $args 0]
- switch -- $next1 {
- pipematch {
- set nexttail [lrange $args 1 end]
- set results [uplevel 1 [list pipematch {*}$nexttail]]
- debug.punk.pipe {>>> pipematch results: $results} 1
-
- set d [_multi_bind_result $initial_returnvarspec $results]
- return [_handle_bind_result $d]
- }
- pipecase {
- set msg "pipesyntax\n"
- append msg "pipecase does not return a value directly in the normal way\n"
- append msg "It will return a casemismatch dict on mismatch\n"
- append msg "But on a successful match - it will use an 'error' mechanism to return {ok result {something}} in the caller's scope -\n"
- append msg "This will appear as an error in the repl, or disrupt pipeline result propagation if not in an appropriate wrapper\n"
- append msg "Call pipecase from within a pipeline script block or wrapper such as pipeswitch or apply."
- error $msg
- }
- }
-
- #temp - this is related to a script for the entire pipeline (functional composition) - not the script for the segment-based x=y or x.=y proc.
- set ::_pipescript ""
-
-
-
- #NOTE:
- #important that for assignment:
- #= x=y ..
- #The second element is always treated as a raw value - not a pipeline instruction.
- #whereas... for execution:
- #.= x=y the second element is a pipeline-significant symbol based on the '=' even if it was passed in as an argument.
- #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway -
- #- but if the pipeline is designed to put an argument in the zero position - then presumably it is intended as a pipeline-significant element anyway
- #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines
- #
- if {$segment_op ne "="} {
- #handle for example:
- #var1.= var2= "etc" |> string toupper
- #
- #var1 will contain ETC (from entire pipeline), var2 will contain etc (from associated segment)
- #
-
- if {([set nexteposn [string last = $next1]] >= 0) && (![punk::pipe::lib::arg_is_script_shaped $next1]) } {
- set nexttail [lrange $args 1 end]
- #*SUB* pipeline recursion.
- #puts "======> recurse based on next1:$next1 "
- if {[string index $next1 $nexteposn-1] eq {.}} {
- #var1.= var2.= ...
- #non pipelined call to self - return result
- set results [uplevel 1 [list $next1 {*}$nexttail]]
- #debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5
- #debug.punk.pipe {>>> results: $results} 1
- return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]]
- }
- #puts "======> recurse assign based on next1:$next1 "
- #if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} {
- #}
- #non pipelined call to plain = assignment - return result
- set results [uplevel 1 [list $next1 {*}$nexttail]]
- #debug.punk.pipe {>>> results: $results} 1
- set d [_multi_bind_result $initial_returnvarspec $results]
- return [_handle_bind_result $d]
- }
- }
-
- set procname $initial_returnvarspec.=$equalsrhs
-
- #---------------------------------------------------------------------
-
- #todo add 'op' argument and handle both .= and =
- #
- #|> data piper symbol
- #<| args piper symbol (arguments supplied at end of pipeline e.g from commandline or from calling and/or currying the command)
- #
-
- set more_pipe_segments 1 ;#first loop
-
- #this contains the main %data% and %datalist% values going forward in the pipeline
- #as well as any extra pipeline vars defined in each |>
- #It also contains any 'args' with names supplied in <|
- set dict_tagval [dict create] ;#cumulative %x% tag dict which operates on the whole length of the pipeline
-
- #determine if there are input args at the end of the pipeline indicated by reverse <| symbol possibly with argspecs e.g transform x y z =0} {
- set tailremaining [lrange $fulltail 0 $firstargpipe_posn-1]
- set argslist [lrange $fulltail $firstargpipe_posn+1 end] ;#Note that this could be a whole other pipeline with |> and/or <| elements.
- set argpipe [lindex $fulltail $firstargpipe_posn]
- set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " b1 b2 b3 |outpipespec> c1 c2 c3
- # for a1 a2 a3 - the pipe to the right is actually an outpipespec and for c1 c2 c3 the pipe to the left is an inpipespec
-
-
- #our initial command list always has *something* before we see any pipespec |>
- #Therefore we initially have a blank inpipespec (although in effect, it comes from the argpipespec <|)
- set inpipespec $argpipespec
- set outpipespec ""
-
- #avoiding regexp on each arg to maintain list reps
- #set tailmap [lmap v $tailremaining {lreplace [split $v {}] 1 end-1}]
- ## set tailmap [lmap v $tailremaining {if {[regexp {^\|(.*)>$} $v _ outpipespec] && !$pipeseen} {set pipeseen 1;set outpipespec} {if {$pipeseen} {set v} 0}}]
- #e.g for: a b c |> e f g |> h
- #set firstpipe_posn [lsearch $tailmap {| >}]
-
- set firstpipe_posn [lsearch $tailremaining "|*>"]
-
- if {$firstpipe_posn >=0} {
- set outpipespec [string range [lindex $tailremaining $firstpipe_posn] 1 end-1]
- set segment_members [lrange $tailremaining 0 $firstpipe_posn-1]
- #set tailremaining [lrange $tailremaining $firstpipe_posn+1 end]
- set tailremaining [lreplace $tailremaining 0 $firstpipe_posn] ;#generally too short for any K combinator benefit? what about lists with scripts? is it dependent on list length or also element content size?
- } else {
- set segment_members $tailremaining
- set tailremaining [list]
- }
-
-
-
- set script_like_first_word 0
- set rhs $equalsrhs
-
- set segment_first_is_script 0 ;#default assumption until tested
-
- set segment_first_word [lindex $segment_members 0]
- if {$segment_op ne "="} {
- if {[punk::pipe::lib::arg_is_script_shaped $segment_first_word]} {
- set segment_first_is_script 1
- }
- } else {
- if {[llength $segment_members] > 1} {
- error "pipedata = can only accept a single argument (got: '$segment_members')" "pipeline $segment_op $initial_returnvarspec $equalsrhs $fulltail" [list pipedata too_many_elements]
- #proc pipeline {segment_op initial_returnvarspec equalsrhs args}
- }
- set segment_members $segment_first_word
- }
-
-
-
- #tailremaining includes x=y during the loop.
- set returnvarspec $initial_returnvarspec
- if {![llength $argslist]} {
- unset -nocomplain previous_result ;# we want it unset for first iteration - differentiate from empty string
- } else {
- set previous_result $argslist
- }
-
- set segment_result_list [list]
- set i 0 ;#segment id
- set j 1 ;#next segment id
- set pipespec(args) $argpipespec ;# from trailing <|
- set pipespec(0,in) $inpipespec
- set pipespec(0,out) $outpipespec
-
- set max_iterations 100 ;# configurable? -1 for no limit ? This is primarily here to aid debugging of runaway loops in initial development .. should probably set to no-limit in final version.
- while {$more_pipe_segments == 1} {
- #---------------------------------
- debug.punk.pipe {[a yellow bold]i$i SEGMENT MEMBERS([llength $segment_members]): $segment_members[a]} 4
- debug.punk.pipe {[a yellow bold]i$i TAIL REMAINING([llength $tailremaining]): $tailremaining[a]} 4
- debug.punk.pipe {[a] inpipespec(prev [a yellow bold]|$pipespec($i,in)[a]>) outpipespec(next [a+ yellow bold]|$pipespec($i,out)>[a])} 4
- debug.punk.pipe {[a cyan bold] segment_first_is_script:$segment_first_is_script} 4
- if {$segment_first_is_script} {
- debug.punk.pipe {[a cyan bold] script segment: [lindex $segment_members 0][a]} 4
- }
-
-
-
- #examine inpipespec early to give faster chance for mismatch. ie before scanning segment for argument position
- set segment_result ""
- if {[info exists previous_result]} {
- set prevr $previous_result
- } else {
- set prevr ""
- }
- set pipedvars [dict create]
- if {[string length $pipespec($i,in)]} {
- #check the varspecs within the input piper
- # - data and/or args may have been manipulated
- set d [apply {{mv res} {
- punk::_multi_bind_result $mv $res -levelup 1
- }} $pipespec($i,in) $prevr]
- #temp debug
- #if {[dict exists $d result]} {
- #set jjj [dict get $d result]
- #puts "!!!!! [rep $jjj]"
- #}
- set inpipespec_result [_handle_bind_result $d]
- set pipedvars [dict get $d setvars]
- set prevr $inpipespec_result ;# leftmost spec in |> needs to affect pipeline flow of 'data'
- #puts stdout "inpipespec:|$pipespec($i,in)> prevr:$prevr setvars: $pipedvars"
- }
- debug.punk.pipe {[a] previous_iteration_result: $prevr[a]} 6
- debug.punk.pipe.rep {rep previous_iteration_result [rep $prevr]}
-
-
- if {$i == $max_iterations} {
- puts stderr "ABORTING. Reached max_iterations $max_iterations (todo: make configurable)"
- set more_pipe_segments 0
- }
-
- set insertion_patterns [punk::pipe::lib::_split_equalsrhs $rhs] ;#raises error if rhs of positionspec not like /* or @*
- set segment_has_insertions [expr {[llength $insertion_patterns] > 0}]
- #if {$segment_has_insertions} {
- # puts stdout ">>> $segment_members insertion_patterns $insertion_patterns"
- #}
-
- debug.punk.pipe.var {segment_has_insertions: $insertion_patterns} 5
- debug.punk.pipe.rep {[rep_listname segment_members]} 4
-
-
-
-
- #whether the segment has insertion_patterns or not - apply any modification from the piper argspecs (script will use modified args/data)
- #pipedvars comes from either previous segment |>, or <| args
- if {[dict exists $pipedvars "data"]} {
- #dict set dict_tagval %data% [list [dict get $pipedvars "data"]]
- dict set dict_tagval data [dict get $pipedvars "data"]
- } else {
- if {[info exists previous_result]} {
- dict set dict_tagval data $prevr
- }
- }
- foreach {vname val} $pipedvars {
- #add additionally specified vars and allow overriding of %args% and %data% by not setting them here
- if {$vname eq "data"} {
- #already potentially overridden
- continue
- }
- dict set dict_tagval $vname $val
- }
-
- #todo!
- #segment_script - not in use yet.
- #will require non-iterative pipeline processor to use ... recursive.. or coroutine based
- set script ""
-
- if {!$segment_has_insertions} {
- #debug.punk.pipe.var {[a cyan]SEGMENT has no tags[a]} 7
- #add previous_result as data in end position by default, only if *no* insertions specified (data is just list-wrapped previous_result)
- #set segment_members_filled [concat $segment_members [dict get $dict_tagval %data%]] ;# data flows through by default as single element - not args - because some strings are not valid lists
- #insertion-specs with a trailing * can be used to insert data in args format
- set segment_members_filled $segment_members
- if {[dict exists $dict_tagval data]} {
- lappend segment_members_filled [dict get $dict_tagval data]
- }
-
- } else {
- debug.punk.pipe.var {processing insertion_pattern dict_tagval: $dict_tagval} 4
- set segment_members_filled [list]
- set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign
-
- set rhsmapped [punk::pipe::lib::pipecmd_namemapping $rhs]
- set cmdname "::punk::pipecmds::insertion::_$rhsmapped"
- #glob chars have been mapped - so we can test by comparing info commands result to empty string
- if {[info commands $cmdname] eq ""} {
-
- set insertion_script "proc $cmdname {dict_tagval segmenttail} {\n"
- foreach v_pos $insertion_patterns {
- #puts stdout "v_pos '$v_pos'"
- lassign $v_pos v indexspec positionspec ;#v may be atom, or varname (in pipeline scope)
- #puts stdout "v:'$v' indexspec:'$indexspec' positionspec:'$positionspec'"
- #julz
-
- append insertion_script \n [string map [list $v_pos] {
- lassign [list ] v indexspec positionspec
- }]
-
- if {([string index $v 0] eq "'") && ([string index $v end] eq "'")} {
- set v [string range $v 1 end-1] ;#assume trailing ' is present!
- if {[string length $indexspec]} {
- error "pipesyntax - index not supported on atom" "pipeline $segment_op $initial_returnvarspec $equalsrhs $args" [list pipesyntax index_on_literal]
- }
- append insertion_script \n "set insertion_data [list $v]" ;#sub in shortened $v now -i.e use atom value itself (string within single quotes)
- } elseif {[string is double -strict $v]} {
- #don't treat numbers as variables
- if {[string length $indexspec]} {
- error "pipesyntax - index not supported on number" "pipeline $segment_op $initial_returnvarspec $equalsrhs $args" [list pipesyntax index_on_literal]
- }
- append insertion_script \n {set insertion_data $v}
- } else {
- #todo - we should potentially group by the variable name and pass as a single call to _multi_bind_result - because stateful @ and @@ won't work in independent calls
- append insertion_script \n [string map [list $cmdname] {
- #puts ">>> v: $v dict_tagval:'$dict_tagval'"
- if {$v eq ""} {
- set v "data"
- }
- if {[dict exists $dict_tagval $v]} {
- set insertion_data [dict get $dict_tagval $v]
- #todo - use destructure_func
- set d [punk::_multi_bind_result $indexspec $insertion_data]
- set insertion_data [punk::_handle_bind_result $d]
- } else {
- #review - skip error if varname is 'data' ?
- #e.g we shouldn't really fail for:
- #.=>* list a b c <|
- #??? Technically
- #we need to be careful not to insert empty-list as an argument by default
- error "pipevariable - varname $v not present in pipeline context. pipecontext_vars: [dict keys $dict_tagval] (2)" " pipecontext_vars: [dict keys $dict_tagval]" [list pipevariable variable_not_in_pipeline_scope]
- }
-
- }]
- }
-
-
-
-
- #append script [string map [list