diff --git a/src/bootsupport/modules/argp-0.2.tm b/src/bootsupport/modules/argp-0.2.tm new file mode 100644 index 00000000..1b1f4b78 --- /dev/null +++ b/src/bootsupport/modules/argp-0.2.tm @@ -0,0 +1,259 @@ + +# Tcl parser for optional arguments in function calls and +# commandline arguments +# +# (c) 2001 Bastien Chevreux + +# Index of exported commands +# - argp::registerArgs +# - argp::setArgDefaults +# - argp::setArgsNeeded +# - argp::parseArgs + +# Internal commands +# - argp::CheckValues + +# See end of file for an example on how to use + +package provide argp 0.2 + +namespace eval argp { + variable Optstore + variable Opttypes { + boolean integer double string + } + + namespace export {[a-z]*} +} + + +proc argp::registerArgs { func arglist } { + variable Opttypes + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + #puts $parentns + #puts $caller + #puts $cmangled + + set Optstore(keys,$cmangled) {} + set Optstore(deflist,$cmangled) {} + set Optstore(argneeded,$cmangled) {} + + foreach arg $arglist { + foreach {opt type default allowed} $arg { + set optindex [lsearch -glob $Opttypes $type*] + if { $optindex < 0} { + return -code error "$caller, unknown type $type while registering arguments.\nAllowed types: [string trim $Opttypes]" + } + set type [lindex $Opttypes $optindex] + + lappend Optstore(keys,$cmangled) $opt + set Optstore(type,$opt,$cmangled) $type + set Optstore(default,$opt,$cmangled) $default + set Optstore(allowed,$opt,$cmangled) $allowed + lappend Optstore(deflist,$cmangled) $opt $default + } + } + + if { [catch {CheckValues $caller $cmangled $Optstore(deflist,$cmangled)} res]} { + return -code error "Error in declaration of optional arguments.\n$res" + } +} + +proc argp::setArgDefaults { func arglist } { + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + set Optstore(deflist,$cmangled) {} + foreach {opt default} $arglist { + if {![info exists Optstore(default,$opt,$cmangled)]} { + return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)" + } + set Optstore(default,$opt,$cmangled) $default + } + + # set the new defaultlist + foreach opt $Optstore(keys,$cmangled) { + lappend Optstore(deflist,$cmangled) $opt $Optstore(default,$opt,$cmangled) + } +} + +proc argp::setArgsNeeded { func arglist } { + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + #append caller $parentns :: $func + #set cmangled ${parentns}_$func + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + + set Optstore(argneeded,$cmangled) {} + foreach opt $arglist { + if {![info exists Optstore(default,$opt,$cmangled)]} { + return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)" + } + lappend Optstore(argneeded,$cmangled) $opt + } +} + + +proc argp::parseArgs { args } { + variable Optstore + + if {[llength $args] == 0} { + upvar args a opts o + } else { + upvar args a [lindex $args 0] o + } + + if { [ catch { set caller [lindex [info level -1] 0]}]} { + set caller "main program" + set cmangled "" + } else { + set cmangled [string map {:: _} $caller] + } + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + + # set the defaults + array set o $Optstore(deflist,$cmangled) + + # but unset the needed arguments + foreach key $Optstore(argneeded,$cmangled) { + catch { unset o($key) } + } + + foreach {key val} $a { + if {![info exists Optstore(type,$key,$cmangled)]} { + return -code error "$caller, unknown option $key, must be one of: $Optstore(keys,$cmangled)" + } + switch -exact -- $Optstore(type,$key,$cmangled) { + boolean - + integer { + if { $val == "" } { + return -code error "$caller, $key empty string is not $Optstore(type,$key,$cmangled) value." + } + if { ![string is $Optstore(type,$key,$cmangled) $val]} { + return -code error "$caller, $key $val is not $Optstore(type,$key,$cmangled) value." + } + } + double { + if { $val == "" } { + return -code error "$caller, $key empty string is not double value." + } + if { ![string is double $val]} { + return -code error "$caller, $key $val is not double value." + } + if { [string is integer $val]} { + set val [expr {$val + .0}] + } + } + default { + } + } + set o($key) $val + } + + foreach key $Optstore(argneeded,$cmangled) { + if {![info exists o($key)]} { + return -code error "$caller, needed argument $key was not given." + } + } + + if { [catch { CheckValues $caller $cmangled [array get o]} err]} { + return -code error $err + } + + return +} + + +proc argp::CheckValues { caller cmangled checklist } { + variable Optstore + + #puts "Checking $checklist" + + foreach {key val} $checklist { + if { [llength $Optstore(allowed,$key,$cmangled)] > 0 } { + switch -exact -- $Optstore(type,$key,$cmangled) { + string { + if { [lsearch $Optstore(allowed,$key,$cmangled) $val] < 0} { + return -code error "$caller, $key $val is not in allowed values: $Optstore(allowed,$key,$cmangled)" + } + } + double - + integer { + set found 0 + foreach range $Optstore(allowed,$key,$cmangled) { + if {[llength $range] == 1} { + if { $val == [lindex $range 0] } { + set found 1 + break + } + } elseif {[llength $range] == 2} { + set low [lindex $range 0] + set high [lindex $range 1] + + if { ![string is integer $low] \ + && [string compare "-" $low] != 0} { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a lower value range that is not integer and not \u00b4-\u00b4: $range" + } + if { ![string is integer $high] \ + && [string compare "+" $high] != 0} { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a upper value range that is not integer and not \u00b4+\u00b4: $range" + } + if {[string compare "-" $low] == 0} { + if { [string compare "+" $high] == 0 \ + || $val <= $high } { + set found 1 + break + } + } + if { $val >= $low } { + if {[string compare "+" $high] == 0 \ + || $val <= $high } { + set found 1 + break + } + } + } else { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has an allowed value range containing more than 2 elements: $range" + } + } + if { $found == 0 } { + return -code error "$caller, $key $val is not covered by allowed ranges: $Optstore(allowed,$key,$cmangled)" + } + } + } + } + } +} diff --git a/src/bootsupport/modules/debug-1.0.6.tm b/src/bootsupport/modules/debug-1.0.6.tm new file mode 100644 index 00000000..c2ee57be --- /dev/null +++ b/src/bootsupport/modules/debug-1.0.6.tm @@ -0,0 +1,306 @@ +# Debug - a debug narrative logger. +# -- Colin McCormack / originally Wub server utilities +# +# Debugging areas of interest are represented by 'tokens' which have +# independantly settable levels of interest (an integer, higher is more detailed) +# +# Debug narrative is provided as a tcl script whose value is [subst]ed in the +# caller's scope if and only if the current level of interest matches or exceeds +# the Debug call's level of detail. This is useful, as one can place arbitrarily +# complex narrative in code without unnecessarily evaluating it. +# +# TODO: potentially different streams for different areas of interest. +# (currently only stderr is used. there is some complexity in efficient +# cross-threaded streams.) + +# # ## ### ##### ######## ############# ##################### +## Requisites + +package require Tcl 8.5- + +namespace eval ::debug { + namespace export -clear \ + define on off prefix suffix header trailer \ + names 2array level setting parray pdict \ + nl tab hexl + namespace ensemble create -subcommands {} +} + +# # ## ### ##### ######## ############# ##################### +## API & Implementation + +proc ::debug::noop {args} {} + +proc ::debug::debug {tag message {level 1}} { + variable detail + if {$detail($tag) < $level} { + #puts stderr "$tag @@@ $detail($tag) >= $level" + return + } + + variable prefix + variable suffix + variable header + variable trailer + variable fds + + if {[info exists fds($tag)]} { + set fd $fds($tag) + } else { + set fd stderr + } + + # Assemble the shown text from the user message and the various + # prefixes and suffices (global + per-tag). + + set themessage "" + if {[info exists prefix(::)]} { append themessage $prefix(::) } + if {[info exists prefix($tag)]} { append themessage $prefix($tag) } + append themessage $message + if {[info exists suffix($tag)]} { append themessage $suffix($tag) } + if {[info exists suffix(::)]} { append themessage $suffix(::) } + + # Resolve variables references and command invokations embedded + # into the message with plain text. + set code [catch { + set smessage [uplevel 1 [list ::subst -nobackslashes $themessage]] + set sheader [uplevel 1 [list ::subst -nobackslashes $header]] + set strailer [uplevel 1 [list ::subst -nobackslashes $trailer]] + } __ eo] + + # And dump an internal error if that resolution failed. + if {$code} { + if {[catch { + set caller [info level -1] + }]} { set caller GLOBAL } + if {[string length $caller] >= 1000} { + set caller "[string range $caller 0 200]...[string range $caller end-200 end]" + } + foreach line [split $caller \n] { + puts -nonewline $fd "@@(DebugError from $tag ($eo): $line)" + } + return + } + + # From here we have a good message to show. We only shorten it a + # bit if its a bit excessive in size. + + if {[string length $smessage] > 4096} { + set head [string range $smessage 0 2048] + set tail [string range $smessage end-2048 end] + set smessage "${head}...(truncated)...$tail" + } + + foreach line [split $smessage \n] { + puts $fd "$sheader$tag | $line$strailer" + } + return +} + +# names - return names of debug tags +proc ::debug::names {} { + variable detail + return [lsort [array names detail]] +} + +proc ::debug::2array {} { + variable detail + set result {} + foreach n [lsort [array names detail]] { + if {[interp alias {} debug.$n] ne "::debug::noop"} { + lappend result $n $detail($n) + } else { + lappend result $n -$detail($n) + } + } + return $result +} + +# level - set level and fd for tag +proc ::debug::level {tag {level ""} {fd {}}} { + variable detail + # TODO: Force level >=0. + if {$level ne ""} { + set detail($tag) $level + } + + if {![info exists detail($tag)]} { + set detail($tag) 1 + } + + variable fds + if {$fd ne {}} { + set fds($tag) $fd + } + + return $detail($tag) +} + +proc ::debug::header {text} { variable header $text } +proc ::debug::trailer {text} { variable trailer $text } + +proc ::debug::define {tag} { + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +# Set a prefix/suffix to use for tag. +# The global (tag-independent) prefix/suffix is adressed through tag '::'. +# This works because colon (:) is an illegal character for user-specified tags. + +proc ::debug::prefix {tag {theprefix {}}} { + variable prefix + set prefix($tag) $theprefix + + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +proc ::debug::suffix {tag {theprefix {}}} { + variable suffix + set suffix($tag) $theprefix + + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +# turn on debugging for tag +proc ::debug::on {tag {level ""} {fd {}}} { + variable active + set active($tag) 1 + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::debug $tag + return +} + +# turn off debugging for tag +proc ::debug::off {tag {level ""} {fd {}}} { + variable active + set active($tag) 1 + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::noop + return +} + +proc ::debug::setting {args} { + if {[llength $args] == 1} { + set args [lindex $args 0] + } + set fd stderr + if {[llength $args] % 2} { + set fd [lindex $args end] + set args [lrange $args 0 end-1] + } + foreach {tag level} $args { + if {$level > 0} { + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::debug $tag + } else { + level $tag [expr {-$level}] $fd + interp alias {} debug.$tag {} ::debug::noop + } + } + return +} + +# # ## ### ##### ######## ############# ##################### +## Convenience commands. +# Format arrays and dicts as multi-line message. +# Insert newlines and tabs. + +proc ::debug::nl {} { return \n } +proc ::debug::tab {} { return \t } + +proc ::debug::parray {a {pattern *}} { + upvar 1 $a array + if {![array exists array]} { + error "\"$a\" isn't an array" + } + pdict [array get array] $pattern +} + +proc ::debug::pdict {dict {pattern *}} { + set maxl 0 + set names [lsort -dict [dict keys $dict $pattern]] + foreach name $names { + if {[string length $name] > $maxl} { + set maxl [string length $name] + } + } + set maxl [expr {$maxl + 2}] + set lines {} + foreach name $names { + set nameString [format (%s) $name] + lappend lines [format "%-*s = %s" \ + $maxl $nameString \ + [dict get $dict $name]] + } + return [join $lines \n] +} + +proc ::debug::hexl {data {prefix {}}} { + set r {} + + # Convert the data to hex and to characters. + binary scan $data H*@0a* hexa asciia + + # Replace non-printing characters in the data with dots. + regsub -all -- {[^[:graph:] ]} $asciia {.} asciia + + # Pad with spaces to a full multiple of 32/16. + set n [expr {[string length $hexa] % 32}] + if {$n < 32} { append hexa [string repeat { } [expr {32-$n}]] } + #puts "pad H [expr {32-$n}]" + + set n [expr {[string length $asciia] % 32}] + if {$n < 16} { append asciia [string repeat { } [expr {16-$n}]] } + #puts "pad A [expr {32-$n}]" + + # Reassemble formatted, in groups of 16 bytes/characters. + # The hex part is handled in groups of 32 nibbles. + set addr 0 + while {[string length $hexa]} { + # Get front group of 16 bytes each. + set hex [string range $hexa 0 31] + set ascii [string range $asciia 0 15] + # Prep for next iteration + set hexa [string range $hexa 32 end] + set asciia [string range $asciia 16 end] + + # Convert the hex to pairs of hex digits + regsub -all -- {..} $hex {& } hex + + # Add the hex and latin-1 data to the result buffer + append r $prefix [format %04x $addr] { | } $hex { |} $ascii |\n + incr addr 16 + } + + # And done + return $r +} + +# # ## ### ##### ######## ############# ##################### + +namespace eval debug { + variable detail ; # map: TAG -> level of interest + variable prefix ; # map: TAG -> message prefix to use + variable suffix ; # map: TAG -> message suffix to use + variable fds ; # map: TAG -> handle of open channel to log to. + variable header {} ; # per-line heading, subst'ed + variable trailer {} ; # per-line ending, subst'ed + + # Notes: + # - The tag '::' is reserved. "prefix" and "suffix" use it to store + # the global message prefix / suffix. + # - prefix and suffix are applied per message. + # - header and trailer are per line. And should not generate multiple lines! +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide debug 1.0.6 +return diff --git a/src/bootsupport/modules/flagfilter-0.3.tm b/src/bootsupport/modules/flagfilter-0.3.tm new file mode 100644 index 00000000..1d37e215 --- /dev/null +++ b/src/bootsupport/modules/flagfilter-0.3.tm @@ -0,0 +1,2714 @@ +#package provide flagfilter [namespace eval flagfilter {list [variable version 0.2.3]$version}] +#package provide [set ::pkg flagfilter-0.2.3] [namespace eval [lindex [split $pkg -] 0] {list [variable version [lindex [split $pkg -] 1][set ::pkg {}]]$version}] +# +#package provide [lindex [set pkg {flagfilter 0.2.3}] 0] [namespace eval [lindex $pkg 0] {list [variable version [lindex $pkg 1][set pkg {}]]$version}] +package provide [lassign {flagfilter 0.3} pkg ver]$pkg [namespace eval $pkg[set pkg {}] {list [variable version $::ver[set ::ver {}]]$version}] + +#Note: this is ugly.. particularly when trying to classify flags that are not fully specified i.e raw passthrough. +# - we can't know if a flag -x --x etc is expecting a parameter or not. +#0.2.2 2023-03 JN - added %match% placeholder support. Can be added to the dispatch command to tell it what command was actually matched. e.g tell xxx.tcl script that it was xxx.tcl when we matched on *.tcl + + +namespace eval flagfilter { + package require oolib ;# make 'oolib::collection new' available + + proc do_errorx {msg {code 1}} { + if {$::tcl_interactive} { + error $msg + } else { + puts stderr "|>err $msg" + exit $code + } + } + + proc do_error {msg {then error}} { + set levels [list debug info notice warn error critical alert emergency] + #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 syslog-like level descriptors + lassign $then type code + if {$code eq ""} { + set code 1 + } + set type [string tolower $type] + if {$type in [concat $levels exit]} { + puts -nonewline stderr "|$type> $msg\n" + } else { + puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '\n" + } + 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 -nonewline stderr " (exit suppressed due to tcl_interactive - raising error instead)\n" + if {![string is digit -strict $code]} { + puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '\n" + } + } + 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 -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '\n" + flush stderr + return -code error $msg + } + } + } + } + proc scriptdir {} { + set possibly_linked_script [file dirname [file normalize [file join [info script] ...]]] + if {[file isdirectory $possibly_linked_script]} { + return $possibly_linked_script + } else { + return [file dirname $possibly_linked_script] + } + } + +} + +package require overtype + + +namespace eval flagfilter { + namespace export get_one_flag_value + #review. Tcl can handle args like: {-a -val1 -b -val2} as long as they pair up. + #this will ignore flag-like values if they follow a -flag + # positional values that happen to start with - can still cause issues + #get_flagged_only can return an unpaired list if there are solos, or if it finds no value for the last flaglike element + # e.g from input {something -x -y -z} we will get {-x -y -z} + # + # + + #flagfilter::get_flagged_only may not always get things right when looking at a values list with command processors + #Even if all solos from commands are supplied in solodict - a flag might be solo only in the context of a particualar commandset + #The proper way to get flagged values from an arglist is to run the full parser. + #This then should be restricted to use for a specific subset of args where the supplied solodict is known to apply + proc get_flagged_only {arglist solodict} { + #solodict - solo flags with defaults + set solo_accumulator [dict create] ;#if multiple instances of solo flag found - append defaults to the value to form a list as long as the number of occurrences + #puts ">>>get_flagged_only input $arglist solodict:'$solodict'" + set result [list] + set last_was_flag 0 + set result [list] + set a_idx 0 + set end_of_options 0 + foreach a $arglist { + if {$a eq "--"} { + break + } + if {[dict exists $solodict $a]} { + set last_was_flag 0 + if {[dict exists $solo_accumulator $a]} { + set soloval [concat [dict get $solo_accumulator $a] [dict get $solodict $a]] + } else { + set soloval [dict get $solodict $a] + } + dict set solo_accumulator $a $soloval + #we need to keep order of first appearance + set idx [lsearch $result $a] + if {$idx < 0} { + lappend result $a $soloval + } else { + lset result $idx+1 $soloval + } + } else { + if {!$last_was_flag} { + if {$a eq "--"} { + + } else { + if {[lindex $arglist $a_idx-1] eq "--"} { + #end of options processing - none of the remaining are considered flags/options no matter what they look like + set last_was_flag 0 + break + } else { + if {[string match -* $a]} { + set last_was_flag 1 + lappend result $a ;#flag + } else { + #last wasnt, this isn't - don't output + set last_was_flag 0 + } + } + } + } else { + #we only look for single leading - in the value if last wasn't a flag - but we give -- and soloflags special treatment. + if {$a eq "--"} { + #last was flag + set last_was_flag 0 + } else { + lappend result $a ;#value + set last_was_flag 0 + } + } + } + incr a_idx + } + if {([llength $result] % 2) != 0} { + set last [lindex $result end] + if {[string match -* $last] && ($last ni [dict keys $solodict])} { + lappend result 1 + } + } + #puts ">>>get_flagged_only returning $result" + return $result + } + + + ## get_one_paired_flag_value + #best called with 'catch' unless flag known to be in arglist + #raises an error if no position available after the flag to retrieve value + #raises an error if flag not like -something + #raises an error if flag not found in list + proc get_one_paired_flag_value {arglist flag} { + if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $flag]} { + #regexp excludes plain - and -- + #if {![string match -* $flag]} {} + error "get_one_flag_value flag $flag does not look like a flag. Should be something like -$flag or --$flag" + } + set cindex [lsearch $arglist $flag] + if {$cindex >= 0} { + set valueindex [expr {$cindex + 1}] + if {$valueindex < [llength $arglist]} { + #puts stderr "++++++++++++++++++ get_one_flag_value flag '$flag' returning [lindex $arglist $valueindex]" + return [lindex $arglist $valueindex] + } else { + error "flagfilter::get_one_paired_flag_value no value corresponding to flag $flag (found flag, but reached end of list)" + } + } else { + error "flagfilter::get_one_paired_flag_value $flag not found in arglist: '$arglist'" + } + } +} + +namespace eval flagfilter::obj { + +} + + +namespace eval flagfilter { + variable run_counter 0 ;#Used by get_new_runid to form an id to represent run of main check_flags function. + #used as a basis for some object-instance names etc + proc get_new_runid {} { + variable run_counter + if {[catch {package require Thread}]} { + set tid 0 + } else { + set tid [thread::id] + } + return "ff-[pid]-${tid}-[incr run_counter]" + } + + namespace export check_flags + proc do_debug {lvl debugconfig msg} { + if {$lvl <= [dict get $debugconfig -debugargs]} { + foreach ln [split $msg \n] { + puts -nonewline stderr "|[dict get $debugconfig -source]> $ln\n" + flush stderr + } + } + } + + #---------------------------------------------------------------------- + # DO NOT RELY ON tcl::unsupported - it's named that for a reason and is not meant to be parsed + #wiki.tcl-lang.org/page/dict+tips+and+tricks + proc isdict {v} { + if {[string match "value is a list *" [::tcl::unsupported::representation $v]]} { + return [expr {!([llength $v] % 2)}] + } else { + return [string match "value is a dict *" [::tcl::unsupported::representation $v]] + } + } + + proc dict_format {dict} { + dictformat_rec $dict "" " " + } + proc dictformat_rec {dict indent indentstring} { + # unpack this dimension + set is_empty 1 + dict for {key value} $dict { + set is_empty 0 + if {[isdict $value]} { + append result "$indent[list $key]\n$indent\{\n" + append result "[dictformat_rec $value "$indentstring$indent" $indentstring]\n" + append result "$indent\}\n" + } else { + append result "$indent[list $key] [list $value]\n" + } + } + if {$is_empty} { + #experimental.. + append result "$indent\n" + #append result "" + } + return $result + } + #-------------------------------------------------------------------------- + + #solo 'category' includes longopts with value + #solo flags include the general list of -soloflags, and those specific to the current -commandprocessors spec (mashopts and singleopts) + proc is_this_flag_solo {f solos objp} { + if {![string match -* $f]} { + #not even flaglike + return 0 + } + + + if {$f in $solos} { + #review! - global -soloflags shouldn't override the requirements of a commandprocessor! + #but.. each commandprocessor needs to understand global solos occuring before our match so that we classify correctly.. + #todo - this may need to reference v_map and current position in scanlist to do properly + return 1 + } + if {$f eq "-"} { + #unless the caller declared it as a solo - treat this as a non flag element. (likely use is as a command match) + return 0 + } + if {$f eq "--"} { + #this is it's own type endofoptions + return 0 + } + + set p_opts [$objp get_combined_opts] + + set mashopts [dict get $p_opts mashopts] + set singleopts [dict get $p_opts singleopts] + set pairopts [dict get $p_opts pairopts] + set longopts [dict get $p_opts longopts] + + if {$f in $singleopts} { + return 1 + } + + #"any" keywords used by processors to consume anything - where we're not too worried about classifying a flagvalue vs an operand + #examine these last so that an explicit configuration of flags as pairopts,mashopts etc can still be classified correctly + if {"any" in $singleopts} { + return 1 + } + if {[string first "=" $f] >=1} { + if {"any" in $longopts} { + return 1 + } + #todo foreach longopt - split on = and search + } + + #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash - but if it's a pairopt, but not mashable - we can rule it out now + if {($f in $pairopts) && ($f ni $mashopts)} { + return 0 + } + #todo - suport mashes where one of the mashed flags takes an arg - review: only valid if it's last in the mash? + #(presumably so - unless we there was some other value delimiter such as isnumeric or capitalised flags vs lowercase values - but that seems a step too far - would require some sort of mashspec/mash-strategy config) + #last part of mash may actually be the value too. which complicates things + #linux ls seems to do this for example: + # ls -w 0 + # ls -lw 0 + # ls -lw0 + # also man.. e.g + # man -Tdvi + # man -Hlynx + # man -H + # - note this last one. '-H lynx' doesn't work - so it's a mashable opt that can take a value, but is not in pairopts! (-H with no value uses env value for browser) + # see also comments in is_this_flag_mash + # + + set flagletters [split [string range $f 1 end] ""] + set posn 1 + set is_solo 1 ;#default assumption to disprove + #trailing letters may legitimately not be in mashopts if they are part of a mashed value + #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing + foreach l $flagletters { + if {"-$l" ni $mashopts} { + #presumably an ordinary flag not-known to us + return 0 + } else { + if {"-$l" in $pairopts} { + if {$posn == [llength $flagletters]} { + #in pairopts and mash - but no value for it in the mash - thefore not a solo + return 0 + } else { + #entire tail is the value - this letter is effectively solo + return 1 + } + } elseif {"-$l" in $singleopts} { + #not allowed to take a value - keep processing letters + } else { + #can take a value! but not if at very end of mash. Either way This is a solo + return 1 + } + } + } + return $is_solo + } + #todo? support global (non-processor specific) mash list? -mashflags ? + proc is_this_flag_mash {f objp} { + if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $f]} { + #not even flaglike + return 0 + } + set optinfo [$objp get_combined_opts];#also applies to tail_processor - *usually* empty values for mashopts etc + + #we look at singleopts because even if the flag is in mashopts - when it is alone we don't classify it as a mash + set singleopts pdict get $optinfo singleopts] + if {$f in $singleopts} { + return 0 + } + + set pairopts [dict get $optinfo pairopts] + if {$f in [dict keys $pairopts]} { + #here, the entire arg (f) we are testing is in pairopts - it could still however appear as part of a mash, with or without a trailing value, and with or without other flags before it in the mash (but if neither prefixed,nor tailed then obviously not a mash) + return 0 + } + set mashopts [dict get $optinfo mashopts] + set flagletters [split [string range $f 1 end] ""] + set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash.. unless trailing one also takes a value + # .. in which case value could be at the tail of the mash.. or be the next arg in the list + # We will take absense from singleopts and pairopts to indicate the mashflag *optionally* takes a value + # (ie such a mashopt is a solo that can take a value only as a mashtail) + # presence in pairopts indicates a mashflag must have a value + # presense in singleopts indicates mashflag takes no value ever. + # mashopt cannot be in both singleopts and pairopts. (NAND) + foreach l $flagletters { + if {-$l in $pairopts} { + if {"-$l" in $mashopts} { + #need to consider any remainder in the mash as this value .. if no remainder - then this is a mash, but not 'solo' because this flag needs to consume the following arg. + # We are only concerned with mashness here so just stop processing mash elements when we hit the first one that is a pairopt + break + } else { + #we require the pairopt to explicitly be listed in mashopts as well as pairopts if it is to be allowed to be part of a mash + set is_mash 0 + } + } elseif {"-$l" in $singleopts} { + #singleopt & mashopt - cannot take a value, mashed or otherwise + if {"-$l" ni $mashopts} { + set is_mash 0 + } + } else { + if {"-$l" ni $mashopts} { + set is_mash 0 + } else { + #present only in mashopts - can take a value, but only immediately following in the mash + break + } + } + } + return $is_mash + } + proc is_this_flag_for_me {f objp cf_args} { + set processorname [$objp name] + set optinfo [$objp get_combined_opts] ;#also applies to tail_processor - *usually* empty values for mashopts etc + + if {$processorname in [list "tail_processor"]} { + return 1 + } + if {$processorname in [list "global"]} { + #todo - mashflags for global? + set defaults [dict get $cf_args -defaults] + set extras [dict get $cf_args -extras] + set soloflags [dict get $cf_args -soloflags] + if {$f in [concat $extras $soloflags [dict keys $defaults]]} { + return 1 + } + } + + set singleopts [dict get $optinfo singleopts] + if {"any" in [string tolower $singleopts]} { + #review semantics of 'all' here. does it mean any -xxx.. will match, or only if also in global -soloflags? + return 1 + } + set pairopts [dict get $optinfo pairopts] + set allopts [concat $singleopts [dict keys $pairopts]] + if {$f in $allopts} { + return 1 + } + + #process mashopts last + set mashopts [dict get $optinfo mashopts] + if {"any" in [string tolower $mashopts]} { + #if 'all' in mashopts - it can eat anything - review - is this even useful? + return 1 + } else { + set flagletters [split [string range $f 1 end] ""] + set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash + foreach l $flagletters { + if {"-$l" ni $mashopts} { + set is_mash 0 + } + } + return $is_mash + } + + return 0 + } + + + + proc add_dispatch_raw {recordvar parentname v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname raw] + lappend dispatchinfo $v + dict set drecord $parentname raw $dispatchinfo + } + } + proc add_dispatch_argument {recordvar parentname k v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname arguments] + lappend dispatchinfo $k $v ;#e.g -opt 1 + dict set drecord $parentname arguments $dispatchinfo + } + } + proc lsearch-all-stride-2 {l search} { + set posns [lmap i [lsearch -all $l $search] {expr {($i % 2) == 0 ? $i : [list x]}}] + return [lsearch -all -inline -not $posns x] + } + proc update_dispatch_argument {recordvar parentname k v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname arguments] + #can't assume there aren't repeat values e.g -v -v + #dict set dispatchinfo $k $v + if {[package vcompare [info tclversion] 8.7a5] >= 0} { + set posns [lsearch -all -stride 2 $dispatchinfo $k] + } else { + set posns [lsearch-all-stride-2 $dispatchinfo $k] + } + set lastitem [lindex $posns end] + if {[string length $lastitem]} { + set val_idx [expr {$lastitem + 1}] + set dispatchinfo [lreplace $dispatchinfo[set dispatchinfo {}] $val_idx $val_idx $v] ;# inlineK + dict set drecord $parentname arguments $dispatchinfo + } else { + error "Unable to update dispatch argument $k with value $v in dispatch record for $parentname" + } + #dict set drecord $parentname $dispatchinfo + } + } + + #Note the difference between this and is_command_match. + #Lack of a 'match' element does not cause a commandspec to skip allocating an operand it encounters + #Note that this isn't a general test to be applied to the entire argument list. + # - an arg may get matched by an earlier processor making it unavailable to be allocated by another processor + # so this test only applies during the ordered examination of args + proc can_this_commandspec_allocate_this_arg {flag cspec cf_args} { + set cmdinfo [lindex $cspec 1] + if {$cmdinfo eq "tail_processor"} { + return 1 + } + if {$cmdinfo eq "global"} { + set defaults [dict get $cf_args -defaults] + set soloflags [dict get $cf_args -soloflags] + set extras [dict get $cf_args -extras] + if {$flag in [concat $soloflags $extras [dict keys $defaults]]} { + return 1 + } + } + if {![dict exists $cmdinfo match]} { + return 1 + } + set matchspeclist [dict get $cmdinfo match] + foreach matchspec $matchspeclist { + if {[regexp -- $matchspec $flag]} { + return 1 + } + } + #only block it if there was a match pattern specified but it didn't match + return 0 + } + #Note - returns false for a cspec that has no match specified. + #A command/subcommand with no match specification is allowed to allocate any value - so be careful with this + # - it should not be used to *stop* an arg being allocated if the processor has no 'match' specified, or if it is another type of processor like 'tail_handler'. + proc is_command_match {flag cspec} { + set pinfo [lindex $cspec 1] + if {[dict exists $pinfo match]} { + set matchspeclist [dict get $pinfo match] + foreach matchspec $matchspeclist { + if {[regexp -- $matchspec $flag]} { + return 1 + } + } + return 0 + } else { + return 0 + } + } + proc is_command_match_any {f commandprocessors} { + foreach comspec $commandprocessors { + lassign $comspec cmdname cmdinfo + if {[dict exists $cmdinfo match]} { + set matchlist [dict get $cmdinfo match] + foreach matchspec $matchlist { + if {[regexp -- $matchspec $f]} { + #actually a command + return true + } + } + } + } + return false + } + + #determine if f is potentially a flag that takes a parameter from the next argument. + #e.g --x=y (longopt) does not consume following arg but --something *might* + proc is_candidate_toplevel_param_flag {f solos commandprocessors} { + if {[is_command_match_any $f $commandprocessors]} { + return false + } + if {$f in $solos} { + return 0 + } + if {$f in {- --}} { + return 0 + } + #longopts (--x=blah) and alternative --x blah + #possibly also -x=blah + if {[string match -* $f]} { + if {[string first "=" $f]>1} { + return 0 + } + } + return [expr {[string match -* $f]}] + } + + + + + + + + + + + + + + + + + + + #review - should we be using control::assert here? + #It depends if this is intended to raise error at runtime - would using control::assert and disabling assertions cause problems? + #todo - show caller info + proc assert_equal {a b} { + if {![expr {$a eq $b}]} { + error "assert_equal $a $b" + } + } + + + + + + #{1 unallocated 2 unallocated 3 unallocated 4 unallocated 5 unallocated 6 unallocated} ;#initial v_map + #1 2 3 4 5 6 ;#original list posns example + # 2 6 ;#map_remaining example (scanlist) + #1 3 4 5 ;#map_allocated example + #{1 {cmd1 operand} 2 unallocated 3 {cmd2 operand} 4 {cmd2 flag} 5 {cmd2 flagvalue} 6 unallocated} ;#v_map updated example + oo::class create class_vmap { + variable o_map + variable o_remaining + variable o_allocated + variable o_values + variable o_codemap + variable o_flagcategory + constructor {values} { + set o_codemap [dict create \ + operand op \ + flagvalue fv \ + soloflag so \ + flag fl \ + unallocated un \ + endofoptions eo \ + ] + set o_flagcategory [list "flag" "flagvalue" "soloflag"] + set o_values $values + #set o_remaining [lsearch -all $values *] ;#create a list of indices e.g 0 1 2 3 4 5 6 + #lsearch -all * is fast for very small lists - but lseq wins from size around 30+ + if {[llength $values]} { + if {[llength $values] < 30} { + #common case is short lists - but we don't want to penalize large lists + set o_remaining [lsearch -all $values *] + } else { + #punk::lib::range wraps lseq if available + set o_remaining [punk::lib::range 0 [llength $values]-1] + } + } else { + set o_remaining [list] + } + set o_allocated [list] + set o_map [list] + foreach posn $o_remaining { + lappend o_map $posn unallocated + } + } + method load {values rem alloc map} { + set o_values $values + set o_remaining $rem + set o_allocated $alloc + set o_map $map + } + method copy_to {obj} { + $obj load $o_values $o_remaining $o_allocated $o_map + } + method update_map_from {obj} { + #very basic sanity check first + if {[llength $o_values] ne [llength [$obj get_values]]} { + error "[self class].update_map_from cannot update. length of values mismatch" + } + + set newmap [$obj get_map] + } + + method get_codemap {} { + return $o_codemap + } + method get_values {} { + return $o_values + } + method get_remaining {} { + return $o_remaining + } + method get_allocated {} { + return $o_allocated + } + method get_map {} { + return $o_map + } + method argnum_from_remaining_posn {scanlist_posn} { + set vidx [lindex $o_remaining $scanlist_posn] + if {![string is digit -strict $vidx]} { + return -code error "[self class].argnum_from_remaining_posn cannot determine argnum from scanlist position:$scanlist_posn using unallocated list:'$o_remaining'" + } + return $vidx + } + + method allocate {objp argnum type value} { + set processorname [$objp name] + if {$processorname eq "tail_processor"} { + set owner "unallocated" + } else { + set owner [$objp parentname] + } + if {$argnum > [llength $o_values]-1} { + return -code error "[self class].allocate cannot allocate argnum:$argnum. Only [llength $o_values] items in value list" + } + if {$argnum in $o_allocated} { + return -code error "[self class].allocate already allocated '$processorname' argnum:'$argnum' type:'$type' val:'$value' remaining:$o_remaining allocated:$o_allocated map:$o_map" + } + lappend o_allocated $argnum + set o_allocated [lsort -dictionary $o_allocated] + dict set o_map $argnum [list $owner $type $value] + set scanlist_posn [lsearch $o_remaining $argnum] + set o_remaining [lreplace $o_remaining[set o_remaining {}] $scanlist_posn $scanlist_posn] ;#inlineK + + + + } + + method get_list_unflagged_by_class {classmatch} { + set resultlist [list] + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + switch -- $type { + flag - flagvalue - soloflag {} + default { + lappend resultlist $val + } + } + } + } + return $resultlist + } + + method get_list_flagged_by_class {classmatch} { + set list_flagged [list] + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + switch -- $type { + flag - flagvalue - soloflag { + lappend list_flagged $val + } + } + } + } + return $list_flagged + } + + method get_merged_flagged_by_class {classmatch} { + variable flagcategory + set all_flagged [list] + set seenflag [dict create] ;#key = -flagname val=earliest vindex + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + set a [llength $all_flagged] ;#index into all_flagged list we are building + switch -- $type { + soloflag { + if {[dict exists $seenflag $val]} { + set seenindex [dict get $seenflag $val] + set seenindexplus [expr {$seenindex+1}] + set existingvals [lindex $all_flagged $seenindexplus] + lappend existingvals 1 ;#1 indicating presence - stored as list rather than a count. todo: consider global or per-solo options to support incrementing instead? + lset all_flagged $seenindexplus $existingvals + } else { + dict set seenflag $val $a + lappend all_flagged $val 1 + } + } + flag { + if {![dict exists $seenflag $val]} { + dict set seenflag $val $a + lappend all_flagged $val + } + #no need to do anything if already seen - flagvalue must be next, and it will work out where to go. + } + flagvalue { + set idxflagfor [expr {$k -1}] + set flagforinfo [dict get $o_map $idxflagfor] + lassign $flagforinfo ffclass fftype ffval + #jn "--" following a flag could result in us getting here accidentaly.. review + set seenindex [dict get $seenflag $ffval] + if {$seenindex == [expr {$a-1}]} { + #usual case - this is a flagvalue following the first instance of the flag + lappend all_flagged $val + } else { + #write the value back to the seenindex+1 + set seenindexplus [expr {$seenindex+1}] + set existingvals [lindex $all_flagged $seenindexplus] + lappend existingvals $val ;#we keep multiples as a list + lset all_flagged $seenindexplus $existingvals + } + } + } + } + } + return $all_flagged + } + method typedrange_class_type_from_arg {argclass argtype} { + #set o_flagcategory [list "flag" "flagvalue" "soloflag"] + if {$argclass eq "unallocated"} { + switch -- $argtype { + flag - flagvalue - soloflag { + return [list unallocated flagtype] + } + default { + if {![string length $argtype]} { + #should only happen if something wrong with the tail_processor - rather than error out, for now at least make it stand out in the . + set argtype UNKNOWN + } + return [list unallocated ${argtype}type] ;#e.g unallocated_operand, unallocated_endofoptions + } + } + } else { + return [list $argclass argtype] ;# e.g command something + } + } + + method get_ranges_from_classifications {classifications} { + #puts stderr "get_ranges_from_classifications $classifications" + #examine classifications and create a list of ranges + set ranges [list];# e.g [list {unallocated 0 4} {cmd1 5 7} {unallocated 8 8} {cmd2 9 9} {cmd3 10 10} {unallocated 11 15}] + set seen_commands [list] + dict for {posn arginfo} $classifications { + set is_new_cmd 0 + set is_sub_cmd 0 + set is_continuation 0 + set rangename [lindex $ranges end 0] + set alloc [lindex $arginfo 0] ;#e.g of form 0 {unallocated operand} 1 {lscmd operand} 2 {lscmd soloflag} 3 {lscmd.dir operand} 4 {unallocated flag} + set cmdname "" + if {$alloc ne "unallocated"} { + if {$alloc ni $seen_commands} { + if {![llength $seen_commands]} { + set cmdname $alloc + set is_new_cmd 1 + } else { + set tail [lindex $seen_commands end] + if {$tail eq "unallocated"} { + set cmdname $alloc + set is_new_cmd 1 + } else { + if {[string first . $alloc] >= 0} { + set prefixcheck [lindex [split $alloc .] 0] + if {![string equal -length [string length $prefixcheck] $prefixcheck $tail]} { + #this is not unallocated, not a subcommand of the previous seen ie new command + set cmdname $alloc + set is_new_cmd 1 + } else { + set cmdname $prefixcheck + set is_sub_cmd 1 + set is_continuation 1 + } + } else { + set cmdname $alloc + set is_new_cmd 1 + } + } + } + } else { + set cmdname $alloc + set is_continuation 1 + } + if {$is_continuation} { + lassign [lindex $ranges end] _cmd n a b + set ranges [lrange $ranges 0 end-1] + lappend ranges [list command $n $a [incr b]] + flagfilter::assert_equal $b $posn + } elseif {$is_new_cmd} { + lappend seen_commands $alloc + if {$rangename eq ""} { + lappend ranges [list command $cmdname $posn $posn] + } else { + lassign [lindex $ranges end] _cmd n a b + lappend ranges [list command $cmdname [incr b] $posn] + flagfilter::assert_equal $b $posn + } + } else { + error "coding error during dispatch" + } + } else { + if {$rangename eq ""} { + lappend ranges [list unallocated mixed 0 0] + } else { + lassign [lindex $ranges end] class n a b + if {$class eq "unallocated"} { + #continuation - extend + set ranges [lrange $ranges 0 end-1] + lappend ranges [list unallocated mixed $a [incr b]] + } else { + #change from allocated to unallocated + lappend ranges [list unallocated mixed [incr b] $posn] + flagfilter::assert_equal $b $posn + } + } + } + } + set rangesbytype [list] + foreach oldrange $ranges { + lassign $oldrange oldrangeclass oldrangetype A B ;#A,B for original range bounds, a,b for bounds of sub-ranges we are creating + set last_type "" + set newrangelist [list] + set inner_range [list 0 0] + if {$oldrangeclass ne "unallocated"} { + #pass through - user can split commands further themselves by referencing the classifications map where each arg position is listed + set last_type $oldrangeclass ;#note the deliberate slight misuse - we are using the 'class' here rather than the type as we aren't looking at types within a command range + lappend rangesbytype $oldrange + } else { + #puts stdout "???????????????????????????????????????????????A$A B$B examining old range:'$oldrange'" + for {set i $A} {$i <= $B} {incr i} { + lassign [lindex $rangesbytype end] last_class last_type a b ;#enough just to use the type without the class + set a_info [dict get $classifications $i] + lassign $a_info argclass argtype v + lassign [my typedrange_class_type_from_arg $argclass $argtype] newrangeclass newrangetype + if {$last_type eq ""} { + lappend rangesbytype [list "unallocated" $newrangetype 0 0] + } else { + if {$last_type eq $newrangetype} { + set rangesbytype [lrange $rangesbytype 0 end-1] + lappend rangesbytype [list $last_class $last_type $a $i] + } else { + lappend rangesbytype [list $newrangeclass $newrangetype $i $i] + } + } + } + } + } + + return [list -ranges $ranges -rangesbytype $rangesbytype] + } + + method grid {} { + set posns [dict keys $o_map] + set col1 [string repeat " " 15] + set col [string repeat " " 4] + set pline "[overtype::left $col1 {var indices}] " + foreach p $posns { + append pline [overtype::left $col $p] + } + set remline "[overtype::left $col1 {unallocated}] " + foreach vidx $posns { + if {$vidx ni $o_remaining} { + append remline [overtype::left $col "."] + } else { + set tp [lindex [dict get $o_map $vidx] 1] + #set tp [string map $o_codemap $tp] + if {[dict exists $o_codemap $tp]} { + set tp [dict get $o_codemap $tp] + } + append remline [overtype::left $col $tp] + } + } + set cmdlist [list] + dict for {vidx info} $o_map { + if {[lindex $info 0] ne "unallocated"} { + set c [lindex [split [lindex $info 0] .] 0] + if {$c ni $cmdlist} { + lappend cmdlist $c + } + } + } + set clinelist [list] + foreach c $cmdlist { + set cline "[overtype::left $col1 $c] " + dict for {vidx info} $o_map { + lassign $info class type v + if {($c eq $class) || [string equal -length [string length "$c."] "$c." $class]} { + #set tp [string map $o_codemap $type] + if {[dict exists $o_codemap $type]} { + set tp [dict get $o_codemap $type] + } + append cline [overtype::left $col $tp] + } else { + append cline [overtype::left $col "."] + } + } + lappend clinelist $cline + } + + + set aline "[overtype::left $col1 {allocated}] " + foreach vidx $posns { + if {$vidx ni $o_allocated} { + append aline [overtype::left $col "."] + } else { + set tp [lindex [dict get $o_map $vidx] 1] + #set tp [string map $o_codemap $tp] + if {[dict exists $o_codemap $tp]} { + set tp [dict get $o_codemap $tp] + } + append aline [overtype::left $col $tp] + } + } + + return "$pline\n$remline\n[join $clinelist \n]\n$aline\n" + } + + } + + + #!todo - check if -commandprocessors members will collide with existing -flags in values before moving them + #!todo - skip optional sub-flag value if the next arg following its parent is a flag i.e proper handling of -commandprocessors {cmd {cmd sub "default}} when only cmd supplied. + #!important to fix. At the moment it could eat a further unflagged item in values later in the list which was intended for a different -commandprocessors member! + #add support for -commandprocessors {-cmd {-cmd -othercmd "default"}} to be a safe way to specify a linked -flag move that does the same. + proc allocate_arguments {PROCESSORS solos values cf_args caller} { + set runid [lindex [split [namespace tail $PROCESSORS] _] 1] ;# objname is of form PROCESSORS_pid-threadid-counter where "PROCESSORS_" is a literal + #puts stderr ">>>>>>> solos: $solos" + dict set debugc -debugargs [dict get $cf_args -debugargs] + dict set debugc -source "allocate_arguments $caller" + + set defaults [dict get $cf_args -defaults] + + set cmdprocessor_records [$PROCESSORS get_commandspecs] + + + set sep "\uFFFE" ;#argument-subargument separator (choose something else if this causes problems.. but we want something unlikely (or preferably impossible?) to be in a commandline, ideally a single character, and which at least shows something on screen during debug) + set sepstr "\\uFFFE" ;#for human readable error msg + #\u001E was tried and doesn't output on some terminals) + + set remaining_unflagged [dict create] + + set extra_flags_from_positionals [list] ;#values moved to -values + set moved_to_flagged [dict create] + + #implied_ are values supplied from defaults when a flag or operand was not found + set implied_flagged [list] + set implied_unflagged [list] + + + set dispatch [dict create] + #sanitize and raise error if sep somehow in values + if {[string first $sep $cmdprocessor_records] >= 0} { + do_error "allocate_arguments flags error separator '$sep' ($sepstr) found in values " + } + #-------------------------------------- + set VMAP [flagfilter::class_vmap create flagfilter::VMAP_$runid $values] + #-------------------------------------- + + set unconsumed_flags_and_values [list] + set unflagged [dict create] + + ###################### + #main -commandprocessors loop which scans the valuelist + set values_index 0 ;#track where we are up to as we allocate values to unflagged elements + set source_values $values ;#start with all including -flagged + + #todo - get rid of most of these flags last_x_was etc - and just do lookups into the v_map + # as this will probably involve *lots* of small functiona calls - keep this boolean version and check for performance issues. + set a_index 0 + set is_args_flag 0 + set last_arg_was_paramflag 0 ;#a flag that expects a parameter to follow + set last_arg_was_solo 0 + set solo_flags [dict keys $solos] ;#solos is a dict of -flag (preprocessed) + set end_of_options 0 + set end_of_options_index -1 ;#as later processors can rescan - we need to make sure they only look at arguments after this point + set last_p_found [dict create by "" index "" item ""] + set sequence 0 + set argerrors [list] ;#despite being a list - we will break out at first entry and return for now. + set parsestatus "ok" + + #set LAUNCHED [oolib::collection create col_processors_launched_$runid] + #set MATCHED [oolib::collection create col_processors_matched_$runid] + #oo::objdefine col_processors_matched_$runid { + # method test {} { + # return 1 + # } + #} + + #set objp [$PROCESSORS object_from_record $p] ;#temp convenience + + foreach objp [$PROCESSORS items] { + set objparent [$objp parent] + #$LAUNCHED add $objp [$objp name] + set p [$objp get_def] ;#individual record e.g {mycmd {match run singleopts {-x}}} or {tail_processor {}} + + lassign $p parentname pinfo + set is_sub [$objp is_sub] ;#is subargument - should look to see if last related spec got a value and abort if not. + set is_p_flag [$objp is_flag] ;#sub can be a flag even if parent isn't + set processorname [$objp name] + if {[$objp is_sub]} { + if {![[$objp parent] found_match]} { + continue + } + set p_sub [dict get $pinfo sub] + } + do_debug 3 $debugc " =========================>> p $p sequence:$sequence a_index $a_index" + + if {$processorname in [list "global" "tail_processor"]} { + dict set last_p_found by $processorname + #dict set last_p_found index $a_index + #dict set last_p_found item $a + } + # -format {x {sub y default "default"}} means y is dependent on x being present and shouldn't eat if the next value isn't flaglike + # -format {-x {sub -y}} does the same for moving positionals to the flagged list. + + + #set remaining_values [lrange $source_values $a_index end] + ##################################### + # full rescans for later processors + set remaining_values $source_values ;#source_values shrinks as commands take arguments + set a_index 0 + ##################################### + + do_debug 3 $debugc "-------->________>p '$processorname' remaining vals $remaining_values" + + #!todo - use v_map as an extra determinant to stop sequence for a command-set. (don't extend beyond where args have already been snipped by another command) + if {[$objp name] eq "tail_processor"} { + set mapcopy [flagfilter::class_vmap new {}] ;#no need to supply values as we are copying data from $VMAP + $VMAP copy_to $mapcopy + $objp set_map_object $mapcopy + } else { + $objp set_map_object $VMAP + } + foreach a $remaining_values { + set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index] + if {![string is integer -strict $argnum]} { + error "arg '$a' scan_index:$a_index - calculated argnum:'$argnum' is invalid" + + } + set sub_operand 0 + do_debug 3 $debugc "$argnum >eoptions_idx:$end_of_options_index a_index:$a_index __________________________________________________________a $a" + if {$end_of_options_index > -1} { + set end_of_options [expr {$a_index >= $end_of_options_index}] + } + + #review - data with leading - may be unintentionally interpreted as a flag + if {[string trim $a] eq "--"} { + #generally means end of options processing.. + #review - pass -- through?? + set last_arg_was_paramflag 0 ;#we don't treat first arg following end_of_options as belonging to the flag! - it is potentially an operand to the command + set is_solo_flag 0 + set end_of_options 1 + set end_of_options_index $a_index + #if {[lindex $p 0] eq "tail_processor"} { + $objp allocate $argnum "endofoptions" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + #} + } else { + if {($last_arg_was_paramflag) && ([$objp arg_is_defined_solo_to_me $a])} { + #last flag expecting param - but this flag *known* to be solo + #keep it simple and break out at first solo_flag related error ...unless it is trailing flag in the list + lappend argerrors [list flagerror solo_flag_following_non_solo_flag bad_flag $a] + set last_arg_was_solo 1 + break + } + #set is_solo_flag [expr {($a in $solo_flags)}] + #set is_solo_flag [is_this_flag_solo $a $solo_flags $objp] + set is_solo_flag [$objp arg_is_defined_solo_to_me $a] + + if {!$end_of_options} { + if {!$last_arg_was_paramflag} { + if {!$is_solo_flag} { + set is_args_flag [is_candidate_toplevel_param_flag $a $solo_flags $cmdprocessor_records] + #set is_args_flag [string match -* $a] + } + if {$is_args_flag || $is_solo_flag} { + if {[dict get $last_p_found by] eq $processorname} { + if {![is_this_flag_for_me $a $objp $cf_args]} { + if {$processorname ne "globalXXX"} { + do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit an unrecognized option: $a" + break + } + } + } + } + } else { + #last was flag expecting a param + set is_args_flag 0 + set is_solo_flag 0 + } + } else { + #end_of_options - ignore solo and other flags now. + set is_args_flag 0 + set is_solo_flag 0 + set last_arg_was_paramflag 0 + + } + + #puts stderr "!!!!!!!!!!!!!!!!!!1 here is_args_flag:$is_args_flag" + do_debug 3 $debugc " >________>________>is_p_flag: $is_p_flag last_arg_was_paramflag:$last_arg_was_paramflag is_args_flag:$is_args_flag is_solo: $is_solo_flag (soloflags:$solo_flags) a:$a " + if {!$is_args_flag && !$is_solo_flag } { + + if {!$last_arg_was_paramflag} { + if {[dict get $last_p_found by] eq $processorname} { + if {$processorname ne "tail_processor"} { + #we already found our unflagged value - and now we've hit another - time to break and hand it to a subcommand processor if any + do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit another value: $a" + break + } + } + set sequence_ok 1 ;#default assumption + set can_allocate [can_this_commandspec_allocate_this_arg $a $p $cf_args] + + if {$can_allocate} { + if {$is_sub} { + #!todo - use v_map as sequence terminator + #check if our find is in sequence + #we are only hunting non-flagged items and the the previous finder removes 1 from the source_values list + #therefore the a_index of our find should be the same if we are processing the very next argument. + #we have already checked that it was a related entity which found the last one. + #todo - review if it matters when parents/siblings don't eat all the way up to the next -flag. + #todo - implement a 'gather' key to keep eating in sequence and accumulate the values as a list + if {$a_index > [dict get $last_p_found index]} { + do_debug 3 $debugc "OUT OF SEQUENCE a_index:$a_index vs last_found index:[dict get $last_p_found index], $processorname disengaging - ignoring value $a and leaving it to the next processor" + set last_arg_was_paramflag 0 + do_debug 3 $debugc "<--- breaking --->" + break + } elseif {$a_index < [dict get $last_p_found index]} { + #too early.... found something before previous match + do_debug 3 $debugc "+++++++++++++++out of sequence $processorname - too early.. keeping scanning" + set sequence_ok 0 + } + if {$sequence_ok} { + set sub_operand 1 + } + } + } + + if {$can_allocate && $sequence_ok} { + #found a non-flagged value in the argumentlist to either reallocate to flagged values or to positional values + if {[dict exists $pinfo dispatch]} { + if {!$is_sub} { + #this must be the arg that caused the match + dict set dispatch $parentname [list command [dict get $pinfo dispatch] matched $a arguments [list] raw [list]] + } else { + #todo + lappend argerrors [list unsupported_dispatch $processorname] + } + } + if {$sub_operand} { + if {[dict exists $dispatch $parentname]} { + #todo - defaults? + add_dispatch_argument "dispatch" $parentname $processorname $a + add_dispatch_raw "dispatch" $parentname $a + } else { + #warning? + #lappend argerrors [list subcommand_unable_to_add_operand $processorname] + do_debug 3 $debugc "subcommand $processorname aborting scanning because parent command wasn't activated" + break + } + } + do_debug 2 $debugc " >+++++++>++++++++>++++++++>setting $processorname [if {$is_p_flag} {list -} {}]value $a" + if {$processorname eq "tail_processor"} { + set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index] + set argname arg$argnum + lappend remaining_unflagged $argname $a + lappend unconsumed_flags_and_values $a + dict set unflagged $argname $a + } elseif {$is_p_flag} { + $objp set_matched_argument $argnum $a + if {$is_sub} { + dict set extra_flags_from_positionals $p_sub $a + } else { + dict set extra_flags_from_positionals $parentname $a + } + lappend moved_to_flagged $processorname $a + #if has dependent commands ? - check for deep subcommand match? + } else { + $objp set_matched_argument $argnum $a + #lappend positional_values $a + dict set unflagged $processorname $a + } + do_debug 4 $debugc " >________>________>________>source_values :'$source_values'" + do_debug 3 $debugc " >________>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]" + + #---------------------------- + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + #------------------------------ + $objp allocate $argnum "operand" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr values_index ;#only increment when we allocate a value to one of the members of -commandprocessors + set last_arg_was_paramflag 0 + if {$processorname ne "tail_processor"} { + #don't break until we hit an unrecognized flag or another unflagged value + incr a_index -1 + #don't increment a_index before break, because we have shortened the list by 1. + #do_debug 3 $debugc "----breaking---" + #break + } else { + #decrement to compensate for shortened list because tail_processor continues to end + incr a_index -1 + } + } + + } else { + #last_arg_was_paramflag + set lastarg [dict get $last_p_found item] + #puts stdout "+++ lastarg: $lastarg source_values: [dict get $last_p_found source_values] a_index: $a_index" + if {$processorname eq "tail_processor"} { + lappend unconsumed_flags_and_values $a + } + if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $lastarg $objp $cf_args]} { + update_dispatch_argument "dispatch" $parentname $lastarg $a + add_dispatch_raw "dispatch" $parentname $a + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + $objp allocate $argnum "flagvalue" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + } + set last_arg_was_paramflag 0 + } + } else { + # is a flag of some sort ({!$is_args_flag && !$is_solo_flag} = false) + if {$processorname eq "tail_processor"} { + lappend unconsumed_flags_and_values $a + } + if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $a $objp $cf_args]} { + if {$is_solo_flag} { + add_dispatch_argument "dispatch" $parentname $a 1 + add_dispatch_raw "dispatch" $parentname $a + set last_arg_was_solo 1 + set last_arg_was_paramflag 0 + $objp allocate $argnum "soloflag" $a + } else { + add_dispatch_argument "dispatch" $parentname $a "" + add_dispatch_raw "dispatch" $parentname $a + set last_arg_was_solo 0 + set last_arg_was_paramflag 1 + $objp allocate $argnum "flag" $a + } + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + do_debug 4 $debugc " >2_______>________>________>source_values :'$source_values'" + do_debug 3 $debugc " >2_______>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]" + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + } else { + #auto alternate based on last value.. unless end_of_options + if {!$end_of_options} { + if {$a in $solo_flags} { + set last_arg_was_solo 1 + set last_arg_was_paramflag 0 + } else { + set last_arg_was_paramflag 1 + } + } + if {$a_index eq ([llength $source_values]-1)} { + #puts "XXXXXXXXXXXXXXXXXXX $a_index source_values:'$source_values'" + #if at end of list don't retain any last..was info. + set last_arg_was_solo 0 + set last_arg_was_paramflag 0 + } + #skip - don't eat + } + } + } + incr a_index + } + + if {![$objp found_match]} { + + #after break - we have retained vars: $parent, $sub_operand $pinfo $processorname etc + #didn't find an unflagged var - set a default if one was specified. + #do nothing otherwise - check_args will determine if it was -required etc. + #review - should only apply if parent cmd found something? + if {[dict exists $pinfo default]} { + set defaultval [dict get $pinfo default] + if {$is_p_flag} { + if {$is_sub} { + dict set extra_flags_from_positionals $p_sub $defaultval + } else { + dict set extra_flags_from_positionals $processorname $defaultval + } + #lappend moved_to_flagged $processorname $defaultval + lappend implied_flagged $processorname $defaultval + do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval implied_flagged: $implied_flagged " + } else { + lappend implied_unflagged $processorname $defaultval + dict set unflagged $processorname $defaultval + do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval moved_to_flagged: $moved_to_flagged " + } + + if {$is_sub && !$sub_operand} { + if {[dict exists $dispatch $parentname]} { + add_dispatch_argument "dispatch" $parentname $processorname $defaultval + } else { + lappend argerrors [list subcommand_unable_to_add_default_operand $processorname $defaultval] + } + } + } + } + + if {[$objp name] eq "tail_processor"} { + $VMAP update_map_from [$objp get_map_object] + } + + if {[llength $argerrors]} { + set parsestatus "error" + #abort processing at first error - we won't be able to make sense of the remaining args anyway + #even the tail_processor won't be able to classify reliably because flag meanings depend on the configured commands + break + } + } + + #assertion - should be none? + #set remaining_values [lrange $source_values $a_index end] + #do_debug 3 $debugc "-------->________>end of processing - remaining vals $remaining_values" + + do_debug 2 $debugc "========>=========>originals : $values" + do_debug 2 $debugc "[$VMAP get_map]" + do_debug 2 $debugc "========>=========>unconsumed: $unconsumed_flags_and_values" + + + + + + set all_flagged [$VMAP get_merged_flagged_by_class *] + set all_flagged_plus [concat $all_flagged $extra_flags_from_positionals] + + set all_flagged_list [$VMAP get_list_flagged_by_class *] + set all_flagged_list [concat $all_flagged_list $extra_flags_from_positionals] + + set remaining_flagged [$VMAP get_merged_flagged_by_class "unallocated"] + + set remaining_flagged_list [$VMAP get_list_flagged_by_class "unallocated"] + + + set unflagged_list_in_processing_order [dict values $unflagged] + set unflagged_list [$VMAP get_list_unflagged_by_class *] + + set unflagged_list_remaining [$VMAP get_list_unflagged_by_class "unallocated"] + + return [dict create \ + listremaining $unconsumed_flags_and_values \ + parseerrors $argerrors \ + parsestatus $parsestatus \ + flagged $all_flagged_plus \ + flaggedlist $all_flagged_list \ + flaggedremaining $remaining_flagged \ + flaggedlistremaining $remaining_flagged_list \ + unflagged $unflagged \ + unflaggedlist $unflagged_list \ + unflaggedremaining $remaining_unflagged \ + unflaggedlistremaining $unflagged_list_remaining \ + flaggednew $extra_flags_from_positionals \ + arglist [concat $unflagged_list_in_processing_order $all_flagged] \ + arglistremaining [concat $unflagged_list_remaining $remaining_flagged] \ + impliedflagged $implied_flagged \ + impliedunflagged $implied_unflagged \ + dispatch $dispatch \ + classifications [$VMAP get_map] \ + gridstring "\n[$VMAP grid]" \ + vmapobject "flagfilter::VMAP_$runid" \ + ] + } + + + + + + + + + + + + #specialisation for collection class to contain commandprocessors + # we expect to use only a single instance of this + oo::class create col_allprocessors { + superclass oolib::collection + variable o_commandspecs + method add_processor {p} { + my add $p [$p name] + if {[$p is_sub]} { + set parentname [$p parentname] + set obj_parent [my item $parentname] + set col_siblings [$obj_parent children] + $col_siblings add $p [$p name] + } + } + method set_commandspecs {cspecs} { + set o_commandspecs $cspecs + } + method get_commandspecs {} { + set o_commandspecs + } + #treating as singleton.. todo tidy + method name_from_record {rec} { + lassign $rec parentname pinfo + if {[dict exists $pinfo sub]} { + set name [join [list $parentname [dict get $pinfo sub]] .] + } else { + set name $parentname + } + return $name + } + method object_from_record {rec} { + set name [my name_from_record $rec] + return [my item $name] + } + #basic check if arg may consume the following one - not based on any specific info from processors + method arg_appears_standalone {f} { + if {(![string match "-*" $f]) && (![string match "/*" $f])} { + #not even flaglike + return 1 + } + if {$f in {- --}} { + return 1 + } + } + #does any processor define it as solo + method flag_can_be_solo {f} { + foreach objp [my items] { + if {[$objp arg_is_defined_solo_to_me $f]} { + return 1 + } + } + return 0 + } + } + oo::class create col_parents { + superclass oolib::collection + method add_parent {p} { + if {[$p is_sub]} { + error "cannot add a sub-processor to the main parents collection" + } + my add $p [$p name] + } + } + #each parent processor has a children collection which can only accept processors with sub defined. + oo::class create col_childprocessors { + superclass oolib::collection + variable o_ownername + method set_owner {parentname} { + set o_ownername $parentname + } + #owner of the collection (a parent processor) + method owner {} { + return $o_ownername + } + method add_processor {p} { + if {![$p is_sub]} { + error "processor must have 'sub' element to add to the parent's collection" + } + #check name matches this parent.. + + my add $p [$p name] + } + } + + oo::class create cprocessor { + variable o_runid + variable o_name + variable o_definition + variable o_pinfo + variable o_parentname + variable o_is_sub + variable o_col_children + variable o_mashopts + variable o_singleopts + variable o_pairopts + variable o_longopts + variable o_found_match ;#we directly matched a command trigger or positional argument + variable o_matched_argument + variable o_matched_argnum + variable o_matchspec + variable o_vmap + constructor {definition runid} { + set o_vmap "" + set o_definition $definition + set o_runid $runid + if {([llength $o_definition] < 2) || ([llength [lindex $o_definition 0]] != 1)} { + error "[self class].constructor Unable to interpret definition '$o_definition'" + } + lassign $o_definition o_parentname o_pinfo + if {([llength $o_pinfo] %2) != 0} { + error "[self class].constructor second element of definition '$o_definition' not a dict" + } + set o_is_sub [dict exists $o_pinfo sub] + if {!$o_is_sub} { + set o_name $o_parentname + set o_col_children [::flagfilter::col_childprocessors new] + $o_col_children set_owner $o_name + } else { + set o_name [join [list $o_parentname [dict get $o_pinfo sub]] .] + } + if {[dict exists $o_pinfo match]} { + set o_matchspec [dict get $o_pinfo match] + } else { + set o_matchspec {^[^-^/].*} ;#match anything that isn't flaglike + } + set o_found_match 0 + set o_matched_argument "" ;#need o_found_match to differentiate match of empty string + set o_matched_argnum -1 + #load mashopts etc at construction time as they're static + set o_mashopts [list] + set o_singleopts [list] + set o_pairopts [list] + set o_longopts [list] + if {[dict exists $o_pinfo mashopts]} { + lappend o_mashopts {*}[dict get $o_pinfo mashopts] + } + if {[dict exists $o_pinfo singleopts]} { + lappend o_singleopts {*}[dict get $o_pinfo singleopts] + } + if {[dict exists $o_pinfo pairopts]} { + lappend o_pairopts {*}[dict get $o_pinfo pairopts] + } + if {[dict exists $o_pinfo longopts]} { + lappend o_longopts {*}[dict get $o_pinfo longopts] + } + } + destructor { + catch {$o_vmap destroy} + if {!$o_is_sub} { + $o_col_children destroy + } + } + + method name {} { + return $o_name + } + #open things up during oo transition.. + method get_def {} { + return $o_definition + } + method is_flag {} { + if {[my is_sub]} { + #sub can be a flag even if parent isn't + set subname [dict get $o_pinfo sub] + return [string match -* $subname] + } else { + return [string match -* $o_name] + } + } + method has_same_parent {other} { + return [expr {[other parentname] eq $o_parentname}] + } + method is_sub {} { + return $o_is_sub + } + + method set_map_object {map} { + set o_vmap $map + } + method get_map_object {} { + return $o_vmap + } + method allocate {argnum type val} { + if {$o_vmap eq ""} { + error "[self class].allocate ($o_name) vmap is not set." + } + $o_vmap allocate [self object] $argnum $type $val + } + + method found_match {} { + return $o_found_match + } + method matched_argument {} { + return $o_matched_argument + } + method matched_argnum {} { + return $o_matched_argnum + } + method set_matched_argument {argnum a} { + #could be empty string + if {$o_found_match} { + error "[self object].set_matched_argument processor:$o_name already found match '$o_matched_argument' - cannot set again" + } + if {![my can_match $a]} { + error "error [self class].set_matched_argument processor:$o_name cannot match '$a' (matchspec: $o_matchspec)" + } + set o_found_match 1 + set o_matched_argument $a + set o_matched_argnum $argnum + } + method has_explicit_matchspec {} { + return [dict exists $o_pinfo match] + } + method matchspec {} { + return $o_matchspec + } + method can_match {a} { + if {!$o_found_match} { + foreach m $o_matchspec { + if {[regexp -- $m $a]} { + return 1 + } + } + return 0 + } else { + return 0 + } + } + #?? + method can_allocate_flags {} { + } + + + + + + #if we are a parent - this is own name + method parentname {} { + return $o_parentname + } + method parent {} { + return [::flagfilter::obj::PARENTS_$o_runid item $o_parentname] + } + method is_parent {} { + return [expr {!$o_is_sub}] + } + method children {} { + if {!$o_is_sub} { + return $o_col_children + } else { + #raise error? + return "" + } + } + method mashopts {} { + return $o_mashopts + } + method singleopts {} { + return $o_singleopts + } + method pairopts {} { + return $o_pairopts + } + method longopts {} { + return $o_longopts + } + + #whether flag categorized as solo by this processor + method arg_is_defined_solo_to_me {a} { + if {(![string match "-*" $a]) && (![string match "/*" $a])} { + #not even flaglike + return 0 + } + if {[my can_match $a]} { + return 0 + } + if {$a in {- --}} { + #specials not defined as solos + return 0 + } + + if {$o_name eq "global"} { + + } elseif {$o_name eq "tail_processor"} { + + } + + if {$a in $o_singleopts} { + return 1 + } + if {"any" in $o_singleopts} { + return 1 + } + set equalposn [string first "=" $a] + if {$equalposn >=1} { + if {"any" in $o_longopts} { + return 1 + } else { + set namepart [string range $a 0 $equalposn-1] + foreach lo $o_longopts { + if {[string match "${namepart}=*" $lo]} { + return 1 + } + } + } + } + #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash + #- but if it's a pairopt, but not mashable - we can rule it out now + if {($a in $o_pairopts) && ($a ni $o_mashopts)} { + return 0 + } + set flagletters [split [string range $a 1 end] ""] + set posn 1 + #trailing letters may legitimately not be in mashopts if they are part of a mashed value + #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing + foreach l $flagletters { + if {"-$l" ni $o_mashopts} { + #presumably an ordinary flag not-known to us + return 0 + } else { + if {"-$l" in $o_pairopts} { + if {$posn == [llength $flagletters]} { + #in pairopts and mash - but no value for it in the mash - thefore not a solo + return 0 + } else { + #entire tail is the value - this letter is effectively solo + return 1 + } + } elseif {"-$l" in $o_singleopts} { + #not allowed to take a value - keep processing letters + } else { + #can take a value! but not if at very end of mash. Either way This is a solo + return 1 + } + } + } + #This object should not treat the flag as a known solo + #- so if it is allowed to consume it, it may fall back on examining the subsequent argument's flaginess(?) + return 0 + } + + + method get_opts {} { + return [list mashopts $o_mashopts singleopts $o_singleopts pairopts $o_pairopts longopts $o_longopts] + } + #include parent opts + #we use the terminology 'option' for "-" prefixed items belonging to a -commandprocessors spec as opposed to more general -flags + #Note - this may also be called on the default "tail_processor", which will return empty sets, or an overridden tail_processor which may have data + method get_combined_opts {} { + set objparent [::flagfilter::obj::PARENTS_$o_runid item $o_parentname] + set parentopts [$objparent get_opts] + set mashopts [dict get $parentopts mashopts] + set singleopts [dict get $parentopts singleopts] + set pairopts [dict get $parentopts pairopts] + set longopts [dict get $parentopts longopts] + if {[my is_sub]} { + #this spec is a sub + set subopts [my get_opts] + #does order matter? could use struct::set union ? + foreach m [dict get $subopts mashopts] { + if {$m ni $mashopts} { + lappend mashopts $m + } + } + foreach s [dict get $subopts singleopts] { + if {$s ni $singleopts} { + lappend singleopts $s + } + } + foreach po [dict get $subopts pairopts] { + if {$po ni $pairopts} { + lappend pairopts $po + } + } + foreach lo [dict get $subopts longopts] { + if {$lo ni $longopts} { + lappend longopts $lo + } + } + + } + return [list mashopts $mashopts singleopts $singleopts pairopts $pairopts longopts $longopts] + } + + } + + + + + + + + + + + + proc get_command_info {cmdname cspecs} { + foreach item $cspecs { + lassign $item cmd specinfo + if {$cmd eq $cmdname && [dict exists $specinfo dispatch]} { + return $specinfo + } + } + return [list] + } + #### check_flags + # does not support unvalued flags - unless explicitly specified in -soloflags (global) or in -singleopts for a commandprocessor + #e.g not supported: v1 v2 -arg1 arg1val -debug -anotherflag anotherflagval + # - unless -soloflags is something like -soloflags {-debug} or -soloflags {{-debug 1}} where 1 is the default. In this case - we can no longer support accepting a value for -soloflags - the processor will not assign it an argument from the commandline. + #e.g not supported (unless -debug in -soloflags): v1 v2 -arg1 arg1val -anotherflag anotherflagval -debug + #e.g supported: v2 v2 -arg1 arg1val -debug 1 -anotherflag anotherflagval + # supports positional arguments - but only if specified in -commandprocessors + # todo + # - supports -- for treating following arg as value even if it looks like a flag + # - supports - for reading stdin + # expects at least -values + # other options -caller -defaults -required -extras -commandprocessors + # -soloflags (these are flags that *must* be solo - ie they cannot take an argument ) if no default specified they are boolean defaulting to 1, repeated instances in -values will be appended to a list. + # The only flag that can be a mix of solo or not, is the very last flag in the values list. In this case it must not be in the -soloflags list, but it will default to a boolean 1 to indicate presence. + proc check_flags {args} { + set runid [flagfilter::get_new_runid] + #################################################### + #puts "Entered checkflags, args $args" + set distanceToTop [info level] + set callerlist [list] + set was_dispatched_by_another 0 ;#used to + for {set i 1} {$i < $distanceToTop} {incr i} { + set callerlevel [expr {$distanceToTop - $i}] + set callerinfo [info level $callerlevel] + set firstword [lindex $callerinfo 0] + if {[string match "*check_flags*" $firstword]} { + set was_dispatched_by_another 1 + } + lappend callerlist $firstword + } + #puts stdout "callerlist: $callerlist" + + #first handle args for check_flags itself + if {[catch {lindex [info level -1] 0} caller]} { + set caller "" + } + #puts stderr ">>>>check_flags caller $caller" + get_one_paired_flag_value {-x 1} -x ;# + + #manually check for -caller even if unbalanced args + #we only need to use get_one_paired_flag_value because we haven't yet checked args is a properly formed paired list and if -caller is present we want to use it for clearer error messages. + #use normal dict operations to retrieve other flags. + #if failed to retrieve.. fall through to checks below + if {![catch {get_one_paired_flag_value $args -caller} flag_value_result]} { + set caller $flag_value_result + } + #puts stderr ">>>>check_flags caller $caller" + + + + + set cf_defaults [dict create\ + -caller $caller\ + -return [list arglistremaining]\ + -match [list]\ + -commandprocessors [list]\ + -soloflags [list]\ + -extras [list]\ + -defaults [list]\ + -required [list]\ + -values \uFFFF\ + -debugargs 0\ + ] + dict set cf_defaults -debugargsonerror 1 ;#error level to use when dispatch error occurs.. will not set lower than -debugargs + + + + if {([llength $args] % 2) != 0} { + do_error "check_flags error when called from '$caller' :check_flags must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $cf_defaults]' \n got: $args" + } + set cf_args $cf_defaults + foreach {k v} $args { + switch -- $k { + -caller - -return - -match - -commandprocessors - -soloflags - -extras - -defaults - -required - -values - -debugargs - -debugargsonerror { + dict set cf_args $k $v + } + default { + do_error "check_flags error when called from ${caller}: Unknown option '$k': must be one of '[dict keys $cf_defaults]' \nIf calling check_flags directly, put args being checked in -values {...}" + } + } + } + unset args + #################################################### + #now look at -values etc that check_flags is checking + + set caller [dict get $cf_args -caller] + + set debugargs [dict get $cf_args -debugargs] + dict set debugc -debugargs [dict get $cf_args -debugargs] + dict set debugc -source "check_flags $caller" + do_debug 1 $debugc "DEBUG-START $caller" + + set returnkey [dict get $cf_args -return] + set defaults [dict get $cf_args -defaults] + if {([llength $defaults] % 2) != 0} { + do_error "check_flags error when called from '$caller' :-defaults must be a list containing an even number of arguments of form: -flag value'" + } + set required [dict get $cf_args -required] + + + set acceptextra [dict get $cf_args -extras] + + set supplied [string trim [dict get $cf_args -values]] + set soloflags [dict get $cf_args -soloflags] ;#By their nature - solo flags are unlikely to be automatically 'required' - review + set solos_with_defaults [list] + foreach solo_spec $soloflags { + if {[llength $solo_spec] == 1} { + lappend solos_with_defaults $solo_spec 1 + } else { + lappend solos_with_defaults [lindex $solo_spec 0] [lindex $solo_spec 1] + } + + } + + if {$debugargs >= 3} { + set prefix "| $caller>" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + puts -nonewline stderr "$prefix input\n" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #puts stderr "$caller $cf_args" + dict for {k v} $cf_args { + if {$k ne "-commandprocessors"} { + puts -nonewline stderr "$prefix \[$k\]\n" + puts -nonewline stderr "$prefix $v\n" + } + } + if {$debugargs >=4} { + puts -nonewline stderr "$prefix \[-commandprocessors\]\n" + foreach record [dict get $cf_args -commandprocessors] { + puts -nonewline stderr "$prefix $record\n" + } + } + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #dict for {key val} $cf_args { + # puts stderr " $key" + # puts stderr " $val" + #} + } + + + ################################################################################################## + # allocate_arguments does the main work of processing non-flagged items in the main supplied argument list into flagged versions depending on the specs in -commandprocessors + # It sets defaults only for those arguments processed by a '-commandprocessors' spec. + # We must supply it with the -soloflags info because the solo flags affect what is considered an operand. + set command_specs [dict get $cf_args -commandprocessors] ;#may be empty list - that's ok - it will still populate the 'flagged' and 'arglist' return-dict members. + + #some of these are keys returned by allocate_arguments + # - some (e.g supplied) are added by check_flags + # This list is the list of -return values that can be used with check_args + set flaginfo_returns [list \ + parseerrors \ + parsestatus \ + flagged \ + flaggedremaining \ + flaggednew \ + unflagged \ + unflaggedremaining \ + unflaggedlistremaining \ + listremaining \ + arglist \ + arglistremaining \ + impliedunflagged \ + impliedflagged \ + classifications \ + gridstring \ + ranges \ + dispatch \ + dispatchstatuslist \ + dispatchresultlist \ + dispatchstatus \ + supplied \ + defaults \ + status \ + vmapobject \ + ] + + set PROCESSORS [col_allprocessors create ::flagfilter::obj::PROCESSORS_$runid] + set PARENTS [col_parents create ::flagfilter::obj::PARENTS_$runid] + + # + #set command_specs [concat [list {global {}}] $command_specs] + lappend command_specs {tail_processor {}} + + foreach cspec $command_specs { + set obj [cprocessor new $cspec $runid] ;#runid gives access to the context-objects PROCESSORS_runid & PARENTS_runid + if {[$obj is_parent]} { + $PARENTS add_parent $obj + } + #do_debug 1 $debugc "CONFIGURING OBJECT for commandprocessor [$obj name]" + $PROCESSORS add_processor $obj + } + do_debug 1 $debugc "ADDED [$PROCESSORS count] processors to main commandprocessor collection" + do_debug 1 $debugc "ADDED [$PARENTS count] processors to the parents collection" + $PROCESSORS set_commandspecs $command_specs + + #allocate_arguments uses the PROCESSORS object + set processed_arguments [allocate_arguments $PROCESSORS $solos_with_defaults $supplied $cf_args $caller] + #set processed_arguments [allocate_arguments {} $supplied] + + set newly_flagged_positionals [dict get $processed_arguments flaggednew] + set unflaggedremaining [dict get $processed_arguments unflaggedremaining] + set unflaggedlistremaining [dict get $processed_arguments unflaggedlistremaining] + set dispatch [dict get $processed_arguments dispatch] + set flaggedremaining [dict get $processed_arguments flaggedremaining] + set RETURNED_VMAP [dict get $processed_arguments vmapobject] + + + + if {$debugargs >= 3} { + set prefix "| $caller>" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + puts -nonewline stderr "$prefix output\n" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #puts stderr "processed_arguments: $processed_arguments" + dict for {key val} $processed_arguments { + puts -nonewline stderr "$prefix $key\n" + puts -nonewline stderr "$prefix $val\n" + } + puts -nonewline stderr "$prefix [string repeat - 30]\n" + } + + ################################################################################################## + + + + + + if {![llength $newly_flagged_positionals]} { + if {($supplied eq "\uFFFF") || ![llength $supplied]} { + #do_error "check_flags error when called from ${caller}: missing or empty -values" + } + } + + #probably not something to enforce... we might pass on unbalanced lists to other check_args etc. + #if {([llength $supplied] % 2) != 0} { + # do_error "${caller}: Error. $caller must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $defaults]'\n received values: $supplied" + #} + + + + set new_arg_list [dict get $processed_arguments arglistremaining] + set flagged_list [dict get $processed_arguments flagged] + #set suppliedkeys_with_extrakeys [concat [dict keys $supplied] [dict keys $newly_flagged_positionals]] + #puts stdout "suppliedkeys and new keys: $suppliedkeys_with_extrakeys" + + #todo - add flaggednew to required if all was specified? + #check invalid flags if not indicated in -extras , either explicitly or with 'extra' + set flags_from_required [get_flagged_only $required {}] + #set known_flags [lsort -unique -nocase [concat [dict keys $defaults] $flags_from_required $soloflags]] ;#why -nocase? why should -l and -L collapse to the uppercase version? + set known_flags [punk::lib::lunique_unordered [concat [dict keys $defaults] $flags_from_required $soloflags ]] + foreach spec $command_specs { + lassign $spec parentname pinfo + if {[string match -* $parentname] && $parentname ni $known_flags} { + lappend known_flags $parentname + } + if {[dict exists $pinfo sub]} { + if {[string match -* [dict get $pinfo sub]]} { + lappend known_flags [dict get $pinfo sub] + } + } + } + do_debug 2 $debugc "------------------->known_flags: $known_flags soloflags:$soloflags" + set invalid_flags [list] + if {"all" ni [string tolower $acceptextra]} { + if {"none" in [string tolower $acceptextra]} { + set ok_extras [list] + } elseif {[llength $acceptextra]} { + set ok_extras $acceptextra + } + #todo + #puts stderr " check_flags - temporary disable of checking for invalid flags" + set pairflagged $flagged_list + foreach {f v} $pairflagged { + if {$f ni $acceptextra && $f ni $known_flags} { + lappend invalid_flags $f + } + } + } + if {[llength $invalid_flags]} { + do_error "check_flags $caller error when called from ${caller}: unknown flags '$invalid_flags'" + } + + set calc_required [list] + set keywords_in_required [lsearch -inline -all -not $required -*] + set bad_keywords_in_required [lsearch -regexp -nocase -all -inline -not $keywords_in_required "all|none"] + if {[llength $bad_keywords_in_required]} { + do_error "check_flags error when called from ${caller}: bad flags in '-required' it must be a list of flags of the form -flagname or ONLY one of the keywords 'none' or 'all'" + } + #keywords_in_required now known to be only comprised of (possibly case variant) values of all|none + if {[llength $keywords_in_required] > 1} { + do_error "check_flags error when called from ${caller}: specifying both 'none' and 'all' in -required is not valid, and repeated values are not valid." + } + if {"none" eq [string tolower [lindex $keywords_in_required 0]]} { + set calc_required [list] + } + set flags [lsearch -inline -all $required -*] + + if {[llength $required]} { + if {[lsearch -nocase $keywords_in_required "all"] >= 0} { + #'all' can be present with other flags - and indicates we also require all the flags from -defaults + dict for {k -} $defaults { + if {$k ni $calc_required} { + lappend calc_required $k + } + } + } + } + + set classifications [dict get $processed_arguments classifications] ;#assertion - ordered by numerically increasing key representing positions in supplied argument list + set rangesets [$RETURNED_VMAP get_ranges_from_classifications $classifications] + set ranges [dict get $rangesets -ranges] + set rangesbytype [dict get $rangesets -rangesbytype] ;#unallocated are split into flag,operand and endofoptions - further splitting is easy enough to do by looking up the classifications list for each position in the supplied arg list. + #tailflags are the same for all dispatch items + set tailflagspaired [tailflagspaired $defaults $supplied $classifications $rangesbytype] + + + set dict_supplied [dict create supplied $supplied] + set dict_defaults [dict create defaults $defaults] + set dict_ranges [dict create ranges $ranges] + set dict_rangesbytype [dict create rangesbytype $rangesbytype] + set raise_dispatch_error_instead_of_return "" + set dict_dispatch_results [list dispatchstatuslist [list] dispatchresultlist [list] dispatchstatus "ok"] + #todo - only dispatch if no unallocated args (must get tail_processor to allocate known flags to 'global') + if {[llength $dispatch]} { + set dispatchstatuslist [list] + set dispatchresultlist [list] + set dispatchstatus "ok" + #each dispatch entry is a commandname and dict + #set dispatchrecord [lrange $dispatch 0 1] + set re_argnum {%arg([0-9^%]+)%} + set re_argtake {%argtake([0-9^%]+)%} + set re_dquotedparts {(?:(?:\"[^\"]*\")|(?:\"[^\"]*"))|(?:\S*[^ \"])} ;#for use with regexp -all -inline + #e.g {"a b" 'b x' "x cd "e f" g a} -> {"a b"} 'b x' {"x cd "} e f {" g a} + #dumb-editor rebalancing quote for above comment " + foreach {parentname dispatchrecord} $dispatch { + set commandinfo [get_command_info $parentname $command_specs] + + do_debug 1 $debugc ">>>>>DISPATCHRECORD: $dispatchrecord" + + # e.g lscmd lscmd natsortcommandline_ls lscmd.dir x + + do_debug 2 $debugc "commandinfo for $parentname: $commandinfo" + set command [dict get $dispatchrecord command] + #support for %x% placeholders in dispatchrecord command + set command [string map {%match% %matched%} $command] ;#alias + set command [string map [list %matched% [dict get $dispatchrecord matched]] $command] + + set argnum_indices [regexp -indices -all -inline $re_argnum $command] + if {[llength $argnum_indices]} { + foreach {argx_indices x_indices} $argnum_indices { + #argx eg %arg12% + set argx [string range $command {*}$argx_indices] + set x [string range $command {*}$x_indices] + set command [string map [list $argx [lindex [dict get $dispatchrecord arguments] $x]] $command] + } + } + + set argsreduced [dict get $dispatchrecord arguments] + #set rawparts [regexp -all -inline $re_dquotedparts [dict get $dispatchrecord raw]] + + #review! + #how will this behave differently on unix + package require punk::winrun + set rawparts [punk::winrun::unquote_wintcl [dict get $dispatchrecord raw]] + #set argtake_indices [regexp -indices -all -inline $re_argtake $command] + + + set start 0 + while {[regexp -start $start -indices $re_argtake $command argx_indices x_indices]} { + #argx eg %argtake12% + set argx [string range $command {*}$argx_indices] + set x [string range $command {*}$x_indices] + set argval [lindex [dict get $dispatchrecord arguments] $x] + set replacementlen [string length $argval] + set command [string map [list $argx $argval] $command] + set start [expr {[lindex $argx_indices 0] + $replacementlen}] + set argsreduced [lremove $argsreduced $x] + set rawparts [lremove $rawparts $x] + } + dict set dispatchrecord arguments $argsreduced + if {$start > 0} { + set rawreduced [join $rawparts] + dict set dispatchrecord raw $rawreduced + } + + set argvals [dict get $dispatchrecord arguments] + set matched_operands [list] + set matched_opts [list] + set matched_in_order [list] + set prefix "${parentname}." + set prefixlen [string length $prefix] + foreach {k v} $argvals { + #puts "$$$$ $k" + if {[string equal -length $prefixlen $prefix $k]} { + #key is prefixed with "commandname." + set k [string replace $k 0 $prefixlen-1] + } + #todo - -- ? + if {[string match -* $k]} { + lappend matched_opts $k $v + lappend matched_in_order $k $v + } else { + set kparts [split $k .] + lappend matched_operands $v + lappend matched_in_order $v + } + } + + if {![dict exists $commandinfo dispatchtype]} { + set dispatchtype tcl + } else { + set dispatchtype [dict get $commandinfo dispatchtype] + } + if {![dict exists $commandinfo dispatchglobal]} { + if {$dispatchtype eq "tcl"} { + set dispatchglobal 1 + } else { + set dispatchglobal 0 + } + } else { + set dispatchglobal [dict get $commandinfo dispatchglobal] + } + #generally we only want to dispatch remaining flagged, and only at the tail end.(as opposed to flags occurring between command groups) + # -It doesn't usually make much sense to dispatch remaining unflagged items, and it would be rare to require flags occurring before the command. + #however - there are potential commands such as help, dryrun or maybe an analysis command that may need to see unconsumed operands or even look 'back' at prior items + ##update 2023-03 - we definitely want to look back to prior non-matches when we match on a script e.g tclsh8.6 -someflag etc xxx.tcl scriptarg1 -etc + # if we match and dispatch on *.tcl - then we may need 'tclsh8.6 -someflag etc' as the interpreter (possibly with arguments) to use. + # we may need a 'script' dispatchtype (as well as the option to just pass these prior arguments as additional options for some other dispatchtypes) + # + # todo - add supported dispatchglobal values such as all, pre, post, allpre, allpost, and classifications + # where pre & post are only those occurring directly before and after the command and its args, i.e not extending beyond any prior or subsequent other command. + # classifications would be flagged as -classifications $classifications whereas pre and post would be added directly if specified singly, or flagged with -pre, -post etc if multiple are specified + # Those beginning with 'all' should also be wrapped in flags, because potentially they come from disjointed sections of the argumentlist + # - and we generally shouldn't supply arguments next to each other that weren't contiguous in the original list + # The 1,true,yes,tailflagspaired value is designed for the usecase where a common set of tail flags e.g -debug can apply to any commands matched by the filter. + # tail = all unallocated args after final command, including operands and end-of-options '--' (todo) + # tailflags = all unallocated *contiguous* flags after the final command and final operands. (ie it will deliberately miss flags following last command if there is a later operand) (todo) + # tailflagspaired = same as tailflags, but any solo-flags are defaulted to 1 (flags not merged, so there might be duplicate keys) so that it's a fully paired list + # In other situations - post may make sense to get the very next set of unconsumed arguments. + if {[string tolower $dispatchglobal] in [list 1 true yes tailflagspaired]} { + set command_range_posn [lsearch -index 1 $ranges $parentname] + set extraflags $tailflagspaired + } else { + set extraflags [list] + } + + #jn concat allows $command to itself be a list + ##tcl dispatchtype + dict set dispatchrecord dispatchtype $dispatchtype + switch -- $dispatchtype { + tcl { + do_debug 1 $debugc "DISPATCHING with tcl arg order: $command $matched_operands $matched_opts $extraflags" + #set commandline [list $command {*}$matched_operands {*}$matched_opts {*}$extraflags] + set commandline [concat $command $matched_operands $matched_opts $extraflags] + } + raw { + do_debug 1 $debugc "DISPATCHING with raw args : $command [dict get $dispatchrecord raw]" + #set commandline [list $command {*}[dict get $dispatchrecord raw] {*}$extraflags] + set commandline [concat $command [dict get $dispatchrecord raw] $extraflags] + } + shell { + do_debug 1 $debugc "DISPATCHING with shell args : $command [dict get $dispatchrecord raw]" + #assume the shell arguments are in one quoted string? + set commandline [concat $command [list [dict get $dispatchrecord raw]] $extraflags] + } + default { + #non quoted shell? raw + defaults? + do_debug 1 $debugc "DISPATCHING with given arg order: $command $matched_in_order $extraflags" + #set commandline [list $command {*}$matched_in_order {*}$extraflags] + set commandline [concat $command $matched_in_order $extraflags] + } + } + + dict set dispatchrecord asdispatched $commandline + set dispatchresult "" + set dispatcherror "" + if {![catch {{*}$commandline} cmdresult]} { + set dispatchresult $cmdresult + lappend dispatchstatuslist [list status ok cmd $parentname outputlength [string length $cmdresult]] + lappend dispatchresultlist $cmdresult + } else { + set dispatchstatus "error" + set dispatcherror $cmdresult + #don't add to dispatchresultlist + lappend dispatchstatuslist [list status err cmd $parentname outputlength 0 error $cmdresult] + if {!$was_dispatched_by_another} { + #this is the first (or a direct) call to check_flags - so make sure error gets raised in this proc rather than just storing the error in the data and returning + set raise_dispatch_error_instead_of_return "dispatchstatuslist:\n[join $dispatchstatuslist \n] \nerrinfo:\n $::errorInfo" + dict set dispatchrecord result $dispatchresult + dict set dispatchrecord error $dispatcherror + dict set dispatch $parentname $dispatchrecord + + break + #return -code error "check_flags error during command dispatch:\n$cmdresult" + } + #we've been dispatched from another check_flags - so ok to propagate the error up via the dispatchrecord/dispatchstatuslist + } + dict set dispatchrecord result $dispatchresult + dict set dispatchrecord error $dispatcherror + dict set dispatch $parentname $dispatchrecord + } + + set dict_dispatch_results [list dispatchcaller $caller dispatchstatuslist $dispatchstatuslist dispatchresultlist $dispatchresultlist dispatchstatus $dispatchstatus] + } + #end llength $dispatch + + + set combined [dict merge $dict_defaults $dict_supplied $processed_arguments $dict_ranges $dict_rangesbytype $dict_dispatch_results] + dict set combined dispatch $dispatch ;#update with asdispatched info + if {([dict get $combined parsestatus] eq "ok") && ([dict get $combined dispatchstatus] eq "ok")} { + dict set combined status "ok" + } else { + dict set combined status "error" + } + do_debug 1 $debugc "COMBINED:$combined" + + + set returnkey [string tolower $returnkey] + if {"all" in $returnkey} { + set returnval $combined + #set returnval [dict merge $combined $dict_dispatch_results] + } else { + if {[llength $returnkey] == 1} { + set invalid 0 + #todo - support multiple merge? + set right "" + if {[regexp -all {\|} $returnkey] == 1} { + lassign [split $returnkey |] left right + set joinparts [split $left ,] + } else { + set joinparts [split $returnkey ,] + } + foreach j [concat $joinparts $right] { + if {$j ni $flaginfo_returns} { + set invalid 1 + } + } + set returnval [list] + if {!$invalid} { + foreach j $joinparts { + lappend returnval {*}[dict get $combined $j] + } + if {[string length $right]} { + set returnval [dict merge $returnval $defaults $returnval] + } + } else { + set returnval [list callerrors [list "-return '$returnkey' not valid"]] + } + } else { + set callerrors [list] + set returnval [dict create] + foreach rk $returnkey { + if {$returnkey in $flaginfo_returns} { + dict set returnval $rk [dict get $combined $returnkey] + } else { + lappend callerrors [list "-return '$returnkey' not valid"] + } + } + if {[llength $callerrors]} { + dict set returnval callerrors $callerrors + } + } + } + + do_debug 1 $debugc "[string repeat = 40]" + do_debug 1 $debugc "dispatch_results: $dict_dispatch_results" + do_debug 1 $debugc "[string repeat - 40]" + + if {[string length $raise_dispatch_error_instead_of_return]} { + set errdebug [dict get $cf_args -debugargsonerror] + if {$errdebug > [dict get $cf_args -debugargs]} { + dict set debugc -debugargs $errdebug + } + } + + set debuglevel_return 2 + set debugdict [concat {*}[lmap k [dict keys $combined] {list $k $debuglevel_return}]] ;#create a dict of keys from combined, all defaulted to $debuglevel_return + if {[llength [dict get $combined parseerrors]]} { + dict set debugdict "parseerrors" 0 + } else { + dict set debugdict "parseerrors" 2 + } + dict set debugdict "defaults" 1 + dict set debugdict "supplied" 1 + dict set debugdict "dispatch" 1 + dict set debugdict "ranges" 1 + dict set debugdict "rangesbytype" 1 + dict set debugdict "dispatchstatus" 1 + if {[dict get $combined "status"] eq "ok"} { + dict set debugdict "status" 1 + } else { + dict set debugdict "status" 0 + } + + do_debug 1 $debugc "returning '$returnkey'" + do_debug 1 $debugc "returnval '$returnval'" + if {([llength $returnval] % 2) == 0} { + do_debug 1 $debugc "returnkeys '[dict keys $returnval]'" + } + do_debug 1 $debugc "[string repeat = 40]" + dict for {k v} $combined { + set dlev [dict get $debugdict $k] + switch -- $k { + dispatch { + set col1 [string repeat " " 12] + #process as paired list rather than dict (support repeated commands) + set i 0 + foreach {cmdname cmdinfo} $v { + set field1 [string repeat " " [expr {[string length $cmdname]}]] + set col2_dispatch [string repeat " " [expr {[string length $cmdname] + 15}]] + set j 0 + foreach {ckey cval} $cmdinfo { + + if {$i == 0 && $j == 0} { + set c1 [overtype::left $col1 "dispatch"] + } else { + set c1 [overtype::left $col1 { ... }] + } + + if {$j == 0} { + set f1 [overtype::left $field1 $cmdname] + set c2 [overtype::left $col2_dispatch "$f1 $ckey"] + } else { + set f1 [overtype::left $field1 ...] + set c2 [overtype::left $col2_dispatch "$f1 $ckey"] + } + #leave at debug level 1 - because dispatch is generally important + do_debug $dlev $debugc "${c1}${c2} $cval" + + incr j + } + incr i + } + + #do_debug 1 $debugc "[overtype::left $col1 $k] [lindex $v 0] [list [lindex $v 1]]" + #foreach {nm rem} [lrange $v 2 end] { + # do_debug 1 $debugc "[overtype::left $col1 { ... }] $nm [list $rem]" + #} + } + dispatchresultlist { + set col1 [string repeat " " 25] + set i 0 + foreach dresult $v { + if {$i == 0} { + set c1 [overtype::left $col1 $k] + } else { + set c1 [overtype::left $col1 { ... }] + } + do_debug $dlev $debugc "$c1 $dresult" + incr i + } + } + classifications { + set col1 [string repeat " " 25] + set len [dict size $v] + if {$len == 0} { + do_debug $dlev $debugc "[overtype::left $col1 $k]" + continue + } + set max [expr {$len -1}] + set numlines [expr $len / 3 + 1] + if {($len % 3) == 0} { + incr numlines -1 + } + set j 0 + for {set ln 0} {$ln < $numlines} {incr ln} { + if {$ln == 0} { + set c1 "[overtype::left $col1 $k]" + } else { + set c1 "[overtype::left $col1 { ... }]" + } + set line "" + for {set col 0} {$col < 3} {incr col} { + if {$j <= $max} { + append line "$j [list [dict get $v $j]] " + } + incr j + } + do_debug $dlev $debugc "$c1 [string trim $line]" + } + } + gridstring { + set col1 [string repeat " " 25] + set i 0 + foreach ln [split $v \n] { + if {$i == 0} { + set c1 [overtype::left $col1 $k] + } else { + set c1 [overtype::left $col1 { ... }] + } + do_debug $dlev $debugc "$c1 $ln" + incr i + } + } + default { + set col1 [string repeat " " 25] + do_debug $dlev $debugc "[overtype::left $col1 $k] $v" + } + } + } + + + # --------------------------------- + foreach obj [$PARENTS items] { + catch {$obj destroy} + } + $PARENTS destroy + #puts "PROCESSORS: $PROCESSORS" + foreach obj [$PROCESSORS items] { + catch {$obj destroy} + } + $PROCESSORS destroy + catch {$RETURNED_VMAP destroy} + # --------------------------------- + + do_debug 1 $debugc "[string repeat = 40]" + do_debug 1 $debugc "DEBUG-END $caller" + if {[string length $raise_dispatch_error_instead_of_return]} { + return -code error $raise_dispatch_error_instead_of_return + } + + + return $returnval + } + + proc tailflagspaired {defaults supplied classifications rangesbytype} { + lassign [lindex $rangesbytype end] c tp a b + if {($c eq "unallocated") && ($tp eq "flagtype")} { + set tail_unallocated [lrange $supplied $a $b] + } else { + set tail_unallocated [list] + } + #set extraflags [list] + set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated] + #dict merge based operation can't work if there are solo_flags? + #review + if {[llength $tail_unallocated]} { + for {set i $a} {$i <=$b} {incr i} { + set arginfo [dict get $classifications $i] + lassign $arginfo class ftype v + switch -- $ftype { + flag - flagvalue { + lappend extraflags $v + } + soloflag { + lappend extraflags $v + if {[dict exists $defaults $v]} { + lappend extraflags [dict get $defaults $v] + } else { + lappend extraflags 1 + } + } + } + } + foreach {k v} [dict get $defaults] { + if {$k ni $extraflags} { + lappend extraflags $k $v + } + } + } else { + set extraflags $defaults + } + return $extraflags + } + + proc tailflagspaired1 {defaults supplied classifications rangesbytype} { + lassign [lindex $rangesbytype end] c tp a b + if {($c eq "unallocated") && ($tp eq "flagtype")} { + set tail_unallocated [lrange $supplied $a $b] + } else { + set tail_unallocated [list] + } + #set all_post_unallocated_ranges [lsearch -all -inline -index 0 [lrange $rangesbytype $command_range_posn end] "unallocated"] + + set extraflags [list] + + #set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated] + #dict merge based operation can't work if there are solo_flags with no value set + if {[llength $tail_unallocated]} { + for {set i $a} {$i <=$b} {incr i} { + set arginfo [dict get $classifications $i] + lassign $arginfo class ftype v + switch -- $ftype { + flag - flagvalue { + lappend extraflags $v + } + soloflag { + lappend extraflags $v + if {[dict exists $defaults $v]} { + lappend extraflags [dict get $defaults $v] + } else { + lappend extraflags 1 + } + } + } + } + foreach {k v} [dict get $defaults] { + if {$k ni $extraflags} { + lappend extraflags $k $v + } + } + } else { + set extraflags $defaults + } + + } + + + +} + + +namespace eval flagfilter { + + #punk::lib::dict_merge_ordered + + + + #retrieve *only* names that are dependant on the provided namekey - not the key itself + # (query is sorted by the trailing numerical index which represents order the arguments were processed) + proc flag_array_get_sorted_subs {arrname sep namekey} { + upvar $arrname arr + set allsubs [array names arr ${namekey}.*${sep}name,*] + set rnames [lmap nm $allsubs {string reverse $nm}] + set sorted_rnames [lsort -dictionary $rnames] + set ordered [lmap nm $sorted_rnames {string reverse $nm}] + return $ordered + } + + proc flag_array_get_sorted_siblings {arrname sep namekey} { + #determine parent by looking at dot - but confirm parent name is in array. + + } + + + + #dictionary based lsort of reversed names which are presumed to have a trailing separator of some sort and a number e.g: name,0 name,1 ... name,10 etc. + #use -dictionary to ensure embedded numbers are sorted as integers + proc array_names_sorted_by_tail {arrname nameglob} { + upvar $arrname arr + set matched_names [array names arr $nameglob] + set rnames [lmap nm $matched_names {string reverse $nm}] + set sorted_rnames [lsort -dictionary $rnames] + return [lmap nm $sorted_rnames {string reverse $nm}] + } + + +} + + + + + diff --git a/src/bootsupport/modules/funcl-0.1.tm b/src/bootsupport/modules/funcl-0.1.tm new file mode 100644 index 00000000..ccdc9d99 --- /dev/null +++ b/src/bootsupport/modules/funcl-0.1.tm @@ -0,0 +1,322 @@ +package provide funcl [namespace eval funcl { + variable version + set version 0.1 +}] +#funcl = function list (nested call structure) +# +#a basic functional composition o combinator +#o(f,g)(x) == f(g(x)) + +namespace eval funcl { + + #from punk + proc arg_is_script_shaped {arg} { + if {[string first " " $arg] >= 0} { + return 1 + } elseif {[string first \n $arg] >= 0} { + return 1 + } elseif {[string first ";" $arg] >= 0} { + return 1 + } elseif {[string first \t $arg] >= 0} { + return 1 + } else { + return 0 + } + } + + + proc o args { + set closing [string repeat {]} [expr [llength $args]-1]] + set body "[join $args { [}] \$data $closing" + return $body + } + + proc o_ args { + set body "" + set tails [lrepeat [llength $args] ""] + puts stdout "tails: $tails" + + set end [lindex $args end] + if {[llength $end] == 1 && [arg_is_script_shaped $end]} { + set endfunc [string map " $end" {uplevel 1 [list if 1 ]}] + } else { + set endfunc $end + } + if {[llength $args] == 1} { + return $endfunc + } + + set wrap { [} + append wrap $endfunc + append wrap { ]} + + set i 0 + foreach cmdlist [lrange $args 0 end-1] { + set is_script 0 + if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} { + set is_script 1 + set script [lindex $cmdlist 0] + } + set t "" + if {$i > 0} { + append body { [} + } + set posn [lsearch $cmdlist _] + if {$posn <= 0} { + append body $cmdlist + if {$i == [expr {[llength $args] -2}]} { + #append body " \$data" + append body " $wrap" + } + if {$i > 0} { + set t {]} + } + } else { + append body [lrange $cmdlist 0 $posn-1] + if {$i == [expr {[llength $args] -2}]} { + #append body " \$data" + append body " $wrap" + } + set t [lrange $cmdlist $posn+1 end] + if {$i > 0} { + append t { ]} + } + } + lset tails $i $t + incr i + } + append body [join [lreverse $tails] " "] + puts stdout "tails: $tails" + + return $body + } + + #review - consider _call -- if count > 1 then they must all be callable cmdlists(?) + # what does it mean to have additional _fn wrapper with no other elements? (no actual function) + #e.g _fn 2 5 6 somefunc {_fn 1 3 {_call 1 3 xxx}} {_fn 1 4 command {_fn ...}} + # what type indicates running subtrees in parallel vs sequentially? + # any reason to have _call count other than 1? Presumably the parent node indicates the parallelism/sequentialism etc. + # + # + # accept or return a funcl (or funcltree if multiple funcls in one commandlist) + # also accept/return a call - return empty list if passed a call + proc next_funcl {funcl_or_tree} { + if {[lindex $funcl_or_tree 0] eq "_call"} { + return [list] + } + if {[lindex $funcl_or_tree 0] in [list "_fn" "_call"]} { + set funcl $funcl_or_tree + } else { + error "funcltree not implemented" + } + + + set count [lindex $funcl 1] + if {$count == 0} { + #null funcl.. what is it? metadata/placeholder? + return $funcl + } + set indices [lrange $funcl 2 [expr {1 + $count}]] + set i 0 + foreach idx $indices { + if {$i > 0} { + #todo - return a funcltree + error "multi funcl not implemented" + } + set next [lindex $funcl $idx] + incr i + } + + return $next + + } + + #convert a funcl to a tcl script + proc funcl_script {funcl} { + if {![llength $funcl]} { + return "" + } + set body "" + set tails [list] + + set type [lindex $funcl 0] + if {$type ni [list "_fn" "_call"]} { + #todo - handle funcltree + error "type $type not implemented" + } + + + #only count of 1 with index 3 supported(?) + if {$type eq "_call"} { + #leaf + set cmdlist [lindex $funcl 3] + return $cmdlist + } + + #we will use next_funcl to walk the nodes.. todo support treefuncl response from next_funcl which could branch multiple times. + #by continually passing back the resulting treefuncl/funcl to next_funcl we can process in correct order (?) + # we would still need to maintain state to stitch it back together once returned from a subtree.. + # ie multiple tail parts + set count [lindex $funcl 1] + + if {$count == 1} { + set idx [lindex $funcl 2] + if {$idx == 3} { + set cmdlist_pre [list] + } else { + set cmdlist_pre [lrange $funcl 3 $idx-1] + } + append body $cmdlist_pre + set t [lrange $funcl $idx+1 end] + lappend tails $t + } else { + #?? + error "funcl_script branching not yet supported" + } + + + set get_next 1 + set i 1 + while {$get_next} { + set funcl [next_funcl $funcl] + if {![llength $funcl]} { + set get_next 0 + } + lassign $funcl type count idx ;#todo support count > 1 + if {$type eq "_call"} { + set get_next 0 + } + set t "" + if {$type eq "_call"} { + append body { [} + append body [lindex $funcl $idx] + append body { ]} + } else { + append body { [} + if {$idx == 3} { + set cmdlist_pre [list] + } else { + set cmdlist_pre [lrange $funcl 3 $idx-1] + } + append body $cmdlist_pre + set t [lrange $funcl $idx+1 end] + lappend tails $t + lappend tails { ]} + } + incr i + } + append body [join [lreverse $tails] " "] + #puts stdout "tails: $tails" + + return $body + } + + + interp alias "" o_of "" funcl::o_of_n 1 + + #o_of_n + #tcl list rep o combinator + # + # can take lists of ordinary commandlists, scripts and funcls + # _fn 1 x where 1 indicates number of subfuncls and where x indicates next funcl position (_fn list or _arg) + # _fn 0 indicates next item is an unwrapped commandlist (terminal command) + # + #o_of is equivalent to o_of_n 1 (1 argument o combinator) + #last n args are passed to the prior function + #e.g for n=1 f a b = f(a(b)) + #e.g for n=2, e f a b = e(f(a b)) + proc o_of_n {n args} { + puts stdout "o_of_n '$args'" + if {$n != 1} { + error "o_of_n only implemented for 1 sub-funcl" + } + set comp [list] ;#composition list + set end [lindex $args end] + if {[lindex $end 0] in {_fn _call}]} { + #is_funcl + set endfunc [lindex $args end] + } else { + if {[llength $end] == 1 && [arg_is_script_shaped $end]} { + #set endfunc [string map [list $end] {uplevel 1 [list if 1 ]}] + set endfunc [list _call 1 3 [list uplevel 1 [list if 1 [lindex $end 0]]]] + } else { + set endfunc [list _call 1 3 [list {*}$end]] + } + } + + if {[llength $args] == 1} { + return $endfunc + } + set comp $endfunc + set revlist [lreverse [lrange $args 0 end-1]] + foreach cmdlist $revlist { + puts stderr "o_of_n >>-- $cmdlist" + if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} { + set is_script 1 + set script [lindex $cmdlist 0] + set arglist [list data] + + set comp [list _fn 1 6 call_script $script $arglist $comp] + } else { + set posn1 [expr {[llength $cmdlist] + 2 + $n}] + set comp [list _fn $n $posn1 {*}$cmdlist $comp] + } + } + return $comp + } + proc call_script {script argnames args} { + uplevel 3 [list if 1 [list apply [list $argnames $script] {*}$args]] + } + proc funcl_script_test {scr} { + do_funcl_script_test $scr + } + proc do_funcl_script_test {scr} { + #set j "in do_funcl_script_test" + #set data "xxx" + #puts '$scr' + if 1 $scr + } + + #standard o_ with no script-handling + proc o_plain args { + set body "" + set i 0 + set tails [lrepeat [llength $args] ""] + #puts stdout "tails: $tails" + foreach cmdlist $args { + set t "" + if {$i > 0} { + append body { [} + } + set posn [lsearch $cmdlist _] + if {$posn <= 0} { + append body $cmdlist + if {$i == [expr {[llength $args] -1}]} { + append body " \$data" + } + if {$i > 0} { + set t {]} + } + } else { + append body [lrange $cmdlist 0 $posn-1] + if {$i == [expr {[llength $args] -1}]} { + append body " \$data" + } + set t [lrange $cmdlist $posn+1 end] + if {$i > 0} { + append t { ]} + } + } + lset tails $i $t + incr i + } + append body [join [lreverse $tails] " "] + #puts stdout "tails: $tails" + + return $body + } + #timings suggest no faster to split out the first item from the cmdlist loop +} + + + diff --git a/src/bootsupport/modules/include_modules.config b/src/bootsupport/modules/include_modules.config index c79eb6da..922ff786 100644 --- a/src/bootsupport/modules/include_modules.config +++ b/src/bootsupport/modules/include_modules.config @@ -2,18 +2,29 @@ #bootsupport modules can be pulled in from within other areas of src or from the built module folders of the project #They must be already built, so generally shouldn't come directly from src/modules. +#we want showdict - but it needs punk pipeline notation. +#this requires pulling in punk - which brings in lots of other stuff +#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 set bootsupport_modules [list\ src/vendormodules commandstack\ src/vendormodules cksum\ + src/vendormodules debug\ src/vendormodules dictutils\ src/vendormodules fauxlink\ src/vendormodules fileutil\ src/vendormodules http\ src/vendormodules md5\ + src/vendormodules metaface\ src/vendormodules modpod\ src/vendormodules oolib\ src/vendormodules overtype\ + src/vendormodules pattern\ + src/vendormodules patterncmd\ + src/vendormodules patternlib\ + src/vendormodules patternpredator2\ src/vendormodules sha1\ src/vendormodules tomlish\ src/vendormodules test::tomlish\ @@ -25,8 +36,15 @@ set bootsupport_modules [list\ src/vendormodules textutil::trim\ src/vendormodules textutil::wcswidth\ src/vendormodules uuid\ - modules punkcheck\ + modules argp\ + modules flagfilter\ + modules funcl\ modules natsort\ + modules punk\ + modules punkapp\ + modules punkcheck\ + modules punkcheck::cli\ + modules punk::aliascore\ modules punk::ansi\ modules punk::assertion\ modules punk::args\ @@ -35,6 +53,7 @@ set bootsupport_modules [list\ modules punk::cap::handlers::scriptlibs\ modules punk::cap::handlers::templates\ modules punk::char\ + modules punk::config\ modules punk::console\ modules punk::du\ modules punk::encmime\ @@ -46,6 +65,7 @@ set bootsupport_modules [list\ modules punk::mix::cli\ modules punk::mix::util\ modules punk::mix::templates\ + modules punk::repl::codethread\ modules punk::mix::commandset::buildsuite\ modules punk::mix::commandset::debug\ modules punk::mix::commandset::doc\ @@ -55,14 +75,18 @@ set bootsupport_modules [list\ modules punk::mix::commandset::project\ modules punk::mix::commandset::repo\ modules punk::mix::commandset::scriptwrap\ + modules punk::mod\ + modules punk::nav::fs\ modules punk::ns\ modules punk::overlay\ modules punk::path\ modules punk::packagepreference\ modules punk::repo\ modules punk::tdl\ + modules punk::unixywindows\ modules punk::zip\ modules punk::winpath\ + modules shellfilter\ modules textblock\ modules natsort\ modules oolib\ diff --git a/src/bootsupport/modules/metaface-1.2.5.tm b/src/bootsupport/modules/metaface-1.2.5.tm new file mode 100644 index 00000000..4c88cb16 --- /dev/null +++ b/src/bootsupport/modules/metaface-1.2.5.tm @@ -0,0 +1,6411 @@ +package require dictutils +package provide metaface [namespace eval metaface { + variable version + set version 1.2.5 +}] + + + + +#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 vinfo $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 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 {}} +proc ::p::-1::M {_ID_} { + 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 { + foreach m [dict keys [set ::p::${IID}::_iface::o_methods]] { + lappend members $m + } + } + return $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 {}} +proc ::p::-1::P {_ID_} { + 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 { + foreach prop [dict keys [set ::p::${IID}::_iface::o_properties]] { + lappend members $prop + } + } + 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_} { + 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/pattern-1.2.4.tm b/src/bootsupport/modules/pattern-1.2.4.tm new file mode 100644 index 00000000..5d76af04 --- /dev/null +++ b/src/bootsupport/modules/pattern-1.2.4.tm @@ -0,0 +1,1285 @@ +#PATTERN +# - A prototype-based Object system. +# +# Julian Noble 2003 +# License: Public domain +# + +# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. +# +# +# Pattern uses a mixture of class-based and prototype-based object instantiation. +# +# A pattern object has 'properties' and 'methods' +# The system makes a distinction between them with regards to the access syntax for write operations, +# and yet provides unity in access syntax for read operations. +# e.g >object . myProperty +# will return the value of the property 'myProperty' +# >ojbect . myMethod +# will return the result of the method 'myMethod' +# contrast this with the write operations: +# set [>object . myProperty .] blah +# >object . myMethod blah +# however, the property can also be read using: +# set [>object . myProperty .] +# Note the trailing . to give us a sort of 'reference' to the property. +# this is NOT equivalent to +# set [>object . myProperty] +# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property +# i.e it is equivalent in this case to: set blah + +#All objects are represented by a command, the name of which contains a leading ">". +#Any commands in the interp which use this naming convention are assumed to be a pattern object. +#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) + +#All user-added properties & methods of the wrapped object are accessed +# using the separator character "." +#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." +# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) +# you would use the 'Create' metamethod on the pattern object like so: +# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject +# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties +# of the object it was created from. ( + + +#The use of the access-syntax separator character "." allows objects to be kept +# 'clean' in the sense that the only methods &/or properties that can be called this way are ones +# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax +# so you are free to implement your own 'Create' method on your object that doesn't conflict with +# the metamethod. + +#Chainability (or how to violate the Law of Demeter!) +#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other +# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference +# structure, without the need to regress to enter matching brackets as is required when using +# standard TCL command syntax. +# ie instead of: +# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething +# we can use: +# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething +# +# This separates out the object-traversal syntax from the TCL command syntax. + +# . is the 'traversal operator' when it appears between items in a commandlist +# . is the 'reference operator' when it is the last item in a commandlist +# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. +# It marks breaks in the multidimensional structure that correspond to how the data is stored. +# e.g obj . arraydata x y , x1 y1 z1 +# represents an element of a 5-dimensional array structured as a plane of cubes +# e.g2 obj . arraydata x y z , x1 y1 +# represents an element of a 5-dimensional array structured as a cube of planes +# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 +# .. is the 'meta-traversal operator' when it appears between items in a commandlist +# .. is the 'meta-info operator'(?) when it is the last item in a commandlist + + +#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing +# implement iStacks & pStacks (interface stacks & pattern stacks) + +#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 + + +#------------------------------------------------------------ +# System objects. +#------------------------------------------------------------ +#::p::-1 ::p::internals::>metaface +#::p::0 ::p::ifaces::>null +#::p::1 ::>pattern +#------------------------------------------------------------ + +#TODO + +#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) + + +#CHANGES +#2018-09 - v 1.2.2 +# varied refactoring +# Changed invocant datastructure curried into commands (the _ID_ structure) +# Changed MAP structure to dict +# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) +# updated test suites +#2018-08 - v 1.2.1 +# split ::p::predatorX functions into separate files (pkgs) +# e.g patternpredator2-1.0.tm +# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken +# +#2017-08 - v 1.1.6 Fairly big overhaul +# New predator function using coroutines +# Added bang operator ! +# Fixed Constructor chaining +# Added a few tests to test::pattern +# +#2008-03 - preserve ::errorInfo during var writes + +#2007-11 +#Major overhaul + new functionality + new tests v 1.1 +# new dispatch system - 'predator'. +# (preparing for multiple interface stacks, multiple invocants etc) +# +# +#2006-05 +# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. +# +#2005-12 +# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. +# +# Fixed so that PatternVariable default applied on Create. +# +# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: +# - heading towards multiple-interface objects +# +#2005-10-28 +# 1.0.8.1 passes 80/80 tests +# >object .. Destroy - improved cleanup of interfaces & namespaces. +# +#2005-10-26 +# fixes to refsync (still messy!) +# remove variable traces on REF vars during .. Destroy +# passes 76/76 +# +#2005-10-24 +# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. +# 1.0.8.0 now passes 75/76 +# +#2005-10-19 +# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) +# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) +# 1.0.8.0 (passes 74/76) +# tests now in own package +# usage: +# package require test::pattern +# test::p::list +# test::p::run ?nameglob? ?-version ? +# +#2005-09?-12 +# +# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. +# fixed @next@ so that destination method resolved at interface compile time instead of call time +# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. +# (before, the overlay only occured when '.. Method' was used to override.) +# +# +# miscellaneous tidy-ups +# +# 1.0.7.8 (passes 71/73) +# +#2005-09-10 +# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value +# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. +# +#2005-09-07 +# bugfix indexed write to list property +# bugfix Variable default value +# 1.0.7.7 (passes 70/72) +# fails: +# arrayproperty.test - array-entire-reference +# properties.test - property_getter_filter_via_ObjectRef +# +#2005-04-22 +# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) +# +# 1.0.7.4 +# +#2004-11-05 +# basic PropertyRead implementation (non-indexed - no tests!) +# +#2004-08-22 +# object creation speedups - (pattern::internals::obj simplified/indirected) +# +#2004-08-17 +# indexed property setter fixes + tests +# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) +# +#2004-08-16 +# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) +# +#2004-08-15 +# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) +# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger +# - also trigger on curried traces to indexed properties i.e list and array elements. +# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. +# +# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] +# +#2004-08-05 +# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) +# +# fix + add tests to support method & property of same name. (method precedence) +# +#2004-08-04 +# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) +# +# 1.0.7.1 +# use objectref array access to read properties even when some props unset; + test +# unset property using array access on object reference; + test +# +# +#2004-07-21 +# object reference changes - array property values appear as list value when accessed using upvared array. +# bugfixes + tests - properties containing lists (multidimensional access) +# +#1.0.7 +# +#2004-07-20 +# fix default property value append problem +# +#2004-07-17 +# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods +# ( +# +#2004-06-18 +# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. +# +#2004-06-05 +# change argsafety operator to be anything with leading - +# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' +# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, +# the entire dash-prefixed operator is also passed in as an argument. +# e.g >object . doStuff -window . +# will call the doStuff method with the 2 parameters -window . +# >object . doStuff - . +# will call doStuff with single parameter . +# >object . doStuff - -window . +# will result in a reference to the doStuff method with the argument -window 'curried' in. +# +#2004-05-19 +#1.0.6 +# fix so custom constructor code called. +# update Destroy metamethod to unset $self +# +#1.0.4 - 2004-04-22 +# bug fixes regarding method specialisation - added test +# +#------------------------------------------------------------ + +package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] + + +namespace eval pattern::util { + + # Generally better to use 'package require $minver-' + # - this only gives us a different error + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } +} + +package require patterncmd 1.2.4- +package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) + + + +#package require cmdline +package require overtype + +#package require md5 ;#will be loaded if/when needed +#package require md4 +#package require uuid + + + + + +namespace eval pattern { + variable initialised 0 + + + if 0 { + if {![catch {package require twapi_base} ]} { + #twapi is a windows only package + #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. + # If available - windows seems to provide a fast uuid generator.. + #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) + # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) + interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok + } else { + #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) + # (e.g 200usec 2018 corei9) + #(with or without tcllibc?) + #very first call is extremely slow though - 3.5seconds on 2018 corei9 + package require uuid + interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate + } + #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) + } + + +} + + + + + + +namespace eval p { + #this is also the interp alias namespace. (object commands created here , then renamed into place) + #the object aliases are named as incrementing integers.. !todo - consider uuids? + variable ID 0 + namespace eval internals {} + + + #!?? + #namespace export ?? + variable coroutine_instance 0 +} + +#------------------------------------------------------------------------------------- +#review - what are these for? +#note - this function is deliberately not namespaced +# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features +proc process_pattern_aliases {object args} { + set o [namespace tail $object] + interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] + interp alias {} process_method_$o {} [$object .. Method .] + interp alias {} process_constructor_$o {} [$object .. Constructor .] +} +#------------------------------------------------------------------------------------- + + + + +#!store all interface objects here? +namespace eval ::p::ifaces {} + + + +#K combinator - see http://wiki.tcl.tk/1923 +#proc ::p::K {x y} {set x} +#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] + + + + + + + + +proc ::p::internals::(VIOLATE) {_ID_ violation_script} { + #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] + set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] + + if {![dict get $processed explicitvars]} { + #no explicit var statements - we need the implicit ones + set self [set ::p::${_ID_}::(self)] + set IFID [lindex [set $self] 1 0 end] + #upvar ::p::${IFID}:: self_IFINFO + + + set varDecls {} + set vlist [array get ::p::${IFID}:: v,name,*] + set _k ""; set v "" + if {[llength $vlist]} { + append varDecls "upvar #0 " + foreach {_k v} $vlist { + append varDecls "::p::\${_ID_}::$v $v " + } + append varDecls "\n" + } + + #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] + set violation_script $varDecls\n[dict get $processed body] + + #tidy up + unset processed varDecls self IFID _k v + } else { + set violation_script [dict get $processed body] + } + unset processed + + + + + #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. + eval "unset violation_script;$violation_script" +} + + +proc ::p::internals::DestroyObjectsBelowNamespace {ns} { + #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" + + set nsparts [split [string trim [string map {:: :} $ns] :] :] + if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { + #ns not of form ::p::?::_ref + + foreach obj [info commands ${ns}::>*] { + #catch {::p::meta::Destroy $obj} + #puts ">>found object $obj below ns $ns - destroying $obj" + $obj .. Destroy + } + } + + #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] + #foreach tinfo $traces { + # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo + #} + #unset -nocomplain ${ns}::-->PATTERN_ANCHOR + + foreach sub [namespace children $ns] { + ::p::internals::DestroyObjectsBelowNamespace $sub + } +} + + + + +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# + + + + + + + + + +proc ::p::get_new_object_id {} { + tailcall incr ::p::ID + #tailcall ::pattern::new_uuid +} + +#create a new minimal object - with no interfaces or patterns. + +#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} +proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { + + #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" + + if {$OID eq "-2"} { + set OID [::p::get_new_object_id] + #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) + #set OID [pattern::new_uuid] + } + #if $wrapped provided it is assumed to be an existing namespace. + #if {[string length $wrapped]} { + # #??? + #} + + #sanity check - alias must not exist for this OID + if {[llength [interp alias {} ::p::$OID]]} { + error "Object alias '::p::$OID' already exists - cannot create new object with this id" + } + + #system 'varspaces' - + + #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://wiki.tcl.tk/1030 'Dangers of creative writing') + #set o_open 1 - every object is initially also an open interface (?) + #NOTE! comments within namespace eval slow it down. + namespace eval ::p::$OID { + #namespace ensemble create + namespace eval _ref {} + namespace eval _meta {} + namespace eval _iface { + variable o_usedby; + variable o_open 1; + array set o_usedby [list]; + variable o_varspace "" ; + variable o_varspaces [list]; + variable o_methods [dict create]; + variable o_properties [dict create]; + variable o_variables; + variable o_propertyunset_handlers; + set o_propertyunset_handlers [dict create] + } + } + + #set alias ::p::$OID + + #objectid alis default_method object_command wrapped_namespace + set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] + + #MAP is a dict + set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] + + + + #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token + #we've already checked that ::p::$OID doesn't pre-exist + # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias + #interp alias {} ::p::$OID {} ::p::internals::predator $MAP + + + # _ID_ structure + set invocants_dict [dict create this [list $INVOCANTDATA] ] + #puts stdout "New _ID_structure: $interfaces_dict" + set _ID_ [dict create i $invocants_dict context ""] + + + interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ + #rename the command into place - thus the alias & the command name no longer match! + rename ::p::$OID $cmd + + set ::p::${OID}::_meta::map $MAP + + # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something + interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ + + #set p2 [string map {> ?} $cmd] + #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ + + + #trace add command $cmd delete "$cmd .. Destroy ;#" + #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" + + trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" + #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) + + #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" + + + #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" + #trace add command $cmd delete "puts deleting$cmd ;#" + #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" + + + #puts "--> new_object returning map $MAP" + return $MAP +} + + + + +#>x .. Create >y +# ".." is special case equivalent to "._." +# (whereas in theory it would be ".default.") +# "." is equivalent to ".default." is equivalent to ".default.default." (...) + +#>x ._. Create >y +#>x ._.default. Create >y ??? +# +# + +# create object using 'blah' as source interface-stack ? +#>x .blah. .. Create >y +#>x .blah,_. ._. Create .iStackDestination. >y + + + +# +# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] +# the 1st item, blah in this case becomes the 'default' iStack. +# +#>x .*. +# cast to object with all iStacks +# +#>x .*,!_. +# cast to object with all iStacks except _ +# +# --------------------- +#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' +# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. +# +#eg1: >x & >y . some_multi_method arg arg +# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) +# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' +# The invocant signature is thus {these 2} +# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) +# Invocation roles can be specified in the call using the @ operator. +# e.g >x & >y @ points . some_multi_method arg arg +# The invocant signature for this is: {points 2} +# +#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path +# This has the signature {objects n plane 1} where n depends on the length of the list $objects +# +# +# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. +# e.g set pointset [>x & >y .] +# We can now call multimethods on $pointset +# + + + + + + +#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) +proc ::pattern::predatorversion {{ver ""}} { + variable active_predatorversion + set allowed_predatorversions {1 2} + set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions + + if {![info exists active_predatorversion]} { + set first_time_set 1 + } else { + set first_time_set 0 + } + + if {$ver eq ""} { + #get version + if {$first_time_set} { + set active_predatorversions $default_predatorversion + } + return $active_predatorversion + } else { + #set version + if {$ver ni $allowed_predatorversions} { + error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" + } + + if {!$first_time_set} { + if {$active_predatorversion eq $ver} { + #puts stderr "Active predator version is already '$ver'" + #ok - nothing to do + return $active_predatorversion + } else { + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + rename ::p::internals::predator ::p::predator$active_predatorversion + } + } + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + + rename ::p::predator$ver ::p::internals::predator + set active_predatorversion $ver + + return $active_predatorversion + } +} +::pattern::predatorversion 2 + + + + + + + + + + + + +# >pattern has object ID 1 +# meta interface has object ID 0 +proc ::pattern::init args { + + if {[set ::pattern::initialised]} { + if {[llength $args]} { + #if callers want to avoid this error, they can do their own check of $::pattern::initialised + error "pattern package is already initialised. Unable to apply args: $args" + } else { + return 1 + } + } + + #this seems out of date. + # - where is PatternPropertyRead? + # - Object is obsolete + # - Coinjoin, Combine don't seem to exist + array set ::p::metaMethods { + Clone object + Conjoin object + Combine object + Create object + Destroy simple + Info simple + Object simple + PatternProperty simple + PatternPropertyWrite simple + PatternPropertyUnset simple + Property simple + PropertyWrite simple + PatternMethod simple + Method simple + PatternVariable simple + Variable simple + Digest simple + PatternUnknown simple + Unknown simple + } + array set ::p::metaProperties { + Properties object + Methods object + PatternProperties object + PatternMethods object + } + + + + + + #create metaface - IID = -1 - also OID = -1 + # all objects implement this special interface - accessed via the .. operator. + + + + + + set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface + + + #OID = 0 + ::p::internals::new_object ::p::ifaces::>null "" 0 + + #? null object has itself as level0 & level1 interfaces? + #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] + + #null interface should always have 'usedby' members. It should never be extended. + array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array + set ::p::0::_iface::o_open 0 + + set ::p::0::_iface::o_constructor [list] + set ::p::0::_iface::o_variables [list] + set ::p::0::_iface::o_properties [dict create] + set ::p::0::_iface::o_methods [dict create] + set ::p::0::_iface::o_varspace "" + set ::p::0::_iface::o_varspaces [list] + array set ::p::0::_iface::o_definition [list] + set ::p::0::_iface::o_propertyunset_handlers [dict create] + + + + + ############################### + # OID = 1 + # >pattern + ############################### + ::p::internals::new_object ::>pattern "" 1 + + #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] + + + array set ::p::1::_iface::o_usedby [list] ;#'usedby' array + + set _self ::pattern + + #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 + #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 + + + + #1)this object references its interfaces + #lappend ID $IFID $IFID_1 + #lset SELFMAP 1 0 $IFID + #lset SELFMAP 2 0 $IFID_1 + + + #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] + #proc ::>pattern args $body + + + + + ####################################################################################### + #OID = 2 + # >ifinfo interface for accessing interfaces. + # + ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object + set ::p::2::_iface::o_constructor [list] + set ::p::2::_iface::o_variables [list] + set ::p::2::_iface::o_properties [dict create] + set ::p::2::_iface::o_methods [dict create] + set ::p::2::_iface::o_varspace "" + set ::p::2::_iface::o_varspaces [list] + array set ::p::2::_iface::o_definition [list] + set ::p::2::_iface::o_open 1 ;#open for extending + + ::p::ifaces::>2 .. AddInterface 2 + + #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations + #(bootstrap because we can't yet use metaface methods on it) + + + + proc ::p::2::_iface::isOpen.1 {_ID_} { + return $::p::2::_iface::o_open + } + interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 + + proc ::p::2::_iface::isClosed.1 {_ID_} { + return [expr {!$::p::2::_iface::o_open}] + } + interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 + + proc ::p::2::_iface::open.1 {_ID_} { + set ::p::2::_iface::o_open 1 + } + interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 + + proc ::p::2::_iface::close.1 {_ID_} { + set ::p::2::_iface::o_open 0 + } + interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 + + + #proc ::p::2::_iface::(GET)properties.1 {_ID_} { + # set ::p::2::_iface::o_properties + #} + #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 + + #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties + + + #proc ::p::2::_iface::(GET)methods.1 {_ID_} { + # set ::p::2::_iface::o_methods + #} + #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 + #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods + + + + + + #link from object to interface (which in this case are one and the same) + + #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] + #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] + #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] + #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] + + interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen + interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed + interp alias {} ::p::2::open {} ::p::2::_iface::open + interp alias {} ::p::2::close {} ::p::2::_iface::close + + + #namespace eval ::p::2 "namespace export $method" + + ####################################################################################### + + + + + + + set ::pattern::initialised 1 + + + ::p::internals::new_object ::p::>interface "" 3 + #create a convenience object on which to manipulate the >ifinfo interface + #set IF [::>pattern .. Create ::p::>interface] + set IF ::p::>interface + + + #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? + # (or is forcing end user to add their own pStack/iStack ok .. ?) + # + ::p::>interface .. AddPatternInterface 2 ;# + + ::p::>interface .. PatternVarspace _iface + + ::p::>interface .. PatternProperty methods + ::p::>interface .. PatternPropertyRead methods {} { + varspace _iface + var {o_methods alias} + return $alias + } + ::p::>interface .. PatternProperty properties + ::p::>interface .. PatternPropertyRead properties {} { + varspace _iface + var o_properties + return $o_properties + } + ::p::>interface .. PatternProperty variables + + ::p::>interface .. PatternProperty varspaces + + ::p::>interface .. PatternProperty definition + + ::p::>interface .. Constructor {{usedbylist {}}} { + #var this + #set this @this@ + #set ns [$this .. Namespace] + #puts "-> creating ns ${ns}::_iface" + #namespace eval ${ns}::_iface {} + + varspace _iface + var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces + + set o_constructor [list] + set o_variables [list] + set o_properties [dict create] + set o_methods [dict create] + set o_varspaces [list] + array set o_definition [list] + + foreach usedby $usedbylist { + set o_usedby(i$usedby) 1 + } + + + } + ::p::>interface .. PatternMethod isOpen {} { + varspace _iface + var o_open + + return $o_open + } + ::p::>interface .. PatternMethod isClosed {} { + varspace _iface + var o_open + + return [expr {!$o_open}] + } + ::p::>interface .. PatternMethod open {} { + varspace _iface + var o_open + set o_open 1 + } + ::p::>interface .. PatternMethod close {} { + varspace _iface + var o_open + set o_open 0 + } + ::p::>interface .. PatternMethod refCount {} { + varspace _iface + var o_usedby + return [array size o_usedby] + } + + set ::p::2::_iface::o_open 1 + + + + + uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} + #uplevel #0 {package require patternlib} + return 1 +} + + + +proc ::p::merge_interface {old new} { + #puts stderr " ** ** ** merge_interface $old $new" + set ns_old ::p::$old + set ns_new ::p::$new + + upvar #0 ::p::${new}:: IFACE + upvar #0 ::p::${old}:: IFACEX + + if {![catch {set c_arglist $IFACEX(c,args)}]} { + #constructor + #for now.. just add newer constructor regardless of any existing one + #set IFACE(c,args) $IFACEX(c,args) + + #if {![info exists IFACE(c,args)]} { + # #target interface didn't have a constructor + # + #} else { + # # + #} + } + + + set methods [::list] + foreach nm [array names IFACEX m-1,name,*] { + lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) + } + + #puts " *** merge interface $old -> $new ****merging-in methods: $methods " + + foreach method $methods { + if {![info exists IFACE(m-1,name,$method)]} { + #target interface doesn't yet have this method + + set THISNAME $method + + if {![string length [info command ${ns_new}::$method]]} { + + if {![set ::p::${old}::_iface::o_open]} { + #interp alias {} ${ns_new}::$method {} ${ns_old}::$method + #namespace eval $ns_new "namespace export [namespace tail $method]" + } else { + #wait to compile + } + + } else { + error "merge interface - command collision " + } + #set i 2 ??? + set i 1 + + } else { + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + + set i [incr IFACE(m-1,chain,$method)] + + set THISNAME ___system___override_${method}_$i + + #move metadata using subindices for delegated methods + set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) + set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) + set IFACE(mp-$i,$method) $IFACE(mp-1,$method) + + set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) + set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) + + + #set next [::p::next_script $IFID0 $method] + if {![string length [info command ${ns_new}::$THISNAME]]} { + if {![set ::p::${old}::_iface::o_open]} { + interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method + namespace eval $ns_new "namespace export $method" + } else { + #wait for compile + } + } else { + error "merge_interface - command collision " + } + + } + + array set IFACE [::list \ + m-1,chain,$method $i \ + m-1,body,$method $IFACEX(m-1,body,$method) \ + m-1,args,$method $IFACEX(m-1,args,$method) \ + m-1,name,$method $THISNAME \ + m-1,iface,$method $old \ + ] + + } + + + + + + #array set ${ns_new}:: [array get ${ns_old}::] + + + #!todo - review + #copy everything else across.. + + foreach {nm v} [array get IFACEX] { + #puts "-.- $nm" + if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { + set IFACE($nm) $v + } + } + + #!todo -write a test + set ::p::${new}::_iface::o_open 1 + + #!todo - is this done also when iface compiled? + #namespace eval ::p::$new {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place + + return +} + + + + +#detect attempt to treat a reference to a method as a property +proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { +#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" + lassign [lrange $args end-2 end] vtraced vidx op + #NOTE! cannot rely on vtraced as it may have been upvared + + switch -- $op { + write { + error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + unset { + #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace + #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #!todo - don't use vtraced! + trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #pointless raising an error as "Any errors in unset traces are ignored" + #error "cannot unset. $field is a method not a property" + } + read { + error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + array { + error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" + #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" + } + } + + return +} + + + + +#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. +# +# The 'dispatcher' is an object instance's underlying object command. +# + +#proc ::p::make_dispatcher {obj ID IFID} { +# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { +# ::p::@IID@ $methprop @oid@ {*}$args +# }] +# return +#} + + + + +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +#aliased from ::p::${OID}:: +# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something +proc ::p::internals::no_default_method {_ID_ args} { + puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped + tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" +} + +#force 1 will extend an interface even if shared. (??? why is this necessary here?) +#if IID empty string - create the interface. +proc ::p::internals::expand_interface {IID {force 0}} { + #puts stdout ">>> expand_interface $IID [info level -1]<<<" + if {![string length $IID]} { + #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) + set iid [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$iid + return $iid + } else { + if {[set ::p::${IID}::_iface::o_open]} { + #interface open for extending - shared or not! + return $IID + } + + if {[array size ::p::${IID}::_iface::o_usedby] > 1} { + #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby + + #oops.. shared interface. Copy before specialising it. + set prev_IID $IID + + #set IID [::p::internals::new_interface] + set IID [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$IID + + ::p::internals::linkcopy_interface $prev_IID $IID + #assert: prev_usedby contains at least one other element. + } + + #whether copied or not - mark as open for extending. + set ::p::${IID}::_iface::o_open 1 + return $IID + } +} + +#params: old - old (shared) interface ID +# new - new interface ID +proc ::p::internals::linkcopy_interface {old new} { + #puts stderr " ** ** ** linkcopy_interface $old $new" + set ns_old ::p::${old}::_iface + set ns_new ::p::${new}::_iface + + + + foreach nsmethod [info commands ${ns_old}::*.1] { + #puts ">>> adding $nsmethod to iface $new" + set tail [namespace tail $nsmethod] + set method [string range $tail 0 end-2] ;#strip .1 + + if {![llength [info commands ${ns_new}::$method]]} { + + set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 + + #link from new interface namespace to existing one. + #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) + #!todo? verify? + #- actual link is chainslot to chainslot + interp alias {} ${ns_new}::$method.1 {} $oldhead + + #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? + + + #chainhead pointer within new interface + interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 + + namespace eval $ns_new "namespace export $method" + + #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { + # lappend ${ns_new}::o_methods $method + #} + } else { + if {$method eq "(VIOLATE)"} { + #ignore for now + #!todo + continue + } + + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + #warning - existing chainslot will be completely shadowed by linked method. + # - existing one becomes unreachable. #!todo review!? + + + error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" + + } + } + + + #foreach propinf [set ${ns_old}::o_properties] { + # lassign $propinf prop _default + # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop + # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop + # lappend ${ns_new}::o_properties $propinf + #} + + + set ${ns_new}::o_variables [set ${ns_old}::o_variables] + set ${ns_new}::o_properties [set ${ns_old}::o_properties] + set ${ns_new}::o_methods [set ${ns_old}::o_methods] + set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] + + + set ::p::${old}::_iface::o_usedby(i$new) linkcopy + + + #obsolete.? + array set ::p::${new}:: [array get ::p::${old}:: ] + + + + #!todo - is this done also when iface compiled? + #namespace eval ::p::${new}::_iface {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' + + return +} +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +pattern::init + +return $::pattern::version diff --git a/src/bootsupport/modules/patterncmd-1.2.4.tm b/src/bootsupport/modules/patterncmd-1.2.4.tm new file mode 100644 index 00000000..4107b8af --- /dev/null +++ b/src/bootsupport/modules/patterncmd-1.2.4.tm @@ -0,0 +1,645 @@ +package provide patterncmd [namespace eval patterncmd { + variable version + + set version 1.2.4 +}] + + +namespace eval pattern { + variable idCounter 1 ;#used by pattern::uniqueKey + + namespace eval cmd { + namespace eval util { + package require overtype + variable colwidths_lib [dict create] + variable colwidths_lib_default 15 + + dict set colwidths_lib "library" [list ch " " num 21 head "|" tail ""] + dict set colwidths_lib "version" [list ch " " num 7 head "|" tail ""] + dict set colwidths_lib "type" [list ch " " num 9 head "|" tail ""] + dict set colwidths_lib "note" [list ch " " num 31 head "|" tail "|"] + + proc colhead {type args} { + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + set line "" + foreach colname [dict keys $colwidths] { + append line "[col $type $colname [string totitle $colname] {*}$args]" + } + return $line + } + proc colbreak {type} { + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + set line "" + foreach colname [dict keys $colwidths] { + append line "[col $type $colname {} -backchar - -headoverridechar + -tailoverridechar +]" + } + return $line + } + proc col {type col val args} { + # args -head bool -tail bool ? + #---------------------------------------------------------------------------- + set known_opts [list -backchar -headchar -tailchar -headoverridechar -tailoverridechar -justify] + dict set default -backchar "" + dict set default -headchar "" + dict set default -tailchar "" + dict set default -headoverridechar "" + dict set default -tailoverridechar "" + dict set default -justify "left" + if {([llength $args] % 2) != 0} { + error "(pattern::cmd::util::col) ERROR: uneven options supplied - must be of form '-option value' " + } + foreach {k v} $args { + if {$k ni $known_opts} { + error "((pattern::cmd::util::col) ERROR: option '$k' not in known options: '$known_opts'" + } + } + set opts [dict merge $default $args] + set backchar [dict get $opts -backchar] + set headchar [dict get $opts -headchar] + set tailchar [dict get $opts -tailchar] + set headoverridechar [dict get $opts -headoverridechar] + set tailoverridechar [dict get $opts -tailoverridechar] + set justify [dict get $opts -justify] + #---------------------------------------------------------------------------- + + + + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + #calculate headwidths + set headwidth 0 + set tailwidth 0 + foreach {key def} $colwidths { + set thisheadlen [string length [dict get $def head]] + if {$thisheadlen > $headwidth} { + set headwidth $thisheadlen + } + set thistaillen [string length [dict get $def tail]] + if {$thistaillen > $tailwidth} { + set tailwidth $thistaillen + } + } + + + set spec [dict get $colwidths $col] + if {[string length $backchar]} { + set ch $backchar + } else { + set ch [dict get $spec ch] + } + set num [dict get $spec num] + set headchar [dict get $spec head] + set tailchar [dict get $spec tail] + + if {[string length $headchar]} { + set headchar $headchar + } + if {[string length $tailchar]} { + set tailchar $tailchar + } + #overrides only apply if the head/tail has a length + if {[string length $headchar]} { + if {[string length $headoverridechar]} { + set headchar $headoverridechar + } + } + if {[string length $tailchar]} { + if {[string length $tailoverridechar]} { + set tailchar $tailoverridechar + } + } + set head [string repeat $headchar $headwidth] + set tail [string repeat $tailchar $tailwidth] + + set base [string repeat $ch [expr {$headwidth + $num + $tailwidth}]] + if {$justify eq "left"} { + set left_done [overtype::left $base "$head$val"] + return [overtype::right $left_done "$tail"] + } elseif {$justify in {centre center}} { + set mid_done [overtype::centre $base $val] + set left_mid_done [overtype::left $mid_done $head] + return [overtype::right $left_mid_done $tail] + } else { + set right_done [overtype::right $base "$val$tail"] + return [overtype::left $right_done $head] + } + + } + + } + } + +} + +#package require pattern + +proc ::pattern::libs {} { + set libs [list \ + pattern {-type core -note "alternative:pattern2"}\ + pattern2 {-type core -note "alternative:pattern"}\ + patterncmd {-type core}\ + metaface {-type core}\ + patternpredator2 {-type core}\ + patterndispatcher {-type core}\ + patternlib {-type core}\ + patterncipher {-type optional -note optional}\ + ] + + + + package require overtype + set result "" + + append result "[cmd::util::colbreak lib]\n" + append result "[cmd::util::colhead lib -justify centre]\n" + append result "[cmd::util::colbreak lib]\n" + foreach libname [dict keys $libs] { + set libinfo [dict get $libs $libname] + + append result [cmd::util::col lib library $libname] + if {[catch [list package present $libname] ver]} { + append result [cmd::util::col lib version "N/A"] + } else { + append result [cmd::util::col lib version $ver] + } + append result [cmd::util::col lib type [dict get $libinfo -type]] + + if {[dict exists $libinfo -note]} { + set note [dict get $libinfo -note] + } else { + set note "" + } + append result [cmd::util::col lib note $note] + append result "\n" + } + append result "[cmd::util::colbreak lib]\n" + return $result +} + +proc ::pattern::record {recname fields} { + if {[uplevel 1 [list namespace which $recname]] ne ""} { + error "(pattern::record) Can't create command '$recname': A command of that name already exists" + } + + set index -1 + set accessor [list ::apply { + {index rec args} + { + if {[llength $args] == 0} { + return [lindex $rec $index] + } + if {[llength $args] == 1} { + return [lreplace $rec $index $index [lindex $args 0]] + } + error "Invalid number of arguments." + } + + }] + + set map {} + foreach field $fields { + dict set map $field [linsert $accessor end [incr index]] + } + uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec] +} +proc ::pattern::record2 {recname fields} { + if {[uplevel 1 [list namespace which $recname]] ne ""} { + error "(pattern::record) Can't create command '$recname': A command of that name already exists" + } + + set index -1 + set accessor [list ::apply] + + set template { + {rec args} + { + if {[llength $args] == 0} { + return [lindex $rec %idx%] + } + if {[llength $args] == 1} { + return [lreplace $rec %idx% %idx% [lindex $args 0]] + } + error "Invalid number of arguments." + } + } + + set map {} + foreach field $fields { + set body [string map [list %idx% [incr index]] $template] + dict set map $field [list ::apply $body] + } + uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec] +} + +proc ::argstest {args} { + package require cmdline + +} + +proc ::pattern::objects {} { + set result [::list] + + foreach ns [namespace children ::pp] { + #lappend result [::list [namespace tail $ns] [set ${ns}::(self)]] + set ch [namespace tail $ns] + if {[string range $ch 0 2] eq "Obj"} { + set OID [string range $ch 3 end] ;#OID need not be digits (!?) + lappend result [::list $OID [list OID $OID object_command [set pp::${ch}::v_object_command] usedby [array names ${ns}::_iface::o_usedby]]] + } + } + + + + + return $result +} + + + +proc ::pattern::name {num} { + #!todo - fix + #set ::p::${num}::(self) + + lassign [interp alias {} ::p::$num] _predator info + if {![string length $_predator$info]} { + error "No object found for num:$num (no interp alias for ::p::$num)" + } + set invocants [dict get $info i] + set invocants_with_role_this [dict get $invocants this] + set invocant_this [lindex $invocants_with_role_this 0] + + + #lassign $invocant_this id info + #set map [dict get $info map] + #set fields [lindex $map 0] + lassign $invocant_this _id _ns _defaultmethod name _etc + return $name +} + + +proc ::pattern::with {cmd script} { + foreach c [info commands ::p::-1::*] { + interp alias {} [namespace tail $c] {} $c $cmd + } + interp alias {} . {} $cmd . + interp alias {} .. {} $cmd .. + + return [uplevel 1 $script] +} + + + + + +#system diagnostics etc + +proc ::pattern::varspace_list {IID} { + namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_variables o_variables + + set varspaces [list] + dict for {vname vdef} $o_variables { + set vs [dict get $vdef varspace] + if {$vs ni $varspaces} { + lappend varspaces $vs + } + } + if {$o_varspace ni $varspaces} { + lappend varspaces $o_varspace + } + return $varspaces +} + +proc ::pattern::check_interfaces {} { + foreach ns [namespace children ::p] { + set IID [namespace tail $ns] + if {[string is digit $IID]} { + foreach ref [array names ${ns}::_iface::o_usedby] { + set OID [string range $ref 1 end] + if {![namespace exists ::p::${OID}::_iface]} { + puts -nonewline stdout "\r\nPROBLEM!!!!!!!!! nonexistant/invalid object $OID referenced by Interface $IID\r\n" + } else { + puts -nonewline stdout . + } + + + #if {![info exists ::p::${OID}::(self)]} { + # puts "PROBLEM!!!!!!!!! nonexistant object $OID referenced by Interface $IID" + #} + } + } + } + puts -nonewline stdout "\r\n" +} + + +#from: http://wiki.tcl.tk/8766 (Introspection on aliases) +#usedby: metaface-1.1.6+ +#required because aliases can be renamed. +#A renamed alias will still return it's target with 'interp alias {} oldname' +# - so given newname - we require which_alias to return the same info. + proc ::pattern::which_alias {cmd} { + uplevel 1 [list ::trace add execution $cmd enterstep ::error] + catch {uplevel 1 $cmd} res + uplevel 1 [list ::trace remove execution $cmd enterstep ::error] + #puts stdout "which_alias $cmd returning '$res'" + return $res + } +# [info args] like proc following an alias recursivly until it reaches +# the proc it originates from or cannot determine it. +# accounts for default parameters set by interp alias +# + + + +proc ::pattern::aliasargs {cmd} { + set orig $cmd + + set defaultargs [list] + + # loop until error or return occurs + while {1} { + # is it a proc already? + if {[string equal [info procs $cmd] $cmd]} { + set result [info args $cmd] + # strip off the interp set default args + return [lrange $result [llength $defaultargs] end] + } + # is it a built in or extension command we can get no args for? + if {![string equal [info commands $cmd] $cmd]} { + error "\"$orig\" isn't a procedure" + } + + # catch bogus cmd names + if {[lsearch [interp aliases {}] $cmd]==-1} { + if {[catch {::pattern::which_alias $cmd} alias]} { + error "\"$orig\" isn't a procedure or alias or command" + } + #set cmd [lindex $alias 0] + if {[llength $alias]>1} { + set cmd [lindex $alias 0] + set defaultargs [concat [lrange $alias 1 end] $defaultargs] + } else { + set cmd $alias + } + } else { + + if {[llength [set cmdargs [interp alias {} $cmd]]]>0} { + # check if it is aliased in from another interpreter + if {[catch {interp target {} $cmd} msg]} { + error "Cannot resolve \"$orig\", alias leads to another interpreter." + } + if {$msg != {} } { + error "Not recursing into slave interpreter \"$msg\".\ + \"$orig\" could not be resolved." + } + # check if defaults are set for the alias + if {[llength $cmdargs]>1} { + set cmd [lindex $cmdargs 0] + set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs] + } else { + set cmd $cmdargs + } + } + } + } + } +proc ::pattern::aliasbody {cmd} { + set orig $cmd + + set defaultargs [list] + + # loop until error or return occurs + while {1} { + # is it a proc already? + if {[string equal [info procs $cmd] $cmd]} { + set result [info body $cmd] + # strip off the interp set default args + return $result + #return [lrange $result [llength $defaultargs] end] + } + # is it a built in or extension command we can get no args for? + if {![string equal [info commands $cmd] $cmd]} { + error "\"$orig\" isn't a procedure" + } + + # catch bogus cmd names + if {[lsearch [interp aliases {}] $cmd]==-1} { + if {[catch {::pattern::which_alias $cmd} alias]} { + error "\"$orig\" isn't a procedure or alias or command" + } + #set cmd [lindex $alias 0] + if {[llength $alias]>1} { + set cmd [lindex $alias 0] + set defaultargs [concat [lrange $alias 1 end] $defaultargs] + } else { + set cmd $alias + } + } else { + + if {[llength [set cmdargs [interp alias {} $cmd]]]>0} { + # check if it is aliased in from another interpreter + if {[catch {interp target {} $cmd} msg]} { + error "Cannot resolve \"$orig\", alias leads to another interpreter." + } + if {$msg != {} } { + error "Not recursing into slave interpreter \"$msg\".\ + \"$orig\" could not be resolved." + } + # check if defaults are set for the alias + if {[llength $cmdargs]>1} { + set cmd [lindex $cmdargs 0] + set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs] + } else { + set cmd $cmdargs + } + } + } + } + } + + + + + +proc ::pattern::uniqueKey2 {} { + #!todo - something else?? + return [clock seconds]-[incr ::pattern::idCounter] +} + +#used by patternlib package +proc ::pattern::uniqueKey {} { + return [incr ::pattern::idCounter] + #uuid with tcllibc is about 30us compared with 2us + # for large datasets, e.g about 100K inserts this would be pretty noticable! + #!todo - uuid pool with background thread to repopulate when idle? + #return [uuid::uuid generate] +} + + + +#------------------------------------------------------------------------------------------------------------------------- + +proc ::pattern::test1 {} { + set msg "OK" + + puts stderr "next line should say:'--- saystuff:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. PatternMethod saystuff args { + puts stderr "--- saystuff: $args" + } + ::>thing .. Create ::>jjj + + ::>jjj . saystuff $msg + ::>jjj .. Destroy + ::>thing .. Destroy +} + +proc ::pattern::test2 {} { + set msg "OK" + + puts stderr "next line should say:'--- property 'stuff' value:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. PatternProperty stuff $msg + + ::>thing .. Create ::>jjj + + puts stderr "--- property 'stuff' value:[::>jjj . stuff]" + ::>jjj .. Destroy + ::>thing .. Destroy +} + +proc ::pattern::test3 {} { + set msg "OK" + + puts stderr "next line should say:'--- property 'stuff' value:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. Property stuff $msg + + puts stderr "--- property 'stuff' value:[::>thing . stuff]" + ::>thing .. Destroy +} + +#--------------------------------- +#unknown/obsolete + + + + + + + + +#proc ::p::internals::showargs {args {ch stdout}} {puts $ch $args} +if {0} { + proc ::p::internals::new_interface {{usedbylist {}}} { + set OID [incr ::p::ID] + ::p::internals::new_object ::p::ifaces::>$OID "" $OID + puts "obsolete >> new_interface created object $OID" + foreach usedby $usedbylist { + set ::p::${OID}::_iface::o_usedby(i$usedby) 1 + } + set ::p::${OID}::_iface::o_varspace "" ;#default varspace is the object's namespace. (varspace is absolute if it has leading :: , otherwise it's a relative namespace below the object's namespace) + #NOTE - o_varspace is only the default varspace for when new methods/properties are added. + # it is possible to create some methods/props with one varspace value, then create more methods/props with a different varspace value. + + set ::p::${OID}::_iface::o_constructor [list] + set ::p::${OID}::_iface::o_variables [list] + set ::p::${OID}::_iface::o_properties [dict create] + set ::p::${OID}::_iface::o_methods [dict create] + array set ::p::${OID}::_iface::o_definition [list] + set ::p::${OID}::_iface::o_open 1 ;#open for extending + return $OID + } + + + #temporary way to get OID - assumes single 'this' invocant + #!todo - make generic. + proc ::pattern::get_oid {_ID_} { + #puts stderr "#* get_oid: [lindex [dict get $_ID_ i this] 0 0]" + return [lindex [dict get $_ID_ i this] 0 0] + + #set invocants [dict get $_ID_ i] + #set invocant_roles [dict keys $invocants] + #set role_members [dict get $invocants this] + ##set this_invocant [lindex $role_members 0] ;#for the role 'this' we assume only one invocant in the list. + #set this_invocant [lindex [dict get $_ID_ i this] 0] ; + #lassign $this_invocant OID this_info + # + #return $OID + } + + #compile the uncompiled level1 interface + #assert: no more than one uncompiled interface present at level1 + proc ::p::meta::PatternCompile {self} { + ???? + + upvar #0 $self SELFMAP + set ID [lindex $SELFMAP 0 0] + + set patterns [lindex $SELFMAP 1 1] ;#list of level1 interfaces + + set iid -1 + foreach i $patterns { + if {[set ::p::${i}::_iface::o_open]} { + set iid $i ;#found it + break + } + } + + if {$iid > -1} { + #!todo + + ::p::compile_interface $iid + set ::p::${iid}::_iface::o_open 0 + } else { + #no uncompiled interface present at level 1. Do nothing. + return + } + } + + + proc ::p::meta::Def {self} { + error ::p::meta::Def + + upvar #0 $self SELFMAP + set self_ID [lindex $SELFMAP 0 0] + set IFID [lindex $SELFMAP 1 0 end] + + set maxc1 0 + set maxc2 0 + + set arrName ::p::${IFID}:: + + upvar #0 $arrName state + + array set methods {} + + foreach nm [array names state] { + if {[regexp {^m-1,name,(.+)} $nm _match mname]} { + set methods($mname) [set state($nm)] + + if {[string length $mname] > $maxc1} { + set maxc1 [string length $mname] + } + if {[string length [set state($nm)]] > $maxc2} { + set maxc2 [string length [set state($nm)]] + } + } + } + set bg1 [string repeat " " [expr {$maxc1 + 2}]] + set bg2 [string repeat " " [expr {$maxc2 + 2}]] + + + set r {} + foreach nm [lsort -dictionary [array names methods]] { + set arglist $state(m-1,args,$nm) + append r "[overtype::left $bg1 $nm] : [overtype::left $bg2 $methods($nm)] [::list $arglist]\n" + } + return $r + } + + + +} \ No newline at end of file diff --git a/src/bootsupport/modules/patternlib-1.2.6.tm b/src/bootsupport/modules/patternlib-1.2.6.tm new file mode 100644 index 00000000..bd4b3e59 --- /dev/null +++ b/src/bootsupport/modules/patternlib-1.2.6.tm @@ -0,0 +1,2590 @@ +#JMN 2004 +#public domain + + +package provide patternlib [namespace eval patternlib { + + variable version + set version 1.2.6 +}] + + + +#Change History +#------------------------------------------------------------------------------- +#2022-05 +# added . search and . itemKeys methods to >collection to enable lookups by value +#2021-09 +# Add >keyvalprotector - an object to overload various collection methods such as 'remove' to stop deletion of specific items. +# +#2006-05 +# deprecate 'del' in favour of 'remove' - 'del' still there but delegated to 'remove'. todo - emit deprecation warnings. +# +#2005-04 +# remove 'name' method - incorporate indexed retrieval into 'names' method +# !todo? - adjust key/keys methods for consistency? +# +#2004-10 +# initial key aliases support +# fix negative index support on some methods e.g remove +#2004-08 +# separated >collection predicate methods out onto separate 'mixin' object >predicatedCollection +# added $posn $result variables to predicate methods, changed varnames from $k $v to $key $value +# +#2004-06-05 +# added 'sort' method to sort on values. +# fixed 'keySort' method to accept multiple sort options +# added predicate methods 'all' 'allKeys' 'collectAll' +#2004-06-01 +# '>collection . names' method now accepts optional 'glob' parameter to filter result +#2004-05-19 +#fix '>collection . clear' method so consecutive calls don't raise an error +#------------------------------------------------------------------------------- + +namespace eval ::patternlib::util { + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } + + #bloom filter experiment https://wiki.tcl-lang.org/page/A+Simple+Bloom+Filter + # k-hashes + # m-bits + # n-elements + # optimal value of k: (m/n)ln(2) + #proc bloom_optimalNumHashes {capacity_n bitsize_m} { + # expr { round((double($bitsize_m) / $capacity_n) * log(2))} + #} + #proc bloom_optimalNumBits {capacity fpp} { + # expr {entier(-$capacity * log($fpp) / (log(2) * log(2)))} + #} + +} +::patternlib::util::package_require_min pattern 1.2.4 +#package require pattern +::pattern::init ;# initialises (if not already) + + +namespace eval ::patternlib {namespace export {[a-z]*} + namespace export {[>]*} + + variable keyCounter 0 ;#form part of unique keys for collections when items added without any key specified + proc uniqueKey {} { + return [incr ::patternlib::keyCounter] + } + +#!todo - multidimensional collection +# - o_list as nested list +# - o_array with compound keys(?) how will we unambiguously delimit dimensions in a concatenated key? +# - perhaps a key is always a list length n where n is the number of dimensions? +# - therefore we'll need an extra level of nesting for the current base case n=1 +# +# - how about a nested dict for each key-structure (o_list & o_array) ? + +#COLLECTION +# +#!todo? - consider putting the actual array & list vars in the objects namespace, and using the instancevars to hold their names +# - consider array-style access using traced var named same as collection. +# would this defeat the purpose ? if it was faster, would users always use array syntax in preference.. in which case they may as well just use arrays..? +#!todo - add boolean property to force unique values as well as keys + + +#::pattern::create >collection + + + + +::>pattern .. Create >collection +set COL >collection +#process_pattern_aliases [namespace origin >collection] +#process_pattern_aliases ::patternlib::>collection +$COL .. Property version 1.0 +$COL .. PatternDefaultMethod item + +set PV [$COL .. PatternVariable .] + +$PV o_data +#$PV o_array +#$PV o_list +$PV o_alias +$PV this + +#for invert method +$PV o_dupes 0 + + +$COL .. PatternProperty bgEnum + + +#PV o_ns + +$PV m_i_filteredCollection + +#set ID [lindex [set >collection] 0 0] ;#context ID +#set IID [lindex [set >collection] 1 0] ;#level 1 base-interface ID + +$COL .. Constructor {args} { + var o_data m_i_filteredCollection o_count o_bgEnum + + var this + set this @this@ + + set m_i_filteredCollection 0 + if {![llength $args]} { + set o_data [dict create] + #array set o_array [list] + #set o_list [list] + set o_count 0 + } elseif {[llength $args] == 1} { + set o_data [dict create] + set pairs [lindex $args 0] + if {[llength $pairs] % 2} { + error "patternllib::>collection - if an argument given to constructor, it must have an even number of elements. Bad args: $args" + } + set keys_seen [list] + foreach key [dict keys $pairs] { + if {[string is integer -strict $key] } { + error ">collection key must be non-integer. Bad key: $key. No items added." + } + if {$key in $keys_seen} { + error "key '$key' already exists in this collection. No items added." + } + lappend keys_seen $key + } + unset keys_seen + #rely on dict ordering guarantees (post 8.5? preserves order?) + set o_data [dict merge $o_data[set o_data {}] $pairs] + set o_count [dict size $o_data] + } else { + error "patternlib::>collection constructor did not understand arguments supplied. Try a dict as a single argument." + } + array set o_alias [list] + + array set o_bgEnum [list] + @next@ +} +#comment block snipped from collection Constructor + #--------------------------------------------- + #set o_selfID [lindex [set $o_this] 0] ;#object id always available in methods as $_ID_ anyway + # + #### OBSOLETE - left as example of an approach + #make count property traceable (e.g so property ref can be bound to Tk widgets) + #!todo - manually update o_count in relevant methods faster?? + # should avoid trace calls for addList methods, shuffle etc + # + #set handler ::p::${_ID_}::___count_TraceHandler + #proc $handler {_ID_ vname vidx op} { + # #foreach {vname vidx op} [lrange $args end-2 end] {break} + # #! we shouldn't trust this vname - it may be that we are being accessed via upvar so it is a different name + # + # #this is only a 'write' handler + # set ::p::[lindex ${_ID_} 0 0]::o_count [llength [set ::p::[lindex ${_ID_} 0 0]::o_list]] + # return + #} + #trace add variable o_list {write} [list $handler $_ID_] + #### + # + # + #puts "--->collection constructor id: $_ID_" + + + + +set PM [$COL .. PatternMethod .] + + +#!review - why do we need the count method as well as the property? +#if needed - document why. +# read traces on count property can be bypassed by method call... shouldn't we avoid that? +#2018 - in theory write traces on the . count property are very useful from an application-writer's perpective. +# +$COL .. PatternMethod count {} { + #we don't require any instance vars to be upvar'ed - argless [var] stops them automatically being added. + #we directly refer to the ::O:: var if only accessing a few times rather than upvar'ing. + var o_data + dict size $o_data +} + +$COL .. PatternProperty count +$COL .. PatternPropertyWrite count {_val} { + var + error "count property is read-only" +} + +$COL .. PatternPropertyUnset count {} { + var +} ;#cannot raise error's in unset trace handlers - simply fail to unset silently + +$COL .. PatternMethod isEmpty {} { + #var o_list + #return [expr {[llength $o_list] == 0}] + var o_data + expr {[dict size $o_data] == 0} +} + +$COL .. PatternProperty inverted 0 + + + +###### +# item +###### +#defaults to fifo when no idx supplied (same as 'pair' method). !review? is lifo more logical/intuitive/useful? +# i.e [>obj . item] returns the 1st element in the list +#[>obj . item -1] returns the last element (equiv to "end" keyword used by Tcl list commands) +#[>obj . item -2] returns 2nd last element (equiv to "end-1") + + +$COL .. PatternMethod item {{idx 0}} { + #with pattern::0::$OID access.. was measured faster than item2 : approx 110us vs 140us for 26element collection accessed via string (time {>col $key} 10000) + # (still at least 20 times slower than a plain array... at <5us) + var o_data o_alias + + #!todo - review 'string is digit' vs 'string is integer' ?? + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + set keys [dict keys $o_data] + if {[catch {dict get $o_data [lindex $keys $idx]} result]} { + var this + error "no such index : '$idx' in collection: $this" + } else { + return $result + } + } else { + if {[catch {dict get $o_data $idx} result]} { + if {[catch {set o_alias($idx)} nextIdx ]} { + var this + error "no such index: '$idx' in collection: $this" + } else { + #try again + #return $o_array($nextIdx) + #tailcall? + #item $_ID_ $nextIdx + #puts stdout "\n\n\n !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! about to call tailcall item $_ID_ $nextIdx \n\n\n" + tailcall item $_ID_ $nextIdx + } + } else { + return $result + } + } +} + + + +if {0} { +#leave this here for comparison. +$COL .. PatternMethod item2 {{idx 0}} { + var o_array o_list o_alias this + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + if {[catch {set o_array([lindex $o_list $idx])} result]} { + error "no such index : '$idx' in collection: $this" + } else { + return $result + } + } else { + if {[catch {set o_array($idx)} result]} { + + if {[catch {set o_alias($idx)} nextIdx ]} { + error "no such index: '$idx' in collection: $this" + } else { + #try again + #return $o_array($nextIdx) + item $_ID_ $nextIdx + } + } else { + return $result + } + } + +} +} + +#simple no-frills access for speed.. (timed at 43us vs 63us for item (depending on dispatch method!)) +$COL .. PatternMethod itemNamed {idx} { + var o_data + dict get $o_data $idx +} +$COL .. PatternMethod in {idx} { + var o_data + dict get $o_data $idx +} + +$COL .. PatternMethod itemAt {idx} { + var o_data + dict get $o_data [lindex [dict keys $o_data] $idx] +} + +$COL .. PatternMethod replace {idx val} { + var o_data o_alias this + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + if {[catch {dict set o_data [lindex [dict keys $o_data] $idx] $val}]} { + error "no such index: '$idx' in collection: $this" + } else { + return $val + } + } else { + if {[catch {dict set o_data $idx $val}]} { + if {[catch {set o_alias($idx)} nextIdx ]} { + error "no such index: '$idx' in collection: $this" + } else { + #try again + tailcall replace $_ID_ $nextIdx $val + } + + } else { + return $val + } + } +} + +#if the supplied index is an alias, return the underlying key; else return the index supplied. +$COL .. PatternMethod realKey {idx} { + var o_alias + + if {[catch {set o_alias($idx)} key]} { + return $idx + } else { + return $key + } +} + +#note alias feature is possibly ill-considered. +#if we delete an item - should we delete corresponding alias? If not - we then would need to allow adding under an alias only if the corresponding key is missing. +$COL .. PatternMethod alias {newAlias existingKeyOrAlias} { + var o_alias + + #set existingKey [realKey $_ID_ $existingKeyOrAlias] + #alias to the supplied KeyOrAlias - not the underlying key + + if {[string is integer -strict $newAlias]} { + error "collection key alias cannot be integer" + } + + if {[string length $existingKeyOrAlias]} { + set o_alias($newAlias) $existingKeyOrAlias + } else { + unset o_alias($newAlias) + } +} +$COL .. PatternMethod aliases {{key ""}} { + var o_alias + + if {[string length $key]} { + set result [list] + #lsearch -stride? + foreach {n v} [array get o_alias] { + if {$v eq $key} { + lappend result $n $v + } + } + + return $result + } else { + return [array get o_alias] + } +} + +#'pop' & 'unshift' methods !todo - optimize so lsearch not called when numerical idx/posn already supplied + +#default to removing item from the end, otherwise from supplied index (position or key) +#!todo - accept alias indices +#!todo - review.. should any corresponding alias be destroyed when the corresponding item is popped (or removed in any way?) +#!todo - review.. for performance.. shouldn't pop NOT accept an index? +#if we need to pop from other than the end.. this could be a separate function. Do other langs use pop with an index?? +$COL .. PatternMethod pop {{idx ""}} { + var o_data o_count + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] end] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + set posn [lsearch -exact [dict keys $o_data] $key] + + if {($posn >= 0) && ($posn < [dict size $o_data])} { + set result [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } +} +$COL .. PatternMethod poppair {} { + var o_data o_count + set key [lindex [dict keys $o_data] end] + set val [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return [list $key $val] +} + + + +#!todo - add 'push' method... (basically specialized versions of 'add') +#push - add at end (effectively an alias for add) +#shift - add at start ???bad name? this is completely at odds with for example the common Perl shift function, which returns and removes the first element of an array. +#add - add at end + +#ordered +$COL .. PatternMethod items {} { + var o_data + + dict values $o_data +} + + + + +#### +#pair +#### +#fifo-style accesss when no idx supplied (likewise with 'add' method) +$COL .. PatternMethod pair {{idx 0}} { + var o_data + + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + + if {[catch {dict get $o_data $key} val]} { + error "no such index: '$idx'" + } else { + return [list $key $val] + } +} +$COL .. PatternMethod pairs {} { + var o_data + set o_data +} + +$COL .. PatternMethod get {} { + var o_data + set o_data +} +#todo - fix >pattern so that methods don't collide with builtins +#may require change to use oo - or copy 'my' mechanism to call own methods +$COL .. PatternMethod Info {} { + var o_data + return [dict info $o_data] +} +#2006-05-21.. args to add really should be in key, value order? +# - this the natural order in array-like lists +# - however.. key should be optional. + +$COL .. PatternMethod add {val args} { + #(using args instead of {key ""} enables use of empty string as a key ) + + var o_data o_alias o_count this + + if {![llength $args]} { + set key "_[::patternlib::uniqueKey]_" + } else { + #!todo - could we handle multiple val,key pairs without impacting performance of the common case? + if {[llength $args] > 1} { + error "add method expected 'val' and optional 'key' - got: $val $args" + + } + + set key [lindex $args 0] + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + + if {[dict exists $o_data $key]} { + #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]" + error "key '$key' already exists in collection $this" + } + if {[info exists o_alias($key)]} { + if {[dict exists $o_data $o_alias($key)]} { + #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias + error "key '$key' already exists as an alias for $o_alias($key) in collection $this" + } + } + + dict set o_data $key $val + + + set posn $o_count + incr o_count + + return $posn +} + + +#should the 'stack' methods such as shift,push,pop,peek actually be on a separate interface? +#what then of methods like 'count' which apply equally well to collections and stacks? + +#Alias for 'add' - is there a way to alias this to add implementation with zero overhead?? +$COL .. PatternMethod push {val args} { + #(using args instead of {key ""} enables use of empty string as a key ) + + var o_data o_alias o_count this + + if {![llength $args]} { + set key "_[::patternlib::uniqueKey]_" + } else { + #!todo - could we handle multiple val,key pairs without impacting performance of the common case? + if {[llength $args] > 1} { + error "add method expected 'val' and optional 'key' - got: $val $args" + + } + + set key [lindex $args 0] + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + + if {[dict exists $o_data $key]} { + #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]" + error "key '$key' already exists in collection $this" + } + if {[info exists o_alias($key)]} { + if {[dict exists $o_data $o_alias($key)]} { + #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias + error "key '$key' already exists as an alias for $o_alias($key) in collection $this" + } + } + + dict set o_data $key $val + + + set posn $o_count + incr o_count + + return $posn +} + + +#shift/unshift - roughly analogous to those found in Perl & PHP +#unshift adds 1 or more values to the beginning of the collection. +$COL .. PatternMethod unshift {values {keys ""}} { + var o_data o_count + + if {![llength $keys]} { + for {set i 0} {$i < [llength $values]} {incr i} { + lappend keys "_[::patternlib::uniqueKey]_" + } + } else { + #check keys before we insert any of them. + foreach newkey $keys { + if {[string is integer -strict $newkey]} { + error "cannot accept key '$newkey', >collection keys must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + } + if {[llength $values] != [llength $keys]} { + error "unshift requires same number of keys as values. (or no keys for auto-generated keys) Received [llength $values] values, [llength $keys] keys" + } + + #separate loop through keys because we want to fail the whole operation if any are invalid. + + set existing_keys [dict keys $o_data] + foreach newkey $keys { + if {$newkey in $exisint_keys} { + #puts stderr "==============> key $key already exists in this collection" + error "key '$newkey' already exists in this collection" + } + } + + + #ok - looks like entire set can be inserted. + set newpairs [list] + foreach val $values key $keys { + lappend newpairs $key $val + } + set o_data [concat $newpairs $o_data[set o_data {}]] + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#default to removing item from the beginning, otherwise from supplied index (position or key) +#!todo - accept alias indices +$COL .. PatternMethod shift {{idx ""}} { + var o_data o_count + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] 0] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + set posn [lsearch -exact [dict keys $o_data] $key] + + if {($posn >= 0) && (($posn/2) < [dict size $o_data])} { + set result [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } +} + + +$COL .. PatternMethod peek {} { + var o_data + + #set o_array([lindex $o_list end]) + + #dict get $o_data [lindex [dict keys $o_data] end] + lindex $o_data end +} + +$COL .. PatternMethod peekKey {} { + var o_data + #lindex $o_list end + lindex $o_data end-1 +} + + +$COL .. PatternMethod insert {val args} { + var o_data o_count + + set idx 0 + set key "" + + if {[llength $args] <= 2} { + #standard arg (ordered) style: + #>obj . insert $value $position $key + + lassign $args idx key + } else { + #allow for literate programming style: + #e.g + # >obj . insert $value at $listPosition as $key + + if {[catch {array set iargs $args}]} { + error "insert did not understand argument list. +usage: +>obj . insert \$val \$position \$key +>obj . insert \$val at \$position as \$key" + } + if {[info exists iargs(at)]} { + set idx $iargs(at) + } + if {[info exists iargs(as)]} { + set key $iargs(as) + } + } + + if {![string length $key]} { + set key "_[::patternlib::uniqueKey]_" + } + + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + + + if {[dict exists $o_data $key]} { + #puts stderr "==============> key $key already exists in this collection" + error "key '$key' already exists in this collection" + } + + if {$idx eq "end"} { + #lappend o_list $key + #standard dict set will add it to the end anyway + dict set o_data $key $val + + } else { + #set o_list [linsert $o_list $idx $key] + + #treat dict as list + set o_data [linsert $o_data[set o_data {}] [expr {$idx*2}] $key $val] + } + + + #set o_array($key) $val + + + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#!todo - deprecate and give it a better name! addDict addPairs ? +$COL .. PatternMethod addArray {list} { + var + puts stderr "patternlib::>collection WARNING: addArray deprecated - call addPairs with same argument instead" + tailcall addPairs $_ID_ $list +} +$COL .. PatternMethod addPairs {list} { + var o_data o_alias o_count + if {[llength $list] % 2} { + error "must supply an even number of elements" + } + + set aliaslist [array names o_alias] + #set keylist [dict keys $o_data] + foreach newkey [dict keys $list] { + if {[string is integer -strict $newkey] } { + error ">collection key must be non-integer. Bad key: $newkey. No items added." + } + + #if {$newkey in $keylist} {} + #for small to medium collections - testing for newkey in $keylist is probably faster, + # but we optimise here for potentially large existing collections, where presumably a dict exists lookup will be more efficient. + if {[dict exists $o_data $newkey]} { + error "key '$newkey' already exists in this collection. No items added." + } + #The assumption is that there are in general relatively few aliases - so a list test is appropriate + if {$newkey in $aliaslist} { + if {[dict exists $o_data $o_alias($newkey)]} { + error "key '$newkey' already exists as an alias for $o_alias($newkey) in collection. No items added " + } + } + #! check if $list contains dups? + #- slows method down - for little benefit? + } + #!todo - test? (but we need a loop to test for integer keys.. so what's the point?) + #set intersection [struct::set intersect [dict keys $list] [dict keys $o_data]] + #if {[llength $intersection]} { + # error "keys '$intersection' already present in this collection. No items added." + #} + + + #rely on dict ordering guarantees (post 8.5? preserves order?) + set o_data [dict merge $o_data[set o_data {}] $list] + + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} +$COL .. PatternMethod addList {list} { + var o_data o_count + + foreach val $list { + dict set o_data "_[::patternlib::uniqueKey]_" $val + #!todo - test. Presumably lappend faster because we don't need to check existing keys.. + #..but.. is there shimmering involved in treating o_data as a list? + #lappend o_data _[::patternlib::uniqueKey]_ $val + + #tested 2008-06 tcl8.6a0 lappend was slower as the gain is lost (and more!) during subsequent [dict size $o_data] + } + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#'del' is not a very good name... as we're not really 'deleting' anything. +# 'remove' seems better, and appears to be more consistent with other languages' collection implementations. +#!todo - handle 'endRange' parameter for removing ranges of items. +$COL .. PatternMethod del {idx {endRange ""}} { + var + #!todo - emit a deprecation warning for 'del' + tailcall remove $_ID_ $idx $endRange +} + +$COL .. PatternMethod remove {idx {endRange ""}} { + var o_data o_count o_alias this + + if {[string length $endRange]} { + 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} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx' in collection: $this" + } else { + #try with next key in alias chain... + #return [remove $_ID_ $nextKey] + tailcall remove $_ID_ $nextKey + } + } + } + + dict unset o_data $key + + set o_count [dict size $o_data] + return +} + +#ordered +$COL .. PatternMethod names {{globOrIdx {}}} { + var o_data + + if {[llength $globOrIdx]} { + if {[string is integer -strict $globOrIdx]} { + #Idx + set idx $globOrIdx + + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + + + if {[catch {lindex [dict keys $o_data] $idx} result]} { + error "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] + } +} + +#ordered +$COL .. PatternMethod keys {} { + #like 'names' but without globbing + var o_data + dict keys $o_data +} + +#Unfortunately the string 'name' is highly collidable when mixing in a collection over existing objects +# - !todo - review. Is it worth adjusting the collection methodnames to avoid a few common collision cases? +# - some sort of resolution order/interface-selection is clearly required anyway +# so perhaps it's generally best not to bother being 'polite' here, and implement a robust understandable resolution mechanism. +# In the mean time however... we'll at least avoid 'name'! +# +#$PM name {{posn 0}} { +# var o_array o_list +# +# if {$posn < 0} { +# set posn "end-[expr {abs($posn + 1)}]" +# } +# +# if {[catch {lindex $o_list $posn} result]} { +# error "no such index : '$posn'" +# } else { +# return $result +# } +#} + +$COL .. PatternMethod key {{posn 0}} { + var o_data + + if {$posn < 0} { + set posn "end-[expr {abs($posn + 1)}]" + } + + if {[catch {lindex [dict keys $o_data] $posn} result]} { + error "no such index : '$posn'" + } else { + return $result + } +} + + +#!todo - consider use of 'end-x' syntax for 'to', and implications re consistency with other commands. +$COL .. PatternMethod setPosn {idx to} { + var o_data + + if {![string is integer -strict $to]} { + error "destination position must be numeric, consider reKey method if you are trying to change the string key under which this value is stored" + } + + if {[string is integer -strict $idx]} { + set idx [expr {$idx % [dict size $o_data]}] + + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + } + + set to [expr {$to % [dict size $o_data]}] + + + set val [dict get $o_data $key] + dict unset o_data $key + + #treat dict as list + set o_data [linsert $o_data[set o_data {}] [expr {$posn*2}] $key $val] + + #set o_list [lreplace $o_list $posn $posn] + #set o_list [linsert $o_list $to $key] + + return $to +} +#!todo - improve efficiency of calls to other functions on this object.. 'inline'?? +#presumably the collection object functionality will be long-term stable because it's purpose is to be a core datastructure; therefore it should be reasonable to favour efficiency over maintainability. +$COL .. PatternMethod incrPosn {idx {by 1}} { + var o_data + if {[string is integer -strict $idx]} { + set idx [expr {$idx % [dict size $o_data]}] + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + } + + set newPosn [expr {($posn + $by) % [dict size $o_data]}] + + setPosn $_ID_ $posn $newPosn + return $newPosn +} +$COL .. PatternMethod decrPosn {idx {by 1}} { + var + return [incrPosn $_ID_ $idx [expr {- $by}]] +} +$COL .. PatternMethod move {idx to} { + var + return [setPosn $_ID_ $idx $to] +} +$COL .. PatternMethod posn {key} { + var o_data + return [lsearch -exact [dict keys $o_data] $key] +} + +#!todo? - disallow numeric values for newKey so as to be consistent with add +#!note! - item can be reKeyed out from under an alias such that the alias chain no longer points to anything +# - this is ok. +$COL .. PatternMethod reKey {idx newKey} { + var o_data o_alias + + + if {[dict exists $o_data $newKey]} { + #puts stderr "==============> reKey collision, key $newKey already exists in this collection" + error "reKey collision, key '$newKey' already exists in this collection" + } + if {[info exists o_alias($newKey)]} { + if {[dict exists $o_data $o_alias($newKey)]} { + error "reKey collision, key '$newKey' already present as an alias in this collection" + } else { + set newKey $o_alias($newKey) + } + } + + + + 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} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx'" + } else { + #try with next key in alias chain... + #return [reKey $_ID_ $nextKey $newKey] + tailcall reKey $_ID_ $nextKey $newKey + } + } + } + + #set o_list [lreplace $o_list $posn $posn $newKey] + ##atomic? (traces on array?) + #set o_array($newKey) $o_array($key) + #unset o_array($key) + + dict set o_data $newKey [dict get $o_data $key] + dict unset o_data $key + + return +} +$COL .. PatternMethod hasKey {key} { + var o_data + dict exists $o_data $key +} +$COL .. PatternMethod hasAlias {key} { + var o_alias + info exists o_alias($key) +} + +#either key or alias +$COL .. PatternMethod hasIndex {key} { + var o_data o_alias + if {[dict exists $o_data $key]} { + return 1 + } else { + return [info exists o_alias($key)] + } +} + + +#Shuffle methods from http://mini.net/tcl/941 +$COL .. PatternMethod shuffleFast {} { + #shuffle6 - fast, but some orders more likely than others. + + var o_data + + set keys [dict keys $o_data] + + set n [llength $keys] + for { set i 1 } { $i < $n } { incr i } { + set j [expr { int( rand() * $n ) }] + set temp [lindex $keys $i] + lset keys $i [lindex $keys $j] + lset keys $j $temp + } + + #rebuild dict in new order + #!todo - can we do the above 'in place'? + set newdata [dict create] + foreach k $keys { + dict set newdata $k [dict get $o_data $k] + } + set o_data $newdata + + return +} +$COL .. PatternMethod shuffle {} { + #shuffle5a + + var o_data + + set n 1 + set keys [list] ;#sorted list of keys + foreach k [dict keys $o_data] { + #set index [expr {int(rand()*$n)}] + + #set slist [linsert [::pattern::K $keys [set keys {}]] $index $k] + + #faster alternative.. 'inline K' [lindex [list a b] 0] ~ [K a b] + set keys [linsert [lindex [list $keys [set keys {}]] 0] [expr {int(rand()*$n)}] $k] + incr n + } + + #rebuild dict in new order + #!todo - can we do the above 'in place'? + set newdata [dict create] + foreach k $keys { + dict set newdata $k [dict get $o_data $k] + } + set o_data $newdata + + return +} + + +#search is a somewhat specialised form of 'itemKeys' +$COL .. PatternMethod search {value args} { + var o_data + #only search on values as it's possible for keys to match - especially with options such as -glob + 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 + } +} + +#inverse lookup +$COL .. PatternMethod itemKeys {value} { + var o_data + #only search on values as it's possible for keys to match + 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 +} + +#invert: +#change collection to be indexed by its values with the old keys as new values. +# - keys of duplicate values become a list keyed on the value. +#e.g the array equivalent is: +# arr(a) 1 +# arr(b) 2 +# arr(c) 2 +#becomes +# inv(1) a +# inv(2) {b c} +#where the order of duplicate-value keys is not defined. +# +#As the total number of keys may change on inversion - order is not preserved if there are ANY duplicates. +# + + +#!todo - try just [lreverse $o_data] ?? + + +$COL .. PatternMethod invert {{splitvalues ""}} { + + var o_data o_count o_dupes o_inverted + + + if {$splitvalues eq ""} { + #not overridden - use o_dupes from last call to determine if values are actually keylists. + if {$o_dupes > 0} { + set splitvalues 1 + } else { + set splitvalues 0 + } + } + + + #set data [array get o_array] + set data $o_data + + if {$o_count > 500} { + #an arbitrary optimisation for 'larger' collections. + #- should theoretically keep the data size and save some reallocations. + #!todo - test & review + # + foreach nm [dict keys $o_data] { + dict unset o_data $nm + } + } else { + set o_data [dict create] + } + + if {!$splitvalues} { + dict for {k v} $data { + dict set o_data $v $k + } + } else { + dict for {k v} $data { + #we're splitting values because each value is a list of keys + #therefore sub should be unique - no need for lappend in this branch. + foreach sub $v { + #if {[info exists o_array($sub)]} { + # puts stderr "---here! v:$v sub:$sub k:$k" + # lappend o_array($sub) $k + #} else { + dict set o_data $sub $k + #} + } + } + } + + + if {[dict size $o_data] != $o_count} { + #must have been some dupes + + set o_dupes [expr {$o_count - [dict size $o_data]}] + #update count to match inverted collection + set o_count [dict size $o_data] + } else { + set o_dupes 0 + } + + set o_inverted [expr {!$o_inverted}] + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $o_dupes +} + + + + + + +#NOTE: values are treated as lists and split into separate keys for inversion only if requested! +# To treat values as keylists - set splitvalues 1 +# To treat each value atomically - set splitvalues 0 +# i.e only set splitvalues 1 if you know the values represent duplicate keys from a previous call to invert! +# +# +#Initially call invert with splitvalues = 0 +#To keep calling invert and get back where you started.. +# The rule is... if the previous call to invert returned > 0... pass 1 on the next call. +# +$COL .. PatternMethod invert_manual {{splitvalues 0}} { + #NOTE - the list nesting here is *tricky* - It probably isn't broken. + + var o_list o_array o_count + + set data [array get o_array] + + if {$o_count > 500} { + #an arbitrary optimisation for 'large' collections. + #- should theoretically keep the array size and save some reallocations. + #!todo - test & review + # + foreach nm [array names o_array] { + unset o_array($nm) + } + } else { + array unset o_array + } + + if {!$splitvalues} { + foreach {k v} $data { + lappend o_array($v) $k + } + } else { + foreach {k v} $data { + #we're splitting values because each value is a list of keys + #therefore sub should be unique - no need for lappend in this branch. + foreach sub $v { + #if {[info exists o_array($sub)]} { + # puts stderr "---here! v:$v sub:$sub k:$k" + # lappend o_array($sub) $k + #} else { + set o_array($sub) $k + #} + } + } + } + + + if {[array size o_array] != $o_count} { + #must have been some dupes + set o_list [array names o_array] + + + set dupes [expr {$o_count - [array size o_array]}] + #update count to match inverted collection + set o_count [array size o_array] + } else { + #review - are these machinations worthwhile for order preservation? what speed penalty do we pay? + array set prev $data + set i -1 + if {$splitvalues} { + #values are lists of length one. Take lindex 0 so list values aren't overnested. + foreach oldkey $o_list { + lset o_list [incr i] [lindex $prev($oldkey) 0] + } + } else { + foreach oldkey $o_list { + lset o_list [incr i] $prev($oldkey) + } + } + + set dupes 0 + } + + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $dupes +} + + + +#Note that collections cannot be inverted without loss of information if they have duplicates AND compound keys +# (keys that are lists) +$COL .. PatternMethod invert_lossy {{splitvalues 1}} { + var o_list o_array o_count + + set data [array get o_array] + + if {$o_count > 500} { + #an arbitrary optimisation for 'large' collections. + #- should theoretically keep the array size and save some reallocations. + #!todo - test & review + # + foreach nm [array names o_array] { + unset o_array($nm) + } + } else { + array unset o_array + } + + if {!$splitvalues} { + foreach {k v} $data { + #note! we must check for existence and use 'set' for first case. + #using 'lappend' only will result in deeper nestings on each invert! + #If you don't understand this - don't change it! + if {[info exists o_array($v)]} { + lappend o_array($v) $k + } else { + set o_array($v) $k + } + } + } else { + foreach {k v} $data { + #length test necessary to avoid incorrect 'un-nesting' + #if {[llength $v] > 1} { + foreach sub $v { + if {[info exists o_array($sub)]} { + lappend o_array($sub) $k + } else { + set o_array($sub) $k + } + } + #} else { + # if {[info exists o_array($v)]} { + # lappend o_array($v) $k + # } else { + # set o_array($v) $k + # } + #} + } + } + + + if {[array size o_array] != $o_count} { + #must have been some dupes + set o_list [array names o_array] + + + set dupes [expr {$o_count - [array size o_array]}] + #update count to match inverted collection + set o_count [array size o_array] + } else { + #review - are these machinations worthwhile for order preservation? what speed penalty do we pay? + array set prev $data + set i -1 + foreach oldkey $o_list { + lset o_list [incr i] $prev($oldkey) + } + set dupes 0 + } + + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $dupes +} + +$COL .. PatternMethod reverse {} { + var o_data + + 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 +} + +$COL .. PatternMethod keySort {{options -ascii}} { + var o_data + + set keys [lsort {*}$options [dict keys $o_data]] + + set dictnew [dict create] + foreach k $keys { + dict set dictnew $k [dict get $o_data $k] + } + set o_data $dictnew + + return +} + +#!todo - allow simple options in combination with options such as -command and -object. Redo args handling completely for more complex sorting. +$COL .. PatternMethod sort {args} { + var o_data + + #defaults + set options [dict create -index 1] ;#values always in subelement 1 of name-value pair list for sorting. + + set options_simple [list] + + + for {set i 0} {$i < [llength $args]} {incr i} { + set a [lindex $args $i] + switch -- $a { + -indices - + -ascii - + -dictionary - + -integer - + -real - + -increasing - + -decreasing { + #dict set options $a 1 + lappend options_simple $a + } + -unique { + #not a valid option + #this would stuff up the data... + #!todo? - remove dups from collection if this option used? - alias the keys? + } + -object { + #!todo - treat value as object and allow sorting by sub-values .eg >col1 . sort -object ". sub . property" -increasing + #may be slow - but handy. Consider -indexed property to store/cache these values on first run + } + -command { + dict set options $a [lindex $args [incr i]] + } + -index { + #allow sorting on subindices of the value. + dict set options -index [concat [dict get $options -index] [lindex $args [incr i]] ] + } + default { + #unrecognised option - print usage? + } + } + } + + + + if {[set posn [lsearch -exact $options_simple "-indices"]] >= 0} { + + var o_array + + set slist [list] + foreach k [dict keys $o_data] { + lappend slist [list $k [dict get $o_data $k]] + } + return [lsort {*}$options_simple {*}$options $slist] + + + + #set options_simple [lreplace $options_simple $posn $posn] ;# + #set slist [list] + #foreach {n v} [array get ::p::[lindex ${_ID_} 0 0]::o_array] { + # lappend slist [list $n $v] + #} + #set slist [lsort {*}$options_simple {*}$options $slist] + #foreach i $slist { + # #determine the position in the collections list + # lappend result {*}[lsearch -exact $o_list [lindex $i 0]] + #} + #return $result + } else { + set slist [list] + dict for {k v} $o_data { + lappend slist [list $k $v] + } + #set slist [lsort {*}$options_simple {*}$options $slist] + set slist [lsort {*}$options_simple {*}$options $slist[set slist {}]] ;#K combinator for efficiency + + + #set o_list [lsearch -all -inline -subindices -index 0 $slist *] + + set o_data [dict create] + foreach pair $slist { + dict set o_data [lindex $pair 0] [lindex $pair 1] + } + + + + return + } + +} + + +$COL .. PatternMethod clear {} { + var o_data o_count + + set o_data [dict create] + set o_count 0 + #aliases? + return +} + +#see http://wiki.tcl.tk/15271 - A generic collection traversal interface +# +#!todo - options: -progresscommand -errorcommand (-granularity ?) (-self ? (to convert to an iterator?)) +#!todo? - lazy retrieval of items so that all changes to the collection are available to a running asynch enumeration? +# - should this be an option? which mechanism should be the default? +# - currently only the keylist is treated in 'snapshot' fashion +# so values could be changed and the state could be invalidated by other code during an enumeration +# +$COL .. PatternMethod enumerate {args} { + #---------- + lassign [lrange $args end-1 end] cmd seed + set optionlist [list] + foreach a [lrange $args 0 end-2] { + lappend optionlist $a + } + set opt(-direction) left + set opt(-completioncommand) "" + array set opt $optionlist + #---------- + var o_data + + if {[string tolower [string index $opt(-direction) 0]] eq "r"} { + #'right' 'RIGHT' 'r' etc. + set list [lreverse [dict keys $o_data]] + } else { + #normal left-right order + set list [dict keys $o_data] + } + + if {![string length $opt(-completioncommand)]} { + #standard synchronous processing + foreach k $list { + set seed [uplevel #0 [list {*}$cmd $seed [dict get $o_data $k]]] + } + return $seed + } else { + #ASYNCHRONOUS enumeration + var this o_bgEnum + #!todo - make id unique + #!todo - facility to abort running enumeration. + set enumID enum[array size o_bgEnum] + + set seedvar [$this . bgEnum $enumID .] + set $seedvar $seed + + after 0 [list $this . _doBackgroundEnum $enumID $list $cmd $seedvar $opt(-completioncommand)] + return $enumID + } +} + +#!todo - make private? - put on a separate interface? +$COL .. PatternMethod _doBackgroundEnum {enumID slice cmd seedvar completioncommand} { + var this o_data + + + #Note that we don't post to the eventqueue using 'foreach s $slice' + # we only schedule another event after each item is processed + # - otherwise we would be spamming the eventqueue with items. + + #!todo? - accept a -granularity option to allow handling of n list-items per event? + + if {[llength $slice]} { + set slice [lassign $slice head] + + set script [string map [list %cmd% $cmd %seedvar% $seedvar %val% [dict get $o_data $head]] { + %cmd% [set %seedvar%] %val% + }] + + #post to eventqueue and re-enter _doBackgroundEnum + # + after idle [list after 0 [subst {set $seedvar \[uplevel #0 [list $script] \]; $this . _doBackgroundEnum $enumID [list $slice] [list $cmd] $seedvar [list $completioncommand]}]] + + } else { + #done. + + set script [string map [list %cmd% $completioncommand %seedvar% $seedvar] { + lindex [list [%cmd% [set %seedvar%]] [unset %seedvar%]] 0 + }] + + after idle [list after 0 [list uplevel #0 $script]] + } + + return +} + +$COL .. PatternMethod enumeratorstate {} { + var o_bgEnum + parray o_bgEnum +} + +#proc ::bgerror {args} { +# puts stderr "=bgerror===>$args" +#} + + +#map could be done in terms of the generic 'enumerate' method.. but it's slower. +# +#$PM map2 {proc} { +# var +# enumerate $_ID_ [list ::map-helper $proc] [list] +#} +#proc ::map-helper {proc accum item} { +# lappend accum [uplevel #0 [list {*}$proc $item]] +#} + +$COL .. PatternMethod map {cmd} { + var o_data + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list {*}$cmd $v]] + } + + return $seed +} +$COL .. PatternMethod objectmap {cmd} { + var o_data + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list $v {*}$cmd]] + } + + return $seed +} + + +#End core collection functionality. +#collection 'mixin' interfaces + +>pattern .. Create >keyvalprotector +>keyvalprotector .. PatternVariable o_protectedkeys +>keyvalprotector .. PatternVariable o_protectedvals + +#!todo - write test regarding errors in Constructors for mixins like this +# - an error (e.g from bad args) can cause errors with vars after it's re-run with correct args +>keyvalprotector .. Constructor {args} { + var this o_protectedkeys o_protectedvals + set this @this@ + #---------------------------------------------------------------------------- + set known_opts [list -keys -vals ] + dict set default -keys [list] + dict set default -vals [list] + if {([llength $args] % 2) != 0} { + error "(>keyvalprotector .. Constructor) ERROR: uneven options supplied - must be of form '-option value' " + } + foreach {k v} $args { + if {$k ni $known_opts} { + error "(>keyvalprotector .. Constructor) ERROR: option '$k' not in known options: '$known_opts'" + } + } + set opts [dict merge $default $args] + set o_protectedkeys [dict get $opts -keys] + set o_protectedvals [dict get $opts -vals] + #---------------------------------------------------------------------------- + set protections [concat $o_protectedkeys $o_protectedvals] + if {![llength $protections]} { + error "(>keyvalprotector .. Constructor) ERROR: must supply at least one argument to -vals or -keys" + } + +} +>keyvalprotector .. PatternMethod clear {} { + error "(>keyvalprotector . clear) ERROR: This collection is protected by a >keyvalprotector mixin. Cannot clear" +} +>keyvalprotector .. PatternMethod pop {{idx ""}} { + var o_data o_count o_protectedkeys o_protectedvals + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] end] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . pop) ERROR: Cannot pop object with index '$idx', key '$key' from collection." + } + set posn [lsearch -exact [dict keys $o_data] $key] + if {($posn >= 0) && ($posn < [dict size $o_data])} { + set result [dict get $o_data $key] + if {$result in $o_protectedvals} { + error "(>keyvalprotector . pop) ERROR: Cannot pop object '$result' with index '$idx', key '$key' from collection." + } + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } + +} +>keyvalprotector .. PatternMethod remove {idx {endRange ""}} { + var this o_data o_count o_alias o_protectedkeys o_protectedvals + + if {[string length $endRange]} { + 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] + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' key '$key' from collection" + } + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + if {$posn < 0} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx' in collection: $this" + } else { + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' from collection" + } + #try with next key in alias chain... + #return [remove $_ID_ $nextKey] + tailcall remove $_ID_ $nextKey + } + } + } + + dict unset o_data $key + + set o_count [dict size $o_data] + return +} + +#1) +#predicate methods (order preserving) +#usage: +# >collection .. Create >c1 +# >predicatedCollection .. Create >c1 ;#overlay predicate methods on existing collection + +#e.g >col1 . all {$val > 14} +#e.g >col1 . filterToCollection {$val > 19} . count +#e.g >col1 . filter {[string match "x*" $key]} +#!todo - fix. currying fails.. + +::>pattern .. Create >predicatedCollection +#process_pattern_aliases ::patternlib::>predicatedCollection + +set PM [>predicatedCollection .. PatternMethod .] + +>predicatedCollection .. PatternMethod filter {predicate} { + var this o_list o_array + set result [list] + + #!note (jmn 2004) how could we do smart filtering based on $posn? + #i.e it would make sense to lrange $o_list based on $posn... + #but what about complicated expressions where $posn is a set of ranges and/or combined with tests on $key & $val ?? + #Seems better to provide an alternative efficient means of generating subcolllections/ranges to perform predicate operations upon. + #given this, is $posn even useful? + + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $val + } + incr posn + } + set result +} +>predicatedCollection .. PatternMethod filterToKeys {predicate} { + var this o_list o_array + set result [list] + + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $key + } + incr posn + } + set result +} +>predicatedCollection .. PatternMethod filterToCollection {predicate {destCollection {}}} { + #!todo - collection not in subordinate namespace? -> if subordinate, should imply modification of sub's contents will be reflected in parent? + #!todo - implement as 'view' on current collection object.. extra o_list variables? + #!todo - review/document 'expected' key collision behaviour - source keys used as dest keys.. -autokey option required? + var this o_list o_array m_i_filteredCollection + + incr m_i_filteredCollection + if {![string length $destCollection]} { + #!todo? - implement 'one-shot' object (similar to RaTcl) + set result [::patternlib::>collection .. Create [$this .. Namespace]::>filteredCollection-$m_i_filteredCollection] + } else { + set result $destCollection + } + + #### + #externally manipulate new collection + #set ADD [$c . add .] + #foreach key $o_list { + # set val $o_array($key) + # if $predicate { + # $ADD $val $key + # } + #} + ### + + #internal manipulation faster + #set cID [lindex [set $result] 0] + set cID [lindex [$result --] 0] + + #use list to get keys so as to preserve order + set posn 0 + upvar #0 ::p::${cID}::o_array cARRAY ::p::${cID}::o_list cLIST + foreach key $o_list { + set val $o_array($key) + if $predicate { + if {[info exists cARRAY($key)]} { + error "key '$key' already exists in this collection" + } + lappend cLIST $key + set cARRAY($key) $val + } + incr posn + } + + return $result +} + +#NOTE! unbraced expr/if statements. We want to evaluate the predicate. +>predicatedCollection .. PatternMethod any {predicate} { + var this o_list o_array + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + return 1 + } + incr posn + } + return 0 +} +>predicatedCollection .. PatternMethod all {predicate} { + var this o_list o_array + set posn 0 + foreach key $o_list { + set val $o_array($key) + if !($predicate) { + return 0 + } + incr posn + } + return 1 +} +>predicatedCollection .. PatternMethod dropWhile {predicate} { + var this o_list o_array + set result [list] + set _idx 0 + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + incr _idx + } else { + break + } + incr posn + } + set remaining [lrange $o_list $_idx end] + foreach key $remaining { + set val $o_array($key) + lappend result $val + } + return $result +} +>predicatedCollection .. PatternMethod takeWhile {predicate} { + var this o_list o_array + set result [list] + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $val + } else { + break + } + incr posn + } + set result +} + + + +#end >collection mixins +###################################### + + + + +#----------------------------------------------------------- +#!TODO - methods for converting an arrayHandle to & from a hashMap efficiently? +# Why do we need both? apart from the size variable, what is the use of hashMap? +#----------------------------------------------------------- +#::pattern::create >hashMap +::>pattern .. Create >hashMap + +>hashMap .. PatternVariable o_size +>hashMap .. PatternVariable o_array + +>hashMap .. Constructor {args} { + var o_array o_size + array set o_array [list] + set o_size 0 +} +>hashMap .. PatternDefaultMethod "item" +>hashMap .. PatternMethod item {key} { + var o_array + set o_array($key) +} +>hashMap .. PatternMethod items {} { + var o_array + + set result [list] + foreach nm [array names o_array] { + lappend result $o_array($nm) + } + return $result +} +>hashMap .. PatternMethod pairs {} { + var o_array + + array get o_array +} +>hashMap .. PatternMethod add {val key} { + var o_array o_size + + set o_array($key) $val + incr o_size + return $key +} + +>hashMap .. PatternMethod del {key} { + var + puts stderr "warning: 'del' method of >hashMap deprecated. Use 'remove' instead." + remove $_ID_ $key +} +>hashMap .. PatternMethod remove {key} { + var o_array o_size + unset o_array($key) + incr o_size -1 + return $key +} +>hashMap .. PatternMethod count {} { + var o_size + #array size o_array + return $o_size +} +>hashMap .. PatternMethod count2 {} { + var o_array + #array size o_array ;#slow, at least for TCLv8.4.4 + #even array statistics is faster than array size ! + #e.g return [lindex [array statistics o_array] 0] + #but.. apparently there are circumstances where array statistics doesn't report the correct size. + return [array size o_array] +} +>hashMap .. PatternMethod names {} { + var o_array + array names o_array +} +>hashMap .. PatternMethod keys {} { + #synonym for names + var o_array + array names o_array +} +>hashMap .. PatternMethod hasKey {key} { + var o_array + return [info exists o_array($key)] +} +>hashMap .. PatternMethod clear {} { + var o_array o_size + unset o_array + set o_size 0 + return +} +#>hashMap .. Ready 1 + + + + + + + + + + + + + + + +#explicitly create metadata. Not required for user-defined patterns. +# this is only done here because this object is used for the metadata of all objects +# so the object must have all it's methods/props before its own metadata structure can be built. +#uplevel 1 "::pattern::object ::pattern::>_nullMeta createMetadata >collection" +#uplevel 1 "::patternlib::>collection .. CreateMetadata ::patternlib::>collection" + + + + +if 0 { + + +#----------------------------------------------------------- +#::pattern::create >arrayHandle { +# variable o_arrayName +# variable this +#} +::>pattern .. Create >arrayHandle + +>arrayHandle .. PatternVariable o_arrayName +>arrayHandle .. PatternVariable this + +>arrayHandle .. Constructor {args} { + var o_arrayName this + set this @this@ + + + set o_arrayName [$this .. Namespace]::array + + upvar #0 $o_arrayName $this + #? how to automatically update this after a namespace import? + + array set $o_arrayName [list] + +} +>arrayHandle .. PatternMethod array {} { + var o_arrayName + return $o_arrayName +} + +#------------------------------------------------------- +#---- some experiments +>arrayHandle .. PatternMethod up {varname} { + var o_arrayName + + #is it dodgy to hard-code the calling depth? + #will it be different for different object systems? + #Will it even be consistent for the same object. + # Is this method necessary anyway? - + # - users can always instead do: + # upvar #0 [>instance . array] var + + uplevel 3 [list upvar 0 $o_arrayName $varname] + + return +} +>arrayHandle .. PatternMethod global {varname} { + var o_arrayName + # upvar #0 [>instance . array] var + + if {![string match ::* $varname]} { + set varname ::$varname + } + + upvar #0 $o_arrayName $varname + + return +} +>arrayHandle .. PatternMethod depth {} { + var o_arrayName + # + for {set i 0} {$i < [info level]} { + puts "${i}: [uplevel $i [list namespace current] , [info level $i]]" + } + +} + # -------------------------------------------- + + +>arrayHandle .. PatternMethod item {key} { + var o_arrayName + set ${o_arrayName}($key) +} +>arrayHandle .. PatternMethod items {} { + var o_arrayName + + set result [list] + foreach nm [array names $o_arrayName] { + lappend result [set ${o_arrayName}($nm)] + } + return $result +} +>arrayHandle .. PatternMethod pairs {} { + var o_arrayName + + array get $o_arrayName +} +>arrayHandle .. PatternMethod add {val key} { + var o_arrayName + + set ${o_arrayName}($key) $val + return $key +} +>arrayHandle .. PatternMethod del {key} { + puts stderr "Warning: 'del' method of >arrayHandle deprecated. Use 'remove' instead." + remove $_ID_ $key +} +>arrayHandle .. PatternMethod remove {key} { + var o_arrayName + unset ${o_arrayName}($key) + return $key +} +>arrayHandle .. PatternMethod size {} { + var o_arrayName + return [array size $o_arrayName] +} +>arrayHandle .. PatternMethod count {} { + #alias for size + var o_arrayName + return [array size $o_arrayName] +} +>arrayHandle .. PatternMethod statistics {} { + var o_arrayName + return [array statistics $o_arrayName] +} +>arrayHandle .. PatternMethod names {} { + var o_arrayName + array names $o_arrayName +} +>arrayHandle .. PatternMethod keys {} { + #synonym for names + var o_arrayName + array names $o_arrayName +} +>arrayHandle .. PatternMethod hasKey {key} { + var o_arrayName + + return [info exists ${o_arrayName}($key)] +} +>arrayHandle .. PatternMethod clear {} { + var o_arrayName + unset $o_arrayName + array set $o_arrayName [list] + + return +} +#>arrayHandle .. Ready 1 + + + + +::>pattern .. Create >matrix + +>matrix .. PatternVariable o_array +>matrix .. PatternVariable o_size + +>matrix .. Constructor {args} { + var o_array o_size + + array set o_array [list] + set o_size 0 +} + + +#process_pattern_aliases ::patternlib::>matrix + +set PM [>matrix .. PatternMethod .] + +>matrix .. PatternMethod item {args} { + var o_array + + if {![llength $args]} { + error "indices required" + } else { + + } + if [info exists o_array($args)] { + return $o_array($args) + } else { + error "no such index: '$args'" + } +} +>matrix .. PatternMethod items {} { + var o_array + + set result [list] + foreach nm [array names o_array] { + lappend result $o_array($nm) + } + return $result +} +>matrix .. PatternMethod pairs {} { + var o_array + + array get o_array +} +>matrix .. PatternMethod slice {args} { + var o_array + + if {"*" ni $args} { + lappend args * + } + + array get o_array $args +} +>matrix .. PatternMethod add {val args} { + var o_array o_size + + if {![llength $args]} { + error "indices required" + } + + set o_array($args) $val + incr o_size + + #return [array size o_array] + return $o_size +} +>matrix .. PatternMethod names {} { + var o_array + array names o_array +} +>matrix .. PatternMethod keys {} { + #synonym for names + var o_array + array names o_array +} +>matrix .. PatternMethod hasKey {args} { + var o_array + + return [info exists o_array($args)] +} +>matrix .. PatternMethod clear {} { + var o_array o_size + unset o_array + set o_size 0 + return +} +>matrix .. PatternMethod count {} { + var o_size + return $o_size +} +>matrix .. PatternMethod count2 {} { + var o_array + #see comments for >hashMap count2 + return [array size o_array] +} +#>matrix .. Ready 1 + +#-------------------------------------------------------- +#tree data structure (based *loosely* on API at http://www.msen.com/%7Eclif/treeNobj.html - discussed in Clif Flynts book Tcl programming) +#!todo - compare API to http://tcllib.sourceforge.net/doc/tree.html +#!todo - create an >itree (inheritance tree) where node data is readable/writable on children unless overridden. +::>pattern .. Create >tree + +set _NODE [::>pattern .. Create [>tree .. Namespace]::>node] +set _TREE_NODE $_NODE +#process_pattern_aliases $_TREE_NODE + +$_NODE .. PatternVariable o_treens ;#tree namespace +$_NODE .. PatternVariable o_idref +$_NODE .. PatternVariable o_nodePrototype + +#$_NODE .. PatternProperty data +$_NODE .. PatternProperty info + +$_NODE .. PatternProperty tree +$_NODE .. PatternProperty parent +$_NODE .. PatternProperty children +$_NODE .. PatternMethod addNode {} { + set nd_id [incr $o_idref] + set nd [$o_nodePrototype .. Create ${o_treens}::>n-$nd_id -tree $o_tree -parent @this@] + @this@ . add $nd n-$nd_id + + return n-$nd_id +} +#flat list of all nodes below this +#!todo - something else? ad-hoc collections? +#!todo - non-recursive version? tail-call opt? +$_NODE .. PatternMethod nodes {} { + set result [list] + + #use(abuse?) our knowledge of >collection internals + foreach n $o_list { + #eval lappend result $n [$o_array($n) . nodes] + #!todo - test + lappend result $n {*}[$o_array($n) . nodes] + } + return $result +} +#count of number of descendants +#!todo - non-recursive version? tail-call opt? +$_NODE .. PatternMethod size {} { + set result 0 + #use(abuse?) our knowledge of >collection internals + foreach n $o_list { + incr result [expr {1 + [$o_array($n) . size]}] + } + return $result +} +$_NODE .. PatternMethod isLeaf {} { + #!todo - way to stop unused vars being uplevelled? + var o_tree + + #tailcall isEmpty $_ID_ ;#fails. because isEmpty is from >collection interface - so different ns? + tailcall [@this@ . isEmpty .] +} +$_NODE .. Constructor {args} { + array set A $args + + set o_tree $A(-tree) + set o_parent $A(-parent) + + #array set o_data [list] + array set o_info [list] + + set o_nodePrototype [::patternlib::>tree .. Namespace]::>node + set o_idref [$o_tree . nodeID .] + set o_treens [$o_tree .. Namespace] + #set o_children [::patternlib::>collection .. Create [@this@ .. Namespace]::>children] + + #overlay children collection directly on the node + set o_children [::patternlib::>collection .. Create @this@] + + return +} + +>tree .. PatternProperty test blah +>tree .. PatternProperty nodeID 0 ;#public only so node can access.. need 'friend' concept? +>tree .. PatternVariable o_ns +>tree .. Constructor {args} { + set o_ns [@this@ .. Namespace] + + #>tree is itself also a node (root node) + #overlay new 'root' node onto existing tree, pass tree to constructor + [::patternlib::>tree .. Namespace]::>node .. Create @this@ -tree @this@ -parent "" +} + + + + +unset _NODE + + + + +#-------------------------------------------------------- +#a basic binary search tree experiment +# - todo - 'scheme' property to change behaviour? e.g balanced tree +::>pattern .. Create >bst +#process_pattern_aliases ::patternlib::>bst +>bst .. PatternVariable o_NS ;#namespace +>bst .. PatternVariable o_this ;#namespace +>bst .. PatternVariable o_nodeID + +>bst .. PatternProperty root "" +>bst .. Constructor {args} { + set o_this @this@ + set o_NS [$o_this .. Namespace] + namespace eval ${o_NS}::nodes {} + puts stdout ">bst constructor" + set o_nodeID 0 +} +>bst .. PatternMethod insert {key args} { + set newnode [::patternlib::>bstnode .. Create ${o_NS}::nodes::>n-[incr o_nodeID]] + set [$newnode . key .] $key + if {[llength $args]} { + set [$newnode . value .] $args + } + if {![string length $o_root]} { + set o_root $newnode + set [$newnode . parent .] $o_this + } else { + set ipoint {} ;#insertion point + set tpoint $o_root ;#test point + set side {} + while {[string length $tpoint]} { + set ipoint $tpoint + if {[$newnode . key] < [$tpoint . key]} { + set tpoint [$tpoint . left] + set side left + } else { + set tpoint [$tpoint . right] + set side right + } + } + set [$newnode . parent .] $ipoint + set [$ipoint . $side .] $newnode + } + return $newnode +} +>bst .. PatternMethod item {key} { + if {![string length $o_root]} { + error "item $key not found" + } else { + set tpoint $o_root + while {[string length $tpoint]} { + if {[$tpoint . key] eq $key} { + return $tpoint + } else { + if {$key < [$tpoint . key]} { + set tpoint [$tpoint . left] + } else { + set tpoint [$tpoint . right] + } + } + } + error "item $key not found" + } +} +>bst .. PatternMethod inorder-walk {} { + if {[string length $o_root]} { + $o_root . inorder-walk + } + puts {} +} +>bst .. PatternMethod view {} { + array set result [list] + + if {[string length $o_root]} { + array set result [$o_root . view 0 [list]] + } + + foreach depth [lsort [array names result]] { + puts "$depth: $result($depth)" + } + +} +::>pattern .. Create >bstnode +#process_pattern_aliases ::patternlib::>bstnode +>bstnode .. PatternProperty parent +>bstnode .. PatternProperty left "" +>bstnode .. PatternProperty right "" +>bstnode .. PatternProperty key +>bstnode .. PatternProperty value + +>bstnode .. PatternMethod inorder-walk {} { + if {[string length $o_left]} { + $o_left . inorder-walk + } + + puts -nonewline "$o_key " + + if {[string length $o_right]} { + $o_right . inorder-walk + } + + return +} +>bstnode .. PatternMethod view {depth state} { + #!todo - show more useful representation of structure + set lower [incr depth] + + if {[string length $o_left]} { + set state [$o_left . view $lower $state] + } + + if {[string length $o_right]} { + set state [$o_right . view $lower $state] + } + + + array set s $state + lappend s($depth) $o_key + + return [array get s] +} + + +#-------------------------------------------------------- +#::pattern::create ::pattern::>metaObject +#::pattern::>metaObject PatternProperty methods +#::pattern::>metaObject PatternProperty properties +#::pattern::>metaObject PatternProperty PatternMethods +#::pattern::>metaObject PatternProperty patternProperties +#::pattern::>metaObject Constructor args { +# set this @this@ +# +# set [$this . methods .] [::>collection create [$this namespace]::methods] +# set [$this . properties .] [::>collection create [$this namespace]::properties] +# set [$this . PatternMethods .] [::>collection create [$this namespace]::PatternMethods] +# set [$this . patternProperties .] [::>collection create [$this namespace]::patternProperties] +# +#} + + + + #tidy up + unset PV + unset PM + + + +#-------------------------------------------------------- +::>pattern .. Create >enum +#process_pattern_aliases ::patternlib::>enum +>enum .. PatternMethod item {{idx 0}} { + var o_array o_list + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + if {[catch {set o_array([lindex $o_list $idx])} result]} { + error "no such index : '$idx'" + } else { + return $result + } + } else { + if {[catch {set o_array($idx)} result]} { + error "no such index: '$idx'" + } else { + return $result + } + } +} + + + +#proc makeenum {type identifiers} { +# #!!todo - make generated procs import into whatever current system context? +# +# upvar #0 wbpbenum_${type}_number a1 wbpbenum_number_${type} a2 +# +# #obliterate any previous enum for this type +# catch {unset a1} +# catch {unset a2} +# +# set n 0 +# foreach id $identifiers { +# set a1($id) $n +# set a2($n) $id +# incr n +# } +# proc ::${type}_to_number key [string map [list @type@ $type] { +# upvar #0 wbpbenum_@type@_number ary +# if {[catch {set ary($key)} num]} { +# return -code error "unknown @type@ '$key'" +# } +# return $num +# }] +# +# proc ::number_to_${type} {number} [string map [list @type@ $type] { +# upvar #0 wbpbenum_number_@type@ ary +# if {[catch {set ary($number)} @type@]} { +# return -code error "no @type@ for '$number'" +# } +# return $@type@ +# }] +# +# #eval "namespace eval ::sysnexus {namespace export number_to_${type}; namespace export ${type}_to_number}" +# #eval "namespace eval :: {namespace import -force sysnexus::number_to_${type} sysnexus::${type}_to_number}" +#} +# +#-------------------------------------------------------- +::>pattern .. Create >nest +>nest .. PatternVariable THIS +>nest .. PatternProperty data -autoclone +>nest .. Constructor {args} { + var o_data + var THIS + set THIS @this@ + array set o_data [list] +} +>nest .. PatternMethod item {args} { + set THIS @this@ + return [$THIS . data [join $args ,]] +} + +# +# e.g +# set [>nest a , b . data c .] blah +# >nest a , b , c +# +# set [>nest w x , y . data z .] etc +# >nest w x , y , z +#-------------------------------------------------------- + +} + +} + + +#package require patternlibtemp diff --git a/src/bootsupport/modules/patternpredator2-1.2.4.tm b/src/bootsupport/modules/patternpredator2-1.2.4.tm new file mode 100644 index 00000000..457d5742 --- /dev/null +++ b/src/bootsupport/modules/patternpredator2-1.2.4.tm @@ -0,0 +1,754 @@ +package provide patternpredator2 1.2.4 + +proc ::p::internals::jaws {OID _ID_ args} { + #puts stderr ">>>(patternpredator2 lib)jaws called with _ID_:$_ID_ args: $args" + #set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + yield + set w 1 + + set stack [list] + set wordcount [llength $args] + set terminals [list . .. , # @ !] ;#tokens which require the current stack to be evaluated first + set unsupported 0 + set operator "" + set operator_prev "" ;#used only by argprotect to revert to previous operator + + + if {$OID ne "null"} { + #!DO NOT use upvar here for MAP! (calling set on a MAP in another iteration/call will overwrite a map for another object!) + #upvar #0 ::p::${OID}::_meta::map MAP + set MAP [set ::p::${OID}::_meta::map] + } else { + # error "jaws - OID = 'null' ???" + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] ;#MAP taken from _ID_ will be missing 'interfaces' key + } + set invocantdata [dict get $MAP invocantdata] + lassign $invocantdata OID alias default_method object_command wrapped + + set finished_args 0 ;#whether we've completely processed all args in the while loop and therefor don't need to peform the final word processing code + + #don't use 'foreach word $args' - we sometimes need to backtrack a little by manipulating $w + while {$w < $wordcount} { + set word [lindex $args [expr {$w -1}]] + #puts stdout "w:$w word:$word stack:$stack" + + if {$operator eq "argprotect"} { + set operator $operator_prev + lappend stack $word + incr w + } else { + if {[llength $stack]} { + if {$word in $terminals} { + set reduction [list 0 $_ID_ {*}$stack ] + #puts stderr ">>>jaws yielding value: $reduction triggered by word $word in position:$w" + + + set _ID_ [yield $reduction] + set stack [list] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#Do not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces [list level0 {} level1 {}]] + #puts stderr "WARNING REVIEW: jaws-branch - leave empty??????" + } + + #review - 2018. switched to _ID_ instead of MAP + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command + #lassign [dict get $MAP invocantdata] OID alias default_method object_command + + + #puts stdout "---->>> yielded _ID_: $_ID_ OID:$OID alias:$alias default_method:$default_method object_command:$object_command" + set operator $word + #don't incr w + #incr w + } else { + if {$operator eq "argprotect"} { + set operator $operator_prev + set operator_prev "" + lappend stack $word + } else { + #only look for leading argprotect chacter (-) if we're not already in argprotect mode + if {$word eq "--"} { + set operator_prev $operator + set operator "argprotect" + #Don't add the plain argprotector to the stack + } elseif {[string match "-*" $word]} { + #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument) + set operator_prev $operator + set operator "argprotect" + lappend stack $word + } else { + lappend stack $word + } + } + + + incr w + } + } else { + #no stack + switch -- $word {.} { + + if {$OID ne "null"} { + #we know next word is a property or method of a pattern object + incr w + set nextword [lindex $args [expr {$w - 1}]] + set command ::p::${OID}::$nextword + set stack [list $command] ;#2018 j + set operator . + if {$w eq $wordcount} { + set finished_args 1 + } + } else { + # don't incr w + #set nextword [lindex $args [expr {$w - 1}]] + set command $object_command ;#taken from the MAP + set stack [list "_exec_" $command] + set operator . + } + + + } {..} { + incr w + set nextword [lindex $args [expr {$w -1}]] + set command ::p::-1::$nextword + #lappend stack $command ;#lappend a small number of items to an empty list is slower than just setting the list. + set stack [list $command] ;#faster, and intent is clearer than lappend. + set operator .. + if {$w eq $wordcount} { + set finished_args 1 + } + } {,} { + #puts stdout "Stackless comma!" + + + if {$OID ne "null"} { + set command ::p::${OID}::$default_method + } else { + set command [list $default_method $object_command] + #object_command in this instance presumably be a list and $default_method a list operation + #e.g "lindex {A B C}" + } + #lappend stack $command + set stack [list $command] + set operator , + } {--} { + set operator_prev $operator + set operator argprotect + #no stack - + } {!} { + set command $object_command + set stack [list "_exec_" $object_command] + #puts stdout "!!!! !!!! $stack" + set operator ! + } default { + if {$operator eq ""} { + if {$OID ne "null"} { + set command ::p::${OID}::$default_method + } else { + set command [list $default_method $object_command] + } + set stack [list $command] + set operator , + lappend stack $word + } else { + #no stack - so we don't expect to be in argprotect mode already. + if {[string match "-*" $word]} { + #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument) + set operator_prev $operator + set operator "argprotect" + lappend stack $word + } else { + lappend stack $word + } + + } + } + incr w + } + + } + } ;#end while + + #process final word outside of loop + #assert $w == $wordcount + #trailing operators or last argument + if {!$finished_args} { + set word [lindex $args [expr {$w -1}]] + if {$operator eq "argprotect"} { + set operator $operator_prev + set operator_prev "" + + lappend stack $word + incr w + } else { + + + switch -- $word {.} { + if {![llength $stack]} { + #set stack [list "_result_" [::p::internals::ref_to_object $_ID_]] + yieldto return [::p::internals::ref_to_object $_ID_] + error "assert: never gets here" + + } else { + #puts stdout "==== $stack" + #assert - whenever _ID_ changed in this proc - we have updated the $OID variable + yieldto return [::p::internals::ref_to_stack $OID $_ID_ $stack] + error "assert: never gets here" + } + set operator . + + } {..} { + #trailing .. after chained call e.g >x . item 0 .. + #puts stdout "$$$$$$$$$$$$ [list 0 $_ID_ {*}$stack] $$$$" + #set reduction [list 0 $_ID_ {*}$stack] + yieldto return [yield [list 0 $_ID_ {*}$stack]] + } {#} { + set unsupported 1 + } {,} { + set unsupported 1 + } {&} { + set unsupported 1 + } {@} { + set unsupported 1 + } {--} { + + #set reduction [list 0 $_ID_ {*}$stack[set stack [list]]] + #puts stdout " -> -> -> about to call yield $reduction <- <- <-" + set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]] ] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}} ] + } + yieldto return $MAP + } {!} { + #error "untested branch" + set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]]] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] + } + lassign [dict get $MAP invocantdata] OID alias default_command object_command + set command $object_command + set stack [list "_exec_" $command] + set operator ! + } default { + if {$operator eq ""} { + #error "untested branch" + lassign [dict get $MAP invocantdata] OID alias default_command object_command + #set command ::p::${OID}::item + set command ::p::${OID}::$default_command + lappend stack $command + set operator , + + } + #do not look for argprotect items here (e.g -option) as the final word can't be an argprotector anyway. + lappend stack $word + } + if {$unsupported} { + set unsupported 0 + error "trailing '$word' not supported" + + } + + #if {$operator eq ","} { + # incr wordcount 2 + # set stack [linsert $stack end-1 . item] + #} + incr w + } + } + + + #final = 1 + #puts stderr ">>>jaws final return value: [list 1 $_ID_ {*}$stack]" + + return [list 1 $_ID_ {*}$stack] +} + + + +#trailing. directly after object +proc ::p::internals::ref_to_object {_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 object_command + set refname ::p::${OID}::_ref::__OBJECT + + array set $refname [list] ;#important to initialise the variable as an array here - or initial read attempts on elements will not fire traces + + set traceCmd [list ::p::predator::object_read_trace $OID $_ID_] + if {[list {read} $traceCmd] ni [trace info variable $refname]} { + #puts stdout "adding read trace on variable '$refname' - traceCmd:'$traceCmd'" + trace add variable $refname {read} $traceCmd + } + set traceCmd [list ::p::predator::object_array_trace $OID $_ID_] + if {[list {array} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {array} $traceCmd + } + + set traceCmd [list ::p::predator::object_write_trace $OID $_ID_] + if {[list {write} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {write} $traceCmd + } + + set traceCmd [list ::p::predator::object_unset_trace $OID $_ID_] + if {[list {unset} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {unset} $traceCmd + } + return $refname +} + + +proc ::p::internals::create_or_update_reference {OID _ID_ refname command} { + #if {[lindex $fullstack 0] eq "_exec_"} { + # #strip it. This instruction isn't relevant for a reference. + # set commandstack [lrange $fullstack 1 end] + #} else { + # set commandstack $fullstack + #} + #set argstack [lassign $commandstack command] + #set field [string map {> __OBJECT_} [namespace tail $command]] + + + + set reftail [namespace tail $refname] + set argstack [lassign [split $reftail +] field] + set field [string map {> __OBJECT_} [namespace tail $command]] + + #puts stderr "refname:'$refname' command: $command field:$field" + + + if {$OID ne "null"} { + upvar #0 ::p::${OID}::_meta::map MAP + } else { + #set map [dict get [lindex [dict get $_ID_ i this] 0 1] map] + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}}] + } + lassign [dict get $MAP invocantdata] OID alias default_method object_command + + + + if {$OID ne "null"} { + interp alias {} $refname {} $command $_ID_ {*}$argstack + } else { + interp alias {} $refname {} $command {*}$argstack + } + + + #set iflist [lindex $map 1 0] + set iflist [dict get $MAP interfaces level0] + #set iflist [dict get $MAP interfaces level0] + set field_is_property_like 0 + foreach IFID [lreverse $iflist] { + #tcl (braced) expr has lazy evaluation for &&, || & ?: operators - so this should be reasonably efficient. + if {[llength [info commands ::p::${IFID}::_iface::(GET)$field]] || [llength [info commands ::p::${IFID}::_iface::(SET)$field]]} { + set field_is_property_like 1 + #There is a setter or getter (but not necessarily an entry in the o_properties dict) + break + } + } + + + + + #whether field is a property or a method - remove any commandrefMisuse_TraceHandler + foreach tinfo [trace info variable $refname] { + #puts "-->removing traces on $refname: $tinfo" + if {[lindex $tinfo 1 0] eq "::p::internals::commandrefMisuse_TraceHandler"} { + trace remove variable $refname {*}$tinfo + } + } + + if {$field_is_property_like} { + #property reference + + + set this_invocantdata [lindex [dict get $_ID_ i this] 0] + lassign $this_invocantdata OID _alias _defaultmethod object_command + #get fully qualified varspace + + # + set propdict [$object_command .. GetPropertyInfo $field] + if {[dict exist $propdict $field]} { + set field_is_a_property 1 + set propinfo [dict get $propdict $field] + set varspace [dict get $propinfo varspace] + if {$varspace eq ""} { + set full_varspace ::p::${OID} + } else { + if {[::string match "::*" $varspace]} { + set full_varspace $varspace + } else { + set full_varspace ::p::${OID}::$varspace + } + } + } else { + set field_is_a_property 0 + #no propertyinfo - this field was probably established as a PropertyRead and/or PropertyWrite without a Property + #this is ok - and we still set the trace infrastructure below (app may convert it to a normal Property later) + set full_varspace ::p::${OID} + } + + + + + + #We only trace on entire property.. not array elements (if references existed to both the array and an element both traces would be fired -(entire array trace first)) + set Hndlr [::list ::p::predator::propvar_write_TraceHandler $OID $field] + if { [::list {write} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} { + trace add variable ${full_varspace}::o_${field} {write} $Hndlr + } + set Hndlr [::list ::p::predator::propvar_unset_TraceHandler $OID $field] + if { [::list {unset} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} { + trace add variable ${full_varspace}::o_${field} {unset} $Hndlr + } + + + #supply all data in easy-access form so that propref_trace_read is not doing any extra work. + set get_cmd ::p::${OID}::(GET)$field + set traceCmd [list ::p::predator::propref_trace_read $get_cmd $_ID_ $refname $field $argstack] + + if {[list {read} $traceCmd] ni [trace info variable $refname]} { + set fieldvarname ${full_varspace}::o_${field} + + + #synch the refvar with the real var if it exists + #catch {set $refname [$refname]} + if {[array exists $fieldvarname]} { + if {![llength $argstack]} { + #unindexed reference + array set $refname [array get $fieldvarname] + #upvar $fieldvarname $refname + } else { + set s0 [lindex $argstack 0] + #refs to nonexistant array members common? (catch vs 'info exists') + if {[info exists ${fieldvarname}($s0)]} { + set $refname [set ${fieldvarname}($s0)] + } + } + } else { + #refs to uninitialised props actually should be *very* common. + #If we use 'catch', it means retrieving refs to non-initialised props is slower. Fired catches can be relatively expensive. + #Because it's common to get a ref to uninitialised props (e.g for initial setting of their value) - we will use 'info exists' instead of catch. + + #set errorInfo_prev $::errorInfo ;#preserve errorInfo across catches! + + #puts stdout " ---->>!!! ref to uninitialised prop $field $argstack !!!<------" + + + if {![llength $argstack]} { + #catch {set $refname [set ::p::${OID}::o_$field]} + if {[info exists $fieldvarname]} { + set $refname [set $fieldvarname] + #upvar $fieldvarname $refname + } + } else { + if {[llength $argstack] == 1} { + #catch {set $refname [lindex [set ::p::${OID}::o_$field] [lindex $argstack 0]]} + if {[info exists $fieldvarname]} { + set $refname [lindex [set $fieldvarname] [lindex $argstack 0]] + } + + } else { + #catch {set $refname [lindex [set ::p::${OID}::o_$field] $argstack]} + if {[info exists $fieldvarname]} { + set $refname [lindex [set $fieldvarname] $argstack] + } + } + } + + #! what if someone has put a trace on ::errorInfo?? + #set ::errorInfo $errorInfo_prev + } + trace add variable $refname {read} $traceCmd + + set traceCmd [list ::p::predator::propref_trace_write $_ID_ $OID $full_varspace $refname] + trace add variable $refname {write} $traceCmd + + set traceCmd [list ::p::predator::propref_trace_unset $_ID_ $OID $refname] + trace add variable $refname {unset} $traceCmd + + + set traceCmd [list ::p::predator::propref_trace_array $_ID_ $OID $refname] + # puts "**************** installing array variable trace on ref:$refname - cmd:$traceCmd" + trace add variable $refname {array} $traceCmd + } + + } else { + #puts "$refname ====> adding refMisuse_traceHandler $alias $field" + #matching variable in order to detect attempted use as property and throw error + + #2018 + #Note that we are adding a trace on a variable (the refname) which does not exist. + #this is fine - except that the trace won't fire for attempt to write it as an array using syntax such as set $ref(someindex) + #we could set the ref to an empty array - but then we have to also undo this if a property with matching name is added + ##array set $refname {} ;#empty array + # - the empty array would mean a slightly better error message when misusing a command ref as an array + #but this seems like a code complication for little benefit + #review + + trace add variable $refname {read write unset array} [list ::p::internals::commandrefMisuse_TraceHandler $OID $field] + } +} + + + +#trailing. after command/property +proc ::p::internals::ref_to_stack {OID _ID_ fullstack} { + if {[lindex $fullstack 0] eq "_exec_"} { + #strip it. This instruction isn't relevant for a reference. + set commandstack [lrange $fullstack 1 end] + } else { + set commandstack $fullstack + } + set argstack [lassign $commandstack command] + set field [string map {> __OBJECT_} [namespace tail $command]] + + + #!todo? + # - make every object's OID unpredictable and sparse (UUID) and modify 'namespace child' etc to prevent iteration/inspection of ::p namespace. + # - this would only make sense for an environment where any meta methods taking a code body (e.g .. Method .. PatternMethod etc) are restricted. + + + #references created under ::p::${OID}::_ref are effectively inside a 'varspace' within the object itself. + # - this would in theory allow a set of interface functions on the object which have direct access to the reference variables. + + + set refname ::p::${OID}::_ref::[join [concat $field $argstack] +] + + if {[llength [info commands $refname]]} { + #todo - review - what if the field changed to/from a property/method? + #probably should fix that where such a change is made and leave this short circuit here to give reasonable performance for existing refs + return $refname + } + ::p::internals::create_or_update_reference $OID $_ID_ $refname $command + return $refname +} + + +namespace eval pp { + variable operators [list .. . -- - & @ # , !] + variable operators_notin_args "" + foreach op $operators { + append operators_notin_args "({$op} ni \$args) && " + } + set operators_notin_args [string trimright $operators_notin_args " &"] ;#trim trailing spaces and ampersands + #set operators_notin_args {({.} ni $args) && ({,} ni $args) && ({..} ni $args)} +} +interp alias {} strmap {} string map ;#stop code editor from mono-colouring our big string mapped code blocks! + + + + + +# 2017 ::p::predator2 is the development version - intended for eventual use as the main dispatch mechanism. +#each map is a 2 element list of lists. +# form: {$commandinfo $interfaceinfo} +# commandinfo is of the form: {ID Namespace defaultmethod commandname _?} + +#2018 +#each map is a dict. +#form: {invocantdata {ID Namespace defaultmethod commandname _?} interfaces {level0 {} level1 {}}} + + +#OID = Object ID (integer for now - could in future be a uuid) +proc ::p::predator2 {_ID_ args} { + #puts stderr "predator2: _ID_:'$_ID_' args:'$args'" + #set invocants [dict get $_ID_ i] + #set invocant_roles [dict keys $invocants] + + #For now - we are 'this'-centric (single dispatch). todo - adapt for multiple roles, multimethods etc. + #set this_role_members [dict get $invocants this] + #set this_invocant [lindex [dict get $_ID_ i this] 0] ;#for the role 'this' we assume only one invocant in the list. + #lassign $this_invocant this_OID this_info_dict + + set this_OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + + set cheat 1 ;# + #------- + #Optimise the next most common use case. A single . followed by args which contain no other operators (non-chained call) + #(it should be functionally equivalent to remove this shortcut block) + if {$cheat} { + if { ([lindex $args 0] eq {.}) && ([llength $args] > 1) && ([llength [lsearch -all -inline $args .]] == 1) && ({,} ni $args) && ({..} ni $args) && ({--} ni $args) && ({!} ni $args)} { + + set remaining_args [lassign $args dot method_or_prop] + + #how will we do multiple apis? (separate interface stacks) apply? apply [list [list _ID_ {*}$arglist] ::p::${stackid?}::$method_or_prop ::p::${this_OID}] ??? + set command ::p::${this_OID}::$method_or_prop + #REVIEW! + #e.g what if the method is named "say hello" ?? (hint - it will break because we will look for 'say') + #if {[llength $command] > 1} { + # error "methods with spaces not included in test suites - todo fix!" + #} + #Dont use {*}$command - (so we can support methods with spaces) + #if {![llength [info commands $command]]} {} + if {[namespace which $command] eq ""} { + if {[namespace which ::p::${this_OID}::(UNKNOWN)] ne ""} { + #lset command 0 ::p::${this_OID}::(UNKNOWN) ;#seems wrong - command could have spaces + set command ::p::${this_OID}::(UNKNOWN) + #tailcall {*}$command $_ID_ $cmdname {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + tailcall $command $_ID_ $method_or_prop {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "(::p::predator2) error running command:'$command' argstack:'[lrange $args 2 end]'\n - command not found and no 'unknown' handler" "method '$method_or_prop' not found" + } + } else { + #tailcall {*}$command $_ID_ {*}$remaining_args + tailcall $command $_ID_ {*}$remaining_args + } + } + } + #------------ + + + if {([llength $args] == 1) && ([lindex $args 0] eq "..")} { + return $_ID_ + } + + + #puts stderr "pattern::predator (test version) called with: _ID_:$_ID_ args:$args" + + + + #puts stderr "this_info_dict: $this_info_dict" + + + + + if {![llength $args]} { + #should return some sort of public info.. i.e probably not the ID which is an implementation detail + #return cmd + return [lindex [dict get [set ::p::${this_OID}::_meta::map] invocantdata] 0] ;#Object ID + + #return a dict keyed on object command name - (suitable as use for a .. Create 'target') + #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method object_command wrapped + #return [list $object_command [list -id $this_OID ]] + } elseif {[llength $args] == 1} { + #short-circuit the single index case for speed. + if {[lindex $args 0] ni {.. . -- - & @ # , !}} { + #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method + lassign [lindex [dict get $_ID_ i this] 0] this_OID alias default_method + + tailcall ::p::${this_OID}::$default_method $_ID_ [lindex $args 0] + } elseif {[lindex $args 0] eq {--}} { + + #!todo - we could hide the invocant by only allowing this call from certain uplevel procs.. + # - combined with using UUIDs for $OID, and a secured/removed metaface on the object + # - (and also hiding of [interp aliases] command so they can't iterate and examine all aliases) + # - this could effectively hide the object's namespaces,vars etc from the caller (?) + return [set ::p::${this_OID}::_meta::map] + } + } + + + + #upvar ::p::coroutine_instance c ;#coroutine names must be unique per call to predator (not just per object - or we could get a clash during some cyclic calls) + #incr c + #set reduce ::p::reducer${this_OID}_$c + set reduce ::p::reducer${this_OID}_[incr ::p::coroutine_instance] + #puts stderr "..................creating reducer $reduce with args $this_OID _ID_ $args" + coroutine $reduce ::p::internals::jaws $this_OID $_ID_ {*}$args + + + set current_ID_ $_ID_ + + set final 0 + set result "" + while {$final == 0} { + #the argument given here to $reduce will be returned by 'yield' within the coroutine context (jaws) + set reduction_args [lassign [$reduce $current_ID_[set current_ID_ [list]] ] final current_ID_ command] + #puts stderr "..> final:$final current_ID_:'$current_ID_' command:'$command' reduction_args:'$reduction_args'" + #if {[string match *Destroy $command]} { + # puts stdout " calling Destroy reduction_args:'$reduction_args'" + #} + if {$final == 1} { + + if {[llength $command] == 1} { + if {$command eq "_exec_"} { + tailcall {*}$reduction_args + } + if {[llength [info commands $command]]} { + tailcall {*}$command $current_ID_ {*}$reduction_args + } + set cmdname [namespace tail $command] + set this_OID [lindex [dict get $current_ID_ i this] 0 0] + if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} { + lset command 0 ::p::${this_OID}::(UNKNOWN) + tailcall {*}$command $current_ID_ $cmdname {*}$reduction_args ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "1)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found" + } + + } else { + #e.g lindex {a b c} + tailcall {*}$command {*}$reduction_args + } + + + } else { + if {[lindex $command 0] eq "_exec_"} { + set result [uplevel 1 [list {*}[lrange $command 1 end] {*}$reduction_args]] + + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {} ] + } else { + if {[llength $command] == 1} { + if {![llength [info commands $command]]} { + set cmdname [namespace tail $command] + set this_OID [lindex [dict get $current_ID_ i this] 0 0] + if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} { + + lset command 0 ::p::${this_OID}::(UNKNOWN) + set result [uplevel 1 [list {*}$command $current_ID_ $cmdname {*}$reduction_args]] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "2)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found" + } + } else { + #set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]] + set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]] + + } + } else { + set result [uplevel 1 [list {*}$command {*}$reduction_args]] + } + + if {[llength [info commands $result]]} { + if {([llength $result] == 1) && ([string first ">" [namespace tail $result]] == 0)} { + #looks like a pattern command + set current_ID_ [$result .. INVOCANTDATA] + + + #todo - determine if plain .. INVOCANTDATA is sufficient instead of .. UPDATEDINVOCANTDATA + #if {![catch {$result .. INVOCANTDATA} result_invocantdata]} { + # set current_ID_ $result_invocantdata + #} else { + # return -code error -errorinfo "3)error running command:'$command' argstack:'$reduction_args'\n - Failed to access result:'$result' as a pattern object." "Failed to access result:'$result' as a pattern object" + #} + } else { + #non-pattern command + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}] + } + } else { + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}] + #!todo - allow further operations on non-command values. e.g dicts, lists & strings (treat strings as lists) + + } + } + + } + } + error "Assert: Shouldn't get here (end of ::p::predator2)" + #return $result +} diff --git a/src/bootsupport/modules/punk-0.1.tm b/src/bootsupport/modules/punk-0.1.tm new file mode 100644 index 00000000..2d6e61da --- /dev/null +++ b/src/bootsupport/modules/punk-0.1.tm @@ -0,0 +1,7806 @@ +#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 +} + + + +#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 withe punk repl - todo - tone it down. +#namespace eval ::punk::repl::codethread { +# variable running 0 +#} +package require punk::lib +package require punk::ansi +#require aliascore after punk::lib & punk::ansi are loaded +package require punk::aliascore ;#mostly punk::lib aliases +punk::aliascore::init + +package require punk::repl::codethread +package require punk::config +#package require textblock +if {![llength [info commands ::ansistring]]} { + namespace import punk::ansi::ansistring +} +package require punk::console +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 +if {[catch { + package require punk::packagepreference +} errM]} { + puts stderr "Failed to load punk::packagepreference" +} +punk::packagepreference::install + +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]} { + 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]} { + 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 objclone {obj} { + append obj2 $obj {} + } + interp alias "" strlen "" ::punk::strlen + interp alias "" str_len "" ::punk::strlen + interp alias "" objclone "" ::punk::objclone + #proc ::strlen {str} { + # string length [append str2 $str {}] + #} + #proc ::objclone {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} + + 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 + } + + #review - there are various type of uuid - we should use something consistent across platforms + #twapi is used on windows because it's about 5 times faster - but is this more important than consistency? + #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway + #(counterpoint: in the case of punk - we currently need twapi anyway on windows) + #does tcllib's uuid use the same mechanisms on different platforms anyway? + proc ::punk::uuid {} { + set has_twapi 0 + if 0 { + if {"windows" eq $::tcl_platform(platform)} { + if {![catch { + set loader [zzzload::pkg_wait twapi] + } errM]} { + if {$loader in [list failed loading]} { + puts stderr "Unexpected problem during thread-load of pkg twapi - zzload::pkg_wait returned $loader" + } + } else { + package require twapi + } + if {[package provide twapi] ne ""} { + set has_twapi 1 + } + } + } + if {!$has_twapi} { + if {[catch {package require uuid} errM]} { + error "Unable to load a package for uuid on this platform. Try installing tcllib's uuid (any platform) - or twapi for windows" + } + return [uuid::uuid generate] + } else { + return [twapi::new_uuid] + } + } + + #get last command result that was run through the repl + proc ::punk::get_runchunk {args} { + set argd [punk::args::get_dict { + *opts + -1 -optional 1 -type none + -2 -optional 1 -type none + *values -min 0 -max 0 + } $args] + #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 + } + #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + # + #we can't provide a float comparison suitable for every situation, + #but we pick something reasonable, keep it stable, and document it. + proc float_almost_equal {a b} { + package require math::constants + set diff [expr {abs($a - $b)}] + if {$diff <= $math::constants::eps} { + return 1 + } + set A [expr {abs($a)}] + set B [expr {abs($b)}] + set largest [expr {($B > $A) ? $B : $A}] + return [expr {$diff <= $largest * $math::constants::eps}] + } + + #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. + proc boolean_equal {a b} { + #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. + expr {($a && 1) == ($b && 1)} + } + #debatable whether boolean_almost_equal is likely to be surprising or helpful. + #values from a calculation that are extremely close to zero but aren't false could also be surprising - especially if they compare equal numerically + #perhaps a fuzzy-boolean is a step too far for a default - but it's inline with float-comparison for pattern-matching. use an even more complex classifier? (^&~) ? + proc boolean_almost_equal {a b} { + if {[string is double -strict $a]} { + if {[float_almost_equal $a 0]} { + set a 0 + } + } + if {[string is double -strict $b]} { + if {[float_almost_equal $b 0]} { + set b 0 + } + } + #must handle true,no etc. + expr {($a && 1) == ($b && 1)} + } + + + 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} " "<"] ;# (> required for insertionspecs at rhs of = & .= ) + #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname + + #except when prefixed directly by pin classifier ^ + set protect_terminals [list "^"] ;# e.g sequence ^# + #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string + #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' + set in_brackets 0 ;#count depth + set in_atom 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + set indq 0 + set inesc 0 ;#whether last char was backslash (see also punk::escv) + set prevc "" + set char_index 0 + foreach c [split $varspecs ""] { + if {$indq} { + if {$inesc} { + #puts stderr "inesc adding '$c'" + append token $c + } else { + if {$c eq {"}} { + set indq 0 + } else { + append token $c + } + } + } elseif {$in_atom} { + #ignore dquotes/brackets in atoms - pass through + append token $c + #set nextc [lindex $chars $char_index+1] + if {$c eq "'"} { + set in_atom 0 + } + } elseif {$in_brackets > 0} { + append token $c + if {$c eq ")"} { + incr in_brackets -1 + } + } else { + if {$c eq {"} && !$inesc} { + set indq 1 + } elseif {$c eq ","} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. + #lassign [scan $token %${first_term}s%s] var spec + set var [string range $token 0 $first_term-1] + set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list [string trim $var] [string trim $spec]] + set token "" + set token_index -1 ;#reduce by 1 because , not included in next token + set first_term -1 + } else { + append token $c + switch -exact -- $c { + ' { + set in_atom 1 + } + ( { + incr in_brackets + } + default { + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } + } + } + } + } + set prevc $c + if {$c eq "\\"} { + #review + if {$inesc} { + set inesc 0 + } else { + set token [string range $token 0 end-1] + set inesc 1 + } + } else { + set inesc 0 + } + incr token_index + incr char_index + } + if {[string length $token]} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + #lassign [scan $token %${first_term}s%s] var spec + set var [string range $token 0 $first_term-1] + set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list [string trim $var] [string trim $spec]] + } + return $varlist + } + proc _split_var_key_at_unbracketed_comma {varspecs} { + set varlist [list] + set var_terminals [list "@" "/" "#" "!"] + #except when prefixed directly by pin classifier ^ + set protect_terminals [list "^"] ;# e.g sequence ^# + #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string + #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' + set in_brackets 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + set prevc "" + foreach c [split $varspecs ""] { + if {$in_brackets} { + append token $c + if {$c eq ")"} { + set in_brackets 0 + } + } else { + if {$c eq ","} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + lassign [scan $token %${first_term}s%s] var spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list $var $spec] + set token "" + set token_index -1 ;#reduce by 1 because , not included in next token + set first_term -1 + } else { + append token $c + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } elseif {$c eq "("} { + set in_brackets 1 + } + } + } + set prevc $c + incr token_index + } + if {[string length $token]} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + lassign [scan $token %${first_term}s%s] var spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list $var $spec] + } + return $varlist + } + proc _split_var_key_at_unbracketed_comma1 {varspecs} { + set varlist [list] + set var_terminals [list "@" "/" "#" "!"] + set in_brackets 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + foreach c [split $varspecs ""] { + if {$in_brackets} { + if {$c eq ")"} { + set in_brackets 0 + } + append token $c + } else { + if {$c eq ","} { + 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 ""] + } + 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 + } + } + #? { + 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! + 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 + 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 (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 { + 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 + } + %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 { + 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 {$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 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 globvalue-get-keys-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 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 globvalue-get-pairs-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 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 globvalue-get-values-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 + } + } + }] + } + + error "globkeyvalue-get-pairs todo" + } + @* { + 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 + }] + } + %* { + 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 {[string first "end" $index] >=0} { + 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]} { + 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} { + #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"} { + if {[string match *-* $index]} { + lappend INDEX_OPERATIONS string-range + 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 "*"} { + 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 + } + + #todo - recurse into bracketed sub parts + #JMN3 + #e.g @*/(x@0,y@2) + proc _var_classify {multivar} { + set cmdname ::punk::pipecmds::var_classify::_[pipecmd_namemapping $multivar] + if {[info commands $cmdname] ne ""} { + return [$cmdname] + } + + + #comma seems a natural choice to split varspecs, + #but also for list and dict subelement access + #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) + #so / will indicate subelements e.g @0/1 for lindex $list 0 1 + #set valsource_key_list [_split_var_key_at_unbracketed_comma $multivar] + set valsource_key_list [_split_patterns_memoized $multivar] + + + + #mutually exclusive - atom/pin + #set map [list "" ' ^ &] ;#0 = default/var/not-yet-determined 1 = atom 2 = pin + #set var_class [lmap var $valsource_key_list {expr {([set m [lsearch $map [string index [lindex $var 0] 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] + #0 - novar + #1 - atom ' + #2 - pin ^ + #3 - boolean & + #4 - integer + #5 - double + #6 - var + #7 - glob (no classifier and contains * or ?) + #8 - numeric + #9 - > (+) + #10 - < (-) + + set var_names [list] + set var_class [list] + set varspecs_trimmed [list] ;#raw varspecs without pin/atom modifiers - or empty string for glob + + + set leading_classifiers [list "'" "&" "^" ] + set trailing_classifiers [list + -] + set possible_number_start [list - + . 0 1 2 3 4 5 6 7 8 9 > <] + + foreach v_key $valsource_key_list { + lassign $v_key v key + set vname $v ;#default + set classes [list] + if {$v eq ""} { + lappend var_class [list $v_key 0] + lappend varspecs_trimmed $v_key + } else { + set lastchar [string index $v end] + switch -- $lastchar { + + { + lappend classes 9 + set vname [string range $v 0 end-1] + } + - { + lappend classes 10 + set vname [string range $v 0 end-1] + } + } + set firstchar [string index $v 0] + switch -- $firstchar { + ' { + lappend var_class [list $v_key 1] + #set vname [string range $v 1 end] + lappend varspecs_trimmed [list $vname $key] + } + ^ { + lappend classes [list 2] + #use vname - may already have trailing +/- stripped + set vname [string range $vname 1 end] + set secondclassifier [string index $v 1] + switch -- $secondclassifier { + "&" { + #pinned boolean + lappend classes 3 + set vname [string range $v 2 end] + } + "#" { + #pinned numeric comparison instead of string comparison + #e.g set x 2 + # this should match: ^#x.= list 2.0 + lappend classes 8 + set vname [string range $vname 1 end] + } + "*" { + #pinned glob + lappend classes 7 + set vname [string range $v 2 end] + } + } + #todo - check for second tag - & for pinned boolean? + #consider requiring ^# for numeric comparisons. currently no way to do a strictly string comparison on pinned variables.... default ^var really shouldn't be doing any magic. + #while we're at it.. pinned glob would be nice. ^* + #maybe even pinned scan ^% ? regex? ^/ or ^? these would be hard to have corresponding literals in the pattern mini-lang. + #These all limit the range of varnames permissible - which is no big deal. + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed [list $vname $key] + } + & { + #we require boolean literals to be single-quoted so we can use cross-binding on boolean vars. + #ie &true is the variable true whereas &'true' or &'1' &'t' etc are literal booleans + #allow exception of &1 &0 to be literal booleans - because we disallow 0 & 1 as varnames in other contexts anyway - so it would be more consistent not to treat as varnames here. + lappend var_class [list $v_key 3] + set vname [string range $v 1 end] + lappend varspecs_trimmed [list $vname $key] + } + default { + if {([string first ? $v]) >=0 || ([string first * $v] >=0)} { + lappend var_class [list $v_key 7] ;#glob + #leave vname as the full glob + lappend varspecs_trimmed [list "" $key] + } else { + #scan vname not v - will either be same as v - or possibly stripped of trailing +/- + set numtestv [join [scan $vname %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, decimal points and sci notation - fails to handle leading dot e.g .5 + #leading . still need to test directly for double + if {[string is double -strict $vname] || [string is double -strict $numtestv]} { + if {[string is integer -strict $numtestv]} { + #this will pick up boolean 1 or 0 - but that's ok - they require "&" marker if boolean comparison desired + #integer test before double.. + #note there is also string is wide (string is wideinteger) for larger ints.. + lappend classes 4 + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed $v_key + } else { + #double + #sci notation 1e123 etc + #also large numbers like 1000000000 - even without decimal point - (tcl bignum) + lappend classes 5 + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed $v_key + } + } else { + lappend var_class [list $v_key 6] ;#var + lappend varspecs_trimmed $v_key + } + } + } + } + } + lappend var_names $vname + } + + set result [list var_names $var_names var_class $var_class varspecs_trimmed $varspecs_trimmed] + + proc $cmdname {} [list return $result] + debug.punk.pipe.compile {proc $cmdname} + return $result + } + + + + #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 [_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::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::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::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::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 + #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 var_display_names [list] + foreach v $var_names { + if {$v eq ""} { + lappend var_display_names {{}} + } else { + lappend var_display_names $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::objclone $args] + set nscaller [uplevel 1 [list 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::objclone $args] + set nscaller [uplevel 1 [list namespace current]] + tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller] + } + + #map rhs to names suitable to use in pipemcd proc name (whitespace mapping) + # (for .= and = pipecmds) + proc pipecmd_namemapping {rhs} { + #used to build a command from a pattern which could contain :: - we need to map it to keep it a single command in the desired namespace. + #glob chars will prevent us using simple test {[info commands $cmd] eq ""} to test existence + #we could use the test {$cmd in [info commands]} - but mapping away globchars is more robust, allowing the simpler test + set rhs [string trim $rhs];#ignore all leading & trailing whitespace + set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token + set rhs [tcl::string::map {: ? * } $rhs] + #review - we don't expect other command-incompatible chars such as colon? + return $rhs + } + + #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 [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 ::namespace path]] + if {$cmdns ni $existing_path} { + uplevel 1 [list ::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 [_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 ::namespace path]] + if {$cmdns ni $existing_path} { + uplevel 1 [list ::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? + } + 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 ] + 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 + } + + + + + #todo - consider whether we can use < for insertion/iteration combinations + # =a<,b< iterate once through + # =a><,b>< cartesian product + # =a<>,b<> ??? zip ? + # + # ie = {a b c} |> .=< inspect + # would call inspect 3 times, once for each argument + # .= list {a b c} {x y z} |a/0,b/1> .=a><,b>< list + # would produce list of cartesian pairs? + # + proc _split_equalsrhs {insertionpattern} { + #map the insertionpattern so we can use faster globless info command search + set name_mapped [pipecmd_namemapping $insertionpattern] + set cmdname ::punk::pipecmds::split_rhs::_$name_mapped + if {[info commands $cmdname] ne ""} { + return [$cmdname] + } + + set lst_var_indexposition [punk::_split_patterns_memoized $insertionpattern] + set i 0 + set return_triples [list] + foreach v_pos $lst_var_indexposition { + lassign $v_pos v index_and_position + #e.g varname@@data/ok>0 varname/1/0>end + #ensure only one ">" is detected + if {![string length $index_and_position]} { + set indexspec "" + set positionspec "" + } else { + set chars [split $index_and_position ""] + set posns [lsearch -all $chars ">"] + if {[llength $posns] > 1} { + error "pipesyntax error in segment insertion pattern '$insertionpattern' -v '$v' multiple '>' characters. Pattern not understood." "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] + } + if {![llength $posns]} { + set indexspec $index_and_position + set positionspec "" + } else { + set splitposn [lindex $posns 0] + set indexspec [string range $index_and_position 0 $splitposn-1] + set positionspec [string range $index_and_position $splitposn+1 end] + } + } + + #review - + if {($positionspec in [list "*" "/*" "@*" "/" "@"]) || ($v eq "*" && $positionspec eq "")} { + set star "" + if {$v eq "*"} { + set v "" + set star "*" + } + if {[string index $positionspec end] eq "*"} { + set star "*" + } + #it is always possible to insert at end of list regardless of current length - so /end* and @end* are equivalent + #as are /end and @end + #lset lst_var_indexposition $i [list $v "/end$star"] + set triple [list $v $indexspec "/end$star"] + } else { + if {$positionspec eq ""} { + #e.g just =varname + #lset lst_var_indexposition $i [list $v "/end"] + set triple [list $v $indexspec "/end"] + #error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' missing position spec e.g /0" + } else { + if {[string index $indexspec 0] ni [list "" "/" "@"]} { + error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' bad index spec '$indexspec'" "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] + } + set triple [list $v $indexspec $positionspec] + } + } + lappend return_triples $triple + incr i + } + proc $cmdname {} [list return $return_triples] + return $return_triples + } + + 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 + } + + # + # + # relatively slow on even small sized scripts + proc arg_is_script_shaped2 {arg} { + set re {^(\s|;|\n)$} + set chars [split $arg ""] + if {[lsearch -regex $chars $re] >=0} { + return 1 + } else { + return 0 + } + } + + #exclude quoted whitespace + proc arg_is_script_shaped {arg} { + if {[tcl::string::first \n $arg] >= 0} { + return 1 + } elseif {[tcl::string::first ";" $arg] >= 0} { + return 1 + } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { + lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found + return [expr {$part2 ne ""}] + } else { + return 0 + } + } + proc _rhs_tail_split {fullrhs} { + set inq 0; set indq 0 + set equalsrhs "" + set i 0 + foreach ch [split $fullrhs ""] { + if {$inq} { + append equalsrhs $ch + if {$ch eq {'}} { + set inq 0 + } + } elseif {$indq} { + append equalsrhs $ch + if {$ch eq {"}} { + set indq 0 + } + } else { + switch -- $ch { + {'} { + set inq 1 + } + {"} { + set indq 1 + } + " " { + #whitespace outside of quoting + break + } + 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z {} + default { + #\t not a literal for switch jumpTable bytecompile - review - can we do it without for example string mapping to (and without a literal binary tab in source file)? + #we can't (reliably?) put \t as one of our switch keys + # + if {$ch eq "\t"} { + break + } + } + } + append equalsrhs $ch + } + incr i + } + set tail [tcl::string::range $fullrhs $i end] + return [list $equalsrhs $tail] + } + + # -- + #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 + + switch -- $next1 { + pipematch { + 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 first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { + #*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 {[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 [_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 [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 <| + #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 $getv]{ + # + #}] + #maintenance - index logic should be similar identical? to to match_assign - which only needs to process atoms because it (for now?) delegates all pipeline ops here, so no vars available (single segment assign) + #tag: positionspechandler + + + #puts stdout "=== list_insertion_script '$positionspec' segmenttail " + set script2 [punk::list_insertion_script $positionspec segmenttail ] + set script2 [string map [list "\$insertion_data" ] $script2] + append insertion_script \n $script2 + + } + append insertion_script \n {set segmenttail} + append insertion_script \n "}" + #puts stderr "$insertion_script" + debug.punk.pipe.compile {creating proc ::punk::pipecmds::insertion::_$rhsmapped } 4 + eval $insertion_script + } + + set segment_members_filled [::punk::pipecmds::insertion::_$rhsmapped $dict_tagval [lindex [list $segmenttail [unset segmenttail]] 0] ] + + #set segment_members_filled $segmenttail + #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) + + } + set rhs [string map $dict_tagval $rhs] ;#obsolete? + + debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 + + + # script index could have changed!!! todo fix! + + #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) + if {(!$segment_first_is_script ) && $segment_op eq ".="} { + #no scriptiness detected + + #debug.punk.pipe.rep {[a yellow bold][rep_listname segment_members_filled][a]} 4 + + set cmdlist_result [uplevel 1 $segment_members_filled] + #debug.punk.pipe {[a green bold]forward_result: $forward_result[a]} 4 + #debug.punk.pipe.rep {[a yellow bold]forward_result REP: [rep $forward_result][a]} 4 + + #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] + + set segment_result [_handle_bind_result $d] + #puts stderr ">>forward_result: $forward_result segment_result $segment_result" + + + } elseif {$segment_op eq "="} { + #slightly different semantics for assigment! + #We index into the DATA - not the position within the segment! + #(an = segment must take a single argument, as opposed to a .= segment) + #(This was a deliberate design choice for consistency with set, and to reduce errors.) + #(we could have allowed multiple args to = e.g to form a list, but it was tried, and the edge-cases were unintuitive and prone to user error) + #(The choice to restrict to single argument, but allow insertion and appending via insertion-specs is more explicit and reliable even though the insertion-specs operate differently to those of .=) + # + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = + # must return: {a b c} not a b c + # + if {!$segment_has_insertions} { + set segment_members_filled $segment_members + if {[dict exists $dict_tagval data]} { + if {![llength $segment_members_filled]} { + set segment_members_filled [dict get $dict_tagval data] + } else { + lappend segment_members_filled [dict get $dict_tagval data] + } + } + } + + set d [_multi_bind_result $returnvarspec [lindex [list $segment_members_filled [unset segment_members_filled ]] 0]] + set segment_result [_handle_bind_result $d] + + + } elseif {$segment_first_is_script || $segment_op eq "script"} { + #script + debug.punk.pipe {[a+ cyan bold].. evaluating as script[a]} 2 + + set script [lindex $segment_members 0] + + #build argument lists for 'apply' + set segmentargnames [list] + set segmentargvals [list] + foreach {k val} $dict_tagval { + if {$k eq "args"} { + #skip args - it is manually added at the end of the apply list if it's a valid tcl list + continue + } + lappend segmentargnames $k + lappend segmentargvals $val + } + + set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list + #puts "------> rep prevr argsdatalist: [rep $argsdatalist]" + set add_argsdata 0 + if {[dict exists $dict_tagval "args"]} { + set argsdatalist [dict get $dict_tagval "args"] + #see if the raw result can be treated as a list + if {[catch {lindex $argsdatalist 0}]} { + #we cannot supply 'args' + set pre_script "" + #todo - only add trace if verbose warnings enabled? + append pre_script "trace add variable args read punk::pipeline_args_read_trace_error\n" + set script $pre_script + append script $segment_first_word + set add_argsdata 0 + } else { + set add_argsdata 1 + } + } + + debug.punk.pipe.rep {>> [rep_listname segmentargvals]} 4 + set ns [uplevel 1 {::namespace current}] + if {!$add_argsdata} { + debug.punk.pipe {APPLY1: (args not set; not a list) segment vars:$segmentargnames} 4 + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals" + set evaluation [uplevel 1 [list ::apply [::list $segmentargnames $script $ns] {*}$segmentargvals]] + } else { + debug.punk.pipe {APPLY2: (args is set)segment vars:$segmentargnames} 4 + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals $argsdatalist" + #pipeline script context should be one below calling context - so upvar v v will work + #ns with leading colon will fail with apply + set evaluation [uplevel 1 [list ::apply [::list [::concat $segmentargnames args] $script $ns] {*}$segmentargvals {*}$argsdatalist]] + } + + debug.punk.pipe.rep {script result, evaluation: [rep_listname evaluation]} 4 + #puts "---> rep script evaluation result: [rep $evaluation]" + #set d [_multi_bind_result $returnvarspec [punk::K $evaluation [unset evaluation]]] + + #trailing segment_members are *pipedata* scripts - as opposed to ordinary pipeline scripts! + set tail_scripts [lrange $segment_members 1 end] + if {[llength $tail_scripts]} { + set r [pipedata $evaluation {*}$tail_scripts] + } else { + set r $evaluation + } + set d [_multi_bind_result $returnvarspec [lindex [list $r [unset r]] 0]] + set segment_result [_handle_bind_result $d] + } else { + #tags ? + #debug.punk.pipe {>>raw commandline: [concat $rhs $segment_members_filled]} 5 + if 0 { + + + + #set s [list uplevel 1 [concat $rhs $segment_members_filled]] + if {![info exists pscript]} { + upvar ::_pipescript pscript + } + if {![info exists pscript]} { + #set pscript $s + set pscript [funcl::o_of_n 1 $segment_members] + } else { + #set pscript [string map [list

$pscript] {uplevel 1 [concat $rhs $segment_members_filled [

]]}] + #set snew "set pipe_$i \[uplevel 1 \[list $rhs $segment_members_filled " + #append snew "set pipe_[expr $i -1]" + #append pscript $snew + set pscript [funcl::o_of_n 1 $segment_members $pscript] + + } + } + set cmdlist_result [uplevel 1 $segment_members_filled] + #set d [_multi_bind_result $returnvarspec [punk::K $segment_members_filled [unset segment_members_filled]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result]] 0 ]] + + #multi_bind_result needs to return a funcl for rhs of: + #lindex [list [set syncvar [main pipeline.. ]] [rhs binding funcl...] 1 ] + #which uses syncvar + # + #The lhs of 'list' runs first so now syncvar can be the root level of the rhs function list and bind the necessary vars. + #NOTE: unintuitively, we are returning the value of rhs to the main pipleline! (leftmost binding) this is because the leftmost binding determines what goes back to the pipeline result + + set segment_result [_handle_bind_result $d] + } + #the subresult doesn't need to go backwards - as the final assignment can emit the result into a variable + #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section + #It may however make a good debug point + #puts stderr "segment $i segment_result:$segment_result" + + debug.punk.pipe.rep {[rep_listname segment_result]} 3 + + + + + + #examine tailremaining. + # either x x x |?> y y y ... + # or just y y y + #we want the x side for next loop + + #set up the conditions for the next loop + #|> x=y args + # inpipespec - contents of previous piper |xxx> + # outpipespec - empty or content of subsequent piper |xxx> + # previous_result + # assignment (x=y) + + + set pipespec($j,in) $pipespec($i,out) + set outpipespec "" + set tailmap "" + set next_pipe_posn -1 + if {[llength $tailremaining]} { + + #set tailmap [lmap v $tailremaining {lreplace [split $v {}] 1 end-1}] + ##e.g for: a b c |> e f g |> h + #set next_pipe_posn [lsearch $tailmap {| >}] + set next_pipe_posn [lsearch $tailremaining "|*>"] + + set outpipespec [string range [lindex $tailremaining $next_pipe_posn] 1 end-1] + } + set pipespec($j,out) $outpipespec + + + set script_like_first_word 0 + if {[llength $tailremaining] || $next_pipe_posn >= 0} { + + if {$next_pipe_posn >=0} { + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for + set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] + + } else { + set next_all_members $tailremaining + set tailremaining [list] + } + + + #assignment is the arg immediately following |> operator e.g x.=blah or x=etc (or a normal commandlist or script!) + set segment_first_word "" + set returnvarspec "" ;# the lhs of x=y + set segment_op "" + set rhs "" + set segment_first_is_script 0 + if {[llength $next_all_members]} { + if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + set segment_first_word [lindex $next_all_members 0] + set segment_first_is_script 1 + set segment_op "" + set segment_members $next_all_members + } else { + set possible_assignment [lindex $next_all_members 0] + #set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} + if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { + set segment_op ".=" + set segment_first_word [lindex $next_all_members 1] + set script_like_first_word [arg_is_script_shaped $segment_first_word] + if {$script_like_first_word} { + set segment_first_is_script 1 ;#relative to segment_members which no longer includes the .= + } + set segment_members [lrange $next_all_members 1 end] + } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { + set segment_op "=" + #never scripts + #must be at most a single element after the = ! + if {[llength $next_all_members] > 2} { + #raise this as pipesyntax as opposed to pipedata? + error "pipesyntax - at most one element can follow = (got [lrange $next_all_members 1 end])" "pipeline $segment_op $returnvarspec $rhs [lrange $next_all_members 1 end]" [list pipesyntax too_many_elements] + } + set segment_first_word [lindex $next_all_members 1] + if {[catch {llength $segment_first_word}]} { + set segment_is_list 0 ;#only used for segment_op = + } else { + set segment_is_list 1 ;#only used for segment_op = + } + + set segment_members $segment_first_word + } else { + #no assignment operator and not script shaped + set segment_op "" + set returnvarspec "" + set segment_first_word [lindex $next_all_members 0] + set segment_first_word [lindex $next_all_members 1] + set segment_members $next_all_members + #puts stderr ">>3 no-operator segment_first_word: '$segment_first_word'" + } + } + + + } else { + #?? two pipes in a row ? + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 + set segment_members return + set segment_first_word return + } + + #set forward_result $segment_result + #JMN2 + set previous_result $segment_result + #set previous_result [join $segment_result] + } else { + debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a]} 4 + #output pipe spec at tail of pipeline + + set pipedvars [dict create] + if {[string length $pipespec($i,out)]} { + set d [apply {{mv res} { + punk::_multi_bind_result $mv $res -levelup 1 + }} $pipespec($i,out) $segment_result] + set segment_result [_handle_bind_result $d] + set pipedvars [dict get $d setvars] + } + + set more_pipe_segments 0 + } + + #the segment_result is based on the leftmost var on the lhs of the .= + #whereas forward_result is always the entire output of the segment + #JMN2 + #lappend segment_result_list [join $segment_result] + lappend segment_result_list $segment_result + incr i + incr j + } ;# end while + + return [lindex $segment_result_list end] + #JMN2 + #return $segment_result_list + #return $forward_result + } + + + #just an experiment + #what advantage/difference versus [llength [lrange $data $start $end]] ??? + proc data_range_length {data start end} { + set datalen [llength $data] + + #normalize to s and e + if {$start eq "end"} { + set s [expr {$datalen - 1}] + } elseif {[string match end-* $start]} { + set stail [string range $start 4 end] + set posn [expr {$datalen - $stail -1}] + if {$posn < 0} { + return 0 + } + set s $posn + } else { + #int + if {($start < 0) || ($start > ($datalen -1))} { + return 0 + } + set s $start + } + if {$end eq "end"} { + set e [expr {$datalen - 1}] + } elseif {[string match end-* $end]} { + set etail [string range $end 4 end] + set posn [expr {$datalen - $etail -1}] + if {$posn < 0} { + return 0 + } + set e $posn + } else { + #int + if {($end < 0)} { + return 0 + } + set e $end + } + if {$s > ($datalen -1)} { + return 0 + } + if {$e > ($datalen -1)} { + set e [expr {$datalen -1}] + } + + + + if {$e < $s} { + return 0 + } + + return [expr {$e - $s + 1}] + } + + # unknown -- + # This procedure is called when a Tcl command is invoked that doesn't + # exist in the interpreter. It takes the following steps to make the + # command available: + # + # 1. See if the autoload facility can locate the command in a + # Tcl script file. If so, load it and execute it. + # 2. If the command was invoked interactively at top-level: + # (a) see if the command exists as an executable UNIX program. + # If so, "exec" the command. + # (b) see if the command requests csh-like history substitution + # in one of the common forms !!, !, or ^old^new. If + # so, emulate csh's history substitution. + # (c) see if the command is a unique abbreviation for another + # command. If so, invoke the command. + # + # Arguments: + # args - A list whose elements are the words of the original + # command, including the command name. + + #review - we shouldn't really be doing this + #We need to work out if we can live with the real default unknown and just inject some special cases at the beginning before falling-back to the normal one + + proc ::unknown args { + #puts stderr "unk>$args" + variable ::tcl::UnknownPending + global auto_noexec auto_noload env tcl_interactive errorInfo errorCode + + if {[info exists errorInfo]} { + set savedErrorInfo $errorInfo + } + if {[info exists errorCode]} { + set savedErrorCode $errorCode + } + + set name [lindex $args 0] + if {![info exists auto_noload]} { + # + # Make sure we're not trying to load the same proc twice. + # + if {[info exists UnknownPending($name)]} { + return -code error "self-referential recursion\ + in \"unknown\" for command \"$name\"" + } + set UnknownPending($name) pending + set ret [catch { + auto_load $name [uplevel 1 {::namespace current}] + } msg opts] + unset UnknownPending($name) + if {$ret != 0} { + dict append opts -errorinfo "\n (autoloading \"$name\")" + return -options $opts $msg + } + if {![array size UnknownPending]} { + unset UnknownPending + } + if {$msg} { + if {[info exists savedErrorCode]} { + set ::errorCode $savedErrorCode + } else { + unset -nocomplain ::errorCode + } + if {[info exists savedErrorInfo]} { + set errorInfo $savedErrorInfo + } else { + unset -nocomplain errorInfo + } + set code [catch {uplevel 1 $args} msg opts] + if {$code == 1} { + # + # Compute stack trace contribution from the [uplevel]. + # Note the dependence on how Tcl_AddErrorInfo, etc. + # construct the stack trace. + # + set errInfo [dict get $opts -errorinfo] + set errCode [dict get $opts -errorcode] + set cinfo $args + if {[string length [encoding convertto utf-8 $cinfo]] > 150} { + set cinfo [string range $cinfo 0 150] + while {[string length [encoding convertto utf-8 $cinfo]] > 150} { + set cinfo [string range $cinfo 0 end-1] + } + append cinfo ... + } + set tail "\n (\"uplevel\" body line 1)\n invoked\ + from within\n\"uplevel 1 \$args\"" + set expect "$msg\n while executing\n\"$cinfo\"$tail" + if {$errInfo eq $expect} { + # + # The stack has only the eval from the expanded command + # Do not generate any stack trace here. + # + dict unset opts -errorinfo + dict incr opts -level + return -options $opts $msg + } + # + # Stack trace is nested, trim off just the contribution + # from the extra "eval" of $args due to the "catch" above. + # + set last [string last $tail $errInfo] + if {$last + [string length $tail] != [string length $errInfo]} { + # Very likely cannot happen + return -options $opts $msg + } + set errInfo [string range $errInfo 0 $last-1] + set tail "\"$cinfo\"" + set last [string last $tail $errInfo] + if {$last < 0 || $last + [string length $tail] != [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo $errInfo $msg + } + set errInfo [string range $errInfo 0 $last-1] + set tail "\n invoked from within\n" + set last [string last $tail $errInfo] + if {$last + [string length $tail] == [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo [string range $errInfo 0 $last-1] $msg + } + set tail "\n while executing\n" + set last [string last $tail $errInfo] + if {$last + [string length $tail] == [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo [string range $errInfo 0 $last-1] $msg + } + return -options $opts $msg + } else { + dict incr opts -level + return -options $opts $msg + } + } + } + #set isrepl [expr {[file tail [file rootname [info script]]] eq "repl"}] + set isrepl [punk::repl::codethread::is_running] ;#may not be reading though + if {$isrepl} { + #set ::tcl_interactive 1 + } + if {$isrepl || (([info level] == 1) && (([info script] eq "" ) ) + && ([info exists tcl_interactive] && $tcl_interactive))} { + if {![info exists auto_noexec]} { + set new [auto_execok $name] + if {$new ne ""} { + set redir "" + if {[namespace which -command console] eq ""} { + set redir ">&@stdout <@stdin" + } + + + #windows experiment todo - use twapi and named pipes + #twapi::namedpipe_server {\\.\pipe\something} + #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones + #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc + # + + if {[string first " " $new] > 0} { + set c1 $name + } else { + set c1 $new + } + + # -- --- --- --- --- + set idlist_stdout [list] + set idlist_stderr [list] + #set shellrun::runout "" + #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks + #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] + #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] + + if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it + #not a trivial task + + #This runs external executables in a context in which they are not attached to a terminal + #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output + #ctrl-c propagation also needs to be considered + + set teehandle punksh + uplevel 1 [list ::catch \ + [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + + if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { + dict set ::tcl::UnknownOptions -code error + set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" + } else { + #no point returning "exitcode 0" if that's the only non-error return. + #It is misleading. Better to return empty string. + set ::tcl::UnknownResult "" + } + } else { + set repl_runid [punk::get_repl_runid] + #set ::punk::last_run_display [list] + + set redir ">&@stdout <@stdin" + uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr + #todo - there is probably no way around this but to somehow exec in the context of a completely separate console + #This is probably a tricky problem - especially to do cross-platform + # + # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit + if {[dict get $::tcl::UnknownOptions -code] == 0} { + set c green + set m "ok" + } else { + set c yellow + set m "errorCode $::errorCode" + } + set chunklist [list] + lappend chunklist [list "info" "[a $c]$m[a] " ] + if {$repl_runid != 0} { + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + + } + + foreach id $idlist_stdout { + shellfilter::stack::remove stdout $id + } + foreach id $idlist_stderr { + shellfilter::stack::remove stderr $id + } + # -- --- --- --- --- + + + #uplevel 1 [list ::catch \ + # [concat exec $redir $new [lrange $args 1 end]] \ + # ::tcl::UnknownResult ::tcl::UnknownOptions] + + #puts "===exec with redir:$redir $::tcl::UnknownResult ==" + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + } + + if {$name eq "!!"} { + set newcmd [history event] + } elseif {[regexp {^!(.+)$} $name -> event]} { + set newcmd [history event $event] + } elseif {[regexp {^\^([^^]*)\^([^^]*)\^?$} $name -> old new]} { + set newcmd [history event -1] + catch {regsub -all -- $old $newcmd $new newcmd} + } + if {[info exists newcmd]} { + tclLog $newcmd + history change $newcmd 0 + uplevel 1 [list ::catch $newcmd \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + + set ret [catch {set candidates [info commands $name*]} msg] + if {$name eq "::"} { + set name "" + } + if {$ret != 0} { + dict append opts -errorinfo \ + "\n (expanding command prefix \"$name\" in unknown)" + return -options $opts $msg + } + # Filter out bogus matches when $name contained + # a glob-special char [Bug 946952] + if {$name eq ""} { + # Handle empty $name separately due to strangeness + # in [string first] (See RFE 1243354) + set cmds $candidates + } else { + set cmds [list] + foreach x $candidates { + if {[string first $name $x] == 0} { + lappend cmds $x + } + } + } + + #punk - disable prefix match search + set default_cmd_search 0 + if {$default_cmd_search} { + if {[llength $cmds] == 1} { + uplevel 1 [list ::catch [lreplace $args 0 0 [lindex $cmds 0]] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + if {[llength $cmds]} { + return -code error "ambiguous command name \"$name\": [lsort $cmds]" + } + } else { + #punk hacked version - report matches but don't run + if {[llength $cmds]} { + return -code error "unknown command name \"$name\": possible match(es) [lsort $cmds]" + } + + } + + + } + return -code error -errorcode [list TCL LOOKUP COMMAND $name] "invalid command name $name" + } + + proc know {cond body} { + set existing [info body ::unknown] + #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) + ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##if {$body ni $existing} { + package require base64 + set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered + #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { + #--------------------------------------- + if {![catch {expr {@c@}} res] && $res} { + debug.punk.unknown {HANDLED BY: punk unknown_handler ([llength $args]) args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + return [eval {@b@}] + } else { + debug.punk.unknown {skipped: punk unknown_handler ([llength $args]) args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + } + #--------------------------------------- + }]$existing + #} + } + + proc know? {{len 2000}} { + puts [string range [info body ::unknown] 0 $len] + } + proc decodescript {b64} { + if {[ catch { + package require base64 + base64::decode $b64 + } scr]} { + return "" + } else { + return "($scr)" + } + } + + # --------------------------- + # commands that should be aliased in safe interps that need to use punk repl + # + proc get_repl_runid {} { + if {[interp issafe]} { + if {[info commands ::tsv::exists] eq ""} { + puts stderr "punk::get_repl_runid cannot operate directly in safe interp - install the appropriate punk aliases" + error "punk::get_repl_runid punk repl aliases not installed" + } + #if safe interp got here - there must presumably be a direct set of aliases on tsv::* commands + } + if {[tsv::exists repl runid]} { + return [tsv::get repl runid] + } else { + return 0 + } + } + #ensure we don't get into loop in unknown when in safe interp - which won't have tsv + proc set_repl_last_unknown {args} { + if {[interp issafe]} { + if {[info commands ::tsv::set] eq ""} { + puts stderr "punk::set_repl_last_unknown cannot operate directly in safe interp - install an alias to tsv::set repl last_unknown" + return + } + #tsv::* somehow working - possibly custom aliases for tsv functionality ? review + } + if {[info commands ::tsv::set] eq ""} { + puts stderr "set_repl_last_unknown - tsv unavailable!" + return + } + tsv::set repl last_unknown {*}$args + } + # --------------------------- + + + proc configure_unknown {} { + #----------------------------- + #these are critical e.g core behaviour or important for repl displaying output correctly + + #---------------- + #for var="val {a b c}" + #proc ::punk::val {{v {}}} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version + proc ::punk::val [list [list v [purelist]]] {return $v} + #---------------- + + #can't use know - because we don't want to return before original unknown body is called. + proc ::unknown {args} [string cat { + package require base64 + #set ::punk::last_run_display [list] + #set ::repl::last_unknown [lindex $args 0] ;#jn + #tsv::set repl last_unknown [lindex $args 0] ;#REVIEW + punk::set_repl_last_unknown [lindex $args 0] + }][info body ::unknown] + + + #handle process return dict of form {exitcode num etc blah} + #ie when the return result as a whole is treated as a command + #exitcode must be the first key + know {[lindex $args 0 0] eq "exitcode"} { + uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]] + } + + + #----------------------------- + # + # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. + + #todo - repl output info that it was evaluated as an expression + #know {[expr $args] || 1} {expr $args} + know {[expr $args] || 1} {tailcall expr $args} + + #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc + #punk::lib::range is defined as a wrapper to lseq if it is available (8.7+) + know {[regexp {^([+-]*[0-9_]+)\.\.([+-]*[0-9_]+)$} [lindex $args 0 0] -> from to]} {punk::lib::range $from $to} + + + #NOTE: + #we don't allow setting namespace qualified vars in the lhs assignment pattern. + #The principle is that we shouldn't be setting vars outside of the immediate calling scope. + #(It would also be difficult and error-prone and generally make the pipelines less re-usable and reliable) + #Therefore ::nswhatever::blah= x is the pipeline: blah= x - where the corresponding command, if any is first resolved in ::nswhatever + #We will require that the namespace already exists - which is consistent with if the command were to be run without unknown + proc ::punk::_unknown_assign_dispatch {matchedon pattern equalsrhs args} { + set tail [lassign $args hd] + #puts "-> _unknown_assign_dispatch '$partzerozero' pattern:'$pattern' equalsrhs:'$equalsrhs' args:'$args' argshd:'$hd' argstail:'$tail'" + if {$hd ne $matchedon} { + if {[llength $tail]} { + error "unknown_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $tail" + } + #regexp $punk::re_assign $hd _ pattern equalsrhs + #we assume the whole pipeline has been provided as the head + #regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail + regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs tail + } + #NOTE: - it doesn't make sense to call 'namespace' qualifiers or 'namespace tail' on a compound hd such as v,::etc= blah + # we only look at leftmost namespace-like thing and need to take account of the pattern syntax + # e.g for ::etc,'::x'= + # the ns is :: and the tail is etc,'::x'= + # (Tcl's namespace qualifiers/tail won't help here) + if {[string match ::* $hd]} { + set patterns [punk::_split_patterns_memoized $hd] + #get a pair-list something like: {::x /0} {etc {}} + set ns [namespace qualifiers [lindex $patterns 0 0]] + set nslen [string length $ns] + set patterntail [string range $ns $nslen end] + } else { + set ns "" + set patterntail $pattern + } + if {[string length $ns] && ![namespace exists $ns]} { + error "unknown_assign_dispatch: namespace '$ns' not found. (Note that pipeline lhs variables cannot be namespaced)" + } else { + set nscaller [uplevel 1 [list ::namespace current]] + #jmn + set rhsmapped [pipecmd_namemapping $equalsrhs] + set commands [uplevel 1 [list ::info commands $pattern=$rhsmapped]] ;#uplevel - or else we are checking from perspective of this namespace ::punk + #we must check for exact match of the command in the list - because command could have glob chars. + if {"$pattern=$rhsmapped" in $commands} { + puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'" + #we call the namespaced function - we don't evaluate it *in* the namespace. + #REVIEW + #warn for now...? + #tailcall $pattern=$equalsrhs {*}$args + tailcall $pattern=$rhsmapped {*}$tail + } + } + #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" + #ignore the namespace.. + #We could interpret the fact that the nonexistant pipe was called with a namespace to indicate that's where the pipecommand should be created.. + #But.. we would need to ensure 1st (compiling) invocation runs the same way as subsequent invocations. + #namespace evaling match_assign here probably wouldn't accomplish that and may create surprises with regards to where lhs vars(if any) are created + tailcall ::punk::match_assign $patterntail $equalsrhs {*}$tail + #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] + } + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) + #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list + #e.g x=a\nb c + #x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained + # + #know {[regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + #know {[regexp {^{([^\t\r\n=]*)\=([^ \t\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + + + + proc ::punk::_unknown_compare {val1 val2 args} { + if {![string length [string trim $val2]]} { + if {[llength $args] > 1} { + #error "Extra args after comparison operator ==. usage e.g : \$var1==\$var2 or \$var1==\$var2 + 2" + set val2 [string cat {*}[lrange $args 1 end]] + return [expr {$val1 eq $val2}] + } + return $val1 + } elseif {[llength $args] == 1} { + #simple comparison + if {[string is digit -strict $val1$val2]} { + return [expr {$val1 == $val2}] + } else { + return [string equal $val1 $val2] + } + } elseif {![catch {expr $val2 {*}[lrange $args 1 end]} evaluated]} { + if {[string is digit -strict $val1$evaluated]} { + return [expr {$val1 == $evaluated}] + } else { + return [expr {$val1 eq $evaluated}] + } + } else { + set evaluated [uplevel 1 [list {*}$val2 {*}[lrange $args 1 end]]] + if {[string is digit -strict $val1$evaluated]} { + return [expr {$val1 == $evaluated}] + } else { + return [expr {$val1 eq $evaluated}] + } + } + } + #ensure == is after = in know sequence + #.* on left is pretty broad - todo: make it a little more specific to avoid unexpected interactions + know {[regexp {(.*)==(.*)} [lindex $args 0] _ val1 val2]} {tailcall ::punk::_unknown_compare $val1 $val2 {*}$args} + #.= must come after = here to ensure it comes before = in the 'unknown' proc + #set punk::re_dot_assign {([^=]*)\.=(.*)} + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] _ varspecs rhs]} { + # set tail [expr {([lindex $args 0] eq [lindex $args 0 0]) ? [lrange $args 1 end] : [concat [lrange [lindex $args 0] 1 end] [lrange $args 1 end] ] }] + # tailcall ::punk::match_exec $varspecs $rhs {*}$tail + # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] + # } + # + + + + proc ::punk::_unknown_dot_assign_dispatch {partzerozero pattern equalsrhs args} { + #puts stderr ". unknown dispatch $partzerozero" + set argstail [lassign $args hd] + + #this equates to auto-flattening the head.. which seems like a bad idea, the structure was there for a reason. + #we should require explicit {*} expansion if the intention is for the args to be joined in at that level. + #expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } + + if {$hd ne $partzerozero} { + if {[llength $argstail]} { + error "unknown_dot_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $argstail" + } + #regexp $punk::re_assign $hd _ pattern equalsrhs + #we assume the whole pipeline has been provided as the head + #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + + regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs argstail + } + #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail + + + return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]] + + } + + # + know {[regexp {^([^\t\r\n=]*)\=([^\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + know {[regexp {^{([^\t\r\n=]*)\=([^\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + + #variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + + #add escaping backslashes to a value + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g + #set ktest {a"b} + #@@[escv $ktest].= list a"b val + #without escv: + #@@"a\\"b".= list a"b val + #with more backslashes in keys the escv use becomes more apparent: + #set ktest {\\x} + #@@[escv $ktest].= list $ktest val + #without escv we would need: + #@@\\\\\\\\x.= list $ktest val + proc escv {v} { + #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically + #thanks to DKF + regsub -all {\W} $v {\\&} + } + interp alias {} escv {} punk::escv + #review + #set v "\u2767" + # + #escv $v + #\ + #the + + + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { + # set argstail [lassign $args hd] + # #set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable! + # #avoid using the return from expr and it works: + # expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } + # + # tailcall ::punk::match_exec $varspecs $rhs {*}$tail + # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] + #} + + } + configure_unknown + #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. + # + + #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc + #Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation. + proc % {args} { + set arglist [lassign $args assign] ;#tail, head + if {$assign eq ".="} { + tailcall {*}[list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] + } + + set is_script [punk::arg_is_script_shaped $assign] + + if {!$is_script && [string index $assign end] eq "="} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + #set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + #set dumbeditor {\}} + if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=\{]*)=$} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax punk::% unable to interpret pipeline '$args'" "% $args" [list pipesyntax unable_to_interpret] + } + } else { + if {$is_script} { + set cmdlist [list ::punk::pipeline "script" "" "" {*}$args] + } else { + set cmdlist [list ::punk::pipeline ".=" "" "" {*}$args] + } + } + tailcall {*}$cmdlist + + + #result-based mismatch detection can probably never work nicely.. + #we need out-of-band method to detect mismatch. Otherwise we can't match on mismatch results! + # + set result [uplevel 1 $cmdlist] + #pipeline result not guaranteed to be a proper list so we can't use list methods to directly look for 'binding mismatch' + #.. but if we use certain string methods - we shimmer the case where the main result is a list + #string match doesn't seem to change the rep.. though it does generate a string rep. + #puts >>1>[rep $result] + if {[catch {lrange $result 0 1} first2wordsorless]} { + #if we can't get as a list then it definitely isn't the semi-structured 'binding mismatch' + return $result + } else { + if {$first2wordsorless eq {binding mismatch}} { + error $result + } else { + #puts >>2>[rep $result] + return $result + } + } + } + + proc ispipematch {args} { + expr {[lindex [uplevel 1 [list pipematch {*}$args]] 0] eq "ok"} + } + + #pipe initiator which will never raise an error *except for pipesyntax* , but always returns {ok {result something}} or {error {mismatch something}} or, for tcl errors {error {reason something}} + proc pipematch {args} { + #debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2 + variable re_dot_assign + variable re_assign + + set arglist [lassign $args assign] + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + # set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + # set dumbeditor {\}} + if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=]*)=.*} $assign _ returnvarspecs]} { + set cmdlist [list $assign {*}$arglist] + #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax punk::pipematch unable to interpret pipeline '$args'" "pipematch $args" [pipesyntax unable_to_interpret] + } + } else { + set cmdlist $args + #script? + #set cmdlist [list ::punk::pipeline .= "" "" {*}$args] + } + + if {[catch {uplevel 1 $cmdlist} result erroptions]} { + #puts stderr "pipematch erroptions:$erroptions" + #debug.punk.pipe {pipematch error $result} 4 + set ecode [dict get $erroptions -errorcode] + switch -- [lindex $ecode 0] { + binding { + if {[lindex $ecode 1] eq "mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + #return [dict create error [dict create mismatch $result]] + #puts stderr "pipematch converting error to {error {mismatch }}" + return [list error [list mismatch $result]] + } + } + pipesyntax { + #error $result + return -options $erroptions $result + } + casematch { + return $result + } + } + #return [dict create error [dict create reason $result]] + return [list error [list reason $result]] + } else { + return [list ok [list result $result]] + #debug.punk.pipe {pipematch result $result } 4 + #return [dict create ok [dict create result $result]] + } + } + + proc pipenomatchvar {varname args} { + if {[string first = $varname] >=0} { + #first word "pipesyntax" is looked for by pipecase + error "pipesyntax pipenomatch expects a simple varname as first argument" "pipenomatchvar $varname $args" [list pipesyntax expected_simple_varname] + } + #debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2 + + set assign [lindex $args 0] + set arglist [lrange $args 1 end] + if {[string first = $assign] >= 0} { + variable re_dot_assign + variable re_assign + #what if we get passed a script block containing = ?? e.g {error x=a} + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + } elseif {[regexp $re_dot_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs $rhs {*}$arglist] + } elseif {[regexp $re_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::pipeline = $returnvarspecs $rhs {*}$arglist] + } else { + debug.punk.pipe {[a+ yellow bold] Unexpected arg following pipenomatchvar variable [a]} 0 + set cmdlist $args + #return [dict create error [dict create reason [dict create pipematch bad_first_word value $assign pipeline [list pipematch $assign {*}$args]]]] + } + } else { + set cmdlist $args + } + + upvar 1 $varname nomatchvar + if {[catch {uplevel 1 $cmdlist} result erroptions]} { + set ecode [dict get $erroptions -errorcode] + debug.punk.pipe {[a+ yellow bold]pipematchnomatch error $result[a]} 3 + if {[lindex $ecode 0] eq "pipesyntax"} { + set errordict [dict create error [dict create pipesyntax $result]] + set nomatchvar $errordict + return -options $erroptions $result + } + if {[lrange $ecode 0 1] eq "binding mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + set errordict [dict create error [dict create mismatch $result]] + set nomatchvar $errordict + return -options $erroptions $result + } + set errordict [dict create error [dict create reason $result]] + set nomatchvar $errordict + #re-raise the error for pipeswitch to deal with + return -options $erroptions $result + } else { + debug.punk.pipe {pipematchnomatch result $result } 4 + set nomatchvar "" + #uplevel 1 [list set $varname ""] + #return raw result only - to pass through to pipeswitch + return $result + #return [dict create ok [dict create result $result]] + } + } + + #should only raise an error for pipe syntax errors - all other errors should be wrapped + proc pipecase {args} { + #debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9 + set arglist [lassign $args assign] + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + #set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + set cmdlist [list ::= {*}$arglist] + } elseif {![punk::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + #set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + #set dumbeditor {\}} + + if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=]*)=.*} $assign _ returnvarspecs]} { + set cmdlist [list $assign {*}$arglist] + #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax pipecase unable to interpret pipeline '$args'" + } + #todo - account for insertion-specs e.g x=* x.=/0* + } else { + #script? + set cmdlist [list ::punk::pipeline .= "" "" {*}$args] + } + + + if {[catch {uplevel 1 [list ::if 1 $cmdlist]} result erroptions]} { + #puts stderr "====>>> result: $result erroptions" + set ecode [dict get $erroptions -errorcode] + switch -- [lindex $ecode 0] { + pipesyntax { + #error $result + return -options $erroptions $result + } + casenomatch { + return -options $erroptions $result + } + binding { + if {[lindex $ecode 1] eq "mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + #return [dict create error [dict create mismatch $result]] + # + #NOTE: casemismatch is part of the api for pipecase. It is a casemismatch rather than an error - because for a pipecase - a casemismatch is an expected event (many casemismatches - one match) + return [dict create casemismatch $result] + } + } + } + + #we can't always treat $result as a list - may be an error string which can't be represented as a list, and there may be no useful errorCode + #todo - use errorCode instead + if {[catch {lindex $result 0} word1]} { + #tailcall error $result + return -options $erroptions $result + } else { + switch -- $word1 { + switcherror - funerror { + error $result "pipecase [lsearch -all -inline $args "*="]" + } + resultswitcherror - resultfunerror { + #recast the error as a result without @@ok wrapping + #use the tailcall return to stop processing other cases in the switch! + tailcall return [dict create error $result] + } + ignore { + #suppress error, but use normal return + return [dict create error [dict create suppressed $result]] + } + default { + #normal tcl error + #return [dict create error [dict create reason $result]] + tailcall error $result "pipecase $args" [list caseerror] + } + } + } + } else { + tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] + } + + } + + #note that pipeswitch deliberately runs in callers scope to have direct access to variables - it is akin to a control structure. + #It also - somewhat unusually accepts args - which we provide as 'switchargs' + #This is unorthodox/risky in that it will clobber any existing var of that name in callers scope. + #Solve using documentation.. consider raising error if 'switchargs' already exists, which would require user to unset switchargs in some circumstances. + proc pipeswitch {pipescript args} { + #set nextargs $args + #unset args + #upvar args upargs + #set upargs $nextargs + upvar switchargs switchargs + set switchargs $args + uplevel 1 [::list ::if 1 $pipescript] + } + #static-closure version - because we shouldn't be writing back to calling context vars directly + #Tcl doesn't (2023) have mutable closures - but for functional pipeline composition - we probably don't want that anyway! + #pipeswitchc is preferable to pipeswitch in that we can access context without risk of affecting it, but is less performant. (particularly in global scope.. but that probably isn't an important usecase) + proc pipeswitchc {pipescript args} { + set binding {} + if {[info level] == 1} { + #up 1 is global + set get_vars [list info vars] + } else { + set get_vars [list info locals] + } + set vars [uplevel 1 {*}$get_vars] + set posn [lsearch $vars switchargs] + set vars [lreplace $vars $posn $posn] + foreach v $vars { + upvar 1 $v var + if {(![array exists var]) && [info exists var]} { + lappend binding [list $v $var] ;#values captured as defaults for apply args. + } + } + lappend binding [list switchargs $args] + apply [list $binding $pipescript [uplevel 1 {::namespace current}]] + } + + proc pipedata {data args} { + #puts stderr "'$args'" + set r $data + for {set i 0} {$i < [llength $args]} {incr i} { + set e [lindex $args $i] + #review: string is list is as slow as catch {llength $e} - and also affects ::errorInfo unlike other string is commands. bug/enhancement report? + if {![string is list $e]} { + #not a list - assume script and run anyway + set r [apply [list {data} $e] $r] + } else { + if {[llength $e] == 1} { + switch -- $e { + > { + #output to calling context. only pipedata return value and '> varname' should affect caller. + incr i + uplevel 1 [list set [lindex $args $i] $r] + } + % - pipematch - ispipematch { + incr i + set e2 [lindex $args $i] + #set body [list $e {*}$e2] + #append body { $data} + + set body [list $e {*}$e2] + append body { {*}$data} + + + set applylist [list {data} $body] + #puts stderr $applylist + set r [apply $applylist $r] + } + pipeswitch - pipeswitchc { + #pipeswitch takes a script not a list. + incr i + set e2 [lindex $args $i] + set body [list $e $e2] + #pipeswitch takes 'args' - so expand $data when in pipedata context + append body { {*}$data} + #use applylist instead of uplevel when in pipedata context! + #can use either switchdata/data but not vars in calling context of 'pipedata' command. + #this is consistent with pipeswitch running in a % / .= pipeline which can only access vars in immediate calling context. + set applylist [list {data} $body] + #puts stderr $applylist + set r [apply $applylist $r] + } + default { + #puts "other single arg: [list $e $r]" + append e { $data} + set r [apply [list {data} $e] $r] + } + } + } elseif {[llength $e] == 0} { + #do nothing - pass data through + #leave r as is. + } else { + set r [apply [list {data} $e] $r] + } + } + } + return $r + } + + + proc scriptlibpath {{shortname {}} args} { + upvar ::punk::config::running running_config + set scriptlib [dict get $running_config scriptlib] + if {[string match "lib::*" $shortname]} { + set relpath [string map [list "lib::" "" "::" "/"] $shortname] + set relpath [string trimleft $relpath "/"] + set fullpath $scriptlib/$relpath + } else { + set shortname [string trimleft $shortname "/"] + set fullpath $scriptlib/$shortname + } + return $fullpath + } + + + #useful for aliases e.g treemore -> xmore tree + proc xmore {args} { + if {[llength $args]} { + uplevel #0 [list {*}$args | more] + } else { + error "usage: punk::xmore args where args are run as {*}\$args | more" + } + } + + + #environment path as list + # + #return *appendable* pipeline - i.e no args via <| + proc path_list_pipe {{glob *}} { + if {$::tcl_platform(platform) eq "windows"} { + set sep ";" + } else { + # : ok for linux/bsd ... mac? + set sep ":" + } + set cond [string map [list $glob] {expr {[string length $item] && [string match $item]}}] + #env members such as ''path' not case sensitive on windows - but are on some other platforms (at least FreeBSD) + return [list .= set ::env(PATH) |> .=>2 string trimright $sep |> .=>1 split $sep |> list_filter_cond $cond ] + } + proc path_list {{glob *}} { + set pipe [punk::path_list_pipe $glob] + {*}$pipe + } + proc path {{glob *}} { + set pipe [punk::path_list_pipe $glob] + {*}$pipe |> list_as_lines + } + + #------------------------------------------------------------------- + #sh 'test' equivalent - to be used with exitcode of process + # + + #single evaluation to get exitcode + proc sh_test {args} { + set a1 [lindex $args 0] + if {$a1 in [list -b -c -d -e -f -h -L -s -S -x -w]} { + set a2 [lindex $args 1] + if {![catch { + set attrinfo [file attributes $a2] + } errM]} { + if {[dict exists $attrinfo -vfs] && [dict get $attrinfo -vfs] == 1} { + puts stderr "WARNING: external 'test' being called on vfs path. External command will probably not have access to the vfs. Use 'TEST' for Tcl view of vfs mounted filesystems." + } + } + } + tailcall run test {*}$args + } + + #whether v is an integer from perspective of unix test command. + #can be be bigger than a tcl int or wide ie bignum - but must be whole number + #test doesn't handle 1.0 - so we shouldn't auto-convert + proc is_sh_test_integer {v} { + if {[string first . $v] >=0 || [string first e $v] >= 0} { + return false + } + #if it is double but not sci notation and has no dots - then we can treat as a large integer for 'test' + if {[string is double -strict $v]} { + return true + } else { + return false + } + } + #can use double-evaluation to get true/false + #faster tcl equivalents where possible to accuratley provide, and fallthrough to sh for compatibility of unimplemented + #The problem with fallthrough is that sh/bash etc have a different view of existant files + #e.g unix files such as /dev/null vs windows devices such as CON,PRN + #e.g COM1 is mapped as /dev/ttyS1 in wsl (?) + #Note also - tcl can have vfs mounted file which will appear as a directory to Tcl - but a file to external commands! + #We will stick with the Tcl view of the file system. + #User can use their own direct calls to external utils if + #Note we can't support $? directly in Tcl - script would have to test ${?} or use [set ?] + proc sh_TEST {args} { + upvar ? lasterr + set lasterr 0 + set a1 [lindex $args 0] + set a2 [lindex $args 1] + set a3 [lindex $args 2] + set fileops [list -b -c -d -e -f -h -L -s -S -x -w] + if {[llength $args] == 1} { + #equivalent of -n STRING + set boolresult [expr {[string length $a1] != 0}] + } elseif {[llength $args] == 2} { + if {$a1 in $fileops} { + if {$::tcl_platform(platform) eq "windows"} { + #e.g trailing dot or trailing space + if {[punk::winpath::illegalname_test $a2]} { + #protect with \\?\ to stop windows api from parsing + #will do nothing if already prefixed with \\?\ + + set a2 [punk::winpath::illegalname_fix $a2] + } + } + } + switch -- $a1 { + -b { + #dubious utility on FreeBSD, windows? + #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' + #Linux apparently uses them though + if{[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "blockSpecial"}] + } else { + set boolresult false + } + } + -c { + #e.g on windows CON,NUL + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "characterSpecial"}] + } else { + set boolresult false + } + } + -d { + set boolresult [file isdirectory $a2] + } + -e { + set boolresult [file exists $a2] + } + -f { + #e.g on windows CON,NUL + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "file"}] + } else { + set boolresult false + } + } + -h - + -L { + set boolresult [expr {[file type $a2] eq "link"}] + } + -s { + set boolresult [expr {[file exists $a2] && ([file size $a2] > 0 )}] + } + -S { + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "socket"}] + } else { + set boolresult false + } + } + -x { + set boolresult [expr {[file exists $a2] && [file executable $a2]}] + } + -w { + set boolresult [expr {[file exists $a2] && [file writable $a2]}] + } + -z { + set boolresult [expr {[string length $a2] == 0}] + } + -n { + set boolresult [expr {[string length $a2] != 0}] + } + default { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + } + } + } elseif {[llength $args] == 3} { + switch -- $a2 { + "=" { + #test does string comparisons + set boolresult [string equal $a1 $a3] + } + "!=" { + #string comparison + set boolresult [expr {$a1 ne $a3}] + } + "-eq" { + #test expects a possibly-large integer-like thing + #shell scripts will + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 == $a3}] + } + "-ge" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 >= $a3}] + } + "-gt" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 > $a3}] + } + "-le" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 <= $a3}] + } + "-lt" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 < $a3}] + } + "-ne" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 != $a3}] + } + default { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + + } + } + } else { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + } + + #normalize 1,0 etc to true,false + #we want to make it obvious we are not just reporting exitcode 0 for example - which represents true in tcl. + if {$boolresult} { + return true + } else { + if {$lasterr == 0} { + set lasterr 1 + } + return false + } + + + } + proc sh_echo {args} { + tailcall run echo {*}$args + } + proc sh_ECHO {args} { + #execute the result of the run command - which is something like: 'exitcode n' - to get true/false + tailcall apply {arglist {uplevel #0 [run echo {*}$arglist]} ::} $args + } + + + #sh style true/false for process exitcode. 0 is true - everything else false + proc exitcode {args} { + set c [lindex $args 0] + if {[string is integer -strict $c]} { + #return [expr {$c == 0}] + #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true + if {$c == 0} { + return true + } else { + return false + } + } else { + return false + } + } + #------------------------------------------------------------------- + + namespace export help aliases alias exitcode % pipedata pipecase pipeline pipematch pipeswitch pipeswitchc pipecase linelist linesort inspect list_as_lines val treemore + + #namespace ensemble create + + + + + #tilde + #These aliases work fine for interactive use - but the result is always a string int-rep + #interp alias {} ~ {} file join $::env(HOME) ;#HOME must be capitalized to work cross platform (lowercase home works on windows - but probably not elsewhere) + #interp alias {} ~ {} apply {args {file join $::env(HOME) $args}} + proc ~ {args} { + set hdir [punk::objclone $::env(HOME)] + file pathtype $hdir + set d $hdir + #use the file join 2-arg optimisation to avoid losing path-rep - probably doesn't give any advantage on all Tcl versions + foreach a $args { + set d [file join $d $a] + } + file pathtype $d + return [punk::objclone $d] + } + interp alias {} ~ {} punk::~ + + + #maint - punk::args has similar + #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args + #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? + #JMN + #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values + #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. + #only supports -flag val pairs, not solo options + #If an option is supplied multiple times - only the last value is used. + #TODO - remove + proc get_leading_opts_and_values {defaults rawargs args} { + if {[llength $defaults] %2 != 0} { + error "get_leading_opts_and_values expected first argument 'defaults' to be a dictionary" + } + dict for {k v} $defaults { + if {![string match -* $k]} { + error "get_leading_opts_and_values problem with supplied defaults. Expect each key to begin with a dash. Got key '$k'" + } + } + #puts "--> [info frame -2] <--" + set cmdinfo [dict get [info frame -2] cmd] + #we can't treat cmdinfo as a list - it may be something like {command {*}$args} in which case lindex $cmdinfo 0 won't work + #hopefully first word is a plain proc name if this function was called in the normal manner - directly from a proc + #we will break at first space and assume the lhs of that will give enough info to be reasonable - (alternatively we could use entire cmdinfo - but it might be big and ugly) + set caller [regexp -inline {\S+} $cmdinfo] + + #if called from commandline or some other contexts such as outside of a proc in a namespace - caller may just be "namespace" + if {$caller eq "namespace"} { + set caller "get_leading_opts_and_values called from namespace" + } + + # ------------------------------ + if {$caller ne "get_leading_opts_and_values"} { + #check our own args + lassign [get_leading_opts_and_values {-anyopts 0 -minvalues 0 -maxvalues -1} $args] _o ownopts _v ownvalues + if {[llength $ownvalues] > 0} { + error "get_leading_opts_and_values expected: a dictionary of defaults, a list of args and at most two option pairs -minvalues and -maxvalues - got extra arguments: '$ownvalues'" + } + set opt_minvalues [dict get $ownopts -minvalues] + set opt_maxvalues [dict get $ownopts -maxvalues] + set opt_anyopts [dict get $ownopts -anyopts] + } else { + #don't check our own args if we called ourself + set opt_minvalues 0 + set opt_maxvalues 0 + set opt_anyopts 0 + } + # ------------------------------ + + if {[set eopts [lsearch $rawargs "--"]] >= 0} { + set values [lrange $rawargs $eopts+1 end] + set arglist [lrange $rawargs 0 $eopts-1] + } else { + if {[lsearch $rawargs -*] >= 0} { + #to support option values with leading dash e.g -offset -1 , we can't just take the last flagindex + set i 0 + foreach {k v} $rawargs { + if {![string match -* $k]} { + break + } + if {$i+1 >= [llength $rawargs]} { + #no value for last flag + error "bad options for $caller. No value supplied for last option $k" + } + incr i 2 + } + set arglist [lrange $rawargs 0 $i-1] + set values [lrange $rawargs $i end] + } else { + set values $rawargs ;#no -flags detected + set arglist [list] + } + } + if {$opt_maxvalues == -1} { + #only check min + if {[llength $values] < $opt_minvalues} { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected at least $opt_minvalues" + } + } else { + if {[llength $values] < $opt_minvalues || [llength $values] > $opt_maxvalues} { + if {$opt_minvalues == $opt_maxvalues} { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected exactly $opt_minvalues" + } else { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected between $opt_minvalues and $opt_maxvalues inclusive" + } + } + } + + if {!$opt_anyopts} { + set checked_args [dict create] + for {set i 0} {$i < [llength $arglist]} {incr i} { + #allow this to error out with message indicating expected flags + dict set checked_args [tcl::prefix match -message "options for $caller. Unexpected option" [dict keys $defaults] [lindex $arglist $i]] [lindex $arglist $i+1] + incr i ;#skip val + } + } else { + set checked_args $arglist + } + set opts [dict merge $defaults $checked_args] + + #maintain order of opts $opts values $values as caller may use lassign. + return [dict create opts $opts values $values] + } + + + + + + + + + + #-------------------------------------------------- + #some haskell-like operations + #group equivalent + #http://zvon.org/other/haskell/Outputlist/group_f.html + #as we can't really distinguish a single element list from a string we will use 2 functions + proc group_list1 {lst} { + set out [list] + set prev [lindex $lst 0] + set g [list] + foreach i $lst { + if {$i eq $prev} { + lappend g $i + } else { + lappend out $g + set g [list $i] + } + set prev $i + } + lappend out $g + return $out + } + proc group_list {lst} { + set out [list] + set next [lindex $lst 1] + set tail [lassign $lst x] + set g [list $x] + set y [lindex $tail 0] + set last_condresult [expr {$x}] + set n 1 ;#start at one instead of zero for lookahead + foreach x $tail { + set y [lindex $tail $n] + set condresult [expr {$x}] + if {$condresult eq $last_condresult} { + lappend g $x + } else { + lappend out $g + set g [list $x] + set last_condresult $condresult + } + incr n + } + lappend out $g + return $out + } + + #NOT attempting to match haskell other than in overall concept. + # + #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. + #Haskell seems to take an entire lambda so varnames can be user-specified - but the 'magic' there is in it's choice of submitting 2 elements at a time + #We could do similar .. but we'll focus on comprehensibility for the basic cases - especially as begginning and end of list issues could be confusing. + # + #vars: index prev, prev0, prev1, item, next, next0, next1,nextr, cond + #(nextr is a bit obscure - but basically means next-repeat ie if no next - use same value. just once though.) + #group by cond result or first 3 wordlike parts of error + #e.g group_list_by {[lindex $item 0]} {{a 1} {a 2} {b 1}} + proc group_list_by {cond lst} { + set out [list] + set prev [list] + set next [lindex $lst 1] + set tail [lassign $lst item] + set g [list $item] + set next [lindex $tail 0] + if {$prev eq ""} { + set prev0 0 + set prev1 1 + set prevr $item + } else { + set prev0 $prev + set prev1 $prev + set prevr $prev + } + if {$next eq ""} { + set next0 0 + set next1 1 + set nextr $item + } else { + set next0 $next + set next1 $next + set nextr $next + } + set last_condresult [apply {{index cond prev prev0 prev1 prevr item next next0 next1 nextr} { + if {[catch {expr $cond} r]} { + puts stderr "index: 0 ERROR $r" + set wordlike_parts [regexp -inline -all {\S+} $r] + set r [list ERROR {*}[lrange $wordlike_parts 0 2]] + } + set r + } + } 0 $cond $prev $prev0 $prev1 $prevr $item $next $next0 $next1 $nextr] + set n 1 ;#start at one instead of zero for lookahead + #note - n also happens to matchi zero-based index of original list + set prev $item + foreach item $tail { + set next [lindex $tail $n] + if {$prev eq ""} { + set prev0 0 + set prev1 1 + set prevr $item + } else { + set prev0 $prev + set prev1 $prev + set prevr $prev + } + if {$next eq ""} { + set next0 0 + set next1 1 + set nextr $item + } else { + set next0 $next + set next1 $next + set nextr $next + } + set condresult [apply {{index cond prev prev0 prev1 prevr item next next0 next1 nextr} { + if {[catch {expr $cond} r]} { + puts stderr "index: $index ERROR $r" + set wordlike_parts [regexp -inline -all {\S+} $r] + set r [list ERROR {*}[lrange $wordlike_parts 0 2]] + } + set r + } + } $n $cond $prev $prev0 $prev1 $prevr $item $next $next0 $next1 $nextr] + if {$condresult eq $last_condresult} { + lappend g $item + } else { + lappend out $g + set g [list $item] + set last_condresult $condresult + } + incr n + set prev $item + } + lappend out $g + return $out + } + + #group_numlist ? preserve representation of numbers rather than use string comparison? + + + # - group_string + #.= punk::group_string "aabcccdefff" + # aa b ccc d e fff + proc group_string {str} { + lmap v [group_list [split $str ""]] {string cat {*}$v} + } + + #lists may be of unequal lengths + proc transpose_lists {list_rows} { + set res {} + #set widest [pipedata $list_rows {lmap v $data {llength $v}} {tcl::mathfunc::max {*}$data}] + set widest [tcl::mathfunc::max {*}[lmap v $list_rows {llength $v}]] + for {set j 0} {$j < $widest} {incr j} { + set newrow {} + foreach oldrow $list_rows { + if {$j >= [llength $oldrow]} { + continue + } else { + lappend newrow [lindex $oldrow $j] + } + } + lappend res $newrow + } + return $res + } + proc transpose_strings {list_of_strings} { + set charlists [lmap v $list_of_strings {split $v ""}] + set tchars [transpose_lists $charlists] + lmap v $tchars {string cat {*}$v} + } + + package require struct::matrix + #transpose a serialized matrix using the matrix command + #Note that we can have missing row values below and to right + #e.g + #a + #a b + #a + proc transpose_matrix {matrix_rows} { + set mcmd [struct::matrix] + #serialization format: numcols numrows rowlist + set widest [tcl::mathfunc::max {*}[lmap v $matrix_rows {llength $v}]] + $mcmd deserialize [list [llength $matrix_rows] $widest $matrix_rows] + $mcmd transpose + set result [lindex [$mcmd serialize] 2] ;#strip off dimensions + $mcmd destroy + return $result + } + + set objname [namespace current]::matrixchain + if {$objname ni [info commands $objname]} { + oo::class create matrixchain { + variable mcmd + constructor {matrixcommand} { + puts "wrapping $matrixcommand with [self]" + set mcmd $matrixcommand + } + destructor { + puts "matrixchain destructor called for [self] (wrapping $mcmd)" + $mcmd destroy + } + method unknown {args} { + if {[llength $args]} { + switch -- [lindex $args 0] { + add - delete - insert - transpose - sort - set - swap { + $mcmd {*}$args + return [self] ;#result is the wrapper object for further chaining in pipelines + } + default { + tailcall $mcmd {*}$args + } + } + } else { + #will error.. but we should pass that on + tailcall $mcmd + } + } + } + } + + #review + #how do we stop matrix pipelines from leaving commands around? i.e how do we call destroy on the matrixchain wrapper if not explicitly? + #Perhaps will be solved by: Tip 550: Garbage collection for TclOO + #Theoretically this should allow tidy up of objects created within the pipeline automatically + #If the object name is placed in the pipeline variable dict then it should survive across segment apply scripts and only go out of scope at the end. + proc matrix_command_from_rows {matrix_rows} { + set mcmd [struct::matrix] + set widest [tcl::mathfunc::max {*}[lmap v $matrix_rows {llength $v}]] + $mcmd deserialize [list [llength $matrix_rows] $widest $matrix_rows] + #return $mcmd + set wrapper [punk::matrixchain new $mcmd] + } + + #-------------------------------------------------- + + proc list_filter_cond {itemcond listval} { + set filtered_list [list] + set binding {} + if {[info level] == 1} { + #up 1 is global + set get_vars [list ::info vars] + } else { + set get_vars [list ::info locals] + } + set vars [uplevel 1 {*}$get_vars] + set posn [lsearch $vars item] + set vars [lreplace $vars $posn $posn] + foreach v $vars { + upvar 1 $v var + if {(![array exists var]) && [info exists var]} { + lappend binding [list $v $var] ;#values captured as defaults for apply args. + } + } + #lappend binding [list item $args] + + #puts stderr "binding: [join $binding \n]" + #apply [list $binding $pipescript [uplevel 1 ::namespace current]] + foreach item $listval { + set bindlist [list {*}$binding [list item $item]] + if {[apply [list $bindlist $itemcond [uplevel 1 ::namespace current]] ]} { + lappend filtered_list $item + } + } + return $filtered_list + } + + + proc ls {args} { + if {![llength $args]} { + set args [list [pwd]] + } + if {[llength $args] ==1} { + return [glob -nocomplain -tails -dir [lindex $args 0] *] + } else { + set result [dict create] + foreach a $args { + set k [file normalize $a] + set contents [glob -nocomplain -tails -dir $a *] + dict set result $k $contents + } + return $result + } + } + + + + #linelistraw is essentially split $text \n so is only really of use for pipelines, where the argument order is more convenient + #like linelist - but keeps leading and trailing empty lines + #single \n produces {} {} + #the result can be joined to reform the arg if a single arg supplied + # + proc linelistraw {args} { + set linelist [list] + foreach {a} $args { + set nlsplit [split $a \n] + lappend linelist {*}$nlsplit + } + #return [split $text \n] + return $linelist + } + proc linelist1 {args} { + set linelist [list] + foreach {a} $args { + set nlsplit [split $a \n] + set start 0 + set end "end" + + if {[lindex $nlsplit 0] eq ""} { + set start 1 + } + if {[lindex $nlsplit end] eq ""} { + set end "end-1" + } + set alist [lrange $nlsplit $start $end] + lappend linelist {*}$alist + } + return $linelist + } + + + #An implementation of a notoriously controversial metric. + proc LOC {args} { + set argspecs [subst { + -dir -default "\uFFFF" + -exclude_dupfiles -default 1 -type boolean + -exclude_punctlines -default 1 -type boolean + -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } + }] + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + + # -- --- --- --- --- --- + set opt_dir [dict get $opts -dir] + if {$opt_dir eq "\uFFFF"} { + set opt_dir [pwd] ;#pwd can take over a ms on windows in a not terribly deep path even with SSDs - so as a general rule we don't use it in the original defaults list + } + # -- --- --- --- --- --- + set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] + set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars + set opt_punctchars [dict get $opts -punctchars] + # -- --- --- --- --- --- + + + set filepaths [punk::path::treefilenames -dir $opt_dir {*}$searchspecs] + set loc 0 + set dupfileloc 0 + set seentails [list] + set dupfilecount 0 + set extensions [list] + set purepunctlines 0 + foreach fpath $filepaths { + set isdupfile 0 + set floc 0 + set fpurepunctlines 0 + set ext [file extension $fpath] + if {$ext ni $extensions} { + lappend extensions $ext + } + if {!$opt_exclude_punctlines} { + set floc [llength [linelist -line {trimright} -block {trimall} [fcat $fpath]]] + } else { + set lines [linelist -line {trimright} -block {trimall} [fcat $fpath]] + set mapawaypunctuation [list] + foreach p $opt_punctchars empty {} { + lappend mapawaypunctuation $p $empty + } + foreach ln $lines { + if {[string length [string trim [string map $mapawaypunctuation $ln]]] > 0} { + incr floc + } else { + incr fpurepunctlines + } + } + } + if {[file tail $fpath] in $seentails} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + } + if {!$isdupfile || ($isdupfile && !$opt_exclude_dupfiles)} { + incr loc $floc + incr purepunctlines $fpurepunctlines + } + + lappend seentails [file tail $fpath] + } + if {$opt_exclude_punctlines} { + return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions purepunctuationlines $purepunctlines] + } + return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions] + } + + + + #!!!todo fix - linedict is unfinished and non-functioning + #linedict based on indents + proc linedict {args} { + set data [lindex $args 0] + set opts [lrange $args 1 end] ;#todo + set nlsplit [split $data \n] + set rootindent -1 + set stepindent -1 + + #set wordlike_parts [regexp -inline -all {\S+} $lastitem] + set d [dict create] + set keys [list] + set i 1 + set firstkeyline "N/A" + set firststepline "N/A" + foreach ln $nlsplit { + if {![string length [string trim $ln]]} { + incr i + continue + } + set is_rootkey 0 + regexp {(\s*)(.*)} $ln _ space linedata + puts stderr ">>line:'$ln' [string length $space] $linedata" + set this_indent [string length $space] + if {$rootindent < 0} { + set firstkeyline $ln + set rootindent $this_indent + } + if {$this_indent == $rootindent} { + set is_rootkey 1 + } + if {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline" + } + if {$is_rootkey} { + dict set d $linedata {} + lappend keys $linedata + } else { + if {$stepindent < 0} { + set stepindent $this_indent + set firststepline $ln + } + if {$this_indent == $stepindent} { + dict set d [lindex $keys end] $ln + } else { + if {($this_indent % $stepindent) != 0} { + error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline" + } + + #todo fix! + set parentkey [lindex $keys end] + lappend keys [list $parentkey $ln] + set oldval [dict get $d $parentkey] + if {[string length $oldval]} { + set new [dict create $oldval $ln] + } else { + dict set d $parentkey $ln + } + + } + } + incr i + } + return $d + } + proc dictline {d} { + puts stderr "unimplemented" + set lines [list] + + return $lines + } + + + proc ooinspect {obj} { + set obj [uplevel 1 [list namespace which -command $obj]] + set isa [lmap type {object class metaclass} { + if {![info object isa $type $obj]} continue + set type + }] + foreach tp $isa { + switch -- $tp { + class { + lappend info {class superclasses} {class mixins} {class filters} + lappend info {class methods} {class methods} + lappend info {class variables} {class variables} + } + object { + lappend info {object class} {object mixins} {object filters} + lappend info {object methods} {object methods} + lappend info {object variables} {object variables} + lappend info {object namespace} {object vars} ;#{object commands} + } + } + } + + set result [dict create isa $isa] + foreach args $info { + dict set result $args [info {*}$args $obj] + foreach opt {-private -all} { + catch { + dict set result [list {*}$args $opt] [info {*}$args $obj $opt] + } + } + } + dict filter $result value {?*} + } + + + #pipeline inspect + #e.g + #= {a z c} |> inspect -label input_dict |> lsort |> {inspect $data} + proc inspect {args} { + set defaults [list -label "" -limit 20 -channel stderr -showcount 1 -ansi 1] + set flags [list] + set endoptsposn [lsearch $args --] ;#first -- if data expected to contain --, then should always be called with --. e.g inspect -- + if {$endoptsposn >= 0} { + set flags [lrange $args 0 $endoptsposn-1] + set pipeargs [lrange $args $endoptsposn+1 end] + } else { + #no explicit end of opts marker + #last trailing elements of args after taking *known* -tag v pairs is the value to inspect + for {set i 0} {$i < [llength $args]} {incr i} { + set k [lindex $args $i] + if {$k in [dict keys $defaults]} { + lappend flags {*}[lrange $args $i $i+1] + incr i + } else { + #first unrecognised option represents end of flags + break + } + } + set pipeargs [lrange $args $i end] + } + foreach {k v} $flags { + if {$k ni [dict keys $defaults]} { + error "inspect: unknown option $k. Known options: [dict keys $defaults]. If data contains flaglike elements, consider calling with end-of-opts marker. e.g inspect --" + } + } + set opts [dict merge $defaults $flags] + # -- --- --- --- --- + set label [dict get $opts -label] + set channel [dict get $opts -channel] + set showcount [dict get $opts -showcount] + if {[string length $label]} { + set label "${label}: " + } + set limit [dict get $opts -limit] + set opt_ansi [dict get $opts -ansi] + switch -- [string tolower $opt_ansi] { + 0 - 1 - 2 {} + view {set opt_ansi 2} + default { + error "inspect -ansi 0|1|2|view - received -ansi $opt_ansi" + } + } + # -- --- --- --- --- + + set more "" + if {[llength $pipeargs] == 1} { + #usual case is data as a single element + set val [lindex $pipeargs 0] + set count 1 + } else { + #but the pipeline segment could have an insertion-pattern ending in * + set val $pipeargs + set count [llength $pipeargs] + } + switch -- [string tolower $channel] { + nul - null - /dev/null { + return $val + } + } + set displayval $val ;#default - may be overridden based on -limit + + if {$count > 1} { + #val is a list + set llen [llength $val] + if {$limit > 0 && ($limit < $llen)} { + set displayval [lrange $val 0 $limit-1] + if {$llen > $limit} { + set more "..." + } + } + } else { + #not a valid tcl list - limit by lines + if {$limit > 0} { + set rawlines [split $val \n] + set llen [llength $rawlines] + set displaylines [lrange $rawlines 0 $limit-1] + set displayval [join $displaylines "\n"] + if {$llen > $limit} { + set more "\n..." + } + } + + } + if {$showcount} { + set displaycount "[a purple bold]($count)[a] " + if {$showcount} { + set countspace [expr {[string length $count] + 3}] ;#lhs margin size of count number plus brackets and one space + set margin [string repeat " " $countspace] + set displayval [string map [list \r "" \n "\n$margin"] $displayval] + } + } else { + set displaycount "" + } + if {$opt_ansi == 0} { + set displayval [punk::ansi::ansistrip $displayval] + } elseif {$opt_ansi == 2} { + set displayval [ansistring VIEW $displayval] + } + if {![string length $more]} { + puts $channel "$displaycount$label[a green bold]$displayval[a]" + } else { + puts $channel "$displaycount$label[a green bold]$displayval[a yellow bold]$more[a]" + } + return $val + } + + + + #return list of {chan chunk} elements + proc help_chunks {args} { + set chunks [list] + set linesep [string repeat - 76] + set mascotblock "" + catch { + package require patternpunk + #lappend chunks [list stderr [>punk . rhs]] + append mascotblock [textblock::frame [>punk . banner -title "Punk Shell" -left Tcl -right [package provide Tcl]]] + } + + set topic [lindex $args end] + set argopts [lrange $args 0 end-1] + + + set text "" + append text "Punk core navigation commands:\n" + + #todo - load from source code annotation? + set cmdinfo [list] + lappend cmdinfo [list help "This help. To see available subitems type: help topics"] + lappend cmdinfo [list dev "(ensemble command to make new projects/modules and to generate docs)"] + lappend cmdinfo [list a? "view ANSI colours\n e.g a? web"] + lappend cmdinfo [list ./ "view/change directory"] + lappend cmdinfo [list ../ "go up one directory"] + lappend cmdinfo [list ./new "make new directory and switch to it"] + lappend cmdinfo [list n/ "view/change namespace (accepts ns path globs e.g **::*get* to match comands at any level )"] + lappend cmdinfo [list n// "view/change namespace (with command listing)"] + lappend cmdinfo [list nn/ "go up one namespace"] + lappend cmdinfo [list n/new "make child namespace and switch to it"] + + set cmds [lsearch -all -inline -index 0 -subindices $cmdinfo *] + set descr [lsearch -all -inline -index 1 -subindices $cmdinfo *] + set widest1 [tcl::mathfunc::max {*}[lmap v $cmds {string length $v}]] + set widest2 [tcl::mathfunc::max {*}[lmap v $descr {string length $v}]] + set t [textblock::class::table new -show_seps 0] + foreach c $cmds d $descr { + #append text " [overtype::left $col1 $c][overtype::left $col2 $d]" \n + $t add_row [list $c $d] + } + set widest1 [$t column_datawidth 0] + $t configure_column 0 -minwidth [expr {$widest1 + 2}] + set widest2 [$t column_datawidth 1] + $t configure_column 1 -minwidth [expr {$widest2 + 1}] + append text [$t print] + + + set warningblock "" + + if {[catch {package require textblock} errM]} { + set introblock $mascotblock + append introblock \n $text + append warningblock \n "WARNING: textblock package couldn't be loaded. Side-by-side display not available" + + } else { + set introblock [textblock::join -- " " \n$mascotblock " " $text] + } + + + lappend chunks [list stdout $introblock] + + + if {$topic in [list tcl]} { + if {[punk::lib::system::has_script_var_bug]} { + append warningblock \n "minor warning: punk::lib::system::has_script_var_bug returned true! (string rep for list variable in script generated when script changed)" + } + if {[punk::lib::system::has_safeinterp_compile_bug]} { + set indent " " + append warningblock \n "[a+ web-red]warning: punk::lib::system::has_safeinterp_compile_bug returned true!" \n + append warningblock "${indent}(ensemble commands not compiled in safe interps - heavy performance impact in safe interps)" \n + append warningblock "${indent}see https://core.tcl-lang.org/tcl/tktview/1095bf7f75" + append warningblock [a] + } + } + + set text "" + if {$topic in [list env environment]} { + #todo - move to punk::config? + upvar ::punk::config::punk_env_vars_config punkenv_config + upvar ::punk::config::other_env_vars_config otherenv_config + + set known_punk [dict keys $punkenv_config] + set known_other [dict keys $otherenv_config] + append text \n + set usetable 1 + if {$usetable} { + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + if {"windows" eq $::tcl_platform(platform)} { + #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. + #The Tcl ::env array is linked to the underlying process view of the environment + #- but info exists ::env(var) can misreport as true if it has been deleted by setting to empty string rather than using unset. + #an 'array get' will resynchronise. + #Even if an env variable didn't exist before - setting it to empty string can get it to this inconsistent state. + array get ::env + } + #do an array read on ::env + foreach {v vinfo} $punkenv_config { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + set help "" + if {[dict exists $vinfo help]} { + set help [dict get $vinfo help] + } + $t add_row [list $v $c2 $help] + } + $t configure_column 0 -headers [list "Punk environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} + + set punktable [$t print] + $t destroy + + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + foreach {v vinfo} $otherenv_config { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + $t add_row [list $v $c2] + } + $t configure_column 0 -headers [list "Other environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} + + set othertable [$t print] + $t destroy + append text [textblock::join -- $punktable " " $othertable]\n + } else { + + append text $linesep\n + append text "punk environment vars:\n" + append text $linesep\n + set col1 [string repeat " " 25] + set col2 [string repeat " " 50] + foreach v $known_punk { + set c1 [overtype::left $col1 $v] + if {[info exists ::env($v)]} { + set c2 [overtype::left $col2 [set ::env($v)] + } else { + set c2 [overtype::right $col2 "(NOT SET)"] + } + append text "$c1 $c2\n" + } + append text $linesep\n + } + + lappend chunks [list stdout $text] + } + + if {$topic in [list console terminal]} { + lappend cstring_tests [dict create\ + type "PM "\ + msg "PRIVACY MESSAGE"\ + f7 punk::ansi::controlstring_PM\ + f7desc "7bit ESC ^"\ + f8 punk::ansi::controlstring_PM8\ + f8desc "8bit \\x9e"\ + ] + lappend cstring_tests [dict create\ + type SOS\ + msg "STRING"\ + f7 punk::ansi::controlstring_SOS\ + f7desc "7bit ESC X"\ + f8 punk::ansi::controlstring_SOS8\ + f8desc "8bit \\x98"\ + ] + lappend cstring_tests [dict create\ + type APC\ + msg "APPLICATION PROGRAM COMMAND"\ + f7 punk::ansi::controlstring_APC\ + f7desc "7bit ESC _"\ + f8 punk::ansi::controlstring_APC8\ + f8desc "8bit \\x9f"\ + ] + + foreach test $cstring_tests { + set m [[dict get $test f7] [dict get $test msg]] + set hidden_width_m [punk::console::test_char_width $m] + set m8 [[dict get $test f8] [dict get $test msg]] + set hidden_width_m8 [punk::console::test_char_width $m8] + if {$hidden_width_m != 0 || $hidden_width_m8 != 0} { + if {$hidden_width_m == 0} { + set d "[a+ green bold][dict get $test f7desc] [a red]${m}[a]" + } else { + set d "[a+ yellow bold][dict get $test f7desc] [a red]$m[a]" + } + if {$hidden_width_m8 == 0} { + set d8 "[a+ green ][dict get $test f8desc] [a red]$m8[a]" + } else { + set d8 "[a+ yellow bold][dict get $test f8desc] [a red]$m8[a]" + } + append warningblock \n "WARNING: terminal doesn't hide all [dict get $test type] control strings: $d $d8" + } + } + } + + lappend chunks [list stderr $warningblock] + if {$topic in [list topics help]} { + set text "" + set topics [dict create\ + "topics|help" "List help topics"\ + "tcl" "Tcl version warnings"\ + "env|environment" "punkshell environment vars"\ + "console|terminal" "Some console behaviour tests and warnings"\ + ] + + set t [textblock::class::table new -show_seps 0] + $t add_column -headers [list "Topic"] + $t add_column + foreach {k v} $topics { + $t add_row [list $k $v] + } + set widest0 [$t column_datawidth 0] + $t configure_column 0 -minwidth [expr {$widest0 + 4}] + append text \n[$t print] + + lappend chunks [list stdout $text] + } + + return $chunks + } + proc help {args} { + set chunks [help_chunks {*}$args] + foreach chunk $chunks { + lassign $chunk chan text + puts -nonewline $chan $text + } + } + proc mode {{raw_or_line query}} { + package require punk::console + tailcall ::punk::console::mode $raw_or_line + } + + #this hides windows cmd's mode command - probably no big deal - anyone who needs it will know how to exec it. + interp alias {} mode {} punk::mode + + + #NOTE: an alias may match in a namespace - but not have a corresponding command that matches that name (alias renamed) + proc aliases {{glob *}} { + set ns [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command + set ns_mapped [string map {:: \uFFFF} $ns] + #puts stderr "aliases ns: $ns_mapped" + set segments [split $ns_mapped \uFFFF] ;#include empty string before leading :: + if {![string length [lindex $segments end]]} { + #special case for :: only include leading segment rather thatn {} {} + set segments [lrange $segments 0 end-1] + } + set segcount [llength $segments] ;#only match number of segments matching current ns + + + set all_aliases [interp aliases {}] + set matched [list] + foreach a $all_aliases { + #normalize with leading :: + if {![string match ::* $a]} { + set abs ::$a + } else { + set abs $a + } + + set asegs [split [string map {:: \uFFFF} $abs] \uFFFF] + set acount [llength $asegs] + #puts "alias $abs acount:$acount asegs:$asegs segcount:$segcount segments: $segments" + if {[expr {$acount - 1}] == $segcount} { + if {[lrange $asegs 0 end-1] eq $segments} { + if {[string match $glob [lindex $asegs end]]} { + #report this alias in the current namespace - even though there may be no matching command + lappend matched $a ;#add raw alias token which may or may not have leading :: + } + } + } + } + #set matched_abs [lsearch -all -inline $all_aliases $glob] + + return $matched + } + + proc alias {{aliasorglob ""} args} { + set nsthis [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command + if {[llength $args]} { + if {$aliasorglob in [interp aliases ""]} { + set existing [interp alias "" $aliasorglob] + puts stderr "Overwriting existing alias $aliasorglob -> $existing with $aliasorglob -> $args (in current session only)" + } + if {([llength $args] ==1) && [string trim [lindex $args 0]] eq ""} { + #use empty string/whitespace as intention to delete alias + return [interp alias "" $aliasorglob ""] + } + return [interp alias "" $aliasorglob "" {*}$args] + } else { + if {![string length $aliasorglob]} { + set aliaslist [punk::aliases] + puts -nonewline stderr $aliaslist + return + } + #we need to first check for exact match of alias that happens to have glob chars i.e the supplied aliasorglob looks like a glob but is actually directly an alias + set target [interp alias "" $aliasorglob] + if {[llength $target]} { + return $target + } + + if {([string first "*" $aliasorglob] >= 0) || ([string first "?" $aliasorglob] >= 0)} { + set aliaslist [punk::aliases $aliasorglob] + puts -nonewline stderr $aliaslist + return + } + return [list] + } + } + + #pipeline-toys - put in lib/scriptlib? + ##geometric mean + #alias gmean .=> llength |> expr 1.0 / |e> .=i>* tcl::mathop::* |> .=>1,e>3 expr ** {::tcl::tm::add {*}$data; set path} |> inspect -label added_to_module_path <0/#| + + + + + + #interp alias {} c {} clear ;#external executable 'clear' may not always be available + #todo - review + interp alias {} clear {} ::punk::reset + interp alias {} c {} ::punk::reset + proc reset {} { + if {[llength [info commands ::punk::repl::reset_terminal]]} { + #punk::repl::reset_terminal notifies prompt system of reset + punk::repl::reset_terminal + } else { + puts -nonewline stdout [punk::ansi::reset] + } + } + + + + #fileutil::cat except with checking for windows illegal path names (when on windows platform) + interp alias {} fcat {} punk::mix::util::fcat + + #---------------------------------------------- + interp alias {} linelistraw {} punk::linelistraw + + # 'path' collides with kettle path in kettle::doc function - todo - patch kettle? + interp alias {} PATH {} punk::path + + interp alias {} path_list {} punk::path_list + interp alias {} list_filter_cond {} punk::list_filter_cond + + + interp alias {} inspect {} punk::inspect + interp alias {} ooinspect {} punk::ooinspect + + interp alias {} linedict {} punk::linedict + interp alias {} dictline {} punk::dictline + + #todo - pipepure - evaluate pipeline in a slave interp without commands that have side-effects. (safe interp?) + interp alias {} % {} punk::% + interp alias {} pipeswitch {} punk::pipeswitch + interp alias {} pipeswitchc {} punk::pipeswitchc ;#closure version - more correct + interp alias {} pipecase {} punk::pipecase + interp alias {} pipematch {} punk::pipematch + interp alias {} ispipematch {} punk::ispipematch + interp alias {} pipenomatchvar {} punk::pipenomatchvar + interp alias {} pipedata {} punk::pipedata + interp alias {} pipeset {} punk::pipeset + interp alias {} pipealias {} punk::pipealias + interp alias {} listset {} punk::listset ;#identical to pipeset + + + #non-core aliases + interp alias {} is_list_all_in_list {} punk::lib::is_list_all_in_list + interp alias {} is_list_all_ni_list {} punk::libis_list_all_ni_list + + + + #interp alias {} = {} ::punk::pipeline = "" "" + #interp alias {} = {} ::punk::match_assign "" "" + interp alias {} .= {} ::punk::pipeline .= "" "" + #proc .= {args} { + # #uplevel 1 [list ::punk::pipeline .= "" "" {*}$args] + # tailcall ::punk::pipeline .= "" "" {*}$args + #} + + + interp alias {} rep {} ::tcl::unsupported::representation + interp alias {} dis {} ::tcl::unsupported::disassemble + + + + # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion + interp alias {} l {} sh_runout -n ls -A ;#plain text listing + #interp alias {} ls {} sh_runout -n ls -AF --color=always + interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less + #note that shell globbing with * won't work on unix systems when using unknown/exec + interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) + interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. + # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? + #interp alias {} lw {} ls -aFv --color=always + + interp alias {} dir {} shellrun::runconsole dir + + # punk::nav::fs + package require punk::nav::fs + interp alias {} ./ {} punk::nav::fs::d/ + interp alias {} ../ {} punk::nav::fs::dd/ + interp alias {} d/ {} punk::nav::fs::d/ + interp alias {} dd/ {} punk::nav::fs::dd/ + + interp alias {} vwd {} punk::nav::fs::vwd ;#return punk::nav::fs::VIRTUAL_CWD - and report to stderr pwd if different + interp alias {} dirlist {} punk::nav::fs::dirlist + interp alias {} dirfiles {} punk::nav::fs::dirfiles + interp alias {} dirfiles_dict {} punk::nav::fs::dirfiles_dict + + interp alias {} ./new {} punk::nav::fs::d/new + interp alias {} d/new {} punk::nav::fs::d/new + interp alias {} ./~ {} punk::nav::fs::d/~ + interp alias {} d/~ {} punk::nav::fs::d/~ + interp alias "" x/ "" punk::nav::fs::x/ + + + if {$::tcl_platform(platform) eq "windows"} { + set has_powershell 1 + interp alias {} dl {} dir /q + interp alias {} dw {} dir /W/D + } else { + #todo - natsorted equivalent + #interp alias {} dl {} + interp alias {} dl {} puts stderr "not implemented" + interp alias {} dw {} puts stderr "not implemented" + #todo - powershell detection on other platforms + set has_powershell 0 + } + if {$has_powershell} { + #see also powershell runspaces etc: + # powershell runspaces e.g $rs=[RunspaceFactory]::CreateRunspace() + # $ps = [Powershell]::Create() + + interp alias {} ps {} exec >@stdout pwsh -nolo -nop -c + interp alias {} psx {} runx -n pwsh -nop -nolo -c + interp alias {} psr {} run -n pwsh -nop -nolo -c + interp alias {} psout {} runout -n pwsh -nop -nolo -c + interp alias {} pserr {} runerr -n pwsh -nop -nolo -c + interp alias {} psls {} shellrun::runconsole pwsh -nop -nolo -c ls + interp alias {} psps {} shellrun::runconsole pwsh -nop -nolo -c ps + } else { + set ps_missing "powershell missing (powershell is open source and can be installed on windows and most unix-like platforms)" + interp alias {} ps {} puts stderr $ps_missing + interp alias {} psx {} puts stderr $ps_missing + interp alias {} psr {} puts stderr $ps_missing + interp alias {} psout {} puts stderr $ps_missing + interp alias {} pserr {} puts stderr $ps_missing + interp alias {} psls {} puts stderr $ps_missing + interp alias {} psps {} puts stderr $ps_missing + } + proc psencode {cmdline} { + + } + proc psdecode {encodedcmd} { + + } + + proc repl {startstop} { + switch -- $startstop { + stop { + if {[punk::repl::codethread::is_running]} { + puts stdout "Attempting repl stop. Try ctrl-c or exit command to leave interpreter" + set ::repl::done 1 + } + } + start { + if {[punk::repl::codethread::is_running]} { + repl::start stdin + } + } + default { + error "repl unknown action '$startstop' - must be start or stop" + } + } + } + +} + + +# -- --- --- --- +#Load decks. commandset packages are not loaded until the deck is called. +# -- --- --- --- +package require punk::mod +#punk::mod::cli set_alias pmod +punk::mod::cli set_alias app + +#todo - change to punk::dev +package require punk::mix +punk::mix::cli set_alias dev +punk::mix::cli set_alias deck ;#deprecate! + +#todo - add punk::deck for managing cli modules and commandsets + +package require punkcheck::cli +punkcheck::cli set_alias pcheck +punkcheck::cli set_alias punkcheck +# -- --- --- --- + +package provide punk [namespace eval punk { + #FUNCTL + variable version + set version 0.1 +}] + + + diff --git a/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/bootsupport/modules/punk/aliascore-0.1.0.tm new file mode 100644 index 00000000..83c02d0b --- /dev/null +++ b/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -0,0 +1,272 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.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) 2024 +# +# @@ Meta Begin +# Application punk::aliascore 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::aliascore 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::aliascore] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::aliascore +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::aliascore +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::aliascore::class { +# #*** !doctools +# #[subsection {Namespace punk::aliascore::class}] +# #[para] class definitions +# if {[info commands [namespace current]::interface_sample1] eq ""} { +# #*** !doctools +# #[list_begin enumerated] +# +# # oo::class create interface_sample1 { +# # #*** !doctools +# # #[enum] CLASS [class interface_sample1] +# # #[list_begin definitions] +# +# # method test {arg1} { +# # #*** !doctools +# # #[call class::interface_sample1 [method test] [arg arg1]] +# # #[para] test method +# # puts "test: $arg1" +# # } +# +# # #*** !doctools +# # #[list_end] [comment {-- end definitions interface_sample1}] +# # } +# +# #*** !doctools +# #[list_end] [comment {--- end class enumeration ---}] +# } +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::aliascore { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + variable aliases + #use absolute ns ie must be prefixed with :: + #single element commands are imported if source command already exists, otherwise aliased. multi element commands are aliased + + #functions must be in export list of their source namespace + set aliases [tcl::dict::create\ + tstr ::punk::lib::tstr\ + list_as_lines ::punk::lib::list_as_lines\ + lines_as_list ::punk::lib::lines_as_list\ + linelist ::punk::lib::linelist\ + linesort ::punk::lib::linesort\ + pdict ::punk::lib::pdict\ + plist {::punk::lib::pdict -roottype list}\ + showlist {::punk::lib::showdict -roottype list}\ + showdict ::punk::lib::showdict\ + ansistrip ::punk::ansi::ansistrip\ + stripansi ::punk::ansi::ansistrip\ + ansiwrap ::punk::ansi::ansiwrap\ + colour ::punk::console::colour\ + ansi ::punk::console::ansi\ + color ::punk::console::colour\ + a+ ::punk::console::code_a+\ + A+ {::punk::console::code_a+ forcecolour}\ + a ::punk::console::code_a\ + A {::punk::console::code_a forcecolour}\ + a? ::punk::console::code_a?\ + A? {::punk::console::code_a? forcecolor}\ + smcup ::punk::console::enable_alt_screen\ + rmcup ::punk::console::disable_alt_screen\ + ] + + #*** !doctools + #[subsection {Namespace punk::aliascore}] + #[para] Core API functions for punk::aliascore + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + #todo - options as to whether we should raise an error if collisions found, undo aliases etc? + proc init {args} { + set defaults {-force 0} + set opts [dict merge $defaults $args] + set opt_force [dict get $opts -force] + + variable aliases + if {!$opt_force} { + set existing [list] + set conflicts [list] + foreach {a cmd} $aliases { + if {[tcl::info::commands ::$a] ne ""} { + lappend existing $a + if {[llength $cmd] > 1} { + #use alias mechanism + set existing_target [interp alias "" $a] + } else { + #using namespace import + #check origin + set existing_target [tcl::namespace::origin $cmd] + } + if {$existing_target ne $cmd} { + #command exists in global ns but doesn't match our defined aliases/imports + lappend conflicts $a + } + } + } + if {[llength $conflicts]} { + error "punk::aliascore::init declined to create any aliases or imports because -force == 0 and conflicts found:$conflicts" + } + } + set tempns ::temp_[info cmdcount] ;#temp ns for renames + dict for {a cmd} $aliases { + #puts "aliascore $a -> $cmd" + if {[llength $cmd] > 1} { + interp alias {} $a {} {*}$cmd + } else { + if {[tcl::info::commands $cmd] ne ""} { + #todo - ensure exported? noclobber? + if {[tcl::namespace::tail $a] eq [tcl::namespace::tail $cmd]} { + #puts stderr "importing $cmd" + tcl::namespace::eval :: [list namespace import $cmd] + } else { + #target command name differs from exported name + #e.g stripansi -> punk::ansi::ansistrip + #import and rename + #puts stderr "importing $cmd (with rename to ::$a)" + tcl::namespace::eval $tempns [list namespace import $cmd] + catch {rename ${tempns}::[namespace tail $cmd] ::$a} + } + } else { + interp alias {} $a {} {*}$cmd + } + } + } + #tcl::namespace::delete $tempns + return [dict keys $aliases] + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::aliascore ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#interp alias {} list_as_lines {} punk::lib::list_as_lines +#interp alias {} lines_as_list {} punk::lib::lines_as_list +#interp alias {} ansistrip {} punk::ansi::ansistrip ;#review +#interp alias {} linelist {} punk::lib::linelist ;#critical for = assignment features +#interp alias {} linesort {} punk::lib::linesort + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::aliascore::lib { + namespace export {[a-z]*} ;# Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace punk::aliascore::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::aliascore::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval punk::aliascore::system { + #*** !doctools + #[subsection {Namespace punk::aliascore::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::aliascore [namespace eval punk::aliascore { + variable pkg punk::aliascore + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/punk/config-0.1.tm b/src/bootsupport/modules/punk/config-0.1.tm new file mode 100644 index 00000000..206b560b --- /dev/null +++ b/src/bootsupport/modules/punk/config-0.1.tm @@ -0,0 +1,475 @@ + +tcl::namespace::eval punk::config { + variable loaded + variable startup ;#include env overrides + variable running + variable punk_env_vars + variable other_env_vars + + variable vars + + namespace export {[a-z]*} + + #todo - XDG_DATA_HOME etc + #https://specifications.freedesktop.org/basedir-spec/latest/ + # see also: http://hiphish.github.io/blog/2020/08/30/dotfiles-were-a-mistake/ + + proc init {} { + variable defaults + variable startup + variable running + variable punk_env_vars + variable punk_env_vars_config + variable other_env_vars + variable other_env_vars_config + + set exename "" + catch { + #catch for safe interps + #safe base will return empty string, ordinary safe interp will raise error + set exename [tcl::info::nameofexecutable] + } + if {$exename ne ""} { + set exefolder [file dirname $exename] + #default file logs to logs folder at same level as exe if writable, or empty string + set log_folder [file normalize $exefolder/../logs] + #tcl::dict::set startup scriptlib $exefolder/scriptlib + #tcl::dict::set startup apps $exefolder/../../punkapps + + #todo - use punk main.tcl location instead - exefolder doesn't work if system tclsh used etc + set default_scriptlib $exefolder/scriptlib + set default_apps $exefolder/../../punkapps + if {[file isdirectory $log_folder] && [file writable $log_folder]} { + #tcl::dict::set startup logfile_stdout $log_folder/repl-exec-stdout.txt + #tcl::dict::set startup logfile_stderr $log_folder/repl-exec-stderr.txt + set default_logfile_stdout $log_folder/repl-exec-stdout.txt + set default_logfile_stderr $log_folder/repl-exec-stderr.txt + } else { + set default_logfile_stdout "" + set default_logfile_stderr "" + } + } else { + #probably a safe interp - which cannot access info nameofexecutable even if access given to the location via punk::island + #review - todo? + #tcl::dict::set startup scriptlib "" + #tcl::dict::set startup apps "" + set default_scriptlib "" + set default_apps "" + set default_logfile_stdout "" + set default_logfile_stderr "" + } + + # auto_exec_mechanism ;#whether to use exec instead of experimental shellfilter::run + + #optional channel transforms on stdout/stderr. + #can sometimes be useful to distinguish eventloop stdout/stderr writes compared to those triggered directly from repl commands + #If no distinction necessary - should use default_color_ + #The counterpart: default_color__repl is a transform that is added and removed with each repl evaluation. + #startup color_stdout - parameters as suitable for punk::ansi::a+ (test with 'punk::ansi::a?') e.g "cyan bold" ;#not a good idea to default + set default_color_stdout brightwhite ;#stdout colour including background calls (after etc) + set default_color_stdout_repl "" ;#stdout colour applied during direct repl call only + #This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful. + #set default_color_stderr "red bold" + #set default_color_stderr "web-lightsalmon" + set default_color_stderr yellow ;#limit to basic colours for wider terminal support. yellow = term-olive + set default_color_stderr_repl "" ;#during repl call only + + set homedir "" + if {[catch { + #depending on which build of tcl - some safe interps prior to bugfix https://core.tcl-lang.org/tcl/info/3aa487993f will return a homedir value in an unmodified safe interp + #other 'safe' interps may have explicitly made this available - we shouldn't override that decision here using interp issafe so we can't compensate for versions which shouldn't really be returning this in the safe interp + set homedir [file home] + } errM]} { + #tcl 8.6 doesn't have file home.. try again + if {[info exists ::env(HOME)]} { + set homedir $::env(HOME) + } + } + + + # per user xdg vars + # --- + set default_xdg_config_home "" ;#config data - portable + set default_xdg_data_home "" ;#data the user likely to want to be portable + set default_xdg_cache_home "" ;#local cache + set default_xdg_state_home "" ;#persistent user data such as logs, but not as important or as portable as those in xdg_data_home + # --- + set default_xdg_data_dirs "" ;#non-user specific + #xdg_config_dirs ? + #xdg_runtime_dir ? + + + #review. we are assuming if we can't get a home dir - then all the xdg vars including xdg_data_dirs aren't likely to be useful (as presumably filesystem access is absent) + #(safe interp generally won't have access to ::env either) + #This coupling doesn't necessarily hold - its possible the relevant env vars were copied to a safe interp - although that would be a policy that would make disabling 'info home' inconsistent. + if {$homedir ne ""} { + if {"windows" eq $::tcl_platform(platform)} { + #as much as I'd prefer to use ~/.local/share and ~/.config to keep them more consistent with unixlike platforms - the vast majority of apps put them where microsoft wants them. + #we have a choice of LOCALAPPDATA vs APPDATA (local to machine vs potentially roaming/redirected in a corporate environment) + #using the roaming location should not impact users who aren't using a domain controller but is potentially much more convenient for those who do. + if {[info exists ::env(APPDATA)]} { + set default_xdg_config_home $::env(APPDATA) + set default_xdg_data_home $::env(APPDATA) + } + + #The xdg_cache_home should be kept local + if {[info exists ::env(LOCALAPPDATA)]} { + set default_xdg_cache_home $::env(LOCALAPPDATA) + set default_xdg_state_home $::env(LOCALAPPDATA) + } + + if {[info exists ::env(PROGRAMDATA)]} { + #- equiv env(ALLUSERSPROFILE) ? + set default_xdg_data_dirs $::env(PROGRAMDATA) + } + + } else { + #follow defaults as specified on freedesktop.org e.g https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html + set default_xdg_config_home [file join $homedir .config] + set default_xdg_data_home [file join $homedir .local share] + set default_xdg_cache_home [file join $homedir .cache] + set default_xdg_state_home [file join $homedir .local state] + set default_xdg_data_dirs /usr/local/share + } + } + + set defaults [dict create\ + apps $default_apps\ + config ""\ + configset ".punkshell"\ + scriptlib $default_scriptlib\ + color_stdout $default_color_stdout\ + color_stdout_repl $default_color_stdout_repl\ + color_stderr $default_color_stderr\ + color_stderr_repl $default_color_stderr_repl\ + logfile_stdout $default_logfile_stdout\ + logfile_stderr $default_logfile_stderr\ + logfile_active 0\ + syslog_stdout "127.0.0.1:514"\ + syslog_stderr "127.0.0.1:514"\ + syslog_active 0\ + auto_exec_mechanism exec\ + auto_noexec 0\ + xdg_config_home $default_xdg_config_home\ + xdg_data_home $default_xdg_data_home\ + xdg_cache_home $default_xdg_cache_home\ + xdg_state_home $default_xdg_state_home\ + xdg_data_dirs $default_xdg_data_dirs\ + theme_posh_override ""\ + posh_theme ""\ + posh_themes_path ""\ + ] + + set startup $defaults + #load values from saved config file - $xdg_config_home/punk/punk.config ? + #typically we want env vars to override the stored config - as env vars conventionally used on some commandlines. + #that's possibly ok for the PUNK_ vars + #however.. others like the xdg vars and NOCOLOR may apply to other apps.. and we may want to override them from the saved config? + #making some env vars override saved config values and some not would be potentially confusing. may need one/more specific settings or env vars to determine which takes precedence? + #simpler is probably just to let env vars take precedence - and warn when saving or viewing config that the saved values are being overridden + #- requiring user to manually unset any unwanted env vars when launching? + + #we are likely to want the saved configs for subshells/decks to override them however. + + #todo - load/save config file + + #todo - define which configvars are settable in env + #list of varname varinfo where varinfo is a sub dictionary (type key is mandatory, with value from: string,pathlist,boolean) + set punk_env_vars_config [dict create \ + PUNK_APPS {type pathlist}\ + PUNK_CONFIG {type string}\ + PUNK_CONFIGSET {type string}\ + PUNK_SCRIPTLIB {type string}\ + PUNK_AUTO_EXEC_MECHANISM {type string}\ + PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"}\ + PUNK_COLOR_STDERR {type string help "stderr colour transform. Use 'punk::ansi::a?' to see colour names"}\ + PUNK_COLOR_STDERR_REPL {type string help "stderr colour transform only while command running (not active during 'after')"}\ + PUNK_COLOR_STDOUT {type string help "stdout colour transform. Use 'punk::ansi::a?' to see colour names"}\ + PUNK_COLOR_STDOUT_REPL {type string help "stdout colour transform only while command running (not active during 'after')"}\ + PUNK_LOGFILE_STDOUT {type string}\ + PUNK_LOGFILE_STDERR {type string}\ + PUNK_LOGFILE_ACTIVE {type string}\ + PUNK_SYSLOG_STDOUT {type string}\ + PUNK_SYSLOG_STDERR {type string}\ + PUNK_SYSLOG_ACTIVE {type string}\ + PUNK_THEME_POSH_OVERRIDE {type string}\ + ] + set punk_env_vars [dict keys $punk_env_vars_config] + + #override with env vars if set + foreach {evar varinfo} $punk_env_vars_config { + if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] + set f [set ::env($evar)] + if {$f ne "default"} { + #e.g PUNK_SCRIPTLIB -> scriptlib + set varname [tcl::string::tolower [tcl::string::range $evar 5 end]] + if {$vartype eq "pathlist"} { + #colon vs semicolon path sep is problematic for windows environments where unix-like systems such as cygwin/wsl are used and a variable may be set for either the native path separator or the unix-like system + #Even without the colon vs semicolon issue, native vs unix-like paths on windows mixed environment systems can cause grief. + #For now at least, we will simply respect the platform pathSeparator and hope the user manages the environment variables appropriately. + #some programs do automatic translation - which is a nice idea in principle - but is also prone to error as we don't know if it's already occurred or not depending on how things are launched. + #An example of where this sort of thing can go wrong is env(TCLLIBPATH) - which is defined as a space separated list not requiring further splitting + # - but some programs have been known to split this value on colon anyway, which breaks things on windows. + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } + } + } + } + + # https://no-color.org + #if {[info exists ::env(NO_COLOR)]} { + # if {$::env(NO_COLOR) ne ""} { + # set colour_disabled 1 + # } + #} + set other_env_vars_config [dict create\ + NO_COLOR {type string}\ + XDG_CONFIG_HOME {type string}\ + XDG_DATA_HOME {type string}\ + XDG_CACHE_HOME {type string}\ + XDG_STATE_HOME {type string}\ + XDG_DATA_DIRS {type pathlist}\ + POSH_THEME {type string}\ + POSH_THEMES_PATH {type string}\ + TCLLIBPATH {type string}\ + ] + lassign [split [info tclversion] .] tclmajorv tclminorv + #don't rely on lseq or punk::lib for now.. + set relevant_minors [list] + for {set i 0} {$i <= $tclminorv} {incr i} { + lappend relevant_minors $i + } + foreach minor $relevant_minors { + set vname TCL${tclmajorv}_${minor}_TM_PATH + if {$minor eq $tclminorv || [info exists ::env($vname)]} { + dict set other_env_vars_config $vname {type string} + } + } + set other_env_vars [dict keys $other_env_vars_config] + + foreach {evar varinfo} $other_env_vars_config { + if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] + set f [set ::env($evar)] + if {$f ne "default"} { + set varname [tcl::string::tolower $evar] + if {$vartype eq "pathlist"} { + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } + } + } + } + + + #unset -nocomplain vars + + #todo + set running [tcl::dict::create] + set running [tcl::dict::merge $running $startup] + } + init + + #todo + proc Apply {config} { + puts stderr "punk::config::Apply partially implemented" + set configname [string map {-config ""} $config] + if {$configname in {startup running}} { + upvar ::punk::config::$configname applyconfig + + if {[dict exists $applyconfig auto_noexec]} { + set auto [dict get $applyconfig auto_noexec] + if {![string is boolean -strict $auto]} { + error "config::Apply error - invalid data for auto_noexec:'$auto' - expected boolean" + } + if {$auto} { + set ::auto_noexec 1 + } else { + #puts "auto_noexec false" + unset -nocomplain ::auto_noexec + } + } + + } else { + error "no config named '$config' found" + } + return "apply done" + } + Apply startup + + #todo - consider how to divide up settings, categories, 'devices', decks etc + proc get_running_global {varname} { + variable running + if {[dict exists $running $varname]} { + return [dict get $running $varname] + } + error "No such global configuration item '$varname' found in running config" + } + proc get_startup_global {varname} { + variable startup + if {[dict exists $startup $varname]} { + return [dict get $startup $varname] + } + error "No such global configuration item '$varname' found in startup config" + } + + proc get {whichconfig {globfor *}} { + variable startup + variable running + switch -- $whichconfig { + config - startup - startup-config - startup-configuration { + #show *startup* config - different behaviour may be confusing to those used to router startup and running configs + set configdata $startup + } + running - running-config - running-configuration { + set configdata $running + } + default { + error "Unknown config name '$whichconfig' - try startup or running" + } + } + if {$globfor eq "*"} { + return $configdata + } else { + set keys [dict keys $configdata [string tolower $globfor]] + set filtered [dict create] + foreach k $keys { + dict set filtered $k [dict get $configdata $k] + } + return $filtered + } + } + + proc configure {args} { + set argd [punk::args::get_dict { + + whichconfig -type string -choices {startup running} + } $args] + + } + + proc show {whichconfig {globfor *}} { + #todo - tables for console + set configdata [punk::config::get $whichconfig $globfor] + return [punk::lib::showdict $configdata] + } + + #e.g + # copy running-config startup-config + # copy startup-config test-config.cfg + # copy backup-config.cfg running-config + #review - consider the merge vs overwrite feature of some routers.. where copy to running-config does a merge rather than an overwrite + #This is to allow partial configs to be loaded to running, whereas a save of running to any target is always a complete configuration + proc copy {args} { + set argd [punk::args::get_dict { + *proc -name punk::config::copy -help "Copy a partial or full configuration from one config to another + If a target config has additional settings, then the source config can be considered to be partial with regards to the target. + " + -type -default "" -choices {replace merge} -help "Defaults to merge when target is running-config + Defaults to replace when source is running-config" + *values -min 2 -max 2 + fromconfig -help "running or startup or file name (not fully implemented)" + toconfig -help "running or startup or file name (not fully implemented)" + } $args] + set fromconfig [dict get $argd values fromconfig] + set toconfig [dict get $argd values toconfig] + set fromconfig [string map {-config ""} $fromconfig] + set toconfig [string map {-config ""} $toconfig] + + set copytype [dict get $argd opts -type] + + + #todo - warn & prompt if doing merge copy to startup + switch -exact -- $fromconfig-$toconfig { + running-startup { + if {$copytype eq ""} { + set copytype replace ;#full configuration + } + if {$copytype eq "replace"} { + error "punk::config::copy error. full configuration copy from running to startup config not yet supported" + } else { + error "punk::config::copy error. merge configuration copy from running to startup config not yet supported" + } + } + startup-running { + #default type merge - even though it's not always what is desired + if {$copytype eq ""} { + set copytype merge ;#load in a partial configuration + } + + #warn/prompt either way + if {$copytype eq "replace"} { + #some routers require use of a separate command for this branch. + #presumably to ensure the user doesn't accidentally load partials onto a running system + # + error "punk::config::copy error. full configuration copy from startup to overwrite running config not supported" + } else { + error "punk::config::copy error. merge copy from possibly partial configuration: startup to running config not currently supported" + } + } + default { + error "punk::config::copy error. copy must from running to startup or startup to running. File sources/targets not yet supported" + } + } + } + + + + + +} + + + + + +#todo - move to cli? +::tcl::namespace::eval punk::config { + #todo - something better - 'previous' rather than reverting to startup + proc channelcolors {{onoff {}}} { + variable running + variable startup + + if {![string length $onoff]} { + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } else { + if {![string is boolean $onoff]} { + error "channelcolors: invalid value $onoff - expected boolean: true|false|on|off|1|0|yes|no" + } + if {$onoff} { + dict set running color_stdout [dict get $startup color_stdout] + dict set running color_stderr [dict get $startup color_stderr] + } else { + dict set running color_stdout "" + dict set running color_stderr "" + } + } + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } +} + +package provide punk::config [tcl::namespace::eval punk::config { + variable version + set version 0.1 + +}] \ No newline at end of file diff --git a/src/bootsupport/modules/punk/mod-0.1.tm b/src/bootsupport/modules/punk/mod-0.1.tm new file mode 100644 index 00000000..58906c88 --- /dev/null +++ b/src/bootsupport/modules/punk/mod-0.1.tm @@ -0,0 +1,164 @@ +#punkapps app manager +# deck cli + +namespace eval punk::mod::cli { + namespace export help list run + namespace ensemble create + + # namespace ensemble configure [namespace current] -unknown punk::mod::cli::_unknown + if 0 { + proc _unknown {ns args} { + puts stderr "punk::mod::cli::_unknown '$ns' '$args'" + puts stderr "punk::mod::cli::help $args" + puts stderr "arglen:[llength $args]" + punk::mod::cli::help {*}$args + } + } + + #cli must have _init method - usually used to load commandsets lazily + # + variable initialised 0 + proc _init {args} { + variable initialised + if {$initialised} { + return + } + #... + set initialised 1 + } + + proc help {args} { + set basehelp [punk::mix::base help {*}$args] + #namespace export + return $basehelp + } + proc getraw {appname} { + upvar ::punk::config::running running_config + set app_folders [dict get $running_config apps] + #todo search each app folder + set bases [::list] + set versions [::list] + set mains [::list] + set appinfo [::list bases {} mains {} versions {}] + + foreach containerfolder $app_folders { + lappend bases $containerfolder + if {[file exists $containerfolder]} { + if {[file exists $containerfolder/$appname/main.tcl]} { + #exact match - only return info for the exact one specified + set namematches $appname + set parts [split $appname -] + } else { + set namematches [glob -nocomplain -dir $containerfolder -type d -tail ${appname}-*] + set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort? + } + foreach nm $namematches { + set mainfile $containerfolder/$nm/main.tcl + set parts [split $nm -] + if {[llength $parts] == 1} { + set ver "" + } else { + set ver [lindex $parts end] + } + if {$ver ni $versions} { + lappend versions $ver + lappend mains $ver $mainfile + } else { + puts stderr "punk::apps::app version '$ver' of app '$appname' already encountered at $mainfile. (will use earliest encountered in running-config apps and ignore others of same version)" + } + } + } else { + puts stderr "punk::apps::app missing apps_folder:'$containerfolder' Ensure apps_folder is set in punk::config" + } + } + dict set appinfo versions $versions + #todo - natsort! + set sorted_versions [lsort $versions] + set latest [lindex $sorted_versions 0] + if {$latest eq "" && [llength $sorted_versions] > 1} { + set latest [lindex $sorted_versions 1 + } + dict set appinfo latest $latest + + dict set appinfo bases $bases + dict set appinfo mains $mains + return $appinfo + } + + proc list {{glob *}} { + upvar ::punk::config::running running_config + set apps_folder [dict get $running_config apps] + if {[file exists $apps_folder]} { + if {[file exists $apps_folder/$glob]} { + #tailcall source $apps_folder/$glob/main.tcl + return $glob + } + set apps [glob -nocomplain -dir $apps_folder -type d -tail $glob] + if {[llength $apps] == 0} { + if {[string first * $glob] <0 && [string first ? $glob] <0} { + #no glob chars supplied - only launch if exact match for name part + set namematches [glob -nocomplain -dir $apps_folder -type d -tail ${glob}-*] + set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort? + if {[llength $namematches] > 0} { + set latest [lindex $namematches end] + lassign $latest nm ver + #tailcall source $apps_folder/$latest/main.tcl + } + } + } + + return $apps + } + } + + #todo - way to launch as separate process + # solo-opts only before appname - args following appname are passed to the app + proc run {args} { + set nameposn [lsearch -not $args -*] + if {$nameposn < 0} { + error "punkapp::run unable to determine application name" + } + set appname [lindex $args $nameposn] + set controlargs [lrange $args 0 $nameposn-1] + set appargs [lrange $args $nameposn+1 end] + + set appinfo [punk::mod::cli::getraw $appname] + if {[llength [dict get $appinfo versions]]} { + set ver [dict get $appinfo latest] + puts stdout "info: $appinfo" + set ::argc [llength $appargs] + set ::argv $appargs + source [dict get $appinfo mains $ver] + if {"-hideconsole" in $controlargs} { + puts stderr "attempting console hide" + #todo - something better - a callback when window mapped? + after 500 {::punkapp::hide_console} + } + return $appinfo + } else { + error "punk::mod::cli unable to run '$appname'. main.tcl not found in [dict get $appinfo bases]" + } + } + + +} + +namespace eval punk::mod::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command help + package require punk::mix::base + package require punk::overlay + punk::overlay::custom_from_base [namespace current] ::punk::mix::base +} + +package provide punk::mod [namespace eval punk::mod { + variable version + set version 0.1 + +}] + + + diff --git a/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/bootsupport/modules/punk/nav/fs-0.1.0.tm new file mode 100644 index 00000000..fdffa091 --- /dev/null +++ b/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -0,0 +1,1373 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.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) 2024 +# +# @@ Meta Begin +# Application punk::nav::fs 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::nav::fs 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::nav::fs] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::nav::fs +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::nav::fs +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::lib +package require punk::args +package require punk::ansi +package require punk::winpath +package require punk::du +package require commandstack +#*** !doctools +#[item] [package {Tcl 8.6}] +#[item] [package {punk::lib}] +#[item] [package {punk::args}] +#[item] [package {punk::winpath}] +#[item] [package {punk::du}] +#[item] [package {punk::commandstack}] + +if {"windows" eq $::tcl_platform(platform)} { + catch {package require punk::unixywindows} +} +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::nav::fs::class { + #*** !doctools + #[subsection {Namespace punk::nav::fs::class}] + #[para] class definitions + #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::nav::fs { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + + #Both tcl's notion of pwd and VIRTUAL_CWD can be out of sync with the process CWD. This happens when in a VFS. + #We can also have VIRTUAL_CWD navigate to spaces that Tcl's cd can't - review + + variable VIRTUAL_CWD ;#cwd that tracks pwd except when in zipfs locations that are not at or below a mountpoint + if {![interp issafe]} { + set VIRTUAL_CWD [pwd] + } else { + set VIRTUAL_CWD "" + } + proc vwd {} { + variable VIRTUAL_CWD + set cwd [pwd] + if {$cwd ne $VIRTUAL_CWD} { + puts stderr "pwd: $cwd" + } + return $::punk::nav::fs::VIRTUAL_CWD + } + + #TODO - maintain per 'volume/server' CWD + #e.g cd and ./ to: + # d: + # //zipfs: + # //server + # https://example.com + # should return to the last CWD for that volume/server + + #VIRTUAL_CWD follows pwd when changed via cd + set stackrecord [commandstack::rename_command -renamer punk::nav::fs cd {args} { + if {![catch { + $COMMANDSTACKNEXT {*}$args + } errM]} { + set ::punk::nav::fs::VIRTUAL_CWD [pwd] + } else { + error $errM + } + }] + + #*** !doctools + #[subsection {Namespace punk::nav::fs}] + #[para] Core API functions for punk::nav::fs + #[list_begin definitions] + + + #NOTE - as we expect to run other apps (e.g Tk) in the same process, but possibly different threads - we should be careful about use of cd which is per-process not per-thread. + #As this function recurses and calls cd multiple times - it's not thread-safe. + #Another thread could theoretically cd whilst this is running. + #Most likely this will then just error-out - but there is a possibility we could end up in the wrong directory, or cause the same problems in the other thread. + #REVIEW - consider looking at current directory only at the beginning and do a single cd to an absolute path. + #currently this allows ./ subdir subdir2 nonexistant and we cd to subdir/subdir2 even though an error is produced at the end. + #This offers a convenience for repl useage at the slight cost of more potential cross-thread cd interference + #- although presumably most library code shouldn't be changing CWD anyway. + #Ideally the user/repl should be in control of the processes working directory and we shouldn't have to worry too much here. + #Notably for example tcltest-2.5.5 at least uses cd - so this is something that may be best run in a separate process (for each test suite?) + #This seems unfortunate - as a multithreaded set of test runs might otherwise have made some sense.. but perhaps for tests more serious isolation is a good idea. + #It also seems common to cd when loading certain packages e.g tls from starkit. + #While in most/normal cases the library will cd back to the remembered working directory after only a brief time - there seem to be many opportunities for issues + #if the repl is used to launch/run a number of things in the one process + proc d/ {args} { + variable VIRTUAL_CWD + + set is_win [expr {"windows" eq $::tcl_platform(platform)}] + + set repl_runid 0 + if {[info commands ::punk::get_repl_runid] ne ""} { + set repl_runid [punk::get_repl_runid] + } + #set ::punk::last_run_display [list] + + if {([llength $args]) && ([lindex $args 0] eq "")} { + set args [lrange $args 1 end] + } + + + if {![llength $args]} { + #ls is too slow even over a fairly low-latency network + #set out [runout -n ls -aFC] + if {[string match //zipfs:/* $VIRTUAL_CWD]} { + if {[Zipfs_path_within_zipfs_mounts $VIRTUAL_CWD]} { + if {[pwd] ne $VIRTUAL_CWD} { + commandstack::basecall cd $VIRTUAL_CWD + } + } + set matchinfo [dirfiles_dict -searchbase $VIRTUAL_CWD] + } else { + if {[pwd] ne $VIRTUAL_CWD} { + commandstack::basecall cd $VIRTUAL_CWD + } + set matchinfo [dirfiles_dict -searchbase [pwd]] + } + set dircount [llength [dict get $matchinfo dirs]] + set filecount [llength [dict get $matchinfo files]] + set symlinkcount [llength [dict get $matchinfo links]] ;#doesn't include windows shelllinks (.lnk) + #set location [file normalize [dict get $matchinfo location]] + set location [dict get $matchinfo location] + + + #result for glob is count of matches - use dirfiles etc for script access to results + set result [list location $location dircount $dircount filecount $filecount symlinks $symlinkcount] + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + lappend result filebytes [punk::lib::format_number $filebytes] + } + if {[punk::nav::fs::system::codethread_is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + #if ansi is off - punk::console::titleset will try 'local' api method - which can fail + catch {::punk::console::titleset [lrange $result 1 end]} + } + } + if {[string match //zipfs:/* $location]} { + set stripbase 0 + } else { + set stripbase 1 + } + set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo] + set chunklist [list] + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + if {[file normalize $VIRTUAL_CWD] ne [pwd]} { + lappend chunklist [list stderr "[punk::ansi::a+ red]PWD:[pwd] VIRTUAL_CWD:$VIRTUAL_CWD[punk::ansi::a]"] + } + lappend chunklist [list result $result] + if {$repl_runid != 0} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + } else { + punk::nav::fs::system::emit_chunklist $chunklist + } + #puts stdout "-->[ansistring VIEW $result]" + return $result + } else { + set atail [lassign $args a1] + if {[llength $args] == 1} { + set a1 [lindex $args 0] + switch -exact -- $a1 { + . - ./ { + tailcall punk::nav::fs::d/ + } + .. - ../ { + if {$VIRTUAL_CWD eq "//zipfs:/" && ![string match //zipfs:/* [pwd]]} { + #exit back to last nonzipfs path that was in use + set VIRTUAL_CWD [pwd] + tailcall punk::nav::fs::d/ + } + + #we need to use normjoin to allow navigation to //server instead of just to //server/share (//server browsing unimplemented - review) + # [file join //server ..] would become /server/.. - use normjoin to get //server + # file dirname //server/share would stay as //server/share + #set up1 [file dirname $VIRTUAL_CWD] + set up1 [punk::path::normjoin $VIRTUAL_CWD ..] + if {[string match //zipfs:/* $up1]} { + if {[Zipfs_path_within_zipfs_mounts $up1]} { + cd $up1 + set VIRTUAL_CWD $up1 + } else { + set VIRTUAL_CWD $up1 + } + } else { + cd $up1 + #set VIRTUAL_CWD [file normalize $a1] + } + tailcall punk::nav::fs::d/ + } + } + + if {[file pathtype $a1] ne "relative"} { + if { ![string match //zipfs:/* $a1]} { + if {[file type $a1] eq "directory"} { + cd $a1 + #set VIRTUAL_CWD $a1 + tailcall punk::nav::fs::d/ + } + } + } + + + if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} { + if {[file type $a1] eq "directory"} { + cd $a1 + #set VIRTUAL_CWD [file normalize $a1] + tailcall punk::nav::fs::d/ + } + } + + if {![regexp {[*?]} $a1]} { + #NON-Glob target + #review + if {[string match //zipfs:/* $a1]} { + if {[Zipfs_path_within_zipfs_mounts $a1]} { + commandstack::basecall cd $a1 + } + set VIRTUAL_CWD $a1 + set curdir $a1 + } else { + set target [punk::path::normjoin $VIRTUAL_CWD $a1] + if {[string match //zipfs:/* $VIRTUAL_CWD]} { + if {[Zipfs_path_within_zipfs_mounts $target]} { + commandstack::basecall cd $target + } + } + if {[file type $target] eq "directory"} { + set VIRTUAL_CWD $target + } + } + tailcall punk::nav::fs::d/ + } + set curdir $VIRTUAL_CWD + } else { + set curdir [pwd] + } + + + #globchar somewhere in path - treated as literals except in final segment (for now. todo - make more like ns/ which accepts full path globbing with double ** etc.) + + set searchspec [lindex $args 0] + + set result "" + set chunklist [list] + + #Only merge results if location matches previous (caller can deliberately intersperse bogus globs to force split if desired) + #TODO - remove duplicate file or dir items for overlapping patterns in same location!!! (at least for count, filebyte totals if not for display) + set last_location "" + set this_result [dict create] + foreach searchspec $args { + set path [path_to_absolute $searchspec $curdir $::tcl_platform(platform)] + set has_tailglob [expr {[regexp {[?*]} [file tail $path]]}] + #we have already done a 'cd' if only one unglobbed path was supplied - therefore any remaining non-glob tails must be tested for folderness vs fileness to see what they mean + #this may be slightly surprising if user tries to exactly match both a directory name and a file both as single objects; because the dir will be listed (auto /* applied to it) - but is consistent enough. + #lower level dirfiles or dirfiles_dict can be used to more precisely craft searches. ( d/ will treat dir the same as dir/*) + + set searchspec_relative [expr {[file pathtype $searchspec] eq "relative"}] + if {$has_tailglob} { + set location [file dirname $path] + set glob [file tail $path] + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase [file dirname $searchspec] + } + } else { + if {[string match //zipfs:/* $path]} { + set location $path + set glob * + set searchbase $path + } elseif {[file isdirectory $path]} { + set location $path + set glob * + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase $path + } + } else { + set location [file dirname $path] + set glob [file tail $path] ;#search for exact match file + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase [file dirname $path] + } + } + } + set matchinfo [dirfiles_dict -searchbase $searchbase -tailglob $glob $location] + #puts stderr "=--->$matchinfo" + + + set location [file normalize [dict get $matchinfo location]] + if {[string match //xzipfs:/* $location] || $location ne $last_location} { + #REVIEW - zipfs test disabled with leading x + #emit previous result + if {[dict size $this_result]} { + dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] + lappend chunklist [list result $this_result] + if {$result ne ""} { + append result \n + } + append result $this_result + } + set this_result [dict create] + set dircount 0 + set filecount 0 + } + incr dircount [llength [dict get $matchinfo dirs]] + incr filecount [llength [dict get $matchinfo files]] + + #result for glob is count of matches - use dirfiles etc for script access to results + dict set this_result location $location + dict set this_result dircount $dircount + dict set this_result filecount $filecount + + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + dict incr this_result filebytes $filebytes + } else { + dict incr this_result filebytes 0 ;#ensure key exists! + } + dict lappend this_result pattern [dict get $matchinfo opts -glob] + + if {[string match //zipfs:/* $location]} { + set stripbase 0 + } else { + set stripbase 1 + } + set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo] + + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + + set last_location $location + } + #process final result + if {[dict size $this_result]} { + dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] + lappend chunklist [list result $this_result] + if {$result ne ""} { + append result \n + } + append result $this_result + } + if {[file normalize $VIRTUAL_CWD] ne [pwd]} { + lappend chunklist [list stderr "[punk::ansi::a+ red]PWD:[pwd] VIRTUAL_CWD:$VIRTUAL_CWD[punk::ansi::a]"] + } + + + if {[punk::nav::fs::system::codethread_is_running]} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + } + if {$repl_runid == 0} { + punk::nav::fs::system::emit_chunklist $chunklist + } + return $result + } + } + + proc dd/ {args} { + #set ::punk::last_run_display [list] + set repl_runid 0 + if {[info commands ::punk::get_repl_runid] ne ""} { + set repl_runid [punk::get_repl_runid] + } + if {![llength $args]} { + set path .. + } else { + set path ../[file join {*}$args] + } + set normpath [file normalize $path] + cd $normpath + set matchinfo [dirfiles_dict -searchbase $normpath $normpath] + set dircount [llength [dict get $matchinfo dirs]] + set filecount [llength [dict get $matchinfo files]] + set location [file normalize [dict get $matchinfo location]] + #result for glob is count of matches - use dirfiles etc for script access to results + set result [list location $location dircount $dircount filecount $filecount] + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + lappend result filebytes [punk::lib::format_number $filebytes] + } + + set out [dirfiles_dict_as_lines -stripbase 1 $matchinfo] + #return $out\n[pwd] + set chunklist [list] + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + lappend chunklist [list result $result] + + if {[punk::nav::fs::system::codethread_is_running]} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + if {[llength [info commands ::punk::console::titleset]]} { + catch {::punk::console::titleset [lrange $result 1 end]} ;#strip location key + } + } + if {$repl_runid == 0} { + punk::nav::fs::system::emit_chunklist $chunklist + } + return $result + } + + proc d/new {args} { + if {![llength $args]} { + error "usage: d/new

\[ ...\]" + } + set a1 [lindex $args 0] + set curdir [pwd] + set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)] + set fullpath [file join $path1 {*}[lrange $args 1 end]] + + if {[file exists $fullpath]} { + error "Folder $fullpath already exists" + } + file mkdir $fullpath + d/ $fullpath + } + + #todo use unknown to allow d/~c:/etc ?? + proc d/~ {args} { + set home $::env(HOME) + set target [file join $home {*}$args] + if {![file isdirectory $target]} { + error "Folder $target not found" + } + d/ $target + } + + + #run a file + proc x/ {args} { + if {![llength $args]} { + set result [d/] + append result \n "x/ ?args?" + return $result + } + set curdir [pwd] + #todo - allow wish for those who want it.. but in punk we try to use tclsh or a kit and load Tk as a library + set scriptconfig [dict create\ + tcl [list exe tclsh extensions [list ".tcl" ".tm" ".tk" ".kit"]]\ + python [list exe python extensions [list ".py"]]\ + lua [list exe lua extensions [list ".lua"]]\ + perl [list exe perl extensions [list ".pl"]]\ + php [list exe php extensions [list ".php"]]\ + ] + set tcl_extensions [list ".tcl" ".tm" ".kit" ".tk"] ;#todo - load from config + set py_extensions [list ".py"] + set lua_extensions [list ".lua"] + set perl_extensions [list ".pl"] + + set script_extensions [list] + set extension_lookup [dict create] + tcl::dict::for {lang langinfo} $scriptconfig { + set extensions [dict get $langinfo extensions] + lappend script_extensions {*}$extensions + foreach e $extensions { + dict set extension_lookup $e $lang ;#provide reverse lookup + } + } + + #some executables (e.g tcl) can use arguments prior to the script + #use first entry on commandline for which a file exists *and has a script extension - or is executable* as the script to run + #we can't always just assume that first existant file on commandline is the one being run, as it might be config file + #e.g php -c php.ini -f script.php + set scriptlang "" + set scriptfile "" + foreach a $args { + set ext [file extension $a] + if {$ext in $script_extensions && [file exists $a]} { + set scriptlang [dict get $extension_lookup $ext] + set scriptfile $a + break + } + } + puts "scriptlang: $scriptlang scriptfile:$scriptfile" + + #todo - allow sh scripts with no extension ... look at shebang etc? + if {$scriptfile ne "" && $scriptlang ne ""} { + set path [path_to_absolute $scriptfile $curdir $::tcl_platform(platform)] + if {[file type $path] eq "file"} { + set ext [file extension $path] + set extlower [string tolower $ext] + if {$extlower in $tcl_extensions} { + set newargs [lrange $args 1 end] ;#todo - fix to allow script in position other than first + set ::argv0 $path + set ::argc [llength $newargs] + set ::argv $newargs + tailcall source $path + } elseif {$extlower in $py_extensions} { + set pycmd [auto_execok python] + tailcall {*}$pycmd {*}$args + } elseif {$extlower in $script_extensions} { + set exename [dict get $scriptconfig $scriptlang exe] + set cmd [auto_execok $exename] + tailcall {*}$cmd $args + } else { + set fd [open $path r] + set chunk [read $fd 4000]; close $fd + #consider any commented line near top of file containing 'tcl' as likely to be a tcl script of some sort and attempt to source it. + set toplines [split $chunk \n] + set tcl_indicator 0 + foreach ln $toplines { + set ln [string trim $ln] + if {[string match "#*tcl*" $ln]} { + set tcl_indicator 1 + break + } + } + if {$tcl_indicator} { + set newargs [lrange $args 1 end] ;#todo - fix to allow script in position other than first. + set ::argv0 $path + set ::argc [llength $newargs] + set ::argv $newargs + tailcall source $path + } + puts stderr "Cannot run [file extension $path] file directly ([file tail $path]) as tcl script. Ensure file has a known tcl extension ($tcl_extensions) or add a commented hint in the file such as #!/usr/bin/env tclsh" + return [pwd] + } + } + } else { + puts stderr "No script executable known for this" + } + + } + + + proc dirlist {{location ""}} { + set contents [dirfiles_dict $location] + return [dirfiles_dict_as_lines -stripbase 1 $contents] + } + + + #dirfiles dirfiles_dict always deliberately return absolute *unnormalized* path + #e.g when cwd is c:/repo/jn/punk dirfiles ../../ will return something like: + # c:/repo/jn/punk/../../blah + #dirfiles assumes we don't have glob chars in the filenames or paths - dirfiles_dict can be called directly with explicit -tailglob in the rare case that assumption doesn't hold + # dirfiles will test last segment (tail) of supplied searchspecs for fileness vs folderness (when no globchars present in tail) so that it can pass the appropriate flags downstream + proc dirfiles {args} { + set argspecs { + -stripbase -default 1 -type boolean + -formatsizes -default 1 -type boolean -help "Format file size numbers for clarity" + *values -min 0 -max -1 + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts values_dict + + set opt_stripbase [dict get $opts -stripbase] + set opt_formatsizes [dict get $opts -formatsizes] + + #todo - support multiple searchspecs - dirfiles_dict should merge results when same folder + set searchspec "" + dict for {_index val} $values_dict { + set searchspec $val + break + } + + set relativepath [expr {[file pathtype $searchspec] eq "relative"}] + set has_tailglobs [regexp {[?*]} [file tail $searchspec]] + + #dirfiles_dict would handle simple cases of globs within paths anyway - but we need to explicitly set tailglob here in all branches so that next level doesn't need to do file vs dir checks to determine user intent. + #(dir-listing vs file-info when no glob-chars present is inherently ambiguous so we test file vs dir to make an assumption - more explicit control via -tailglob can be done manually with dirfiles_dict) + if {$relativepath} { + set searchbase [pwd] + if {!$has_tailglobs} { + if {[file isdirectory [file join $searchbase $searchspec]]} { + set location [file join $searchbase $searchspec] + set tailglob * + } else { + set location [file dirname [file join $searchbase $searchspec]] + set tailglob [file tail $searchspec] ;#use exact match as a glob - will retrieve size,attributes etc. + } + } else { + #tailglobs exist - and we operate under assumption globchars aren't present in file/folder names - so no folderness/fileness check needed. + set location [file dirname [file join $searchbase $searchspec]] + set tailglob [file tail $searchspec] + } + } else { + #for absolute paths - searchbase AND location will change depending on globiness of tail and fileness vs folderness + if {!$has_tailglobs} { + if {[file isdirectory $searchspec]} { + set searchbase $searchspec + set location $searchspec + set tailglob * + } else { + set searchbase [file dirname $searchspec] + set location [file dirname $searchspec] + set tailglob [file tail $searchspec] ;#literal glob for single file - retrieves properties + } + } else { + set searchbase [file dirname $searchspec] + set location [file dirname $searchspec] + set tailglob [file tail $searchspec] + } + } + puts "--> -searchbase:$searchbase searchspec:$searchspec -tailglob:$tailglob location:$location" + set contents [dirfiles_dict -searchbase $searchbase -tailglob $tailglob $location] + return [dirfiles_dict_as_lines -stripbase $opt_stripbase -formatsizes $opt_formatsizes $contents] + } + + #todo - package as punk::nav::fs + #todo - in thread + #todo - streaming version + #glob patterns in path prior to final segment should already be resolved before using dirfiles_dict - as the underlying filesystem mechanisms can't do nested globbing themselves. + #dirfiles_dict will assume the path up to the final segment is literal even if globchars are included therein. + #final segment globs will be recognised only if -tailglob is passed as empty string + #if -tailglob not supplied and last segment has globchars - presume searchspec parendir is the container and last segment is globbing within that. + #if -tailglob not supplied and last segment has no globchars - presume searchspec is a container(directory) and use glob * + #caller should use parentdir as location and set tailglob to search-pattern or exact match if location is intended to match a file rather than a directory + #examples: + # somewhere/files = search is effectively somewhere/files/* (location somewhere/files glob is *) + # somewhere/files/* = (as above) + # -tailglob * somewhere/files = (as above) + # + # -tailglob "" somewhere/files = search somewhere folder for exactly 'files' (location somewhere glob is files) + # -tailglob files somewhere = (as above) + # + # somewhere/f* = search somewhere folder for f* (location somewhere glob is f*) + # -tailglob f* somewhere = (as above) + # + # This somewhat clumsy API is so that simple searches can be made in a default sensible manner without requiring extra -tailglob argument for the common cases - with lack of trailing glob segment indicating a directory listing + # - but we need to distinguish somewhere/files as a search of that folder vs somewhere/files as a search for exactly 'files' within somewhere, hence the -tailglob option to fine-tune. + # - this also in theory allows file/directory names to contain glob chars - although this is probably unlikely and/or unwise and not likely to be usable on all platforms. + # + #if caller supplies a tailglob as empty string - presume the caller hasn't set location to parentdir - and that last element is the search pattern. + # -searchbase is always passed through - and is only used to construct a location path if a relative searchspec was supplied + proc dirfiles_dict {args} { + set argspecs { + *opts -any 0 + -searchbase -default "" + -tailglob -default "\uFFFF" + #with_sizes & with_times must accept 0|1|f|d|l where f = files d = dirs l = links (punk::du) + -with_sizes -default "\uFFFF" -type string + -with_times -default "\uFFFF" -type string + *values -min 0 -max -1 -type string + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + + #puts stderr "searchspecs: $searchspecs [llength $searchspecs]" + #puts stdout "arglist: $opts" + + if {[llength $searchspecs] > 1} { + #review - spaced paths ? + error "dirfiles_dict: multiple listing not *yet* supported" + } + set searchspec [lindex $searchspecs 0] + # -- --- --- --- --- --- --- + set opt_searchbase [dict get $opts -searchbase] + set opt_tailglob [dict get $opts -tailglob] + set opt_with_sizes [dict get $opts -with_sizes] + set opt_with_times [dict get $opts -with_times] + # -- --- --- --- --- --- --- + + #we don't want to normalize.. + #for example if the user supplies ../ we want to see ../result + + set is_relativesearchspec [expr {[file pathtype $searchspec] eq "relative"}] + if {$opt_searchbase eq ""} { + set searchbase . + } else { + set searchbase $opt_searchbase + } + + + switch -- $opt_tailglob { + "" { + if {$searchspec eq ""} { + set location + } else { + if {$is_relativesarchspec} { + #set location [file dirname [file join $opt_searchbase $searchspec]] + set location [punk::path::normjoin $searchbase $searchspec ..] + } else { + set location [punk::path::normjoin $searchspec ..] + } + #here tail is treated as a search-pattern within location whether or not it contains glob chars "?" or "*" + set match_contents [file tail $searchspec] + } + } + "\uFFFF" { + set searchtail_has_globs [regexp {[*?]} [file tail $searchspec]] + if {$searchtail_has_globs} { + if {$is_relativesearchspec} { + #set location [file dirname [file join $searchbase $searchspec]] + #e.g subdir/* or sub/etc/x* + set location [punk::path::normjoin $searchbase $searchspec ..] + } else { + set location [punk::path::normjoin $searchspec ..] + } + set match_contents [file tail $searchspec] + } else { + #user didn't supply a glob within tail segment, nor did they specify a separate -tailglob - presume they want a directory listing + if {$searchspec eq ""} { + set location $searchbase + } else { + if {$is_relativesearchspec} { + #set location [file join $searchbase $searchspec] + set location [punk::path::normjoin $searchbase $searchspec] + } else { + #absolute path for search + set location $searchspec + } + } + set match_contents * + } + } + default { + #-tailglob supplied separately - ignore any globiness in tail segment of searchspec and treat literally + if {$searchspec eq ""} { + set location $searchbase + } else { + if {$is_relativesearchspec} { + #set location [file join $searchbase $searchspec] + set location [punk::path::normjoin $searchbase $searchspec] + } else { + set location $searchspec + } + } + set match_contents $opt_tailglob + } + } + puts stdout "searchbase: $searchbase searchspec:$searchspec" + + set in_vfs 0 + if {[llength [package provide vfs]]} { + foreach mount [vfs::filesystem info] { + if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} { + set in_vfs 1 + break + } + } + } + + if {$opt_with_sizes eq "\uFFFF"} { + #leave up to listing-provider defaults + set next_opt_with_sizes "" + } else { + set next_opt_with_sizes [list -with_sizes $opt_with_sizes] + } + if {$opt_with_times eq "\uFFFF"} { + #leave up to listing-provider defaults + set next_opt_with_times "" + } else { + set next_opt_with_times [list -with_times $opt_with_times] + } + if {$in_vfs} { + set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } else { + set in_zipfs 0 + if {[info commands ::tcl::zipfs::mount] ne ""} { + if {[string match //zipfs:/* $location]} { + set in_zipfs 1 + } + #dict for {zmount zpath} [zipfs mount] { + # if {[punk::mix::base::lib::path_a_atorbelow_b $location $zmount]} { + # set in_zipfs 1 + # break + # } + #} + } + if {$in_zipfs} { + #relative vs absolute? review - cwd valid for //zipfs:/ ?? + set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } else { + set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } + } + + set dirs [dict get $listing dirs] + set files [dict get $listing files] + set filesizes [dict get $listing filesizes] + set vfsmounts [dict get $listing vfsmounts] + set flaggedhidden [dict get $listing flaggedhidden] + + + set nonportable [list] ;#illegal file/folder names for windows e.g trailing dot or trailing space - can still be read if //?/ AND shortname used + set underlayfiles [list] + set underlayfilesizes [list] + if {[llength $vfsmounts]} { + foreach vfsmount $vfsmounts { + if {[set fposn [lsearch $files $vfsmount]] >= 0} { + lappend underlayfiles [lindex $files $fposn] + set files [lreplace $files $fposn $fposn] + #for any change to files list must change filesizes too if list exists + if {[llength $filesizes]} { + lappend underlayfilesizes [lindex $filesizes $fposn] + set filesizes [lreplace $filesizes $fposn $fposn] + } + lappend dirs $vfsmount + } elseif {$vfsmount in $dirs} { + #either dirlisting mech was aware of vfs.. or mountpoint is overlaying an underlying folder + #for now - do nothing + #todo - review. way to query dirlisting mech to see if we are hiding a folder? + + } else { + #vfs mount but dirlisting mechanism didn't detect as file or folder + lappend dirs $vfsmount + } + } + } + + + #NOTE: -types {hidden d} * may return . & .. on unix platforms - but will not show them on windows. + #A mounted vfs exe (e.g sometclkit.exe) may be returned by -types {hidden d} on windows - but at the same time has "-hidden 0" in the result of file attr. + + #non-unix platforms may have attributes to indicate hidden status even if filename doesn't have leading dot. + #mac & windows have these + #windows doesn't consider dotfiles as hidden - mac does (?) + #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden + if {$::tcl_platform(platform) ne "windows"} { + lappend flaggedhidden {*}[lsearch -all -inline [concat $dirs $files] ".*"] + #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs + #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely + #set flaggedhidden [lsort -unique $flaggedhidden] + set flaggedhidden [punk::lib::lunique_unordered $flaggedhidden] + } + + set dirs [lsort $dirs] ;#todo - natsort + + + + #foreach d $dirs { + # if {[lindex [file system $d] 0] eq "tclvfs"} { + # lappend vfs $d [file system $d] + # } + #} + + #glob -types {hidden} will not always return the combination of glob -types {hidden f} && -types {hidden d} (on windows anyway) + + # -- --- + #can't lsort files without lsorting filesizes + #Note - the sort by index would convert an empty filesizes list to a list of empty strings - one for each entry in files + #We want to preserve the empty list if that's what the dirlisting mechanism returned (presumably because -with_sizes was 0 or explicitly excluded files) + if {[llength $filesizes] == 0} { + set sorted_files [lsort $files] + set sorted_filesizes [list] + } else { + set sortorder [lsort -indices $files] + set sorted_files [list] + set sorted_filesizes [list] + foreach i $sortorder { + lappend sorted_files [lindex $files $i] + lappend sorted_filesizes [lindex $filesizes $i] + } + } + + set files $sorted_files + set filesizes $sorted_filesizes + # -- --- + + + foreach nm [concat $dirs $files] { + if {[punk::winpath::illegalname_test $nm]} { + lappend nonportable $nm + } + } + set front_of_dict [dict create location $location searchbase $opt_searchbase] + set listing [dict merge $front_of_dict $listing] + + set updated [dict create dirs $dirs files $files filesizes $filesizes nonportable $nonportable flaggedhidden $flaggedhidden underlayfiles $underlayfiles underlayfilesizes $underlayfilesizes] + return [dict merge $listing $updated] + } + + #todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing? + proc dirfiles_dict_as_lines {args} { + package require overtype + + set argspecs { + -stripbase -default 0 -type boolean + -formatsizes -default 1 -type boolean + *values -min 1 -max -1 -type dict + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set list_of_dicts [dict values $vals] + + + + # -- --- --- --- --- --- --- --- --- --- --- --- + set opt_stripbase [dict get $opts -stripbase] + set opt_formatsizes [dict get $opts -formatsizes] + # -- --- --- --- --- --- --- --- --- --- --- --- + + #if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied + set common_base "" + set searchbases [list] + set searchbases_with_len [list] + if {$opt_stripbase} { + #todo - case-insensitive comparisons on platforms where that is appropriate (e.g windows) + # - note that the OS could be configured differently in this regard than the default (as could a filesystem such as ZFS), and that for example mounted SMB filesystems are likely to be configured to support the general windows client idea of case-preserving-but-case-insensitive. + # - we may be able to provide a reasonable default for windows vs other - but a proc option is likely needed to allow caller to override the default behaviour on a call by call basis, + # and a config option may be desirable for the user to override the platform default. + # The chosen defaults based on platform may be generally ok - but then not give the desired behaviour when accessing a particular filesystem/mount + if {$::tcl_platform(platform) eq "windows"} { + #case-preserving but case-insensitive matching is the default + foreach d $list_of_dicts { + set str [string tolower [string trim [dict get $d searchbase]]] + lappend searchbases $str + lappend searchbases_with_len [list $str [string length $str]] + } + } else { + #case sensitive + foreach d $list_of_dicts { + set str [string trim [dict get $d searchbase]] + lappend searchbases $str + lappend searchbases_with_len [list $str [string length $str]] + } + } + #if any of the searchbases is empty - there will be no common base - so leave common_base as empty string. + if {"" ni $searchbases} { + set shortest_to_longest [lsort -index 1 -integer $searchbases_with_len] + set prefix_test_list [tcl::prefix all $searchbases [lindex $shortest_to_longest 0 0]] + #if shortest doesn't match all searchbases - we have no common base + if {[llength $prefix_test_list] == [llength $searchbases]} { + set common_base [lindex $shortest_to_longest 0 0]; #we + } + } + } + + foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] { + set $fileset [list] + } + + #set contents [lindex $list_of_dicts 0] + foreach contents $list_of_dicts { + lappend dirs {*}[dict get $contents dirs] + lappend files {*}[dict get $contents files] + lappend links {*}[dict get $contents links] + lappend filesizes {*}[dict get $contents filesizes] + lappend underlayfiles {*}[dict get $contents underlayfiles] + lappend underlayfilesizes {*}[dict get $contents underlayfilesizes] + lappend flaggedhidden {*}[dict get $contents flaggedhidden] + lappend flaggedreadonly {*}[dict get $contents flaggedreadonly] + lappend flaggedsystem {*}[dict get $contents flaggedsystem] + lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective + lappend vfsmounts {*}[dict get $contents vfsmounts] + } + + if {$opt_stripbase && $common_base ne ""} { + set filetails [list] + set dirtails [list] + foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] { + set stripped [list] + foreach f [set $fileset] { + lappend stripped [strip_prefix_depth $f $common_base] + } + set $fileset $stripped + } + #Note: we need to remember to use common_base to rebuild the key when we need to query the dict-based elements: sizes & times - because we didn't strip those keys. + } + + # -- --- --- --- --- --- --- --- --- --- --- + #assign symlinks to the dirs or files collection (the punk::du system doesn't sort this out + #As at 2024-09 for windows symlinks - Tcl can't do file readlink on symlinks created with mklink /D name target (SYMLINKD) or mklink name target (SYMLINK) + #We can't read the target information - best we can do is classify it as a file or a dir + #we can't use 'file type' as that will report just 'link' - but file isfile and file isdirectory work and should work for links on all platforms - REVIEW + set file_symlinks [list] + set dir_symlinks [list] + set dir_shortcuts [list] ;#windows shell links (.lnk) that have a target that is a directory + foreach s $links { + if {[file isfile $s]} { + lappend file_symlinks $s + #will be appended in finfo_plus later + } elseif {[file isdirectory $s]} { + lappend dir_symlinks $s + lappend dirs $s + } else { + #dunno - warn for now + puts stderr "Warning - cannot determine link type for link $s" + } + } + #we now have the issue that our symlinks aren't sorted within the dir/file categorisation - they currently will have to appear at beginning or end - TODO + # -- --- --- --- --- --- --- --- --- --- --- + + + #todo - sort whilst maintaining order for metadata? + #we need to co-sort files only with filesizes (other info such as times is keyed on fname so cosorting not required) + + + #we can't sort on filesize after format_number (unless we were to enforce delim _ which we don't want to do) + if {$opt_formatsizes} { + set filesizes [punk::lib::format_number $filesizes] ;#accepts a list and will process each + } + #col2 with subcolumns + + #remove punk::pipedata dependency - allow use of punk::nav::fs without punk package + #set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + #widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max + set widest2a [tcl::mathfunc::max {*}[lmap v [list {*}$files {*}$file_symlinks ""] {string length $v}]] + + set c2a [string repeat " " [expr {$widest2a + 1}]] + #set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + set widest2b [tcl::mathfunc::max {*}[lmap v [list {*}$filesizes ""] {string length $v}]] + set c2b [string repeat " " [expr {$widest2b + 1}]] + set finfo [list] + foreach f $files s $filesizes { + #note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces + #hence we need to keep the filename as well, properly protected as a list element + lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"] + } + set flink_style [punk::ansi::a+ undercurly underline undt-green] ;#curly green underline with fallback to normal underline + set dlink_style [punk::ansi::a+ undercurly underline undt-green] + #We use an underline so the visual styling of a link can coexist with fg/bg colors applied for other attributes such as hidden + foreach flink $file_symlinks { + lappend finfo [list file $flink display "$flink_style[overtype::left $c2a $flink] [overtype::right $c2b 0]"] + } + + set fshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink] + set dshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink] + #examine windows .lnk shell link files (shortcuts) - these could be encountered on other platforms too - we should still be able to read them + #review - symlink to shortcut? hopefully will just work + #classify as file or directory - fallback to file if unknown/undeterminable + set finfo_plus [list] + foreach fdict $finfo { + set fname [dict get $fdict file] + if {[file extension $fname] eq ".lnk"} { + if {![catch {package require punk::winlnk}]} { + set shortcutinfo [punk::winlnk::file_get_info $fname] + set target_type "file" ;#default/fallback + if {[dict exists $shortcutinfo link_target]} { + set tgt [dict get $shortcutinfo link_target] + if {[file exists $tgt]} { + #file type could return 'link' - we will use ifile/isdirectory + if {[file isfile $tgt]} { + set target_type file + } elseif {[file isdirectory $tgt]} { + set target_type directory + } else { + set target_type file ;## ? + } + } else { + #todo - see if punk::winlnk has info about the type at the time of linking + #for now - treat as file + } + } + switch -- $target_type { + file { + set display [dict get $fdict display] + set display $fshortcut_style$display ;# + dict set fdict display $display + lappend finfo_plus $fdict + } + directory { + #target of link is a dir - for display/categorisation purposes we want to see it as a dir + #will be styled later based on membership of dir_shortcuts + lappend dirs $fname + lappend dir_shortcuts $fname + } + } + } + #if we don't have punk::winlnk to read the .lnk - it will get no special highlighting and just appear as an ordinary file even if it points to a dir + } else { + lappend finfo_plus $fdict + } + } + unset finfo + + + + #set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]] + + set displaylist [list] + set col1 [string repeat " " [expr {$widest1 + 2}]] + set RST [punk::ansi::a] + foreach d $dirs filerec $finfo_plus { + set d1 [punk::ansi::a+ cyan bold] + set d2 [punk::ansi::a+ defaultfg defaultbg normal] + #set f1 [punk::ansi::a+ white bold] + set f1 [punk::ansi::a+ white] + set f2 [punk::ansi::a+ defaultfg defaultbg normal] + set fdisp "" + if {[string length $d]} { + if {$d in $flaggedhidden} { + set d1 [punk::ansi::a+ cyan normal] + } + if {$d in $vfsmounts} { + if {$d in $flaggedhidden} { + #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW + #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows) + #mark it differently for now.. (todo bug report?) + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red Yellow bold] + } else { + set d1 [punk::ansi::a+ green Purple bold] + } + } else { + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red White bold] + } else { + set d1 [punk::ansi::a+ green bold] + } + } + } else { + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red bold] + } + } + #dlink-style & dshortcut_style are for underlines - can be added with colours already set + if {$d in $dir_symlinks} { + append d1 $dlink_style + } elseif {$d in $dir_shortcuts} { + append d1 $dshortcut_style + } + } + if {[llength $filerec]} { + set fname [dict get $filerec file] + set fdisp [dict get $filerec display] + if {$fname in $flaggedhidden} { + set f1 [punk::ansi::a+ Purple] + } else { + if {$fname in $nonportable} { + set f1 [punk::ansi::a+ red bold] + } + } + } + lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST + } + + return [punk::lib::list_as_lines $displaylist] + } + + #pass in base and platform to head towards purity/testability. + #this function can probably never be pure in such a simple form - as it needs to read state from the os storage system configuration + #consider haskells approach of well-typed paths for cross-platform paths: https://hackage.haskell.org/package/path + #review: punk::winpath calls cygpath! + #review: file pathtype is platform dependant + proc path_to_absolute {path base platform} { + set ptype [file pathtype $path] + if {$ptype eq "absolute"} { + set path_absolute $path + } elseif {$ptype eq "volumerelative"} { + if {$platform eq "windows"} { + #unix looking paths like /c/users or /usr/local/etc are reported by tcl as volumerelative.. (as opposed to absolute on unix platforms) + if {[string index $path 0] eq "/"} { + #this conversion should be an option for the ./ command - not built in as a default way of handling volumerelative paths here + #It is more useful on windows to treat /usr/local as a wsl or mingw path - and may be reasonable for ./ - but is likely to surprise if put into utility functions. + #Todo - tidy up. + package require punk::unixywindows + set path_absolute [punk::unixywindows::towinpath $path] + #puts stderr "winpath: $path" + } else { + #todo handle volume-relative paths with volume specified c:etc c: + #note - tcl doesn't handle this properly anyway.. the win32 api should 'remember' the per-volume cwd + #not clear whether tcl can/will fix this - but it means these paths are dangerous. + #The cwd of the process can get out of sync with what tcl thinks is the working directory when you swap drives + #Arguably if ...? + + #set path_absolute $base/$path + set path_absolute $path + } + } else { + # unknown what paths are reported as this on other platforms.. treat as absolute for now + set path_absolute $path + } + } else { + set path_absolute $base/$path + } + if {$platform eq "windows"} { + if {[punk::winpath::illegalname_test $path_absolute]} { + set path_absolute [punk::winpath::illegalname_fix $path_absolute] ;#add dos-device-prefix protection if not already present + } + } + return $path_absolute + } + proc strip_prefix_depth {path prefix} { + set tail [lrange [file split $path] [llength [file split $prefix]] end] + if {[llength $tail]} { + return [file join {*}$tail] + } else { + return "" + } + } + + #REVIEW - at least one version of Tcl during development couldn't navigate using cd to intermediate paths between the zipfs root and the mountpoint. + #TODO - test if this can still occur. + proc Zipfs_path_within_zipfs_mounts {zipfspath} { + if {![string match //zipfs:/* $zipfspath]} {error "Zipfs_path_within_zipfs_mounts error. Supplied zipfspath $zipfspath must be a //zipfs:/* path"} + set is_within_mount 0 + dict for {zmount zpath} [zipfs mount] { + if {[punk::mix::base::lib::path_a_atorbelow_b $zipfspath $zmount]} { + set is_within_mount 1 + break + } + } + return $is_within_mount + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::nav::fs ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::nav::fs::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::nav::fs::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::nav::fs::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +tcl::namespace::eval punk::nav::fs::system { + #*** !doctools + #[subsection {Namespace punk::nav::fs::system}] + #[para] Internal functions that are not part of the API + + #ordinary emission of chunklist when no repl + proc emit_chunklist {chunklist} { + set result "" + foreach record $chunklist { + lassign $record type data + switch -- $type { + stdout { + puts stdout "$data" + } + stderr { + puts stderr $data + } + result {} + default { + puts stdout "$type $data" + } + } + } + return $result + } + + proc codethread_is_running {} { + if {[info commands ::punk::repl::codethread::is_running] ne ""} { + return [punk::repl::codethread::is_running] + } + return 0 + } + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::nav::fs [tcl::namespace::eval punk::nav::fs { + variable pkg punk::nav::fs + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm b/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm new file mode 100644 index 00000000..09b8a0be --- /dev/null +++ b/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm @@ -0,0 +1,259 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.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) 2024 +# +# @@ Meta Begin +# Application punk::repl::codethread 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::repl::codethread 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::repl::codethread] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::repl::codethread +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::repl::codethread +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::config +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::repl::codethread::class { + #*** !doctools + #[subsection {Namespace punk::repl::codethread::class}] + #[para] class definitions + #if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::repl::codethread { + tcl::namespace::export * + variable replthread + variable replthread_cond + variable running 0 + + variable output_stdout "" + variable output_stderr "" + + #variable xyz + + #*** !doctools + #[subsection {Namespace punk::repl::codethread}] + #[para] Core API functions for punk::repl::codethread + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + proc is_running {} { + variable running + return $running + } + proc runscript {script} { + #puts stderr "->runscript" + variable replthread_cond + variable output_stdout "" + variable output_stderr "" + #expecting to be called from a thread::send in parent repl - ie in the toplevel interp so that the sub-interp "code" is available + #if a thread::send is done from the commandline in a codethread - Tcl will + if {"code" ni [interp children] || ![info exists replthread_cond]} { + #in case someone tries calling from codethread directly - don't do anything or change any state + #(direct caller could create an interp named code at the level "" -> "code" -"code" and add a replthread_cond value to avoid this check - but it probably won't do anything useful) + #if called directly - the context will be within the first 'code' interp. + #inappropriate caller could add superfluous entries to shellfilter stack if function errors out + #inappropriate caller could affect tsv vars (if their interp allows that anyway) + puts stderr "runscript is meant to be called from the parent repl thread via a thread::send to the codethread" + return + } + set outstack [list] + set errstack [list] + upvar ::punk::config::running running_config + if {[string length [dict get $running_config color_stdout_repl]] && [interp eval code punk::console::colour]} { + lappend outstack [interp eval code [list shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout_repl]]]] + } + lappend outstack [interp eval code [list shellfilter::stack::add stdout tee_to_var -settings {-varname ::punk::repl::codethread::output_stdout}]] + + if {[string length [dict get $running_config color_stderr_repl]] && [interp eval code punk::console::colour]} { + lappend errstack [interp eval code [list shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr_repl]]]] + # #lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour cyan]] + } + lappend errstack [interp eval code [list shellfilter::stack::add stderr tee_to_var -settings {-varname ::punk::repl::codethread::output_stderr}]] + + #an experiment + #set errhandle [shellfilter::stack::item_tophandle stderr] + #interp transfer "" $errhandle code + + set scope [interp eval code [list set ::punk::ns::ns_current]] + set status [catch { + interp eval code [list tcl::namespace::inscope $scope $script] + } result] + + + flush stdout + flush stderr + + #interp transfer code $errhandle "" + #flush $errhandle + set lastoutchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stdout]] end] + set lasterrchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stderr]] end] + #puts stderr "-->[ansistring VIEW -lf 1 $lastoutchar$lasterrchar]" + + set tid [thread::id] + tsv::set codethread_$tid info [list lastoutchar $lastoutchar lasterrchar $lasterrchar] + tsv::set codethread_$tid status $status + tsv::set codethread_$tid result $result + tsv::set codethread_$tid errorcode $::errorCode + + + #only remove from shellfilter::stack the items we added to stack in this function + foreach s [lreverse $outstack] { + interp eval code [list shellfilter::stack::remove stdout $s] + } + foreach s [lreverse $errstack] { + interp eval code [list shellfilter::stack::remove stderr $s] + } + thread::cond notify $replthread_cond + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::repl::codethread ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::repl::codethread::lib { + tcl::namespace::export * + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::repl::codethread::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::repl::codethread::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +tcl::namespace::eval punk::repl::codethread::system { + #*** !doctools + #[subsection {Namespace punk::repl::codethread::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::repl::codethread [tcl::namespace::eval punk::repl::codethread { + variable pkg punk::repl::codethread + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/punk/unixywindows-0.1.0.tm b/src/bootsupport/modules/punk/unixywindows-0.1.0.tm new file mode 100644 index 00000000..1d0a3957 --- /dev/null +++ b/src/bootsupport/modules/punk/unixywindows-0.1.0.tm @@ -0,0 +1,237 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application punk::unixywindows 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + +#for illegalname_test +package require punk::winpath + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::unixywindows { + #'cached' name to make obvious it could be out of date - and to distinguish from unixyroot arg + variable cachedunixyroot "" + + + #----------------- + #e.g something like c:/Users/geek/scoop/apps/msys2/current c:/msys2 + proc get_unixyroot {} { + variable cachedunixyroot + if {![string length $cachedunixyroot]} { + if {![catch { + set result [exec cygpath -m /] ;# -m gives result with forward-slashes - which is ok for windows paths in a Tcl context. + set cachedunixyroot [punk::objclone $result] + file pathtype $cachedunixyroot; #this call causes the int-rep to be path + #set ::punk::last_run_display [list] ;#hack - review shouldn't really be necessary.. but because we call winpath from ./ - the repl looks for last_run_display + } errM]} { + + } else { + puts stderr "Warning: Failed to determine base for unix-like paths - using default of c:/msys2" + file pathtype [set cachedunixyroot [punk::objclone "c:/msys2"]] + } + } + #will have been shimmered from string to 'path' internal rep by 'file pathtype' call + + #let's return a different copy as it's so easy to lose path-rep + set copy [punk::objclone $cachedunixyroot] + return $copy + } + proc refresh_unixyroot {} { + variable cachedunixyroot + set result [exec cygpath -m /] ;# -m gives result with forward-slashes - which is ok for windows paths in a Tcl context. + set cachedunixyroot [punk::objclone $result] + file pathtype $cachedunixyroot; #this call causes the int-rep to be path + + set copy [punk::objclone $cachedunixyroot] + return $copy + } + proc set_unixyroot {windows_path} { + variable cachedunixyroot + file pathtype $windows_path + set cachedunixyroot [punk::objclone $windows_path] + #return the original - but probably int-rep will have shimmered to path even if started out as string + #- that's probably what we want. Caller can use as string or path without affecting cachedunixyroot + return $windows_path + } + + + proc windir {path} { + if {$path eq "~"} { + #as the tilde hasn't been normalized.. we can't assume we're running on the actual platform + return ~/.. + } + return [file dirname [towinpath $path]] + } + + #REVIEW high-coupling + proc cdwin {path} { + set path [towinpath $path] + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + ::punk::console::titleset $path + } + } + cd $path + } + proc cdwindir {path} { + set path [towinpath $path] + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + ::punk::console::titleset $path + } + } + cd [file dirname $path] + } + + #NOTE - this is an expensive operation - avoid where possible. + #review - is this intended to be useful/callable on non-windows platforms? + #it should in theory be useable from another platform that wants to create a path for use on windows. + #In this case - we shouldn't examine what volumes exist (assume A: .. Z: are valid) + #review zipfs:// other uri schemes? + proc towinpath {unixypath {unixyroot ""}} { + #NOTE: tcl file exists gives different answers on windows for paths like /c depending on cwd (presumably based on file pathtype of volumerelative) + #(Tcl is also somewhat broken as at 2023 as far as volume relative paths - process can get out of sync with tcl if cd to a vol relative path is used) + #This is add odds with attempting to navigate on a windows system which has cygwin, wsl etc... It also makes it difficult for functions intended to operate independent of CWD. + #e.g there is potential confusion when there is a c folder on c: drive (c:/c) + #I will attempt to provide a coherent operation for winpath ./ ../ etc , but it may disallow for example; change to /something or /x where these don't match a driveletter or /mnt + #whereas tcl may allow cd to /something if a something folder happens to exist on the current volume based on cwd. + #I think it's preferable to require an explicit driveletter /x or /mnt when using unix-like paths on windows - but practical considerations may prove me wrong.. + #It's possible that this function should also ignore the current set of driveletters - and operate completely independent of whether a path actually exists + #This makes it hard to use things like 'file normalize' - which also looks at things like current volume. + # + #Note for example the results of 'which' grep on windows can produce a path like /c/Users/somewhere/bin/grep + #which tcl's file normalize may change to C:/c/Users or X:/c/Users - based on current volumen. Given that C:/c might exist - this can be problematic in a couple of ways. + #The mixing of unix-like and windows commands on the same machine is a large part of the problem.. but this mix is now common + # + #convert /c/etc to C:/etc + set re_slash_x_slash {^/([[:alpha:]]){1}/.*} + set re_slash_else {^/([[:alpha:]]*)(.*)} + set volumes [file volumes] + #exclude things like //zipfs:/ ?? + set driveletters [list] + foreach v $volumes { + if {[regexp {^([[:alpha:]]){1}:/$} $v _ letter]} { + lappend driveletters $letter + } + } + #puts stderr "->$driveletters" + + set path [punk::objclone $unixypath] ;#take another copy that we can deliberatley shimmer to path and know is separate to the supplied argument + set supplied_pathtype [file pathtype $path] ;#we don't care about the pathtype so much as the act of making this call shimmers to a path internal-rep + + #copy of var that we can treat as a string without affecting path rep + #Note that some but not all read operations will lose path rep e.g 'string length $x' will lose any path-rep $x had, (but 'string equal $x something' won't) + #Todo - make int-rep tests to check stability of these behaviours across Tcl versions! + set strcopy_path [punk::objclone $path] + + set str_newpath "" + + set have_pathobj 0 + + if {[regexp $re_slash_x_slash $strcopy_path _ letter]} { + #upper case appears to be windows canonical form + set str_newpath [string toupper $letter]:/[string range $strcopy_path 3 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}/.*} [string tolower $strcopy_path] _ letter]} { + set str_newpath [string toupper $letter]:/[string range $strcopy_path 7 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}$} [string tolower $strcopy_path] _ letter]} { + set str_newpath [string toupper $letter]:/ + } elseif {[regexp $re_slash_else $strcopy_path _ firstpart remainder]} { + #could be for example /c or /something/users + if {[string length $firstpart] == 1} { + set letter $firstpart + set str_newpath [string toupper $letter]:/ + } else { + #according to regex we have a single leading slash + set str_tail [string range $strcopy_path 1 end] + if {$unixyroot eq ""} { + set unixyroot [get_unixyroot] + } else { + file pathtype $unixyroot; #side-effect generates int-rep of type path ) + } + set pathobj [file join $unixyroot $str_tail] + file pathtype $pathobj + set have_pathobj 1 + } + } + + if {!$have_pathobj} { + if {$str_newpath eq ""} { + #dunno - pass through + set pathobj $path + } else { + set pathobj [punk::objclone $str_newpath] + file pathtype $pathobj + } + } + + + + #puts stderr "=> $path" + #things like 'which' seem to return a path minus the .exe - so we'll just test the containing folder + # + #By now file normalize shouldn't do too many shannanigans related to cwd.. + #We want it to look at cwd for relative paths.. + #but we don't consider things like /c/Users to be relative even on windows where it would normally mean a volume-relative path e.g c:/c/Users if cwd happens to be somewhere on C: at the time. + #if {![file exists [file dirname $path]]} { + # set path [file normalize $path] + # #may still not exist.. that's ok. + #} + + + + #file normalize may change backslashes to forward slashes.. including things like the special \\?\ prefix which is intended to stop windows api from parsing a name + #2023 - this is ok as //?/ also seems to work.. but it is unclear if that is because Tcl is re-converting to backslashes + if {[punk::winpath::illegalname_test $pathobj]} { + set pathobj [punk::winpath::illegalname_fix $pathobj] + } + + return $pathobj + } + + #---------------------------------------------- + #leave the unixywindows related aliases available on all platforms + #interp alias {} cdwin {} punk::unixywindows::cdwin + #interp alias {} cdwindir {} punk::unixywindoes::cdwindir + #interp alias {} towinpath {} punk::unixywindows::towinpath + #interp alias {} windir {} punk::unixywindows::windir + #---------------------------------------------- + + + +} + + + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::unixywindows [namespace eval punk::unixywindows { + variable version + set version 0.1.0 +}] +return diff --git a/src/bootsupport/modules/punkapp-0.1.tm b/src/bootsupport/modules/punkapp-0.1.tm new file mode 100644 index 00000000..ce46856b --- /dev/null +++ b/src/bootsupport/modules/punkapp-0.1.tm @@ -0,0 +1,239 @@ +#utilities for punk apps to call + +package provide punkapp [namespace eval punkapp { + variable version + set version 0.1 +}] + +namespace eval punkapp { + variable result + variable waiting "no" + proc hide_dot_window {} { + #alternative to wm withdraw . + #see https://wiki.tcl-lang.org/page/wm+withdraw + wm geometry . 1x1+0+0 + wm overrideredirect . 1 + wm transient . + } + proc is_toplevel {w} { + if {![llength [info commands winfo]]} { + return 0 + } + expr {[winfo toplevel $w] eq $w && ![catch {$w cget -menu}]} + } + proc get_toplevels {{w .}} { + if {![llength [info commands winfo]]} { + return [list] + } + set list {} + if {[is_toplevel $w]} { + lappend list $w + } + foreach w [winfo children $w] { + lappend list {*}[get_toplevels $w] + } + return $list + } + + proc make_toplevel_next {prefix} { + set top [get_toplevel_next $prefix] + return [toplevel $top] + } + #possible race condition if multiple calls made without actually creating the toplevel, or gap if highest existing closed in the meantime + #todo - reserve_toplevel_next ? keep list of toplevels considered 'allocated' even if never created or already destroyed? what usecase? + #can call wm withdraw to to reserve newly created toplevel. To stop re-use of existing names after destruction would require a list or at least a record of highest created for each prefix + proc get_toplevel_next {prefix} { + set base [string trim $prefix .] ;# .myapp -> myapp .myapp.somewindow -> myapp.somewindow . -> "" + + + + } + proc exit {{toplevel ""}} { + variable waiting + variable result + variable default_result + set toplevels [get_toplevels] + if {[string length $toplevel]} { + set wposn [lsearch $toplevels $toplevel] + if {$wposn > 0} { + destroy $toplevel + } + } else { + #review + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + puts stderr "punkapp::exit called without toplevel - showing console" + show_console + return 0 + } else { + puts stderr "punkapp::exit called without toplevel - exiting" + if {$waiting ne "no"} { + if {[info exists result(shell)]} { + set temp [set result(shell)] + unset result(shell) + set waiting $temp + } else { + set waiting "" + } + } else { + ::exit + } + } + } + + set controllable [get_user_controllable_toplevels] + if {![llength $controllable]} { + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + show_console + } else { + if {$waiting ne "no"} { + if {[info exists result(shell)]} { + set temp [set result(shell)] + unset result(shell) + set waiting $temp + } elseif {[info exists result($toplevel)]} { + set temp [set result($toplevel)] + unset result($toplevel) + set waiting $temp + } elseif {[info exists default_result]} { + set temp $default_result + unset default_result + set waiting $temp + } else { + set waiting "" + } + } else { + ::exit + } + } + } + } + proc close_window {toplevel} { + wm withdraw $toplevel + if {![llength [get_user_controllable_toplevels]]} { + punkapp::exit $toplevel + } + destroy $toplevel + } + proc wait {args} { + variable waiting + variable default_result + if {[dict exists $args -defaultresult]} { + set default_result [dict get $args -defaultresult] + } + foreach t [punkapp::get_toplevels] { + if {[wm protocol $t WM_DELETE_WINDOW] eq ""} { + wm protocol $t WM_DELETE_WINDOW [list punkapp::close_window $t] + } + } + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + puts stderr "repl eventloop seems to be running - punkapp::wait not required" + } else { + if {$waiting eq "no"} { + set waiting "waiting" + vwait ::punkapp::waiting + return $::punkapp::waiting + } + } + } + + #A window can be 'visible' according to this - but underneath other windows etc + #REVIEW - change name? + proc get_visible_toplevels {{w .}} { + if {![llength [info commands winfo]]} { + return [list] + } + set list [get_toplevels $w] + set mapped [lmap v $list {expr {[winfo ismapped $v] ? $v : {}}}] + set mapped [concat {*}$mapped] ;#ignore {} + set visible [list] + foreach m $mapped { + if {[wm overrideredirect $m] == 0 } { + lappend visible $m + } else { + if {[winfo height $m] >1 && [winfo width $m] > 1} { + #technically even a 1x1 is visible.. but in practice even a 10x10 is hardly likely to be noticeable when overrideredirect == 1 + #as a convention - 1x1 with no controls is used to make a window invisible so we'll treat anything larger as visible + lappend visible $m + } + } + } + return $visible + } + proc get_user_controllable_toplevels {{w .}} { + set visible [get_visible_toplevels $w] + set controllable [list] + foreach v $visible { + if {[wm overrideredirect $v] == 0} { + lappend controllable $v + } + } + #only return visible windows with overrideredirect == 0 because there exists some user control. + #todo - review.. consider checking if position is outside screen areas? Technically controllable.. but not easily + return $controllable + } + proc hide_console {args} { + set opts [dict create -force 0] + if {([llength $args] % 2) != 0} { + error "hide_console expects pairs of arguments. e.g -force 1" + } + #set known_opts [dict keys $defaults] + foreach {k v} $args { + switch -- $k { + -force { + dict set opts $k $v + } + default { + error "Unrecognised options '$k' known options: [dict keys $opts]" + } + } + } + set force [dict get $opts -force] + + if {!$force} { + if {![llength [get_user_controllable_toplevels]]} { + puts stderr "Cannot hide console while no user-controllable windows available" + return 0 + } + } + if {$::tcl_platform(platform) eq "windows"} { + #hide won't work for certain consoles cush as conemu,wezterm - and doesn't really make sense for tabbed windows anyway. + #It would be nice if we could tell the console window to hide just the relevant tab - or the whole window if only one tab present - but this is unlikely to be possible in any standard way. + #an ordinary cmd.exe or pwsh.exe or powershell.exe window can be hidden ok though. + #(but with wezterm - process is cmd.exe - but it has style popup and can't be hidden with a twapi::hide_window call) + package require twapi + set h [twapi::get_console_window] + set pid [twapi::get_window_process $h] + set pinfo [twapi::get_process_info $pid -name] + set pname [dict get $pinfo -name] + set wstyle [twapi::get_window_style $h] + #tclkitsh/tclsh? + if {($pname in [list cmd.exe pwsh.exe powershell.exe] || [string match punk*.exe $pname]) && "popup" ni $wstyle} { + twapi::hide_window $h + return 1 + } else { + puts stderr "punkapp::hide_console unable to hide this type of console window" + return 0 + } + } else { + #todo + puts stderr "punkapp::hide_console unimplemented on this platform (todo)" + return 0 + } + } + + proc show_console {} { + if {$::tcl_platform(platform) eq "windows"} { + package require twapi + if {![catch {set h [twapi::get_console_window]} errM]} { + twapi::show_window $h -activate -normal + } else { + #no console - assume launched from something like wish? + catch {console show} + } + } else { + #todo + puts stderr "punkapp::show_console unimplemented on this platform" + } + } + +} diff --git a/src/bootsupport/modules/punkcheck/cli-0.1.0.tm b/src/bootsupport/modules/punkcheck/cli-0.1.0.tm new file mode 100644 index 00000000..609df5c3 --- /dev/null +++ b/src/bootsupport/modules/punkcheck/cli-0.1.0.tm @@ -0,0 +1,333 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application punkcheck::cli 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + +package require punk::mix::util + +namespace eval punkcheck::cli { + namespace ensemble create + #package require punk::overlay + #punk::overlay::import_commandset debug. ::punk:mix::commandset::debug + + #init proc required - used for lazy loading of commandsets + variable initialised 0 + proc _init {args} { + variable initialised + if {$initialised} { + return + } + puts stderr "punkcheck::cli::init $args" + + set initialised 1 + } + + proc help {args} { + set basehelp [punk::mix::base help {*}$args] + return $basehelp + } + + proc paths {{path {}}} { + if {$path eq {}} { set path [pwd] } + set search_from $path + set bottom_to_top [list] + while {[string length [set pcheck_file [punkcheck::cli::lib::find_nearest_file $search_from]]]} { + set pcheck_folder [file dirname $pcheck_file] + lappend bottom_to_top $pcheck_file + set search_from [file dirname $pcheck_folder] + } + return $bottom_to_top + } + #todo! - group by fileset + proc status {{path {}}} { + if {$path eq {}} { set path [pwd] } + set fullpath [file normalize $path] + + set ftype [file type $fullpath] + + + set files [list] + if {$ftype eq "file"} { + set container [file dirname $fullpath] + lappend files $fullpath + } else { + set container $fullpath + #vfs can mask mounted files - so we can't just use 'file type' or glob with -type f + ##set files [glob -nocomplain -dir $fullpath -type f *] + package require punk::nav::fs + set folderinfo [punk::nav::fs::dirfiles_dict $fullpath] + set files [concat [dict get $folderinfo files] [dict get $folderinfo underlayfiles]] + } + set punkcheck_files [paths $container] + set repodict [punk::repo::find_repo $container] + + if {![llength $punkcheck_files]} { + puts stderr "No .punkcheck files found at or above this folder" + } + + set table "" + set files_with_records [list] + foreach p $punkcheck_files { + set basedir [file dirname $p] + set recordlist [punkcheck::load_records_from_file $p] + set tgt_dict [punkcheck::recordlist::records_as_target_dict $recordlist] + + foreach f $files { + set relpath [punkcheck::lib::path_relative $basedir $f] + + if {[dict exists $tgt_dict $relpath]} { + set filerec [dict get $tgt_dict $relpath] + set records [punkcheck::dict_getwithdefault $filerec body [list]] + if {$ftype eq "file"} { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set pcheck \n + foreach irec $records { + append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } else { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set display_records [list] + set pcheck \n + set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] + if {[llength $latest_install_record]} { + lappend display_records $latest_install_record + } + if {$latest_install_record ne [lindex $records end]} { + lappend display_records [lindex $records end] + } + foreach irec $display_records { + append pcheck "[format %-14s [dict get $irec tag]] [punkcheck::dict_getwithdefault $irec -tsiso "no-timestamp"]" + set bodyrecords [punkcheck::dict_getwithdefault $irec body [list]] + set source_files [list] + set source_files_changed [list] + set source_folders [list] + set source_folders_changed [list] + foreach r $bodyrecords { + if {[dict get $r tag] eq "SOURCE"} { + set path [dict get $r -path] + set changed [dict get $r -changed] + switch -- [dict get $r -type] { + file { + lappend source_files $path + if {$changed} { + lappend source_files_changed $path + } + } + directory { + lappend source_folders $path + if {$changed} { + lappend source_folders_changed $path + } + } + } + } + } + if {[llength $source_files]} { + append pcheck " source files : [llength $source_files] (changed [llength $source_files_changed])" + } + if {[llength $source_folders]} { + append pcheck " source folders: [llength $source_folders] (changed [llength $source_folders_changed])" + } + append pcheck \n + + #append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } + append table "$f $pcheck" \n + } + } + } + return $table + } + proc status_by_file {{path {}}} { + if {$path eq {}} { set path [pwd] } + set fullpath [file normalize $path] + set ftype [file type $fullpath] + set files [list] + if {$ftype eq "file"} { + set container [file dirname $fullpath] + lappend files $fullpath + } else { + set container $fullpath + set files [glob -nocomplain -dir $fullpath -type f *] + } + set punkcheck_files [paths $container] + set repodict [punk::repo::find_repo $container] + + if {![llength $punkcheck_files]} { + puts stderr "No .punkcheck files found at or above this folder" + } + + set table "" + set files_with_records [list] + foreach p $punkcheck_files { + set basedir [file dirname $p] + set recordlist [punkcheck::load_records_from_file $p] + set tgt_dict [punkcheck::recordlist::records_as_target_dict $recordlist] + foreach f $files { + set relpath [punkcheck::lib::path_relative $basedir $f] + + if {[dict exists $tgt_dict $relpath]} { + set filerec [dict get $tgt_dict $relpath] + set records [punkcheck::dict_getwithdefault $filerec body [list]] + if {$ftype eq "file"} { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set pcheck \n + foreach irec $records { + append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } else { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set display_records [list] + set pcheck \n + set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] + lappend display_records $latest_install_record + if {$latest_install_record ne [lindex $records end]} { + lappend display_records [lindex $records end] + } + foreach irec $display_records { + append pcheck "[format %-14s [dict get $irec tag]] [punkcheck::dict_getwithdefault $irec -tsiso "no-timestamp"]" + set bodyrecords [punkcheck::dict_getwithdefault $irec body [list]] + set source_files [list] + set source_files_changed [list] + set source_folders [list] + set source_folders_changed [list] + foreach r $bodyrecords { + if {[dict get $r tag] eq "SOURCE"} { + set path [dict get $r -path] + set changed [dict get $r -changed] + switch -- [dict get $r -type] { + file { + lappend source_files $path + if {$changed} { + lappend source_files_changed $path + } + } + directory { + lappend source_folders $path + if {$changed} { + lappend source_folders_changed $path + } + } + } + } + } + if {[llength $source_files]} { + append pcheck " source files : [llength $source_files] (changed [llength $source_files_changed])" + } + if {[llength $source_folders]} { + append pcheck " source folders: [llength $source_folders] (changed [llength $source_folders_changed])" + } + append pcheck \n + + #append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } + append table "$f $pcheck" \n + } + } + } + return $table + } +} + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punkcheck::cli::lib { + namespace path ::punk::mix::util ;#askuser, do_in_path, foreach-file etc + + proc find_nearest_file {{path {}}} { + if {$path eq {}} { set path [pwd] } + set folder [lib::scanup $path lib::is_punkchecked_folder] + if {$folder eq ""} { + return "" + } else { + return [file join $folder .punkcheck] + } + } + + proc is_punkchecked_folder {{path {}}} { + if {$path eq {}} { set path [pwd] } + foreach control { + .punkcheck + } { + set control [file join $path $control] + if {[file isfile $control]} {return 1} + } + return 0 + } + + proc scanup {path cmd} { + if {$path eq {}} { set path [pwd] } + #based on kettle::path::scanup + if {[file pathtype $path] eq "relative"} { + set path [file normalize $path] + } + while {1} { + # Found the proper directory, per the predicate. + if {[{*}$cmd $path]} { return $path } + + # Not found, walk to parent + set new [file dirname $path] + + # Stop when reaching the root. + if {$new eq $path} { return {} } + if {$new eq {}} { return {} } + + # Ok, truly walk up. + set path $new + } + return {} + } + +} + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punkcheck::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command status + package require punk::mix::base + package require punk::overlay + punk::overlay::custom_from_base [namespace current] ::punk::mix::base +} + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punkcheck::cli [namespace eval punkcheck::cli { + variable version + set version 0.1.0 +}] +return + + diff --git a/src/bootsupport/modules/shellfilter-0.1.9.tm b/src/bootsupport/modules/shellfilter-0.1.9.tm new file mode 100644 index 00000000..b8f4dec0 --- /dev/null +++ b/src/bootsupport/modules/shellfilter-0.1.9.tm @@ -0,0 +1,3078 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + + #'tag' is an identifier for the log source. + # each tag will use it's own thread to write to the configured log target + proc open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + package require shellthread + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #note new_worker + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + + +namespace eval shellfilter::ansi { + #maint warning - + #ansistrip from punk::ansi is better/more comprehensive + proc stripcodes {text} { + #obsolete? + #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). + dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. + #line endings can theoretically occur within an ansi escape sequence (review e.g title?) + set inputlist [split $text ""] + set outputlist [list] + + #self-contained 2 byte ansi escape sequences - review more? + set 2bytecodes_dict [dict create\ + "reset_terminal" "\033c"\ + "save_cursor_posn" "\u001b7"\ + "restore_cursor_posn" "\u001b8"\ + "cursor_up_one" "\u001bM"\ + ] + set 2bytecodes [dict values $2bytecodes_dict] + + set in_escapesequence 0 + #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls + set i 0 + foreach u $inputlist { + set v [lindex $inputlist $i+1] + set uv ${u}${v} + if {$in_escapesequence eq "2b"} { + #2nd byte - done. + set in_escapesequence 0 + } elseif {$in_escapesequence != 0} { + set escseq [dict get $escape_terminals $in_escapesequence] + if {$u in $escseq} { + set in_escapesequence 0 + } elseif {$uv in $escseq} { + set in_escapseequence 2b ;#flag next byte as last in sequence + } + } else { + #handle both 7-bit and 8-bit CSI and OSC + if {[regexp {^(?:\033\[|\u009b)} $uv]} { + set in_escapesequence CSI + } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { + set in_escapesequence OSC + } elseif {$uv in $2bytecodes} { + #self-contained e.g terminal reset - don't pass through. + set in_escapesequence 2b + } else { + lappend outputlist $u + } + } + incr i + } + return [join $outputlist ""] + } + +} +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method flush {ch} { + return "" + } + method write {ch bytes} { + set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + foreach v $o_datavars { + append $v $stringdata + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method write {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [encoding convertfrom $o_enc $bytes] + if 0 { + if {"utf-16le" in [encoding names]} { + set logdata [encoding convertfrom utf-16le $bytes] + } else { + set logdata [encoding convertto utf-8 $bytes] + #set logdata [encoding convertfrom unicode $bytes] + #set logdata $bytes + } + } + #set logdata $bytes + #set logdata [string map [list \r -r- \n -n-] $logdata] + #if {[string equal [string range $logdata end-1 end] "\r\n"]} { + # set logdata [string range $logdata 0 end-2] + #} + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $logdata + #return $bytes + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [dict get $tf -encoding] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_colour + variable o_do_colour + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method Trackcodes {chunk} { + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + append emit $o_do_colour$pt$o_do_normal + #append emit $pt + } else { + append emit $pt + } + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } else { + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + append emit $o_do_colour$trailing_pt$o_do_normal + } else { + append emit $trailing_pt + } + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string last \x1b $buf] == [llength $buf]-1} { + #only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit [string range $buf 0 end-1] + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[llength $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + } else { + set emit_anyway 1 + } + } else { + set possible_code_len [expr {[llength $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set emit "" + } else { + set emit_anyway 1 + } + } + } + } + if {$emit_anyway} { + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + set emit $o_do_colour$buf$o_do_normal + } else { + set emit $buf + } + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + set emit $o_do_colour$buf$o_do_normal + } else { + set emit $buf + } + #set emit $buf + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + return + } + method write {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set streaminfo [my Trackcodes $instring] + set emit [dict get $streaminfo emit] + if {[dict get $streaminfo stacksize] == 0} { + #no ansi on the stack - we can wrap + #review + set outstring "$o_do_colour$emit$o_do_normal" + } else { + set outstring $emit + } + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $outstring] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + #todo - implement as oo + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + set t [textblock::class::table new $tableprefix] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + set stack [lreplace $stack $idposn end] + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + set stack [lreplace $stack $bottom_pop_posn end] + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + #add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + ::shellfilter::log::open $tag {-syslog ""} + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create \ + -tag redirected_$chan -syslog "" -file ""\ + ] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list \ + \r cr\ + \n lf\ + {"} doublequote\ + {'} singlequote\ + "`" backquote\ + "^" caret\ + \t tab\ + " " sp\ + "\[" lsquare\ + "\]" rsquare\ + "(" lbracket\ + ")" rbracket\ + "\{" lbrace\ + "\}" rbrace\ + \\ backslash\ + / forwardslash\ + ] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list \ + "\"" [list "\"" "\"" "\""]\ + {^} [list "\"" "\"" "^"]\ + "'" [list "'" "'" "'"]\ + "\{" [list "\{" "\}" "\}"]\ + {[} [list {[} {]} {]}]\ + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create \ + -teehandle command \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -tclscript 0 \ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if $experiment { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list \ + -timeout \ + -outprefix \ + -errprefix \ + -debug \ + -copytempfile \ + -outbuffering \ + -errbuffering \ + -inbuffering \ + -readprocesstranslation \ + -outtranslation \ + -stdinhandler \ + -outchan \ + -errchan \ + -inchan \ + -teehandle\ + ] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {([llength $args] % 2) != 0} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -outbuffering none \ + -errbuffering none \ + -readprocesstranslation auto \ + -outtranslation lf \ + -inbuffering none \ + -timeout 900000\ + -outprefix ""\ + -errprefix ""\ + -debug 0\ + -copytempfile 0\ + -stdinhandler ""\ + ] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + chan configure $wrerr -blocking 0 + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem\ + {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + }\ + {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list \ + [chan configure $inchan -buffering] \ + [chan configure $outchan -buffering] \ + [chan configure $errchan -buffering] \ + ] + + set remember_in_out_err_translation [list \ + [chan configure $inchan -translation] \ + [chan configure $outchan -translation] \ + [chan configure $errchan -translation] \ + ] + + + + + + chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + after $timeout [string map [list %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }] + + + vwait $waitvar + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.1.9 +}] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argp-0.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argp-0.2.tm new file mode 100644 index 00000000..1b1f4b78 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argp-0.2.tm @@ -0,0 +1,259 @@ + +# Tcl parser for optional arguments in function calls and +# commandline arguments +# +# (c) 2001 Bastien Chevreux + +# Index of exported commands +# - argp::registerArgs +# - argp::setArgDefaults +# - argp::setArgsNeeded +# - argp::parseArgs + +# Internal commands +# - argp::CheckValues + +# See end of file for an example on how to use + +package provide argp 0.2 + +namespace eval argp { + variable Optstore + variable Opttypes { + boolean integer double string + } + + namespace export {[a-z]*} +} + + +proc argp::registerArgs { func arglist } { + variable Opttypes + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + #puts $parentns + #puts $caller + #puts $cmangled + + set Optstore(keys,$cmangled) {} + set Optstore(deflist,$cmangled) {} + set Optstore(argneeded,$cmangled) {} + + foreach arg $arglist { + foreach {opt type default allowed} $arg { + set optindex [lsearch -glob $Opttypes $type*] + if { $optindex < 0} { + return -code error "$caller, unknown type $type while registering arguments.\nAllowed types: [string trim $Opttypes]" + } + set type [lindex $Opttypes $optindex] + + lappend Optstore(keys,$cmangled) $opt + set Optstore(type,$opt,$cmangled) $type + set Optstore(default,$opt,$cmangled) $default + set Optstore(allowed,$opt,$cmangled) $allowed + lappend Optstore(deflist,$cmangled) $opt $default + } + } + + if { [catch {CheckValues $caller $cmangled $Optstore(deflist,$cmangled)} res]} { + return -code error "Error in declaration of optional arguments.\n$res" + } +} + +proc argp::setArgDefaults { func arglist } { + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + set Optstore(deflist,$cmangled) {} + foreach {opt default} $arglist { + if {![info exists Optstore(default,$opt,$cmangled)]} { + return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)" + } + set Optstore(default,$opt,$cmangled) $default + } + + # set the new defaultlist + foreach opt $Optstore(keys,$cmangled) { + lappend Optstore(deflist,$cmangled) $opt $Optstore(default,$opt,$cmangled) + } +} + +proc argp::setArgsNeeded { func arglist } { + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + #append caller $parentns :: $func + #set cmangled ${parentns}_$func + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + + set Optstore(argneeded,$cmangled) {} + foreach opt $arglist { + if {![info exists Optstore(default,$opt,$cmangled)]} { + return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)" + } + lappend Optstore(argneeded,$cmangled) $opt + } +} + + +proc argp::parseArgs { args } { + variable Optstore + + if {[llength $args] == 0} { + upvar args a opts o + } else { + upvar args a [lindex $args 0] o + } + + if { [ catch { set caller [lindex [info level -1] 0]}]} { + set caller "main program" + set cmangled "" + } else { + set cmangled [string map {:: _} $caller] + } + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + + # set the defaults + array set o $Optstore(deflist,$cmangled) + + # but unset the needed arguments + foreach key $Optstore(argneeded,$cmangled) { + catch { unset o($key) } + } + + foreach {key val} $a { + if {![info exists Optstore(type,$key,$cmangled)]} { + return -code error "$caller, unknown option $key, must be one of: $Optstore(keys,$cmangled)" + } + switch -exact -- $Optstore(type,$key,$cmangled) { + boolean - + integer { + if { $val == "" } { + return -code error "$caller, $key empty string is not $Optstore(type,$key,$cmangled) value." + } + if { ![string is $Optstore(type,$key,$cmangled) $val]} { + return -code error "$caller, $key $val is not $Optstore(type,$key,$cmangled) value." + } + } + double { + if { $val == "" } { + return -code error "$caller, $key empty string is not double value." + } + if { ![string is double $val]} { + return -code error "$caller, $key $val is not double value." + } + if { [string is integer $val]} { + set val [expr {$val + .0}] + } + } + default { + } + } + set o($key) $val + } + + foreach key $Optstore(argneeded,$cmangled) { + if {![info exists o($key)]} { + return -code error "$caller, needed argument $key was not given." + } + } + + if { [catch { CheckValues $caller $cmangled [array get o]} err]} { + return -code error $err + } + + return +} + + +proc argp::CheckValues { caller cmangled checklist } { + variable Optstore + + #puts "Checking $checklist" + + foreach {key val} $checklist { + if { [llength $Optstore(allowed,$key,$cmangled)] > 0 } { + switch -exact -- $Optstore(type,$key,$cmangled) { + string { + if { [lsearch $Optstore(allowed,$key,$cmangled) $val] < 0} { + return -code error "$caller, $key $val is not in allowed values: $Optstore(allowed,$key,$cmangled)" + } + } + double - + integer { + set found 0 + foreach range $Optstore(allowed,$key,$cmangled) { + if {[llength $range] == 1} { + if { $val == [lindex $range 0] } { + set found 1 + break + } + } elseif {[llength $range] == 2} { + set low [lindex $range 0] + set high [lindex $range 1] + + if { ![string is integer $low] \ + && [string compare "-" $low] != 0} { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a lower value range that is not integer and not \u00b4-\u00b4: $range" + } + if { ![string is integer $high] \ + && [string compare "+" $high] != 0} { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a upper value range that is not integer and not \u00b4+\u00b4: $range" + } + if {[string compare "-" $low] == 0} { + if { [string compare "+" $high] == 0 \ + || $val <= $high } { + set found 1 + break + } + } + if { $val >= $low } { + if {[string compare "+" $high] == 0 \ + || $val <= $high } { + set found 1 + break + } + } + } else { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has an allowed value range containing more than 2 elements: $range" + } + } + if { $found == 0 } { + return -code error "$caller, $key $val is not covered by allowed ranges: $Optstore(allowed,$key,$cmangled)" + } + } + } + } + } +} diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/debug-1.0.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/debug-1.0.6.tm new file mode 100644 index 00000000..c2ee57be --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/debug-1.0.6.tm @@ -0,0 +1,306 @@ +# Debug - a debug narrative logger. +# -- Colin McCormack / originally Wub server utilities +# +# Debugging areas of interest are represented by 'tokens' which have +# independantly settable levels of interest (an integer, higher is more detailed) +# +# Debug narrative is provided as a tcl script whose value is [subst]ed in the +# caller's scope if and only if the current level of interest matches or exceeds +# the Debug call's level of detail. This is useful, as one can place arbitrarily +# complex narrative in code without unnecessarily evaluating it. +# +# TODO: potentially different streams for different areas of interest. +# (currently only stderr is used. there is some complexity in efficient +# cross-threaded streams.) + +# # ## ### ##### ######## ############# ##################### +## Requisites + +package require Tcl 8.5- + +namespace eval ::debug { + namespace export -clear \ + define on off prefix suffix header trailer \ + names 2array level setting parray pdict \ + nl tab hexl + namespace ensemble create -subcommands {} +} + +# # ## ### ##### ######## ############# ##################### +## API & Implementation + +proc ::debug::noop {args} {} + +proc ::debug::debug {tag message {level 1}} { + variable detail + if {$detail($tag) < $level} { + #puts stderr "$tag @@@ $detail($tag) >= $level" + return + } + + variable prefix + variable suffix + variable header + variable trailer + variable fds + + if {[info exists fds($tag)]} { + set fd $fds($tag) + } else { + set fd stderr + } + + # Assemble the shown text from the user message and the various + # prefixes and suffices (global + per-tag). + + set themessage "" + if {[info exists prefix(::)]} { append themessage $prefix(::) } + if {[info exists prefix($tag)]} { append themessage $prefix($tag) } + append themessage $message + if {[info exists suffix($tag)]} { append themessage $suffix($tag) } + if {[info exists suffix(::)]} { append themessage $suffix(::) } + + # Resolve variables references and command invokations embedded + # into the message with plain text. + set code [catch { + set smessage [uplevel 1 [list ::subst -nobackslashes $themessage]] + set sheader [uplevel 1 [list ::subst -nobackslashes $header]] + set strailer [uplevel 1 [list ::subst -nobackslashes $trailer]] + } __ eo] + + # And dump an internal error if that resolution failed. + if {$code} { + if {[catch { + set caller [info level -1] + }]} { set caller GLOBAL } + if {[string length $caller] >= 1000} { + set caller "[string range $caller 0 200]...[string range $caller end-200 end]" + } + foreach line [split $caller \n] { + puts -nonewline $fd "@@(DebugError from $tag ($eo): $line)" + } + return + } + + # From here we have a good message to show. We only shorten it a + # bit if its a bit excessive in size. + + if {[string length $smessage] > 4096} { + set head [string range $smessage 0 2048] + set tail [string range $smessage end-2048 end] + set smessage "${head}...(truncated)...$tail" + } + + foreach line [split $smessage \n] { + puts $fd "$sheader$tag | $line$strailer" + } + return +} + +# names - return names of debug tags +proc ::debug::names {} { + variable detail + return [lsort [array names detail]] +} + +proc ::debug::2array {} { + variable detail + set result {} + foreach n [lsort [array names detail]] { + if {[interp alias {} debug.$n] ne "::debug::noop"} { + lappend result $n $detail($n) + } else { + lappend result $n -$detail($n) + } + } + return $result +} + +# level - set level and fd for tag +proc ::debug::level {tag {level ""} {fd {}}} { + variable detail + # TODO: Force level >=0. + if {$level ne ""} { + set detail($tag) $level + } + + if {![info exists detail($tag)]} { + set detail($tag) 1 + } + + variable fds + if {$fd ne {}} { + set fds($tag) $fd + } + + return $detail($tag) +} + +proc ::debug::header {text} { variable header $text } +proc ::debug::trailer {text} { variable trailer $text } + +proc ::debug::define {tag} { + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +# Set a prefix/suffix to use for tag. +# The global (tag-independent) prefix/suffix is adressed through tag '::'. +# This works because colon (:) is an illegal character for user-specified tags. + +proc ::debug::prefix {tag {theprefix {}}} { + variable prefix + set prefix($tag) $theprefix + + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +proc ::debug::suffix {tag {theprefix {}}} { + variable suffix + set suffix($tag) $theprefix + + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +# turn on debugging for tag +proc ::debug::on {tag {level ""} {fd {}}} { + variable active + set active($tag) 1 + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::debug $tag + return +} + +# turn off debugging for tag +proc ::debug::off {tag {level ""} {fd {}}} { + variable active + set active($tag) 1 + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::noop + return +} + +proc ::debug::setting {args} { + if {[llength $args] == 1} { + set args [lindex $args 0] + } + set fd stderr + if {[llength $args] % 2} { + set fd [lindex $args end] + set args [lrange $args 0 end-1] + } + foreach {tag level} $args { + if {$level > 0} { + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::debug $tag + } else { + level $tag [expr {-$level}] $fd + interp alias {} debug.$tag {} ::debug::noop + } + } + return +} + +# # ## ### ##### ######## ############# ##################### +## Convenience commands. +# Format arrays and dicts as multi-line message. +# Insert newlines and tabs. + +proc ::debug::nl {} { return \n } +proc ::debug::tab {} { return \t } + +proc ::debug::parray {a {pattern *}} { + upvar 1 $a array + if {![array exists array]} { + error "\"$a\" isn't an array" + } + pdict [array get array] $pattern +} + +proc ::debug::pdict {dict {pattern *}} { + set maxl 0 + set names [lsort -dict [dict keys $dict $pattern]] + foreach name $names { + if {[string length $name] > $maxl} { + set maxl [string length $name] + } + } + set maxl [expr {$maxl + 2}] + set lines {} + foreach name $names { + set nameString [format (%s) $name] + lappend lines [format "%-*s = %s" \ + $maxl $nameString \ + [dict get $dict $name]] + } + return [join $lines \n] +} + +proc ::debug::hexl {data {prefix {}}} { + set r {} + + # Convert the data to hex and to characters. + binary scan $data H*@0a* hexa asciia + + # Replace non-printing characters in the data with dots. + regsub -all -- {[^[:graph:] ]} $asciia {.} asciia + + # Pad with spaces to a full multiple of 32/16. + set n [expr {[string length $hexa] % 32}] + if {$n < 32} { append hexa [string repeat { } [expr {32-$n}]] } + #puts "pad H [expr {32-$n}]" + + set n [expr {[string length $asciia] % 32}] + if {$n < 16} { append asciia [string repeat { } [expr {16-$n}]] } + #puts "pad A [expr {32-$n}]" + + # Reassemble formatted, in groups of 16 bytes/characters. + # The hex part is handled in groups of 32 nibbles. + set addr 0 + while {[string length $hexa]} { + # Get front group of 16 bytes each. + set hex [string range $hexa 0 31] + set ascii [string range $asciia 0 15] + # Prep for next iteration + set hexa [string range $hexa 32 end] + set asciia [string range $asciia 16 end] + + # Convert the hex to pairs of hex digits + regsub -all -- {..} $hex {& } hex + + # Add the hex and latin-1 data to the result buffer + append r $prefix [format %04x $addr] { | } $hex { |} $ascii |\n + incr addr 16 + } + + # And done + return $r +} + +# # ## ### ##### ######## ############# ##################### + +namespace eval debug { + variable detail ; # map: TAG -> level of interest + variable prefix ; # map: TAG -> message prefix to use + variable suffix ; # map: TAG -> message suffix to use + variable fds ; # map: TAG -> handle of open channel to log to. + variable header {} ; # per-line heading, subst'ed + variable trailer {} ; # per-line ending, subst'ed + + # Notes: + # - The tag '::' is reserved. "prefix" and "suffix" use it to store + # the global message prefix / suffix. + # - prefix and suffix are applied per message. + # - header and trailer are per line. And should not generate multiple lines! +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide debug 1.0.6 +return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/flagfilter-0.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/flagfilter-0.3.tm new file mode 100644 index 00000000..1d37e215 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/flagfilter-0.3.tm @@ -0,0 +1,2714 @@ +#package provide flagfilter [namespace eval flagfilter {list [variable version 0.2.3]$version}] +#package provide [set ::pkg flagfilter-0.2.3] [namespace eval [lindex [split $pkg -] 0] {list [variable version [lindex [split $pkg -] 1][set ::pkg {}]]$version}] +# +#package provide [lindex [set pkg {flagfilter 0.2.3}] 0] [namespace eval [lindex $pkg 0] {list [variable version [lindex $pkg 1][set pkg {}]]$version}] +package provide [lassign {flagfilter 0.3} pkg ver]$pkg [namespace eval $pkg[set pkg {}] {list [variable version $::ver[set ::ver {}]]$version}] + +#Note: this is ugly.. particularly when trying to classify flags that are not fully specified i.e raw passthrough. +# - we can't know if a flag -x --x etc is expecting a parameter or not. +#0.2.2 2023-03 JN - added %match% placeholder support. Can be added to the dispatch command to tell it what command was actually matched. e.g tell xxx.tcl script that it was xxx.tcl when we matched on *.tcl + + +namespace eval flagfilter { + package require oolib ;# make 'oolib::collection new' available + + proc do_errorx {msg {code 1}} { + if {$::tcl_interactive} { + error $msg + } else { + puts stderr "|>err $msg" + exit $code + } + } + + proc do_error {msg {then error}} { + set levels [list debug info notice warn error critical alert emergency] + #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 syslog-like level descriptors + lassign $then type code + if {$code eq ""} { + set code 1 + } + set type [string tolower $type] + if {$type in [concat $levels exit]} { + puts -nonewline stderr "|$type> $msg\n" + } else { + puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '\n" + } + 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 -nonewline stderr " (exit suppressed due to tcl_interactive - raising error instead)\n" + if {![string is digit -strict $code]} { + puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '\n" + } + } + 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 -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '\n" + flush stderr + return -code error $msg + } + } + } + } + proc scriptdir {} { + set possibly_linked_script [file dirname [file normalize [file join [info script] ...]]] + if {[file isdirectory $possibly_linked_script]} { + return $possibly_linked_script + } else { + return [file dirname $possibly_linked_script] + } + } + +} + +package require overtype + + +namespace eval flagfilter { + namespace export get_one_flag_value + #review. Tcl can handle args like: {-a -val1 -b -val2} as long as they pair up. + #this will ignore flag-like values if they follow a -flag + # positional values that happen to start with - can still cause issues + #get_flagged_only can return an unpaired list if there are solos, or if it finds no value for the last flaglike element + # e.g from input {something -x -y -z} we will get {-x -y -z} + # + # + + #flagfilter::get_flagged_only may not always get things right when looking at a values list with command processors + #Even if all solos from commands are supplied in solodict - a flag might be solo only in the context of a particualar commandset + #The proper way to get flagged values from an arglist is to run the full parser. + #This then should be restricted to use for a specific subset of args where the supplied solodict is known to apply + proc get_flagged_only {arglist solodict} { + #solodict - solo flags with defaults + set solo_accumulator [dict create] ;#if multiple instances of solo flag found - append defaults to the value to form a list as long as the number of occurrences + #puts ">>>get_flagged_only input $arglist solodict:'$solodict'" + set result [list] + set last_was_flag 0 + set result [list] + set a_idx 0 + set end_of_options 0 + foreach a $arglist { + if {$a eq "--"} { + break + } + if {[dict exists $solodict $a]} { + set last_was_flag 0 + if {[dict exists $solo_accumulator $a]} { + set soloval [concat [dict get $solo_accumulator $a] [dict get $solodict $a]] + } else { + set soloval [dict get $solodict $a] + } + dict set solo_accumulator $a $soloval + #we need to keep order of first appearance + set idx [lsearch $result $a] + if {$idx < 0} { + lappend result $a $soloval + } else { + lset result $idx+1 $soloval + } + } else { + if {!$last_was_flag} { + if {$a eq "--"} { + + } else { + if {[lindex $arglist $a_idx-1] eq "--"} { + #end of options processing - none of the remaining are considered flags/options no matter what they look like + set last_was_flag 0 + break + } else { + if {[string match -* $a]} { + set last_was_flag 1 + lappend result $a ;#flag + } else { + #last wasnt, this isn't - don't output + set last_was_flag 0 + } + } + } + } else { + #we only look for single leading - in the value if last wasn't a flag - but we give -- and soloflags special treatment. + if {$a eq "--"} { + #last was flag + set last_was_flag 0 + } else { + lappend result $a ;#value + set last_was_flag 0 + } + } + } + incr a_idx + } + if {([llength $result] % 2) != 0} { + set last [lindex $result end] + if {[string match -* $last] && ($last ni [dict keys $solodict])} { + lappend result 1 + } + } + #puts ">>>get_flagged_only returning $result" + return $result + } + + + ## get_one_paired_flag_value + #best called with 'catch' unless flag known to be in arglist + #raises an error if no position available after the flag to retrieve value + #raises an error if flag not like -something + #raises an error if flag not found in list + proc get_one_paired_flag_value {arglist flag} { + if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $flag]} { + #regexp excludes plain - and -- + #if {![string match -* $flag]} {} + error "get_one_flag_value flag $flag does not look like a flag. Should be something like -$flag or --$flag" + } + set cindex [lsearch $arglist $flag] + if {$cindex >= 0} { + set valueindex [expr {$cindex + 1}] + if {$valueindex < [llength $arglist]} { + #puts stderr "++++++++++++++++++ get_one_flag_value flag '$flag' returning [lindex $arglist $valueindex]" + return [lindex $arglist $valueindex] + } else { + error "flagfilter::get_one_paired_flag_value no value corresponding to flag $flag (found flag, but reached end of list)" + } + } else { + error "flagfilter::get_one_paired_flag_value $flag not found in arglist: '$arglist'" + } + } +} + +namespace eval flagfilter::obj { + +} + + +namespace eval flagfilter { + variable run_counter 0 ;#Used by get_new_runid to form an id to represent run of main check_flags function. + #used as a basis for some object-instance names etc + proc get_new_runid {} { + variable run_counter + if {[catch {package require Thread}]} { + set tid 0 + } else { + set tid [thread::id] + } + return "ff-[pid]-${tid}-[incr run_counter]" + } + + namespace export check_flags + proc do_debug {lvl debugconfig msg} { + if {$lvl <= [dict get $debugconfig -debugargs]} { + foreach ln [split $msg \n] { + puts -nonewline stderr "|[dict get $debugconfig -source]> $ln\n" + flush stderr + } + } + } + + #---------------------------------------------------------------------- + # DO NOT RELY ON tcl::unsupported - it's named that for a reason and is not meant to be parsed + #wiki.tcl-lang.org/page/dict+tips+and+tricks + proc isdict {v} { + if {[string match "value is a list *" [::tcl::unsupported::representation $v]]} { + return [expr {!([llength $v] % 2)}] + } else { + return [string match "value is a dict *" [::tcl::unsupported::representation $v]] + } + } + + proc dict_format {dict} { + dictformat_rec $dict "" " " + } + proc dictformat_rec {dict indent indentstring} { + # unpack this dimension + set is_empty 1 + dict for {key value} $dict { + set is_empty 0 + if {[isdict $value]} { + append result "$indent[list $key]\n$indent\{\n" + append result "[dictformat_rec $value "$indentstring$indent" $indentstring]\n" + append result "$indent\}\n" + } else { + append result "$indent[list $key] [list $value]\n" + } + } + if {$is_empty} { + #experimental.. + append result "$indent\n" + #append result "" + } + return $result + } + #-------------------------------------------------------------------------- + + #solo 'category' includes longopts with value + #solo flags include the general list of -soloflags, and those specific to the current -commandprocessors spec (mashopts and singleopts) + proc is_this_flag_solo {f solos objp} { + if {![string match -* $f]} { + #not even flaglike + return 0 + } + + + if {$f in $solos} { + #review! - global -soloflags shouldn't override the requirements of a commandprocessor! + #but.. each commandprocessor needs to understand global solos occuring before our match so that we classify correctly.. + #todo - this may need to reference v_map and current position in scanlist to do properly + return 1 + } + if {$f eq "-"} { + #unless the caller declared it as a solo - treat this as a non flag element. (likely use is as a command match) + return 0 + } + if {$f eq "--"} { + #this is it's own type endofoptions + return 0 + } + + set p_opts [$objp get_combined_opts] + + set mashopts [dict get $p_opts mashopts] + set singleopts [dict get $p_opts singleopts] + set pairopts [dict get $p_opts pairopts] + set longopts [dict get $p_opts longopts] + + if {$f in $singleopts} { + return 1 + } + + #"any" keywords used by processors to consume anything - where we're not too worried about classifying a flagvalue vs an operand + #examine these last so that an explicit configuration of flags as pairopts,mashopts etc can still be classified correctly + if {"any" in $singleopts} { + return 1 + } + if {[string first "=" $f] >=1} { + if {"any" in $longopts} { + return 1 + } + #todo foreach longopt - split on = and search + } + + #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash - but if it's a pairopt, but not mashable - we can rule it out now + if {($f in $pairopts) && ($f ni $mashopts)} { + return 0 + } + #todo - suport mashes where one of the mashed flags takes an arg - review: only valid if it's last in the mash? + #(presumably so - unless we there was some other value delimiter such as isnumeric or capitalised flags vs lowercase values - but that seems a step too far - would require some sort of mashspec/mash-strategy config) + #last part of mash may actually be the value too. which complicates things + #linux ls seems to do this for example: + # ls -w 0 + # ls -lw 0 + # ls -lw0 + # also man.. e.g + # man -Tdvi + # man -Hlynx + # man -H + # - note this last one. '-H lynx' doesn't work - so it's a mashable opt that can take a value, but is not in pairopts! (-H with no value uses env value for browser) + # see also comments in is_this_flag_mash + # + + set flagletters [split [string range $f 1 end] ""] + set posn 1 + set is_solo 1 ;#default assumption to disprove + #trailing letters may legitimately not be in mashopts if they are part of a mashed value + #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing + foreach l $flagletters { + if {"-$l" ni $mashopts} { + #presumably an ordinary flag not-known to us + return 0 + } else { + if {"-$l" in $pairopts} { + if {$posn == [llength $flagletters]} { + #in pairopts and mash - but no value for it in the mash - thefore not a solo + return 0 + } else { + #entire tail is the value - this letter is effectively solo + return 1 + } + } elseif {"-$l" in $singleopts} { + #not allowed to take a value - keep processing letters + } else { + #can take a value! but not if at very end of mash. Either way This is a solo + return 1 + } + } + } + return $is_solo + } + #todo? support global (non-processor specific) mash list? -mashflags ? + proc is_this_flag_mash {f objp} { + if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $f]} { + #not even flaglike + return 0 + } + set optinfo [$objp get_combined_opts];#also applies to tail_processor - *usually* empty values for mashopts etc + + #we look at singleopts because even if the flag is in mashopts - when it is alone we don't classify it as a mash + set singleopts pdict get $optinfo singleopts] + if {$f in $singleopts} { + return 0 + } + + set pairopts [dict get $optinfo pairopts] + if {$f in [dict keys $pairopts]} { + #here, the entire arg (f) we are testing is in pairopts - it could still however appear as part of a mash, with or without a trailing value, and with or without other flags before it in the mash (but if neither prefixed,nor tailed then obviously not a mash) + return 0 + } + set mashopts [dict get $optinfo mashopts] + set flagletters [split [string range $f 1 end] ""] + set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash.. unless trailing one also takes a value + # .. in which case value could be at the tail of the mash.. or be the next arg in the list + # We will take absense from singleopts and pairopts to indicate the mashflag *optionally* takes a value + # (ie such a mashopt is a solo that can take a value only as a mashtail) + # presence in pairopts indicates a mashflag must have a value + # presense in singleopts indicates mashflag takes no value ever. + # mashopt cannot be in both singleopts and pairopts. (NAND) + foreach l $flagletters { + if {-$l in $pairopts} { + if {"-$l" in $mashopts} { + #need to consider any remainder in the mash as this value .. if no remainder - then this is a mash, but not 'solo' because this flag needs to consume the following arg. + # We are only concerned with mashness here so just stop processing mash elements when we hit the first one that is a pairopt + break + } else { + #we require the pairopt to explicitly be listed in mashopts as well as pairopts if it is to be allowed to be part of a mash + set is_mash 0 + } + } elseif {"-$l" in $singleopts} { + #singleopt & mashopt - cannot take a value, mashed or otherwise + if {"-$l" ni $mashopts} { + set is_mash 0 + } + } else { + if {"-$l" ni $mashopts} { + set is_mash 0 + } else { + #present only in mashopts - can take a value, but only immediately following in the mash + break + } + } + } + return $is_mash + } + proc is_this_flag_for_me {f objp cf_args} { + set processorname [$objp name] + set optinfo [$objp get_combined_opts] ;#also applies to tail_processor - *usually* empty values for mashopts etc + + if {$processorname in [list "tail_processor"]} { + return 1 + } + if {$processorname in [list "global"]} { + #todo - mashflags for global? + set defaults [dict get $cf_args -defaults] + set extras [dict get $cf_args -extras] + set soloflags [dict get $cf_args -soloflags] + if {$f in [concat $extras $soloflags [dict keys $defaults]]} { + return 1 + } + } + + set singleopts [dict get $optinfo singleopts] + if {"any" in [string tolower $singleopts]} { + #review semantics of 'all' here. does it mean any -xxx.. will match, or only if also in global -soloflags? + return 1 + } + set pairopts [dict get $optinfo pairopts] + set allopts [concat $singleopts [dict keys $pairopts]] + if {$f in $allopts} { + return 1 + } + + #process mashopts last + set mashopts [dict get $optinfo mashopts] + if {"any" in [string tolower $mashopts]} { + #if 'all' in mashopts - it can eat anything - review - is this even useful? + return 1 + } else { + set flagletters [split [string range $f 1 end] ""] + set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash + foreach l $flagletters { + if {"-$l" ni $mashopts} { + set is_mash 0 + } + } + return $is_mash + } + + return 0 + } + + + + proc add_dispatch_raw {recordvar parentname v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname raw] + lappend dispatchinfo $v + dict set drecord $parentname raw $dispatchinfo + } + } + proc add_dispatch_argument {recordvar parentname k v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname arguments] + lappend dispatchinfo $k $v ;#e.g -opt 1 + dict set drecord $parentname arguments $dispatchinfo + } + } + proc lsearch-all-stride-2 {l search} { + set posns [lmap i [lsearch -all $l $search] {expr {($i % 2) == 0 ? $i : [list x]}}] + return [lsearch -all -inline -not $posns x] + } + proc update_dispatch_argument {recordvar parentname k v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname arguments] + #can't assume there aren't repeat values e.g -v -v + #dict set dispatchinfo $k $v + if {[package vcompare [info tclversion] 8.7a5] >= 0} { + set posns [lsearch -all -stride 2 $dispatchinfo $k] + } else { + set posns [lsearch-all-stride-2 $dispatchinfo $k] + } + set lastitem [lindex $posns end] + if {[string length $lastitem]} { + set val_idx [expr {$lastitem + 1}] + set dispatchinfo [lreplace $dispatchinfo[set dispatchinfo {}] $val_idx $val_idx $v] ;# inlineK + dict set drecord $parentname arguments $dispatchinfo + } else { + error "Unable to update dispatch argument $k with value $v in dispatch record for $parentname" + } + #dict set drecord $parentname $dispatchinfo + } + } + + #Note the difference between this and is_command_match. + #Lack of a 'match' element does not cause a commandspec to skip allocating an operand it encounters + #Note that this isn't a general test to be applied to the entire argument list. + # - an arg may get matched by an earlier processor making it unavailable to be allocated by another processor + # so this test only applies during the ordered examination of args + proc can_this_commandspec_allocate_this_arg {flag cspec cf_args} { + set cmdinfo [lindex $cspec 1] + if {$cmdinfo eq "tail_processor"} { + return 1 + } + if {$cmdinfo eq "global"} { + set defaults [dict get $cf_args -defaults] + set soloflags [dict get $cf_args -soloflags] + set extras [dict get $cf_args -extras] + if {$flag in [concat $soloflags $extras [dict keys $defaults]]} { + return 1 + } + } + if {![dict exists $cmdinfo match]} { + return 1 + } + set matchspeclist [dict get $cmdinfo match] + foreach matchspec $matchspeclist { + if {[regexp -- $matchspec $flag]} { + return 1 + } + } + #only block it if there was a match pattern specified but it didn't match + return 0 + } + #Note - returns false for a cspec that has no match specified. + #A command/subcommand with no match specification is allowed to allocate any value - so be careful with this + # - it should not be used to *stop* an arg being allocated if the processor has no 'match' specified, or if it is another type of processor like 'tail_handler'. + proc is_command_match {flag cspec} { + set pinfo [lindex $cspec 1] + if {[dict exists $pinfo match]} { + set matchspeclist [dict get $pinfo match] + foreach matchspec $matchspeclist { + if {[regexp -- $matchspec $flag]} { + return 1 + } + } + return 0 + } else { + return 0 + } + } + proc is_command_match_any {f commandprocessors} { + foreach comspec $commandprocessors { + lassign $comspec cmdname cmdinfo + if {[dict exists $cmdinfo match]} { + set matchlist [dict get $cmdinfo match] + foreach matchspec $matchlist { + if {[regexp -- $matchspec $f]} { + #actually a command + return true + } + } + } + } + return false + } + + #determine if f is potentially a flag that takes a parameter from the next argument. + #e.g --x=y (longopt) does not consume following arg but --something *might* + proc is_candidate_toplevel_param_flag {f solos commandprocessors} { + if {[is_command_match_any $f $commandprocessors]} { + return false + } + if {$f in $solos} { + return 0 + } + if {$f in {- --}} { + return 0 + } + #longopts (--x=blah) and alternative --x blah + #possibly also -x=blah + if {[string match -* $f]} { + if {[string first "=" $f]>1} { + return 0 + } + } + return [expr {[string match -* $f]}] + } + + + + + + + + + + + + + + + + + + + #review - should we be using control::assert here? + #It depends if this is intended to raise error at runtime - would using control::assert and disabling assertions cause problems? + #todo - show caller info + proc assert_equal {a b} { + if {![expr {$a eq $b}]} { + error "assert_equal $a $b" + } + } + + + + + + #{1 unallocated 2 unallocated 3 unallocated 4 unallocated 5 unallocated 6 unallocated} ;#initial v_map + #1 2 3 4 5 6 ;#original list posns example + # 2 6 ;#map_remaining example (scanlist) + #1 3 4 5 ;#map_allocated example + #{1 {cmd1 operand} 2 unallocated 3 {cmd2 operand} 4 {cmd2 flag} 5 {cmd2 flagvalue} 6 unallocated} ;#v_map updated example + oo::class create class_vmap { + variable o_map + variable o_remaining + variable o_allocated + variable o_values + variable o_codemap + variable o_flagcategory + constructor {values} { + set o_codemap [dict create \ + operand op \ + flagvalue fv \ + soloflag so \ + flag fl \ + unallocated un \ + endofoptions eo \ + ] + set o_flagcategory [list "flag" "flagvalue" "soloflag"] + set o_values $values + #set o_remaining [lsearch -all $values *] ;#create a list of indices e.g 0 1 2 3 4 5 6 + #lsearch -all * is fast for very small lists - but lseq wins from size around 30+ + if {[llength $values]} { + if {[llength $values] < 30} { + #common case is short lists - but we don't want to penalize large lists + set o_remaining [lsearch -all $values *] + } else { + #punk::lib::range wraps lseq if available + set o_remaining [punk::lib::range 0 [llength $values]-1] + } + } else { + set o_remaining [list] + } + set o_allocated [list] + set o_map [list] + foreach posn $o_remaining { + lappend o_map $posn unallocated + } + } + method load {values rem alloc map} { + set o_values $values + set o_remaining $rem + set o_allocated $alloc + set o_map $map + } + method copy_to {obj} { + $obj load $o_values $o_remaining $o_allocated $o_map + } + method update_map_from {obj} { + #very basic sanity check first + if {[llength $o_values] ne [llength [$obj get_values]]} { + error "[self class].update_map_from cannot update. length of values mismatch" + } + + set newmap [$obj get_map] + } + + method get_codemap {} { + return $o_codemap + } + method get_values {} { + return $o_values + } + method get_remaining {} { + return $o_remaining + } + method get_allocated {} { + return $o_allocated + } + method get_map {} { + return $o_map + } + method argnum_from_remaining_posn {scanlist_posn} { + set vidx [lindex $o_remaining $scanlist_posn] + if {![string is digit -strict $vidx]} { + return -code error "[self class].argnum_from_remaining_posn cannot determine argnum from scanlist position:$scanlist_posn using unallocated list:'$o_remaining'" + } + return $vidx + } + + method allocate {objp argnum type value} { + set processorname [$objp name] + if {$processorname eq "tail_processor"} { + set owner "unallocated" + } else { + set owner [$objp parentname] + } + if {$argnum > [llength $o_values]-1} { + return -code error "[self class].allocate cannot allocate argnum:$argnum. Only [llength $o_values] items in value list" + } + if {$argnum in $o_allocated} { + return -code error "[self class].allocate already allocated '$processorname' argnum:'$argnum' type:'$type' val:'$value' remaining:$o_remaining allocated:$o_allocated map:$o_map" + } + lappend o_allocated $argnum + set o_allocated [lsort -dictionary $o_allocated] + dict set o_map $argnum [list $owner $type $value] + set scanlist_posn [lsearch $o_remaining $argnum] + set o_remaining [lreplace $o_remaining[set o_remaining {}] $scanlist_posn $scanlist_posn] ;#inlineK + + + + } + + method get_list_unflagged_by_class {classmatch} { + set resultlist [list] + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + switch -- $type { + flag - flagvalue - soloflag {} + default { + lappend resultlist $val + } + } + } + } + return $resultlist + } + + method get_list_flagged_by_class {classmatch} { + set list_flagged [list] + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + switch -- $type { + flag - flagvalue - soloflag { + lappend list_flagged $val + } + } + } + } + return $list_flagged + } + + method get_merged_flagged_by_class {classmatch} { + variable flagcategory + set all_flagged [list] + set seenflag [dict create] ;#key = -flagname val=earliest vindex + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + set a [llength $all_flagged] ;#index into all_flagged list we are building + switch -- $type { + soloflag { + if {[dict exists $seenflag $val]} { + set seenindex [dict get $seenflag $val] + set seenindexplus [expr {$seenindex+1}] + set existingvals [lindex $all_flagged $seenindexplus] + lappend existingvals 1 ;#1 indicating presence - stored as list rather than a count. todo: consider global or per-solo options to support incrementing instead? + lset all_flagged $seenindexplus $existingvals + } else { + dict set seenflag $val $a + lappend all_flagged $val 1 + } + } + flag { + if {![dict exists $seenflag $val]} { + dict set seenflag $val $a + lappend all_flagged $val + } + #no need to do anything if already seen - flagvalue must be next, and it will work out where to go. + } + flagvalue { + set idxflagfor [expr {$k -1}] + set flagforinfo [dict get $o_map $idxflagfor] + lassign $flagforinfo ffclass fftype ffval + #jn "--" following a flag could result in us getting here accidentaly.. review + set seenindex [dict get $seenflag $ffval] + if {$seenindex == [expr {$a-1}]} { + #usual case - this is a flagvalue following the first instance of the flag + lappend all_flagged $val + } else { + #write the value back to the seenindex+1 + set seenindexplus [expr {$seenindex+1}] + set existingvals [lindex $all_flagged $seenindexplus] + lappend existingvals $val ;#we keep multiples as a list + lset all_flagged $seenindexplus $existingvals + } + } + } + } + } + return $all_flagged + } + method typedrange_class_type_from_arg {argclass argtype} { + #set o_flagcategory [list "flag" "flagvalue" "soloflag"] + if {$argclass eq "unallocated"} { + switch -- $argtype { + flag - flagvalue - soloflag { + return [list unallocated flagtype] + } + default { + if {![string length $argtype]} { + #should only happen if something wrong with the tail_processor - rather than error out, for now at least make it stand out in the . + set argtype UNKNOWN + } + return [list unallocated ${argtype}type] ;#e.g unallocated_operand, unallocated_endofoptions + } + } + } else { + return [list $argclass argtype] ;# e.g command something + } + } + + method get_ranges_from_classifications {classifications} { + #puts stderr "get_ranges_from_classifications $classifications" + #examine classifications and create a list of ranges + set ranges [list];# e.g [list {unallocated 0 4} {cmd1 5 7} {unallocated 8 8} {cmd2 9 9} {cmd3 10 10} {unallocated 11 15}] + set seen_commands [list] + dict for {posn arginfo} $classifications { + set is_new_cmd 0 + set is_sub_cmd 0 + set is_continuation 0 + set rangename [lindex $ranges end 0] + set alloc [lindex $arginfo 0] ;#e.g of form 0 {unallocated operand} 1 {lscmd operand} 2 {lscmd soloflag} 3 {lscmd.dir operand} 4 {unallocated flag} + set cmdname "" + if {$alloc ne "unallocated"} { + if {$alloc ni $seen_commands} { + if {![llength $seen_commands]} { + set cmdname $alloc + set is_new_cmd 1 + } else { + set tail [lindex $seen_commands end] + if {$tail eq "unallocated"} { + set cmdname $alloc + set is_new_cmd 1 + } else { + if {[string first . $alloc] >= 0} { + set prefixcheck [lindex [split $alloc .] 0] + if {![string equal -length [string length $prefixcheck] $prefixcheck $tail]} { + #this is not unallocated, not a subcommand of the previous seen ie new command + set cmdname $alloc + set is_new_cmd 1 + } else { + set cmdname $prefixcheck + set is_sub_cmd 1 + set is_continuation 1 + } + } else { + set cmdname $alloc + set is_new_cmd 1 + } + } + } + } else { + set cmdname $alloc + set is_continuation 1 + } + if {$is_continuation} { + lassign [lindex $ranges end] _cmd n a b + set ranges [lrange $ranges 0 end-1] + lappend ranges [list command $n $a [incr b]] + flagfilter::assert_equal $b $posn + } elseif {$is_new_cmd} { + lappend seen_commands $alloc + if {$rangename eq ""} { + lappend ranges [list command $cmdname $posn $posn] + } else { + lassign [lindex $ranges end] _cmd n a b + lappend ranges [list command $cmdname [incr b] $posn] + flagfilter::assert_equal $b $posn + } + } else { + error "coding error during dispatch" + } + } else { + if {$rangename eq ""} { + lappend ranges [list unallocated mixed 0 0] + } else { + lassign [lindex $ranges end] class n a b + if {$class eq "unallocated"} { + #continuation - extend + set ranges [lrange $ranges 0 end-1] + lappend ranges [list unallocated mixed $a [incr b]] + } else { + #change from allocated to unallocated + lappend ranges [list unallocated mixed [incr b] $posn] + flagfilter::assert_equal $b $posn + } + } + } + } + set rangesbytype [list] + foreach oldrange $ranges { + lassign $oldrange oldrangeclass oldrangetype A B ;#A,B for original range bounds, a,b for bounds of sub-ranges we are creating + set last_type "" + set newrangelist [list] + set inner_range [list 0 0] + if {$oldrangeclass ne "unallocated"} { + #pass through - user can split commands further themselves by referencing the classifications map where each arg position is listed + set last_type $oldrangeclass ;#note the deliberate slight misuse - we are using the 'class' here rather than the type as we aren't looking at types within a command range + lappend rangesbytype $oldrange + } else { + #puts stdout "???????????????????????????????????????????????A$A B$B examining old range:'$oldrange'" + for {set i $A} {$i <= $B} {incr i} { + lassign [lindex $rangesbytype end] last_class last_type a b ;#enough just to use the type without the class + set a_info [dict get $classifications $i] + lassign $a_info argclass argtype v + lassign [my typedrange_class_type_from_arg $argclass $argtype] newrangeclass newrangetype + if {$last_type eq ""} { + lappend rangesbytype [list "unallocated" $newrangetype 0 0] + } else { + if {$last_type eq $newrangetype} { + set rangesbytype [lrange $rangesbytype 0 end-1] + lappend rangesbytype [list $last_class $last_type $a $i] + } else { + lappend rangesbytype [list $newrangeclass $newrangetype $i $i] + } + } + } + } + } + + return [list -ranges $ranges -rangesbytype $rangesbytype] + } + + method grid {} { + set posns [dict keys $o_map] + set col1 [string repeat " " 15] + set col [string repeat " " 4] + set pline "[overtype::left $col1 {var indices}] " + foreach p $posns { + append pline [overtype::left $col $p] + } + set remline "[overtype::left $col1 {unallocated}] " + foreach vidx $posns { + if {$vidx ni $o_remaining} { + append remline [overtype::left $col "."] + } else { + set tp [lindex [dict get $o_map $vidx] 1] + #set tp [string map $o_codemap $tp] + if {[dict exists $o_codemap $tp]} { + set tp [dict get $o_codemap $tp] + } + append remline [overtype::left $col $tp] + } + } + set cmdlist [list] + dict for {vidx info} $o_map { + if {[lindex $info 0] ne "unallocated"} { + set c [lindex [split [lindex $info 0] .] 0] + if {$c ni $cmdlist} { + lappend cmdlist $c + } + } + } + set clinelist [list] + foreach c $cmdlist { + set cline "[overtype::left $col1 $c] " + dict for {vidx info} $o_map { + lassign $info class type v + if {($c eq $class) || [string equal -length [string length "$c."] "$c." $class]} { + #set tp [string map $o_codemap $type] + if {[dict exists $o_codemap $type]} { + set tp [dict get $o_codemap $type] + } + append cline [overtype::left $col $tp] + } else { + append cline [overtype::left $col "."] + } + } + lappend clinelist $cline + } + + + set aline "[overtype::left $col1 {allocated}] " + foreach vidx $posns { + if {$vidx ni $o_allocated} { + append aline [overtype::left $col "."] + } else { + set tp [lindex [dict get $o_map $vidx] 1] + #set tp [string map $o_codemap $tp] + if {[dict exists $o_codemap $tp]} { + set tp [dict get $o_codemap $tp] + } + append aline [overtype::left $col $tp] + } + } + + return "$pline\n$remline\n[join $clinelist \n]\n$aline\n" + } + + } + + + #!todo - check if -commandprocessors members will collide with existing -flags in values before moving them + #!todo - skip optional sub-flag value if the next arg following its parent is a flag i.e proper handling of -commandprocessors {cmd {cmd sub "default}} when only cmd supplied. + #!important to fix. At the moment it could eat a further unflagged item in values later in the list which was intended for a different -commandprocessors member! + #add support for -commandprocessors {-cmd {-cmd -othercmd "default"}} to be a safe way to specify a linked -flag move that does the same. + proc allocate_arguments {PROCESSORS solos values cf_args caller} { + set runid [lindex [split [namespace tail $PROCESSORS] _] 1] ;# objname is of form PROCESSORS_pid-threadid-counter where "PROCESSORS_" is a literal + #puts stderr ">>>>>>> solos: $solos" + dict set debugc -debugargs [dict get $cf_args -debugargs] + dict set debugc -source "allocate_arguments $caller" + + set defaults [dict get $cf_args -defaults] + + set cmdprocessor_records [$PROCESSORS get_commandspecs] + + + set sep "\uFFFE" ;#argument-subargument separator (choose something else if this causes problems.. but we want something unlikely (or preferably impossible?) to be in a commandline, ideally a single character, and which at least shows something on screen during debug) + set sepstr "\\uFFFE" ;#for human readable error msg + #\u001E was tried and doesn't output on some terminals) + + set remaining_unflagged [dict create] + + set extra_flags_from_positionals [list] ;#values moved to -values + set moved_to_flagged [dict create] + + #implied_ are values supplied from defaults when a flag or operand was not found + set implied_flagged [list] + set implied_unflagged [list] + + + set dispatch [dict create] + #sanitize and raise error if sep somehow in values + if {[string first $sep $cmdprocessor_records] >= 0} { + do_error "allocate_arguments flags error separator '$sep' ($sepstr) found in values " + } + #-------------------------------------- + set VMAP [flagfilter::class_vmap create flagfilter::VMAP_$runid $values] + #-------------------------------------- + + set unconsumed_flags_and_values [list] + set unflagged [dict create] + + ###################### + #main -commandprocessors loop which scans the valuelist + set values_index 0 ;#track where we are up to as we allocate values to unflagged elements + set source_values $values ;#start with all including -flagged + + #todo - get rid of most of these flags last_x_was etc - and just do lookups into the v_map + # as this will probably involve *lots* of small functiona calls - keep this boolean version and check for performance issues. + set a_index 0 + set is_args_flag 0 + set last_arg_was_paramflag 0 ;#a flag that expects a parameter to follow + set last_arg_was_solo 0 + set solo_flags [dict keys $solos] ;#solos is a dict of -flag (preprocessed) + set end_of_options 0 + set end_of_options_index -1 ;#as later processors can rescan - we need to make sure they only look at arguments after this point + set last_p_found [dict create by "" index "" item ""] + set sequence 0 + set argerrors [list] ;#despite being a list - we will break out at first entry and return for now. + set parsestatus "ok" + + #set LAUNCHED [oolib::collection create col_processors_launched_$runid] + #set MATCHED [oolib::collection create col_processors_matched_$runid] + #oo::objdefine col_processors_matched_$runid { + # method test {} { + # return 1 + # } + #} + + #set objp [$PROCESSORS object_from_record $p] ;#temp convenience + + foreach objp [$PROCESSORS items] { + set objparent [$objp parent] + #$LAUNCHED add $objp [$objp name] + set p [$objp get_def] ;#individual record e.g {mycmd {match run singleopts {-x}}} or {tail_processor {}} + + lassign $p parentname pinfo + set is_sub [$objp is_sub] ;#is subargument - should look to see if last related spec got a value and abort if not. + set is_p_flag [$objp is_flag] ;#sub can be a flag even if parent isn't + set processorname [$objp name] + if {[$objp is_sub]} { + if {![[$objp parent] found_match]} { + continue + } + set p_sub [dict get $pinfo sub] + } + do_debug 3 $debugc " =========================>> p $p sequence:$sequence a_index $a_index" + + if {$processorname in [list "global" "tail_processor"]} { + dict set last_p_found by $processorname + #dict set last_p_found index $a_index + #dict set last_p_found item $a + } + # -format {x {sub y default "default"}} means y is dependent on x being present and shouldn't eat if the next value isn't flaglike + # -format {-x {sub -y}} does the same for moving positionals to the flagged list. + + + #set remaining_values [lrange $source_values $a_index end] + ##################################### + # full rescans for later processors + set remaining_values $source_values ;#source_values shrinks as commands take arguments + set a_index 0 + ##################################### + + do_debug 3 $debugc "-------->________>p '$processorname' remaining vals $remaining_values" + + #!todo - use v_map as an extra determinant to stop sequence for a command-set. (don't extend beyond where args have already been snipped by another command) + if {[$objp name] eq "tail_processor"} { + set mapcopy [flagfilter::class_vmap new {}] ;#no need to supply values as we are copying data from $VMAP + $VMAP copy_to $mapcopy + $objp set_map_object $mapcopy + } else { + $objp set_map_object $VMAP + } + foreach a $remaining_values { + set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index] + if {![string is integer -strict $argnum]} { + error "arg '$a' scan_index:$a_index - calculated argnum:'$argnum' is invalid" + + } + set sub_operand 0 + do_debug 3 $debugc "$argnum >eoptions_idx:$end_of_options_index a_index:$a_index __________________________________________________________a $a" + if {$end_of_options_index > -1} { + set end_of_options [expr {$a_index >= $end_of_options_index}] + } + + #review - data with leading - may be unintentionally interpreted as a flag + if {[string trim $a] eq "--"} { + #generally means end of options processing.. + #review - pass -- through?? + set last_arg_was_paramflag 0 ;#we don't treat first arg following end_of_options as belonging to the flag! - it is potentially an operand to the command + set is_solo_flag 0 + set end_of_options 1 + set end_of_options_index $a_index + #if {[lindex $p 0] eq "tail_processor"} { + $objp allocate $argnum "endofoptions" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + #} + } else { + if {($last_arg_was_paramflag) && ([$objp arg_is_defined_solo_to_me $a])} { + #last flag expecting param - but this flag *known* to be solo + #keep it simple and break out at first solo_flag related error ...unless it is trailing flag in the list + lappend argerrors [list flagerror solo_flag_following_non_solo_flag bad_flag $a] + set last_arg_was_solo 1 + break + } + #set is_solo_flag [expr {($a in $solo_flags)}] + #set is_solo_flag [is_this_flag_solo $a $solo_flags $objp] + set is_solo_flag [$objp arg_is_defined_solo_to_me $a] + + if {!$end_of_options} { + if {!$last_arg_was_paramflag} { + if {!$is_solo_flag} { + set is_args_flag [is_candidate_toplevel_param_flag $a $solo_flags $cmdprocessor_records] + #set is_args_flag [string match -* $a] + } + if {$is_args_flag || $is_solo_flag} { + if {[dict get $last_p_found by] eq $processorname} { + if {![is_this_flag_for_me $a $objp $cf_args]} { + if {$processorname ne "globalXXX"} { + do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit an unrecognized option: $a" + break + } + } + } + } + } else { + #last was flag expecting a param + set is_args_flag 0 + set is_solo_flag 0 + } + } else { + #end_of_options - ignore solo and other flags now. + set is_args_flag 0 + set is_solo_flag 0 + set last_arg_was_paramflag 0 + + } + + #puts stderr "!!!!!!!!!!!!!!!!!!1 here is_args_flag:$is_args_flag" + do_debug 3 $debugc " >________>________>is_p_flag: $is_p_flag last_arg_was_paramflag:$last_arg_was_paramflag is_args_flag:$is_args_flag is_solo: $is_solo_flag (soloflags:$solo_flags) a:$a " + if {!$is_args_flag && !$is_solo_flag } { + + if {!$last_arg_was_paramflag} { + if {[dict get $last_p_found by] eq $processorname} { + if {$processorname ne "tail_processor"} { + #we already found our unflagged value - and now we've hit another - time to break and hand it to a subcommand processor if any + do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit another value: $a" + break + } + } + set sequence_ok 1 ;#default assumption + set can_allocate [can_this_commandspec_allocate_this_arg $a $p $cf_args] + + if {$can_allocate} { + if {$is_sub} { + #!todo - use v_map as sequence terminator + #check if our find is in sequence + #we are only hunting non-flagged items and the the previous finder removes 1 from the source_values list + #therefore the a_index of our find should be the same if we are processing the very next argument. + #we have already checked that it was a related entity which found the last one. + #todo - review if it matters when parents/siblings don't eat all the way up to the next -flag. + #todo - implement a 'gather' key to keep eating in sequence and accumulate the values as a list + if {$a_index > [dict get $last_p_found index]} { + do_debug 3 $debugc "OUT OF SEQUENCE a_index:$a_index vs last_found index:[dict get $last_p_found index], $processorname disengaging - ignoring value $a and leaving it to the next processor" + set last_arg_was_paramflag 0 + do_debug 3 $debugc "<--- breaking --->" + break + } elseif {$a_index < [dict get $last_p_found index]} { + #too early.... found something before previous match + do_debug 3 $debugc "+++++++++++++++out of sequence $processorname - too early.. keeping scanning" + set sequence_ok 0 + } + if {$sequence_ok} { + set sub_operand 1 + } + } + } + + if {$can_allocate && $sequence_ok} { + #found a non-flagged value in the argumentlist to either reallocate to flagged values or to positional values + if {[dict exists $pinfo dispatch]} { + if {!$is_sub} { + #this must be the arg that caused the match + dict set dispatch $parentname [list command [dict get $pinfo dispatch] matched $a arguments [list] raw [list]] + } else { + #todo + lappend argerrors [list unsupported_dispatch $processorname] + } + } + if {$sub_operand} { + if {[dict exists $dispatch $parentname]} { + #todo - defaults? + add_dispatch_argument "dispatch" $parentname $processorname $a + add_dispatch_raw "dispatch" $parentname $a + } else { + #warning? + #lappend argerrors [list subcommand_unable_to_add_operand $processorname] + do_debug 3 $debugc "subcommand $processorname aborting scanning because parent command wasn't activated" + break + } + } + do_debug 2 $debugc " >+++++++>++++++++>++++++++>setting $processorname [if {$is_p_flag} {list -} {}]value $a" + if {$processorname eq "tail_processor"} { + set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index] + set argname arg$argnum + lappend remaining_unflagged $argname $a + lappend unconsumed_flags_and_values $a + dict set unflagged $argname $a + } elseif {$is_p_flag} { + $objp set_matched_argument $argnum $a + if {$is_sub} { + dict set extra_flags_from_positionals $p_sub $a + } else { + dict set extra_flags_from_positionals $parentname $a + } + lappend moved_to_flagged $processorname $a + #if has dependent commands ? - check for deep subcommand match? + } else { + $objp set_matched_argument $argnum $a + #lappend positional_values $a + dict set unflagged $processorname $a + } + do_debug 4 $debugc " >________>________>________>source_values :'$source_values'" + do_debug 3 $debugc " >________>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]" + + #---------------------------- + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + #------------------------------ + $objp allocate $argnum "operand" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr values_index ;#only increment when we allocate a value to one of the members of -commandprocessors + set last_arg_was_paramflag 0 + if {$processorname ne "tail_processor"} { + #don't break until we hit an unrecognized flag or another unflagged value + incr a_index -1 + #don't increment a_index before break, because we have shortened the list by 1. + #do_debug 3 $debugc "----breaking---" + #break + } else { + #decrement to compensate for shortened list because tail_processor continues to end + incr a_index -1 + } + } + + } else { + #last_arg_was_paramflag + set lastarg [dict get $last_p_found item] + #puts stdout "+++ lastarg: $lastarg source_values: [dict get $last_p_found source_values] a_index: $a_index" + if {$processorname eq "tail_processor"} { + lappend unconsumed_flags_and_values $a + } + if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $lastarg $objp $cf_args]} { + update_dispatch_argument "dispatch" $parentname $lastarg $a + add_dispatch_raw "dispatch" $parentname $a + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + $objp allocate $argnum "flagvalue" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + } + set last_arg_was_paramflag 0 + } + } else { + # is a flag of some sort ({!$is_args_flag && !$is_solo_flag} = false) + if {$processorname eq "tail_processor"} { + lappend unconsumed_flags_and_values $a + } + if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $a $objp $cf_args]} { + if {$is_solo_flag} { + add_dispatch_argument "dispatch" $parentname $a 1 + add_dispatch_raw "dispatch" $parentname $a + set last_arg_was_solo 1 + set last_arg_was_paramflag 0 + $objp allocate $argnum "soloflag" $a + } else { + add_dispatch_argument "dispatch" $parentname $a "" + add_dispatch_raw "dispatch" $parentname $a + set last_arg_was_solo 0 + set last_arg_was_paramflag 1 + $objp allocate $argnum "flag" $a + } + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + do_debug 4 $debugc " >2_______>________>________>source_values :'$source_values'" + do_debug 3 $debugc " >2_______>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]" + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + } else { + #auto alternate based on last value.. unless end_of_options + if {!$end_of_options} { + if {$a in $solo_flags} { + set last_arg_was_solo 1 + set last_arg_was_paramflag 0 + } else { + set last_arg_was_paramflag 1 + } + } + if {$a_index eq ([llength $source_values]-1)} { + #puts "XXXXXXXXXXXXXXXXXXX $a_index source_values:'$source_values'" + #if at end of list don't retain any last..was info. + set last_arg_was_solo 0 + set last_arg_was_paramflag 0 + } + #skip - don't eat + } + } + } + incr a_index + } + + if {![$objp found_match]} { + + #after break - we have retained vars: $parent, $sub_operand $pinfo $processorname etc + #didn't find an unflagged var - set a default if one was specified. + #do nothing otherwise - check_args will determine if it was -required etc. + #review - should only apply if parent cmd found something? + if {[dict exists $pinfo default]} { + set defaultval [dict get $pinfo default] + if {$is_p_flag} { + if {$is_sub} { + dict set extra_flags_from_positionals $p_sub $defaultval + } else { + dict set extra_flags_from_positionals $processorname $defaultval + } + #lappend moved_to_flagged $processorname $defaultval + lappend implied_flagged $processorname $defaultval + do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval implied_flagged: $implied_flagged " + } else { + lappend implied_unflagged $processorname $defaultval + dict set unflagged $processorname $defaultval + do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval moved_to_flagged: $moved_to_flagged " + } + + if {$is_sub && !$sub_operand} { + if {[dict exists $dispatch $parentname]} { + add_dispatch_argument "dispatch" $parentname $processorname $defaultval + } else { + lappend argerrors [list subcommand_unable_to_add_default_operand $processorname $defaultval] + } + } + } + } + + if {[$objp name] eq "tail_processor"} { + $VMAP update_map_from [$objp get_map_object] + } + + if {[llength $argerrors]} { + set parsestatus "error" + #abort processing at first error - we won't be able to make sense of the remaining args anyway + #even the tail_processor won't be able to classify reliably because flag meanings depend on the configured commands + break + } + } + + #assertion - should be none? + #set remaining_values [lrange $source_values $a_index end] + #do_debug 3 $debugc "-------->________>end of processing - remaining vals $remaining_values" + + do_debug 2 $debugc "========>=========>originals : $values" + do_debug 2 $debugc "[$VMAP get_map]" + do_debug 2 $debugc "========>=========>unconsumed: $unconsumed_flags_and_values" + + + + + + set all_flagged [$VMAP get_merged_flagged_by_class *] + set all_flagged_plus [concat $all_flagged $extra_flags_from_positionals] + + set all_flagged_list [$VMAP get_list_flagged_by_class *] + set all_flagged_list [concat $all_flagged_list $extra_flags_from_positionals] + + set remaining_flagged [$VMAP get_merged_flagged_by_class "unallocated"] + + set remaining_flagged_list [$VMAP get_list_flagged_by_class "unallocated"] + + + set unflagged_list_in_processing_order [dict values $unflagged] + set unflagged_list [$VMAP get_list_unflagged_by_class *] + + set unflagged_list_remaining [$VMAP get_list_unflagged_by_class "unallocated"] + + return [dict create \ + listremaining $unconsumed_flags_and_values \ + parseerrors $argerrors \ + parsestatus $parsestatus \ + flagged $all_flagged_plus \ + flaggedlist $all_flagged_list \ + flaggedremaining $remaining_flagged \ + flaggedlistremaining $remaining_flagged_list \ + unflagged $unflagged \ + unflaggedlist $unflagged_list \ + unflaggedremaining $remaining_unflagged \ + unflaggedlistremaining $unflagged_list_remaining \ + flaggednew $extra_flags_from_positionals \ + arglist [concat $unflagged_list_in_processing_order $all_flagged] \ + arglistremaining [concat $unflagged_list_remaining $remaining_flagged] \ + impliedflagged $implied_flagged \ + impliedunflagged $implied_unflagged \ + dispatch $dispatch \ + classifications [$VMAP get_map] \ + gridstring "\n[$VMAP grid]" \ + vmapobject "flagfilter::VMAP_$runid" \ + ] + } + + + + + + + + + + + + #specialisation for collection class to contain commandprocessors + # we expect to use only a single instance of this + oo::class create col_allprocessors { + superclass oolib::collection + variable o_commandspecs + method add_processor {p} { + my add $p [$p name] + if {[$p is_sub]} { + set parentname [$p parentname] + set obj_parent [my item $parentname] + set col_siblings [$obj_parent children] + $col_siblings add $p [$p name] + } + } + method set_commandspecs {cspecs} { + set o_commandspecs $cspecs + } + method get_commandspecs {} { + set o_commandspecs + } + #treating as singleton.. todo tidy + method name_from_record {rec} { + lassign $rec parentname pinfo + if {[dict exists $pinfo sub]} { + set name [join [list $parentname [dict get $pinfo sub]] .] + } else { + set name $parentname + } + return $name + } + method object_from_record {rec} { + set name [my name_from_record $rec] + return [my item $name] + } + #basic check if arg may consume the following one - not based on any specific info from processors + method arg_appears_standalone {f} { + if {(![string match "-*" $f]) && (![string match "/*" $f])} { + #not even flaglike + return 1 + } + if {$f in {- --}} { + return 1 + } + } + #does any processor define it as solo + method flag_can_be_solo {f} { + foreach objp [my items] { + if {[$objp arg_is_defined_solo_to_me $f]} { + return 1 + } + } + return 0 + } + } + oo::class create col_parents { + superclass oolib::collection + method add_parent {p} { + if {[$p is_sub]} { + error "cannot add a sub-processor to the main parents collection" + } + my add $p [$p name] + } + } + #each parent processor has a children collection which can only accept processors with sub defined. + oo::class create col_childprocessors { + superclass oolib::collection + variable o_ownername + method set_owner {parentname} { + set o_ownername $parentname + } + #owner of the collection (a parent processor) + method owner {} { + return $o_ownername + } + method add_processor {p} { + if {![$p is_sub]} { + error "processor must have 'sub' element to add to the parent's collection" + } + #check name matches this parent.. + + my add $p [$p name] + } + } + + oo::class create cprocessor { + variable o_runid + variable o_name + variable o_definition + variable o_pinfo + variable o_parentname + variable o_is_sub + variable o_col_children + variable o_mashopts + variable o_singleopts + variable o_pairopts + variable o_longopts + variable o_found_match ;#we directly matched a command trigger or positional argument + variable o_matched_argument + variable o_matched_argnum + variable o_matchspec + variable o_vmap + constructor {definition runid} { + set o_vmap "" + set o_definition $definition + set o_runid $runid + if {([llength $o_definition] < 2) || ([llength [lindex $o_definition 0]] != 1)} { + error "[self class].constructor Unable to interpret definition '$o_definition'" + } + lassign $o_definition o_parentname o_pinfo + if {([llength $o_pinfo] %2) != 0} { + error "[self class].constructor second element of definition '$o_definition' not a dict" + } + set o_is_sub [dict exists $o_pinfo sub] + if {!$o_is_sub} { + set o_name $o_parentname + set o_col_children [::flagfilter::col_childprocessors new] + $o_col_children set_owner $o_name + } else { + set o_name [join [list $o_parentname [dict get $o_pinfo sub]] .] + } + if {[dict exists $o_pinfo match]} { + set o_matchspec [dict get $o_pinfo match] + } else { + set o_matchspec {^[^-^/].*} ;#match anything that isn't flaglike + } + set o_found_match 0 + set o_matched_argument "" ;#need o_found_match to differentiate match of empty string + set o_matched_argnum -1 + #load mashopts etc at construction time as they're static + set o_mashopts [list] + set o_singleopts [list] + set o_pairopts [list] + set o_longopts [list] + if {[dict exists $o_pinfo mashopts]} { + lappend o_mashopts {*}[dict get $o_pinfo mashopts] + } + if {[dict exists $o_pinfo singleopts]} { + lappend o_singleopts {*}[dict get $o_pinfo singleopts] + } + if {[dict exists $o_pinfo pairopts]} { + lappend o_pairopts {*}[dict get $o_pinfo pairopts] + } + if {[dict exists $o_pinfo longopts]} { + lappend o_longopts {*}[dict get $o_pinfo longopts] + } + } + destructor { + catch {$o_vmap destroy} + if {!$o_is_sub} { + $o_col_children destroy + } + } + + method name {} { + return $o_name + } + #open things up during oo transition.. + method get_def {} { + return $o_definition + } + method is_flag {} { + if {[my is_sub]} { + #sub can be a flag even if parent isn't + set subname [dict get $o_pinfo sub] + return [string match -* $subname] + } else { + return [string match -* $o_name] + } + } + method has_same_parent {other} { + return [expr {[other parentname] eq $o_parentname}] + } + method is_sub {} { + return $o_is_sub + } + + method set_map_object {map} { + set o_vmap $map + } + method get_map_object {} { + return $o_vmap + } + method allocate {argnum type val} { + if {$o_vmap eq ""} { + error "[self class].allocate ($o_name) vmap is not set." + } + $o_vmap allocate [self object] $argnum $type $val + } + + method found_match {} { + return $o_found_match + } + method matched_argument {} { + return $o_matched_argument + } + method matched_argnum {} { + return $o_matched_argnum + } + method set_matched_argument {argnum a} { + #could be empty string + if {$o_found_match} { + error "[self object].set_matched_argument processor:$o_name already found match '$o_matched_argument' - cannot set again" + } + if {![my can_match $a]} { + error "error [self class].set_matched_argument processor:$o_name cannot match '$a' (matchspec: $o_matchspec)" + } + set o_found_match 1 + set o_matched_argument $a + set o_matched_argnum $argnum + } + method has_explicit_matchspec {} { + return [dict exists $o_pinfo match] + } + method matchspec {} { + return $o_matchspec + } + method can_match {a} { + if {!$o_found_match} { + foreach m $o_matchspec { + if {[regexp -- $m $a]} { + return 1 + } + } + return 0 + } else { + return 0 + } + } + #?? + method can_allocate_flags {} { + } + + + + + + #if we are a parent - this is own name + method parentname {} { + return $o_parentname + } + method parent {} { + return [::flagfilter::obj::PARENTS_$o_runid item $o_parentname] + } + method is_parent {} { + return [expr {!$o_is_sub}] + } + method children {} { + if {!$o_is_sub} { + return $o_col_children + } else { + #raise error? + return "" + } + } + method mashopts {} { + return $o_mashopts + } + method singleopts {} { + return $o_singleopts + } + method pairopts {} { + return $o_pairopts + } + method longopts {} { + return $o_longopts + } + + #whether flag categorized as solo by this processor + method arg_is_defined_solo_to_me {a} { + if {(![string match "-*" $a]) && (![string match "/*" $a])} { + #not even flaglike + return 0 + } + if {[my can_match $a]} { + return 0 + } + if {$a in {- --}} { + #specials not defined as solos + return 0 + } + + if {$o_name eq "global"} { + + } elseif {$o_name eq "tail_processor"} { + + } + + if {$a in $o_singleopts} { + return 1 + } + if {"any" in $o_singleopts} { + return 1 + } + set equalposn [string first "=" $a] + if {$equalposn >=1} { + if {"any" in $o_longopts} { + return 1 + } else { + set namepart [string range $a 0 $equalposn-1] + foreach lo $o_longopts { + if {[string match "${namepart}=*" $lo]} { + return 1 + } + } + } + } + #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash + #- but if it's a pairopt, but not mashable - we can rule it out now + if {($a in $o_pairopts) && ($a ni $o_mashopts)} { + return 0 + } + set flagletters [split [string range $a 1 end] ""] + set posn 1 + #trailing letters may legitimately not be in mashopts if they are part of a mashed value + #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing + foreach l $flagletters { + if {"-$l" ni $o_mashopts} { + #presumably an ordinary flag not-known to us + return 0 + } else { + if {"-$l" in $o_pairopts} { + if {$posn == [llength $flagletters]} { + #in pairopts and mash - but no value for it in the mash - thefore not a solo + return 0 + } else { + #entire tail is the value - this letter is effectively solo + return 1 + } + } elseif {"-$l" in $o_singleopts} { + #not allowed to take a value - keep processing letters + } else { + #can take a value! but not if at very end of mash. Either way This is a solo + return 1 + } + } + } + #This object should not treat the flag as a known solo + #- so if it is allowed to consume it, it may fall back on examining the subsequent argument's flaginess(?) + return 0 + } + + + method get_opts {} { + return [list mashopts $o_mashopts singleopts $o_singleopts pairopts $o_pairopts longopts $o_longopts] + } + #include parent opts + #we use the terminology 'option' for "-" prefixed items belonging to a -commandprocessors spec as opposed to more general -flags + #Note - this may also be called on the default "tail_processor", which will return empty sets, or an overridden tail_processor which may have data + method get_combined_opts {} { + set objparent [::flagfilter::obj::PARENTS_$o_runid item $o_parentname] + set parentopts [$objparent get_opts] + set mashopts [dict get $parentopts mashopts] + set singleopts [dict get $parentopts singleopts] + set pairopts [dict get $parentopts pairopts] + set longopts [dict get $parentopts longopts] + if {[my is_sub]} { + #this spec is a sub + set subopts [my get_opts] + #does order matter? could use struct::set union ? + foreach m [dict get $subopts mashopts] { + if {$m ni $mashopts} { + lappend mashopts $m + } + } + foreach s [dict get $subopts singleopts] { + if {$s ni $singleopts} { + lappend singleopts $s + } + } + foreach po [dict get $subopts pairopts] { + if {$po ni $pairopts} { + lappend pairopts $po + } + } + foreach lo [dict get $subopts longopts] { + if {$lo ni $longopts} { + lappend longopts $lo + } + } + + } + return [list mashopts $mashopts singleopts $singleopts pairopts $pairopts longopts $longopts] + } + + } + + + + + + + + + + + + proc get_command_info {cmdname cspecs} { + foreach item $cspecs { + lassign $item cmd specinfo + if {$cmd eq $cmdname && [dict exists $specinfo dispatch]} { + return $specinfo + } + } + return [list] + } + #### check_flags + # does not support unvalued flags - unless explicitly specified in -soloflags (global) or in -singleopts for a commandprocessor + #e.g not supported: v1 v2 -arg1 arg1val -debug -anotherflag anotherflagval + # - unless -soloflags is something like -soloflags {-debug} or -soloflags {{-debug 1}} where 1 is the default. In this case - we can no longer support accepting a value for -soloflags - the processor will not assign it an argument from the commandline. + #e.g not supported (unless -debug in -soloflags): v1 v2 -arg1 arg1val -anotherflag anotherflagval -debug + #e.g supported: v2 v2 -arg1 arg1val -debug 1 -anotherflag anotherflagval + # supports positional arguments - but only if specified in -commandprocessors + # todo + # - supports -- for treating following arg as value even if it looks like a flag + # - supports - for reading stdin + # expects at least -values + # other options -caller -defaults -required -extras -commandprocessors + # -soloflags (these are flags that *must* be solo - ie they cannot take an argument ) if no default specified they are boolean defaulting to 1, repeated instances in -values will be appended to a list. + # The only flag that can be a mix of solo or not, is the very last flag in the values list. In this case it must not be in the -soloflags list, but it will default to a boolean 1 to indicate presence. + proc check_flags {args} { + set runid [flagfilter::get_new_runid] + #################################################### + #puts "Entered checkflags, args $args" + set distanceToTop [info level] + set callerlist [list] + set was_dispatched_by_another 0 ;#used to + for {set i 1} {$i < $distanceToTop} {incr i} { + set callerlevel [expr {$distanceToTop - $i}] + set callerinfo [info level $callerlevel] + set firstword [lindex $callerinfo 0] + if {[string match "*check_flags*" $firstword]} { + set was_dispatched_by_another 1 + } + lappend callerlist $firstword + } + #puts stdout "callerlist: $callerlist" + + #first handle args for check_flags itself + if {[catch {lindex [info level -1] 0} caller]} { + set caller "" + } + #puts stderr ">>>>check_flags caller $caller" + get_one_paired_flag_value {-x 1} -x ;# + + #manually check for -caller even if unbalanced args + #we only need to use get_one_paired_flag_value because we haven't yet checked args is a properly formed paired list and if -caller is present we want to use it for clearer error messages. + #use normal dict operations to retrieve other flags. + #if failed to retrieve.. fall through to checks below + if {![catch {get_one_paired_flag_value $args -caller} flag_value_result]} { + set caller $flag_value_result + } + #puts stderr ">>>>check_flags caller $caller" + + + + + set cf_defaults [dict create\ + -caller $caller\ + -return [list arglistremaining]\ + -match [list]\ + -commandprocessors [list]\ + -soloflags [list]\ + -extras [list]\ + -defaults [list]\ + -required [list]\ + -values \uFFFF\ + -debugargs 0\ + ] + dict set cf_defaults -debugargsonerror 1 ;#error level to use when dispatch error occurs.. will not set lower than -debugargs + + + + if {([llength $args] % 2) != 0} { + do_error "check_flags error when called from '$caller' :check_flags must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $cf_defaults]' \n got: $args" + } + set cf_args $cf_defaults + foreach {k v} $args { + switch -- $k { + -caller - -return - -match - -commandprocessors - -soloflags - -extras - -defaults - -required - -values - -debugargs - -debugargsonerror { + dict set cf_args $k $v + } + default { + do_error "check_flags error when called from ${caller}: Unknown option '$k': must be one of '[dict keys $cf_defaults]' \nIf calling check_flags directly, put args being checked in -values {...}" + } + } + } + unset args + #################################################### + #now look at -values etc that check_flags is checking + + set caller [dict get $cf_args -caller] + + set debugargs [dict get $cf_args -debugargs] + dict set debugc -debugargs [dict get $cf_args -debugargs] + dict set debugc -source "check_flags $caller" + do_debug 1 $debugc "DEBUG-START $caller" + + set returnkey [dict get $cf_args -return] + set defaults [dict get $cf_args -defaults] + if {([llength $defaults] % 2) != 0} { + do_error "check_flags error when called from '$caller' :-defaults must be a list containing an even number of arguments of form: -flag value'" + } + set required [dict get $cf_args -required] + + + set acceptextra [dict get $cf_args -extras] + + set supplied [string trim [dict get $cf_args -values]] + set soloflags [dict get $cf_args -soloflags] ;#By their nature - solo flags are unlikely to be automatically 'required' - review + set solos_with_defaults [list] + foreach solo_spec $soloflags { + if {[llength $solo_spec] == 1} { + lappend solos_with_defaults $solo_spec 1 + } else { + lappend solos_with_defaults [lindex $solo_spec 0] [lindex $solo_spec 1] + } + + } + + if {$debugargs >= 3} { + set prefix "| $caller>" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + puts -nonewline stderr "$prefix input\n" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #puts stderr "$caller $cf_args" + dict for {k v} $cf_args { + if {$k ne "-commandprocessors"} { + puts -nonewline stderr "$prefix \[$k\]\n" + puts -nonewline stderr "$prefix $v\n" + } + } + if {$debugargs >=4} { + puts -nonewline stderr "$prefix \[-commandprocessors\]\n" + foreach record [dict get $cf_args -commandprocessors] { + puts -nonewline stderr "$prefix $record\n" + } + } + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #dict for {key val} $cf_args { + # puts stderr " $key" + # puts stderr " $val" + #} + } + + + ################################################################################################## + # allocate_arguments does the main work of processing non-flagged items in the main supplied argument list into flagged versions depending on the specs in -commandprocessors + # It sets defaults only for those arguments processed by a '-commandprocessors' spec. + # We must supply it with the -soloflags info because the solo flags affect what is considered an operand. + set command_specs [dict get $cf_args -commandprocessors] ;#may be empty list - that's ok - it will still populate the 'flagged' and 'arglist' return-dict members. + + #some of these are keys returned by allocate_arguments + # - some (e.g supplied) are added by check_flags + # This list is the list of -return values that can be used with check_args + set flaginfo_returns [list \ + parseerrors \ + parsestatus \ + flagged \ + flaggedremaining \ + flaggednew \ + unflagged \ + unflaggedremaining \ + unflaggedlistremaining \ + listremaining \ + arglist \ + arglistremaining \ + impliedunflagged \ + impliedflagged \ + classifications \ + gridstring \ + ranges \ + dispatch \ + dispatchstatuslist \ + dispatchresultlist \ + dispatchstatus \ + supplied \ + defaults \ + status \ + vmapobject \ + ] + + set PROCESSORS [col_allprocessors create ::flagfilter::obj::PROCESSORS_$runid] + set PARENTS [col_parents create ::flagfilter::obj::PARENTS_$runid] + + # + #set command_specs [concat [list {global {}}] $command_specs] + lappend command_specs {tail_processor {}} + + foreach cspec $command_specs { + set obj [cprocessor new $cspec $runid] ;#runid gives access to the context-objects PROCESSORS_runid & PARENTS_runid + if {[$obj is_parent]} { + $PARENTS add_parent $obj + } + #do_debug 1 $debugc "CONFIGURING OBJECT for commandprocessor [$obj name]" + $PROCESSORS add_processor $obj + } + do_debug 1 $debugc "ADDED [$PROCESSORS count] processors to main commandprocessor collection" + do_debug 1 $debugc "ADDED [$PARENTS count] processors to the parents collection" + $PROCESSORS set_commandspecs $command_specs + + #allocate_arguments uses the PROCESSORS object + set processed_arguments [allocate_arguments $PROCESSORS $solos_with_defaults $supplied $cf_args $caller] + #set processed_arguments [allocate_arguments {} $supplied] + + set newly_flagged_positionals [dict get $processed_arguments flaggednew] + set unflaggedremaining [dict get $processed_arguments unflaggedremaining] + set unflaggedlistremaining [dict get $processed_arguments unflaggedlistremaining] + set dispatch [dict get $processed_arguments dispatch] + set flaggedremaining [dict get $processed_arguments flaggedremaining] + set RETURNED_VMAP [dict get $processed_arguments vmapobject] + + + + if {$debugargs >= 3} { + set prefix "| $caller>" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + puts -nonewline stderr "$prefix output\n" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #puts stderr "processed_arguments: $processed_arguments" + dict for {key val} $processed_arguments { + puts -nonewline stderr "$prefix $key\n" + puts -nonewline stderr "$prefix $val\n" + } + puts -nonewline stderr "$prefix [string repeat - 30]\n" + } + + ################################################################################################## + + + + + + if {![llength $newly_flagged_positionals]} { + if {($supplied eq "\uFFFF") || ![llength $supplied]} { + #do_error "check_flags error when called from ${caller}: missing or empty -values" + } + } + + #probably not something to enforce... we might pass on unbalanced lists to other check_args etc. + #if {([llength $supplied] % 2) != 0} { + # do_error "${caller}: Error. $caller must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $defaults]'\n received values: $supplied" + #} + + + + set new_arg_list [dict get $processed_arguments arglistremaining] + set flagged_list [dict get $processed_arguments flagged] + #set suppliedkeys_with_extrakeys [concat [dict keys $supplied] [dict keys $newly_flagged_positionals]] + #puts stdout "suppliedkeys and new keys: $suppliedkeys_with_extrakeys" + + #todo - add flaggednew to required if all was specified? + #check invalid flags if not indicated in -extras , either explicitly or with 'extra' + set flags_from_required [get_flagged_only $required {}] + #set known_flags [lsort -unique -nocase [concat [dict keys $defaults] $flags_from_required $soloflags]] ;#why -nocase? why should -l and -L collapse to the uppercase version? + set known_flags [punk::lib::lunique_unordered [concat [dict keys $defaults] $flags_from_required $soloflags ]] + foreach spec $command_specs { + lassign $spec parentname pinfo + if {[string match -* $parentname] && $parentname ni $known_flags} { + lappend known_flags $parentname + } + if {[dict exists $pinfo sub]} { + if {[string match -* [dict get $pinfo sub]]} { + lappend known_flags [dict get $pinfo sub] + } + } + } + do_debug 2 $debugc "------------------->known_flags: $known_flags soloflags:$soloflags" + set invalid_flags [list] + if {"all" ni [string tolower $acceptextra]} { + if {"none" in [string tolower $acceptextra]} { + set ok_extras [list] + } elseif {[llength $acceptextra]} { + set ok_extras $acceptextra + } + #todo + #puts stderr " check_flags - temporary disable of checking for invalid flags" + set pairflagged $flagged_list + foreach {f v} $pairflagged { + if {$f ni $acceptextra && $f ni $known_flags} { + lappend invalid_flags $f + } + } + } + if {[llength $invalid_flags]} { + do_error "check_flags $caller error when called from ${caller}: unknown flags '$invalid_flags'" + } + + set calc_required [list] + set keywords_in_required [lsearch -inline -all -not $required -*] + set bad_keywords_in_required [lsearch -regexp -nocase -all -inline -not $keywords_in_required "all|none"] + if {[llength $bad_keywords_in_required]} { + do_error "check_flags error when called from ${caller}: bad flags in '-required' it must be a list of flags of the form -flagname or ONLY one of the keywords 'none' or 'all'" + } + #keywords_in_required now known to be only comprised of (possibly case variant) values of all|none + if {[llength $keywords_in_required] > 1} { + do_error "check_flags error when called from ${caller}: specifying both 'none' and 'all' in -required is not valid, and repeated values are not valid." + } + if {"none" eq [string tolower [lindex $keywords_in_required 0]]} { + set calc_required [list] + } + set flags [lsearch -inline -all $required -*] + + if {[llength $required]} { + if {[lsearch -nocase $keywords_in_required "all"] >= 0} { + #'all' can be present with other flags - and indicates we also require all the flags from -defaults + dict for {k -} $defaults { + if {$k ni $calc_required} { + lappend calc_required $k + } + } + } + } + + set classifications [dict get $processed_arguments classifications] ;#assertion - ordered by numerically increasing key representing positions in supplied argument list + set rangesets [$RETURNED_VMAP get_ranges_from_classifications $classifications] + set ranges [dict get $rangesets -ranges] + set rangesbytype [dict get $rangesets -rangesbytype] ;#unallocated are split into flag,operand and endofoptions - further splitting is easy enough to do by looking up the classifications list for each position in the supplied arg list. + #tailflags are the same for all dispatch items + set tailflagspaired [tailflagspaired $defaults $supplied $classifications $rangesbytype] + + + set dict_supplied [dict create supplied $supplied] + set dict_defaults [dict create defaults $defaults] + set dict_ranges [dict create ranges $ranges] + set dict_rangesbytype [dict create rangesbytype $rangesbytype] + set raise_dispatch_error_instead_of_return "" + set dict_dispatch_results [list dispatchstatuslist [list] dispatchresultlist [list] dispatchstatus "ok"] + #todo - only dispatch if no unallocated args (must get tail_processor to allocate known flags to 'global') + if {[llength $dispatch]} { + set dispatchstatuslist [list] + set dispatchresultlist [list] + set dispatchstatus "ok" + #each dispatch entry is a commandname and dict + #set dispatchrecord [lrange $dispatch 0 1] + set re_argnum {%arg([0-9^%]+)%} + set re_argtake {%argtake([0-9^%]+)%} + set re_dquotedparts {(?:(?:\"[^\"]*\")|(?:\"[^\"]*"))|(?:\S*[^ \"])} ;#for use with regexp -all -inline + #e.g {"a b" 'b x' "x cd "e f" g a} -> {"a b"} 'b x' {"x cd "} e f {" g a} + #dumb-editor rebalancing quote for above comment " + foreach {parentname dispatchrecord} $dispatch { + set commandinfo [get_command_info $parentname $command_specs] + + do_debug 1 $debugc ">>>>>DISPATCHRECORD: $dispatchrecord" + + # e.g lscmd lscmd natsortcommandline_ls lscmd.dir x + + do_debug 2 $debugc "commandinfo for $parentname: $commandinfo" + set command [dict get $dispatchrecord command] + #support for %x% placeholders in dispatchrecord command + set command [string map {%match% %matched%} $command] ;#alias + set command [string map [list %matched% [dict get $dispatchrecord matched]] $command] + + set argnum_indices [regexp -indices -all -inline $re_argnum $command] + if {[llength $argnum_indices]} { + foreach {argx_indices x_indices} $argnum_indices { + #argx eg %arg12% + set argx [string range $command {*}$argx_indices] + set x [string range $command {*}$x_indices] + set command [string map [list $argx [lindex [dict get $dispatchrecord arguments] $x]] $command] + } + } + + set argsreduced [dict get $dispatchrecord arguments] + #set rawparts [regexp -all -inline $re_dquotedparts [dict get $dispatchrecord raw]] + + #review! + #how will this behave differently on unix + package require punk::winrun + set rawparts [punk::winrun::unquote_wintcl [dict get $dispatchrecord raw]] + #set argtake_indices [regexp -indices -all -inline $re_argtake $command] + + + set start 0 + while {[regexp -start $start -indices $re_argtake $command argx_indices x_indices]} { + #argx eg %argtake12% + set argx [string range $command {*}$argx_indices] + set x [string range $command {*}$x_indices] + set argval [lindex [dict get $dispatchrecord arguments] $x] + set replacementlen [string length $argval] + set command [string map [list $argx $argval] $command] + set start [expr {[lindex $argx_indices 0] + $replacementlen}] + set argsreduced [lremove $argsreduced $x] + set rawparts [lremove $rawparts $x] + } + dict set dispatchrecord arguments $argsreduced + if {$start > 0} { + set rawreduced [join $rawparts] + dict set dispatchrecord raw $rawreduced + } + + set argvals [dict get $dispatchrecord arguments] + set matched_operands [list] + set matched_opts [list] + set matched_in_order [list] + set prefix "${parentname}." + set prefixlen [string length $prefix] + foreach {k v} $argvals { + #puts "$$$$ $k" + if {[string equal -length $prefixlen $prefix $k]} { + #key is prefixed with "commandname." + set k [string replace $k 0 $prefixlen-1] + } + #todo - -- ? + if {[string match -* $k]} { + lappend matched_opts $k $v + lappend matched_in_order $k $v + } else { + set kparts [split $k .] + lappend matched_operands $v + lappend matched_in_order $v + } + } + + if {![dict exists $commandinfo dispatchtype]} { + set dispatchtype tcl + } else { + set dispatchtype [dict get $commandinfo dispatchtype] + } + if {![dict exists $commandinfo dispatchglobal]} { + if {$dispatchtype eq "tcl"} { + set dispatchglobal 1 + } else { + set dispatchglobal 0 + } + } else { + set dispatchglobal [dict get $commandinfo dispatchglobal] + } + #generally we only want to dispatch remaining flagged, and only at the tail end.(as opposed to flags occurring between command groups) + # -It doesn't usually make much sense to dispatch remaining unflagged items, and it would be rare to require flags occurring before the command. + #however - there are potential commands such as help, dryrun or maybe an analysis command that may need to see unconsumed operands or even look 'back' at prior items + ##update 2023-03 - we definitely want to look back to prior non-matches when we match on a script e.g tclsh8.6 -someflag etc xxx.tcl scriptarg1 -etc + # if we match and dispatch on *.tcl - then we may need 'tclsh8.6 -someflag etc' as the interpreter (possibly with arguments) to use. + # we may need a 'script' dispatchtype (as well as the option to just pass these prior arguments as additional options for some other dispatchtypes) + # + # todo - add supported dispatchglobal values such as all, pre, post, allpre, allpost, and classifications + # where pre & post are only those occurring directly before and after the command and its args, i.e not extending beyond any prior or subsequent other command. + # classifications would be flagged as -classifications $classifications whereas pre and post would be added directly if specified singly, or flagged with -pre, -post etc if multiple are specified + # Those beginning with 'all' should also be wrapped in flags, because potentially they come from disjointed sections of the argumentlist + # - and we generally shouldn't supply arguments next to each other that weren't contiguous in the original list + # The 1,true,yes,tailflagspaired value is designed for the usecase where a common set of tail flags e.g -debug can apply to any commands matched by the filter. + # tail = all unallocated args after final command, including operands and end-of-options '--' (todo) + # tailflags = all unallocated *contiguous* flags after the final command and final operands. (ie it will deliberately miss flags following last command if there is a later operand) (todo) + # tailflagspaired = same as tailflags, but any solo-flags are defaulted to 1 (flags not merged, so there might be duplicate keys) so that it's a fully paired list + # In other situations - post may make sense to get the very next set of unconsumed arguments. + if {[string tolower $dispatchglobal] in [list 1 true yes tailflagspaired]} { + set command_range_posn [lsearch -index 1 $ranges $parentname] + set extraflags $tailflagspaired + } else { + set extraflags [list] + } + + #jn concat allows $command to itself be a list + ##tcl dispatchtype + dict set dispatchrecord dispatchtype $dispatchtype + switch -- $dispatchtype { + tcl { + do_debug 1 $debugc "DISPATCHING with tcl arg order: $command $matched_operands $matched_opts $extraflags" + #set commandline [list $command {*}$matched_operands {*}$matched_opts {*}$extraflags] + set commandline [concat $command $matched_operands $matched_opts $extraflags] + } + raw { + do_debug 1 $debugc "DISPATCHING with raw args : $command [dict get $dispatchrecord raw]" + #set commandline [list $command {*}[dict get $dispatchrecord raw] {*}$extraflags] + set commandline [concat $command [dict get $dispatchrecord raw] $extraflags] + } + shell { + do_debug 1 $debugc "DISPATCHING with shell args : $command [dict get $dispatchrecord raw]" + #assume the shell arguments are in one quoted string? + set commandline [concat $command [list [dict get $dispatchrecord raw]] $extraflags] + } + default { + #non quoted shell? raw + defaults? + do_debug 1 $debugc "DISPATCHING with given arg order: $command $matched_in_order $extraflags" + #set commandline [list $command {*}$matched_in_order {*}$extraflags] + set commandline [concat $command $matched_in_order $extraflags] + } + } + + dict set dispatchrecord asdispatched $commandline + set dispatchresult "" + set dispatcherror "" + if {![catch {{*}$commandline} cmdresult]} { + set dispatchresult $cmdresult + lappend dispatchstatuslist [list status ok cmd $parentname outputlength [string length $cmdresult]] + lappend dispatchresultlist $cmdresult + } else { + set dispatchstatus "error" + set dispatcherror $cmdresult + #don't add to dispatchresultlist + lappend dispatchstatuslist [list status err cmd $parentname outputlength 0 error $cmdresult] + if {!$was_dispatched_by_another} { + #this is the first (or a direct) call to check_flags - so make sure error gets raised in this proc rather than just storing the error in the data and returning + set raise_dispatch_error_instead_of_return "dispatchstatuslist:\n[join $dispatchstatuslist \n] \nerrinfo:\n $::errorInfo" + dict set dispatchrecord result $dispatchresult + dict set dispatchrecord error $dispatcherror + dict set dispatch $parentname $dispatchrecord + + break + #return -code error "check_flags error during command dispatch:\n$cmdresult" + } + #we've been dispatched from another check_flags - so ok to propagate the error up via the dispatchrecord/dispatchstatuslist + } + dict set dispatchrecord result $dispatchresult + dict set dispatchrecord error $dispatcherror + dict set dispatch $parentname $dispatchrecord + } + + set dict_dispatch_results [list dispatchcaller $caller dispatchstatuslist $dispatchstatuslist dispatchresultlist $dispatchresultlist dispatchstatus $dispatchstatus] + } + #end llength $dispatch + + + set combined [dict merge $dict_defaults $dict_supplied $processed_arguments $dict_ranges $dict_rangesbytype $dict_dispatch_results] + dict set combined dispatch $dispatch ;#update with asdispatched info + if {([dict get $combined parsestatus] eq "ok") && ([dict get $combined dispatchstatus] eq "ok")} { + dict set combined status "ok" + } else { + dict set combined status "error" + } + do_debug 1 $debugc "COMBINED:$combined" + + + set returnkey [string tolower $returnkey] + if {"all" in $returnkey} { + set returnval $combined + #set returnval [dict merge $combined $dict_dispatch_results] + } else { + if {[llength $returnkey] == 1} { + set invalid 0 + #todo - support multiple merge? + set right "" + if {[regexp -all {\|} $returnkey] == 1} { + lassign [split $returnkey |] left right + set joinparts [split $left ,] + } else { + set joinparts [split $returnkey ,] + } + foreach j [concat $joinparts $right] { + if {$j ni $flaginfo_returns} { + set invalid 1 + } + } + set returnval [list] + if {!$invalid} { + foreach j $joinparts { + lappend returnval {*}[dict get $combined $j] + } + if {[string length $right]} { + set returnval [dict merge $returnval $defaults $returnval] + } + } else { + set returnval [list callerrors [list "-return '$returnkey' not valid"]] + } + } else { + set callerrors [list] + set returnval [dict create] + foreach rk $returnkey { + if {$returnkey in $flaginfo_returns} { + dict set returnval $rk [dict get $combined $returnkey] + } else { + lappend callerrors [list "-return '$returnkey' not valid"] + } + } + if {[llength $callerrors]} { + dict set returnval callerrors $callerrors + } + } + } + + do_debug 1 $debugc "[string repeat = 40]" + do_debug 1 $debugc "dispatch_results: $dict_dispatch_results" + do_debug 1 $debugc "[string repeat - 40]" + + if {[string length $raise_dispatch_error_instead_of_return]} { + set errdebug [dict get $cf_args -debugargsonerror] + if {$errdebug > [dict get $cf_args -debugargs]} { + dict set debugc -debugargs $errdebug + } + } + + set debuglevel_return 2 + set debugdict [concat {*}[lmap k [dict keys $combined] {list $k $debuglevel_return}]] ;#create a dict of keys from combined, all defaulted to $debuglevel_return + if {[llength [dict get $combined parseerrors]]} { + dict set debugdict "parseerrors" 0 + } else { + dict set debugdict "parseerrors" 2 + } + dict set debugdict "defaults" 1 + dict set debugdict "supplied" 1 + dict set debugdict "dispatch" 1 + dict set debugdict "ranges" 1 + dict set debugdict "rangesbytype" 1 + dict set debugdict "dispatchstatus" 1 + if {[dict get $combined "status"] eq "ok"} { + dict set debugdict "status" 1 + } else { + dict set debugdict "status" 0 + } + + do_debug 1 $debugc "returning '$returnkey'" + do_debug 1 $debugc "returnval '$returnval'" + if {([llength $returnval] % 2) == 0} { + do_debug 1 $debugc "returnkeys '[dict keys $returnval]'" + } + do_debug 1 $debugc "[string repeat = 40]" + dict for {k v} $combined { + set dlev [dict get $debugdict $k] + switch -- $k { + dispatch { + set col1 [string repeat " " 12] + #process as paired list rather than dict (support repeated commands) + set i 0 + foreach {cmdname cmdinfo} $v { + set field1 [string repeat " " [expr {[string length $cmdname]}]] + set col2_dispatch [string repeat " " [expr {[string length $cmdname] + 15}]] + set j 0 + foreach {ckey cval} $cmdinfo { + + if {$i == 0 && $j == 0} { + set c1 [overtype::left $col1 "dispatch"] + } else { + set c1 [overtype::left $col1 { ... }] + } + + if {$j == 0} { + set f1 [overtype::left $field1 $cmdname] + set c2 [overtype::left $col2_dispatch "$f1 $ckey"] + } else { + set f1 [overtype::left $field1 ...] + set c2 [overtype::left $col2_dispatch "$f1 $ckey"] + } + #leave at debug level 1 - because dispatch is generally important + do_debug $dlev $debugc "${c1}${c2} $cval" + + incr j + } + incr i + } + + #do_debug 1 $debugc "[overtype::left $col1 $k] [lindex $v 0] [list [lindex $v 1]]" + #foreach {nm rem} [lrange $v 2 end] { + # do_debug 1 $debugc "[overtype::left $col1 { ... }] $nm [list $rem]" + #} + } + dispatchresultlist { + set col1 [string repeat " " 25] + set i 0 + foreach dresult $v { + if {$i == 0} { + set c1 [overtype::left $col1 $k] + } else { + set c1 [overtype::left $col1 { ... }] + } + do_debug $dlev $debugc "$c1 $dresult" + incr i + } + } + classifications { + set col1 [string repeat " " 25] + set len [dict size $v] + if {$len == 0} { + do_debug $dlev $debugc "[overtype::left $col1 $k]" + continue + } + set max [expr {$len -1}] + set numlines [expr $len / 3 + 1] + if {($len % 3) == 0} { + incr numlines -1 + } + set j 0 + for {set ln 0} {$ln < $numlines} {incr ln} { + if {$ln == 0} { + set c1 "[overtype::left $col1 $k]" + } else { + set c1 "[overtype::left $col1 { ... }]" + } + set line "" + for {set col 0} {$col < 3} {incr col} { + if {$j <= $max} { + append line "$j [list [dict get $v $j]] " + } + incr j + } + do_debug $dlev $debugc "$c1 [string trim $line]" + } + } + gridstring { + set col1 [string repeat " " 25] + set i 0 + foreach ln [split $v \n] { + if {$i == 0} { + set c1 [overtype::left $col1 $k] + } else { + set c1 [overtype::left $col1 { ... }] + } + do_debug $dlev $debugc "$c1 $ln" + incr i + } + } + default { + set col1 [string repeat " " 25] + do_debug $dlev $debugc "[overtype::left $col1 $k] $v" + } + } + } + + + # --------------------------------- + foreach obj [$PARENTS items] { + catch {$obj destroy} + } + $PARENTS destroy + #puts "PROCESSORS: $PROCESSORS" + foreach obj [$PROCESSORS items] { + catch {$obj destroy} + } + $PROCESSORS destroy + catch {$RETURNED_VMAP destroy} + # --------------------------------- + + do_debug 1 $debugc "[string repeat = 40]" + do_debug 1 $debugc "DEBUG-END $caller" + if {[string length $raise_dispatch_error_instead_of_return]} { + return -code error $raise_dispatch_error_instead_of_return + } + + + return $returnval + } + + proc tailflagspaired {defaults supplied classifications rangesbytype} { + lassign [lindex $rangesbytype end] c tp a b + if {($c eq "unallocated") && ($tp eq "flagtype")} { + set tail_unallocated [lrange $supplied $a $b] + } else { + set tail_unallocated [list] + } + #set extraflags [list] + set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated] + #dict merge based operation can't work if there are solo_flags? + #review + if {[llength $tail_unallocated]} { + for {set i $a} {$i <=$b} {incr i} { + set arginfo [dict get $classifications $i] + lassign $arginfo class ftype v + switch -- $ftype { + flag - flagvalue { + lappend extraflags $v + } + soloflag { + lappend extraflags $v + if {[dict exists $defaults $v]} { + lappend extraflags [dict get $defaults $v] + } else { + lappend extraflags 1 + } + } + } + } + foreach {k v} [dict get $defaults] { + if {$k ni $extraflags} { + lappend extraflags $k $v + } + } + } else { + set extraflags $defaults + } + return $extraflags + } + + proc tailflagspaired1 {defaults supplied classifications rangesbytype} { + lassign [lindex $rangesbytype end] c tp a b + if {($c eq "unallocated") && ($tp eq "flagtype")} { + set tail_unallocated [lrange $supplied $a $b] + } else { + set tail_unallocated [list] + } + #set all_post_unallocated_ranges [lsearch -all -inline -index 0 [lrange $rangesbytype $command_range_posn end] "unallocated"] + + set extraflags [list] + + #set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated] + #dict merge based operation can't work if there are solo_flags with no value set + if {[llength $tail_unallocated]} { + for {set i $a} {$i <=$b} {incr i} { + set arginfo [dict get $classifications $i] + lassign $arginfo class ftype v + switch -- $ftype { + flag - flagvalue { + lappend extraflags $v + } + soloflag { + lappend extraflags $v + if {[dict exists $defaults $v]} { + lappend extraflags [dict get $defaults $v] + } else { + lappend extraflags 1 + } + } + } + } + foreach {k v} [dict get $defaults] { + if {$k ni $extraflags} { + lappend extraflags $k $v + } + } + } else { + set extraflags $defaults + } + + } + + + +} + + +namespace eval flagfilter { + + #punk::lib::dict_merge_ordered + + + + #retrieve *only* names that are dependant on the provided namekey - not the key itself + # (query is sorted by the trailing numerical index which represents order the arguments were processed) + proc flag_array_get_sorted_subs {arrname sep namekey} { + upvar $arrname arr + set allsubs [array names arr ${namekey}.*${sep}name,*] + set rnames [lmap nm $allsubs {string reverse $nm}] + set sorted_rnames [lsort -dictionary $rnames] + set ordered [lmap nm $sorted_rnames {string reverse $nm}] + return $ordered + } + + proc flag_array_get_sorted_siblings {arrname sep namekey} { + #determine parent by looking at dot - but confirm parent name is in array. + + } + + + + #dictionary based lsort of reversed names which are presumed to have a trailing separator of some sort and a number e.g: name,0 name,1 ... name,10 etc. + #use -dictionary to ensure embedded numbers are sorted as integers + proc array_names_sorted_by_tail {arrname nameglob} { + upvar $arrname arr + set matched_names [array names arr $nameglob] + set rnames [lmap nm $matched_names {string reverse $nm}] + set sorted_rnames [lsort -dictionary $rnames] + return [lmap nm $sorted_rnames {string reverse $nm}] + } + + +} + + + + + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/funcl-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/funcl-0.1.tm new file mode 100644 index 00000000..ccdc9d99 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/funcl-0.1.tm @@ -0,0 +1,322 @@ +package provide funcl [namespace eval funcl { + variable version + set version 0.1 +}] +#funcl = function list (nested call structure) +# +#a basic functional composition o combinator +#o(f,g)(x) == f(g(x)) + +namespace eval funcl { + + #from punk + proc arg_is_script_shaped {arg} { + if {[string first " " $arg] >= 0} { + return 1 + } elseif {[string first \n $arg] >= 0} { + return 1 + } elseif {[string first ";" $arg] >= 0} { + return 1 + } elseif {[string first \t $arg] >= 0} { + return 1 + } else { + return 0 + } + } + + + proc o args { + set closing [string repeat {]} [expr [llength $args]-1]] + set body "[join $args { [}] \$data $closing" + return $body + } + + proc o_ args { + set body "" + set tails [lrepeat [llength $args] ""] + puts stdout "tails: $tails" + + set end [lindex $args end] + if {[llength $end] == 1 && [arg_is_script_shaped $end]} { + set endfunc [string map " $end" {uplevel 1 [list if 1 ]}] + } else { + set endfunc $end + } + if {[llength $args] == 1} { + return $endfunc + } + + set wrap { [} + append wrap $endfunc + append wrap { ]} + + set i 0 + foreach cmdlist [lrange $args 0 end-1] { + set is_script 0 + if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} { + set is_script 1 + set script [lindex $cmdlist 0] + } + set t "" + if {$i > 0} { + append body { [} + } + set posn [lsearch $cmdlist _] + if {$posn <= 0} { + append body $cmdlist + if {$i == [expr {[llength $args] -2}]} { + #append body " \$data" + append body " $wrap" + } + if {$i > 0} { + set t {]} + } + } else { + append body [lrange $cmdlist 0 $posn-1] + if {$i == [expr {[llength $args] -2}]} { + #append body " \$data" + append body " $wrap" + } + set t [lrange $cmdlist $posn+1 end] + if {$i > 0} { + append t { ]} + } + } + lset tails $i $t + incr i + } + append body [join [lreverse $tails] " "] + puts stdout "tails: $tails" + + return $body + } + + #review - consider _call -- if count > 1 then they must all be callable cmdlists(?) + # what does it mean to have additional _fn wrapper with no other elements? (no actual function) + #e.g _fn 2 5 6 somefunc {_fn 1 3 {_call 1 3 xxx}} {_fn 1 4 command {_fn ...}} + # what type indicates running subtrees in parallel vs sequentially? + # any reason to have _call count other than 1? Presumably the parent node indicates the parallelism/sequentialism etc. + # + # + # accept or return a funcl (or funcltree if multiple funcls in one commandlist) + # also accept/return a call - return empty list if passed a call + proc next_funcl {funcl_or_tree} { + if {[lindex $funcl_or_tree 0] eq "_call"} { + return [list] + } + if {[lindex $funcl_or_tree 0] in [list "_fn" "_call"]} { + set funcl $funcl_or_tree + } else { + error "funcltree not implemented" + } + + + set count [lindex $funcl 1] + if {$count == 0} { + #null funcl.. what is it? metadata/placeholder? + return $funcl + } + set indices [lrange $funcl 2 [expr {1 + $count}]] + set i 0 + foreach idx $indices { + if {$i > 0} { + #todo - return a funcltree + error "multi funcl not implemented" + } + set next [lindex $funcl $idx] + incr i + } + + return $next + + } + + #convert a funcl to a tcl script + proc funcl_script {funcl} { + if {![llength $funcl]} { + return "" + } + set body "" + set tails [list] + + set type [lindex $funcl 0] + if {$type ni [list "_fn" "_call"]} { + #todo - handle funcltree + error "type $type not implemented" + } + + + #only count of 1 with index 3 supported(?) + if {$type eq "_call"} { + #leaf + set cmdlist [lindex $funcl 3] + return $cmdlist + } + + #we will use next_funcl to walk the nodes.. todo support treefuncl response from next_funcl which could branch multiple times. + #by continually passing back the resulting treefuncl/funcl to next_funcl we can process in correct order (?) + # we would still need to maintain state to stitch it back together once returned from a subtree.. + # ie multiple tail parts + set count [lindex $funcl 1] + + if {$count == 1} { + set idx [lindex $funcl 2] + if {$idx == 3} { + set cmdlist_pre [list] + } else { + set cmdlist_pre [lrange $funcl 3 $idx-1] + } + append body $cmdlist_pre + set t [lrange $funcl $idx+1 end] + lappend tails $t + } else { + #?? + error "funcl_script branching not yet supported" + } + + + set get_next 1 + set i 1 + while {$get_next} { + set funcl [next_funcl $funcl] + if {![llength $funcl]} { + set get_next 0 + } + lassign $funcl type count idx ;#todo support count > 1 + if {$type eq "_call"} { + set get_next 0 + } + set t "" + if {$type eq "_call"} { + append body { [} + append body [lindex $funcl $idx] + append body { ]} + } else { + append body { [} + if {$idx == 3} { + set cmdlist_pre [list] + } else { + set cmdlist_pre [lrange $funcl 3 $idx-1] + } + append body $cmdlist_pre + set t [lrange $funcl $idx+1 end] + lappend tails $t + lappend tails { ]} + } + incr i + } + append body [join [lreverse $tails] " "] + #puts stdout "tails: $tails" + + return $body + } + + + interp alias "" o_of "" funcl::o_of_n 1 + + #o_of_n + #tcl list rep o combinator + # + # can take lists of ordinary commandlists, scripts and funcls + # _fn 1 x where 1 indicates number of subfuncls and where x indicates next funcl position (_fn list or _arg) + # _fn 0 indicates next item is an unwrapped commandlist (terminal command) + # + #o_of is equivalent to o_of_n 1 (1 argument o combinator) + #last n args are passed to the prior function + #e.g for n=1 f a b = f(a(b)) + #e.g for n=2, e f a b = e(f(a b)) + proc o_of_n {n args} { + puts stdout "o_of_n '$args'" + if {$n != 1} { + error "o_of_n only implemented for 1 sub-funcl" + } + set comp [list] ;#composition list + set end [lindex $args end] + if {[lindex $end 0] in {_fn _call}]} { + #is_funcl + set endfunc [lindex $args end] + } else { + if {[llength $end] == 1 && [arg_is_script_shaped $end]} { + #set endfunc [string map [list $end] {uplevel 1 [list if 1 ]}] + set endfunc [list _call 1 3 [list uplevel 1 [list if 1 [lindex $end 0]]]] + } else { + set endfunc [list _call 1 3 [list {*}$end]] + } + } + + if {[llength $args] == 1} { + return $endfunc + } + set comp $endfunc + set revlist [lreverse [lrange $args 0 end-1]] + foreach cmdlist $revlist { + puts stderr "o_of_n >>-- $cmdlist" + if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} { + set is_script 1 + set script [lindex $cmdlist 0] + set arglist [list data] + + set comp [list _fn 1 6 call_script $script $arglist $comp] + } else { + set posn1 [expr {[llength $cmdlist] + 2 + $n}] + set comp [list _fn $n $posn1 {*}$cmdlist $comp] + } + } + return $comp + } + proc call_script {script argnames args} { + uplevel 3 [list if 1 [list apply [list $argnames $script] {*}$args]] + } + proc funcl_script_test {scr} { + do_funcl_script_test $scr + } + proc do_funcl_script_test {scr} { + #set j "in do_funcl_script_test" + #set data "xxx" + #puts '$scr' + if 1 $scr + } + + #standard o_ with no script-handling + proc o_plain args { + set body "" + set i 0 + set tails [lrepeat [llength $args] ""] + #puts stdout "tails: $tails" + foreach cmdlist $args { + set t "" + if {$i > 0} { + append body { [} + } + set posn [lsearch $cmdlist _] + if {$posn <= 0} { + append body $cmdlist + if {$i == [expr {[llength $args] -1}]} { + append body " \$data" + } + if {$i > 0} { + set t {]} + } + } else { + append body [lrange $cmdlist 0 $posn-1] + if {$i == [expr {[llength $args] -1}]} { + append body " \$data" + } + set t [lrange $cmdlist $posn+1 end] + if {$i > 0} { + append t { ]} + } + } + lset tails $i $t + incr i + } + append body [join [lreverse $tails] " "] + #puts stdout "tails: $tails" + + return $body + } + #timings suggest no faster to split out the first item from the cmdlist loop +} + + + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config index c79eb6da..922ff786 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config @@ -2,18 +2,29 @@ #bootsupport modules can be pulled in from within other areas of src or from the built module folders of the project #They must be already built, so generally shouldn't come directly from src/modules. +#we want showdict - but it needs punk pipeline notation. +#this requires pulling in punk - which brings in lots of other stuff +#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 set bootsupport_modules [list\ src/vendormodules commandstack\ src/vendormodules cksum\ + src/vendormodules debug\ src/vendormodules dictutils\ src/vendormodules fauxlink\ src/vendormodules fileutil\ src/vendormodules http\ src/vendormodules md5\ + src/vendormodules metaface\ src/vendormodules modpod\ src/vendormodules oolib\ src/vendormodules overtype\ + src/vendormodules pattern\ + src/vendormodules patterncmd\ + src/vendormodules patternlib\ + src/vendormodules patternpredator2\ src/vendormodules sha1\ src/vendormodules tomlish\ src/vendormodules test::tomlish\ @@ -25,8 +36,15 @@ set bootsupport_modules [list\ src/vendormodules textutil::trim\ src/vendormodules textutil::wcswidth\ src/vendormodules uuid\ - modules punkcheck\ + modules argp\ + modules flagfilter\ + modules funcl\ modules natsort\ + modules punk\ + modules punkapp\ + modules punkcheck\ + modules punkcheck::cli\ + modules punk::aliascore\ modules punk::ansi\ modules punk::assertion\ modules punk::args\ @@ -35,6 +53,7 @@ set bootsupport_modules [list\ modules punk::cap::handlers::scriptlibs\ modules punk::cap::handlers::templates\ modules punk::char\ + modules punk::config\ modules punk::console\ modules punk::du\ modules punk::encmime\ @@ -46,6 +65,7 @@ set bootsupport_modules [list\ modules punk::mix::cli\ modules punk::mix::util\ modules punk::mix::templates\ + modules punk::repl::codethread\ modules punk::mix::commandset::buildsuite\ modules punk::mix::commandset::debug\ modules punk::mix::commandset::doc\ @@ -55,14 +75,18 @@ set bootsupport_modules [list\ modules punk::mix::commandset::project\ modules punk::mix::commandset::repo\ modules punk::mix::commandset::scriptwrap\ + modules punk::mod\ + modules punk::nav::fs\ modules punk::ns\ modules punk::overlay\ modules punk::path\ modules punk::packagepreference\ modules punk::repo\ modules punk::tdl\ + modules punk::unixywindows\ modules punk::zip\ modules punk::winpath\ + modules shellfilter\ modules textblock\ modules natsort\ modules oolib\ diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/metaface-1.2.5.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/metaface-1.2.5.tm new file mode 100644 index 00000000..4c88cb16 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/metaface-1.2.5.tm @@ -0,0 +1,6411 @@ +package require dictutils +package provide metaface [namespace eval metaface { + variable version + set version 1.2.5 +}] + + + + +#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 vinfo $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 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 {}} +proc ::p::-1::M {_ID_} { + 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 { + foreach m [dict keys [set ::p::${IID}::_iface::o_methods]] { + lappend members $m + } + } + return $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 {}} +proc ::p::-1::P {_ID_} { + 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 { + foreach prop [dict keys [set ::p::${IID}::_iface::o_properties]] { + lappend members $prop + } + } + 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_} { + 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/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm new file mode 100644 index 00000000..5d76af04 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm @@ -0,0 +1,1285 @@ +#PATTERN +# - A prototype-based Object system. +# +# Julian Noble 2003 +# License: Public domain +# + +# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. +# +# +# Pattern uses a mixture of class-based and prototype-based object instantiation. +# +# A pattern object has 'properties' and 'methods' +# The system makes a distinction between them with regards to the access syntax for write operations, +# and yet provides unity in access syntax for read operations. +# e.g >object . myProperty +# will return the value of the property 'myProperty' +# >ojbect . myMethod +# will return the result of the method 'myMethod' +# contrast this with the write operations: +# set [>object . myProperty .] blah +# >object . myMethod blah +# however, the property can also be read using: +# set [>object . myProperty .] +# Note the trailing . to give us a sort of 'reference' to the property. +# this is NOT equivalent to +# set [>object . myProperty] +# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property +# i.e it is equivalent in this case to: set blah + +#All objects are represented by a command, the name of which contains a leading ">". +#Any commands in the interp which use this naming convention are assumed to be a pattern object. +#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) + +#All user-added properties & methods of the wrapped object are accessed +# using the separator character "." +#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." +# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) +# you would use the 'Create' metamethod on the pattern object like so: +# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject +# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties +# of the object it was created from. ( + + +#The use of the access-syntax separator character "." allows objects to be kept +# 'clean' in the sense that the only methods &/or properties that can be called this way are ones +# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax +# so you are free to implement your own 'Create' method on your object that doesn't conflict with +# the metamethod. + +#Chainability (or how to violate the Law of Demeter!) +#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other +# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference +# structure, without the need to regress to enter matching brackets as is required when using +# standard TCL command syntax. +# ie instead of: +# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething +# we can use: +# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething +# +# This separates out the object-traversal syntax from the TCL command syntax. + +# . is the 'traversal operator' when it appears between items in a commandlist +# . is the 'reference operator' when it is the last item in a commandlist +# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. +# It marks breaks in the multidimensional structure that correspond to how the data is stored. +# e.g obj . arraydata x y , x1 y1 z1 +# represents an element of a 5-dimensional array structured as a plane of cubes +# e.g2 obj . arraydata x y z , x1 y1 +# represents an element of a 5-dimensional array structured as a cube of planes +# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 +# .. is the 'meta-traversal operator' when it appears between items in a commandlist +# .. is the 'meta-info operator'(?) when it is the last item in a commandlist + + +#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing +# implement iStacks & pStacks (interface stacks & pattern stacks) + +#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 + + +#------------------------------------------------------------ +# System objects. +#------------------------------------------------------------ +#::p::-1 ::p::internals::>metaface +#::p::0 ::p::ifaces::>null +#::p::1 ::>pattern +#------------------------------------------------------------ + +#TODO + +#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) + + +#CHANGES +#2018-09 - v 1.2.2 +# varied refactoring +# Changed invocant datastructure curried into commands (the _ID_ structure) +# Changed MAP structure to dict +# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) +# updated test suites +#2018-08 - v 1.2.1 +# split ::p::predatorX functions into separate files (pkgs) +# e.g patternpredator2-1.0.tm +# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken +# +#2017-08 - v 1.1.6 Fairly big overhaul +# New predator function using coroutines +# Added bang operator ! +# Fixed Constructor chaining +# Added a few tests to test::pattern +# +#2008-03 - preserve ::errorInfo during var writes + +#2007-11 +#Major overhaul + new functionality + new tests v 1.1 +# new dispatch system - 'predator'. +# (preparing for multiple interface stacks, multiple invocants etc) +# +# +#2006-05 +# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. +# +#2005-12 +# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. +# +# Fixed so that PatternVariable default applied on Create. +# +# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: +# - heading towards multiple-interface objects +# +#2005-10-28 +# 1.0.8.1 passes 80/80 tests +# >object .. Destroy - improved cleanup of interfaces & namespaces. +# +#2005-10-26 +# fixes to refsync (still messy!) +# remove variable traces on REF vars during .. Destroy +# passes 76/76 +# +#2005-10-24 +# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. +# 1.0.8.0 now passes 75/76 +# +#2005-10-19 +# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) +# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) +# 1.0.8.0 (passes 74/76) +# tests now in own package +# usage: +# package require test::pattern +# test::p::list +# test::p::run ?nameglob? ?-version ? +# +#2005-09?-12 +# +# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. +# fixed @next@ so that destination method resolved at interface compile time instead of call time +# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. +# (before, the overlay only occured when '.. Method' was used to override.) +# +# +# miscellaneous tidy-ups +# +# 1.0.7.8 (passes 71/73) +# +#2005-09-10 +# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value +# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. +# +#2005-09-07 +# bugfix indexed write to list property +# bugfix Variable default value +# 1.0.7.7 (passes 70/72) +# fails: +# arrayproperty.test - array-entire-reference +# properties.test - property_getter_filter_via_ObjectRef +# +#2005-04-22 +# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) +# +# 1.0.7.4 +# +#2004-11-05 +# basic PropertyRead implementation (non-indexed - no tests!) +# +#2004-08-22 +# object creation speedups - (pattern::internals::obj simplified/indirected) +# +#2004-08-17 +# indexed property setter fixes + tests +# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) +# +#2004-08-16 +# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) +# +#2004-08-15 +# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) +# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger +# - also trigger on curried traces to indexed properties i.e list and array elements. +# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. +# +# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] +# +#2004-08-05 +# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) +# +# fix + add tests to support method & property of same name. (method precedence) +# +#2004-08-04 +# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) +# +# 1.0.7.1 +# use objectref array access to read properties even when some props unset; + test +# unset property using array access on object reference; + test +# +# +#2004-07-21 +# object reference changes - array property values appear as list value when accessed using upvared array. +# bugfixes + tests - properties containing lists (multidimensional access) +# +#1.0.7 +# +#2004-07-20 +# fix default property value append problem +# +#2004-07-17 +# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods +# ( +# +#2004-06-18 +# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. +# +#2004-06-05 +# change argsafety operator to be anything with leading - +# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' +# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, +# the entire dash-prefixed operator is also passed in as an argument. +# e.g >object . doStuff -window . +# will call the doStuff method with the 2 parameters -window . +# >object . doStuff - . +# will call doStuff with single parameter . +# >object . doStuff - -window . +# will result in a reference to the doStuff method with the argument -window 'curried' in. +# +#2004-05-19 +#1.0.6 +# fix so custom constructor code called. +# update Destroy metamethod to unset $self +# +#1.0.4 - 2004-04-22 +# bug fixes regarding method specialisation - added test +# +#------------------------------------------------------------ + +package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] + + +namespace eval pattern::util { + + # Generally better to use 'package require $minver-' + # - this only gives us a different error + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } +} + +package require patterncmd 1.2.4- +package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) + + + +#package require cmdline +package require overtype + +#package require md5 ;#will be loaded if/when needed +#package require md4 +#package require uuid + + + + + +namespace eval pattern { + variable initialised 0 + + + if 0 { + if {![catch {package require twapi_base} ]} { + #twapi is a windows only package + #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. + # If available - windows seems to provide a fast uuid generator.. + #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) + # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) + interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok + } else { + #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) + # (e.g 200usec 2018 corei9) + #(with or without tcllibc?) + #very first call is extremely slow though - 3.5seconds on 2018 corei9 + package require uuid + interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate + } + #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) + } + + +} + + + + + + +namespace eval p { + #this is also the interp alias namespace. (object commands created here , then renamed into place) + #the object aliases are named as incrementing integers.. !todo - consider uuids? + variable ID 0 + namespace eval internals {} + + + #!?? + #namespace export ?? + variable coroutine_instance 0 +} + +#------------------------------------------------------------------------------------- +#review - what are these for? +#note - this function is deliberately not namespaced +# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features +proc process_pattern_aliases {object args} { + set o [namespace tail $object] + interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] + interp alias {} process_method_$o {} [$object .. Method .] + interp alias {} process_constructor_$o {} [$object .. Constructor .] +} +#------------------------------------------------------------------------------------- + + + + +#!store all interface objects here? +namespace eval ::p::ifaces {} + + + +#K combinator - see http://wiki.tcl.tk/1923 +#proc ::p::K {x y} {set x} +#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] + + + + + + + + +proc ::p::internals::(VIOLATE) {_ID_ violation_script} { + #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] + set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] + + if {![dict get $processed explicitvars]} { + #no explicit var statements - we need the implicit ones + set self [set ::p::${_ID_}::(self)] + set IFID [lindex [set $self] 1 0 end] + #upvar ::p::${IFID}:: self_IFINFO + + + set varDecls {} + set vlist [array get ::p::${IFID}:: v,name,*] + set _k ""; set v "" + if {[llength $vlist]} { + append varDecls "upvar #0 " + foreach {_k v} $vlist { + append varDecls "::p::\${_ID_}::$v $v " + } + append varDecls "\n" + } + + #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] + set violation_script $varDecls\n[dict get $processed body] + + #tidy up + unset processed varDecls self IFID _k v + } else { + set violation_script [dict get $processed body] + } + unset processed + + + + + #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. + eval "unset violation_script;$violation_script" +} + + +proc ::p::internals::DestroyObjectsBelowNamespace {ns} { + #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" + + set nsparts [split [string trim [string map {:: :} $ns] :] :] + if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { + #ns not of form ::p::?::_ref + + foreach obj [info commands ${ns}::>*] { + #catch {::p::meta::Destroy $obj} + #puts ">>found object $obj below ns $ns - destroying $obj" + $obj .. Destroy + } + } + + #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] + #foreach tinfo $traces { + # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo + #} + #unset -nocomplain ${ns}::-->PATTERN_ANCHOR + + foreach sub [namespace children $ns] { + ::p::internals::DestroyObjectsBelowNamespace $sub + } +} + + + + +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# + + + + + + + + + +proc ::p::get_new_object_id {} { + tailcall incr ::p::ID + #tailcall ::pattern::new_uuid +} + +#create a new minimal object - with no interfaces or patterns. + +#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} +proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { + + #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" + + if {$OID eq "-2"} { + set OID [::p::get_new_object_id] + #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) + #set OID [pattern::new_uuid] + } + #if $wrapped provided it is assumed to be an existing namespace. + #if {[string length $wrapped]} { + # #??? + #} + + #sanity check - alias must not exist for this OID + if {[llength [interp alias {} ::p::$OID]]} { + error "Object alias '::p::$OID' already exists - cannot create new object with this id" + } + + #system 'varspaces' - + + #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://wiki.tcl.tk/1030 'Dangers of creative writing') + #set o_open 1 - every object is initially also an open interface (?) + #NOTE! comments within namespace eval slow it down. + namespace eval ::p::$OID { + #namespace ensemble create + namespace eval _ref {} + namespace eval _meta {} + namespace eval _iface { + variable o_usedby; + variable o_open 1; + array set o_usedby [list]; + variable o_varspace "" ; + variable o_varspaces [list]; + variable o_methods [dict create]; + variable o_properties [dict create]; + variable o_variables; + variable o_propertyunset_handlers; + set o_propertyunset_handlers [dict create] + } + } + + #set alias ::p::$OID + + #objectid alis default_method object_command wrapped_namespace + set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] + + #MAP is a dict + set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] + + + + #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token + #we've already checked that ::p::$OID doesn't pre-exist + # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias + #interp alias {} ::p::$OID {} ::p::internals::predator $MAP + + + # _ID_ structure + set invocants_dict [dict create this [list $INVOCANTDATA] ] + #puts stdout "New _ID_structure: $interfaces_dict" + set _ID_ [dict create i $invocants_dict context ""] + + + interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ + #rename the command into place - thus the alias & the command name no longer match! + rename ::p::$OID $cmd + + set ::p::${OID}::_meta::map $MAP + + # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something + interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ + + #set p2 [string map {> ?} $cmd] + #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ + + + #trace add command $cmd delete "$cmd .. Destroy ;#" + #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" + + trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" + #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) + + #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" + + + #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" + #trace add command $cmd delete "puts deleting$cmd ;#" + #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" + + + #puts "--> new_object returning map $MAP" + return $MAP +} + + + + +#>x .. Create >y +# ".." is special case equivalent to "._." +# (whereas in theory it would be ".default.") +# "." is equivalent to ".default." is equivalent to ".default.default." (...) + +#>x ._. Create >y +#>x ._.default. Create >y ??? +# +# + +# create object using 'blah' as source interface-stack ? +#>x .blah. .. Create >y +#>x .blah,_. ._. Create .iStackDestination. >y + + + +# +# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] +# the 1st item, blah in this case becomes the 'default' iStack. +# +#>x .*. +# cast to object with all iStacks +# +#>x .*,!_. +# cast to object with all iStacks except _ +# +# --------------------- +#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' +# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. +# +#eg1: >x & >y . some_multi_method arg arg +# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) +# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' +# The invocant signature is thus {these 2} +# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) +# Invocation roles can be specified in the call using the @ operator. +# e.g >x & >y @ points . some_multi_method arg arg +# The invocant signature for this is: {points 2} +# +#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path +# This has the signature {objects n plane 1} where n depends on the length of the list $objects +# +# +# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. +# e.g set pointset [>x & >y .] +# We can now call multimethods on $pointset +# + + + + + + +#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) +proc ::pattern::predatorversion {{ver ""}} { + variable active_predatorversion + set allowed_predatorversions {1 2} + set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions + + if {![info exists active_predatorversion]} { + set first_time_set 1 + } else { + set first_time_set 0 + } + + if {$ver eq ""} { + #get version + if {$first_time_set} { + set active_predatorversions $default_predatorversion + } + return $active_predatorversion + } else { + #set version + if {$ver ni $allowed_predatorversions} { + error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" + } + + if {!$first_time_set} { + if {$active_predatorversion eq $ver} { + #puts stderr "Active predator version is already '$ver'" + #ok - nothing to do + return $active_predatorversion + } else { + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + rename ::p::internals::predator ::p::predator$active_predatorversion + } + } + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + + rename ::p::predator$ver ::p::internals::predator + set active_predatorversion $ver + + return $active_predatorversion + } +} +::pattern::predatorversion 2 + + + + + + + + + + + + +# >pattern has object ID 1 +# meta interface has object ID 0 +proc ::pattern::init args { + + if {[set ::pattern::initialised]} { + if {[llength $args]} { + #if callers want to avoid this error, they can do their own check of $::pattern::initialised + error "pattern package is already initialised. Unable to apply args: $args" + } else { + return 1 + } + } + + #this seems out of date. + # - where is PatternPropertyRead? + # - Object is obsolete + # - Coinjoin, Combine don't seem to exist + array set ::p::metaMethods { + Clone object + Conjoin object + Combine object + Create object + Destroy simple + Info simple + Object simple + PatternProperty simple + PatternPropertyWrite simple + PatternPropertyUnset simple + Property simple + PropertyWrite simple + PatternMethod simple + Method simple + PatternVariable simple + Variable simple + Digest simple + PatternUnknown simple + Unknown simple + } + array set ::p::metaProperties { + Properties object + Methods object + PatternProperties object + PatternMethods object + } + + + + + + #create metaface - IID = -1 - also OID = -1 + # all objects implement this special interface - accessed via the .. operator. + + + + + + set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface + + + #OID = 0 + ::p::internals::new_object ::p::ifaces::>null "" 0 + + #? null object has itself as level0 & level1 interfaces? + #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] + + #null interface should always have 'usedby' members. It should never be extended. + array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array + set ::p::0::_iface::o_open 0 + + set ::p::0::_iface::o_constructor [list] + set ::p::0::_iface::o_variables [list] + set ::p::0::_iface::o_properties [dict create] + set ::p::0::_iface::o_methods [dict create] + set ::p::0::_iface::o_varspace "" + set ::p::0::_iface::o_varspaces [list] + array set ::p::0::_iface::o_definition [list] + set ::p::0::_iface::o_propertyunset_handlers [dict create] + + + + + ############################### + # OID = 1 + # >pattern + ############################### + ::p::internals::new_object ::>pattern "" 1 + + #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] + + + array set ::p::1::_iface::o_usedby [list] ;#'usedby' array + + set _self ::pattern + + #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 + #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 + + + + #1)this object references its interfaces + #lappend ID $IFID $IFID_1 + #lset SELFMAP 1 0 $IFID + #lset SELFMAP 2 0 $IFID_1 + + + #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] + #proc ::>pattern args $body + + + + + ####################################################################################### + #OID = 2 + # >ifinfo interface for accessing interfaces. + # + ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object + set ::p::2::_iface::o_constructor [list] + set ::p::2::_iface::o_variables [list] + set ::p::2::_iface::o_properties [dict create] + set ::p::2::_iface::o_methods [dict create] + set ::p::2::_iface::o_varspace "" + set ::p::2::_iface::o_varspaces [list] + array set ::p::2::_iface::o_definition [list] + set ::p::2::_iface::o_open 1 ;#open for extending + + ::p::ifaces::>2 .. AddInterface 2 + + #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations + #(bootstrap because we can't yet use metaface methods on it) + + + + proc ::p::2::_iface::isOpen.1 {_ID_} { + return $::p::2::_iface::o_open + } + interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 + + proc ::p::2::_iface::isClosed.1 {_ID_} { + return [expr {!$::p::2::_iface::o_open}] + } + interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 + + proc ::p::2::_iface::open.1 {_ID_} { + set ::p::2::_iface::o_open 1 + } + interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 + + proc ::p::2::_iface::close.1 {_ID_} { + set ::p::2::_iface::o_open 0 + } + interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 + + + #proc ::p::2::_iface::(GET)properties.1 {_ID_} { + # set ::p::2::_iface::o_properties + #} + #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 + + #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties + + + #proc ::p::2::_iface::(GET)methods.1 {_ID_} { + # set ::p::2::_iface::o_methods + #} + #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 + #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods + + + + + + #link from object to interface (which in this case are one and the same) + + #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] + #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] + #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] + #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] + + interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen + interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed + interp alias {} ::p::2::open {} ::p::2::_iface::open + interp alias {} ::p::2::close {} ::p::2::_iface::close + + + #namespace eval ::p::2 "namespace export $method" + + ####################################################################################### + + + + + + + set ::pattern::initialised 1 + + + ::p::internals::new_object ::p::>interface "" 3 + #create a convenience object on which to manipulate the >ifinfo interface + #set IF [::>pattern .. Create ::p::>interface] + set IF ::p::>interface + + + #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? + # (or is forcing end user to add their own pStack/iStack ok .. ?) + # + ::p::>interface .. AddPatternInterface 2 ;# + + ::p::>interface .. PatternVarspace _iface + + ::p::>interface .. PatternProperty methods + ::p::>interface .. PatternPropertyRead methods {} { + varspace _iface + var {o_methods alias} + return $alias + } + ::p::>interface .. PatternProperty properties + ::p::>interface .. PatternPropertyRead properties {} { + varspace _iface + var o_properties + return $o_properties + } + ::p::>interface .. PatternProperty variables + + ::p::>interface .. PatternProperty varspaces + + ::p::>interface .. PatternProperty definition + + ::p::>interface .. Constructor {{usedbylist {}}} { + #var this + #set this @this@ + #set ns [$this .. Namespace] + #puts "-> creating ns ${ns}::_iface" + #namespace eval ${ns}::_iface {} + + varspace _iface + var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces + + set o_constructor [list] + set o_variables [list] + set o_properties [dict create] + set o_methods [dict create] + set o_varspaces [list] + array set o_definition [list] + + foreach usedby $usedbylist { + set o_usedby(i$usedby) 1 + } + + + } + ::p::>interface .. PatternMethod isOpen {} { + varspace _iface + var o_open + + return $o_open + } + ::p::>interface .. PatternMethod isClosed {} { + varspace _iface + var o_open + + return [expr {!$o_open}] + } + ::p::>interface .. PatternMethod open {} { + varspace _iface + var o_open + set o_open 1 + } + ::p::>interface .. PatternMethod close {} { + varspace _iface + var o_open + set o_open 0 + } + ::p::>interface .. PatternMethod refCount {} { + varspace _iface + var o_usedby + return [array size o_usedby] + } + + set ::p::2::_iface::o_open 1 + + + + + uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} + #uplevel #0 {package require patternlib} + return 1 +} + + + +proc ::p::merge_interface {old new} { + #puts stderr " ** ** ** merge_interface $old $new" + set ns_old ::p::$old + set ns_new ::p::$new + + upvar #0 ::p::${new}:: IFACE + upvar #0 ::p::${old}:: IFACEX + + if {![catch {set c_arglist $IFACEX(c,args)}]} { + #constructor + #for now.. just add newer constructor regardless of any existing one + #set IFACE(c,args) $IFACEX(c,args) + + #if {![info exists IFACE(c,args)]} { + # #target interface didn't have a constructor + # + #} else { + # # + #} + } + + + set methods [::list] + foreach nm [array names IFACEX m-1,name,*] { + lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) + } + + #puts " *** merge interface $old -> $new ****merging-in methods: $methods " + + foreach method $methods { + if {![info exists IFACE(m-1,name,$method)]} { + #target interface doesn't yet have this method + + set THISNAME $method + + if {![string length [info command ${ns_new}::$method]]} { + + if {![set ::p::${old}::_iface::o_open]} { + #interp alias {} ${ns_new}::$method {} ${ns_old}::$method + #namespace eval $ns_new "namespace export [namespace tail $method]" + } else { + #wait to compile + } + + } else { + error "merge interface - command collision " + } + #set i 2 ??? + set i 1 + + } else { + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + + set i [incr IFACE(m-1,chain,$method)] + + set THISNAME ___system___override_${method}_$i + + #move metadata using subindices for delegated methods + set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) + set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) + set IFACE(mp-$i,$method) $IFACE(mp-1,$method) + + set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) + set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) + + + #set next [::p::next_script $IFID0 $method] + if {![string length [info command ${ns_new}::$THISNAME]]} { + if {![set ::p::${old}::_iface::o_open]} { + interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method + namespace eval $ns_new "namespace export $method" + } else { + #wait for compile + } + } else { + error "merge_interface - command collision " + } + + } + + array set IFACE [::list \ + m-1,chain,$method $i \ + m-1,body,$method $IFACEX(m-1,body,$method) \ + m-1,args,$method $IFACEX(m-1,args,$method) \ + m-1,name,$method $THISNAME \ + m-1,iface,$method $old \ + ] + + } + + + + + + #array set ${ns_new}:: [array get ${ns_old}::] + + + #!todo - review + #copy everything else across.. + + foreach {nm v} [array get IFACEX] { + #puts "-.- $nm" + if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { + set IFACE($nm) $v + } + } + + #!todo -write a test + set ::p::${new}::_iface::o_open 1 + + #!todo - is this done also when iface compiled? + #namespace eval ::p::$new {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place + + return +} + + + + +#detect attempt to treat a reference to a method as a property +proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { +#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" + lassign [lrange $args end-2 end] vtraced vidx op + #NOTE! cannot rely on vtraced as it may have been upvared + + switch -- $op { + write { + error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + unset { + #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace + #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #!todo - don't use vtraced! + trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #pointless raising an error as "Any errors in unset traces are ignored" + #error "cannot unset. $field is a method not a property" + } + read { + error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + array { + error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" + #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" + } + } + + return +} + + + + +#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. +# +# The 'dispatcher' is an object instance's underlying object command. +# + +#proc ::p::make_dispatcher {obj ID IFID} { +# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { +# ::p::@IID@ $methprop @oid@ {*}$args +# }] +# return +#} + + + + +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +#aliased from ::p::${OID}:: +# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something +proc ::p::internals::no_default_method {_ID_ args} { + puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped + tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" +} + +#force 1 will extend an interface even if shared. (??? why is this necessary here?) +#if IID empty string - create the interface. +proc ::p::internals::expand_interface {IID {force 0}} { + #puts stdout ">>> expand_interface $IID [info level -1]<<<" + if {![string length $IID]} { + #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) + set iid [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$iid + return $iid + } else { + if {[set ::p::${IID}::_iface::o_open]} { + #interface open for extending - shared or not! + return $IID + } + + if {[array size ::p::${IID}::_iface::o_usedby] > 1} { + #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby + + #oops.. shared interface. Copy before specialising it. + set prev_IID $IID + + #set IID [::p::internals::new_interface] + set IID [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$IID + + ::p::internals::linkcopy_interface $prev_IID $IID + #assert: prev_usedby contains at least one other element. + } + + #whether copied or not - mark as open for extending. + set ::p::${IID}::_iface::o_open 1 + return $IID + } +} + +#params: old - old (shared) interface ID +# new - new interface ID +proc ::p::internals::linkcopy_interface {old new} { + #puts stderr " ** ** ** linkcopy_interface $old $new" + set ns_old ::p::${old}::_iface + set ns_new ::p::${new}::_iface + + + + foreach nsmethod [info commands ${ns_old}::*.1] { + #puts ">>> adding $nsmethod to iface $new" + set tail [namespace tail $nsmethod] + set method [string range $tail 0 end-2] ;#strip .1 + + if {![llength [info commands ${ns_new}::$method]]} { + + set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 + + #link from new interface namespace to existing one. + #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) + #!todo? verify? + #- actual link is chainslot to chainslot + interp alias {} ${ns_new}::$method.1 {} $oldhead + + #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? + + + #chainhead pointer within new interface + interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 + + namespace eval $ns_new "namespace export $method" + + #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { + # lappend ${ns_new}::o_methods $method + #} + } else { + if {$method eq "(VIOLATE)"} { + #ignore for now + #!todo + continue + } + + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + #warning - existing chainslot will be completely shadowed by linked method. + # - existing one becomes unreachable. #!todo review!? + + + error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" + + } + } + + + #foreach propinf [set ${ns_old}::o_properties] { + # lassign $propinf prop _default + # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop + # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop + # lappend ${ns_new}::o_properties $propinf + #} + + + set ${ns_new}::o_variables [set ${ns_old}::o_variables] + set ${ns_new}::o_properties [set ${ns_old}::o_properties] + set ${ns_new}::o_methods [set ${ns_old}::o_methods] + set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] + + + set ::p::${old}::_iface::o_usedby(i$new) linkcopy + + + #obsolete.? + array set ::p::${new}:: [array get ::p::${old}:: ] + + + + #!todo - is this done also when iface compiled? + #namespace eval ::p::${new}::_iface {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' + + return +} +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +pattern::init + +return $::pattern::version diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm new file mode 100644 index 00000000..4107b8af --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm @@ -0,0 +1,645 @@ +package provide patterncmd [namespace eval patterncmd { + variable version + + set version 1.2.4 +}] + + +namespace eval pattern { + variable idCounter 1 ;#used by pattern::uniqueKey + + namespace eval cmd { + namespace eval util { + package require overtype + variable colwidths_lib [dict create] + variable colwidths_lib_default 15 + + dict set colwidths_lib "library" [list ch " " num 21 head "|" tail ""] + dict set colwidths_lib "version" [list ch " " num 7 head "|" tail ""] + dict set colwidths_lib "type" [list ch " " num 9 head "|" tail ""] + dict set colwidths_lib "note" [list ch " " num 31 head "|" tail "|"] + + proc colhead {type args} { + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + set line "" + foreach colname [dict keys $colwidths] { + append line "[col $type $colname [string totitle $colname] {*}$args]" + } + return $line + } + proc colbreak {type} { + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + set line "" + foreach colname [dict keys $colwidths] { + append line "[col $type $colname {} -backchar - -headoverridechar + -tailoverridechar +]" + } + return $line + } + proc col {type col val args} { + # args -head bool -tail bool ? + #---------------------------------------------------------------------------- + set known_opts [list -backchar -headchar -tailchar -headoverridechar -tailoverridechar -justify] + dict set default -backchar "" + dict set default -headchar "" + dict set default -tailchar "" + dict set default -headoverridechar "" + dict set default -tailoverridechar "" + dict set default -justify "left" + if {([llength $args] % 2) != 0} { + error "(pattern::cmd::util::col) ERROR: uneven options supplied - must be of form '-option value' " + } + foreach {k v} $args { + if {$k ni $known_opts} { + error "((pattern::cmd::util::col) ERROR: option '$k' not in known options: '$known_opts'" + } + } + set opts [dict merge $default $args] + set backchar [dict get $opts -backchar] + set headchar [dict get $opts -headchar] + set tailchar [dict get $opts -tailchar] + set headoverridechar [dict get $opts -headoverridechar] + set tailoverridechar [dict get $opts -tailoverridechar] + set justify [dict get $opts -justify] + #---------------------------------------------------------------------------- + + + + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + #calculate headwidths + set headwidth 0 + set tailwidth 0 + foreach {key def} $colwidths { + set thisheadlen [string length [dict get $def head]] + if {$thisheadlen > $headwidth} { + set headwidth $thisheadlen + } + set thistaillen [string length [dict get $def tail]] + if {$thistaillen > $tailwidth} { + set tailwidth $thistaillen + } + } + + + set spec [dict get $colwidths $col] + if {[string length $backchar]} { + set ch $backchar + } else { + set ch [dict get $spec ch] + } + set num [dict get $spec num] + set headchar [dict get $spec head] + set tailchar [dict get $spec tail] + + if {[string length $headchar]} { + set headchar $headchar + } + if {[string length $tailchar]} { + set tailchar $tailchar + } + #overrides only apply if the head/tail has a length + if {[string length $headchar]} { + if {[string length $headoverridechar]} { + set headchar $headoverridechar + } + } + if {[string length $tailchar]} { + if {[string length $tailoverridechar]} { + set tailchar $tailoverridechar + } + } + set head [string repeat $headchar $headwidth] + set tail [string repeat $tailchar $tailwidth] + + set base [string repeat $ch [expr {$headwidth + $num + $tailwidth}]] + if {$justify eq "left"} { + set left_done [overtype::left $base "$head$val"] + return [overtype::right $left_done "$tail"] + } elseif {$justify in {centre center}} { + set mid_done [overtype::centre $base $val] + set left_mid_done [overtype::left $mid_done $head] + return [overtype::right $left_mid_done $tail] + } else { + set right_done [overtype::right $base "$val$tail"] + return [overtype::left $right_done $head] + } + + } + + } + } + +} + +#package require pattern + +proc ::pattern::libs {} { + set libs [list \ + pattern {-type core -note "alternative:pattern2"}\ + pattern2 {-type core -note "alternative:pattern"}\ + patterncmd {-type core}\ + metaface {-type core}\ + patternpredator2 {-type core}\ + patterndispatcher {-type core}\ + patternlib {-type core}\ + patterncipher {-type optional -note optional}\ + ] + + + + package require overtype + set result "" + + append result "[cmd::util::colbreak lib]\n" + append result "[cmd::util::colhead lib -justify centre]\n" + append result "[cmd::util::colbreak lib]\n" + foreach libname [dict keys $libs] { + set libinfo [dict get $libs $libname] + + append result [cmd::util::col lib library $libname] + if {[catch [list package present $libname] ver]} { + append result [cmd::util::col lib version "N/A"] + } else { + append result [cmd::util::col lib version $ver] + } + append result [cmd::util::col lib type [dict get $libinfo -type]] + + if {[dict exists $libinfo -note]} { + set note [dict get $libinfo -note] + } else { + set note "" + } + append result [cmd::util::col lib note $note] + append result "\n" + } + append result "[cmd::util::colbreak lib]\n" + return $result +} + +proc ::pattern::record {recname fields} { + if {[uplevel 1 [list namespace which $recname]] ne ""} { + error "(pattern::record) Can't create command '$recname': A command of that name already exists" + } + + set index -1 + set accessor [list ::apply { + {index rec args} + { + if {[llength $args] == 0} { + return [lindex $rec $index] + } + if {[llength $args] == 1} { + return [lreplace $rec $index $index [lindex $args 0]] + } + error "Invalid number of arguments." + } + + }] + + set map {} + foreach field $fields { + dict set map $field [linsert $accessor end [incr index]] + } + uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec] +} +proc ::pattern::record2 {recname fields} { + if {[uplevel 1 [list namespace which $recname]] ne ""} { + error "(pattern::record) Can't create command '$recname': A command of that name already exists" + } + + set index -1 + set accessor [list ::apply] + + set template { + {rec args} + { + if {[llength $args] == 0} { + return [lindex $rec %idx%] + } + if {[llength $args] == 1} { + return [lreplace $rec %idx% %idx% [lindex $args 0]] + } + error "Invalid number of arguments." + } + } + + set map {} + foreach field $fields { + set body [string map [list %idx% [incr index]] $template] + dict set map $field [list ::apply $body] + } + uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec] +} + +proc ::argstest {args} { + package require cmdline + +} + +proc ::pattern::objects {} { + set result [::list] + + foreach ns [namespace children ::pp] { + #lappend result [::list [namespace tail $ns] [set ${ns}::(self)]] + set ch [namespace tail $ns] + if {[string range $ch 0 2] eq "Obj"} { + set OID [string range $ch 3 end] ;#OID need not be digits (!?) + lappend result [::list $OID [list OID $OID object_command [set pp::${ch}::v_object_command] usedby [array names ${ns}::_iface::o_usedby]]] + } + } + + + + + return $result +} + + + +proc ::pattern::name {num} { + #!todo - fix + #set ::p::${num}::(self) + + lassign [interp alias {} ::p::$num] _predator info + if {![string length $_predator$info]} { + error "No object found for num:$num (no interp alias for ::p::$num)" + } + set invocants [dict get $info i] + set invocants_with_role_this [dict get $invocants this] + set invocant_this [lindex $invocants_with_role_this 0] + + + #lassign $invocant_this id info + #set map [dict get $info map] + #set fields [lindex $map 0] + lassign $invocant_this _id _ns _defaultmethod name _etc + return $name +} + + +proc ::pattern::with {cmd script} { + foreach c [info commands ::p::-1::*] { + interp alias {} [namespace tail $c] {} $c $cmd + } + interp alias {} . {} $cmd . + interp alias {} .. {} $cmd .. + + return [uplevel 1 $script] +} + + + + + +#system diagnostics etc + +proc ::pattern::varspace_list {IID} { + namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_variables o_variables + + set varspaces [list] + dict for {vname vdef} $o_variables { + set vs [dict get $vdef varspace] + if {$vs ni $varspaces} { + lappend varspaces $vs + } + } + if {$o_varspace ni $varspaces} { + lappend varspaces $o_varspace + } + return $varspaces +} + +proc ::pattern::check_interfaces {} { + foreach ns [namespace children ::p] { + set IID [namespace tail $ns] + if {[string is digit $IID]} { + foreach ref [array names ${ns}::_iface::o_usedby] { + set OID [string range $ref 1 end] + if {![namespace exists ::p::${OID}::_iface]} { + puts -nonewline stdout "\r\nPROBLEM!!!!!!!!! nonexistant/invalid object $OID referenced by Interface $IID\r\n" + } else { + puts -nonewline stdout . + } + + + #if {![info exists ::p::${OID}::(self)]} { + # puts "PROBLEM!!!!!!!!! nonexistant object $OID referenced by Interface $IID" + #} + } + } + } + puts -nonewline stdout "\r\n" +} + + +#from: http://wiki.tcl.tk/8766 (Introspection on aliases) +#usedby: metaface-1.1.6+ +#required because aliases can be renamed. +#A renamed alias will still return it's target with 'interp alias {} oldname' +# - so given newname - we require which_alias to return the same info. + proc ::pattern::which_alias {cmd} { + uplevel 1 [list ::trace add execution $cmd enterstep ::error] + catch {uplevel 1 $cmd} res + uplevel 1 [list ::trace remove execution $cmd enterstep ::error] + #puts stdout "which_alias $cmd returning '$res'" + return $res + } +# [info args] like proc following an alias recursivly until it reaches +# the proc it originates from or cannot determine it. +# accounts for default parameters set by interp alias +# + + + +proc ::pattern::aliasargs {cmd} { + set orig $cmd + + set defaultargs [list] + + # loop until error or return occurs + while {1} { + # is it a proc already? + if {[string equal [info procs $cmd] $cmd]} { + set result [info args $cmd] + # strip off the interp set default args + return [lrange $result [llength $defaultargs] end] + } + # is it a built in or extension command we can get no args for? + if {![string equal [info commands $cmd] $cmd]} { + error "\"$orig\" isn't a procedure" + } + + # catch bogus cmd names + if {[lsearch [interp aliases {}] $cmd]==-1} { + if {[catch {::pattern::which_alias $cmd} alias]} { + error "\"$orig\" isn't a procedure or alias or command" + } + #set cmd [lindex $alias 0] + if {[llength $alias]>1} { + set cmd [lindex $alias 0] + set defaultargs [concat [lrange $alias 1 end] $defaultargs] + } else { + set cmd $alias + } + } else { + + if {[llength [set cmdargs [interp alias {} $cmd]]]>0} { + # check if it is aliased in from another interpreter + if {[catch {interp target {} $cmd} msg]} { + error "Cannot resolve \"$orig\", alias leads to another interpreter." + } + if {$msg != {} } { + error "Not recursing into slave interpreter \"$msg\".\ + \"$orig\" could not be resolved." + } + # check if defaults are set for the alias + if {[llength $cmdargs]>1} { + set cmd [lindex $cmdargs 0] + set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs] + } else { + set cmd $cmdargs + } + } + } + } + } +proc ::pattern::aliasbody {cmd} { + set orig $cmd + + set defaultargs [list] + + # loop until error or return occurs + while {1} { + # is it a proc already? + if {[string equal [info procs $cmd] $cmd]} { + set result [info body $cmd] + # strip off the interp set default args + return $result + #return [lrange $result [llength $defaultargs] end] + } + # is it a built in or extension command we can get no args for? + if {![string equal [info commands $cmd] $cmd]} { + error "\"$orig\" isn't a procedure" + } + + # catch bogus cmd names + if {[lsearch [interp aliases {}] $cmd]==-1} { + if {[catch {::pattern::which_alias $cmd} alias]} { + error "\"$orig\" isn't a procedure or alias or command" + } + #set cmd [lindex $alias 0] + if {[llength $alias]>1} { + set cmd [lindex $alias 0] + set defaultargs [concat [lrange $alias 1 end] $defaultargs] + } else { + set cmd $alias + } + } else { + + if {[llength [set cmdargs [interp alias {} $cmd]]]>0} { + # check if it is aliased in from another interpreter + if {[catch {interp target {} $cmd} msg]} { + error "Cannot resolve \"$orig\", alias leads to another interpreter." + } + if {$msg != {} } { + error "Not recursing into slave interpreter \"$msg\".\ + \"$orig\" could not be resolved." + } + # check if defaults are set for the alias + if {[llength $cmdargs]>1} { + set cmd [lindex $cmdargs 0] + set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs] + } else { + set cmd $cmdargs + } + } + } + } + } + + + + + +proc ::pattern::uniqueKey2 {} { + #!todo - something else?? + return [clock seconds]-[incr ::pattern::idCounter] +} + +#used by patternlib package +proc ::pattern::uniqueKey {} { + return [incr ::pattern::idCounter] + #uuid with tcllibc is about 30us compared with 2us + # for large datasets, e.g about 100K inserts this would be pretty noticable! + #!todo - uuid pool with background thread to repopulate when idle? + #return [uuid::uuid generate] +} + + + +#------------------------------------------------------------------------------------------------------------------------- + +proc ::pattern::test1 {} { + set msg "OK" + + puts stderr "next line should say:'--- saystuff:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. PatternMethod saystuff args { + puts stderr "--- saystuff: $args" + } + ::>thing .. Create ::>jjj + + ::>jjj . saystuff $msg + ::>jjj .. Destroy + ::>thing .. Destroy +} + +proc ::pattern::test2 {} { + set msg "OK" + + puts stderr "next line should say:'--- property 'stuff' value:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. PatternProperty stuff $msg + + ::>thing .. Create ::>jjj + + puts stderr "--- property 'stuff' value:[::>jjj . stuff]" + ::>jjj .. Destroy + ::>thing .. Destroy +} + +proc ::pattern::test3 {} { + set msg "OK" + + puts stderr "next line should say:'--- property 'stuff' value:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. Property stuff $msg + + puts stderr "--- property 'stuff' value:[::>thing . stuff]" + ::>thing .. Destroy +} + +#--------------------------------- +#unknown/obsolete + + + + + + + + +#proc ::p::internals::showargs {args {ch stdout}} {puts $ch $args} +if {0} { + proc ::p::internals::new_interface {{usedbylist {}}} { + set OID [incr ::p::ID] + ::p::internals::new_object ::p::ifaces::>$OID "" $OID + puts "obsolete >> new_interface created object $OID" + foreach usedby $usedbylist { + set ::p::${OID}::_iface::o_usedby(i$usedby) 1 + } + set ::p::${OID}::_iface::o_varspace "" ;#default varspace is the object's namespace. (varspace is absolute if it has leading :: , otherwise it's a relative namespace below the object's namespace) + #NOTE - o_varspace is only the default varspace for when new methods/properties are added. + # it is possible to create some methods/props with one varspace value, then create more methods/props with a different varspace value. + + set ::p::${OID}::_iface::o_constructor [list] + set ::p::${OID}::_iface::o_variables [list] + set ::p::${OID}::_iface::o_properties [dict create] + set ::p::${OID}::_iface::o_methods [dict create] + array set ::p::${OID}::_iface::o_definition [list] + set ::p::${OID}::_iface::o_open 1 ;#open for extending + return $OID + } + + + #temporary way to get OID - assumes single 'this' invocant + #!todo - make generic. + proc ::pattern::get_oid {_ID_} { + #puts stderr "#* get_oid: [lindex [dict get $_ID_ i this] 0 0]" + return [lindex [dict get $_ID_ i this] 0 0] + + #set invocants [dict get $_ID_ i] + #set invocant_roles [dict keys $invocants] + #set role_members [dict get $invocants this] + ##set this_invocant [lindex $role_members 0] ;#for the role 'this' we assume only one invocant in the list. + #set this_invocant [lindex [dict get $_ID_ i this] 0] ; + #lassign $this_invocant OID this_info + # + #return $OID + } + + #compile the uncompiled level1 interface + #assert: no more than one uncompiled interface present at level1 + proc ::p::meta::PatternCompile {self} { + ???? + + upvar #0 $self SELFMAP + set ID [lindex $SELFMAP 0 0] + + set patterns [lindex $SELFMAP 1 1] ;#list of level1 interfaces + + set iid -1 + foreach i $patterns { + if {[set ::p::${i}::_iface::o_open]} { + set iid $i ;#found it + break + } + } + + if {$iid > -1} { + #!todo + + ::p::compile_interface $iid + set ::p::${iid}::_iface::o_open 0 + } else { + #no uncompiled interface present at level 1. Do nothing. + return + } + } + + + proc ::p::meta::Def {self} { + error ::p::meta::Def + + upvar #0 $self SELFMAP + set self_ID [lindex $SELFMAP 0 0] + set IFID [lindex $SELFMAP 1 0 end] + + set maxc1 0 + set maxc2 0 + + set arrName ::p::${IFID}:: + + upvar #0 $arrName state + + array set methods {} + + foreach nm [array names state] { + if {[regexp {^m-1,name,(.+)} $nm _match mname]} { + set methods($mname) [set state($nm)] + + if {[string length $mname] > $maxc1} { + set maxc1 [string length $mname] + } + if {[string length [set state($nm)]] > $maxc2} { + set maxc2 [string length [set state($nm)]] + } + } + } + set bg1 [string repeat " " [expr {$maxc1 + 2}]] + set bg2 [string repeat " " [expr {$maxc2 + 2}]] + + + set r {} + foreach nm [lsort -dictionary [array names methods]] { + set arglist $state(m-1,args,$nm) + append r "[overtype::left $bg1 $nm] : [overtype::left $bg2 $methods($nm)] [::list $arglist]\n" + } + return $r + } + + + +} \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternlib-1.2.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternlib-1.2.6.tm new file mode 100644 index 00000000..bd4b3e59 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternlib-1.2.6.tm @@ -0,0 +1,2590 @@ +#JMN 2004 +#public domain + + +package provide patternlib [namespace eval patternlib { + + variable version + set version 1.2.6 +}] + + + +#Change History +#------------------------------------------------------------------------------- +#2022-05 +# added . search and . itemKeys methods to >collection to enable lookups by value +#2021-09 +# Add >keyvalprotector - an object to overload various collection methods such as 'remove' to stop deletion of specific items. +# +#2006-05 +# deprecate 'del' in favour of 'remove' - 'del' still there but delegated to 'remove'. todo - emit deprecation warnings. +# +#2005-04 +# remove 'name' method - incorporate indexed retrieval into 'names' method +# !todo? - adjust key/keys methods for consistency? +# +#2004-10 +# initial key aliases support +# fix negative index support on some methods e.g remove +#2004-08 +# separated >collection predicate methods out onto separate 'mixin' object >predicatedCollection +# added $posn $result variables to predicate methods, changed varnames from $k $v to $key $value +# +#2004-06-05 +# added 'sort' method to sort on values. +# fixed 'keySort' method to accept multiple sort options +# added predicate methods 'all' 'allKeys' 'collectAll' +#2004-06-01 +# '>collection . names' method now accepts optional 'glob' parameter to filter result +#2004-05-19 +#fix '>collection . clear' method so consecutive calls don't raise an error +#------------------------------------------------------------------------------- + +namespace eval ::patternlib::util { + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } + + #bloom filter experiment https://wiki.tcl-lang.org/page/A+Simple+Bloom+Filter + # k-hashes + # m-bits + # n-elements + # optimal value of k: (m/n)ln(2) + #proc bloom_optimalNumHashes {capacity_n bitsize_m} { + # expr { round((double($bitsize_m) / $capacity_n) * log(2))} + #} + #proc bloom_optimalNumBits {capacity fpp} { + # expr {entier(-$capacity * log($fpp) / (log(2) * log(2)))} + #} + +} +::patternlib::util::package_require_min pattern 1.2.4 +#package require pattern +::pattern::init ;# initialises (if not already) + + +namespace eval ::patternlib {namespace export {[a-z]*} + namespace export {[>]*} + + variable keyCounter 0 ;#form part of unique keys for collections when items added without any key specified + proc uniqueKey {} { + return [incr ::patternlib::keyCounter] + } + +#!todo - multidimensional collection +# - o_list as nested list +# - o_array with compound keys(?) how will we unambiguously delimit dimensions in a concatenated key? +# - perhaps a key is always a list length n where n is the number of dimensions? +# - therefore we'll need an extra level of nesting for the current base case n=1 +# +# - how about a nested dict for each key-structure (o_list & o_array) ? + +#COLLECTION +# +#!todo? - consider putting the actual array & list vars in the objects namespace, and using the instancevars to hold their names +# - consider array-style access using traced var named same as collection. +# would this defeat the purpose ? if it was faster, would users always use array syntax in preference.. in which case they may as well just use arrays..? +#!todo - add boolean property to force unique values as well as keys + + +#::pattern::create >collection + + + + +::>pattern .. Create >collection +set COL >collection +#process_pattern_aliases [namespace origin >collection] +#process_pattern_aliases ::patternlib::>collection +$COL .. Property version 1.0 +$COL .. PatternDefaultMethod item + +set PV [$COL .. PatternVariable .] + +$PV o_data +#$PV o_array +#$PV o_list +$PV o_alias +$PV this + +#for invert method +$PV o_dupes 0 + + +$COL .. PatternProperty bgEnum + + +#PV o_ns + +$PV m_i_filteredCollection + +#set ID [lindex [set >collection] 0 0] ;#context ID +#set IID [lindex [set >collection] 1 0] ;#level 1 base-interface ID + +$COL .. Constructor {args} { + var o_data m_i_filteredCollection o_count o_bgEnum + + var this + set this @this@ + + set m_i_filteredCollection 0 + if {![llength $args]} { + set o_data [dict create] + #array set o_array [list] + #set o_list [list] + set o_count 0 + } elseif {[llength $args] == 1} { + set o_data [dict create] + set pairs [lindex $args 0] + if {[llength $pairs] % 2} { + error "patternllib::>collection - if an argument given to constructor, it must have an even number of elements. Bad args: $args" + } + set keys_seen [list] + foreach key [dict keys $pairs] { + if {[string is integer -strict $key] } { + error ">collection key must be non-integer. Bad key: $key. No items added." + } + if {$key in $keys_seen} { + error "key '$key' already exists in this collection. No items added." + } + lappend keys_seen $key + } + unset keys_seen + #rely on dict ordering guarantees (post 8.5? preserves order?) + set o_data [dict merge $o_data[set o_data {}] $pairs] + set o_count [dict size $o_data] + } else { + error "patternlib::>collection constructor did not understand arguments supplied. Try a dict as a single argument." + } + array set o_alias [list] + + array set o_bgEnum [list] + @next@ +} +#comment block snipped from collection Constructor + #--------------------------------------------- + #set o_selfID [lindex [set $o_this] 0] ;#object id always available in methods as $_ID_ anyway + # + #### OBSOLETE - left as example of an approach + #make count property traceable (e.g so property ref can be bound to Tk widgets) + #!todo - manually update o_count in relevant methods faster?? + # should avoid trace calls for addList methods, shuffle etc + # + #set handler ::p::${_ID_}::___count_TraceHandler + #proc $handler {_ID_ vname vidx op} { + # #foreach {vname vidx op} [lrange $args end-2 end] {break} + # #! we shouldn't trust this vname - it may be that we are being accessed via upvar so it is a different name + # + # #this is only a 'write' handler + # set ::p::[lindex ${_ID_} 0 0]::o_count [llength [set ::p::[lindex ${_ID_} 0 0]::o_list]] + # return + #} + #trace add variable o_list {write} [list $handler $_ID_] + #### + # + # + #puts "--->collection constructor id: $_ID_" + + + + +set PM [$COL .. PatternMethod .] + + +#!review - why do we need the count method as well as the property? +#if needed - document why. +# read traces on count property can be bypassed by method call... shouldn't we avoid that? +#2018 - in theory write traces on the . count property are very useful from an application-writer's perpective. +# +$COL .. PatternMethod count {} { + #we don't require any instance vars to be upvar'ed - argless [var] stops them automatically being added. + #we directly refer to the ::O:: var if only accessing a few times rather than upvar'ing. + var o_data + dict size $o_data +} + +$COL .. PatternProperty count +$COL .. PatternPropertyWrite count {_val} { + var + error "count property is read-only" +} + +$COL .. PatternPropertyUnset count {} { + var +} ;#cannot raise error's in unset trace handlers - simply fail to unset silently + +$COL .. PatternMethod isEmpty {} { + #var o_list + #return [expr {[llength $o_list] == 0}] + var o_data + expr {[dict size $o_data] == 0} +} + +$COL .. PatternProperty inverted 0 + + + +###### +# item +###### +#defaults to fifo when no idx supplied (same as 'pair' method). !review? is lifo more logical/intuitive/useful? +# i.e [>obj . item] returns the 1st element in the list +#[>obj . item -1] returns the last element (equiv to "end" keyword used by Tcl list commands) +#[>obj . item -2] returns 2nd last element (equiv to "end-1") + + +$COL .. PatternMethod item {{idx 0}} { + #with pattern::0::$OID access.. was measured faster than item2 : approx 110us vs 140us for 26element collection accessed via string (time {>col $key} 10000) + # (still at least 20 times slower than a plain array... at <5us) + var o_data o_alias + + #!todo - review 'string is digit' vs 'string is integer' ?? + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + set keys [dict keys $o_data] + if {[catch {dict get $o_data [lindex $keys $idx]} result]} { + var this + error "no such index : '$idx' in collection: $this" + } else { + return $result + } + } else { + if {[catch {dict get $o_data $idx} result]} { + if {[catch {set o_alias($idx)} nextIdx ]} { + var this + error "no such index: '$idx' in collection: $this" + } else { + #try again + #return $o_array($nextIdx) + #tailcall? + #item $_ID_ $nextIdx + #puts stdout "\n\n\n !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! about to call tailcall item $_ID_ $nextIdx \n\n\n" + tailcall item $_ID_ $nextIdx + } + } else { + return $result + } + } +} + + + +if {0} { +#leave this here for comparison. +$COL .. PatternMethod item2 {{idx 0}} { + var o_array o_list o_alias this + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + if {[catch {set o_array([lindex $o_list $idx])} result]} { + error "no such index : '$idx' in collection: $this" + } else { + return $result + } + } else { + if {[catch {set o_array($idx)} result]} { + + if {[catch {set o_alias($idx)} nextIdx ]} { + error "no such index: '$idx' in collection: $this" + } else { + #try again + #return $o_array($nextIdx) + item $_ID_ $nextIdx + } + } else { + return $result + } + } + +} +} + +#simple no-frills access for speed.. (timed at 43us vs 63us for item (depending on dispatch method!)) +$COL .. PatternMethod itemNamed {idx} { + var o_data + dict get $o_data $idx +} +$COL .. PatternMethod in {idx} { + var o_data + dict get $o_data $idx +} + +$COL .. PatternMethod itemAt {idx} { + var o_data + dict get $o_data [lindex [dict keys $o_data] $idx] +} + +$COL .. PatternMethod replace {idx val} { + var o_data o_alias this + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + if {[catch {dict set o_data [lindex [dict keys $o_data] $idx] $val}]} { + error "no such index: '$idx' in collection: $this" + } else { + return $val + } + } else { + if {[catch {dict set o_data $idx $val}]} { + if {[catch {set o_alias($idx)} nextIdx ]} { + error "no such index: '$idx' in collection: $this" + } else { + #try again + tailcall replace $_ID_ $nextIdx $val + } + + } else { + return $val + } + } +} + +#if the supplied index is an alias, return the underlying key; else return the index supplied. +$COL .. PatternMethod realKey {idx} { + var o_alias + + if {[catch {set o_alias($idx)} key]} { + return $idx + } else { + return $key + } +} + +#note alias feature is possibly ill-considered. +#if we delete an item - should we delete corresponding alias? If not - we then would need to allow adding under an alias only if the corresponding key is missing. +$COL .. PatternMethod alias {newAlias existingKeyOrAlias} { + var o_alias + + #set existingKey [realKey $_ID_ $existingKeyOrAlias] + #alias to the supplied KeyOrAlias - not the underlying key + + if {[string is integer -strict $newAlias]} { + error "collection key alias cannot be integer" + } + + if {[string length $existingKeyOrAlias]} { + set o_alias($newAlias) $existingKeyOrAlias + } else { + unset o_alias($newAlias) + } +} +$COL .. PatternMethod aliases {{key ""}} { + var o_alias + + if {[string length $key]} { + set result [list] + #lsearch -stride? + foreach {n v} [array get o_alias] { + if {$v eq $key} { + lappend result $n $v + } + } + + return $result + } else { + return [array get o_alias] + } +} + +#'pop' & 'unshift' methods !todo - optimize so lsearch not called when numerical idx/posn already supplied + +#default to removing item from the end, otherwise from supplied index (position or key) +#!todo - accept alias indices +#!todo - review.. should any corresponding alias be destroyed when the corresponding item is popped (or removed in any way?) +#!todo - review.. for performance.. shouldn't pop NOT accept an index? +#if we need to pop from other than the end.. this could be a separate function. Do other langs use pop with an index?? +$COL .. PatternMethod pop {{idx ""}} { + var o_data o_count + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] end] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + set posn [lsearch -exact [dict keys $o_data] $key] + + if {($posn >= 0) && ($posn < [dict size $o_data])} { + set result [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } +} +$COL .. PatternMethod poppair {} { + var o_data o_count + set key [lindex [dict keys $o_data] end] + set val [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return [list $key $val] +} + + + +#!todo - add 'push' method... (basically specialized versions of 'add') +#push - add at end (effectively an alias for add) +#shift - add at start ???bad name? this is completely at odds with for example the common Perl shift function, which returns and removes the first element of an array. +#add - add at end + +#ordered +$COL .. PatternMethod items {} { + var o_data + + dict values $o_data +} + + + + +#### +#pair +#### +#fifo-style accesss when no idx supplied (likewise with 'add' method) +$COL .. PatternMethod pair {{idx 0}} { + var o_data + + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + + if {[catch {dict get $o_data $key} val]} { + error "no such index: '$idx'" + } else { + return [list $key $val] + } +} +$COL .. PatternMethod pairs {} { + var o_data + set o_data +} + +$COL .. PatternMethod get {} { + var o_data + set o_data +} +#todo - fix >pattern so that methods don't collide with builtins +#may require change to use oo - or copy 'my' mechanism to call own methods +$COL .. PatternMethod Info {} { + var o_data + return [dict info $o_data] +} +#2006-05-21.. args to add really should be in key, value order? +# - this the natural order in array-like lists +# - however.. key should be optional. + +$COL .. PatternMethod add {val args} { + #(using args instead of {key ""} enables use of empty string as a key ) + + var o_data o_alias o_count this + + if {![llength $args]} { + set key "_[::patternlib::uniqueKey]_" + } else { + #!todo - could we handle multiple val,key pairs without impacting performance of the common case? + if {[llength $args] > 1} { + error "add method expected 'val' and optional 'key' - got: $val $args" + + } + + set key [lindex $args 0] + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + + if {[dict exists $o_data $key]} { + #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]" + error "key '$key' already exists in collection $this" + } + if {[info exists o_alias($key)]} { + if {[dict exists $o_data $o_alias($key)]} { + #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias + error "key '$key' already exists as an alias for $o_alias($key) in collection $this" + } + } + + dict set o_data $key $val + + + set posn $o_count + incr o_count + + return $posn +} + + +#should the 'stack' methods such as shift,push,pop,peek actually be on a separate interface? +#what then of methods like 'count' which apply equally well to collections and stacks? + +#Alias for 'add' - is there a way to alias this to add implementation with zero overhead?? +$COL .. PatternMethod push {val args} { + #(using args instead of {key ""} enables use of empty string as a key ) + + var o_data o_alias o_count this + + if {![llength $args]} { + set key "_[::patternlib::uniqueKey]_" + } else { + #!todo - could we handle multiple val,key pairs without impacting performance of the common case? + if {[llength $args] > 1} { + error "add method expected 'val' and optional 'key' - got: $val $args" + + } + + set key [lindex $args 0] + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + + if {[dict exists $o_data $key]} { + #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]" + error "key '$key' already exists in collection $this" + } + if {[info exists o_alias($key)]} { + if {[dict exists $o_data $o_alias($key)]} { + #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias + error "key '$key' already exists as an alias for $o_alias($key) in collection $this" + } + } + + dict set o_data $key $val + + + set posn $o_count + incr o_count + + return $posn +} + + +#shift/unshift - roughly analogous to those found in Perl & PHP +#unshift adds 1 or more values to the beginning of the collection. +$COL .. PatternMethod unshift {values {keys ""}} { + var o_data o_count + + if {![llength $keys]} { + for {set i 0} {$i < [llength $values]} {incr i} { + lappend keys "_[::patternlib::uniqueKey]_" + } + } else { + #check keys before we insert any of them. + foreach newkey $keys { + if {[string is integer -strict $newkey]} { + error "cannot accept key '$newkey', >collection keys must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + } + if {[llength $values] != [llength $keys]} { + error "unshift requires same number of keys as values. (or no keys for auto-generated keys) Received [llength $values] values, [llength $keys] keys" + } + + #separate loop through keys because we want to fail the whole operation if any are invalid. + + set existing_keys [dict keys $o_data] + foreach newkey $keys { + if {$newkey in $exisint_keys} { + #puts stderr "==============> key $key already exists in this collection" + error "key '$newkey' already exists in this collection" + } + } + + + #ok - looks like entire set can be inserted. + set newpairs [list] + foreach val $values key $keys { + lappend newpairs $key $val + } + set o_data [concat $newpairs $o_data[set o_data {}]] + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#default to removing item from the beginning, otherwise from supplied index (position or key) +#!todo - accept alias indices +$COL .. PatternMethod shift {{idx ""}} { + var o_data o_count + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] 0] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + set posn [lsearch -exact [dict keys $o_data] $key] + + if {($posn >= 0) && (($posn/2) < [dict size $o_data])} { + set result [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } +} + + +$COL .. PatternMethod peek {} { + var o_data + + #set o_array([lindex $o_list end]) + + #dict get $o_data [lindex [dict keys $o_data] end] + lindex $o_data end +} + +$COL .. PatternMethod peekKey {} { + var o_data + #lindex $o_list end + lindex $o_data end-1 +} + + +$COL .. PatternMethod insert {val args} { + var o_data o_count + + set idx 0 + set key "" + + if {[llength $args] <= 2} { + #standard arg (ordered) style: + #>obj . insert $value $position $key + + lassign $args idx key + } else { + #allow for literate programming style: + #e.g + # >obj . insert $value at $listPosition as $key + + if {[catch {array set iargs $args}]} { + error "insert did not understand argument list. +usage: +>obj . insert \$val \$position \$key +>obj . insert \$val at \$position as \$key" + } + if {[info exists iargs(at)]} { + set idx $iargs(at) + } + if {[info exists iargs(as)]} { + set key $iargs(as) + } + } + + if {![string length $key]} { + set key "_[::patternlib::uniqueKey]_" + } + + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + + + if {[dict exists $o_data $key]} { + #puts stderr "==============> key $key already exists in this collection" + error "key '$key' already exists in this collection" + } + + if {$idx eq "end"} { + #lappend o_list $key + #standard dict set will add it to the end anyway + dict set o_data $key $val + + } else { + #set o_list [linsert $o_list $idx $key] + + #treat dict as list + set o_data [linsert $o_data[set o_data {}] [expr {$idx*2}] $key $val] + } + + + #set o_array($key) $val + + + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#!todo - deprecate and give it a better name! addDict addPairs ? +$COL .. PatternMethod addArray {list} { + var + puts stderr "patternlib::>collection WARNING: addArray deprecated - call addPairs with same argument instead" + tailcall addPairs $_ID_ $list +} +$COL .. PatternMethod addPairs {list} { + var o_data o_alias o_count + if {[llength $list] % 2} { + error "must supply an even number of elements" + } + + set aliaslist [array names o_alias] + #set keylist [dict keys $o_data] + foreach newkey [dict keys $list] { + if {[string is integer -strict $newkey] } { + error ">collection key must be non-integer. Bad key: $newkey. No items added." + } + + #if {$newkey in $keylist} {} + #for small to medium collections - testing for newkey in $keylist is probably faster, + # but we optimise here for potentially large existing collections, where presumably a dict exists lookup will be more efficient. + if {[dict exists $o_data $newkey]} { + error "key '$newkey' already exists in this collection. No items added." + } + #The assumption is that there are in general relatively few aliases - so a list test is appropriate + if {$newkey in $aliaslist} { + if {[dict exists $o_data $o_alias($newkey)]} { + error "key '$newkey' already exists as an alias for $o_alias($newkey) in collection. No items added " + } + } + #! check if $list contains dups? + #- slows method down - for little benefit? + } + #!todo - test? (but we need a loop to test for integer keys.. so what's the point?) + #set intersection [struct::set intersect [dict keys $list] [dict keys $o_data]] + #if {[llength $intersection]} { + # error "keys '$intersection' already present in this collection. No items added." + #} + + + #rely on dict ordering guarantees (post 8.5? preserves order?) + set o_data [dict merge $o_data[set o_data {}] $list] + + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} +$COL .. PatternMethod addList {list} { + var o_data o_count + + foreach val $list { + dict set o_data "_[::patternlib::uniqueKey]_" $val + #!todo - test. Presumably lappend faster because we don't need to check existing keys.. + #..but.. is there shimmering involved in treating o_data as a list? + #lappend o_data _[::patternlib::uniqueKey]_ $val + + #tested 2008-06 tcl8.6a0 lappend was slower as the gain is lost (and more!) during subsequent [dict size $o_data] + } + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#'del' is not a very good name... as we're not really 'deleting' anything. +# 'remove' seems better, and appears to be more consistent with other languages' collection implementations. +#!todo - handle 'endRange' parameter for removing ranges of items. +$COL .. PatternMethod del {idx {endRange ""}} { + var + #!todo - emit a deprecation warning for 'del' + tailcall remove $_ID_ $idx $endRange +} + +$COL .. PatternMethod remove {idx {endRange ""}} { + var o_data o_count o_alias this + + if {[string length $endRange]} { + 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} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx' in collection: $this" + } else { + #try with next key in alias chain... + #return [remove $_ID_ $nextKey] + tailcall remove $_ID_ $nextKey + } + } + } + + dict unset o_data $key + + set o_count [dict size $o_data] + return +} + +#ordered +$COL .. PatternMethod names {{globOrIdx {}}} { + var o_data + + if {[llength $globOrIdx]} { + if {[string is integer -strict $globOrIdx]} { + #Idx + set idx $globOrIdx + + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + + + if {[catch {lindex [dict keys $o_data] $idx} result]} { + error "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] + } +} + +#ordered +$COL .. PatternMethod keys {} { + #like 'names' but without globbing + var o_data + dict keys $o_data +} + +#Unfortunately the string 'name' is highly collidable when mixing in a collection over existing objects +# - !todo - review. Is it worth adjusting the collection methodnames to avoid a few common collision cases? +# - some sort of resolution order/interface-selection is clearly required anyway +# so perhaps it's generally best not to bother being 'polite' here, and implement a robust understandable resolution mechanism. +# In the mean time however... we'll at least avoid 'name'! +# +#$PM name {{posn 0}} { +# var o_array o_list +# +# if {$posn < 0} { +# set posn "end-[expr {abs($posn + 1)}]" +# } +# +# if {[catch {lindex $o_list $posn} result]} { +# error "no such index : '$posn'" +# } else { +# return $result +# } +#} + +$COL .. PatternMethod key {{posn 0}} { + var o_data + + if {$posn < 0} { + set posn "end-[expr {abs($posn + 1)}]" + } + + if {[catch {lindex [dict keys $o_data] $posn} result]} { + error "no such index : '$posn'" + } else { + return $result + } +} + + +#!todo - consider use of 'end-x' syntax for 'to', and implications re consistency with other commands. +$COL .. PatternMethod setPosn {idx to} { + var o_data + + if {![string is integer -strict $to]} { + error "destination position must be numeric, consider reKey method if you are trying to change the string key under which this value is stored" + } + + if {[string is integer -strict $idx]} { + set idx [expr {$idx % [dict size $o_data]}] + + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + } + + set to [expr {$to % [dict size $o_data]}] + + + set val [dict get $o_data $key] + dict unset o_data $key + + #treat dict as list + set o_data [linsert $o_data[set o_data {}] [expr {$posn*2}] $key $val] + + #set o_list [lreplace $o_list $posn $posn] + #set o_list [linsert $o_list $to $key] + + return $to +} +#!todo - improve efficiency of calls to other functions on this object.. 'inline'?? +#presumably the collection object functionality will be long-term stable because it's purpose is to be a core datastructure; therefore it should be reasonable to favour efficiency over maintainability. +$COL .. PatternMethod incrPosn {idx {by 1}} { + var o_data + if {[string is integer -strict $idx]} { + set idx [expr {$idx % [dict size $o_data]}] + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + } + + set newPosn [expr {($posn + $by) % [dict size $o_data]}] + + setPosn $_ID_ $posn $newPosn + return $newPosn +} +$COL .. PatternMethod decrPosn {idx {by 1}} { + var + return [incrPosn $_ID_ $idx [expr {- $by}]] +} +$COL .. PatternMethod move {idx to} { + var + return [setPosn $_ID_ $idx $to] +} +$COL .. PatternMethod posn {key} { + var o_data + return [lsearch -exact [dict keys $o_data] $key] +} + +#!todo? - disallow numeric values for newKey so as to be consistent with add +#!note! - item can be reKeyed out from under an alias such that the alias chain no longer points to anything +# - this is ok. +$COL .. PatternMethod reKey {idx newKey} { + var o_data o_alias + + + if {[dict exists $o_data $newKey]} { + #puts stderr "==============> reKey collision, key $newKey already exists in this collection" + error "reKey collision, key '$newKey' already exists in this collection" + } + if {[info exists o_alias($newKey)]} { + if {[dict exists $o_data $o_alias($newKey)]} { + error "reKey collision, key '$newKey' already present as an alias in this collection" + } else { + set newKey $o_alias($newKey) + } + } + + + + 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} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx'" + } else { + #try with next key in alias chain... + #return [reKey $_ID_ $nextKey $newKey] + tailcall reKey $_ID_ $nextKey $newKey + } + } + } + + #set o_list [lreplace $o_list $posn $posn $newKey] + ##atomic? (traces on array?) + #set o_array($newKey) $o_array($key) + #unset o_array($key) + + dict set o_data $newKey [dict get $o_data $key] + dict unset o_data $key + + return +} +$COL .. PatternMethod hasKey {key} { + var o_data + dict exists $o_data $key +} +$COL .. PatternMethod hasAlias {key} { + var o_alias + info exists o_alias($key) +} + +#either key or alias +$COL .. PatternMethod hasIndex {key} { + var o_data o_alias + if {[dict exists $o_data $key]} { + return 1 + } else { + return [info exists o_alias($key)] + } +} + + +#Shuffle methods from http://mini.net/tcl/941 +$COL .. PatternMethod shuffleFast {} { + #shuffle6 - fast, but some orders more likely than others. + + var o_data + + set keys [dict keys $o_data] + + set n [llength $keys] + for { set i 1 } { $i < $n } { incr i } { + set j [expr { int( rand() * $n ) }] + set temp [lindex $keys $i] + lset keys $i [lindex $keys $j] + lset keys $j $temp + } + + #rebuild dict in new order + #!todo - can we do the above 'in place'? + set newdata [dict create] + foreach k $keys { + dict set newdata $k [dict get $o_data $k] + } + set o_data $newdata + + return +} +$COL .. PatternMethod shuffle {} { + #shuffle5a + + var o_data + + set n 1 + set keys [list] ;#sorted list of keys + foreach k [dict keys $o_data] { + #set index [expr {int(rand()*$n)}] + + #set slist [linsert [::pattern::K $keys [set keys {}]] $index $k] + + #faster alternative.. 'inline K' [lindex [list a b] 0] ~ [K a b] + set keys [linsert [lindex [list $keys [set keys {}]] 0] [expr {int(rand()*$n)}] $k] + incr n + } + + #rebuild dict in new order + #!todo - can we do the above 'in place'? + set newdata [dict create] + foreach k $keys { + dict set newdata $k [dict get $o_data $k] + } + set o_data $newdata + + return +} + + +#search is a somewhat specialised form of 'itemKeys' +$COL .. PatternMethod search {value args} { + var o_data + #only search on values as it's possible for keys to match - especially with options such as -glob + 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 + } +} + +#inverse lookup +$COL .. PatternMethod itemKeys {value} { + var o_data + #only search on values as it's possible for keys to match + 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 +} + +#invert: +#change collection to be indexed by its values with the old keys as new values. +# - keys of duplicate values become a list keyed on the value. +#e.g the array equivalent is: +# arr(a) 1 +# arr(b) 2 +# arr(c) 2 +#becomes +# inv(1) a +# inv(2) {b c} +#where the order of duplicate-value keys is not defined. +# +#As the total number of keys may change on inversion - order is not preserved if there are ANY duplicates. +# + + +#!todo - try just [lreverse $o_data] ?? + + +$COL .. PatternMethod invert {{splitvalues ""}} { + + var o_data o_count o_dupes o_inverted + + + if {$splitvalues eq ""} { + #not overridden - use o_dupes from last call to determine if values are actually keylists. + if {$o_dupes > 0} { + set splitvalues 1 + } else { + set splitvalues 0 + } + } + + + #set data [array get o_array] + set data $o_data + + if {$o_count > 500} { + #an arbitrary optimisation for 'larger' collections. + #- should theoretically keep the data size and save some reallocations. + #!todo - test & review + # + foreach nm [dict keys $o_data] { + dict unset o_data $nm + } + } else { + set o_data [dict create] + } + + if {!$splitvalues} { + dict for {k v} $data { + dict set o_data $v $k + } + } else { + dict for {k v} $data { + #we're splitting values because each value is a list of keys + #therefore sub should be unique - no need for lappend in this branch. + foreach sub $v { + #if {[info exists o_array($sub)]} { + # puts stderr "---here! v:$v sub:$sub k:$k" + # lappend o_array($sub) $k + #} else { + dict set o_data $sub $k + #} + } + } + } + + + if {[dict size $o_data] != $o_count} { + #must have been some dupes + + set o_dupes [expr {$o_count - [dict size $o_data]}] + #update count to match inverted collection + set o_count [dict size $o_data] + } else { + set o_dupes 0 + } + + set o_inverted [expr {!$o_inverted}] + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $o_dupes +} + + + + + + +#NOTE: values are treated as lists and split into separate keys for inversion only if requested! +# To treat values as keylists - set splitvalues 1 +# To treat each value atomically - set splitvalues 0 +# i.e only set splitvalues 1 if you know the values represent duplicate keys from a previous call to invert! +# +# +#Initially call invert with splitvalues = 0 +#To keep calling invert and get back where you started.. +# The rule is... if the previous call to invert returned > 0... pass 1 on the next call. +# +$COL .. PatternMethod invert_manual {{splitvalues 0}} { + #NOTE - the list nesting here is *tricky* - It probably isn't broken. + + var o_list o_array o_count + + set data [array get o_array] + + if {$o_count > 500} { + #an arbitrary optimisation for 'large' collections. + #- should theoretically keep the array size and save some reallocations. + #!todo - test & review + # + foreach nm [array names o_array] { + unset o_array($nm) + } + } else { + array unset o_array + } + + if {!$splitvalues} { + foreach {k v} $data { + lappend o_array($v) $k + } + } else { + foreach {k v} $data { + #we're splitting values because each value is a list of keys + #therefore sub should be unique - no need for lappend in this branch. + foreach sub $v { + #if {[info exists o_array($sub)]} { + # puts stderr "---here! v:$v sub:$sub k:$k" + # lappend o_array($sub) $k + #} else { + set o_array($sub) $k + #} + } + } + } + + + if {[array size o_array] != $o_count} { + #must have been some dupes + set o_list [array names o_array] + + + set dupes [expr {$o_count - [array size o_array]}] + #update count to match inverted collection + set o_count [array size o_array] + } else { + #review - are these machinations worthwhile for order preservation? what speed penalty do we pay? + array set prev $data + set i -1 + if {$splitvalues} { + #values are lists of length one. Take lindex 0 so list values aren't overnested. + foreach oldkey $o_list { + lset o_list [incr i] [lindex $prev($oldkey) 0] + } + } else { + foreach oldkey $o_list { + lset o_list [incr i] $prev($oldkey) + } + } + + set dupes 0 + } + + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $dupes +} + + + +#Note that collections cannot be inverted without loss of information if they have duplicates AND compound keys +# (keys that are lists) +$COL .. PatternMethod invert_lossy {{splitvalues 1}} { + var o_list o_array o_count + + set data [array get o_array] + + if {$o_count > 500} { + #an arbitrary optimisation for 'large' collections. + #- should theoretically keep the array size and save some reallocations. + #!todo - test & review + # + foreach nm [array names o_array] { + unset o_array($nm) + } + } else { + array unset o_array + } + + if {!$splitvalues} { + foreach {k v} $data { + #note! we must check for existence and use 'set' for first case. + #using 'lappend' only will result in deeper nestings on each invert! + #If you don't understand this - don't change it! + if {[info exists o_array($v)]} { + lappend o_array($v) $k + } else { + set o_array($v) $k + } + } + } else { + foreach {k v} $data { + #length test necessary to avoid incorrect 'un-nesting' + #if {[llength $v] > 1} { + foreach sub $v { + if {[info exists o_array($sub)]} { + lappend o_array($sub) $k + } else { + set o_array($sub) $k + } + } + #} else { + # if {[info exists o_array($v)]} { + # lappend o_array($v) $k + # } else { + # set o_array($v) $k + # } + #} + } + } + + + if {[array size o_array] != $o_count} { + #must have been some dupes + set o_list [array names o_array] + + + set dupes [expr {$o_count - [array size o_array]}] + #update count to match inverted collection + set o_count [array size o_array] + } else { + #review - are these machinations worthwhile for order preservation? what speed penalty do we pay? + array set prev $data + set i -1 + foreach oldkey $o_list { + lset o_list [incr i] $prev($oldkey) + } + set dupes 0 + } + + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $dupes +} + +$COL .. PatternMethod reverse {} { + var o_data + + 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 +} + +$COL .. PatternMethod keySort {{options -ascii}} { + var o_data + + set keys [lsort {*}$options [dict keys $o_data]] + + set dictnew [dict create] + foreach k $keys { + dict set dictnew $k [dict get $o_data $k] + } + set o_data $dictnew + + return +} + +#!todo - allow simple options in combination with options such as -command and -object. Redo args handling completely for more complex sorting. +$COL .. PatternMethod sort {args} { + var o_data + + #defaults + set options [dict create -index 1] ;#values always in subelement 1 of name-value pair list for sorting. + + set options_simple [list] + + + for {set i 0} {$i < [llength $args]} {incr i} { + set a [lindex $args $i] + switch -- $a { + -indices - + -ascii - + -dictionary - + -integer - + -real - + -increasing - + -decreasing { + #dict set options $a 1 + lappend options_simple $a + } + -unique { + #not a valid option + #this would stuff up the data... + #!todo? - remove dups from collection if this option used? - alias the keys? + } + -object { + #!todo - treat value as object and allow sorting by sub-values .eg >col1 . sort -object ". sub . property" -increasing + #may be slow - but handy. Consider -indexed property to store/cache these values on first run + } + -command { + dict set options $a [lindex $args [incr i]] + } + -index { + #allow sorting on subindices of the value. + dict set options -index [concat [dict get $options -index] [lindex $args [incr i]] ] + } + default { + #unrecognised option - print usage? + } + } + } + + + + if {[set posn [lsearch -exact $options_simple "-indices"]] >= 0} { + + var o_array + + set slist [list] + foreach k [dict keys $o_data] { + lappend slist [list $k [dict get $o_data $k]] + } + return [lsort {*}$options_simple {*}$options $slist] + + + + #set options_simple [lreplace $options_simple $posn $posn] ;# + #set slist [list] + #foreach {n v} [array get ::p::[lindex ${_ID_} 0 0]::o_array] { + # lappend slist [list $n $v] + #} + #set slist [lsort {*}$options_simple {*}$options $slist] + #foreach i $slist { + # #determine the position in the collections list + # lappend result {*}[lsearch -exact $o_list [lindex $i 0]] + #} + #return $result + } else { + set slist [list] + dict for {k v} $o_data { + lappend slist [list $k $v] + } + #set slist [lsort {*}$options_simple {*}$options $slist] + set slist [lsort {*}$options_simple {*}$options $slist[set slist {}]] ;#K combinator for efficiency + + + #set o_list [lsearch -all -inline -subindices -index 0 $slist *] + + set o_data [dict create] + foreach pair $slist { + dict set o_data [lindex $pair 0] [lindex $pair 1] + } + + + + return + } + +} + + +$COL .. PatternMethod clear {} { + var o_data o_count + + set o_data [dict create] + set o_count 0 + #aliases? + return +} + +#see http://wiki.tcl.tk/15271 - A generic collection traversal interface +# +#!todo - options: -progresscommand -errorcommand (-granularity ?) (-self ? (to convert to an iterator?)) +#!todo? - lazy retrieval of items so that all changes to the collection are available to a running asynch enumeration? +# - should this be an option? which mechanism should be the default? +# - currently only the keylist is treated in 'snapshot' fashion +# so values could be changed and the state could be invalidated by other code during an enumeration +# +$COL .. PatternMethod enumerate {args} { + #---------- + lassign [lrange $args end-1 end] cmd seed + set optionlist [list] + foreach a [lrange $args 0 end-2] { + lappend optionlist $a + } + set opt(-direction) left + set opt(-completioncommand) "" + array set opt $optionlist + #---------- + var o_data + + if {[string tolower [string index $opt(-direction) 0]] eq "r"} { + #'right' 'RIGHT' 'r' etc. + set list [lreverse [dict keys $o_data]] + } else { + #normal left-right order + set list [dict keys $o_data] + } + + if {![string length $opt(-completioncommand)]} { + #standard synchronous processing + foreach k $list { + set seed [uplevel #0 [list {*}$cmd $seed [dict get $o_data $k]]] + } + return $seed + } else { + #ASYNCHRONOUS enumeration + var this o_bgEnum + #!todo - make id unique + #!todo - facility to abort running enumeration. + set enumID enum[array size o_bgEnum] + + set seedvar [$this . bgEnum $enumID .] + set $seedvar $seed + + after 0 [list $this . _doBackgroundEnum $enumID $list $cmd $seedvar $opt(-completioncommand)] + return $enumID + } +} + +#!todo - make private? - put on a separate interface? +$COL .. PatternMethod _doBackgroundEnum {enumID slice cmd seedvar completioncommand} { + var this o_data + + + #Note that we don't post to the eventqueue using 'foreach s $slice' + # we only schedule another event after each item is processed + # - otherwise we would be spamming the eventqueue with items. + + #!todo? - accept a -granularity option to allow handling of n list-items per event? + + if {[llength $slice]} { + set slice [lassign $slice head] + + set script [string map [list %cmd% $cmd %seedvar% $seedvar %val% [dict get $o_data $head]] { + %cmd% [set %seedvar%] %val% + }] + + #post to eventqueue and re-enter _doBackgroundEnum + # + after idle [list after 0 [subst {set $seedvar \[uplevel #0 [list $script] \]; $this . _doBackgroundEnum $enumID [list $slice] [list $cmd] $seedvar [list $completioncommand]}]] + + } else { + #done. + + set script [string map [list %cmd% $completioncommand %seedvar% $seedvar] { + lindex [list [%cmd% [set %seedvar%]] [unset %seedvar%]] 0 + }] + + after idle [list after 0 [list uplevel #0 $script]] + } + + return +} + +$COL .. PatternMethod enumeratorstate {} { + var o_bgEnum + parray o_bgEnum +} + +#proc ::bgerror {args} { +# puts stderr "=bgerror===>$args" +#} + + +#map could be done in terms of the generic 'enumerate' method.. but it's slower. +# +#$PM map2 {proc} { +# var +# enumerate $_ID_ [list ::map-helper $proc] [list] +#} +#proc ::map-helper {proc accum item} { +# lappend accum [uplevel #0 [list {*}$proc $item]] +#} + +$COL .. PatternMethod map {cmd} { + var o_data + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list {*}$cmd $v]] + } + + return $seed +} +$COL .. PatternMethod objectmap {cmd} { + var o_data + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list $v {*}$cmd]] + } + + return $seed +} + + +#End core collection functionality. +#collection 'mixin' interfaces + +>pattern .. Create >keyvalprotector +>keyvalprotector .. PatternVariable o_protectedkeys +>keyvalprotector .. PatternVariable o_protectedvals + +#!todo - write test regarding errors in Constructors for mixins like this +# - an error (e.g from bad args) can cause errors with vars after it's re-run with correct args +>keyvalprotector .. Constructor {args} { + var this o_protectedkeys o_protectedvals + set this @this@ + #---------------------------------------------------------------------------- + set known_opts [list -keys -vals ] + dict set default -keys [list] + dict set default -vals [list] + if {([llength $args] % 2) != 0} { + error "(>keyvalprotector .. Constructor) ERROR: uneven options supplied - must be of form '-option value' " + } + foreach {k v} $args { + if {$k ni $known_opts} { + error "(>keyvalprotector .. Constructor) ERROR: option '$k' not in known options: '$known_opts'" + } + } + set opts [dict merge $default $args] + set o_protectedkeys [dict get $opts -keys] + set o_protectedvals [dict get $opts -vals] + #---------------------------------------------------------------------------- + set protections [concat $o_protectedkeys $o_protectedvals] + if {![llength $protections]} { + error "(>keyvalprotector .. Constructor) ERROR: must supply at least one argument to -vals or -keys" + } + +} +>keyvalprotector .. PatternMethod clear {} { + error "(>keyvalprotector . clear) ERROR: This collection is protected by a >keyvalprotector mixin. Cannot clear" +} +>keyvalprotector .. PatternMethod pop {{idx ""}} { + var o_data o_count o_protectedkeys o_protectedvals + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] end] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . pop) ERROR: Cannot pop object with index '$idx', key '$key' from collection." + } + set posn [lsearch -exact [dict keys $o_data] $key] + if {($posn >= 0) && ($posn < [dict size $o_data])} { + set result [dict get $o_data $key] + if {$result in $o_protectedvals} { + error "(>keyvalprotector . pop) ERROR: Cannot pop object '$result' with index '$idx', key '$key' from collection." + } + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } + +} +>keyvalprotector .. PatternMethod remove {idx {endRange ""}} { + var this o_data o_count o_alias o_protectedkeys o_protectedvals + + if {[string length $endRange]} { + 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] + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' key '$key' from collection" + } + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + if {$posn < 0} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx' in collection: $this" + } else { + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' from collection" + } + #try with next key in alias chain... + #return [remove $_ID_ $nextKey] + tailcall remove $_ID_ $nextKey + } + } + } + + dict unset o_data $key + + set o_count [dict size $o_data] + return +} + +#1) +#predicate methods (order preserving) +#usage: +# >collection .. Create >c1 +# >predicatedCollection .. Create >c1 ;#overlay predicate methods on existing collection + +#e.g >col1 . all {$val > 14} +#e.g >col1 . filterToCollection {$val > 19} . count +#e.g >col1 . filter {[string match "x*" $key]} +#!todo - fix. currying fails.. + +::>pattern .. Create >predicatedCollection +#process_pattern_aliases ::patternlib::>predicatedCollection + +set PM [>predicatedCollection .. PatternMethod .] + +>predicatedCollection .. PatternMethod filter {predicate} { + var this o_list o_array + set result [list] + + #!note (jmn 2004) how could we do smart filtering based on $posn? + #i.e it would make sense to lrange $o_list based on $posn... + #but what about complicated expressions where $posn is a set of ranges and/or combined with tests on $key & $val ?? + #Seems better to provide an alternative efficient means of generating subcolllections/ranges to perform predicate operations upon. + #given this, is $posn even useful? + + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $val + } + incr posn + } + set result +} +>predicatedCollection .. PatternMethod filterToKeys {predicate} { + var this o_list o_array + set result [list] + + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $key + } + incr posn + } + set result +} +>predicatedCollection .. PatternMethod filterToCollection {predicate {destCollection {}}} { + #!todo - collection not in subordinate namespace? -> if subordinate, should imply modification of sub's contents will be reflected in parent? + #!todo - implement as 'view' on current collection object.. extra o_list variables? + #!todo - review/document 'expected' key collision behaviour - source keys used as dest keys.. -autokey option required? + var this o_list o_array m_i_filteredCollection + + incr m_i_filteredCollection + if {![string length $destCollection]} { + #!todo? - implement 'one-shot' object (similar to RaTcl) + set result [::patternlib::>collection .. Create [$this .. Namespace]::>filteredCollection-$m_i_filteredCollection] + } else { + set result $destCollection + } + + #### + #externally manipulate new collection + #set ADD [$c . add .] + #foreach key $o_list { + # set val $o_array($key) + # if $predicate { + # $ADD $val $key + # } + #} + ### + + #internal manipulation faster + #set cID [lindex [set $result] 0] + set cID [lindex [$result --] 0] + + #use list to get keys so as to preserve order + set posn 0 + upvar #0 ::p::${cID}::o_array cARRAY ::p::${cID}::o_list cLIST + foreach key $o_list { + set val $o_array($key) + if $predicate { + if {[info exists cARRAY($key)]} { + error "key '$key' already exists in this collection" + } + lappend cLIST $key + set cARRAY($key) $val + } + incr posn + } + + return $result +} + +#NOTE! unbraced expr/if statements. We want to evaluate the predicate. +>predicatedCollection .. PatternMethod any {predicate} { + var this o_list o_array + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + return 1 + } + incr posn + } + return 0 +} +>predicatedCollection .. PatternMethod all {predicate} { + var this o_list o_array + set posn 0 + foreach key $o_list { + set val $o_array($key) + if !($predicate) { + return 0 + } + incr posn + } + return 1 +} +>predicatedCollection .. PatternMethod dropWhile {predicate} { + var this o_list o_array + set result [list] + set _idx 0 + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + incr _idx + } else { + break + } + incr posn + } + set remaining [lrange $o_list $_idx end] + foreach key $remaining { + set val $o_array($key) + lappend result $val + } + return $result +} +>predicatedCollection .. PatternMethod takeWhile {predicate} { + var this o_list o_array + set result [list] + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $val + } else { + break + } + incr posn + } + set result +} + + + +#end >collection mixins +###################################### + + + + +#----------------------------------------------------------- +#!TODO - methods for converting an arrayHandle to & from a hashMap efficiently? +# Why do we need both? apart from the size variable, what is the use of hashMap? +#----------------------------------------------------------- +#::pattern::create >hashMap +::>pattern .. Create >hashMap + +>hashMap .. PatternVariable o_size +>hashMap .. PatternVariable o_array + +>hashMap .. Constructor {args} { + var o_array o_size + array set o_array [list] + set o_size 0 +} +>hashMap .. PatternDefaultMethod "item" +>hashMap .. PatternMethod item {key} { + var o_array + set o_array($key) +} +>hashMap .. PatternMethod items {} { + var o_array + + set result [list] + foreach nm [array names o_array] { + lappend result $o_array($nm) + } + return $result +} +>hashMap .. PatternMethod pairs {} { + var o_array + + array get o_array +} +>hashMap .. PatternMethod add {val key} { + var o_array o_size + + set o_array($key) $val + incr o_size + return $key +} + +>hashMap .. PatternMethod del {key} { + var + puts stderr "warning: 'del' method of >hashMap deprecated. Use 'remove' instead." + remove $_ID_ $key +} +>hashMap .. PatternMethod remove {key} { + var o_array o_size + unset o_array($key) + incr o_size -1 + return $key +} +>hashMap .. PatternMethod count {} { + var o_size + #array size o_array + return $o_size +} +>hashMap .. PatternMethod count2 {} { + var o_array + #array size o_array ;#slow, at least for TCLv8.4.4 + #even array statistics is faster than array size ! + #e.g return [lindex [array statistics o_array] 0] + #but.. apparently there are circumstances where array statistics doesn't report the correct size. + return [array size o_array] +} +>hashMap .. PatternMethod names {} { + var o_array + array names o_array +} +>hashMap .. PatternMethod keys {} { + #synonym for names + var o_array + array names o_array +} +>hashMap .. PatternMethod hasKey {key} { + var o_array + return [info exists o_array($key)] +} +>hashMap .. PatternMethod clear {} { + var o_array o_size + unset o_array + set o_size 0 + return +} +#>hashMap .. Ready 1 + + + + + + + + + + + + + + + +#explicitly create metadata. Not required for user-defined patterns. +# this is only done here because this object is used for the metadata of all objects +# so the object must have all it's methods/props before its own metadata structure can be built. +#uplevel 1 "::pattern::object ::pattern::>_nullMeta createMetadata >collection" +#uplevel 1 "::patternlib::>collection .. CreateMetadata ::patternlib::>collection" + + + + +if 0 { + + +#----------------------------------------------------------- +#::pattern::create >arrayHandle { +# variable o_arrayName +# variable this +#} +::>pattern .. Create >arrayHandle + +>arrayHandle .. PatternVariable o_arrayName +>arrayHandle .. PatternVariable this + +>arrayHandle .. Constructor {args} { + var o_arrayName this + set this @this@ + + + set o_arrayName [$this .. Namespace]::array + + upvar #0 $o_arrayName $this + #? how to automatically update this after a namespace import? + + array set $o_arrayName [list] + +} +>arrayHandle .. PatternMethod array {} { + var o_arrayName + return $o_arrayName +} + +#------------------------------------------------------- +#---- some experiments +>arrayHandle .. PatternMethod up {varname} { + var o_arrayName + + #is it dodgy to hard-code the calling depth? + #will it be different for different object systems? + #Will it even be consistent for the same object. + # Is this method necessary anyway? - + # - users can always instead do: + # upvar #0 [>instance . array] var + + uplevel 3 [list upvar 0 $o_arrayName $varname] + + return +} +>arrayHandle .. PatternMethod global {varname} { + var o_arrayName + # upvar #0 [>instance . array] var + + if {![string match ::* $varname]} { + set varname ::$varname + } + + upvar #0 $o_arrayName $varname + + return +} +>arrayHandle .. PatternMethod depth {} { + var o_arrayName + # + for {set i 0} {$i < [info level]} { + puts "${i}: [uplevel $i [list namespace current] , [info level $i]]" + } + +} + # -------------------------------------------- + + +>arrayHandle .. PatternMethod item {key} { + var o_arrayName + set ${o_arrayName}($key) +} +>arrayHandle .. PatternMethod items {} { + var o_arrayName + + set result [list] + foreach nm [array names $o_arrayName] { + lappend result [set ${o_arrayName}($nm)] + } + return $result +} +>arrayHandle .. PatternMethod pairs {} { + var o_arrayName + + array get $o_arrayName +} +>arrayHandle .. PatternMethod add {val key} { + var o_arrayName + + set ${o_arrayName}($key) $val + return $key +} +>arrayHandle .. PatternMethod del {key} { + puts stderr "Warning: 'del' method of >arrayHandle deprecated. Use 'remove' instead." + remove $_ID_ $key +} +>arrayHandle .. PatternMethod remove {key} { + var o_arrayName + unset ${o_arrayName}($key) + return $key +} +>arrayHandle .. PatternMethod size {} { + var o_arrayName + return [array size $o_arrayName] +} +>arrayHandle .. PatternMethod count {} { + #alias for size + var o_arrayName + return [array size $o_arrayName] +} +>arrayHandle .. PatternMethod statistics {} { + var o_arrayName + return [array statistics $o_arrayName] +} +>arrayHandle .. PatternMethod names {} { + var o_arrayName + array names $o_arrayName +} +>arrayHandle .. PatternMethod keys {} { + #synonym for names + var o_arrayName + array names $o_arrayName +} +>arrayHandle .. PatternMethod hasKey {key} { + var o_arrayName + + return [info exists ${o_arrayName}($key)] +} +>arrayHandle .. PatternMethod clear {} { + var o_arrayName + unset $o_arrayName + array set $o_arrayName [list] + + return +} +#>arrayHandle .. Ready 1 + + + + +::>pattern .. Create >matrix + +>matrix .. PatternVariable o_array +>matrix .. PatternVariable o_size + +>matrix .. Constructor {args} { + var o_array o_size + + array set o_array [list] + set o_size 0 +} + + +#process_pattern_aliases ::patternlib::>matrix + +set PM [>matrix .. PatternMethod .] + +>matrix .. PatternMethod item {args} { + var o_array + + if {![llength $args]} { + error "indices required" + } else { + + } + if [info exists o_array($args)] { + return $o_array($args) + } else { + error "no such index: '$args'" + } +} +>matrix .. PatternMethod items {} { + var o_array + + set result [list] + foreach nm [array names o_array] { + lappend result $o_array($nm) + } + return $result +} +>matrix .. PatternMethod pairs {} { + var o_array + + array get o_array +} +>matrix .. PatternMethod slice {args} { + var o_array + + if {"*" ni $args} { + lappend args * + } + + array get o_array $args +} +>matrix .. PatternMethod add {val args} { + var o_array o_size + + if {![llength $args]} { + error "indices required" + } + + set o_array($args) $val + incr o_size + + #return [array size o_array] + return $o_size +} +>matrix .. PatternMethod names {} { + var o_array + array names o_array +} +>matrix .. PatternMethod keys {} { + #synonym for names + var o_array + array names o_array +} +>matrix .. PatternMethod hasKey {args} { + var o_array + + return [info exists o_array($args)] +} +>matrix .. PatternMethod clear {} { + var o_array o_size + unset o_array + set o_size 0 + return +} +>matrix .. PatternMethod count {} { + var o_size + return $o_size +} +>matrix .. PatternMethod count2 {} { + var o_array + #see comments for >hashMap count2 + return [array size o_array] +} +#>matrix .. Ready 1 + +#-------------------------------------------------------- +#tree data structure (based *loosely* on API at http://www.msen.com/%7Eclif/treeNobj.html - discussed in Clif Flynts book Tcl programming) +#!todo - compare API to http://tcllib.sourceforge.net/doc/tree.html +#!todo - create an >itree (inheritance tree) where node data is readable/writable on children unless overridden. +::>pattern .. Create >tree + +set _NODE [::>pattern .. Create [>tree .. Namespace]::>node] +set _TREE_NODE $_NODE +#process_pattern_aliases $_TREE_NODE + +$_NODE .. PatternVariable o_treens ;#tree namespace +$_NODE .. PatternVariable o_idref +$_NODE .. PatternVariable o_nodePrototype + +#$_NODE .. PatternProperty data +$_NODE .. PatternProperty info + +$_NODE .. PatternProperty tree +$_NODE .. PatternProperty parent +$_NODE .. PatternProperty children +$_NODE .. PatternMethod addNode {} { + set nd_id [incr $o_idref] + set nd [$o_nodePrototype .. Create ${o_treens}::>n-$nd_id -tree $o_tree -parent @this@] + @this@ . add $nd n-$nd_id + + return n-$nd_id +} +#flat list of all nodes below this +#!todo - something else? ad-hoc collections? +#!todo - non-recursive version? tail-call opt? +$_NODE .. PatternMethod nodes {} { + set result [list] + + #use(abuse?) our knowledge of >collection internals + foreach n $o_list { + #eval lappend result $n [$o_array($n) . nodes] + #!todo - test + lappend result $n {*}[$o_array($n) . nodes] + } + return $result +} +#count of number of descendants +#!todo - non-recursive version? tail-call opt? +$_NODE .. PatternMethod size {} { + set result 0 + #use(abuse?) our knowledge of >collection internals + foreach n $o_list { + incr result [expr {1 + [$o_array($n) . size]}] + } + return $result +} +$_NODE .. PatternMethod isLeaf {} { + #!todo - way to stop unused vars being uplevelled? + var o_tree + + #tailcall isEmpty $_ID_ ;#fails. because isEmpty is from >collection interface - so different ns? + tailcall [@this@ . isEmpty .] +} +$_NODE .. Constructor {args} { + array set A $args + + set o_tree $A(-tree) + set o_parent $A(-parent) + + #array set o_data [list] + array set o_info [list] + + set o_nodePrototype [::patternlib::>tree .. Namespace]::>node + set o_idref [$o_tree . nodeID .] + set o_treens [$o_tree .. Namespace] + #set o_children [::patternlib::>collection .. Create [@this@ .. Namespace]::>children] + + #overlay children collection directly on the node + set o_children [::patternlib::>collection .. Create @this@] + + return +} + +>tree .. PatternProperty test blah +>tree .. PatternProperty nodeID 0 ;#public only so node can access.. need 'friend' concept? +>tree .. PatternVariable o_ns +>tree .. Constructor {args} { + set o_ns [@this@ .. Namespace] + + #>tree is itself also a node (root node) + #overlay new 'root' node onto existing tree, pass tree to constructor + [::patternlib::>tree .. Namespace]::>node .. Create @this@ -tree @this@ -parent "" +} + + + + +unset _NODE + + + + +#-------------------------------------------------------- +#a basic binary search tree experiment +# - todo - 'scheme' property to change behaviour? e.g balanced tree +::>pattern .. Create >bst +#process_pattern_aliases ::patternlib::>bst +>bst .. PatternVariable o_NS ;#namespace +>bst .. PatternVariable o_this ;#namespace +>bst .. PatternVariable o_nodeID + +>bst .. PatternProperty root "" +>bst .. Constructor {args} { + set o_this @this@ + set o_NS [$o_this .. Namespace] + namespace eval ${o_NS}::nodes {} + puts stdout ">bst constructor" + set o_nodeID 0 +} +>bst .. PatternMethod insert {key args} { + set newnode [::patternlib::>bstnode .. Create ${o_NS}::nodes::>n-[incr o_nodeID]] + set [$newnode . key .] $key + if {[llength $args]} { + set [$newnode . value .] $args + } + if {![string length $o_root]} { + set o_root $newnode + set [$newnode . parent .] $o_this + } else { + set ipoint {} ;#insertion point + set tpoint $o_root ;#test point + set side {} + while {[string length $tpoint]} { + set ipoint $tpoint + if {[$newnode . key] < [$tpoint . key]} { + set tpoint [$tpoint . left] + set side left + } else { + set tpoint [$tpoint . right] + set side right + } + } + set [$newnode . parent .] $ipoint + set [$ipoint . $side .] $newnode + } + return $newnode +} +>bst .. PatternMethod item {key} { + if {![string length $o_root]} { + error "item $key not found" + } else { + set tpoint $o_root + while {[string length $tpoint]} { + if {[$tpoint . key] eq $key} { + return $tpoint + } else { + if {$key < [$tpoint . key]} { + set tpoint [$tpoint . left] + } else { + set tpoint [$tpoint . right] + } + } + } + error "item $key not found" + } +} +>bst .. PatternMethod inorder-walk {} { + if {[string length $o_root]} { + $o_root . inorder-walk + } + puts {} +} +>bst .. PatternMethod view {} { + array set result [list] + + if {[string length $o_root]} { + array set result [$o_root . view 0 [list]] + } + + foreach depth [lsort [array names result]] { + puts "$depth: $result($depth)" + } + +} +::>pattern .. Create >bstnode +#process_pattern_aliases ::patternlib::>bstnode +>bstnode .. PatternProperty parent +>bstnode .. PatternProperty left "" +>bstnode .. PatternProperty right "" +>bstnode .. PatternProperty key +>bstnode .. PatternProperty value + +>bstnode .. PatternMethod inorder-walk {} { + if {[string length $o_left]} { + $o_left . inorder-walk + } + + puts -nonewline "$o_key " + + if {[string length $o_right]} { + $o_right . inorder-walk + } + + return +} +>bstnode .. PatternMethod view {depth state} { + #!todo - show more useful representation of structure + set lower [incr depth] + + if {[string length $o_left]} { + set state [$o_left . view $lower $state] + } + + if {[string length $o_right]} { + set state [$o_right . view $lower $state] + } + + + array set s $state + lappend s($depth) $o_key + + return [array get s] +} + + +#-------------------------------------------------------- +#::pattern::create ::pattern::>metaObject +#::pattern::>metaObject PatternProperty methods +#::pattern::>metaObject PatternProperty properties +#::pattern::>metaObject PatternProperty PatternMethods +#::pattern::>metaObject PatternProperty patternProperties +#::pattern::>metaObject Constructor args { +# set this @this@ +# +# set [$this . methods .] [::>collection create [$this namespace]::methods] +# set [$this . properties .] [::>collection create [$this namespace]::properties] +# set [$this . PatternMethods .] [::>collection create [$this namespace]::PatternMethods] +# set [$this . patternProperties .] [::>collection create [$this namespace]::patternProperties] +# +#} + + + + #tidy up + unset PV + unset PM + + + +#-------------------------------------------------------- +::>pattern .. Create >enum +#process_pattern_aliases ::patternlib::>enum +>enum .. PatternMethod item {{idx 0}} { + var o_array o_list + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + if {[catch {set o_array([lindex $o_list $idx])} result]} { + error "no such index : '$idx'" + } else { + return $result + } + } else { + if {[catch {set o_array($idx)} result]} { + error "no such index: '$idx'" + } else { + return $result + } + } +} + + + +#proc makeenum {type identifiers} { +# #!!todo - make generated procs import into whatever current system context? +# +# upvar #0 wbpbenum_${type}_number a1 wbpbenum_number_${type} a2 +# +# #obliterate any previous enum for this type +# catch {unset a1} +# catch {unset a2} +# +# set n 0 +# foreach id $identifiers { +# set a1($id) $n +# set a2($n) $id +# incr n +# } +# proc ::${type}_to_number key [string map [list @type@ $type] { +# upvar #0 wbpbenum_@type@_number ary +# if {[catch {set ary($key)} num]} { +# return -code error "unknown @type@ '$key'" +# } +# return $num +# }] +# +# proc ::number_to_${type} {number} [string map [list @type@ $type] { +# upvar #0 wbpbenum_number_@type@ ary +# if {[catch {set ary($number)} @type@]} { +# return -code error "no @type@ for '$number'" +# } +# return $@type@ +# }] +# +# #eval "namespace eval ::sysnexus {namespace export number_to_${type}; namespace export ${type}_to_number}" +# #eval "namespace eval :: {namespace import -force sysnexus::number_to_${type} sysnexus::${type}_to_number}" +#} +# +#-------------------------------------------------------- +::>pattern .. Create >nest +>nest .. PatternVariable THIS +>nest .. PatternProperty data -autoclone +>nest .. Constructor {args} { + var o_data + var THIS + set THIS @this@ + array set o_data [list] +} +>nest .. PatternMethod item {args} { + set THIS @this@ + return [$THIS . data [join $args ,]] +} + +# +# e.g +# set [>nest a , b . data c .] blah +# >nest a , b , c +# +# set [>nest w x , y . data z .] etc +# >nest w x , y , z +#-------------------------------------------------------- + +} + +} + + +#package require patternlibtemp diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm new file mode 100644 index 00000000..457d5742 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm @@ -0,0 +1,754 @@ +package provide patternpredator2 1.2.4 + +proc ::p::internals::jaws {OID _ID_ args} { + #puts stderr ">>>(patternpredator2 lib)jaws called with _ID_:$_ID_ args: $args" + #set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + yield + set w 1 + + set stack [list] + set wordcount [llength $args] + set terminals [list . .. , # @ !] ;#tokens which require the current stack to be evaluated first + set unsupported 0 + set operator "" + set operator_prev "" ;#used only by argprotect to revert to previous operator + + + if {$OID ne "null"} { + #!DO NOT use upvar here for MAP! (calling set on a MAP in another iteration/call will overwrite a map for another object!) + #upvar #0 ::p::${OID}::_meta::map MAP + set MAP [set ::p::${OID}::_meta::map] + } else { + # error "jaws - OID = 'null' ???" + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] ;#MAP taken from _ID_ will be missing 'interfaces' key + } + set invocantdata [dict get $MAP invocantdata] + lassign $invocantdata OID alias default_method object_command wrapped + + set finished_args 0 ;#whether we've completely processed all args in the while loop and therefor don't need to peform the final word processing code + + #don't use 'foreach word $args' - we sometimes need to backtrack a little by manipulating $w + while {$w < $wordcount} { + set word [lindex $args [expr {$w -1}]] + #puts stdout "w:$w word:$word stack:$stack" + + if {$operator eq "argprotect"} { + set operator $operator_prev + lappend stack $word + incr w + } else { + if {[llength $stack]} { + if {$word in $terminals} { + set reduction [list 0 $_ID_ {*}$stack ] + #puts stderr ">>>jaws yielding value: $reduction triggered by word $word in position:$w" + + + set _ID_ [yield $reduction] + set stack [list] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#Do not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces [list level0 {} level1 {}]] + #puts stderr "WARNING REVIEW: jaws-branch - leave empty??????" + } + + #review - 2018. switched to _ID_ instead of MAP + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command + #lassign [dict get $MAP invocantdata] OID alias default_method object_command + + + #puts stdout "---->>> yielded _ID_: $_ID_ OID:$OID alias:$alias default_method:$default_method object_command:$object_command" + set operator $word + #don't incr w + #incr w + } else { + if {$operator eq "argprotect"} { + set operator $operator_prev + set operator_prev "" + lappend stack $word + } else { + #only look for leading argprotect chacter (-) if we're not already in argprotect mode + if {$word eq "--"} { + set operator_prev $operator + set operator "argprotect" + #Don't add the plain argprotector to the stack + } elseif {[string match "-*" $word]} { + #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument) + set operator_prev $operator + set operator "argprotect" + lappend stack $word + } else { + lappend stack $word + } + } + + + incr w + } + } else { + #no stack + switch -- $word {.} { + + if {$OID ne "null"} { + #we know next word is a property or method of a pattern object + incr w + set nextword [lindex $args [expr {$w - 1}]] + set command ::p::${OID}::$nextword + set stack [list $command] ;#2018 j + set operator . + if {$w eq $wordcount} { + set finished_args 1 + } + } else { + # don't incr w + #set nextword [lindex $args [expr {$w - 1}]] + set command $object_command ;#taken from the MAP + set stack [list "_exec_" $command] + set operator . + } + + + } {..} { + incr w + set nextword [lindex $args [expr {$w -1}]] + set command ::p::-1::$nextword + #lappend stack $command ;#lappend a small number of items to an empty list is slower than just setting the list. + set stack [list $command] ;#faster, and intent is clearer than lappend. + set operator .. + if {$w eq $wordcount} { + set finished_args 1 + } + } {,} { + #puts stdout "Stackless comma!" + + + if {$OID ne "null"} { + set command ::p::${OID}::$default_method + } else { + set command [list $default_method $object_command] + #object_command in this instance presumably be a list and $default_method a list operation + #e.g "lindex {A B C}" + } + #lappend stack $command + set stack [list $command] + set operator , + } {--} { + set operator_prev $operator + set operator argprotect + #no stack - + } {!} { + set command $object_command + set stack [list "_exec_" $object_command] + #puts stdout "!!!! !!!! $stack" + set operator ! + } default { + if {$operator eq ""} { + if {$OID ne "null"} { + set command ::p::${OID}::$default_method + } else { + set command [list $default_method $object_command] + } + set stack [list $command] + set operator , + lappend stack $word + } else { + #no stack - so we don't expect to be in argprotect mode already. + if {[string match "-*" $word]} { + #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument) + set operator_prev $operator + set operator "argprotect" + lappend stack $word + } else { + lappend stack $word + } + + } + } + incr w + } + + } + } ;#end while + + #process final word outside of loop + #assert $w == $wordcount + #trailing operators or last argument + if {!$finished_args} { + set word [lindex $args [expr {$w -1}]] + if {$operator eq "argprotect"} { + set operator $operator_prev + set operator_prev "" + + lappend stack $word + incr w + } else { + + + switch -- $word {.} { + if {![llength $stack]} { + #set stack [list "_result_" [::p::internals::ref_to_object $_ID_]] + yieldto return [::p::internals::ref_to_object $_ID_] + error "assert: never gets here" + + } else { + #puts stdout "==== $stack" + #assert - whenever _ID_ changed in this proc - we have updated the $OID variable + yieldto return [::p::internals::ref_to_stack $OID $_ID_ $stack] + error "assert: never gets here" + } + set operator . + + } {..} { + #trailing .. after chained call e.g >x . item 0 .. + #puts stdout "$$$$$$$$$$$$ [list 0 $_ID_ {*}$stack] $$$$" + #set reduction [list 0 $_ID_ {*}$stack] + yieldto return [yield [list 0 $_ID_ {*}$stack]] + } {#} { + set unsupported 1 + } {,} { + set unsupported 1 + } {&} { + set unsupported 1 + } {@} { + set unsupported 1 + } {--} { + + #set reduction [list 0 $_ID_ {*}$stack[set stack [list]]] + #puts stdout " -> -> -> about to call yield $reduction <- <- <-" + set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]] ] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}} ] + } + yieldto return $MAP + } {!} { + #error "untested branch" + set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]]] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] + } + lassign [dict get $MAP invocantdata] OID alias default_command object_command + set command $object_command + set stack [list "_exec_" $command] + set operator ! + } default { + if {$operator eq ""} { + #error "untested branch" + lassign [dict get $MAP invocantdata] OID alias default_command object_command + #set command ::p::${OID}::item + set command ::p::${OID}::$default_command + lappend stack $command + set operator , + + } + #do not look for argprotect items here (e.g -option) as the final word can't be an argprotector anyway. + lappend stack $word + } + if {$unsupported} { + set unsupported 0 + error "trailing '$word' not supported" + + } + + #if {$operator eq ","} { + # incr wordcount 2 + # set stack [linsert $stack end-1 . item] + #} + incr w + } + } + + + #final = 1 + #puts stderr ">>>jaws final return value: [list 1 $_ID_ {*}$stack]" + + return [list 1 $_ID_ {*}$stack] +} + + + +#trailing. directly after object +proc ::p::internals::ref_to_object {_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 object_command + set refname ::p::${OID}::_ref::__OBJECT + + array set $refname [list] ;#important to initialise the variable as an array here - or initial read attempts on elements will not fire traces + + set traceCmd [list ::p::predator::object_read_trace $OID $_ID_] + if {[list {read} $traceCmd] ni [trace info variable $refname]} { + #puts stdout "adding read trace on variable '$refname' - traceCmd:'$traceCmd'" + trace add variable $refname {read} $traceCmd + } + set traceCmd [list ::p::predator::object_array_trace $OID $_ID_] + if {[list {array} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {array} $traceCmd + } + + set traceCmd [list ::p::predator::object_write_trace $OID $_ID_] + if {[list {write} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {write} $traceCmd + } + + set traceCmd [list ::p::predator::object_unset_trace $OID $_ID_] + if {[list {unset} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {unset} $traceCmd + } + return $refname +} + + +proc ::p::internals::create_or_update_reference {OID _ID_ refname command} { + #if {[lindex $fullstack 0] eq "_exec_"} { + # #strip it. This instruction isn't relevant for a reference. + # set commandstack [lrange $fullstack 1 end] + #} else { + # set commandstack $fullstack + #} + #set argstack [lassign $commandstack command] + #set field [string map {> __OBJECT_} [namespace tail $command]] + + + + set reftail [namespace tail $refname] + set argstack [lassign [split $reftail +] field] + set field [string map {> __OBJECT_} [namespace tail $command]] + + #puts stderr "refname:'$refname' command: $command field:$field" + + + if {$OID ne "null"} { + upvar #0 ::p::${OID}::_meta::map MAP + } else { + #set map [dict get [lindex [dict get $_ID_ i this] 0 1] map] + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}}] + } + lassign [dict get $MAP invocantdata] OID alias default_method object_command + + + + if {$OID ne "null"} { + interp alias {} $refname {} $command $_ID_ {*}$argstack + } else { + interp alias {} $refname {} $command {*}$argstack + } + + + #set iflist [lindex $map 1 0] + set iflist [dict get $MAP interfaces level0] + #set iflist [dict get $MAP interfaces level0] + set field_is_property_like 0 + foreach IFID [lreverse $iflist] { + #tcl (braced) expr has lazy evaluation for &&, || & ?: operators - so this should be reasonably efficient. + if {[llength [info commands ::p::${IFID}::_iface::(GET)$field]] || [llength [info commands ::p::${IFID}::_iface::(SET)$field]]} { + set field_is_property_like 1 + #There is a setter or getter (but not necessarily an entry in the o_properties dict) + break + } + } + + + + + #whether field is a property or a method - remove any commandrefMisuse_TraceHandler + foreach tinfo [trace info variable $refname] { + #puts "-->removing traces on $refname: $tinfo" + if {[lindex $tinfo 1 0] eq "::p::internals::commandrefMisuse_TraceHandler"} { + trace remove variable $refname {*}$tinfo + } + } + + if {$field_is_property_like} { + #property reference + + + set this_invocantdata [lindex [dict get $_ID_ i this] 0] + lassign $this_invocantdata OID _alias _defaultmethod object_command + #get fully qualified varspace + + # + set propdict [$object_command .. GetPropertyInfo $field] + if {[dict exist $propdict $field]} { + set field_is_a_property 1 + set propinfo [dict get $propdict $field] + set varspace [dict get $propinfo varspace] + if {$varspace eq ""} { + set full_varspace ::p::${OID} + } else { + if {[::string match "::*" $varspace]} { + set full_varspace $varspace + } else { + set full_varspace ::p::${OID}::$varspace + } + } + } else { + set field_is_a_property 0 + #no propertyinfo - this field was probably established as a PropertyRead and/or PropertyWrite without a Property + #this is ok - and we still set the trace infrastructure below (app may convert it to a normal Property later) + set full_varspace ::p::${OID} + } + + + + + + #We only trace on entire property.. not array elements (if references existed to both the array and an element both traces would be fired -(entire array trace first)) + set Hndlr [::list ::p::predator::propvar_write_TraceHandler $OID $field] + if { [::list {write} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} { + trace add variable ${full_varspace}::o_${field} {write} $Hndlr + } + set Hndlr [::list ::p::predator::propvar_unset_TraceHandler $OID $field] + if { [::list {unset} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} { + trace add variable ${full_varspace}::o_${field} {unset} $Hndlr + } + + + #supply all data in easy-access form so that propref_trace_read is not doing any extra work. + set get_cmd ::p::${OID}::(GET)$field + set traceCmd [list ::p::predator::propref_trace_read $get_cmd $_ID_ $refname $field $argstack] + + if {[list {read} $traceCmd] ni [trace info variable $refname]} { + set fieldvarname ${full_varspace}::o_${field} + + + #synch the refvar with the real var if it exists + #catch {set $refname [$refname]} + if {[array exists $fieldvarname]} { + if {![llength $argstack]} { + #unindexed reference + array set $refname [array get $fieldvarname] + #upvar $fieldvarname $refname + } else { + set s0 [lindex $argstack 0] + #refs to nonexistant array members common? (catch vs 'info exists') + if {[info exists ${fieldvarname}($s0)]} { + set $refname [set ${fieldvarname}($s0)] + } + } + } else { + #refs to uninitialised props actually should be *very* common. + #If we use 'catch', it means retrieving refs to non-initialised props is slower. Fired catches can be relatively expensive. + #Because it's common to get a ref to uninitialised props (e.g for initial setting of their value) - we will use 'info exists' instead of catch. + + #set errorInfo_prev $::errorInfo ;#preserve errorInfo across catches! + + #puts stdout " ---->>!!! ref to uninitialised prop $field $argstack !!!<------" + + + if {![llength $argstack]} { + #catch {set $refname [set ::p::${OID}::o_$field]} + if {[info exists $fieldvarname]} { + set $refname [set $fieldvarname] + #upvar $fieldvarname $refname + } + } else { + if {[llength $argstack] == 1} { + #catch {set $refname [lindex [set ::p::${OID}::o_$field] [lindex $argstack 0]]} + if {[info exists $fieldvarname]} { + set $refname [lindex [set $fieldvarname] [lindex $argstack 0]] + } + + } else { + #catch {set $refname [lindex [set ::p::${OID}::o_$field] $argstack]} + if {[info exists $fieldvarname]} { + set $refname [lindex [set $fieldvarname] $argstack] + } + } + } + + #! what if someone has put a trace on ::errorInfo?? + #set ::errorInfo $errorInfo_prev + } + trace add variable $refname {read} $traceCmd + + set traceCmd [list ::p::predator::propref_trace_write $_ID_ $OID $full_varspace $refname] + trace add variable $refname {write} $traceCmd + + set traceCmd [list ::p::predator::propref_trace_unset $_ID_ $OID $refname] + trace add variable $refname {unset} $traceCmd + + + set traceCmd [list ::p::predator::propref_trace_array $_ID_ $OID $refname] + # puts "**************** installing array variable trace on ref:$refname - cmd:$traceCmd" + trace add variable $refname {array} $traceCmd + } + + } else { + #puts "$refname ====> adding refMisuse_traceHandler $alias $field" + #matching variable in order to detect attempted use as property and throw error + + #2018 + #Note that we are adding a trace on a variable (the refname) which does not exist. + #this is fine - except that the trace won't fire for attempt to write it as an array using syntax such as set $ref(someindex) + #we could set the ref to an empty array - but then we have to also undo this if a property with matching name is added + ##array set $refname {} ;#empty array + # - the empty array would mean a slightly better error message when misusing a command ref as an array + #but this seems like a code complication for little benefit + #review + + trace add variable $refname {read write unset array} [list ::p::internals::commandrefMisuse_TraceHandler $OID $field] + } +} + + + +#trailing. after command/property +proc ::p::internals::ref_to_stack {OID _ID_ fullstack} { + if {[lindex $fullstack 0] eq "_exec_"} { + #strip it. This instruction isn't relevant for a reference. + set commandstack [lrange $fullstack 1 end] + } else { + set commandstack $fullstack + } + set argstack [lassign $commandstack command] + set field [string map {> __OBJECT_} [namespace tail $command]] + + + #!todo? + # - make every object's OID unpredictable and sparse (UUID) and modify 'namespace child' etc to prevent iteration/inspection of ::p namespace. + # - this would only make sense for an environment where any meta methods taking a code body (e.g .. Method .. PatternMethod etc) are restricted. + + + #references created under ::p::${OID}::_ref are effectively inside a 'varspace' within the object itself. + # - this would in theory allow a set of interface functions on the object which have direct access to the reference variables. + + + set refname ::p::${OID}::_ref::[join [concat $field $argstack] +] + + if {[llength [info commands $refname]]} { + #todo - review - what if the field changed to/from a property/method? + #probably should fix that where such a change is made and leave this short circuit here to give reasonable performance for existing refs + return $refname + } + ::p::internals::create_or_update_reference $OID $_ID_ $refname $command + return $refname +} + + +namespace eval pp { + variable operators [list .. . -- - & @ # , !] + variable operators_notin_args "" + foreach op $operators { + append operators_notin_args "({$op} ni \$args) && " + } + set operators_notin_args [string trimright $operators_notin_args " &"] ;#trim trailing spaces and ampersands + #set operators_notin_args {({.} ni $args) && ({,} ni $args) && ({..} ni $args)} +} +interp alias {} strmap {} string map ;#stop code editor from mono-colouring our big string mapped code blocks! + + + + + +# 2017 ::p::predator2 is the development version - intended for eventual use as the main dispatch mechanism. +#each map is a 2 element list of lists. +# form: {$commandinfo $interfaceinfo} +# commandinfo is of the form: {ID Namespace defaultmethod commandname _?} + +#2018 +#each map is a dict. +#form: {invocantdata {ID Namespace defaultmethod commandname _?} interfaces {level0 {} level1 {}}} + + +#OID = Object ID (integer for now - could in future be a uuid) +proc ::p::predator2 {_ID_ args} { + #puts stderr "predator2: _ID_:'$_ID_' args:'$args'" + #set invocants [dict get $_ID_ i] + #set invocant_roles [dict keys $invocants] + + #For now - we are 'this'-centric (single dispatch). todo - adapt for multiple roles, multimethods etc. + #set this_role_members [dict get $invocants this] + #set this_invocant [lindex [dict get $_ID_ i this] 0] ;#for the role 'this' we assume only one invocant in the list. + #lassign $this_invocant this_OID this_info_dict + + set this_OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + + set cheat 1 ;# + #------- + #Optimise the next most common use case. A single . followed by args which contain no other operators (non-chained call) + #(it should be functionally equivalent to remove this shortcut block) + if {$cheat} { + if { ([lindex $args 0] eq {.}) && ([llength $args] > 1) && ([llength [lsearch -all -inline $args .]] == 1) && ({,} ni $args) && ({..} ni $args) && ({--} ni $args) && ({!} ni $args)} { + + set remaining_args [lassign $args dot method_or_prop] + + #how will we do multiple apis? (separate interface stacks) apply? apply [list [list _ID_ {*}$arglist] ::p::${stackid?}::$method_or_prop ::p::${this_OID}] ??? + set command ::p::${this_OID}::$method_or_prop + #REVIEW! + #e.g what if the method is named "say hello" ?? (hint - it will break because we will look for 'say') + #if {[llength $command] > 1} { + # error "methods with spaces not included in test suites - todo fix!" + #} + #Dont use {*}$command - (so we can support methods with spaces) + #if {![llength [info commands $command]]} {} + if {[namespace which $command] eq ""} { + if {[namespace which ::p::${this_OID}::(UNKNOWN)] ne ""} { + #lset command 0 ::p::${this_OID}::(UNKNOWN) ;#seems wrong - command could have spaces + set command ::p::${this_OID}::(UNKNOWN) + #tailcall {*}$command $_ID_ $cmdname {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + tailcall $command $_ID_ $method_or_prop {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "(::p::predator2) error running command:'$command' argstack:'[lrange $args 2 end]'\n - command not found and no 'unknown' handler" "method '$method_or_prop' not found" + } + } else { + #tailcall {*}$command $_ID_ {*}$remaining_args + tailcall $command $_ID_ {*}$remaining_args + } + } + } + #------------ + + + if {([llength $args] == 1) && ([lindex $args 0] eq "..")} { + return $_ID_ + } + + + #puts stderr "pattern::predator (test version) called with: _ID_:$_ID_ args:$args" + + + + #puts stderr "this_info_dict: $this_info_dict" + + + + + if {![llength $args]} { + #should return some sort of public info.. i.e probably not the ID which is an implementation detail + #return cmd + return [lindex [dict get [set ::p::${this_OID}::_meta::map] invocantdata] 0] ;#Object ID + + #return a dict keyed on object command name - (suitable as use for a .. Create 'target') + #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method object_command wrapped + #return [list $object_command [list -id $this_OID ]] + } elseif {[llength $args] == 1} { + #short-circuit the single index case for speed. + if {[lindex $args 0] ni {.. . -- - & @ # , !}} { + #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method + lassign [lindex [dict get $_ID_ i this] 0] this_OID alias default_method + + tailcall ::p::${this_OID}::$default_method $_ID_ [lindex $args 0] + } elseif {[lindex $args 0] eq {--}} { + + #!todo - we could hide the invocant by only allowing this call from certain uplevel procs.. + # - combined with using UUIDs for $OID, and a secured/removed metaface on the object + # - (and also hiding of [interp aliases] command so they can't iterate and examine all aliases) + # - this could effectively hide the object's namespaces,vars etc from the caller (?) + return [set ::p::${this_OID}::_meta::map] + } + } + + + + #upvar ::p::coroutine_instance c ;#coroutine names must be unique per call to predator (not just per object - or we could get a clash during some cyclic calls) + #incr c + #set reduce ::p::reducer${this_OID}_$c + set reduce ::p::reducer${this_OID}_[incr ::p::coroutine_instance] + #puts stderr "..................creating reducer $reduce with args $this_OID _ID_ $args" + coroutine $reduce ::p::internals::jaws $this_OID $_ID_ {*}$args + + + set current_ID_ $_ID_ + + set final 0 + set result "" + while {$final == 0} { + #the argument given here to $reduce will be returned by 'yield' within the coroutine context (jaws) + set reduction_args [lassign [$reduce $current_ID_[set current_ID_ [list]] ] final current_ID_ command] + #puts stderr "..> final:$final current_ID_:'$current_ID_' command:'$command' reduction_args:'$reduction_args'" + #if {[string match *Destroy $command]} { + # puts stdout " calling Destroy reduction_args:'$reduction_args'" + #} + if {$final == 1} { + + if {[llength $command] == 1} { + if {$command eq "_exec_"} { + tailcall {*}$reduction_args + } + if {[llength [info commands $command]]} { + tailcall {*}$command $current_ID_ {*}$reduction_args + } + set cmdname [namespace tail $command] + set this_OID [lindex [dict get $current_ID_ i this] 0 0] + if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} { + lset command 0 ::p::${this_OID}::(UNKNOWN) + tailcall {*}$command $current_ID_ $cmdname {*}$reduction_args ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "1)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found" + } + + } else { + #e.g lindex {a b c} + tailcall {*}$command {*}$reduction_args + } + + + } else { + if {[lindex $command 0] eq "_exec_"} { + set result [uplevel 1 [list {*}[lrange $command 1 end] {*}$reduction_args]] + + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {} ] + } else { + if {[llength $command] == 1} { + if {![llength [info commands $command]]} { + set cmdname [namespace tail $command] + set this_OID [lindex [dict get $current_ID_ i this] 0 0] + if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} { + + lset command 0 ::p::${this_OID}::(UNKNOWN) + set result [uplevel 1 [list {*}$command $current_ID_ $cmdname {*}$reduction_args]] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "2)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found" + } + } else { + #set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]] + set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]] + + } + } else { + set result [uplevel 1 [list {*}$command {*}$reduction_args]] + } + + if {[llength [info commands $result]]} { + if {([llength $result] == 1) && ([string first ">" [namespace tail $result]] == 0)} { + #looks like a pattern command + set current_ID_ [$result .. INVOCANTDATA] + + + #todo - determine if plain .. INVOCANTDATA is sufficient instead of .. UPDATEDINVOCANTDATA + #if {![catch {$result .. INVOCANTDATA} result_invocantdata]} { + # set current_ID_ $result_invocantdata + #} else { + # return -code error -errorinfo "3)error running command:'$command' argstack:'$reduction_args'\n - Failed to access result:'$result' as a pattern object." "Failed to access result:'$result' as a pattern object" + #} + } else { + #non-pattern command + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}] + } + } else { + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}] + #!todo - allow further operations on non-command values. e.g dicts, lists & strings (treat strings as lists) + + } + } + + } + } + error "Assert: Shouldn't get here (end of ::p::predator2)" + #return $result +} diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm new file mode 100644 index 00000000..2d6e61da --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm @@ -0,0 +1,7806 @@ +#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 +} + + + +#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 withe punk repl - todo - tone it down. +#namespace eval ::punk::repl::codethread { +# variable running 0 +#} +package require punk::lib +package require punk::ansi +#require aliascore after punk::lib & punk::ansi are loaded +package require punk::aliascore ;#mostly punk::lib aliases +punk::aliascore::init + +package require punk::repl::codethread +package require punk::config +#package require textblock +if {![llength [info commands ::ansistring]]} { + namespace import punk::ansi::ansistring +} +package require punk::console +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 +if {[catch { + package require punk::packagepreference +} errM]} { + puts stderr "Failed to load punk::packagepreference" +} +punk::packagepreference::install + +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]} { + 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]} { + 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 objclone {obj} { + append obj2 $obj {} + } + interp alias "" strlen "" ::punk::strlen + interp alias "" str_len "" ::punk::strlen + interp alias "" objclone "" ::punk::objclone + #proc ::strlen {str} { + # string length [append str2 $str {}] + #} + #proc ::objclone {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} + + 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 + } + + #review - there are various type of uuid - we should use something consistent across platforms + #twapi is used on windows because it's about 5 times faster - but is this more important than consistency? + #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway + #(counterpoint: in the case of punk - we currently need twapi anyway on windows) + #does tcllib's uuid use the same mechanisms on different platforms anyway? + proc ::punk::uuid {} { + set has_twapi 0 + if 0 { + if {"windows" eq $::tcl_platform(platform)} { + if {![catch { + set loader [zzzload::pkg_wait twapi] + } errM]} { + if {$loader in [list failed loading]} { + puts stderr "Unexpected problem during thread-load of pkg twapi - zzload::pkg_wait returned $loader" + } + } else { + package require twapi + } + if {[package provide twapi] ne ""} { + set has_twapi 1 + } + } + } + if {!$has_twapi} { + if {[catch {package require uuid} errM]} { + error "Unable to load a package for uuid on this platform. Try installing tcllib's uuid (any platform) - or twapi for windows" + } + return [uuid::uuid generate] + } else { + return [twapi::new_uuid] + } + } + + #get last command result that was run through the repl + proc ::punk::get_runchunk {args} { + set argd [punk::args::get_dict { + *opts + -1 -optional 1 -type none + -2 -optional 1 -type none + *values -min 0 -max 0 + } $args] + #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 + } + #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + # + #we can't provide a float comparison suitable for every situation, + #but we pick something reasonable, keep it stable, and document it. + proc float_almost_equal {a b} { + package require math::constants + set diff [expr {abs($a - $b)}] + if {$diff <= $math::constants::eps} { + return 1 + } + set A [expr {abs($a)}] + set B [expr {abs($b)}] + set largest [expr {($B > $A) ? $B : $A}] + return [expr {$diff <= $largest * $math::constants::eps}] + } + + #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. + proc boolean_equal {a b} { + #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. + expr {($a && 1) == ($b && 1)} + } + #debatable whether boolean_almost_equal is likely to be surprising or helpful. + #values from a calculation that are extremely close to zero but aren't false could also be surprising - especially if they compare equal numerically + #perhaps a fuzzy-boolean is a step too far for a default - but it's inline with float-comparison for pattern-matching. use an even more complex classifier? (^&~) ? + proc boolean_almost_equal {a b} { + if {[string is double -strict $a]} { + if {[float_almost_equal $a 0]} { + set a 0 + } + } + if {[string is double -strict $b]} { + if {[float_almost_equal $b 0]} { + set b 0 + } + } + #must handle true,no etc. + expr {($a && 1) == ($b && 1)} + } + + + 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} " "<"] ;# (> required for insertionspecs at rhs of = & .= ) + #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname + + #except when prefixed directly by pin classifier ^ + set protect_terminals [list "^"] ;# e.g sequence ^# + #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string + #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' + set in_brackets 0 ;#count depth + set in_atom 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + set indq 0 + set inesc 0 ;#whether last char was backslash (see also punk::escv) + set prevc "" + set char_index 0 + foreach c [split $varspecs ""] { + if {$indq} { + if {$inesc} { + #puts stderr "inesc adding '$c'" + append token $c + } else { + if {$c eq {"}} { + set indq 0 + } else { + append token $c + } + } + } elseif {$in_atom} { + #ignore dquotes/brackets in atoms - pass through + append token $c + #set nextc [lindex $chars $char_index+1] + if {$c eq "'"} { + set in_atom 0 + } + } elseif {$in_brackets > 0} { + append token $c + if {$c eq ")"} { + incr in_brackets -1 + } + } else { + if {$c eq {"} && !$inesc} { + set indq 1 + } elseif {$c eq ","} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. + #lassign [scan $token %${first_term}s%s] var spec + set var [string range $token 0 $first_term-1] + set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list [string trim $var] [string trim $spec]] + set token "" + set token_index -1 ;#reduce by 1 because , not included in next token + set first_term -1 + } else { + append token $c + switch -exact -- $c { + ' { + set in_atom 1 + } + ( { + incr in_brackets + } + default { + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } + } + } + } + } + set prevc $c + if {$c eq "\\"} { + #review + if {$inesc} { + set inesc 0 + } else { + set token [string range $token 0 end-1] + set inesc 1 + } + } else { + set inesc 0 + } + incr token_index + incr char_index + } + if {[string length $token]} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + #lassign [scan $token %${first_term}s%s] var spec + set var [string range $token 0 $first_term-1] + set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list [string trim $var] [string trim $spec]] + } + return $varlist + } + proc _split_var_key_at_unbracketed_comma {varspecs} { + set varlist [list] + set var_terminals [list "@" "/" "#" "!"] + #except when prefixed directly by pin classifier ^ + set protect_terminals [list "^"] ;# e.g sequence ^# + #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string + #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' + set in_brackets 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + set prevc "" + foreach c [split $varspecs ""] { + if {$in_brackets} { + append token $c + if {$c eq ")"} { + set in_brackets 0 + } + } else { + if {$c eq ","} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + lassign [scan $token %${first_term}s%s] var spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list $var $spec] + set token "" + set token_index -1 ;#reduce by 1 because , not included in next token + set first_term -1 + } else { + append token $c + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } elseif {$c eq "("} { + set in_brackets 1 + } + } + } + set prevc $c + incr token_index + } + if {[string length $token]} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + lassign [scan $token %${first_term}s%s] var spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list $var $spec] + } + return $varlist + } + proc _split_var_key_at_unbracketed_comma1 {varspecs} { + set varlist [list] + set var_terminals [list "@" "/" "#" "!"] + set in_brackets 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + foreach c [split $varspecs ""] { + if {$in_brackets} { + if {$c eq ")"} { + set in_brackets 0 + } + append token $c + } else { + if {$c eq ","} { + 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 ""] + } + 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 + } + } + #? { + 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! + 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 + 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 (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 { + 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 + } + %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 { + 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 {$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 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 globvalue-get-keys-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 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 globvalue-get-pairs-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 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 globvalue-get-values-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 + } + } + }] + } + + error "globkeyvalue-get-pairs todo" + } + @* { + 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 + }] + } + %* { + 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 {[string first "end" $index] >=0} { + 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]} { + 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} { + #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"} { + if {[string match *-* $index]} { + lappend INDEX_OPERATIONS string-range + 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 "*"} { + 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 + } + + #todo - recurse into bracketed sub parts + #JMN3 + #e.g @*/(x@0,y@2) + proc _var_classify {multivar} { + set cmdname ::punk::pipecmds::var_classify::_[pipecmd_namemapping $multivar] + if {[info commands $cmdname] ne ""} { + return [$cmdname] + } + + + #comma seems a natural choice to split varspecs, + #but also for list and dict subelement access + #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) + #so / will indicate subelements e.g @0/1 for lindex $list 0 1 + #set valsource_key_list [_split_var_key_at_unbracketed_comma $multivar] + set valsource_key_list [_split_patterns_memoized $multivar] + + + + #mutually exclusive - atom/pin + #set map [list "" ' ^ &] ;#0 = default/var/not-yet-determined 1 = atom 2 = pin + #set var_class [lmap var $valsource_key_list {expr {([set m [lsearch $map [string index [lindex $var 0] 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] + #0 - novar + #1 - atom ' + #2 - pin ^ + #3 - boolean & + #4 - integer + #5 - double + #6 - var + #7 - glob (no classifier and contains * or ?) + #8 - numeric + #9 - > (+) + #10 - < (-) + + set var_names [list] + set var_class [list] + set varspecs_trimmed [list] ;#raw varspecs without pin/atom modifiers - or empty string for glob + + + set leading_classifiers [list "'" "&" "^" ] + set trailing_classifiers [list + -] + set possible_number_start [list - + . 0 1 2 3 4 5 6 7 8 9 > <] + + foreach v_key $valsource_key_list { + lassign $v_key v key + set vname $v ;#default + set classes [list] + if {$v eq ""} { + lappend var_class [list $v_key 0] + lappend varspecs_trimmed $v_key + } else { + set lastchar [string index $v end] + switch -- $lastchar { + + { + lappend classes 9 + set vname [string range $v 0 end-1] + } + - { + lappend classes 10 + set vname [string range $v 0 end-1] + } + } + set firstchar [string index $v 0] + switch -- $firstchar { + ' { + lappend var_class [list $v_key 1] + #set vname [string range $v 1 end] + lappend varspecs_trimmed [list $vname $key] + } + ^ { + lappend classes [list 2] + #use vname - may already have trailing +/- stripped + set vname [string range $vname 1 end] + set secondclassifier [string index $v 1] + switch -- $secondclassifier { + "&" { + #pinned boolean + lappend classes 3 + set vname [string range $v 2 end] + } + "#" { + #pinned numeric comparison instead of string comparison + #e.g set x 2 + # this should match: ^#x.= list 2.0 + lappend classes 8 + set vname [string range $vname 1 end] + } + "*" { + #pinned glob + lappend classes 7 + set vname [string range $v 2 end] + } + } + #todo - check for second tag - & for pinned boolean? + #consider requiring ^# for numeric comparisons. currently no way to do a strictly string comparison on pinned variables.... default ^var really shouldn't be doing any magic. + #while we're at it.. pinned glob would be nice. ^* + #maybe even pinned scan ^% ? regex? ^/ or ^? these would be hard to have corresponding literals in the pattern mini-lang. + #These all limit the range of varnames permissible - which is no big deal. + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed [list $vname $key] + } + & { + #we require boolean literals to be single-quoted so we can use cross-binding on boolean vars. + #ie &true is the variable true whereas &'true' or &'1' &'t' etc are literal booleans + #allow exception of &1 &0 to be literal booleans - because we disallow 0 & 1 as varnames in other contexts anyway - so it would be more consistent not to treat as varnames here. + lappend var_class [list $v_key 3] + set vname [string range $v 1 end] + lappend varspecs_trimmed [list $vname $key] + } + default { + if {([string first ? $v]) >=0 || ([string first * $v] >=0)} { + lappend var_class [list $v_key 7] ;#glob + #leave vname as the full glob + lappend varspecs_trimmed [list "" $key] + } else { + #scan vname not v - will either be same as v - or possibly stripped of trailing +/- + set numtestv [join [scan $vname %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, decimal points and sci notation - fails to handle leading dot e.g .5 + #leading . still need to test directly for double + if {[string is double -strict $vname] || [string is double -strict $numtestv]} { + if {[string is integer -strict $numtestv]} { + #this will pick up boolean 1 or 0 - but that's ok - they require "&" marker if boolean comparison desired + #integer test before double.. + #note there is also string is wide (string is wideinteger) for larger ints.. + lappend classes 4 + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed $v_key + } else { + #double + #sci notation 1e123 etc + #also large numbers like 1000000000 - even without decimal point - (tcl bignum) + lappend classes 5 + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed $v_key + } + } else { + lappend var_class [list $v_key 6] ;#var + lappend varspecs_trimmed $v_key + } + } + } + } + } + lappend var_names $vname + } + + set result [list var_names $var_names var_class $var_class varspecs_trimmed $varspecs_trimmed] + + proc $cmdname {} [list return $result] + debug.punk.pipe.compile {proc $cmdname} + return $result + } + + + + #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 [_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::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::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::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::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 + #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 var_display_names [list] + foreach v $var_names { + if {$v eq ""} { + lappend var_display_names {{}} + } else { + lappend var_display_names $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::objclone $args] + set nscaller [uplevel 1 [list 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::objclone $args] + set nscaller [uplevel 1 [list namespace current]] + tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller] + } + + #map rhs to names suitable to use in pipemcd proc name (whitespace mapping) + # (for .= and = pipecmds) + proc pipecmd_namemapping {rhs} { + #used to build a command from a pattern which could contain :: - we need to map it to keep it a single command in the desired namespace. + #glob chars will prevent us using simple test {[info commands $cmd] eq ""} to test existence + #we could use the test {$cmd in [info commands]} - but mapping away globchars is more robust, allowing the simpler test + set rhs [string trim $rhs];#ignore all leading & trailing whitespace + set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token + set rhs [tcl::string::map {: ? * } $rhs] + #review - we don't expect other command-incompatible chars such as colon? + return $rhs + } + + #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 [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 ::namespace path]] + if {$cmdns ni $existing_path} { + uplevel 1 [list ::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 [_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 ::namespace path]] + if {$cmdns ni $existing_path} { + uplevel 1 [list ::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? + } + 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 ] + 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 + } + + + + + #todo - consider whether we can use < for insertion/iteration combinations + # =a<,b< iterate once through + # =a><,b>< cartesian product + # =a<>,b<> ??? zip ? + # + # ie = {a b c} |> .=< inspect + # would call inspect 3 times, once for each argument + # .= list {a b c} {x y z} |a/0,b/1> .=a><,b>< list + # would produce list of cartesian pairs? + # + proc _split_equalsrhs {insertionpattern} { + #map the insertionpattern so we can use faster globless info command search + set name_mapped [pipecmd_namemapping $insertionpattern] + set cmdname ::punk::pipecmds::split_rhs::_$name_mapped + if {[info commands $cmdname] ne ""} { + return [$cmdname] + } + + set lst_var_indexposition [punk::_split_patterns_memoized $insertionpattern] + set i 0 + set return_triples [list] + foreach v_pos $lst_var_indexposition { + lassign $v_pos v index_and_position + #e.g varname@@data/ok>0 varname/1/0>end + #ensure only one ">" is detected + if {![string length $index_and_position]} { + set indexspec "" + set positionspec "" + } else { + set chars [split $index_and_position ""] + set posns [lsearch -all $chars ">"] + if {[llength $posns] > 1} { + error "pipesyntax error in segment insertion pattern '$insertionpattern' -v '$v' multiple '>' characters. Pattern not understood." "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] + } + if {![llength $posns]} { + set indexspec $index_and_position + set positionspec "" + } else { + set splitposn [lindex $posns 0] + set indexspec [string range $index_and_position 0 $splitposn-1] + set positionspec [string range $index_and_position $splitposn+1 end] + } + } + + #review - + if {($positionspec in [list "*" "/*" "@*" "/" "@"]) || ($v eq "*" && $positionspec eq "")} { + set star "" + if {$v eq "*"} { + set v "" + set star "*" + } + if {[string index $positionspec end] eq "*"} { + set star "*" + } + #it is always possible to insert at end of list regardless of current length - so /end* and @end* are equivalent + #as are /end and @end + #lset lst_var_indexposition $i [list $v "/end$star"] + set triple [list $v $indexspec "/end$star"] + } else { + if {$positionspec eq ""} { + #e.g just =varname + #lset lst_var_indexposition $i [list $v "/end"] + set triple [list $v $indexspec "/end"] + #error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' missing position spec e.g /0" + } else { + if {[string index $indexspec 0] ni [list "" "/" "@"]} { + error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' bad index spec '$indexspec'" "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] + } + set triple [list $v $indexspec $positionspec] + } + } + lappend return_triples $triple + incr i + } + proc $cmdname {} [list return $return_triples] + return $return_triples + } + + 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 + } + + # + # + # relatively slow on even small sized scripts + proc arg_is_script_shaped2 {arg} { + set re {^(\s|;|\n)$} + set chars [split $arg ""] + if {[lsearch -regex $chars $re] >=0} { + return 1 + } else { + return 0 + } + } + + #exclude quoted whitespace + proc arg_is_script_shaped {arg} { + if {[tcl::string::first \n $arg] >= 0} { + return 1 + } elseif {[tcl::string::first ";" $arg] >= 0} { + return 1 + } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { + lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found + return [expr {$part2 ne ""}] + } else { + return 0 + } + } + proc _rhs_tail_split {fullrhs} { + set inq 0; set indq 0 + set equalsrhs "" + set i 0 + foreach ch [split $fullrhs ""] { + if {$inq} { + append equalsrhs $ch + if {$ch eq {'}} { + set inq 0 + } + } elseif {$indq} { + append equalsrhs $ch + if {$ch eq {"}} { + set indq 0 + } + } else { + switch -- $ch { + {'} { + set inq 1 + } + {"} { + set indq 1 + } + " " { + #whitespace outside of quoting + break + } + 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z {} + default { + #\t not a literal for switch jumpTable bytecompile - review - can we do it without for example string mapping to (and without a literal binary tab in source file)? + #we can't (reliably?) put \t as one of our switch keys + # + if {$ch eq "\t"} { + break + } + } + } + append equalsrhs $ch + } + incr i + } + set tail [tcl::string::range $fullrhs $i end] + return [list $equalsrhs $tail] + } + + # -- + #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 + + switch -- $next1 { + pipematch { + 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 first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { + #*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 {[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 [_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 [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 <| + #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 $getv]{ + # + #}] + #maintenance - index logic should be similar identical? to to match_assign - which only needs to process atoms because it (for now?) delegates all pipeline ops here, so no vars available (single segment assign) + #tag: positionspechandler + + + #puts stdout "=== list_insertion_script '$positionspec' segmenttail " + set script2 [punk::list_insertion_script $positionspec segmenttail ] + set script2 [string map [list "\$insertion_data" ] $script2] + append insertion_script \n $script2 + + } + append insertion_script \n {set segmenttail} + append insertion_script \n "}" + #puts stderr "$insertion_script" + debug.punk.pipe.compile {creating proc ::punk::pipecmds::insertion::_$rhsmapped } 4 + eval $insertion_script + } + + set segment_members_filled [::punk::pipecmds::insertion::_$rhsmapped $dict_tagval [lindex [list $segmenttail [unset segmenttail]] 0] ] + + #set segment_members_filled $segmenttail + #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) + + } + set rhs [string map $dict_tagval $rhs] ;#obsolete? + + debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 + + + # script index could have changed!!! todo fix! + + #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) + if {(!$segment_first_is_script ) && $segment_op eq ".="} { + #no scriptiness detected + + #debug.punk.pipe.rep {[a yellow bold][rep_listname segment_members_filled][a]} 4 + + set cmdlist_result [uplevel 1 $segment_members_filled] + #debug.punk.pipe {[a green bold]forward_result: $forward_result[a]} 4 + #debug.punk.pipe.rep {[a yellow bold]forward_result REP: [rep $forward_result][a]} 4 + + #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] + + set segment_result [_handle_bind_result $d] + #puts stderr ">>forward_result: $forward_result segment_result $segment_result" + + + } elseif {$segment_op eq "="} { + #slightly different semantics for assigment! + #We index into the DATA - not the position within the segment! + #(an = segment must take a single argument, as opposed to a .= segment) + #(This was a deliberate design choice for consistency with set, and to reduce errors.) + #(we could have allowed multiple args to = e.g to form a list, but it was tried, and the edge-cases were unintuitive and prone to user error) + #(The choice to restrict to single argument, but allow insertion and appending via insertion-specs is more explicit and reliable even though the insertion-specs operate differently to those of .=) + # + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = + # must return: {a b c} not a b c + # + if {!$segment_has_insertions} { + set segment_members_filled $segment_members + if {[dict exists $dict_tagval data]} { + if {![llength $segment_members_filled]} { + set segment_members_filled [dict get $dict_tagval data] + } else { + lappend segment_members_filled [dict get $dict_tagval data] + } + } + } + + set d [_multi_bind_result $returnvarspec [lindex [list $segment_members_filled [unset segment_members_filled ]] 0]] + set segment_result [_handle_bind_result $d] + + + } elseif {$segment_first_is_script || $segment_op eq "script"} { + #script + debug.punk.pipe {[a+ cyan bold].. evaluating as script[a]} 2 + + set script [lindex $segment_members 0] + + #build argument lists for 'apply' + set segmentargnames [list] + set segmentargvals [list] + foreach {k val} $dict_tagval { + if {$k eq "args"} { + #skip args - it is manually added at the end of the apply list if it's a valid tcl list + continue + } + lappend segmentargnames $k + lappend segmentargvals $val + } + + set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list + #puts "------> rep prevr argsdatalist: [rep $argsdatalist]" + set add_argsdata 0 + if {[dict exists $dict_tagval "args"]} { + set argsdatalist [dict get $dict_tagval "args"] + #see if the raw result can be treated as a list + if {[catch {lindex $argsdatalist 0}]} { + #we cannot supply 'args' + set pre_script "" + #todo - only add trace if verbose warnings enabled? + append pre_script "trace add variable args read punk::pipeline_args_read_trace_error\n" + set script $pre_script + append script $segment_first_word + set add_argsdata 0 + } else { + set add_argsdata 1 + } + } + + debug.punk.pipe.rep {>> [rep_listname segmentargvals]} 4 + set ns [uplevel 1 {::namespace current}] + if {!$add_argsdata} { + debug.punk.pipe {APPLY1: (args not set; not a list) segment vars:$segmentargnames} 4 + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals" + set evaluation [uplevel 1 [list ::apply [::list $segmentargnames $script $ns] {*}$segmentargvals]] + } else { + debug.punk.pipe {APPLY2: (args is set)segment vars:$segmentargnames} 4 + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals $argsdatalist" + #pipeline script context should be one below calling context - so upvar v v will work + #ns with leading colon will fail with apply + set evaluation [uplevel 1 [list ::apply [::list [::concat $segmentargnames args] $script $ns] {*}$segmentargvals {*}$argsdatalist]] + } + + debug.punk.pipe.rep {script result, evaluation: [rep_listname evaluation]} 4 + #puts "---> rep script evaluation result: [rep $evaluation]" + #set d [_multi_bind_result $returnvarspec [punk::K $evaluation [unset evaluation]]] + + #trailing segment_members are *pipedata* scripts - as opposed to ordinary pipeline scripts! + set tail_scripts [lrange $segment_members 1 end] + if {[llength $tail_scripts]} { + set r [pipedata $evaluation {*}$tail_scripts] + } else { + set r $evaluation + } + set d [_multi_bind_result $returnvarspec [lindex [list $r [unset r]] 0]] + set segment_result [_handle_bind_result $d] + } else { + #tags ? + #debug.punk.pipe {>>raw commandline: [concat $rhs $segment_members_filled]} 5 + if 0 { + + + + #set s [list uplevel 1 [concat $rhs $segment_members_filled]] + if {![info exists pscript]} { + upvar ::_pipescript pscript + } + if {![info exists pscript]} { + #set pscript $s + set pscript [funcl::o_of_n 1 $segment_members] + } else { + #set pscript [string map [list

$pscript] {uplevel 1 [concat $rhs $segment_members_filled [

]]}] + #set snew "set pipe_$i \[uplevel 1 \[list $rhs $segment_members_filled " + #append snew "set pipe_[expr $i -1]" + #append pscript $snew + set pscript [funcl::o_of_n 1 $segment_members $pscript] + + } + } + set cmdlist_result [uplevel 1 $segment_members_filled] + #set d [_multi_bind_result $returnvarspec [punk::K $segment_members_filled [unset segment_members_filled]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result]] 0 ]] + + #multi_bind_result needs to return a funcl for rhs of: + #lindex [list [set syncvar [main pipeline.. ]] [rhs binding funcl...] 1 ] + #which uses syncvar + # + #The lhs of 'list' runs first so now syncvar can be the root level of the rhs function list and bind the necessary vars. + #NOTE: unintuitively, we are returning the value of rhs to the main pipleline! (leftmost binding) this is because the leftmost binding determines what goes back to the pipeline result + + set segment_result [_handle_bind_result $d] + } + #the subresult doesn't need to go backwards - as the final assignment can emit the result into a variable + #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section + #It may however make a good debug point + #puts stderr "segment $i segment_result:$segment_result" + + debug.punk.pipe.rep {[rep_listname segment_result]} 3 + + + + + + #examine tailremaining. + # either x x x |?> y y y ... + # or just y y y + #we want the x side for next loop + + #set up the conditions for the next loop + #|> x=y args + # inpipespec - contents of previous piper |xxx> + # outpipespec - empty or content of subsequent piper |xxx> + # previous_result + # assignment (x=y) + + + set pipespec($j,in) $pipespec($i,out) + set outpipespec "" + set tailmap "" + set next_pipe_posn -1 + if {[llength $tailremaining]} { + + #set tailmap [lmap v $tailremaining {lreplace [split $v {}] 1 end-1}] + ##e.g for: a b c |> e f g |> h + #set next_pipe_posn [lsearch $tailmap {| >}] + set next_pipe_posn [lsearch $tailremaining "|*>"] + + set outpipespec [string range [lindex $tailremaining $next_pipe_posn] 1 end-1] + } + set pipespec($j,out) $outpipespec + + + set script_like_first_word 0 + if {[llength $tailremaining] || $next_pipe_posn >= 0} { + + if {$next_pipe_posn >=0} { + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for + set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] + + } else { + set next_all_members $tailremaining + set tailremaining [list] + } + + + #assignment is the arg immediately following |> operator e.g x.=blah or x=etc (or a normal commandlist or script!) + set segment_first_word "" + set returnvarspec "" ;# the lhs of x=y + set segment_op "" + set rhs "" + set segment_first_is_script 0 + if {[llength $next_all_members]} { + if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + set segment_first_word [lindex $next_all_members 0] + set segment_first_is_script 1 + set segment_op "" + set segment_members $next_all_members + } else { + set possible_assignment [lindex $next_all_members 0] + #set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} + if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { + set segment_op ".=" + set segment_first_word [lindex $next_all_members 1] + set script_like_first_word [arg_is_script_shaped $segment_first_word] + if {$script_like_first_word} { + set segment_first_is_script 1 ;#relative to segment_members which no longer includes the .= + } + set segment_members [lrange $next_all_members 1 end] + } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { + set segment_op "=" + #never scripts + #must be at most a single element after the = ! + if {[llength $next_all_members] > 2} { + #raise this as pipesyntax as opposed to pipedata? + error "pipesyntax - at most one element can follow = (got [lrange $next_all_members 1 end])" "pipeline $segment_op $returnvarspec $rhs [lrange $next_all_members 1 end]" [list pipesyntax too_many_elements] + } + set segment_first_word [lindex $next_all_members 1] + if {[catch {llength $segment_first_word}]} { + set segment_is_list 0 ;#only used for segment_op = + } else { + set segment_is_list 1 ;#only used for segment_op = + } + + set segment_members $segment_first_word + } else { + #no assignment operator and not script shaped + set segment_op "" + set returnvarspec "" + set segment_first_word [lindex $next_all_members 0] + set segment_first_word [lindex $next_all_members 1] + set segment_members $next_all_members + #puts stderr ">>3 no-operator segment_first_word: '$segment_first_word'" + } + } + + + } else { + #?? two pipes in a row ? + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 + set segment_members return + set segment_first_word return + } + + #set forward_result $segment_result + #JMN2 + set previous_result $segment_result + #set previous_result [join $segment_result] + } else { + debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a]} 4 + #output pipe spec at tail of pipeline + + set pipedvars [dict create] + if {[string length $pipespec($i,out)]} { + set d [apply {{mv res} { + punk::_multi_bind_result $mv $res -levelup 1 + }} $pipespec($i,out) $segment_result] + set segment_result [_handle_bind_result $d] + set pipedvars [dict get $d setvars] + } + + set more_pipe_segments 0 + } + + #the segment_result is based on the leftmost var on the lhs of the .= + #whereas forward_result is always the entire output of the segment + #JMN2 + #lappend segment_result_list [join $segment_result] + lappend segment_result_list $segment_result + incr i + incr j + } ;# end while + + return [lindex $segment_result_list end] + #JMN2 + #return $segment_result_list + #return $forward_result + } + + + #just an experiment + #what advantage/difference versus [llength [lrange $data $start $end]] ??? + proc data_range_length {data start end} { + set datalen [llength $data] + + #normalize to s and e + if {$start eq "end"} { + set s [expr {$datalen - 1}] + } elseif {[string match end-* $start]} { + set stail [string range $start 4 end] + set posn [expr {$datalen - $stail -1}] + if {$posn < 0} { + return 0 + } + set s $posn + } else { + #int + if {($start < 0) || ($start > ($datalen -1))} { + return 0 + } + set s $start + } + if {$end eq "end"} { + set e [expr {$datalen - 1}] + } elseif {[string match end-* $end]} { + set etail [string range $end 4 end] + set posn [expr {$datalen - $etail -1}] + if {$posn < 0} { + return 0 + } + set e $posn + } else { + #int + if {($end < 0)} { + return 0 + } + set e $end + } + if {$s > ($datalen -1)} { + return 0 + } + if {$e > ($datalen -1)} { + set e [expr {$datalen -1}] + } + + + + if {$e < $s} { + return 0 + } + + return [expr {$e - $s + 1}] + } + + # unknown -- + # This procedure is called when a Tcl command is invoked that doesn't + # exist in the interpreter. It takes the following steps to make the + # command available: + # + # 1. See if the autoload facility can locate the command in a + # Tcl script file. If so, load it and execute it. + # 2. If the command was invoked interactively at top-level: + # (a) see if the command exists as an executable UNIX program. + # If so, "exec" the command. + # (b) see if the command requests csh-like history substitution + # in one of the common forms !!, !, or ^old^new. If + # so, emulate csh's history substitution. + # (c) see if the command is a unique abbreviation for another + # command. If so, invoke the command. + # + # Arguments: + # args - A list whose elements are the words of the original + # command, including the command name. + + #review - we shouldn't really be doing this + #We need to work out if we can live with the real default unknown and just inject some special cases at the beginning before falling-back to the normal one + + proc ::unknown args { + #puts stderr "unk>$args" + variable ::tcl::UnknownPending + global auto_noexec auto_noload env tcl_interactive errorInfo errorCode + + if {[info exists errorInfo]} { + set savedErrorInfo $errorInfo + } + if {[info exists errorCode]} { + set savedErrorCode $errorCode + } + + set name [lindex $args 0] + if {![info exists auto_noload]} { + # + # Make sure we're not trying to load the same proc twice. + # + if {[info exists UnknownPending($name)]} { + return -code error "self-referential recursion\ + in \"unknown\" for command \"$name\"" + } + set UnknownPending($name) pending + set ret [catch { + auto_load $name [uplevel 1 {::namespace current}] + } msg opts] + unset UnknownPending($name) + if {$ret != 0} { + dict append opts -errorinfo "\n (autoloading \"$name\")" + return -options $opts $msg + } + if {![array size UnknownPending]} { + unset UnknownPending + } + if {$msg} { + if {[info exists savedErrorCode]} { + set ::errorCode $savedErrorCode + } else { + unset -nocomplain ::errorCode + } + if {[info exists savedErrorInfo]} { + set errorInfo $savedErrorInfo + } else { + unset -nocomplain errorInfo + } + set code [catch {uplevel 1 $args} msg opts] + if {$code == 1} { + # + # Compute stack trace contribution from the [uplevel]. + # Note the dependence on how Tcl_AddErrorInfo, etc. + # construct the stack trace. + # + set errInfo [dict get $opts -errorinfo] + set errCode [dict get $opts -errorcode] + set cinfo $args + if {[string length [encoding convertto utf-8 $cinfo]] > 150} { + set cinfo [string range $cinfo 0 150] + while {[string length [encoding convertto utf-8 $cinfo]] > 150} { + set cinfo [string range $cinfo 0 end-1] + } + append cinfo ... + } + set tail "\n (\"uplevel\" body line 1)\n invoked\ + from within\n\"uplevel 1 \$args\"" + set expect "$msg\n while executing\n\"$cinfo\"$tail" + if {$errInfo eq $expect} { + # + # The stack has only the eval from the expanded command + # Do not generate any stack trace here. + # + dict unset opts -errorinfo + dict incr opts -level + return -options $opts $msg + } + # + # Stack trace is nested, trim off just the contribution + # from the extra "eval" of $args due to the "catch" above. + # + set last [string last $tail $errInfo] + if {$last + [string length $tail] != [string length $errInfo]} { + # Very likely cannot happen + return -options $opts $msg + } + set errInfo [string range $errInfo 0 $last-1] + set tail "\"$cinfo\"" + set last [string last $tail $errInfo] + if {$last < 0 || $last + [string length $tail] != [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo $errInfo $msg + } + set errInfo [string range $errInfo 0 $last-1] + set tail "\n invoked from within\n" + set last [string last $tail $errInfo] + if {$last + [string length $tail] == [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo [string range $errInfo 0 $last-1] $msg + } + set tail "\n while executing\n" + set last [string last $tail $errInfo] + if {$last + [string length $tail] == [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo [string range $errInfo 0 $last-1] $msg + } + return -options $opts $msg + } else { + dict incr opts -level + return -options $opts $msg + } + } + } + #set isrepl [expr {[file tail [file rootname [info script]]] eq "repl"}] + set isrepl [punk::repl::codethread::is_running] ;#may not be reading though + if {$isrepl} { + #set ::tcl_interactive 1 + } + if {$isrepl || (([info level] == 1) && (([info script] eq "" ) ) + && ([info exists tcl_interactive] && $tcl_interactive))} { + if {![info exists auto_noexec]} { + set new [auto_execok $name] + if {$new ne ""} { + set redir "" + if {[namespace which -command console] eq ""} { + set redir ">&@stdout <@stdin" + } + + + #windows experiment todo - use twapi and named pipes + #twapi::namedpipe_server {\\.\pipe\something} + #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones + #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc + # + + if {[string first " " $new] > 0} { + set c1 $name + } else { + set c1 $new + } + + # -- --- --- --- --- + set idlist_stdout [list] + set idlist_stderr [list] + #set shellrun::runout "" + #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks + #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] + #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] + + if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it + #not a trivial task + + #This runs external executables in a context in which they are not attached to a terminal + #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output + #ctrl-c propagation also needs to be considered + + set teehandle punksh + uplevel 1 [list ::catch \ + [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + + if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { + dict set ::tcl::UnknownOptions -code error + set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" + } else { + #no point returning "exitcode 0" if that's the only non-error return. + #It is misleading. Better to return empty string. + set ::tcl::UnknownResult "" + } + } else { + set repl_runid [punk::get_repl_runid] + #set ::punk::last_run_display [list] + + set redir ">&@stdout <@stdin" + uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr + #todo - there is probably no way around this but to somehow exec in the context of a completely separate console + #This is probably a tricky problem - especially to do cross-platform + # + # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit + if {[dict get $::tcl::UnknownOptions -code] == 0} { + set c green + set m "ok" + } else { + set c yellow + set m "errorCode $::errorCode" + } + set chunklist [list] + lappend chunklist [list "info" "[a $c]$m[a] " ] + if {$repl_runid != 0} { + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + + } + + foreach id $idlist_stdout { + shellfilter::stack::remove stdout $id + } + foreach id $idlist_stderr { + shellfilter::stack::remove stderr $id + } + # -- --- --- --- --- + + + #uplevel 1 [list ::catch \ + # [concat exec $redir $new [lrange $args 1 end]] \ + # ::tcl::UnknownResult ::tcl::UnknownOptions] + + #puts "===exec with redir:$redir $::tcl::UnknownResult ==" + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + } + + if {$name eq "!!"} { + set newcmd [history event] + } elseif {[regexp {^!(.+)$} $name -> event]} { + set newcmd [history event $event] + } elseif {[regexp {^\^([^^]*)\^([^^]*)\^?$} $name -> old new]} { + set newcmd [history event -1] + catch {regsub -all -- $old $newcmd $new newcmd} + } + if {[info exists newcmd]} { + tclLog $newcmd + history change $newcmd 0 + uplevel 1 [list ::catch $newcmd \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + + set ret [catch {set candidates [info commands $name*]} msg] + if {$name eq "::"} { + set name "" + } + if {$ret != 0} { + dict append opts -errorinfo \ + "\n (expanding command prefix \"$name\" in unknown)" + return -options $opts $msg + } + # Filter out bogus matches when $name contained + # a glob-special char [Bug 946952] + if {$name eq ""} { + # Handle empty $name separately due to strangeness + # in [string first] (See RFE 1243354) + set cmds $candidates + } else { + set cmds [list] + foreach x $candidates { + if {[string first $name $x] == 0} { + lappend cmds $x + } + } + } + + #punk - disable prefix match search + set default_cmd_search 0 + if {$default_cmd_search} { + if {[llength $cmds] == 1} { + uplevel 1 [list ::catch [lreplace $args 0 0 [lindex $cmds 0]] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + if {[llength $cmds]} { + return -code error "ambiguous command name \"$name\": [lsort $cmds]" + } + } else { + #punk hacked version - report matches but don't run + if {[llength $cmds]} { + return -code error "unknown command name \"$name\": possible match(es) [lsort $cmds]" + } + + } + + + } + return -code error -errorcode [list TCL LOOKUP COMMAND $name] "invalid command name $name" + } + + proc know {cond body} { + set existing [info body ::unknown] + #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) + ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##if {$body ni $existing} { + package require base64 + set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered + #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { + #--------------------------------------- + if {![catch {expr {@c@}} res] && $res} { + debug.punk.unknown {HANDLED BY: punk unknown_handler ([llength $args]) args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + return [eval {@b@}] + } else { + debug.punk.unknown {skipped: punk unknown_handler ([llength $args]) args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + } + #--------------------------------------- + }]$existing + #} + } + + proc know? {{len 2000}} { + puts [string range [info body ::unknown] 0 $len] + } + proc decodescript {b64} { + if {[ catch { + package require base64 + base64::decode $b64 + } scr]} { + return "" + } else { + return "($scr)" + } + } + + # --------------------------- + # commands that should be aliased in safe interps that need to use punk repl + # + proc get_repl_runid {} { + if {[interp issafe]} { + if {[info commands ::tsv::exists] eq ""} { + puts stderr "punk::get_repl_runid cannot operate directly in safe interp - install the appropriate punk aliases" + error "punk::get_repl_runid punk repl aliases not installed" + } + #if safe interp got here - there must presumably be a direct set of aliases on tsv::* commands + } + if {[tsv::exists repl runid]} { + return [tsv::get repl runid] + } else { + return 0 + } + } + #ensure we don't get into loop in unknown when in safe interp - which won't have tsv + proc set_repl_last_unknown {args} { + if {[interp issafe]} { + if {[info commands ::tsv::set] eq ""} { + puts stderr "punk::set_repl_last_unknown cannot operate directly in safe interp - install an alias to tsv::set repl last_unknown" + return + } + #tsv::* somehow working - possibly custom aliases for tsv functionality ? review + } + if {[info commands ::tsv::set] eq ""} { + puts stderr "set_repl_last_unknown - tsv unavailable!" + return + } + tsv::set repl last_unknown {*}$args + } + # --------------------------- + + + proc configure_unknown {} { + #----------------------------- + #these are critical e.g core behaviour or important for repl displaying output correctly + + #---------------- + #for var="val {a b c}" + #proc ::punk::val {{v {}}} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version + proc ::punk::val [list [list v [purelist]]] {return $v} + #---------------- + + #can't use know - because we don't want to return before original unknown body is called. + proc ::unknown {args} [string cat { + package require base64 + #set ::punk::last_run_display [list] + #set ::repl::last_unknown [lindex $args 0] ;#jn + #tsv::set repl last_unknown [lindex $args 0] ;#REVIEW + punk::set_repl_last_unknown [lindex $args 0] + }][info body ::unknown] + + + #handle process return dict of form {exitcode num etc blah} + #ie when the return result as a whole is treated as a command + #exitcode must be the first key + know {[lindex $args 0 0] eq "exitcode"} { + uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]] + } + + + #----------------------------- + # + # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. + + #todo - repl output info that it was evaluated as an expression + #know {[expr $args] || 1} {expr $args} + know {[expr $args] || 1} {tailcall expr $args} + + #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc + #punk::lib::range is defined as a wrapper to lseq if it is available (8.7+) + know {[regexp {^([+-]*[0-9_]+)\.\.([+-]*[0-9_]+)$} [lindex $args 0 0] -> from to]} {punk::lib::range $from $to} + + + #NOTE: + #we don't allow setting namespace qualified vars in the lhs assignment pattern. + #The principle is that we shouldn't be setting vars outside of the immediate calling scope. + #(It would also be difficult and error-prone and generally make the pipelines less re-usable and reliable) + #Therefore ::nswhatever::blah= x is the pipeline: blah= x - where the corresponding command, if any is first resolved in ::nswhatever + #We will require that the namespace already exists - which is consistent with if the command were to be run without unknown + proc ::punk::_unknown_assign_dispatch {matchedon pattern equalsrhs args} { + set tail [lassign $args hd] + #puts "-> _unknown_assign_dispatch '$partzerozero' pattern:'$pattern' equalsrhs:'$equalsrhs' args:'$args' argshd:'$hd' argstail:'$tail'" + if {$hd ne $matchedon} { + if {[llength $tail]} { + error "unknown_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $tail" + } + #regexp $punk::re_assign $hd _ pattern equalsrhs + #we assume the whole pipeline has been provided as the head + #regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail + regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs tail + } + #NOTE: - it doesn't make sense to call 'namespace' qualifiers or 'namespace tail' on a compound hd such as v,::etc= blah + # we only look at leftmost namespace-like thing and need to take account of the pattern syntax + # e.g for ::etc,'::x'= + # the ns is :: and the tail is etc,'::x'= + # (Tcl's namespace qualifiers/tail won't help here) + if {[string match ::* $hd]} { + set patterns [punk::_split_patterns_memoized $hd] + #get a pair-list something like: {::x /0} {etc {}} + set ns [namespace qualifiers [lindex $patterns 0 0]] + set nslen [string length $ns] + set patterntail [string range $ns $nslen end] + } else { + set ns "" + set patterntail $pattern + } + if {[string length $ns] && ![namespace exists $ns]} { + error "unknown_assign_dispatch: namespace '$ns' not found. (Note that pipeline lhs variables cannot be namespaced)" + } else { + set nscaller [uplevel 1 [list ::namespace current]] + #jmn + set rhsmapped [pipecmd_namemapping $equalsrhs] + set commands [uplevel 1 [list ::info commands $pattern=$rhsmapped]] ;#uplevel - or else we are checking from perspective of this namespace ::punk + #we must check for exact match of the command in the list - because command could have glob chars. + if {"$pattern=$rhsmapped" in $commands} { + puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'" + #we call the namespaced function - we don't evaluate it *in* the namespace. + #REVIEW + #warn for now...? + #tailcall $pattern=$equalsrhs {*}$args + tailcall $pattern=$rhsmapped {*}$tail + } + } + #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" + #ignore the namespace.. + #We could interpret the fact that the nonexistant pipe was called with a namespace to indicate that's where the pipecommand should be created.. + #But.. we would need to ensure 1st (compiling) invocation runs the same way as subsequent invocations. + #namespace evaling match_assign here probably wouldn't accomplish that and may create surprises with regards to where lhs vars(if any) are created + tailcall ::punk::match_assign $patterntail $equalsrhs {*}$tail + #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] + } + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) + #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list + #e.g x=a\nb c + #x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained + # + #know {[regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + #know {[regexp {^{([^\t\r\n=]*)\=([^ \t\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + + + + proc ::punk::_unknown_compare {val1 val2 args} { + if {![string length [string trim $val2]]} { + if {[llength $args] > 1} { + #error "Extra args after comparison operator ==. usage e.g : \$var1==\$var2 or \$var1==\$var2 + 2" + set val2 [string cat {*}[lrange $args 1 end]] + return [expr {$val1 eq $val2}] + } + return $val1 + } elseif {[llength $args] == 1} { + #simple comparison + if {[string is digit -strict $val1$val2]} { + return [expr {$val1 == $val2}] + } else { + return [string equal $val1 $val2] + } + } elseif {![catch {expr $val2 {*}[lrange $args 1 end]} evaluated]} { + if {[string is digit -strict $val1$evaluated]} { + return [expr {$val1 == $evaluated}] + } else { + return [expr {$val1 eq $evaluated}] + } + } else { + set evaluated [uplevel 1 [list {*}$val2 {*}[lrange $args 1 end]]] + if {[string is digit -strict $val1$evaluated]} { + return [expr {$val1 == $evaluated}] + } else { + return [expr {$val1 eq $evaluated}] + } + } + } + #ensure == is after = in know sequence + #.* on left is pretty broad - todo: make it a little more specific to avoid unexpected interactions + know {[regexp {(.*)==(.*)} [lindex $args 0] _ val1 val2]} {tailcall ::punk::_unknown_compare $val1 $val2 {*}$args} + #.= must come after = here to ensure it comes before = in the 'unknown' proc + #set punk::re_dot_assign {([^=]*)\.=(.*)} + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] _ varspecs rhs]} { + # set tail [expr {([lindex $args 0] eq [lindex $args 0 0]) ? [lrange $args 1 end] : [concat [lrange [lindex $args 0] 1 end] [lrange $args 1 end] ] }] + # tailcall ::punk::match_exec $varspecs $rhs {*}$tail + # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] + # } + # + + + + proc ::punk::_unknown_dot_assign_dispatch {partzerozero pattern equalsrhs args} { + #puts stderr ". unknown dispatch $partzerozero" + set argstail [lassign $args hd] + + #this equates to auto-flattening the head.. which seems like a bad idea, the structure was there for a reason. + #we should require explicit {*} expansion if the intention is for the args to be joined in at that level. + #expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } + + if {$hd ne $partzerozero} { + if {[llength $argstail]} { + error "unknown_dot_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $argstail" + } + #regexp $punk::re_assign $hd _ pattern equalsrhs + #we assume the whole pipeline has been provided as the head + #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + + regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs argstail + } + #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail + + + return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]] + + } + + # + know {[regexp {^([^\t\r\n=]*)\=([^\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + know {[regexp {^{([^\t\r\n=]*)\=([^\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + + #variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + + #add escaping backslashes to a value + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g + #set ktest {a"b} + #@@[escv $ktest].= list a"b val + #without escv: + #@@"a\\"b".= list a"b val + #with more backslashes in keys the escv use becomes more apparent: + #set ktest {\\x} + #@@[escv $ktest].= list $ktest val + #without escv we would need: + #@@\\\\\\\\x.= list $ktest val + proc escv {v} { + #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically + #thanks to DKF + regsub -all {\W} $v {\\&} + } + interp alias {} escv {} punk::escv + #review + #set v "\u2767" + # + #escv $v + #\ + #the + + + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { + # set argstail [lassign $args hd] + # #set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable! + # #avoid using the return from expr and it works: + # expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } + # + # tailcall ::punk::match_exec $varspecs $rhs {*}$tail + # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] + #} + + } + configure_unknown + #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. + # + + #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc + #Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation. + proc % {args} { + set arglist [lassign $args assign] ;#tail, head + if {$assign eq ".="} { + tailcall {*}[list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] + } + + set is_script [punk::arg_is_script_shaped $assign] + + if {!$is_script && [string index $assign end] eq "="} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + #set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + #set dumbeditor {\}} + if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=\{]*)=$} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax punk::% unable to interpret pipeline '$args'" "% $args" [list pipesyntax unable_to_interpret] + } + } else { + if {$is_script} { + set cmdlist [list ::punk::pipeline "script" "" "" {*}$args] + } else { + set cmdlist [list ::punk::pipeline ".=" "" "" {*}$args] + } + } + tailcall {*}$cmdlist + + + #result-based mismatch detection can probably never work nicely.. + #we need out-of-band method to detect mismatch. Otherwise we can't match on mismatch results! + # + set result [uplevel 1 $cmdlist] + #pipeline result not guaranteed to be a proper list so we can't use list methods to directly look for 'binding mismatch' + #.. but if we use certain string methods - we shimmer the case where the main result is a list + #string match doesn't seem to change the rep.. though it does generate a string rep. + #puts >>1>[rep $result] + if {[catch {lrange $result 0 1} first2wordsorless]} { + #if we can't get as a list then it definitely isn't the semi-structured 'binding mismatch' + return $result + } else { + if {$first2wordsorless eq {binding mismatch}} { + error $result + } else { + #puts >>2>[rep $result] + return $result + } + } + } + + proc ispipematch {args} { + expr {[lindex [uplevel 1 [list pipematch {*}$args]] 0] eq "ok"} + } + + #pipe initiator which will never raise an error *except for pipesyntax* , but always returns {ok {result something}} or {error {mismatch something}} or, for tcl errors {error {reason something}} + proc pipematch {args} { + #debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2 + variable re_dot_assign + variable re_assign + + set arglist [lassign $args assign] + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + # set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + # set dumbeditor {\}} + if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=]*)=.*} $assign _ returnvarspecs]} { + set cmdlist [list $assign {*}$arglist] + #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax punk::pipematch unable to interpret pipeline '$args'" "pipematch $args" [pipesyntax unable_to_interpret] + } + } else { + set cmdlist $args + #script? + #set cmdlist [list ::punk::pipeline .= "" "" {*}$args] + } + + if {[catch {uplevel 1 $cmdlist} result erroptions]} { + #puts stderr "pipematch erroptions:$erroptions" + #debug.punk.pipe {pipematch error $result} 4 + set ecode [dict get $erroptions -errorcode] + switch -- [lindex $ecode 0] { + binding { + if {[lindex $ecode 1] eq "mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + #return [dict create error [dict create mismatch $result]] + #puts stderr "pipematch converting error to {error {mismatch }}" + return [list error [list mismatch $result]] + } + } + pipesyntax { + #error $result + return -options $erroptions $result + } + casematch { + return $result + } + } + #return [dict create error [dict create reason $result]] + return [list error [list reason $result]] + } else { + return [list ok [list result $result]] + #debug.punk.pipe {pipematch result $result } 4 + #return [dict create ok [dict create result $result]] + } + } + + proc pipenomatchvar {varname args} { + if {[string first = $varname] >=0} { + #first word "pipesyntax" is looked for by pipecase + error "pipesyntax pipenomatch expects a simple varname as first argument" "pipenomatchvar $varname $args" [list pipesyntax expected_simple_varname] + } + #debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2 + + set assign [lindex $args 0] + set arglist [lrange $args 1 end] + if {[string first = $assign] >= 0} { + variable re_dot_assign + variable re_assign + #what if we get passed a script block containing = ?? e.g {error x=a} + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + } elseif {[regexp $re_dot_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs $rhs {*}$arglist] + } elseif {[regexp $re_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::pipeline = $returnvarspecs $rhs {*}$arglist] + } else { + debug.punk.pipe {[a+ yellow bold] Unexpected arg following pipenomatchvar variable [a]} 0 + set cmdlist $args + #return [dict create error [dict create reason [dict create pipematch bad_first_word value $assign pipeline [list pipematch $assign {*}$args]]]] + } + } else { + set cmdlist $args + } + + upvar 1 $varname nomatchvar + if {[catch {uplevel 1 $cmdlist} result erroptions]} { + set ecode [dict get $erroptions -errorcode] + debug.punk.pipe {[a+ yellow bold]pipematchnomatch error $result[a]} 3 + if {[lindex $ecode 0] eq "pipesyntax"} { + set errordict [dict create error [dict create pipesyntax $result]] + set nomatchvar $errordict + return -options $erroptions $result + } + if {[lrange $ecode 0 1] eq "binding mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + set errordict [dict create error [dict create mismatch $result]] + set nomatchvar $errordict + return -options $erroptions $result + } + set errordict [dict create error [dict create reason $result]] + set nomatchvar $errordict + #re-raise the error for pipeswitch to deal with + return -options $erroptions $result + } else { + debug.punk.pipe {pipematchnomatch result $result } 4 + set nomatchvar "" + #uplevel 1 [list set $varname ""] + #return raw result only - to pass through to pipeswitch + return $result + #return [dict create ok [dict create result $result]] + } + } + + #should only raise an error for pipe syntax errors - all other errors should be wrapped + proc pipecase {args} { + #debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9 + set arglist [lassign $args assign] + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + #set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + set cmdlist [list ::= {*}$arglist] + } elseif {![punk::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + #set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + #set dumbeditor {\}} + + if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=]*)=.*} $assign _ returnvarspecs]} { + set cmdlist [list $assign {*}$arglist] + #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax pipecase unable to interpret pipeline '$args'" + } + #todo - account for insertion-specs e.g x=* x.=/0* + } else { + #script? + set cmdlist [list ::punk::pipeline .= "" "" {*}$args] + } + + + if {[catch {uplevel 1 [list ::if 1 $cmdlist]} result erroptions]} { + #puts stderr "====>>> result: $result erroptions" + set ecode [dict get $erroptions -errorcode] + switch -- [lindex $ecode 0] { + pipesyntax { + #error $result + return -options $erroptions $result + } + casenomatch { + return -options $erroptions $result + } + binding { + if {[lindex $ecode 1] eq "mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + #return [dict create error [dict create mismatch $result]] + # + #NOTE: casemismatch is part of the api for pipecase. It is a casemismatch rather than an error - because for a pipecase - a casemismatch is an expected event (many casemismatches - one match) + return [dict create casemismatch $result] + } + } + } + + #we can't always treat $result as a list - may be an error string which can't be represented as a list, and there may be no useful errorCode + #todo - use errorCode instead + if {[catch {lindex $result 0} word1]} { + #tailcall error $result + return -options $erroptions $result + } else { + switch -- $word1 { + switcherror - funerror { + error $result "pipecase [lsearch -all -inline $args "*="]" + } + resultswitcherror - resultfunerror { + #recast the error as a result without @@ok wrapping + #use the tailcall return to stop processing other cases in the switch! + tailcall return [dict create error $result] + } + ignore { + #suppress error, but use normal return + return [dict create error [dict create suppressed $result]] + } + default { + #normal tcl error + #return [dict create error [dict create reason $result]] + tailcall error $result "pipecase $args" [list caseerror] + } + } + } + } else { + tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] + } + + } + + #note that pipeswitch deliberately runs in callers scope to have direct access to variables - it is akin to a control structure. + #It also - somewhat unusually accepts args - which we provide as 'switchargs' + #This is unorthodox/risky in that it will clobber any existing var of that name in callers scope. + #Solve using documentation.. consider raising error if 'switchargs' already exists, which would require user to unset switchargs in some circumstances. + proc pipeswitch {pipescript args} { + #set nextargs $args + #unset args + #upvar args upargs + #set upargs $nextargs + upvar switchargs switchargs + set switchargs $args + uplevel 1 [::list ::if 1 $pipescript] + } + #static-closure version - because we shouldn't be writing back to calling context vars directly + #Tcl doesn't (2023) have mutable closures - but for functional pipeline composition - we probably don't want that anyway! + #pipeswitchc is preferable to pipeswitch in that we can access context without risk of affecting it, but is less performant. (particularly in global scope.. but that probably isn't an important usecase) + proc pipeswitchc {pipescript args} { + set binding {} + if {[info level] == 1} { + #up 1 is global + set get_vars [list info vars] + } else { + set get_vars [list info locals] + } + set vars [uplevel 1 {*}$get_vars] + set posn [lsearch $vars switchargs] + set vars [lreplace $vars $posn $posn] + foreach v $vars { + upvar 1 $v var + if {(![array exists var]) && [info exists var]} { + lappend binding [list $v $var] ;#values captured as defaults for apply args. + } + } + lappend binding [list switchargs $args] + apply [list $binding $pipescript [uplevel 1 {::namespace current}]] + } + + proc pipedata {data args} { + #puts stderr "'$args'" + set r $data + for {set i 0} {$i < [llength $args]} {incr i} { + set e [lindex $args $i] + #review: string is list is as slow as catch {llength $e} - and also affects ::errorInfo unlike other string is commands. bug/enhancement report? + if {![string is list $e]} { + #not a list - assume script and run anyway + set r [apply [list {data} $e] $r] + } else { + if {[llength $e] == 1} { + switch -- $e { + > { + #output to calling context. only pipedata return value and '> varname' should affect caller. + incr i + uplevel 1 [list set [lindex $args $i] $r] + } + % - pipematch - ispipematch { + incr i + set e2 [lindex $args $i] + #set body [list $e {*}$e2] + #append body { $data} + + set body [list $e {*}$e2] + append body { {*}$data} + + + set applylist [list {data} $body] + #puts stderr $applylist + set r [apply $applylist $r] + } + pipeswitch - pipeswitchc { + #pipeswitch takes a script not a list. + incr i + set e2 [lindex $args $i] + set body [list $e $e2] + #pipeswitch takes 'args' - so expand $data when in pipedata context + append body { {*}$data} + #use applylist instead of uplevel when in pipedata context! + #can use either switchdata/data but not vars in calling context of 'pipedata' command. + #this is consistent with pipeswitch running in a % / .= pipeline which can only access vars in immediate calling context. + set applylist [list {data} $body] + #puts stderr $applylist + set r [apply $applylist $r] + } + default { + #puts "other single arg: [list $e $r]" + append e { $data} + set r [apply [list {data} $e] $r] + } + } + } elseif {[llength $e] == 0} { + #do nothing - pass data through + #leave r as is. + } else { + set r [apply [list {data} $e] $r] + } + } + } + return $r + } + + + proc scriptlibpath {{shortname {}} args} { + upvar ::punk::config::running running_config + set scriptlib [dict get $running_config scriptlib] + if {[string match "lib::*" $shortname]} { + set relpath [string map [list "lib::" "" "::" "/"] $shortname] + set relpath [string trimleft $relpath "/"] + set fullpath $scriptlib/$relpath + } else { + set shortname [string trimleft $shortname "/"] + set fullpath $scriptlib/$shortname + } + return $fullpath + } + + + #useful for aliases e.g treemore -> xmore tree + proc xmore {args} { + if {[llength $args]} { + uplevel #0 [list {*}$args | more] + } else { + error "usage: punk::xmore args where args are run as {*}\$args | more" + } + } + + + #environment path as list + # + #return *appendable* pipeline - i.e no args via <| + proc path_list_pipe {{glob *}} { + if {$::tcl_platform(platform) eq "windows"} { + set sep ";" + } else { + # : ok for linux/bsd ... mac? + set sep ":" + } + set cond [string map [list $glob] {expr {[string length $item] && [string match $item]}}] + #env members such as ''path' not case sensitive on windows - but are on some other platforms (at least FreeBSD) + return [list .= set ::env(PATH) |> .=>2 string trimright $sep |> .=>1 split $sep |> list_filter_cond $cond ] + } + proc path_list {{glob *}} { + set pipe [punk::path_list_pipe $glob] + {*}$pipe + } + proc path {{glob *}} { + set pipe [punk::path_list_pipe $glob] + {*}$pipe |> list_as_lines + } + + #------------------------------------------------------------------- + #sh 'test' equivalent - to be used with exitcode of process + # + + #single evaluation to get exitcode + proc sh_test {args} { + set a1 [lindex $args 0] + if {$a1 in [list -b -c -d -e -f -h -L -s -S -x -w]} { + set a2 [lindex $args 1] + if {![catch { + set attrinfo [file attributes $a2] + } errM]} { + if {[dict exists $attrinfo -vfs] && [dict get $attrinfo -vfs] == 1} { + puts stderr "WARNING: external 'test' being called on vfs path. External command will probably not have access to the vfs. Use 'TEST' for Tcl view of vfs mounted filesystems." + } + } + } + tailcall run test {*}$args + } + + #whether v is an integer from perspective of unix test command. + #can be be bigger than a tcl int or wide ie bignum - but must be whole number + #test doesn't handle 1.0 - so we shouldn't auto-convert + proc is_sh_test_integer {v} { + if {[string first . $v] >=0 || [string first e $v] >= 0} { + return false + } + #if it is double but not sci notation and has no dots - then we can treat as a large integer for 'test' + if {[string is double -strict $v]} { + return true + } else { + return false + } + } + #can use double-evaluation to get true/false + #faster tcl equivalents where possible to accuratley provide, and fallthrough to sh for compatibility of unimplemented + #The problem with fallthrough is that sh/bash etc have a different view of existant files + #e.g unix files such as /dev/null vs windows devices such as CON,PRN + #e.g COM1 is mapped as /dev/ttyS1 in wsl (?) + #Note also - tcl can have vfs mounted file which will appear as a directory to Tcl - but a file to external commands! + #We will stick with the Tcl view of the file system. + #User can use their own direct calls to external utils if + #Note we can't support $? directly in Tcl - script would have to test ${?} or use [set ?] + proc sh_TEST {args} { + upvar ? lasterr + set lasterr 0 + set a1 [lindex $args 0] + set a2 [lindex $args 1] + set a3 [lindex $args 2] + set fileops [list -b -c -d -e -f -h -L -s -S -x -w] + if {[llength $args] == 1} { + #equivalent of -n STRING + set boolresult [expr {[string length $a1] != 0}] + } elseif {[llength $args] == 2} { + if {$a1 in $fileops} { + if {$::tcl_platform(platform) eq "windows"} { + #e.g trailing dot or trailing space + if {[punk::winpath::illegalname_test $a2]} { + #protect with \\?\ to stop windows api from parsing + #will do nothing if already prefixed with \\?\ + + set a2 [punk::winpath::illegalname_fix $a2] + } + } + } + switch -- $a1 { + -b { + #dubious utility on FreeBSD, windows? + #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' + #Linux apparently uses them though + if{[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "blockSpecial"}] + } else { + set boolresult false + } + } + -c { + #e.g on windows CON,NUL + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "characterSpecial"}] + } else { + set boolresult false + } + } + -d { + set boolresult [file isdirectory $a2] + } + -e { + set boolresult [file exists $a2] + } + -f { + #e.g on windows CON,NUL + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "file"}] + } else { + set boolresult false + } + } + -h - + -L { + set boolresult [expr {[file type $a2] eq "link"}] + } + -s { + set boolresult [expr {[file exists $a2] && ([file size $a2] > 0 )}] + } + -S { + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "socket"}] + } else { + set boolresult false + } + } + -x { + set boolresult [expr {[file exists $a2] && [file executable $a2]}] + } + -w { + set boolresult [expr {[file exists $a2] && [file writable $a2]}] + } + -z { + set boolresult [expr {[string length $a2] == 0}] + } + -n { + set boolresult [expr {[string length $a2] != 0}] + } + default { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + } + } + } elseif {[llength $args] == 3} { + switch -- $a2 { + "=" { + #test does string comparisons + set boolresult [string equal $a1 $a3] + } + "!=" { + #string comparison + set boolresult [expr {$a1 ne $a3}] + } + "-eq" { + #test expects a possibly-large integer-like thing + #shell scripts will + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 == $a3}] + } + "-ge" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 >= $a3}] + } + "-gt" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 > $a3}] + } + "-le" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 <= $a3}] + } + "-lt" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 < $a3}] + } + "-ne" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 != $a3}] + } + default { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + + } + } + } else { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + } + + #normalize 1,0 etc to true,false + #we want to make it obvious we are not just reporting exitcode 0 for example - which represents true in tcl. + if {$boolresult} { + return true + } else { + if {$lasterr == 0} { + set lasterr 1 + } + return false + } + + + } + proc sh_echo {args} { + tailcall run echo {*}$args + } + proc sh_ECHO {args} { + #execute the result of the run command - which is something like: 'exitcode n' - to get true/false + tailcall apply {arglist {uplevel #0 [run echo {*}$arglist]} ::} $args + } + + + #sh style true/false for process exitcode. 0 is true - everything else false + proc exitcode {args} { + set c [lindex $args 0] + if {[string is integer -strict $c]} { + #return [expr {$c == 0}] + #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true + if {$c == 0} { + return true + } else { + return false + } + } else { + return false + } + } + #------------------------------------------------------------------- + + namespace export help aliases alias exitcode % pipedata pipecase pipeline pipematch pipeswitch pipeswitchc pipecase linelist linesort inspect list_as_lines val treemore + + #namespace ensemble create + + + + + #tilde + #These aliases work fine for interactive use - but the result is always a string int-rep + #interp alias {} ~ {} file join $::env(HOME) ;#HOME must be capitalized to work cross platform (lowercase home works on windows - but probably not elsewhere) + #interp alias {} ~ {} apply {args {file join $::env(HOME) $args}} + proc ~ {args} { + set hdir [punk::objclone $::env(HOME)] + file pathtype $hdir + set d $hdir + #use the file join 2-arg optimisation to avoid losing path-rep - probably doesn't give any advantage on all Tcl versions + foreach a $args { + set d [file join $d $a] + } + file pathtype $d + return [punk::objclone $d] + } + interp alias {} ~ {} punk::~ + + + #maint - punk::args has similar + #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args + #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? + #JMN + #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values + #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. + #only supports -flag val pairs, not solo options + #If an option is supplied multiple times - only the last value is used. + #TODO - remove + proc get_leading_opts_and_values {defaults rawargs args} { + if {[llength $defaults] %2 != 0} { + error "get_leading_opts_and_values expected first argument 'defaults' to be a dictionary" + } + dict for {k v} $defaults { + if {![string match -* $k]} { + error "get_leading_opts_and_values problem with supplied defaults. Expect each key to begin with a dash. Got key '$k'" + } + } + #puts "--> [info frame -2] <--" + set cmdinfo [dict get [info frame -2] cmd] + #we can't treat cmdinfo as a list - it may be something like {command {*}$args} in which case lindex $cmdinfo 0 won't work + #hopefully first word is a plain proc name if this function was called in the normal manner - directly from a proc + #we will break at first space and assume the lhs of that will give enough info to be reasonable - (alternatively we could use entire cmdinfo - but it might be big and ugly) + set caller [regexp -inline {\S+} $cmdinfo] + + #if called from commandline or some other contexts such as outside of a proc in a namespace - caller may just be "namespace" + if {$caller eq "namespace"} { + set caller "get_leading_opts_and_values called from namespace" + } + + # ------------------------------ + if {$caller ne "get_leading_opts_and_values"} { + #check our own args + lassign [get_leading_opts_and_values {-anyopts 0 -minvalues 0 -maxvalues -1} $args] _o ownopts _v ownvalues + if {[llength $ownvalues] > 0} { + error "get_leading_opts_and_values expected: a dictionary of defaults, a list of args and at most two option pairs -minvalues and -maxvalues - got extra arguments: '$ownvalues'" + } + set opt_minvalues [dict get $ownopts -minvalues] + set opt_maxvalues [dict get $ownopts -maxvalues] + set opt_anyopts [dict get $ownopts -anyopts] + } else { + #don't check our own args if we called ourself + set opt_minvalues 0 + set opt_maxvalues 0 + set opt_anyopts 0 + } + # ------------------------------ + + if {[set eopts [lsearch $rawargs "--"]] >= 0} { + set values [lrange $rawargs $eopts+1 end] + set arglist [lrange $rawargs 0 $eopts-1] + } else { + if {[lsearch $rawargs -*] >= 0} { + #to support option values with leading dash e.g -offset -1 , we can't just take the last flagindex + set i 0 + foreach {k v} $rawargs { + if {![string match -* $k]} { + break + } + if {$i+1 >= [llength $rawargs]} { + #no value for last flag + error "bad options for $caller. No value supplied for last option $k" + } + incr i 2 + } + set arglist [lrange $rawargs 0 $i-1] + set values [lrange $rawargs $i end] + } else { + set values $rawargs ;#no -flags detected + set arglist [list] + } + } + if {$opt_maxvalues == -1} { + #only check min + if {[llength $values] < $opt_minvalues} { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected at least $opt_minvalues" + } + } else { + if {[llength $values] < $opt_minvalues || [llength $values] > $opt_maxvalues} { + if {$opt_minvalues == $opt_maxvalues} { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected exactly $opt_minvalues" + } else { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected between $opt_minvalues and $opt_maxvalues inclusive" + } + } + } + + if {!$opt_anyopts} { + set checked_args [dict create] + for {set i 0} {$i < [llength $arglist]} {incr i} { + #allow this to error out with message indicating expected flags + dict set checked_args [tcl::prefix match -message "options for $caller. Unexpected option" [dict keys $defaults] [lindex $arglist $i]] [lindex $arglist $i+1] + incr i ;#skip val + } + } else { + set checked_args $arglist + } + set opts [dict merge $defaults $checked_args] + + #maintain order of opts $opts values $values as caller may use lassign. + return [dict create opts $opts values $values] + } + + + + + + + + + + #-------------------------------------------------- + #some haskell-like operations + #group equivalent + #http://zvon.org/other/haskell/Outputlist/group_f.html + #as we can't really distinguish a single element list from a string we will use 2 functions + proc group_list1 {lst} { + set out [list] + set prev [lindex $lst 0] + set g [list] + foreach i $lst { + if {$i eq $prev} { + lappend g $i + } else { + lappend out $g + set g [list $i] + } + set prev $i + } + lappend out $g + return $out + } + proc group_list {lst} { + set out [list] + set next [lindex $lst 1] + set tail [lassign $lst x] + set g [list $x] + set y [lindex $tail 0] + set last_condresult [expr {$x}] + set n 1 ;#start at one instead of zero for lookahead + foreach x $tail { + set y [lindex $tail $n] + set condresult [expr {$x}] + if {$condresult eq $last_condresult} { + lappend g $x + } else { + lappend out $g + set g [list $x] + set last_condresult $condresult + } + incr n + } + lappend out $g + return $out + } + + #NOT attempting to match haskell other than in overall concept. + # + #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. + #Haskell seems to take an entire lambda so varnames can be user-specified - but the 'magic' there is in it's choice of submitting 2 elements at a time + #We could do similar .. but we'll focus on comprehensibility for the basic cases - especially as begginning and end of list issues could be confusing. + # + #vars: index prev, prev0, prev1, item, next, next0, next1,nextr, cond + #(nextr is a bit obscure - but basically means next-repeat ie if no next - use same value. just once though.) + #group by cond result or first 3 wordlike parts of error + #e.g group_list_by {[lindex $item 0]} {{a 1} {a 2} {b 1}} + proc group_list_by {cond lst} { + set out [list] + set prev [list] + set next [lindex $lst 1] + set tail [lassign $lst item] + set g [list $item] + set next [lindex $tail 0] + if {$prev eq ""} { + set prev0 0 + set prev1 1 + set prevr $item + } else { + set prev0 $prev + set prev1 $prev + set prevr $prev + } + if {$next eq ""} { + set next0 0 + set next1 1 + set nextr $item + } else { + set next0 $next + set next1 $next + set nextr $next + } + set last_condresult [apply {{index cond prev prev0 prev1 prevr item next next0 next1 nextr} { + if {[catch {expr $cond} r]} { + puts stderr "index: 0 ERROR $r" + set wordlike_parts [regexp -inline -all {\S+} $r] + set r [list ERROR {*}[lrange $wordlike_parts 0 2]] + } + set r + } + } 0 $cond $prev $prev0 $prev1 $prevr $item $next $next0 $next1 $nextr] + set n 1 ;#start at one instead of zero for lookahead + #note - n also happens to matchi zero-based index of original list + set prev $item + foreach item $tail { + set next [lindex $tail $n] + if {$prev eq ""} { + set prev0 0 + set prev1 1 + set prevr $item + } else { + set prev0 $prev + set prev1 $prev + set prevr $prev + } + if {$next eq ""} { + set next0 0 + set next1 1 + set nextr $item + } else { + set next0 $next + set next1 $next + set nextr $next + } + set condresult [apply {{index cond prev prev0 prev1 prevr item next next0 next1 nextr} { + if {[catch {expr $cond} r]} { + puts stderr "index: $index ERROR $r" + set wordlike_parts [regexp -inline -all {\S+} $r] + set r [list ERROR {*}[lrange $wordlike_parts 0 2]] + } + set r + } + } $n $cond $prev $prev0 $prev1 $prevr $item $next $next0 $next1 $nextr] + if {$condresult eq $last_condresult} { + lappend g $item + } else { + lappend out $g + set g [list $item] + set last_condresult $condresult + } + incr n + set prev $item + } + lappend out $g + return $out + } + + #group_numlist ? preserve representation of numbers rather than use string comparison? + + + # - group_string + #.= punk::group_string "aabcccdefff" + # aa b ccc d e fff + proc group_string {str} { + lmap v [group_list [split $str ""]] {string cat {*}$v} + } + + #lists may be of unequal lengths + proc transpose_lists {list_rows} { + set res {} + #set widest [pipedata $list_rows {lmap v $data {llength $v}} {tcl::mathfunc::max {*}$data}] + set widest [tcl::mathfunc::max {*}[lmap v $list_rows {llength $v}]] + for {set j 0} {$j < $widest} {incr j} { + set newrow {} + foreach oldrow $list_rows { + if {$j >= [llength $oldrow]} { + continue + } else { + lappend newrow [lindex $oldrow $j] + } + } + lappend res $newrow + } + return $res + } + proc transpose_strings {list_of_strings} { + set charlists [lmap v $list_of_strings {split $v ""}] + set tchars [transpose_lists $charlists] + lmap v $tchars {string cat {*}$v} + } + + package require struct::matrix + #transpose a serialized matrix using the matrix command + #Note that we can have missing row values below and to right + #e.g + #a + #a b + #a + proc transpose_matrix {matrix_rows} { + set mcmd [struct::matrix] + #serialization format: numcols numrows rowlist + set widest [tcl::mathfunc::max {*}[lmap v $matrix_rows {llength $v}]] + $mcmd deserialize [list [llength $matrix_rows] $widest $matrix_rows] + $mcmd transpose + set result [lindex [$mcmd serialize] 2] ;#strip off dimensions + $mcmd destroy + return $result + } + + set objname [namespace current]::matrixchain + if {$objname ni [info commands $objname]} { + oo::class create matrixchain { + variable mcmd + constructor {matrixcommand} { + puts "wrapping $matrixcommand with [self]" + set mcmd $matrixcommand + } + destructor { + puts "matrixchain destructor called for [self] (wrapping $mcmd)" + $mcmd destroy + } + method unknown {args} { + if {[llength $args]} { + switch -- [lindex $args 0] { + add - delete - insert - transpose - sort - set - swap { + $mcmd {*}$args + return [self] ;#result is the wrapper object for further chaining in pipelines + } + default { + tailcall $mcmd {*}$args + } + } + } else { + #will error.. but we should pass that on + tailcall $mcmd + } + } + } + } + + #review + #how do we stop matrix pipelines from leaving commands around? i.e how do we call destroy on the matrixchain wrapper if not explicitly? + #Perhaps will be solved by: Tip 550: Garbage collection for TclOO + #Theoretically this should allow tidy up of objects created within the pipeline automatically + #If the object name is placed in the pipeline variable dict then it should survive across segment apply scripts and only go out of scope at the end. + proc matrix_command_from_rows {matrix_rows} { + set mcmd [struct::matrix] + set widest [tcl::mathfunc::max {*}[lmap v $matrix_rows {llength $v}]] + $mcmd deserialize [list [llength $matrix_rows] $widest $matrix_rows] + #return $mcmd + set wrapper [punk::matrixchain new $mcmd] + } + + #-------------------------------------------------- + + proc list_filter_cond {itemcond listval} { + set filtered_list [list] + set binding {} + if {[info level] == 1} { + #up 1 is global + set get_vars [list ::info vars] + } else { + set get_vars [list ::info locals] + } + set vars [uplevel 1 {*}$get_vars] + set posn [lsearch $vars item] + set vars [lreplace $vars $posn $posn] + foreach v $vars { + upvar 1 $v var + if {(![array exists var]) && [info exists var]} { + lappend binding [list $v $var] ;#values captured as defaults for apply args. + } + } + #lappend binding [list item $args] + + #puts stderr "binding: [join $binding \n]" + #apply [list $binding $pipescript [uplevel 1 ::namespace current]] + foreach item $listval { + set bindlist [list {*}$binding [list item $item]] + if {[apply [list $bindlist $itemcond [uplevel 1 ::namespace current]] ]} { + lappend filtered_list $item + } + } + return $filtered_list + } + + + proc ls {args} { + if {![llength $args]} { + set args [list [pwd]] + } + if {[llength $args] ==1} { + return [glob -nocomplain -tails -dir [lindex $args 0] *] + } else { + set result [dict create] + foreach a $args { + set k [file normalize $a] + set contents [glob -nocomplain -tails -dir $a *] + dict set result $k $contents + } + return $result + } + } + + + + #linelistraw is essentially split $text \n so is only really of use for pipelines, where the argument order is more convenient + #like linelist - but keeps leading and trailing empty lines + #single \n produces {} {} + #the result can be joined to reform the arg if a single arg supplied + # + proc linelistraw {args} { + set linelist [list] + foreach {a} $args { + set nlsplit [split $a \n] + lappend linelist {*}$nlsplit + } + #return [split $text \n] + return $linelist + } + proc linelist1 {args} { + set linelist [list] + foreach {a} $args { + set nlsplit [split $a \n] + set start 0 + set end "end" + + if {[lindex $nlsplit 0] eq ""} { + set start 1 + } + if {[lindex $nlsplit end] eq ""} { + set end "end-1" + } + set alist [lrange $nlsplit $start $end] + lappend linelist {*}$alist + } + return $linelist + } + + + #An implementation of a notoriously controversial metric. + proc LOC {args} { + set argspecs [subst { + -dir -default "\uFFFF" + -exclude_dupfiles -default 1 -type boolean + -exclude_punctlines -default 1 -type boolean + -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } + }] + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + + # -- --- --- --- --- --- + set opt_dir [dict get $opts -dir] + if {$opt_dir eq "\uFFFF"} { + set opt_dir [pwd] ;#pwd can take over a ms on windows in a not terribly deep path even with SSDs - so as a general rule we don't use it in the original defaults list + } + # -- --- --- --- --- --- + set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] + set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars + set opt_punctchars [dict get $opts -punctchars] + # -- --- --- --- --- --- + + + set filepaths [punk::path::treefilenames -dir $opt_dir {*}$searchspecs] + set loc 0 + set dupfileloc 0 + set seentails [list] + set dupfilecount 0 + set extensions [list] + set purepunctlines 0 + foreach fpath $filepaths { + set isdupfile 0 + set floc 0 + set fpurepunctlines 0 + set ext [file extension $fpath] + if {$ext ni $extensions} { + lappend extensions $ext + } + if {!$opt_exclude_punctlines} { + set floc [llength [linelist -line {trimright} -block {trimall} [fcat $fpath]]] + } else { + set lines [linelist -line {trimright} -block {trimall} [fcat $fpath]] + set mapawaypunctuation [list] + foreach p $opt_punctchars empty {} { + lappend mapawaypunctuation $p $empty + } + foreach ln $lines { + if {[string length [string trim [string map $mapawaypunctuation $ln]]] > 0} { + incr floc + } else { + incr fpurepunctlines + } + } + } + if {[file tail $fpath] in $seentails} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + } + if {!$isdupfile || ($isdupfile && !$opt_exclude_dupfiles)} { + incr loc $floc + incr purepunctlines $fpurepunctlines + } + + lappend seentails [file tail $fpath] + } + if {$opt_exclude_punctlines} { + return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions purepunctuationlines $purepunctlines] + } + return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions] + } + + + + #!!!todo fix - linedict is unfinished and non-functioning + #linedict based on indents + proc linedict {args} { + set data [lindex $args 0] + set opts [lrange $args 1 end] ;#todo + set nlsplit [split $data \n] + set rootindent -1 + set stepindent -1 + + #set wordlike_parts [regexp -inline -all {\S+} $lastitem] + set d [dict create] + set keys [list] + set i 1 + set firstkeyline "N/A" + set firststepline "N/A" + foreach ln $nlsplit { + if {![string length [string trim $ln]]} { + incr i + continue + } + set is_rootkey 0 + regexp {(\s*)(.*)} $ln _ space linedata + puts stderr ">>line:'$ln' [string length $space] $linedata" + set this_indent [string length $space] + if {$rootindent < 0} { + set firstkeyline $ln + set rootindent $this_indent + } + if {$this_indent == $rootindent} { + set is_rootkey 1 + } + if {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline" + } + if {$is_rootkey} { + dict set d $linedata {} + lappend keys $linedata + } else { + if {$stepindent < 0} { + set stepindent $this_indent + set firststepline $ln + } + if {$this_indent == $stepindent} { + dict set d [lindex $keys end] $ln + } else { + if {($this_indent % $stepindent) != 0} { + error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline" + } + + #todo fix! + set parentkey [lindex $keys end] + lappend keys [list $parentkey $ln] + set oldval [dict get $d $parentkey] + if {[string length $oldval]} { + set new [dict create $oldval $ln] + } else { + dict set d $parentkey $ln + } + + } + } + incr i + } + return $d + } + proc dictline {d} { + puts stderr "unimplemented" + set lines [list] + + return $lines + } + + + proc ooinspect {obj} { + set obj [uplevel 1 [list namespace which -command $obj]] + set isa [lmap type {object class metaclass} { + if {![info object isa $type $obj]} continue + set type + }] + foreach tp $isa { + switch -- $tp { + class { + lappend info {class superclasses} {class mixins} {class filters} + lappend info {class methods} {class methods} + lappend info {class variables} {class variables} + } + object { + lappend info {object class} {object mixins} {object filters} + lappend info {object methods} {object methods} + lappend info {object variables} {object variables} + lappend info {object namespace} {object vars} ;#{object commands} + } + } + } + + set result [dict create isa $isa] + foreach args $info { + dict set result $args [info {*}$args $obj] + foreach opt {-private -all} { + catch { + dict set result [list {*}$args $opt] [info {*}$args $obj $opt] + } + } + } + dict filter $result value {?*} + } + + + #pipeline inspect + #e.g + #= {a z c} |> inspect -label input_dict |> lsort |> {inspect $data} + proc inspect {args} { + set defaults [list -label "" -limit 20 -channel stderr -showcount 1 -ansi 1] + set flags [list] + set endoptsposn [lsearch $args --] ;#first -- if data expected to contain --, then should always be called with --. e.g inspect -- + if {$endoptsposn >= 0} { + set flags [lrange $args 0 $endoptsposn-1] + set pipeargs [lrange $args $endoptsposn+1 end] + } else { + #no explicit end of opts marker + #last trailing elements of args after taking *known* -tag v pairs is the value to inspect + for {set i 0} {$i < [llength $args]} {incr i} { + set k [lindex $args $i] + if {$k in [dict keys $defaults]} { + lappend flags {*}[lrange $args $i $i+1] + incr i + } else { + #first unrecognised option represents end of flags + break + } + } + set pipeargs [lrange $args $i end] + } + foreach {k v} $flags { + if {$k ni [dict keys $defaults]} { + error "inspect: unknown option $k. Known options: [dict keys $defaults]. If data contains flaglike elements, consider calling with end-of-opts marker. e.g inspect --" + } + } + set opts [dict merge $defaults $flags] + # -- --- --- --- --- + set label [dict get $opts -label] + set channel [dict get $opts -channel] + set showcount [dict get $opts -showcount] + if {[string length $label]} { + set label "${label}: " + } + set limit [dict get $opts -limit] + set opt_ansi [dict get $opts -ansi] + switch -- [string tolower $opt_ansi] { + 0 - 1 - 2 {} + view {set opt_ansi 2} + default { + error "inspect -ansi 0|1|2|view - received -ansi $opt_ansi" + } + } + # -- --- --- --- --- + + set more "" + if {[llength $pipeargs] == 1} { + #usual case is data as a single element + set val [lindex $pipeargs 0] + set count 1 + } else { + #but the pipeline segment could have an insertion-pattern ending in * + set val $pipeargs + set count [llength $pipeargs] + } + switch -- [string tolower $channel] { + nul - null - /dev/null { + return $val + } + } + set displayval $val ;#default - may be overridden based on -limit + + if {$count > 1} { + #val is a list + set llen [llength $val] + if {$limit > 0 && ($limit < $llen)} { + set displayval [lrange $val 0 $limit-1] + if {$llen > $limit} { + set more "..." + } + } + } else { + #not a valid tcl list - limit by lines + if {$limit > 0} { + set rawlines [split $val \n] + set llen [llength $rawlines] + set displaylines [lrange $rawlines 0 $limit-1] + set displayval [join $displaylines "\n"] + if {$llen > $limit} { + set more "\n..." + } + } + + } + if {$showcount} { + set displaycount "[a purple bold]($count)[a] " + if {$showcount} { + set countspace [expr {[string length $count] + 3}] ;#lhs margin size of count number plus brackets and one space + set margin [string repeat " " $countspace] + set displayval [string map [list \r "" \n "\n$margin"] $displayval] + } + } else { + set displaycount "" + } + if {$opt_ansi == 0} { + set displayval [punk::ansi::ansistrip $displayval] + } elseif {$opt_ansi == 2} { + set displayval [ansistring VIEW $displayval] + } + if {![string length $more]} { + puts $channel "$displaycount$label[a green bold]$displayval[a]" + } else { + puts $channel "$displaycount$label[a green bold]$displayval[a yellow bold]$more[a]" + } + return $val + } + + + + #return list of {chan chunk} elements + proc help_chunks {args} { + set chunks [list] + set linesep [string repeat - 76] + set mascotblock "" + catch { + package require patternpunk + #lappend chunks [list stderr [>punk . rhs]] + append mascotblock [textblock::frame [>punk . banner -title "Punk Shell" -left Tcl -right [package provide Tcl]]] + } + + set topic [lindex $args end] + set argopts [lrange $args 0 end-1] + + + set text "" + append text "Punk core navigation commands:\n" + + #todo - load from source code annotation? + set cmdinfo [list] + lappend cmdinfo [list help "This help. To see available subitems type: help topics"] + lappend cmdinfo [list dev "(ensemble command to make new projects/modules and to generate docs)"] + lappend cmdinfo [list a? "view ANSI colours\n e.g a? web"] + lappend cmdinfo [list ./ "view/change directory"] + lappend cmdinfo [list ../ "go up one directory"] + lappend cmdinfo [list ./new "make new directory and switch to it"] + lappend cmdinfo [list n/ "view/change namespace (accepts ns path globs e.g **::*get* to match comands at any level )"] + lappend cmdinfo [list n// "view/change namespace (with command listing)"] + lappend cmdinfo [list nn/ "go up one namespace"] + lappend cmdinfo [list n/new "make child namespace and switch to it"] + + set cmds [lsearch -all -inline -index 0 -subindices $cmdinfo *] + set descr [lsearch -all -inline -index 1 -subindices $cmdinfo *] + set widest1 [tcl::mathfunc::max {*}[lmap v $cmds {string length $v}]] + set widest2 [tcl::mathfunc::max {*}[lmap v $descr {string length $v}]] + set t [textblock::class::table new -show_seps 0] + foreach c $cmds d $descr { + #append text " [overtype::left $col1 $c][overtype::left $col2 $d]" \n + $t add_row [list $c $d] + } + set widest1 [$t column_datawidth 0] + $t configure_column 0 -minwidth [expr {$widest1 + 2}] + set widest2 [$t column_datawidth 1] + $t configure_column 1 -minwidth [expr {$widest2 + 1}] + append text [$t print] + + + set warningblock "" + + if {[catch {package require textblock} errM]} { + set introblock $mascotblock + append introblock \n $text + append warningblock \n "WARNING: textblock package couldn't be loaded. Side-by-side display not available" + + } else { + set introblock [textblock::join -- " " \n$mascotblock " " $text] + } + + + lappend chunks [list stdout $introblock] + + + if {$topic in [list tcl]} { + if {[punk::lib::system::has_script_var_bug]} { + append warningblock \n "minor warning: punk::lib::system::has_script_var_bug returned true! (string rep for list variable in script generated when script changed)" + } + if {[punk::lib::system::has_safeinterp_compile_bug]} { + set indent " " + append warningblock \n "[a+ web-red]warning: punk::lib::system::has_safeinterp_compile_bug returned true!" \n + append warningblock "${indent}(ensemble commands not compiled in safe interps - heavy performance impact in safe interps)" \n + append warningblock "${indent}see https://core.tcl-lang.org/tcl/tktview/1095bf7f75" + append warningblock [a] + } + } + + set text "" + if {$topic in [list env environment]} { + #todo - move to punk::config? + upvar ::punk::config::punk_env_vars_config punkenv_config + upvar ::punk::config::other_env_vars_config otherenv_config + + set known_punk [dict keys $punkenv_config] + set known_other [dict keys $otherenv_config] + append text \n + set usetable 1 + if {$usetable} { + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + if {"windows" eq $::tcl_platform(platform)} { + #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. + #The Tcl ::env array is linked to the underlying process view of the environment + #- but info exists ::env(var) can misreport as true if it has been deleted by setting to empty string rather than using unset. + #an 'array get' will resynchronise. + #Even if an env variable didn't exist before - setting it to empty string can get it to this inconsistent state. + array get ::env + } + #do an array read on ::env + foreach {v vinfo} $punkenv_config { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + set help "" + if {[dict exists $vinfo help]} { + set help [dict get $vinfo help] + } + $t add_row [list $v $c2 $help] + } + $t configure_column 0 -headers [list "Punk environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} + + set punktable [$t print] + $t destroy + + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + foreach {v vinfo} $otherenv_config { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + $t add_row [list $v $c2] + } + $t configure_column 0 -headers [list "Other environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} + + set othertable [$t print] + $t destroy + append text [textblock::join -- $punktable " " $othertable]\n + } else { + + append text $linesep\n + append text "punk environment vars:\n" + append text $linesep\n + set col1 [string repeat " " 25] + set col2 [string repeat " " 50] + foreach v $known_punk { + set c1 [overtype::left $col1 $v] + if {[info exists ::env($v)]} { + set c2 [overtype::left $col2 [set ::env($v)] + } else { + set c2 [overtype::right $col2 "(NOT SET)"] + } + append text "$c1 $c2\n" + } + append text $linesep\n + } + + lappend chunks [list stdout $text] + } + + if {$topic in [list console terminal]} { + lappend cstring_tests [dict create\ + type "PM "\ + msg "PRIVACY MESSAGE"\ + f7 punk::ansi::controlstring_PM\ + f7desc "7bit ESC ^"\ + f8 punk::ansi::controlstring_PM8\ + f8desc "8bit \\x9e"\ + ] + lappend cstring_tests [dict create\ + type SOS\ + msg "STRING"\ + f7 punk::ansi::controlstring_SOS\ + f7desc "7bit ESC X"\ + f8 punk::ansi::controlstring_SOS8\ + f8desc "8bit \\x98"\ + ] + lappend cstring_tests [dict create\ + type APC\ + msg "APPLICATION PROGRAM COMMAND"\ + f7 punk::ansi::controlstring_APC\ + f7desc "7bit ESC _"\ + f8 punk::ansi::controlstring_APC8\ + f8desc "8bit \\x9f"\ + ] + + foreach test $cstring_tests { + set m [[dict get $test f7] [dict get $test msg]] + set hidden_width_m [punk::console::test_char_width $m] + set m8 [[dict get $test f8] [dict get $test msg]] + set hidden_width_m8 [punk::console::test_char_width $m8] + if {$hidden_width_m != 0 || $hidden_width_m8 != 0} { + if {$hidden_width_m == 0} { + set d "[a+ green bold][dict get $test f7desc] [a red]${m}[a]" + } else { + set d "[a+ yellow bold][dict get $test f7desc] [a red]$m[a]" + } + if {$hidden_width_m8 == 0} { + set d8 "[a+ green ][dict get $test f8desc] [a red]$m8[a]" + } else { + set d8 "[a+ yellow bold][dict get $test f8desc] [a red]$m8[a]" + } + append warningblock \n "WARNING: terminal doesn't hide all [dict get $test type] control strings: $d $d8" + } + } + } + + lappend chunks [list stderr $warningblock] + if {$topic in [list topics help]} { + set text "" + set topics [dict create\ + "topics|help" "List help topics"\ + "tcl" "Tcl version warnings"\ + "env|environment" "punkshell environment vars"\ + "console|terminal" "Some console behaviour tests and warnings"\ + ] + + set t [textblock::class::table new -show_seps 0] + $t add_column -headers [list "Topic"] + $t add_column + foreach {k v} $topics { + $t add_row [list $k $v] + } + set widest0 [$t column_datawidth 0] + $t configure_column 0 -minwidth [expr {$widest0 + 4}] + append text \n[$t print] + + lappend chunks [list stdout $text] + } + + return $chunks + } + proc help {args} { + set chunks [help_chunks {*}$args] + foreach chunk $chunks { + lassign $chunk chan text + puts -nonewline $chan $text + } + } + proc mode {{raw_or_line query}} { + package require punk::console + tailcall ::punk::console::mode $raw_or_line + } + + #this hides windows cmd's mode command - probably no big deal - anyone who needs it will know how to exec it. + interp alias {} mode {} punk::mode + + + #NOTE: an alias may match in a namespace - but not have a corresponding command that matches that name (alias renamed) + proc aliases {{glob *}} { + set ns [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command + set ns_mapped [string map {:: \uFFFF} $ns] + #puts stderr "aliases ns: $ns_mapped" + set segments [split $ns_mapped \uFFFF] ;#include empty string before leading :: + if {![string length [lindex $segments end]]} { + #special case for :: only include leading segment rather thatn {} {} + set segments [lrange $segments 0 end-1] + } + set segcount [llength $segments] ;#only match number of segments matching current ns + + + set all_aliases [interp aliases {}] + set matched [list] + foreach a $all_aliases { + #normalize with leading :: + if {![string match ::* $a]} { + set abs ::$a + } else { + set abs $a + } + + set asegs [split [string map {:: \uFFFF} $abs] \uFFFF] + set acount [llength $asegs] + #puts "alias $abs acount:$acount asegs:$asegs segcount:$segcount segments: $segments" + if {[expr {$acount - 1}] == $segcount} { + if {[lrange $asegs 0 end-1] eq $segments} { + if {[string match $glob [lindex $asegs end]]} { + #report this alias in the current namespace - even though there may be no matching command + lappend matched $a ;#add raw alias token which may or may not have leading :: + } + } + } + } + #set matched_abs [lsearch -all -inline $all_aliases $glob] + + return $matched + } + + proc alias {{aliasorglob ""} args} { + set nsthis [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command + if {[llength $args]} { + if {$aliasorglob in [interp aliases ""]} { + set existing [interp alias "" $aliasorglob] + puts stderr "Overwriting existing alias $aliasorglob -> $existing with $aliasorglob -> $args (in current session only)" + } + if {([llength $args] ==1) && [string trim [lindex $args 0]] eq ""} { + #use empty string/whitespace as intention to delete alias + return [interp alias "" $aliasorglob ""] + } + return [interp alias "" $aliasorglob "" {*}$args] + } else { + if {![string length $aliasorglob]} { + set aliaslist [punk::aliases] + puts -nonewline stderr $aliaslist + return + } + #we need to first check for exact match of alias that happens to have glob chars i.e the supplied aliasorglob looks like a glob but is actually directly an alias + set target [interp alias "" $aliasorglob] + if {[llength $target]} { + return $target + } + + if {([string first "*" $aliasorglob] >= 0) || ([string first "?" $aliasorglob] >= 0)} { + set aliaslist [punk::aliases $aliasorglob] + puts -nonewline stderr $aliaslist + return + } + return [list] + } + } + + #pipeline-toys - put in lib/scriptlib? + ##geometric mean + #alias gmean .=> llength |> expr 1.0 / |e> .=i>* tcl::mathop::* |> .=>1,e>3 expr ** {::tcl::tm::add {*}$data; set path} |> inspect -label added_to_module_path <0/#| + + + + + + #interp alias {} c {} clear ;#external executable 'clear' may not always be available + #todo - review + interp alias {} clear {} ::punk::reset + interp alias {} c {} ::punk::reset + proc reset {} { + if {[llength [info commands ::punk::repl::reset_terminal]]} { + #punk::repl::reset_terminal notifies prompt system of reset + punk::repl::reset_terminal + } else { + puts -nonewline stdout [punk::ansi::reset] + } + } + + + + #fileutil::cat except with checking for windows illegal path names (when on windows platform) + interp alias {} fcat {} punk::mix::util::fcat + + #---------------------------------------------- + interp alias {} linelistraw {} punk::linelistraw + + # 'path' collides with kettle path in kettle::doc function - todo - patch kettle? + interp alias {} PATH {} punk::path + + interp alias {} path_list {} punk::path_list + interp alias {} list_filter_cond {} punk::list_filter_cond + + + interp alias {} inspect {} punk::inspect + interp alias {} ooinspect {} punk::ooinspect + + interp alias {} linedict {} punk::linedict + interp alias {} dictline {} punk::dictline + + #todo - pipepure - evaluate pipeline in a slave interp without commands that have side-effects. (safe interp?) + interp alias {} % {} punk::% + interp alias {} pipeswitch {} punk::pipeswitch + interp alias {} pipeswitchc {} punk::pipeswitchc ;#closure version - more correct + interp alias {} pipecase {} punk::pipecase + interp alias {} pipematch {} punk::pipematch + interp alias {} ispipematch {} punk::ispipematch + interp alias {} pipenomatchvar {} punk::pipenomatchvar + interp alias {} pipedata {} punk::pipedata + interp alias {} pipeset {} punk::pipeset + interp alias {} pipealias {} punk::pipealias + interp alias {} listset {} punk::listset ;#identical to pipeset + + + #non-core aliases + interp alias {} is_list_all_in_list {} punk::lib::is_list_all_in_list + interp alias {} is_list_all_ni_list {} punk::libis_list_all_ni_list + + + + #interp alias {} = {} ::punk::pipeline = "" "" + #interp alias {} = {} ::punk::match_assign "" "" + interp alias {} .= {} ::punk::pipeline .= "" "" + #proc .= {args} { + # #uplevel 1 [list ::punk::pipeline .= "" "" {*}$args] + # tailcall ::punk::pipeline .= "" "" {*}$args + #} + + + interp alias {} rep {} ::tcl::unsupported::representation + interp alias {} dis {} ::tcl::unsupported::disassemble + + + + # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion + interp alias {} l {} sh_runout -n ls -A ;#plain text listing + #interp alias {} ls {} sh_runout -n ls -AF --color=always + interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less + #note that shell globbing with * won't work on unix systems when using unknown/exec + interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) + interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. + # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? + #interp alias {} lw {} ls -aFv --color=always + + interp alias {} dir {} shellrun::runconsole dir + + # punk::nav::fs + package require punk::nav::fs + interp alias {} ./ {} punk::nav::fs::d/ + interp alias {} ../ {} punk::nav::fs::dd/ + interp alias {} d/ {} punk::nav::fs::d/ + interp alias {} dd/ {} punk::nav::fs::dd/ + + interp alias {} vwd {} punk::nav::fs::vwd ;#return punk::nav::fs::VIRTUAL_CWD - and report to stderr pwd if different + interp alias {} dirlist {} punk::nav::fs::dirlist + interp alias {} dirfiles {} punk::nav::fs::dirfiles + interp alias {} dirfiles_dict {} punk::nav::fs::dirfiles_dict + + interp alias {} ./new {} punk::nav::fs::d/new + interp alias {} d/new {} punk::nav::fs::d/new + interp alias {} ./~ {} punk::nav::fs::d/~ + interp alias {} d/~ {} punk::nav::fs::d/~ + interp alias "" x/ "" punk::nav::fs::x/ + + + if {$::tcl_platform(platform) eq "windows"} { + set has_powershell 1 + interp alias {} dl {} dir /q + interp alias {} dw {} dir /W/D + } else { + #todo - natsorted equivalent + #interp alias {} dl {} + interp alias {} dl {} puts stderr "not implemented" + interp alias {} dw {} puts stderr "not implemented" + #todo - powershell detection on other platforms + set has_powershell 0 + } + if {$has_powershell} { + #see also powershell runspaces etc: + # powershell runspaces e.g $rs=[RunspaceFactory]::CreateRunspace() + # $ps = [Powershell]::Create() + + interp alias {} ps {} exec >@stdout pwsh -nolo -nop -c + interp alias {} psx {} runx -n pwsh -nop -nolo -c + interp alias {} psr {} run -n pwsh -nop -nolo -c + interp alias {} psout {} runout -n pwsh -nop -nolo -c + interp alias {} pserr {} runerr -n pwsh -nop -nolo -c + interp alias {} psls {} shellrun::runconsole pwsh -nop -nolo -c ls + interp alias {} psps {} shellrun::runconsole pwsh -nop -nolo -c ps + } else { + set ps_missing "powershell missing (powershell is open source and can be installed on windows and most unix-like platforms)" + interp alias {} ps {} puts stderr $ps_missing + interp alias {} psx {} puts stderr $ps_missing + interp alias {} psr {} puts stderr $ps_missing + interp alias {} psout {} puts stderr $ps_missing + interp alias {} pserr {} puts stderr $ps_missing + interp alias {} psls {} puts stderr $ps_missing + interp alias {} psps {} puts stderr $ps_missing + } + proc psencode {cmdline} { + + } + proc psdecode {encodedcmd} { + + } + + proc repl {startstop} { + switch -- $startstop { + stop { + if {[punk::repl::codethread::is_running]} { + puts stdout "Attempting repl stop. Try ctrl-c or exit command to leave interpreter" + set ::repl::done 1 + } + } + start { + if {[punk::repl::codethread::is_running]} { + repl::start stdin + } + } + default { + error "repl unknown action '$startstop' - must be start or stop" + } + } + } + +} + + +# -- --- --- --- +#Load decks. commandset packages are not loaded until the deck is called. +# -- --- --- --- +package require punk::mod +#punk::mod::cli set_alias pmod +punk::mod::cli set_alias app + +#todo - change to punk::dev +package require punk::mix +punk::mix::cli set_alias dev +punk::mix::cli set_alias deck ;#deprecate! + +#todo - add punk::deck for managing cli modules and commandsets + +package require punkcheck::cli +punkcheck::cli set_alias pcheck +punkcheck::cli set_alias punkcheck +# -- --- --- --- + +package provide punk [namespace eval punk { + #FUNCTL + variable version + set version 0.1 +}] + + + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm new file mode 100644 index 00000000..83c02d0b --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -0,0 +1,272 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.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) 2024 +# +# @@ Meta Begin +# Application punk::aliascore 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::aliascore 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::aliascore] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::aliascore +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::aliascore +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::aliascore::class { +# #*** !doctools +# #[subsection {Namespace punk::aliascore::class}] +# #[para] class definitions +# if {[info commands [namespace current]::interface_sample1] eq ""} { +# #*** !doctools +# #[list_begin enumerated] +# +# # oo::class create interface_sample1 { +# # #*** !doctools +# # #[enum] CLASS [class interface_sample1] +# # #[list_begin definitions] +# +# # method test {arg1} { +# # #*** !doctools +# # #[call class::interface_sample1 [method test] [arg arg1]] +# # #[para] test method +# # puts "test: $arg1" +# # } +# +# # #*** !doctools +# # #[list_end] [comment {-- end definitions interface_sample1}] +# # } +# +# #*** !doctools +# #[list_end] [comment {--- end class enumeration ---}] +# } +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::aliascore { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + variable aliases + #use absolute ns ie must be prefixed with :: + #single element commands are imported if source command already exists, otherwise aliased. multi element commands are aliased + + #functions must be in export list of their source namespace + set aliases [tcl::dict::create\ + tstr ::punk::lib::tstr\ + list_as_lines ::punk::lib::list_as_lines\ + lines_as_list ::punk::lib::lines_as_list\ + linelist ::punk::lib::linelist\ + linesort ::punk::lib::linesort\ + pdict ::punk::lib::pdict\ + plist {::punk::lib::pdict -roottype list}\ + showlist {::punk::lib::showdict -roottype list}\ + showdict ::punk::lib::showdict\ + ansistrip ::punk::ansi::ansistrip\ + stripansi ::punk::ansi::ansistrip\ + ansiwrap ::punk::ansi::ansiwrap\ + colour ::punk::console::colour\ + ansi ::punk::console::ansi\ + color ::punk::console::colour\ + a+ ::punk::console::code_a+\ + A+ {::punk::console::code_a+ forcecolour}\ + a ::punk::console::code_a\ + A {::punk::console::code_a forcecolour}\ + a? ::punk::console::code_a?\ + A? {::punk::console::code_a? forcecolor}\ + smcup ::punk::console::enable_alt_screen\ + rmcup ::punk::console::disable_alt_screen\ + ] + + #*** !doctools + #[subsection {Namespace punk::aliascore}] + #[para] Core API functions for punk::aliascore + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + #todo - options as to whether we should raise an error if collisions found, undo aliases etc? + proc init {args} { + set defaults {-force 0} + set opts [dict merge $defaults $args] + set opt_force [dict get $opts -force] + + variable aliases + if {!$opt_force} { + set existing [list] + set conflicts [list] + foreach {a cmd} $aliases { + if {[tcl::info::commands ::$a] ne ""} { + lappend existing $a + if {[llength $cmd] > 1} { + #use alias mechanism + set existing_target [interp alias "" $a] + } else { + #using namespace import + #check origin + set existing_target [tcl::namespace::origin $cmd] + } + if {$existing_target ne $cmd} { + #command exists in global ns but doesn't match our defined aliases/imports + lappend conflicts $a + } + } + } + if {[llength $conflicts]} { + error "punk::aliascore::init declined to create any aliases or imports because -force == 0 and conflicts found:$conflicts" + } + } + set tempns ::temp_[info cmdcount] ;#temp ns for renames + dict for {a cmd} $aliases { + #puts "aliascore $a -> $cmd" + if {[llength $cmd] > 1} { + interp alias {} $a {} {*}$cmd + } else { + if {[tcl::info::commands $cmd] ne ""} { + #todo - ensure exported? noclobber? + if {[tcl::namespace::tail $a] eq [tcl::namespace::tail $cmd]} { + #puts stderr "importing $cmd" + tcl::namespace::eval :: [list namespace import $cmd] + } else { + #target command name differs from exported name + #e.g stripansi -> punk::ansi::ansistrip + #import and rename + #puts stderr "importing $cmd (with rename to ::$a)" + tcl::namespace::eval $tempns [list namespace import $cmd] + catch {rename ${tempns}::[namespace tail $cmd] ::$a} + } + } else { + interp alias {} $a {} {*}$cmd + } + } + } + #tcl::namespace::delete $tempns + return [dict keys $aliases] + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::aliascore ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#interp alias {} list_as_lines {} punk::lib::list_as_lines +#interp alias {} lines_as_list {} punk::lib::lines_as_list +#interp alias {} ansistrip {} punk::ansi::ansistrip ;#review +#interp alias {} linelist {} punk::lib::linelist ;#critical for = assignment features +#interp alias {} linesort {} punk::lib::linesort + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::aliascore::lib { + namespace export {[a-z]*} ;# Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace punk::aliascore::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::aliascore::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval punk::aliascore::system { + #*** !doctools + #[subsection {Namespace punk::aliascore::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::aliascore [namespace eval punk::aliascore { + variable pkg punk::aliascore + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm new file mode 100644 index 00000000..206b560b --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm @@ -0,0 +1,475 @@ + +tcl::namespace::eval punk::config { + variable loaded + variable startup ;#include env overrides + variable running + variable punk_env_vars + variable other_env_vars + + variable vars + + namespace export {[a-z]*} + + #todo - XDG_DATA_HOME etc + #https://specifications.freedesktop.org/basedir-spec/latest/ + # see also: http://hiphish.github.io/blog/2020/08/30/dotfiles-were-a-mistake/ + + proc init {} { + variable defaults + variable startup + variable running + variable punk_env_vars + variable punk_env_vars_config + variable other_env_vars + variable other_env_vars_config + + set exename "" + catch { + #catch for safe interps + #safe base will return empty string, ordinary safe interp will raise error + set exename [tcl::info::nameofexecutable] + } + if {$exename ne ""} { + set exefolder [file dirname $exename] + #default file logs to logs folder at same level as exe if writable, or empty string + set log_folder [file normalize $exefolder/../logs] + #tcl::dict::set startup scriptlib $exefolder/scriptlib + #tcl::dict::set startup apps $exefolder/../../punkapps + + #todo - use punk main.tcl location instead - exefolder doesn't work if system tclsh used etc + set default_scriptlib $exefolder/scriptlib + set default_apps $exefolder/../../punkapps + if {[file isdirectory $log_folder] && [file writable $log_folder]} { + #tcl::dict::set startup logfile_stdout $log_folder/repl-exec-stdout.txt + #tcl::dict::set startup logfile_stderr $log_folder/repl-exec-stderr.txt + set default_logfile_stdout $log_folder/repl-exec-stdout.txt + set default_logfile_stderr $log_folder/repl-exec-stderr.txt + } else { + set default_logfile_stdout "" + set default_logfile_stderr "" + } + } else { + #probably a safe interp - which cannot access info nameofexecutable even if access given to the location via punk::island + #review - todo? + #tcl::dict::set startup scriptlib "" + #tcl::dict::set startup apps "" + set default_scriptlib "" + set default_apps "" + set default_logfile_stdout "" + set default_logfile_stderr "" + } + + # auto_exec_mechanism ;#whether to use exec instead of experimental shellfilter::run + + #optional channel transforms on stdout/stderr. + #can sometimes be useful to distinguish eventloop stdout/stderr writes compared to those triggered directly from repl commands + #If no distinction necessary - should use default_color_ + #The counterpart: default_color__repl is a transform that is added and removed with each repl evaluation. + #startup color_stdout - parameters as suitable for punk::ansi::a+ (test with 'punk::ansi::a?') e.g "cyan bold" ;#not a good idea to default + set default_color_stdout brightwhite ;#stdout colour including background calls (after etc) + set default_color_stdout_repl "" ;#stdout colour applied during direct repl call only + #This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful. + #set default_color_stderr "red bold" + #set default_color_stderr "web-lightsalmon" + set default_color_stderr yellow ;#limit to basic colours for wider terminal support. yellow = term-olive + set default_color_stderr_repl "" ;#during repl call only + + set homedir "" + if {[catch { + #depending on which build of tcl - some safe interps prior to bugfix https://core.tcl-lang.org/tcl/info/3aa487993f will return a homedir value in an unmodified safe interp + #other 'safe' interps may have explicitly made this available - we shouldn't override that decision here using interp issafe so we can't compensate for versions which shouldn't really be returning this in the safe interp + set homedir [file home] + } errM]} { + #tcl 8.6 doesn't have file home.. try again + if {[info exists ::env(HOME)]} { + set homedir $::env(HOME) + } + } + + + # per user xdg vars + # --- + set default_xdg_config_home "" ;#config data - portable + set default_xdg_data_home "" ;#data the user likely to want to be portable + set default_xdg_cache_home "" ;#local cache + set default_xdg_state_home "" ;#persistent user data such as logs, but not as important or as portable as those in xdg_data_home + # --- + set default_xdg_data_dirs "" ;#non-user specific + #xdg_config_dirs ? + #xdg_runtime_dir ? + + + #review. we are assuming if we can't get a home dir - then all the xdg vars including xdg_data_dirs aren't likely to be useful (as presumably filesystem access is absent) + #(safe interp generally won't have access to ::env either) + #This coupling doesn't necessarily hold - its possible the relevant env vars were copied to a safe interp - although that would be a policy that would make disabling 'info home' inconsistent. + if {$homedir ne ""} { + if {"windows" eq $::tcl_platform(platform)} { + #as much as I'd prefer to use ~/.local/share and ~/.config to keep them more consistent with unixlike platforms - the vast majority of apps put them where microsoft wants them. + #we have a choice of LOCALAPPDATA vs APPDATA (local to machine vs potentially roaming/redirected in a corporate environment) + #using the roaming location should not impact users who aren't using a domain controller but is potentially much more convenient for those who do. + if {[info exists ::env(APPDATA)]} { + set default_xdg_config_home $::env(APPDATA) + set default_xdg_data_home $::env(APPDATA) + } + + #The xdg_cache_home should be kept local + if {[info exists ::env(LOCALAPPDATA)]} { + set default_xdg_cache_home $::env(LOCALAPPDATA) + set default_xdg_state_home $::env(LOCALAPPDATA) + } + + if {[info exists ::env(PROGRAMDATA)]} { + #- equiv env(ALLUSERSPROFILE) ? + set default_xdg_data_dirs $::env(PROGRAMDATA) + } + + } else { + #follow defaults as specified on freedesktop.org e.g https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html + set default_xdg_config_home [file join $homedir .config] + set default_xdg_data_home [file join $homedir .local share] + set default_xdg_cache_home [file join $homedir .cache] + set default_xdg_state_home [file join $homedir .local state] + set default_xdg_data_dirs /usr/local/share + } + } + + set defaults [dict create\ + apps $default_apps\ + config ""\ + configset ".punkshell"\ + scriptlib $default_scriptlib\ + color_stdout $default_color_stdout\ + color_stdout_repl $default_color_stdout_repl\ + color_stderr $default_color_stderr\ + color_stderr_repl $default_color_stderr_repl\ + logfile_stdout $default_logfile_stdout\ + logfile_stderr $default_logfile_stderr\ + logfile_active 0\ + syslog_stdout "127.0.0.1:514"\ + syslog_stderr "127.0.0.1:514"\ + syslog_active 0\ + auto_exec_mechanism exec\ + auto_noexec 0\ + xdg_config_home $default_xdg_config_home\ + xdg_data_home $default_xdg_data_home\ + xdg_cache_home $default_xdg_cache_home\ + xdg_state_home $default_xdg_state_home\ + xdg_data_dirs $default_xdg_data_dirs\ + theme_posh_override ""\ + posh_theme ""\ + posh_themes_path ""\ + ] + + set startup $defaults + #load values from saved config file - $xdg_config_home/punk/punk.config ? + #typically we want env vars to override the stored config - as env vars conventionally used on some commandlines. + #that's possibly ok for the PUNK_ vars + #however.. others like the xdg vars and NOCOLOR may apply to other apps.. and we may want to override them from the saved config? + #making some env vars override saved config values and some not would be potentially confusing. may need one/more specific settings or env vars to determine which takes precedence? + #simpler is probably just to let env vars take precedence - and warn when saving or viewing config that the saved values are being overridden + #- requiring user to manually unset any unwanted env vars when launching? + + #we are likely to want the saved configs for subshells/decks to override them however. + + #todo - load/save config file + + #todo - define which configvars are settable in env + #list of varname varinfo where varinfo is a sub dictionary (type key is mandatory, with value from: string,pathlist,boolean) + set punk_env_vars_config [dict create \ + PUNK_APPS {type pathlist}\ + PUNK_CONFIG {type string}\ + PUNK_CONFIGSET {type string}\ + PUNK_SCRIPTLIB {type string}\ + PUNK_AUTO_EXEC_MECHANISM {type string}\ + PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"}\ + PUNK_COLOR_STDERR {type string help "stderr colour transform. Use 'punk::ansi::a?' to see colour names"}\ + PUNK_COLOR_STDERR_REPL {type string help "stderr colour transform only while command running (not active during 'after')"}\ + PUNK_COLOR_STDOUT {type string help "stdout colour transform. Use 'punk::ansi::a?' to see colour names"}\ + PUNK_COLOR_STDOUT_REPL {type string help "stdout colour transform only while command running (not active during 'after')"}\ + PUNK_LOGFILE_STDOUT {type string}\ + PUNK_LOGFILE_STDERR {type string}\ + PUNK_LOGFILE_ACTIVE {type string}\ + PUNK_SYSLOG_STDOUT {type string}\ + PUNK_SYSLOG_STDERR {type string}\ + PUNK_SYSLOG_ACTIVE {type string}\ + PUNK_THEME_POSH_OVERRIDE {type string}\ + ] + set punk_env_vars [dict keys $punk_env_vars_config] + + #override with env vars if set + foreach {evar varinfo} $punk_env_vars_config { + if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] + set f [set ::env($evar)] + if {$f ne "default"} { + #e.g PUNK_SCRIPTLIB -> scriptlib + set varname [tcl::string::tolower [tcl::string::range $evar 5 end]] + if {$vartype eq "pathlist"} { + #colon vs semicolon path sep is problematic for windows environments where unix-like systems such as cygwin/wsl are used and a variable may be set for either the native path separator or the unix-like system + #Even without the colon vs semicolon issue, native vs unix-like paths on windows mixed environment systems can cause grief. + #For now at least, we will simply respect the platform pathSeparator and hope the user manages the environment variables appropriately. + #some programs do automatic translation - which is a nice idea in principle - but is also prone to error as we don't know if it's already occurred or not depending on how things are launched. + #An example of where this sort of thing can go wrong is env(TCLLIBPATH) - which is defined as a space separated list not requiring further splitting + # - but some programs have been known to split this value on colon anyway, which breaks things on windows. + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } + } + } + } + + # https://no-color.org + #if {[info exists ::env(NO_COLOR)]} { + # if {$::env(NO_COLOR) ne ""} { + # set colour_disabled 1 + # } + #} + set other_env_vars_config [dict create\ + NO_COLOR {type string}\ + XDG_CONFIG_HOME {type string}\ + XDG_DATA_HOME {type string}\ + XDG_CACHE_HOME {type string}\ + XDG_STATE_HOME {type string}\ + XDG_DATA_DIRS {type pathlist}\ + POSH_THEME {type string}\ + POSH_THEMES_PATH {type string}\ + TCLLIBPATH {type string}\ + ] + lassign [split [info tclversion] .] tclmajorv tclminorv + #don't rely on lseq or punk::lib for now.. + set relevant_minors [list] + for {set i 0} {$i <= $tclminorv} {incr i} { + lappend relevant_minors $i + } + foreach minor $relevant_minors { + set vname TCL${tclmajorv}_${minor}_TM_PATH + if {$minor eq $tclminorv || [info exists ::env($vname)]} { + dict set other_env_vars_config $vname {type string} + } + } + set other_env_vars [dict keys $other_env_vars_config] + + foreach {evar varinfo} $other_env_vars_config { + if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] + set f [set ::env($evar)] + if {$f ne "default"} { + set varname [tcl::string::tolower $evar] + if {$vartype eq "pathlist"} { + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } + } + } + } + + + #unset -nocomplain vars + + #todo + set running [tcl::dict::create] + set running [tcl::dict::merge $running $startup] + } + init + + #todo + proc Apply {config} { + puts stderr "punk::config::Apply partially implemented" + set configname [string map {-config ""} $config] + if {$configname in {startup running}} { + upvar ::punk::config::$configname applyconfig + + if {[dict exists $applyconfig auto_noexec]} { + set auto [dict get $applyconfig auto_noexec] + if {![string is boolean -strict $auto]} { + error "config::Apply error - invalid data for auto_noexec:'$auto' - expected boolean" + } + if {$auto} { + set ::auto_noexec 1 + } else { + #puts "auto_noexec false" + unset -nocomplain ::auto_noexec + } + } + + } else { + error "no config named '$config' found" + } + return "apply done" + } + Apply startup + + #todo - consider how to divide up settings, categories, 'devices', decks etc + proc get_running_global {varname} { + variable running + if {[dict exists $running $varname]} { + return [dict get $running $varname] + } + error "No such global configuration item '$varname' found in running config" + } + proc get_startup_global {varname} { + variable startup + if {[dict exists $startup $varname]} { + return [dict get $startup $varname] + } + error "No such global configuration item '$varname' found in startup config" + } + + proc get {whichconfig {globfor *}} { + variable startup + variable running + switch -- $whichconfig { + config - startup - startup-config - startup-configuration { + #show *startup* config - different behaviour may be confusing to those used to router startup and running configs + set configdata $startup + } + running - running-config - running-configuration { + set configdata $running + } + default { + error "Unknown config name '$whichconfig' - try startup or running" + } + } + if {$globfor eq "*"} { + return $configdata + } else { + set keys [dict keys $configdata [string tolower $globfor]] + set filtered [dict create] + foreach k $keys { + dict set filtered $k [dict get $configdata $k] + } + return $filtered + } + } + + proc configure {args} { + set argd [punk::args::get_dict { + + whichconfig -type string -choices {startup running} + } $args] + + } + + proc show {whichconfig {globfor *}} { + #todo - tables for console + set configdata [punk::config::get $whichconfig $globfor] + return [punk::lib::showdict $configdata] + } + + #e.g + # copy running-config startup-config + # copy startup-config test-config.cfg + # copy backup-config.cfg running-config + #review - consider the merge vs overwrite feature of some routers.. where copy to running-config does a merge rather than an overwrite + #This is to allow partial configs to be loaded to running, whereas a save of running to any target is always a complete configuration + proc copy {args} { + set argd [punk::args::get_dict { + *proc -name punk::config::copy -help "Copy a partial or full configuration from one config to another + If a target config has additional settings, then the source config can be considered to be partial with regards to the target. + " + -type -default "" -choices {replace merge} -help "Defaults to merge when target is running-config + Defaults to replace when source is running-config" + *values -min 2 -max 2 + fromconfig -help "running or startup or file name (not fully implemented)" + toconfig -help "running or startup or file name (not fully implemented)" + } $args] + set fromconfig [dict get $argd values fromconfig] + set toconfig [dict get $argd values toconfig] + set fromconfig [string map {-config ""} $fromconfig] + set toconfig [string map {-config ""} $toconfig] + + set copytype [dict get $argd opts -type] + + + #todo - warn & prompt if doing merge copy to startup + switch -exact -- $fromconfig-$toconfig { + running-startup { + if {$copytype eq ""} { + set copytype replace ;#full configuration + } + if {$copytype eq "replace"} { + error "punk::config::copy error. full configuration copy from running to startup config not yet supported" + } else { + error "punk::config::copy error. merge configuration copy from running to startup config not yet supported" + } + } + startup-running { + #default type merge - even though it's not always what is desired + if {$copytype eq ""} { + set copytype merge ;#load in a partial configuration + } + + #warn/prompt either way + if {$copytype eq "replace"} { + #some routers require use of a separate command for this branch. + #presumably to ensure the user doesn't accidentally load partials onto a running system + # + error "punk::config::copy error. full configuration copy from startup to overwrite running config not supported" + } else { + error "punk::config::copy error. merge copy from possibly partial configuration: startup to running config not currently supported" + } + } + default { + error "punk::config::copy error. copy must from running to startup or startup to running. File sources/targets not yet supported" + } + } + } + + + + + +} + + + + + +#todo - move to cli? +::tcl::namespace::eval punk::config { + #todo - something better - 'previous' rather than reverting to startup + proc channelcolors {{onoff {}}} { + variable running + variable startup + + if {![string length $onoff]} { + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } else { + if {![string is boolean $onoff]} { + error "channelcolors: invalid value $onoff - expected boolean: true|false|on|off|1|0|yes|no" + } + if {$onoff} { + dict set running color_stdout [dict get $startup color_stdout] + dict set running color_stderr [dict get $startup color_stderr] + } else { + dict set running color_stdout "" + dict set running color_stderr "" + } + } + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } +} + +package provide punk::config [tcl::namespace::eval punk::config { + variable version + set version 0.1 + +}] \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mod-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mod-0.1.tm new file mode 100644 index 00000000..58906c88 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mod-0.1.tm @@ -0,0 +1,164 @@ +#punkapps app manager +# deck cli + +namespace eval punk::mod::cli { + namespace export help list run + namespace ensemble create + + # namespace ensemble configure [namespace current] -unknown punk::mod::cli::_unknown + if 0 { + proc _unknown {ns args} { + puts stderr "punk::mod::cli::_unknown '$ns' '$args'" + puts stderr "punk::mod::cli::help $args" + puts stderr "arglen:[llength $args]" + punk::mod::cli::help {*}$args + } + } + + #cli must have _init method - usually used to load commandsets lazily + # + variable initialised 0 + proc _init {args} { + variable initialised + if {$initialised} { + return + } + #... + set initialised 1 + } + + proc help {args} { + set basehelp [punk::mix::base help {*}$args] + #namespace export + return $basehelp + } + proc getraw {appname} { + upvar ::punk::config::running running_config + set app_folders [dict get $running_config apps] + #todo search each app folder + set bases [::list] + set versions [::list] + set mains [::list] + set appinfo [::list bases {} mains {} versions {}] + + foreach containerfolder $app_folders { + lappend bases $containerfolder + if {[file exists $containerfolder]} { + if {[file exists $containerfolder/$appname/main.tcl]} { + #exact match - only return info for the exact one specified + set namematches $appname + set parts [split $appname -] + } else { + set namematches [glob -nocomplain -dir $containerfolder -type d -tail ${appname}-*] + set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort? + } + foreach nm $namematches { + set mainfile $containerfolder/$nm/main.tcl + set parts [split $nm -] + if {[llength $parts] == 1} { + set ver "" + } else { + set ver [lindex $parts end] + } + if {$ver ni $versions} { + lappend versions $ver + lappend mains $ver $mainfile + } else { + puts stderr "punk::apps::app version '$ver' of app '$appname' already encountered at $mainfile. (will use earliest encountered in running-config apps and ignore others of same version)" + } + } + } else { + puts stderr "punk::apps::app missing apps_folder:'$containerfolder' Ensure apps_folder is set in punk::config" + } + } + dict set appinfo versions $versions + #todo - natsort! + set sorted_versions [lsort $versions] + set latest [lindex $sorted_versions 0] + if {$latest eq "" && [llength $sorted_versions] > 1} { + set latest [lindex $sorted_versions 1 + } + dict set appinfo latest $latest + + dict set appinfo bases $bases + dict set appinfo mains $mains + return $appinfo + } + + proc list {{glob *}} { + upvar ::punk::config::running running_config + set apps_folder [dict get $running_config apps] + if {[file exists $apps_folder]} { + if {[file exists $apps_folder/$glob]} { + #tailcall source $apps_folder/$glob/main.tcl + return $glob + } + set apps [glob -nocomplain -dir $apps_folder -type d -tail $glob] + if {[llength $apps] == 0} { + if {[string first * $glob] <0 && [string first ? $glob] <0} { + #no glob chars supplied - only launch if exact match for name part + set namematches [glob -nocomplain -dir $apps_folder -type d -tail ${glob}-*] + set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort? + if {[llength $namematches] > 0} { + set latest [lindex $namematches end] + lassign $latest nm ver + #tailcall source $apps_folder/$latest/main.tcl + } + } + } + + return $apps + } + } + + #todo - way to launch as separate process + # solo-opts only before appname - args following appname are passed to the app + proc run {args} { + set nameposn [lsearch -not $args -*] + if {$nameposn < 0} { + error "punkapp::run unable to determine application name" + } + set appname [lindex $args $nameposn] + set controlargs [lrange $args 0 $nameposn-1] + set appargs [lrange $args $nameposn+1 end] + + set appinfo [punk::mod::cli::getraw $appname] + if {[llength [dict get $appinfo versions]]} { + set ver [dict get $appinfo latest] + puts stdout "info: $appinfo" + set ::argc [llength $appargs] + set ::argv $appargs + source [dict get $appinfo mains $ver] + if {"-hideconsole" in $controlargs} { + puts stderr "attempting console hide" + #todo - something better - a callback when window mapped? + after 500 {::punkapp::hide_console} + } + return $appinfo + } else { + error "punk::mod::cli unable to run '$appname'. main.tcl not found in [dict get $appinfo bases]" + } + } + + +} + +namespace eval punk::mod::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command help + package require punk::mix::base + package require punk::overlay + punk::overlay::custom_from_base [namespace current] ::punk::mix::base +} + +package provide punk::mod [namespace eval punk::mod { + variable version + set version 0.1 + +}] + + + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm new file mode 100644 index 00000000..fdffa091 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -0,0 +1,1373 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.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) 2024 +# +# @@ Meta Begin +# Application punk::nav::fs 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::nav::fs 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::nav::fs] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::nav::fs +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::nav::fs +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::lib +package require punk::args +package require punk::ansi +package require punk::winpath +package require punk::du +package require commandstack +#*** !doctools +#[item] [package {Tcl 8.6}] +#[item] [package {punk::lib}] +#[item] [package {punk::args}] +#[item] [package {punk::winpath}] +#[item] [package {punk::du}] +#[item] [package {punk::commandstack}] + +if {"windows" eq $::tcl_platform(platform)} { + catch {package require punk::unixywindows} +} +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::nav::fs::class { + #*** !doctools + #[subsection {Namespace punk::nav::fs::class}] + #[para] class definitions + #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::nav::fs { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + + #Both tcl's notion of pwd and VIRTUAL_CWD can be out of sync with the process CWD. This happens when in a VFS. + #We can also have VIRTUAL_CWD navigate to spaces that Tcl's cd can't - review + + variable VIRTUAL_CWD ;#cwd that tracks pwd except when in zipfs locations that are not at or below a mountpoint + if {![interp issafe]} { + set VIRTUAL_CWD [pwd] + } else { + set VIRTUAL_CWD "" + } + proc vwd {} { + variable VIRTUAL_CWD + set cwd [pwd] + if {$cwd ne $VIRTUAL_CWD} { + puts stderr "pwd: $cwd" + } + return $::punk::nav::fs::VIRTUAL_CWD + } + + #TODO - maintain per 'volume/server' CWD + #e.g cd and ./ to: + # d: + # //zipfs: + # //server + # https://example.com + # should return to the last CWD for that volume/server + + #VIRTUAL_CWD follows pwd when changed via cd + set stackrecord [commandstack::rename_command -renamer punk::nav::fs cd {args} { + if {![catch { + $COMMANDSTACKNEXT {*}$args + } errM]} { + set ::punk::nav::fs::VIRTUAL_CWD [pwd] + } else { + error $errM + } + }] + + #*** !doctools + #[subsection {Namespace punk::nav::fs}] + #[para] Core API functions for punk::nav::fs + #[list_begin definitions] + + + #NOTE - as we expect to run other apps (e.g Tk) in the same process, but possibly different threads - we should be careful about use of cd which is per-process not per-thread. + #As this function recurses and calls cd multiple times - it's not thread-safe. + #Another thread could theoretically cd whilst this is running. + #Most likely this will then just error-out - but there is a possibility we could end up in the wrong directory, or cause the same problems in the other thread. + #REVIEW - consider looking at current directory only at the beginning and do a single cd to an absolute path. + #currently this allows ./ subdir subdir2 nonexistant and we cd to subdir/subdir2 even though an error is produced at the end. + #This offers a convenience for repl useage at the slight cost of more potential cross-thread cd interference + #- although presumably most library code shouldn't be changing CWD anyway. + #Ideally the user/repl should be in control of the processes working directory and we shouldn't have to worry too much here. + #Notably for example tcltest-2.5.5 at least uses cd - so this is something that may be best run in a separate process (for each test suite?) + #This seems unfortunate - as a multithreaded set of test runs might otherwise have made some sense.. but perhaps for tests more serious isolation is a good idea. + #It also seems common to cd when loading certain packages e.g tls from starkit. + #While in most/normal cases the library will cd back to the remembered working directory after only a brief time - there seem to be many opportunities for issues + #if the repl is used to launch/run a number of things in the one process + proc d/ {args} { + variable VIRTUAL_CWD + + set is_win [expr {"windows" eq $::tcl_platform(platform)}] + + set repl_runid 0 + if {[info commands ::punk::get_repl_runid] ne ""} { + set repl_runid [punk::get_repl_runid] + } + #set ::punk::last_run_display [list] + + if {([llength $args]) && ([lindex $args 0] eq "")} { + set args [lrange $args 1 end] + } + + + if {![llength $args]} { + #ls is too slow even over a fairly low-latency network + #set out [runout -n ls -aFC] + if {[string match //zipfs:/* $VIRTUAL_CWD]} { + if {[Zipfs_path_within_zipfs_mounts $VIRTUAL_CWD]} { + if {[pwd] ne $VIRTUAL_CWD} { + commandstack::basecall cd $VIRTUAL_CWD + } + } + set matchinfo [dirfiles_dict -searchbase $VIRTUAL_CWD] + } else { + if {[pwd] ne $VIRTUAL_CWD} { + commandstack::basecall cd $VIRTUAL_CWD + } + set matchinfo [dirfiles_dict -searchbase [pwd]] + } + set dircount [llength [dict get $matchinfo dirs]] + set filecount [llength [dict get $matchinfo files]] + set symlinkcount [llength [dict get $matchinfo links]] ;#doesn't include windows shelllinks (.lnk) + #set location [file normalize [dict get $matchinfo location]] + set location [dict get $matchinfo location] + + + #result for glob is count of matches - use dirfiles etc for script access to results + set result [list location $location dircount $dircount filecount $filecount symlinks $symlinkcount] + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + lappend result filebytes [punk::lib::format_number $filebytes] + } + if {[punk::nav::fs::system::codethread_is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + #if ansi is off - punk::console::titleset will try 'local' api method - which can fail + catch {::punk::console::titleset [lrange $result 1 end]} + } + } + if {[string match //zipfs:/* $location]} { + set stripbase 0 + } else { + set stripbase 1 + } + set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo] + set chunklist [list] + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + if {[file normalize $VIRTUAL_CWD] ne [pwd]} { + lappend chunklist [list stderr "[punk::ansi::a+ red]PWD:[pwd] VIRTUAL_CWD:$VIRTUAL_CWD[punk::ansi::a]"] + } + lappend chunklist [list result $result] + if {$repl_runid != 0} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + } else { + punk::nav::fs::system::emit_chunklist $chunklist + } + #puts stdout "-->[ansistring VIEW $result]" + return $result + } else { + set atail [lassign $args a1] + if {[llength $args] == 1} { + set a1 [lindex $args 0] + switch -exact -- $a1 { + . - ./ { + tailcall punk::nav::fs::d/ + } + .. - ../ { + if {$VIRTUAL_CWD eq "//zipfs:/" && ![string match //zipfs:/* [pwd]]} { + #exit back to last nonzipfs path that was in use + set VIRTUAL_CWD [pwd] + tailcall punk::nav::fs::d/ + } + + #we need to use normjoin to allow navigation to //server instead of just to //server/share (//server browsing unimplemented - review) + # [file join //server ..] would become /server/.. - use normjoin to get //server + # file dirname //server/share would stay as //server/share + #set up1 [file dirname $VIRTUAL_CWD] + set up1 [punk::path::normjoin $VIRTUAL_CWD ..] + if {[string match //zipfs:/* $up1]} { + if {[Zipfs_path_within_zipfs_mounts $up1]} { + cd $up1 + set VIRTUAL_CWD $up1 + } else { + set VIRTUAL_CWD $up1 + } + } else { + cd $up1 + #set VIRTUAL_CWD [file normalize $a1] + } + tailcall punk::nav::fs::d/ + } + } + + if {[file pathtype $a1] ne "relative"} { + if { ![string match //zipfs:/* $a1]} { + if {[file type $a1] eq "directory"} { + cd $a1 + #set VIRTUAL_CWD $a1 + tailcall punk::nav::fs::d/ + } + } + } + + + if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} { + if {[file type $a1] eq "directory"} { + cd $a1 + #set VIRTUAL_CWD [file normalize $a1] + tailcall punk::nav::fs::d/ + } + } + + if {![regexp {[*?]} $a1]} { + #NON-Glob target + #review + if {[string match //zipfs:/* $a1]} { + if {[Zipfs_path_within_zipfs_mounts $a1]} { + commandstack::basecall cd $a1 + } + set VIRTUAL_CWD $a1 + set curdir $a1 + } else { + set target [punk::path::normjoin $VIRTUAL_CWD $a1] + if {[string match //zipfs:/* $VIRTUAL_CWD]} { + if {[Zipfs_path_within_zipfs_mounts $target]} { + commandstack::basecall cd $target + } + } + if {[file type $target] eq "directory"} { + set VIRTUAL_CWD $target + } + } + tailcall punk::nav::fs::d/ + } + set curdir $VIRTUAL_CWD + } else { + set curdir [pwd] + } + + + #globchar somewhere in path - treated as literals except in final segment (for now. todo - make more like ns/ which accepts full path globbing with double ** etc.) + + set searchspec [lindex $args 0] + + set result "" + set chunklist [list] + + #Only merge results if location matches previous (caller can deliberately intersperse bogus globs to force split if desired) + #TODO - remove duplicate file or dir items for overlapping patterns in same location!!! (at least for count, filebyte totals if not for display) + set last_location "" + set this_result [dict create] + foreach searchspec $args { + set path [path_to_absolute $searchspec $curdir $::tcl_platform(platform)] + set has_tailglob [expr {[regexp {[?*]} [file tail $path]]}] + #we have already done a 'cd' if only one unglobbed path was supplied - therefore any remaining non-glob tails must be tested for folderness vs fileness to see what they mean + #this may be slightly surprising if user tries to exactly match both a directory name and a file both as single objects; because the dir will be listed (auto /* applied to it) - but is consistent enough. + #lower level dirfiles or dirfiles_dict can be used to more precisely craft searches. ( d/ will treat dir the same as dir/*) + + set searchspec_relative [expr {[file pathtype $searchspec] eq "relative"}] + if {$has_tailglob} { + set location [file dirname $path] + set glob [file tail $path] + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase [file dirname $searchspec] + } + } else { + if {[string match //zipfs:/* $path]} { + set location $path + set glob * + set searchbase $path + } elseif {[file isdirectory $path]} { + set location $path + set glob * + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase $path + } + } else { + set location [file dirname $path] + set glob [file tail $path] ;#search for exact match file + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase [file dirname $path] + } + } + } + set matchinfo [dirfiles_dict -searchbase $searchbase -tailglob $glob $location] + #puts stderr "=--->$matchinfo" + + + set location [file normalize [dict get $matchinfo location]] + if {[string match //xzipfs:/* $location] || $location ne $last_location} { + #REVIEW - zipfs test disabled with leading x + #emit previous result + if {[dict size $this_result]} { + dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] + lappend chunklist [list result $this_result] + if {$result ne ""} { + append result \n + } + append result $this_result + } + set this_result [dict create] + set dircount 0 + set filecount 0 + } + incr dircount [llength [dict get $matchinfo dirs]] + incr filecount [llength [dict get $matchinfo files]] + + #result for glob is count of matches - use dirfiles etc for script access to results + dict set this_result location $location + dict set this_result dircount $dircount + dict set this_result filecount $filecount + + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + dict incr this_result filebytes $filebytes + } else { + dict incr this_result filebytes 0 ;#ensure key exists! + } + dict lappend this_result pattern [dict get $matchinfo opts -glob] + + if {[string match //zipfs:/* $location]} { + set stripbase 0 + } else { + set stripbase 1 + } + set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo] + + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + + set last_location $location + } + #process final result + if {[dict size $this_result]} { + dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] + lappend chunklist [list result $this_result] + if {$result ne ""} { + append result \n + } + append result $this_result + } + if {[file normalize $VIRTUAL_CWD] ne [pwd]} { + lappend chunklist [list stderr "[punk::ansi::a+ red]PWD:[pwd] VIRTUAL_CWD:$VIRTUAL_CWD[punk::ansi::a]"] + } + + + if {[punk::nav::fs::system::codethread_is_running]} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + } + if {$repl_runid == 0} { + punk::nav::fs::system::emit_chunklist $chunklist + } + return $result + } + } + + proc dd/ {args} { + #set ::punk::last_run_display [list] + set repl_runid 0 + if {[info commands ::punk::get_repl_runid] ne ""} { + set repl_runid [punk::get_repl_runid] + } + if {![llength $args]} { + set path .. + } else { + set path ../[file join {*}$args] + } + set normpath [file normalize $path] + cd $normpath + set matchinfo [dirfiles_dict -searchbase $normpath $normpath] + set dircount [llength [dict get $matchinfo dirs]] + set filecount [llength [dict get $matchinfo files]] + set location [file normalize [dict get $matchinfo location]] + #result for glob is count of matches - use dirfiles etc for script access to results + set result [list location $location dircount $dircount filecount $filecount] + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + lappend result filebytes [punk::lib::format_number $filebytes] + } + + set out [dirfiles_dict_as_lines -stripbase 1 $matchinfo] + #return $out\n[pwd] + set chunklist [list] + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + lappend chunklist [list result $result] + + if {[punk::nav::fs::system::codethread_is_running]} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + if {[llength [info commands ::punk::console::titleset]]} { + catch {::punk::console::titleset [lrange $result 1 end]} ;#strip location key + } + } + if {$repl_runid == 0} { + punk::nav::fs::system::emit_chunklist $chunklist + } + return $result + } + + proc d/new {args} { + if {![llength $args]} { + error "usage: d/new

\[ ...\]" + } + set a1 [lindex $args 0] + set curdir [pwd] + set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)] + set fullpath [file join $path1 {*}[lrange $args 1 end]] + + if {[file exists $fullpath]} { + error "Folder $fullpath already exists" + } + file mkdir $fullpath + d/ $fullpath + } + + #todo use unknown to allow d/~c:/etc ?? + proc d/~ {args} { + set home $::env(HOME) + set target [file join $home {*}$args] + if {![file isdirectory $target]} { + error "Folder $target not found" + } + d/ $target + } + + + #run a file + proc x/ {args} { + if {![llength $args]} { + set result [d/] + append result \n "x/ ?args?" + return $result + } + set curdir [pwd] + #todo - allow wish for those who want it.. but in punk we try to use tclsh or a kit and load Tk as a library + set scriptconfig [dict create\ + tcl [list exe tclsh extensions [list ".tcl" ".tm" ".tk" ".kit"]]\ + python [list exe python extensions [list ".py"]]\ + lua [list exe lua extensions [list ".lua"]]\ + perl [list exe perl extensions [list ".pl"]]\ + php [list exe php extensions [list ".php"]]\ + ] + set tcl_extensions [list ".tcl" ".tm" ".kit" ".tk"] ;#todo - load from config + set py_extensions [list ".py"] + set lua_extensions [list ".lua"] + set perl_extensions [list ".pl"] + + set script_extensions [list] + set extension_lookup [dict create] + tcl::dict::for {lang langinfo} $scriptconfig { + set extensions [dict get $langinfo extensions] + lappend script_extensions {*}$extensions + foreach e $extensions { + dict set extension_lookup $e $lang ;#provide reverse lookup + } + } + + #some executables (e.g tcl) can use arguments prior to the script + #use first entry on commandline for which a file exists *and has a script extension - or is executable* as the script to run + #we can't always just assume that first existant file on commandline is the one being run, as it might be config file + #e.g php -c php.ini -f script.php + set scriptlang "" + set scriptfile "" + foreach a $args { + set ext [file extension $a] + if {$ext in $script_extensions && [file exists $a]} { + set scriptlang [dict get $extension_lookup $ext] + set scriptfile $a + break + } + } + puts "scriptlang: $scriptlang scriptfile:$scriptfile" + + #todo - allow sh scripts with no extension ... look at shebang etc? + if {$scriptfile ne "" && $scriptlang ne ""} { + set path [path_to_absolute $scriptfile $curdir $::tcl_platform(platform)] + if {[file type $path] eq "file"} { + set ext [file extension $path] + set extlower [string tolower $ext] + if {$extlower in $tcl_extensions} { + set newargs [lrange $args 1 end] ;#todo - fix to allow script in position other than first + set ::argv0 $path + set ::argc [llength $newargs] + set ::argv $newargs + tailcall source $path + } elseif {$extlower in $py_extensions} { + set pycmd [auto_execok python] + tailcall {*}$pycmd {*}$args + } elseif {$extlower in $script_extensions} { + set exename [dict get $scriptconfig $scriptlang exe] + set cmd [auto_execok $exename] + tailcall {*}$cmd $args + } else { + set fd [open $path r] + set chunk [read $fd 4000]; close $fd + #consider any commented line near top of file containing 'tcl' as likely to be a tcl script of some sort and attempt to source it. + set toplines [split $chunk \n] + set tcl_indicator 0 + foreach ln $toplines { + set ln [string trim $ln] + if {[string match "#*tcl*" $ln]} { + set tcl_indicator 1 + break + } + } + if {$tcl_indicator} { + set newargs [lrange $args 1 end] ;#todo - fix to allow script in position other than first. + set ::argv0 $path + set ::argc [llength $newargs] + set ::argv $newargs + tailcall source $path + } + puts stderr "Cannot run [file extension $path] file directly ([file tail $path]) as tcl script. Ensure file has a known tcl extension ($tcl_extensions) or add a commented hint in the file such as #!/usr/bin/env tclsh" + return [pwd] + } + } + } else { + puts stderr "No script executable known for this" + } + + } + + + proc dirlist {{location ""}} { + set contents [dirfiles_dict $location] + return [dirfiles_dict_as_lines -stripbase 1 $contents] + } + + + #dirfiles dirfiles_dict always deliberately return absolute *unnormalized* path + #e.g when cwd is c:/repo/jn/punk dirfiles ../../ will return something like: + # c:/repo/jn/punk/../../blah + #dirfiles assumes we don't have glob chars in the filenames or paths - dirfiles_dict can be called directly with explicit -tailglob in the rare case that assumption doesn't hold + # dirfiles will test last segment (tail) of supplied searchspecs for fileness vs folderness (when no globchars present in tail) so that it can pass the appropriate flags downstream + proc dirfiles {args} { + set argspecs { + -stripbase -default 1 -type boolean + -formatsizes -default 1 -type boolean -help "Format file size numbers for clarity" + *values -min 0 -max -1 + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts values_dict + + set opt_stripbase [dict get $opts -stripbase] + set opt_formatsizes [dict get $opts -formatsizes] + + #todo - support multiple searchspecs - dirfiles_dict should merge results when same folder + set searchspec "" + dict for {_index val} $values_dict { + set searchspec $val + break + } + + set relativepath [expr {[file pathtype $searchspec] eq "relative"}] + set has_tailglobs [regexp {[?*]} [file tail $searchspec]] + + #dirfiles_dict would handle simple cases of globs within paths anyway - but we need to explicitly set tailglob here in all branches so that next level doesn't need to do file vs dir checks to determine user intent. + #(dir-listing vs file-info when no glob-chars present is inherently ambiguous so we test file vs dir to make an assumption - more explicit control via -tailglob can be done manually with dirfiles_dict) + if {$relativepath} { + set searchbase [pwd] + if {!$has_tailglobs} { + if {[file isdirectory [file join $searchbase $searchspec]]} { + set location [file join $searchbase $searchspec] + set tailglob * + } else { + set location [file dirname [file join $searchbase $searchspec]] + set tailglob [file tail $searchspec] ;#use exact match as a glob - will retrieve size,attributes etc. + } + } else { + #tailglobs exist - and we operate under assumption globchars aren't present in file/folder names - so no folderness/fileness check needed. + set location [file dirname [file join $searchbase $searchspec]] + set tailglob [file tail $searchspec] + } + } else { + #for absolute paths - searchbase AND location will change depending on globiness of tail and fileness vs folderness + if {!$has_tailglobs} { + if {[file isdirectory $searchspec]} { + set searchbase $searchspec + set location $searchspec + set tailglob * + } else { + set searchbase [file dirname $searchspec] + set location [file dirname $searchspec] + set tailglob [file tail $searchspec] ;#literal glob for single file - retrieves properties + } + } else { + set searchbase [file dirname $searchspec] + set location [file dirname $searchspec] + set tailglob [file tail $searchspec] + } + } + puts "--> -searchbase:$searchbase searchspec:$searchspec -tailglob:$tailglob location:$location" + set contents [dirfiles_dict -searchbase $searchbase -tailglob $tailglob $location] + return [dirfiles_dict_as_lines -stripbase $opt_stripbase -formatsizes $opt_formatsizes $contents] + } + + #todo - package as punk::nav::fs + #todo - in thread + #todo - streaming version + #glob patterns in path prior to final segment should already be resolved before using dirfiles_dict - as the underlying filesystem mechanisms can't do nested globbing themselves. + #dirfiles_dict will assume the path up to the final segment is literal even if globchars are included therein. + #final segment globs will be recognised only if -tailglob is passed as empty string + #if -tailglob not supplied and last segment has globchars - presume searchspec parendir is the container and last segment is globbing within that. + #if -tailglob not supplied and last segment has no globchars - presume searchspec is a container(directory) and use glob * + #caller should use parentdir as location and set tailglob to search-pattern or exact match if location is intended to match a file rather than a directory + #examples: + # somewhere/files = search is effectively somewhere/files/* (location somewhere/files glob is *) + # somewhere/files/* = (as above) + # -tailglob * somewhere/files = (as above) + # + # -tailglob "" somewhere/files = search somewhere folder for exactly 'files' (location somewhere glob is files) + # -tailglob files somewhere = (as above) + # + # somewhere/f* = search somewhere folder for f* (location somewhere glob is f*) + # -tailglob f* somewhere = (as above) + # + # This somewhat clumsy API is so that simple searches can be made in a default sensible manner without requiring extra -tailglob argument for the common cases - with lack of trailing glob segment indicating a directory listing + # - but we need to distinguish somewhere/files as a search of that folder vs somewhere/files as a search for exactly 'files' within somewhere, hence the -tailglob option to fine-tune. + # - this also in theory allows file/directory names to contain glob chars - although this is probably unlikely and/or unwise and not likely to be usable on all platforms. + # + #if caller supplies a tailglob as empty string - presume the caller hasn't set location to parentdir - and that last element is the search pattern. + # -searchbase is always passed through - and is only used to construct a location path if a relative searchspec was supplied + proc dirfiles_dict {args} { + set argspecs { + *opts -any 0 + -searchbase -default "" + -tailglob -default "\uFFFF" + #with_sizes & with_times must accept 0|1|f|d|l where f = files d = dirs l = links (punk::du) + -with_sizes -default "\uFFFF" -type string + -with_times -default "\uFFFF" -type string + *values -min 0 -max -1 -type string + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + + #puts stderr "searchspecs: $searchspecs [llength $searchspecs]" + #puts stdout "arglist: $opts" + + if {[llength $searchspecs] > 1} { + #review - spaced paths ? + error "dirfiles_dict: multiple listing not *yet* supported" + } + set searchspec [lindex $searchspecs 0] + # -- --- --- --- --- --- --- + set opt_searchbase [dict get $opts -searchbase] + set opt_tailglob [dict get $opts -tailglob] + set opt_with_sizes [dict get $opts -with_sizes] + set opt_with_times [dict get $opts -with_times] + # -- --- --- --- --- --- --- + + #we don't want to normalize.. + #for example if the user supplies ../ we want to see ../result + + set is_relativesearchspec [expr {[file pathtype $searchspec] eq "relative"}] + if {$opt_searchbase eq ""} { + set searchbase . + } else { + set searchbase $opt_searchbase + } + + + switch -- $opt_tailglob { + "" { + if {$searchspec eq ""} { + set location + } else { + if {$is_relativesarchspec} { + #set location [file dirname [file join $opt_searchbase $searchspec]] + set location [punk::path::normjoin $searchbase $searchspec ..] + } else { + set location [punk::path::normjoin $searchspec ..] + } + #here tail is treated as a search-pattern within location whether or not it contains glob chars "?" or "*" + set match_contents [file tail $searchspec] + } + } + "\uFFFF" { + set searchtail_has_globs [regexp {[*?]} [file tail $searchspec]] + if {$searchtail_has_globs} { + if {$is_relativesearchspec} { + #set location [file dirname [file join $searchbase $searchspec]] + #e.g subdir/* or sub/etc/x* + set location [punk::path::normjoin $searchbase $searchspec ..] + } else { + set location [punk::path::normjoin $searchspec ..] + } + set match_contents [file tail $searchspec] + } else { + #user didn't supply a glob within tail segment, nor did they specify a separate -tailglob - presume they want a directory listing + if {$searchspec eq ""} { + set location $searchbase + } else { + if {$is_relativesearchspec} { + #set location [file join $searchbase $searchspec] + set location [punk::path::normjoin $searchbase $searchspec] + } else { + #absolute path for search + set location $searchspec + } + } + set match_contents * + } + } + default { + #-tailglob supplied separately - ignore any globiness in tail segment of searchspec and treat literally + if {$searchspec eq ""} { + set location $searchbase + } else { + if {$is_relativesearchspec} { + #set location [file join $searchbase $searchspec] + set location [punk::path::normjoin $searchbase $searchspec] + } else { + set location $searchspec + } + } + set match_contents $opt_tailglob + } + } + puts stdout "searchbase: $searchbase searchspec:$searchspec" + + set in_vfs 0 + if {[llength [package provide vfs]]} { + foreach mount [vfs::filesystem info] { + if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} { + set in_vfs 1 + break + } + } + } + + if {$opt_with_sizes eq "\uFFFF"} { + #leave up to listing-provider defaults + set next_opt_with_sizes "" + } else { + set next_opt_with_sizes [list -with_sizes $opt_with_sizes] + } + if {$opt_with_times eq "\uFFFF"} { + #leave up to listing-provider defaults + set next_opt_with_times "" + } else { + set next_opt_with_times [list -with_times $opt_with_times] + } + if {$in_vfs} { + set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } else { + set in_zipfs 0 + if {[info commands ::tcl::zipfs::mount] ne ""} { + if {[string match //zipfs:/* $location]} { + set in_zipfs 1 + } + #dict for {zmount zpath} [zipfs mount] { + # if {[punk::mix::base::lib::path_a_atorbelow_b $location $zmount]} { + # set in_zipfs 1 + # break + # } + #} + } + if {$in_zipfs} { + #relative vs absolute? review - cwd valid for //zipfs:/ ?? + set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } else { + set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } + } + + set dirs [dict get $listing dirs] + set files [dict get $listing files] + set filesizes [dict get $listing filesizes] + set vfsmounts [dict get $listing vfsmounts] + set flaggedhidden [dict get $listing flaggedhidden] + + + set nonportable [list] ;#illegal file/folder names for windows e.g trailing dot or trailing space - can still be read if //?/ AND shortname used + set underlayfiles [list] + set underlayfilesizes [list] + if {[llength $vfsmounts]} { + foreach vfsmount $vfsmounts { + if {[set fposn [lsearch $files $vfsmount]] >= 0} { + lappend underlayfiles [lindex $files $fposn] + set files [lreplace $files $fposn $fposn] + #for any change to files list must change filesizes too if list exists + if {[llength $filesizes]} { + lappend underlayfilesizes [lindex $filesizes $fposn] + set filesizes [lreplace $filesizes $fposn $fposn] + } + lappend dirs $vfsmount + } elseif {$vfsmount in $dirs} { + #either dirlisting mech was aware of vfs.. or mountpoint is overlaying an underlying folder + #for now - do nothing + #todo - review. way to query dirlisting mech to see if we are hiding a folder? + + } else { + #vfs mount but dirlisting mechanism didn't detect as file or folder + lappend dirs $vfsmount + } + } + } + + + #NOTE: -types {hidden d} * may return . & .. on unix platforms - but will not show them on windows. + #A mounted vfs exe (e.g sometclkit.exe) may be returned by -types {hidden d} on windows - but at the same time has "-hidden 0" in the result of file attr. + + #non-unix platforms may have attributes to indicate hidden status even if filename doesn't have leading dot. + #mac & windows have these + #windows doesn't consider dotfiles as hidden - mac does (?) + #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden + if {$::tcl_platform(platform) ne "windows"} { + lappend flaggedhidden {*}[lsearch -all -inline [concat $dirs $files] ".*"] + #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs + #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely + #set flaggedhidden [lsort -unique $flaggedhidden] + set flaggedhidden [punk::lib::lunique_unordered $flaggedhidden] + } + + set dirs [lsort $dirs] ;#todo - natsort + + + + #foreach d $dirs { + # if {[lindex [file system $d] 0] eq "tclvfs"} { + # lappend vfs $d [file system $d] + # } + #} + + #glob -types {hidden} will not always return the combination of glob -types {hidden f} && -types {hidden d} (on windows anyway) + + # -- --- + #can't lsort files without lsorting filesizes + #Note - the sort by index would convert an empty filesizes list to a list of empty strings - one for each entry in files + #We want to preserve the empty list if that's what the dirlisting mechanism returned (presumably because -with_sizes was 0 or explicitly excluded files) + if {[llength $filesizes] == 0} { + set sorted_files [lsort $files] + set sorted_filesizes [list] + } else { + set sortorder [lsort -indices $files] + set sorted_files [list] + set sorted_filesizes [list] + foreach i $sortorder { + lappend sorted_files [lindex $files $i] + lappend sorted_filesizes [lindex $filesizes $i] + } + } + + set files $sorted_files + set filesizes $sorted_filesizes + # -- --- + + + foreach nm [concat $dirs $files] { + if {[punk::winpath::illegalname_test $nm]} { + lappend nonportable $nm + } + } + set front_of_dict [dict create location $location searchbase $opt_searchbase] + set listing [dict merge $front_of_dict $listing] + + set updated [dict create dirs $dirs files $files filesizes $filesizes nonportable $nonportable flaggedhidden $flaggedhidden underlayfiles $underlayfiles underlayfilesizes $underlayfilesizes] + return [dict merge $listing $updated] + } + + #todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing? + proc dirfiles_dict_as_lines {args} { + package require overtype + + set argspecs { + -stripbase -default 0 -type boolean + -formatsizes -default 1 -type boolean + *values -min 1 -max -1 -type dict + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set list_of_dicts [dict values $vals] + + + + # -- --- --- --- --- --- --- --- --- --- --- --- + set opt_stripbase [dict get $opts -stripbase] + set opt_formatsizes [dict get $opts -formatsizes] + # -- --- --- --- --- --- --- --- --- --- --- --- + + #if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied + set common_base "" + set searchbases [list] + set searchbases_with_len [list] + if {$opt_stripbase} { + #todo - case-insensitive comparisons on platforms where that is appropriate (e.g windows) + # - note that the OS could be configured differently in this regard than the default (as could a filesystem such as ZFS), and that for example mounted SMB filesystems are likely to be configured to support the general windows client idea of case-preserving-but-case-insensitive. + # - we may be able to provide a reasonable default for windows vs other - but a proc option is likely needed to allow caller to override the default behaviour on a call by call basis, + # and a config option may be desirable for the user to override the platform default. + # The chosen defaults based on platform may be generally ok - but then not give the desired behaviour when accessing a particular filesystem/mount + if {$::tcl_platform(platform) eq "windows"} { + #case-preserving but case-insensitive matching is the default + foreach d $list_of_dicts { + set str [string tolower [string trim [dict get $d searchbase]]] + lappend searchbases $str + lappend searchbases_with_len [list $str [string length $str]] + } + } else { + #case sensitive + foreach d $list_of_dicts { + set str [string trim [dict get $d searchbase]] + lappend searchbases $str + lappend searchbases_with_len [list $str [string length $str]] + } + } + #if any of the searchbases is empty - there will be no common base - so leave common_base as empty string. + if {"" ni $searchbases} { + set shortest_to_longest [lsort -index 1 -integer $searchbases_with_len] + set prefix_test_list [tcl::prefix all $searchbases [lindex $shortest_to_longest 0 0]] + #if shortest doesn't match all searchbases - we have no common base + if {[llength $prefix_test_list] == [llength $searchbases]} { + set common_base [lindex $shortest_to_longest 0 0]; #we + } + } + } + + foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] { + set $fileset [list] + } + + #set contents [lindex $list_of_dicts 0] + foreach contents $list_of_dicts { + lappend dirs {*}[dict get $contents dirs] + lappend files {*}[dict get $contents files] + lappend links {*}[dict get $contents links] + lappend filesizes {*}[dict get $contents filesizes] + lappend underlayfiles {*}[dict get $contents underlayfiles] + lappend underlayfilesizes {*}[dict get $contents underlayfilesizes] + lappend flaggedhidden {*}[dict get $contents flaggedhidden] + lappend flaggedreadonly {*}[dict get $contents flaggedreadonly] + lappend flaggedsystem {*}[dict get $contents flaggedsystem] + lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective + lappend vfsmounts {*}[dict get $contents vfsmounts] + } + + if {$opt_stripbase && $common_base ne ""} { + set filetails [list] + set dirtails [list] + foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] { + set stripped [list] + foreach f [set $fileset] { + lappend stripped [strip_prefix_depth $f $common_base] + } + set $fileset $stripped + } + #Note: we need to remember to use common_base to rebuild the key when we need to query the dict-based elements: sizes & times - because we didn't strip those keys. + } + + # -- --- --- --- --- --- --- --- --- --- --- + #assign symlinks to the dirs or files collection (the punk::du system doesn't sort this out + #As at 2024-09 for windows symlinks - Tcl can't do file readlink on symlinks created with mklink /D name target (SYMLINKD) or mklink name target (SYMLINK) + #We can't read the target information - best we can do is classify it as a file or a dir + #we can't use 'file type' as that will report just 'link' - but file isfile and file isdirectory work and should work for links on all platforms - REVIEW + set file_symlinks [list] + set dir_symlinks [list] + set dir_shortcuts [list] ;#windows shell links (.lnk) that have a target that is a directory + foreach s $links { + if {[file isfile $s]} { + lappend file_symlinks $s + #will be appended in finfo_plus later + } elseif {[file isdirectory $s]} { + lappend dir_symlinks $s + lappend dirs $s + } else { + #dunno - warn for now + puts stderr "Warning - cannot determine link type for link $s" + } + } + #we now have the issue that our symlinks aren't sorted within the dir/file categorisation - they currently will have to appear at beginning or end - TODO + # -- --- --- --- --- --- --- --- --- --- --- + + + #todo - sort whilst maintaining order for metadata? + #we need to co-sort files only with filesizes (other info such as times is keyed on fname so cosorting not required) + + + #we can't sort on filesize after format_number (unless we were to enforce delim _ which we don't want to do) + if {$opt_formatsizes} { + set filesizes [punk::lib::format_number $filesizes] ;#accepts a list and will process each + } + #col2 with subcolumns + + #remove punk::pipedata dependency - allow use of punk::nav::fs without punk package + #set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + #widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max + set widest2a [tcl::mathfunc::max {*}[lmap v [list {*}$files {*}$file_symlinks ""] {string length $v}]] + + set c2a [string repeat " " [expr {$widest2a + 1}]] + #set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + set widest2b [tcl::mathfunc::max {*}[lmap v [list {*}$filesizes ""] {string length $v}]] + set c2b [string repeat " " [expr {$widest2b + 1}]] + set finfo [list] + foreach f $files s $filesizes { + #note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces + #hence we need to keep the filename as well, properly protected as a list element + lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"] + } + set flink_style [punk::ansi::a+ undercurly underline undt-green] ;#curly green underline with fallback to normal underline + set dlink_style [punk::ansi::a+ undercurly underline undt-green] + #We use an underline so the visual styling of a link can coexist with fg/bg colors applied for other attributes such as hidden + foreach flink $file_symlinks { + lappend finfo [list file $flink display "$flink_style[overtype::left $c2a $flink] [overtype::right $c2b 0]"] + } + + set fshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink] + set dshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink] + #examine windows .lnk shell link files (shortcuts) - these could be encountered on other platforms too - we should still be able to read them + #review - symlink to shortcut? hopefully will just work + #classify as file or directory - fallback to file if unknown/undeterminable + set finfo_plus [list] + foreach fdict $finfo { + set fname [dict get $fdict file] + if {[file extension $fname] eq ".lnk"} { + if {![catch {package require punk::winlnk}]} { + set shortcutinfo [punk::winlnk::file_get_info $fname] + set target_type "file" ;#default/fallback + if {[dict exists $shortcutinfo link_target]} { + set tgt [dict get $shortcutinfo link_target] + if {[file exists $tgt]} { + #file type could return 'link' - we will use ifile/isdirectory + if {[file isfile $tgt]} { + set target_type file + } elseif {[file isdirectory $tgt]} { + set target_type directory + } else { + set target_type file ;## ? + } + } else { + #todo - see if punk::winlnk has info about the type at the time of linking + #for now - treat as file + } + } + switch -- $target_type { + file { + set display [dict get $fdict display] + set display $fshortcut_style$display ;# + dict set fdict display $display + lappend finfo_plus $fdict + } + directory { + #target of link is a dir - for display/categorisation purposes we want to see it as a dir + #will be styled later based on membership of dir_shortcuts + lappend dirs $fname + lappend dir_shortcuts $fname + } + } + } + #if we don't have punk::winlnk to read the .lnk - it will get no special highlighting and just appear as an ordinary file even if it points to a dir + } else { + lappend finfo_plus $fdict + } + } + unset finfo + + + + #set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]] + + set displaylist [list] + set col1 [string repeat " " [expr {$widest1 + 2}]] + set RST [punk::ansi::a] + foreach d $dirs filerec $finfo_plus { + set d1 [punk::ansi::a+ cyan bold] + set d2 [punk::ansi::a+ defaultfg defaultbg normal] + #set f1 [punk::ansi::a+ white bold] + set f1 [punk::ansi::a+ white] + set f2 [punk::ansi::a+ defaultfg defaultbg normal] + set fdisp "" + if {[string length $d]} { + if {$d in $flaggedhidden} { + set d1 [punk::ansi::a+ cyan normal] + } + if {$d in $vfsmounts} { + if {$d in $flaggedhidden} { + #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW + #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows) + #mark it differently for now.. (todo bug report?) + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red Yellow bold] + } else { + set d1 [punk::ansi::a+ green Purple bold] + } + } else { + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red White bold] + } else { + set d1 [punk::ansi::a+ green bold] + } + } + } else { + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red bold] + } + } + #dlink-style & dshortcut_style are for underlines - can be added with colours already set + if {$d in $dir_symlinks} { + append d1 $dlink_style + } elseif {$d in $dir_shortcuts} { + append d1 $dshortcut_style + } + } + if {[llength $filerec]} { + set fname [dict get $filerec file] + set fdisp [dict get $filerec display] + if {$fname in $flaggedhidden} { + set f1 [punk::ansi::a+ Purple] + } else { + if {$fname in $nonportable} { + set f1 [punk::ansi::a+ red bold] + } + } + } + lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST + } + + return [punk::lib::list_as_lines $displaylist] + } + + #pass in base and platform to head towards purity/testability. + #this function can probably never be pure in such a simple form - as it needs to read state from the os storage system configuration + #consider haskells approach of well-typed paths for cross-platform paths: https://hackage.haskell.org/package/path + #review: punk::winpath calls cygpath! + #review: file pathtype is platform dependant + proc path_to_absolute {path base platform} { + set ptype [file pathtype $path] + if {$ptype eq "absolute"} { + set path_absolute $path + } elseif {$ptype eq "volumerelative"} { + if {$platform eq "windows"} { + #unix looking paths like /c/users or /usr/local/etc are reported by tcl as volumerelative.. (as opposed to absolute on unix platforms) + if {[string index $path 0] eq "/"} { + #this conversion should be an option for the ./ command - not built in as a default way of handling volumerelative paths here + #It is more useful on windows to treat /usr/local as a wsl or mingw path - and may be reasonable for ./ - but is likely to surprise if put into utility functions. + #Todo - tidy up. + package require punk::unixywindows + set path_absolute [punk::unixywindows::towinpath $path] + #puts stderr "winpath: $path" + } else { + #todo handle volume-relative paths with volume specified c:etc c: + #note - tcl doesn't handle this properly anyway.. the win32 api should 'remember' the per-volume cwd + #not clear whether tcl can/will fix this - but it means these paths are dangerous. + #The cwd of the process can get out of sync with what tcl thinks is the working directory when you swap drives + #Arguably if ...? + + #set path_absolute $base/$path + set path_absolute $path + } + } else { + # unknown what paths are reported as this on other platforms.. treat as absolute for now + set path_absolute $path + } + } else { + set path_absolute $base/$path + } + if {$platform eq "windows"} { + if {[punk::winpath::illegalname_test $path_absolute]} { + set path_absolute [punk::winpath::illegalname_fix $path_absolute] ;#add dos-device-prefix protection if not already present + } + } + return $path_absolute + } + proc strip_prefix_depth {path prefix} { + set tail [lrange [file split $path] [llength [file split $prefix]] end] + if {[llength $tail]} { + return [file join {*}$tail] + } else { + return "" + } + } + + #REVIEW - at least one version of Tcl during development couldn't navigate using cd to intermediate paths between the zipfs root and the mountpoint. + #TODO - test if this can still occur. + proc Zipfs_path_within_zipfs_mounts {zipfspath} { + if {![string match //zipfs:/* $zipfspath]} {error "Zipfs_path_within_zipfs_mounts error. Supplied zipfspath $zipfspath must be a //zipfs:/* path"} + set is_within_mount 0 + dict for {zmount zpath} [zipfs mount] { + if {[punk::mix::base::lib::path_a_atorbelow_b $zipfspath $zmount]} { + set is_within_mount 1 + break + } + } + return $is_within_mount + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::nav::fs ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::nav::fs::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::nav::fs::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::nav::fs::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +tcl::namespace::eval punk::nav::fs::system { + #*** !doctools + #[subsection {Namespace punk::nav::fs::system}] + #[para] Internal functions that are not part of the API + + #ordinary emission of chunklist when no repl + proc emit_chunklist {chunklist} { + set result "" + foreach record $chunklist { + lassign $record type data + switch -- $type { + stdout { + puts stdout "$data" + } + stderr { + puts stderr $data + } + result {} + default { + puts stdout "$type $data" + } + } + } + return $result + } + + proc codethread_is_running {} { + if {[info commands ::punk::repl::codethread::is_running] ne ""} { + return [punk::repl::codethread::is_running] + } + return 0 + } + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::nav::fs [tcl::namespace::eval punk::nav::fs { + variable pkg punk::nav::fs + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm new file mode 100644 index 00000000..09b8a0be --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm @@ -0,0 +1,259 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.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) 2024 +# +# @@ Meta Begin +# Application punk::repl::codethread 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::repl::codethread 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::repl::codethread] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::repl::codethread +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::repl::codethread +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::config +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::repl::codethread::class { + #*** !doctools + #[subsection {Namespace punk::repl::codethread::class}] + #[para] class definitions + #if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::repl::codethread { + tcl::namespace::export * + variable replthread + variable replthread_cond + variable running 0 + + variable output_stdout "" + variable output_stderr "" + + #variable xyz + + #*** !doctools + #[subsection {Namespace punk::repl::codethread}] + #[para] Core API functions for punk::repl::codethread + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + proc is_running {} { + variable running + return $running + } + proc runscript {script} { + #puts stderr "->runscript" + variable replthread_cond + variable output_stdout "" + variable output_stderr "" + #expecting to be called from a thread::send in parent repl - ie in the toplevel interp so that the sub-interp "code" is available + #if a thread::send is done from the commandline in a codethread - Tcl will + if {"code" ni [interp children] || ![info exists replthread_cond]} { + #in case someone tries calling from codethread directly - don't do anything or change any state + #(direct caller could create an interp named code at the level "" -> "code" -"code" and add a replthread_cond value to avoid this check - but it probably won't do anything useful) + #if called directly - the context will be within the first 'code' interp. + #inappropriate caller could add superfluous entries to shellfilter stack if function errors out + #inappropriate caller could affect tsv vars (if their interp allows that anyway) + puts stderr "runscript is meant to be called from the parent repl thread via a thread::send to the codethread" + return + } + set outstack [list] + set errstack [list] + upvar ::punk::config::running running_config + if {[string length [dict get $running_config color_stdout_repl]] && [interp eval code punk::console::colour]} { + lappend outstack [interp eval code [list shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout_repl]]]] + } + lappend outstack [interp eval code [list shellfilter::stack::add stdout tee_to_var -settings {-varname ::punk::repl::codethread::output_stdout}]] + + if {[string length [dict get $running_config color_stderr_repl]] && [interp eval code punk::console::colour]} { + lappend errstack [interp eval code [list shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr_repl]]]] + # #lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour cyan]] + } + lappend errstack [interp eval code [list shellfilter::stack::add stderr tee_to_var -settings {-varname ::punk::repl::codethread::output_stderr}]] + + #an experiment + #set errhandle [shellfilter::stack::item_tophandle stderr] + #interp transfer "" $errhandle code + + set scope [interp eval code [list set ::punk::ns::ns_current]] + set status [catch { + interp eval code [list tcl::namespace::inscope $scope $script] + } result] + + + flush stdout + flush stderr + + #interp transfer code $errhandle "" + #flush $errhandle + set lastoutchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stdout]] end] + set lasterrchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stderr]] end] + #puts stderr "-->[ansistring VIEW -lf 1 $lastoutchar$lasterrchar]" + + set tid [thread::id] + tsv::set codethread_$tid info [list lastoutchar $lastoutchar lasterrchar $lasterrchar] + tsv::set codethread_$tid status $status + tsv::set codethread_$tid result $result + tsv::set codethread_$tid errorcode $::errorCode + + + #only remove from shellfilter::stack the items we added to stack in this function + foreach s [lreverse $outstack] { + interp eval code [list shellfilter::stack::remove stdout $s] + } + foreach s [lreverse $errstack] { + interp eval code [list shellfilter::stack::remove stderr $s] + } + thread::cond notify $replthread_cond + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::repl::codethread ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::repl::codethread::lib { + tcl::namespace::export * + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::repl::codethread::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::repl::codethread::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +tcl::namespace::eval punk::repl::codethread::system { + #*** !doctools + #[subsection {Namespace punk::repl::codethread::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::repl::codethread [tcl::namespace::eval punk::repl::codethread { + variable pkg punk::repl::codethread + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/unixywindows-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/unixywindows-0.1.0.tm new file mode 100644 index 00000000..1d0a3957 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/unixywindows-0.1.0.tm @@ -0,0 +1,237 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application punk::unixywindows 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + +#for illegalname_test +package require punk::winpath + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::unixywindows { + #'cached' name to make obvious it could be out of date - and to distinguish from unixyroot arg + variable cachedunixyroot "" + + + #----------------- + #e.g something like c:/Users/geek/scoop/apps/msys2/current c:/msys2 + proc get_unixyroot {} { + variable cachedunixyroot + if {![string length $cachedunixyroot]} { + if {![catch { + set result [exec cygpath -m /] ;# -m gives result with forward-slashes - which is ok for windows paths in a Tcl context. + set cachedunixyroot [punk::objclone $result] + file pathtype $cachedunixyroot; #this call causes the int-rep to be path + #set ::punk::last_run_display [list] ;#hack - review shouldn't really be necessary.. but because we call winpath from ./ - the repl looks for last_run_display + } errM]} { + + } else { + puts stderr "Warning: Failed to determine base for unix-like paths - using default of c:/msys2" + file pathtype [set cachedunixyroot [punk::objclone "c:/msys2"]] + } + } + #will have been shimmered from string to 'path' internal rep by 'file pathtype' call + + #let's return a different copy as it's so easy to lose path-rep + set copy [punk::objclone $cachedunixyroot] + return $copy + } + proc refresh_unixyroot {} { + variable cachedunixyroot + set result [exec cygpath -m /] ;# -m gives result with forward-slashes - which is ok for windows paths in a Tcl context. + set cachedunixyroot [punk::objclone $result] + file pathtype $cachedunixyroot; #this call causes the int-rep to be path + + set copy [punk::objclone $cachedunixyroot] + return $copy + } + proc set_unixyroot {windows_path} { + variable cachedunixyroot + file pathtype $windows_path + set cachedunixyroot [punk::objclone $windows_path] + #return the original - but probably int-rep will have shimmered to path even if started out as string + #- that's probably what we want. Caller can use as string or path without affecting cachedunixyroot + return $windows_path + } + + + proc windir {path} { + if {$path eq "~"} { + #as the tilde hasn't been normalized.. we can't assume we're running on the actual platform + return ~/.. + } + return [file dirname [towinpath $path]] + } + + #REVIEW high-coupling + proc cdwin {path} { + set path [towinpath $path] + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + ::punk::console::titleset $path + } + } + cd $path + } + proc cdwindir {path} { + set path [towinpath $path] + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + ::punk::console::titleset $path + } + } + cd [file dirname $path] + } + + #NOTE - this is an expensive operation - avoid where possible. + #review - is this intended to be useful/callable on non-windows platforms? + #it should in theory be useable from another platform that wants to create a path for use on windows. + #In this case - we shouldn't examine what volumes exist (assume A: .. Z: are valid) + #review zipfs:// other uri schemes? + proc towinpath {unixypath {unixyroot ""}} { + #NOTE: tcl file exists gives different answers on windows for paths like /c depending on cwd (presumably based on file pathtype of volumerelative) + #(Tcl is also somewhat broken as at 2023 as far as volume relative paths - process can get out of sync with tcl if cd to a vol relative path is used) + #This is add odds with attempting to navigate on a windows system which has cygwin, wsl etc... It also makes it difficult for functions intended to operate independent of CWD. + #e.g there is potential confusion when there is a c folder on c: drive (c:/c) + #I will attempt to provide a coherent operation for winpath ./ ../ etc , but it may disallow for example; change to /something or /x where these don't match a driveletter or /mnt + #whereas tcl may allow cd to /something if a something folder happens to exist on the current volume based on cwd. + #I think it's preferable to require an explicit driveletter /x or /mnt when using unix-like paths on windows - but practical considerations may prove me wrong.. + #It's possible that this function should also ignore the current set of driveletters - and operate completely independent of whether a path actually exists + #This makes it hard to use things like 'file normalize' - which also looks at things like current volume. + # + #Note for example the results of 'which' grep on windows can produce a path like /c/Users/somewhere/bin/grep + #which tcl's file normalize may change to C:/c/Users or X:/c/Users - based on current volumen. Given that C:/c might exist - this can be problematic in a couple of ways. + #The mixing of unix-like and windows commands on the same machine is a large part of the problem.. but this mix is now common + # + #convert /c/etc to C:/etc + set re_slash_x_slash {^/([[:alpha:]]){1}/.*} + set re_slash_else {^/([[:alpha:]]*)(.*)} + set volumes [file volumes] + #exclude things like //zipfs:/ ?? + set driveletters [list] + foreach v $volumes { + if {[regexp {^([[:alpha:]]){1}:/$} $v _ letter]} { + lappend driveletters $letter + } + } + #puts stderr "->$driveletters" + + set path [punk::objclone $unixypath] ;#take another copy that we can deliberatley shimmer to path and know is separate to the supplied argument + set supplied_pathtype [file pathtype $path] ;#we don't care about the pathtype so much as the act of making this call shimmers to a path internal-rep + + #copy of var that we can treat as a string without affecting path rep + #Note that some but not all read operations will lose path rep e.g 'string length $x' will lose any path-rep $x had, (but 'string equal $x something' won't) + #Todo - make int-rep tests to check stability of these behaviours across Tcl versions! + set strcopy_path [punk::objclone $path] + + set str_newpath "" + + set have_pathobj 0 + + if {[regexp $re_slash_x_slash $strcopy_path _ letter]} { + #upper case appears to be windows canonical form + set str_newpath [string toupper $letter]:/[string range $strcopy_path 3 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}/.*} [string tolower $strcopy_path] _ letter]} { + set str_newpath [string toupper $letter]:/[string range $strcopy_path 7 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}$} [string tolower $strcopy_path] _ letter]} { + set str_newpath [string toupper $letter]:/ + } elseif {[regexp $re_slash_else $strcopy_path _ firstpart remainder]} { + #could be for example /c or /something/users + if {[string length $firstpart] == 1} { + set letter $firstpart + set str_newpath [string toupper $letter]:/ + } else { + #according to regex we have a single leading slash + set str_tail [string range $strcopy_path 1 end] + if {$unixyroot eq ""} { + set unixyroot [get_unixyroot] + } else { + file pathtype $unixyroot; #side-effect generates int-rep of type path ) + } + set pathobj [file join $unixyroot $str_tail] + file pathtype $pathobj + set have_pathobj 1 + } + } + + if {!$have_pathobj} { + if {$str_newpath eq ""} { + #dunno - pass through + set pathobj $path + } else { + set pathobj [punk::objclone $str_newpath] + file pathtype $pathobj + } + } + + + + #puts stderr "=> $path" + #things like 'which' seem to return a path minus the .exe - so we'll just test the containing folder + # + #By now file normalize shouldn't do too many shannanigans related to cwd.. + #We want it to look at cwd for relative paths.. + #but we don't consider things like /c/Users to be relative even on windows where it would normally mean a volume-relative path e.g c:/c/Users if cwd happens to be somewhere on C: at the time. + #if {![file exists [file dirname $path]]} { + # set path [file normalize $path] + # #may still not exist.. that's ok. + #} + + + + #file normalize may change backslashes to forward slashes.. including things like the special \\?\ prefix which is intended to stop windows api from parsing a name + #2023 - this is ok as //?/ also seems to work.. but it is unclear if that is because Tcl is re-converting to backslashes + if {[punk::winpath::illegalname_test $pathobj]} { + set pathobj [punk::winpath::illegalname_fix $pathobj] + } + + return $pathobj + } + + #---------------------------------------------- + #leave the unixywindows related aliases available on all platforms + #interp alias {} cdwin {} punk::unixywindows::cdwin + #interp alias {} cdwindir {} punk::unixywindoes::cdwindir + #interp alias {} towinpath {} punk::unixywindows::towinpath + #interp alias {} windir {} punk::unixywindows::windir + #---------------------------------------------- + + + +} + + + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::unixywindows [namespace eval punk::unixywindows { + variable version + set version 0.1.0 +}] +return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkapp-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkapp-0.1.tm new file mode 100644 index 00000000..ce46856b --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkapp-0.1.tm @@ -0,0 +1,239 @@ +#utilities for punk apps to call + +package provide punkapp [namespace eval punkapp { + variable version + set version 0.1 +}] + +namespace eval punkapp { + variable result + variable waiting "no" + proc hide_dot_window {} { + #alternative to wm withdraw . + #see https://wiki.tcl-lang.org/page/wm+withdraw + wm geometry . 1x1+0+0 + wm overrideredirect . 1 + wm transient . + } + proc is_toplevel {w} { + if {![llength [info commands winfo]]} { + return 0 + } + expr {[winfo toplevel $w] eq $w && ![catch {$w cget -menu}]} + } + proc get_toplevels {{w .}} { + if {![llength [info commands winfo]]} { + return [list] + } + set list {} + if {[is_toplevel $w]} { + lappend list $w + } + foreach w [winfo children $w] { + lappend list {*}[get_toplevels $w] + } + return $list + } + + proc make_toplevel_next {prefix} { + set top [get_toplevel_next $prefix] + return [toplevel $top] + } + #possible race condition if multiple calls made without actually creating the toplevel, or gap if highest existing closed in the meantime + #todo - reserve_toplevel_next ? keep list of toplevels considered 'allocated' even if never created or already destroyed? what usecase? + #can call wm withdraw to to reserve newly created toplevel. To stop re-use of existing names after destruction would require a list or at least a record of highest created for each prefix + proc get_toplevel_next {prefix} { + set base [string trim $prefix .] ;# .myapp -> myapp .myapp.somewindow -> myapp.somewindow . -> "" + + + + } + proc exit {{toplevel ""}} { + variable waiting + variable result + variable default_result + set toplevels [get_toplevels] + if {[string length $toplevel]} { + set wposn [lsearch $toplevels $toplevel] + if {$wposn > 0} { + destroy $toplevel + } + } else { + #review + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + puts stderr "punkapp::exit called without toplevel - showing console" + show_console + return 0 + } else { + puts stderr "punkapp::exit called without toplevel - exiting" + if {$waiting ne "no"} { + if {[info exists result(shell)]} { + set temp [set result(shell)] + unset result(shell) + set waiting $temp + } else { + set waiting "" + } + } else { + ::exit + } + } + } + + set controllable [get_user_controllable_toplevels] + if {![llength $controllable]} { + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + show_console + } else { + if {$waiting ne "no"} { + if {[info exists result(shell)]} { + set temp [set result(shell)] + unset result(shell) + set waiting $temp + } elseif {[info exists result($toplevel)]} { + set temp [set result($toplevel)] + unset result($toplevel) + set waiting $temp + } elseif {[info exists default_result]} { + set temp $default_result + unset default_result + set waiting $temp + } else { + set waiting "" + } + } else { + ::exit + } + } + } + } + proc close_window {toplevel} { + wm withdraw $toplevel + if {![llength [get_user_controllable_toplevels]]} { + punkapp::exit $toplevel + } + destroy $toplevel + } + proc wait {args} { + variable waiting + variable default_result + if {[dict exists $args -defaultresult]} { + set default_result [dict get $args -defaultresult] + } + foreach t [punkapp::get_toplevels] { + if {[wm protocol $t WM_DELETE_WINDOW] eq ""} { + wm protocol $t WM_DELETE_WINDOW [list punkapp::close_window $t] + } + } + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + puts stderr "repl eventloop seems to be running - punkapp::wait not required" + } else { + if {$waiting eq "no"} { + set waiting "waiting" + vwait ::punkapp::waiting + return $::punkapp::waiting + } + } + } + + #A window can be 'visible' according to this - but underneath other windows etc + #REVIEW - change name? + proc get_visible_toplevels {{w .}} { + if {![llength [info commands winfo]]} { + return [list] + } + set list [get_toplevels $w] + set mapped [lmap v $list {expr {[winfo ismapped $v] ? $v : {}}}] + set mapped [concat {*}$mapped] ;#ignore {} + set visible [list] + foreach m $mapped { + if {[wm overrideredirect $m] == 0 } { + lappend visible $m + } else { + if {[winfo height $m] >1 && [winfo width $m] > 1} { + #technically even a 1x1 is visible.. but in practice even a 10x10 is hardly likely to be noticeable when overrideredirect == 1 + #as a convention - 1x1 with no controls is used to make a window invisible so we'll treat anything larger as visible + lappend visible $m + } + } + } + return $visible + } + proc get_user_controllable_toplevels {{w .}} { + set visible [get_visible_toplevels $w] + set controllable [list] + foreach v $visible { + if {[wm overrideredirect $v] == 0} { + lappend controllable $v + } + } + #only return visible windows with overrideredirect == 0 because there exists some user control. + #todo - review.. consider checking if position is outside screen areas? Technically controllable.. but not easily + return $controllable + } + proc hide_console {args} { + set opts [dict create -force 0] + if {([llength $args] % 2) != 0} { + error "hide_console expects pairs of arguments. e.g -force 1" + } + #set known_opts [dict keys $defaults] + foreach {k v} $args { + switch -- $k { + -force { + dict set opts $k $v + } + default { + error "Unrecognised options '$k' known options: [dict keys $opts]" + } + } + } + set force [dict get $opts -force] + + if {!$force} { + if {![llength [get_user_controllable_toplevels]]} { + puts stderr "Cannot hide console while no user-controllable windows available" + return 0 + } + } + if {$::tcl_platform(platform) eq "windows"} { + #hide won't work for certain consoles cush as conemu,wezterm - and doesn't really make sense for tabbed windows anyway. + #It would be nice if we could tell the console window to hide just the relevant tab - or the whole window if only one tab present - but this is unlikely to be possible in any standard way. + #an ordinary cmd.exe or pwsh.exe or powershell.exe window can be hidden ok though. + #(but with wezterm - process is cmd.exe - but it has style popup and can't be hidden with a twapi::hide_window call) + package require twapi + set h [twapi::get_console_window] + set pid [twapi::get_window_process $h] + set pinfo [twapi::get_process_info $pid -name] + set pname [dict get $pinfo -name] + set wstyle [twapi::get_window_style $h] + #tclkitsh/tclsh? + if {($pname in [list cmd.exe pwsh.exe powershell.exe] || [string match punk*.exe $pname]) && "popup" ni $wstyle} { + twapi::hide_window $h + return 1 + } else { + puts stderr "punkapp::hide_console unable to hide this type of console window" + return 0 + } + } else { + #todo + puts stderr "punkapp::hide_console unimplemented on this platform (todo)" + return 0 + } + } + + proc show_console {} { + if {$::tcl_platform(platform) eq "windows"} { + package require twapi + if {![catch {set h [twapi::get_console_window]} errM]} { + twapi::show_window $h -activate -normal + } else { + #no console - assume launched from something like wish? + catch {console show} + } + } else { + #todo + puts stderr "punkapp::show_console unimplemented on this platform" + } + } + +} diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck/cli-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck/cli-0.1.0.tm new file mode 100644 index 00000000..609df5c3 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck/cli-0.1.0.tm @@ -0,0 +1,333 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application punkcheck::cli 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + +package require punk::mix::util + +namespace eval punkcheck::cli { + namespace ensemble create + #package require punk::overlay + #punk::overlay::import_commandset debug. ::punk:mix::commandset::debug + + #init proc required - used for lazy loading of commandsets + variable initialised 0 + proc _init {args} { + variable initialised + if {$initialised} { + return + } + puts stderr "punkcheck::cli::init $args" + + set initialised 1 + } + + proc help {args} { + set basehelp [punk::mix::base help {*}$args] + return $basehelp + } + + proc paths {{path {}}} { + if {$path eq {}} { set path [pwd] } + set search_from $path + set bottom_to_top [list] + while {[string length [set pcheck_file [punkcheck::cli::lib::find_nearest_file $search_from]]]} { + set pcheck_folder [file dirname $pcheck_file] + lappend bottom_to_top $pcheck_file + set search_from [file dirname $pcheck_folder] + } + return $bottom_to_top + } + #todo! - group by fileset + proc status {{path {}}} { + if {$path eq {}} { set path [pwd] } + set fullpath [file normalize $path] + + set ftype [file type $fullpath] + + + set files [list] + if {$ftype eq "file"} { + set container [file dirname $fullpath] + lappend files $fullpath + } else { + set container $fullpath + #vfs can mask mounted files - so we can't just use 'file type' or glob with -type f + ##set files [glob -nocomplain -dir $fullpath -type f *] + package require punk::nav::fs + set folderinfo [punk::nav::fs::dirfiles_dict $fullpath] + set files [concat [dict get $folderinfo files] [dict get $folderinfo underlayfiles]] + } + set punkcheck_files [paths $container] + set repodict [punk::repo::find_repo $container] + + if {![llength $punkcheck_files]} { + puts stderr "No .punkcheck files found at or above this folder" + } + + set table "" + set files_with_records [list] + foreach p $punkcheck_files { + set basedir [file dirname $p] + set recordlist [punkcheck::load_records_from_file $p] + set tgt_dict [punkcheck::recordlist::records_as_target_dict $recordlist] + + foreach f $files { + set relpath [punkcheck::lib::path_relative $basedir $f] + + if {[dict exists $tgt_dict $relpath]} { + set filerec [dict get $tgt_dict $relpath] + set records [punkcheck::dict_getwithdefault $filerec body [list]] + if {$ftype eq "file"} { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set pcheck \n + foreach irec $records { + append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } else { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set display_records [list] + set pcheck \n + set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] + if {[llength $latest_install_record]} { + lappend display_records $latest_install_record + } + if {$latest_install_record ne [lindex $records end]} { + lappend display_records [lindex $records end] + } + foreach irec $display_records { + append pcheck "[format %-14s [dict get $irec tag]] [punkcheck::dict_getwithdefault $irec -tsiso "no-timestamp"]" + set bodyrecords [punkcheck::dict_getwithdefault $irec body [list]] + set source_files [list] + set source_files_changed [list] + set source_folders [list] + set source_folders_changed [list] + foreach r $bodyrecords { + if {[dict get $r tag] eq "SOURCE"} { + set path [dict get $r -path] + set changed [dict get $r -changed] + switch -- [dict get $r -type] { + file { + lappend source_files $path + if {$changed} { + lappend source_files_changed $path + } + } + directory { + lappend source_folders $path + if {$changed} { + lappend source_folders_changed $path + } + } + } + } + } + if {[llength $source_files]} { + append pcheck " source files : [llength $source_files] (changed [llength $source_files_changed])" + } + if {[llength $source_folders]} { + append pcheck " source folders: [llength $source_folders] (changed [llength $source_folders_changed])" + } + append pcheck \n + + #append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } + append table "$f $pcheck" \n + } + } + } + return $table + } + proc status_by_file {{path {}}} { + if {$path eq {}} { set path [pwd] } + set fullpath [file normalize $path] + set ftype [file type $fullpath] + set files [list] + if {$ftype eq "file"} { + set container [file dirname $fullpath] + lappend files $fullpath + } else { + set container $fullpath + set files [glob -nocomplain -dir $fullpath -type f *] + } + set punkcheck_files [paths $container] + set repodict [punk::repo::find_repo $container] + + if {![llength $punkcheck_files]} { + puts stderr "No .punkcheck files found at or above this folder" + } + + set table "" + set files_with_records [list] + foreach p $punkcheck_files { + set basedir [file dirname $p] + set recordlist [punkcheck::load_records_from_file $p] + set tgt_dict [punkcheck::recordlist::records_as_target_dict $recordlist] + foreach f $files { + set relpath [punkcheck::lib::path_relative $basedir $f] + + if {[dict exists $tgt_dict $relpath]} { + set filerec [dict get $tgt_dict $relpath] + set records [punkcheck::dict_getwithdefault $filerec body [list]] + if {$ftype eq "file"} { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set pcheck \n + foreach irec $records { + append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } else { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set display_records [list] + set pcheck \n + set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] + lappend display_records $latest_install_record + if {$latest_install_record ne [lindex $records end]} { + lappend display_records [lindex $records end] + } + foreach irec $display_records { + append pcheck "[format %-14s [dict get $irec tag]] [punkcheck::dict_getwithdefault $irec -tsiso "no-timestamp"]" + set bodyrecords [punkcheck::dict_getwithdefault $irec body [list]] + set source_files [list] + set source_files_changed [list] + set source_folders [list] + set source_folders_changed [list] + foreach r $bodyrecords { + if {[dict get $r tag] eq "SOURCE"} { + set path [dict get $r -path] + set changed [dict get $r -changed] + switch -- [dict get $r -type] { + file { + lappend source_files $path + if {$changed} { + lappend source_files_changed $path + } + } + directory { + lappend source_folders $path + if {$changed} { + lappend source_folders_changed $path + } + } + } + } + } + if {[llength $source_files]} { + append pcheck " source files : [llength $source_files] (changed [llength $source_files_changed])" + } + if {[llength $source_folders]} { + append pcheck " source folders: [llength $source_folders] (changed [llength $source_folders_changed])" + } + append pcheck \n + + #append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } + append table "$f $pcheck" \n + } + } + } + return $table + } +} + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punkcheck::cli::lib { + namespace path ::punk::mix::util ;#askuser, do_in_path, foreach-file etc + + proc find_nearest_file {{path {}}} { + if {$path eq {}} { set path [pwd] } + set folder [lib::scanup $path lib::is_punkchecked_folder] + if {$folder eq ""} { + return "" + } else { + return [file join $folder .punkcheck] + } + } + + proc is_punkchecked_folder {{path {}}} { + if {$path eq {}} { set path [pwd] } + foreach control { + .punkcheck + } { + set control [file join $path $control] + if {[file isfile $control]} {return 1} + } + return 0 + } + + proc scanup {path cmd} { + if {$path eq {}} { set path [pwd] } + #based on kettle::path::scanup + if {[file pathtype $path] eq "relative"} { + set path [file normalize $path] + } + while {1} { + # Found the proper directory, per the predicate. + if {[{*}$cmd $path]} { return $path } + + # Not found, walk to parent + set new [file dirname $path] + + # Stop when reaching the root. + if {$new eq $path} { return {} } + if {$new eq {}} { return {} } + + # Ok, truly walk up. + set path $new + } + return {} + } + +} + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punkcheck::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command status + package require punk::mix::base + package require punk::overlay + punk::overlay::custom_from_base [namespace current] ::punk::mix::base +} + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punkcheck::cli [namespace eval punkcheck::cli { + variable version + set version 0.1.0 +}] +return + + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.1.9.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.1.9.tm new file mode 100644 index 00000000..b8f4dec0 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.1.9.tm @@ -0,0 +1,3078 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + + #'tag' is an identifier for the log source. + # each tag will use it's own thread to write to the configured log target + proc open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + package require shellthread + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #note new_worker + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + + +namespace eval shellfilter::ansi { + #maint warning - + #ansistrip from punk::ansi is better/more comprehensive + proc stripcodes {text} { + #obsolete? + #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). + dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. + #line endings can theoretically occur within an ansi escape sequence (review e.g title?) + set inputlist [split $text ""] + set outputlist [list] + + #self-contained 2 byte ansi escape sequences - review more? + set 2bytecodes_dict [dict create\ + "reset_terminal" "\033c"\ + "save_cursor_posn" "\u001b7"\ + "restore_cursor_posn" "\u001b8"\ + "cursor_up_one" "\u001bM"\ + ] + set 2bytecodes [dict values $2bytecodes_dict] + + set in_escapesequence 0 + #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls + set i 0 + foreach u $inputlist { + set v [lindex $inputlist $i+1] + set uv ${u}${v} + if {$in_escapesequence eq "2b"} { + #2nd byte - done. + set in_escapesequence 0 + } elseif {$in_escapesequence != 0} { + set escseq [dict get $escape_terminals $in_escapesequence] + if {$u in $escseq} { + set in_escapesequence 0 + } elseif {$uv in $escseq} { + set in_escapseequence 2b ;#flag next byte as last in sequence + } + } else { + #handle both 7-bit and 8-bit CSI and OSC + if {[regexp {^(?:\033\[|\u009b)} $uv]} { + set in_escapesequence CSI + } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { + set in_escapesequence OSC + } elseif {$uv in $2bytecodes} { + #self-contained e.g terminal reset - don't pass through. + set in_escapesequence 2b + } else { + lappend outputlist $u + } + } + incr i + } + return [join $outputlist ""] + } + +} +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method flush {ch} { + return "" + } + method write {ch bytes} { + set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + foreach v $o_datavars { + append $v $stringdata + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method write {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [encoding convertfrom $o_enc $bytes] + if 0 { + if {"utf-16le" in [encoding names]} { + set logdata [encoding convertfrom utf-16le $bytes] + } else { + set logdata [encoding convertto utf-8 $bytes] + #set logdata [encoding convertfrom unicode $bytes] + #set logdata $bytes + } + } + #set logdata $bytes + #set logdata [string map [list \r -r- \n -n-] $logdata] + #if {[string equal [string range $logdata end-1 end] "\r\n"]} { + # set logdata [string range $logdata 0 end-2] + #} + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $logdata + #return $bytes + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [dict get $tf -encoding] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_colour + variable o_do_colour + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method Trackcodes {chunk} { + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + append emit $o_do_colour$pt$o_do_normal + #append emit $pt + } else { + append emit $pt + } + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } else { + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + append emit $o_do_colour$trailing_pt$o_do_normal + } else { + append emit $trailing_pt + } + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string last \x1b $buf] == [llength $buf]-1} { + #only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit [string range $buf 0 end-1] + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[llength $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + } else { + set emit_anyway 1 + } + } else { + set possible_code_len [expr {[llength $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set emit "" + } else { + set emit_anyway 1 + } + } + } + } + if {$emit_anyway} { + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + set emit $o_do_colour$buf$o_do_normal + } else { + set emit $buf + } + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + set emit $o_do_colour$buf$o_do_normal + } else { + set emit $buf + } + #set emit $buf + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + return + } + method write {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set streaminfo [my Trackcodes $instring] + set emit [dict get $streaminfo emit] + if {[dict get $streaminfo stacksize] == 0} { + #no ansi on the stack - we can wrap + #review + set outstring "$o_do_colour$emit$o_do_normal" + } else { + set outstring $emit + } + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $outstring] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + #todo - implement as oo + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + set t [textblock::class::table new $tableprefix] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + set stack [lreplace $stack $idposn end] + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + set stack [lreplace $stack $bottom_pop_posn end] + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + #add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + ::shellfilter::log::open $tag {-syslog ""} + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create \ + -tag redirected_$chan -syslog "" -file ""\ + ] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list \ + \r cr\ + \n lf\ + {"} doublequote\ + {'} singlequote\ + "`" backquote\ + "^" caret\ + \t tab\ + " " sp\ + "\[" lsquare\ + "\]" rsquare\ + "(" lbracket\ + ")" rbracket\ + "\{" lbrace\ + "\}" rbrace\ + \\ backslash\ + / forwardslash\ + ] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list \ + "\"" [list "\"" "\"" "\""]\ + {^} [list "\"" "\"" "^"]\ + "'" [list "'" "'" "'"]\ + "\{" [list "\{" "\}" "\}"]\ + {[} [list {[} {]} {]}]\ + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create \ + -teehandle command \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -tclscript 0 \ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if $experiment { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list \ + -timeout \ + -outprefix \ + -errprefix \ + -debug \ + -copytempfile \ + -outbuffering \ + -errbuffering \ + -inbuffering \ + -readprocesstranslation \ + -outtranslation \ + -stdinhandler \ + -outchan \ + -errchan \ + -inchan \ + -teehandle\ + ] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {([llength $args] % 2) != 0} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -outbuffering none \ + -errbuffering none \ + -readprocesstranslation auto \ + -outtranslation lf \ + -inbuffering none \ + -timeout 900000\ + -outprefix ""\ + -errprefix ""\ + -debug 0\ + -copytempfile 0\ + -stdinhandler ""\ + ] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + chan configure $wrerr -blocking 0 + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem\ + {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + }\ + {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list \ + [chan configure $inchan -buffering] \ + [chan configure $outchan -buffering] \ + [chan configure $errchan -buffering] \ + ] + + set remember_in_out_err_translation [list \ + [chan configure $inchan -translation] \ + [chan configure $outchan -translation] \ + [chan configure $errchan -translation] \ + ] + + + + + + chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + after $timeout [string map [list %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }] + + + vwait $waitvar + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.1.9 +}] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argp-0.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argp-0.2.tm new file mode 100644 index 00000000..1b1f4b78 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argp-0.2.tm @@ -0,0 +1,259 @@ + +# Tcl parser for optional arguments in function calls and +# commandline arguments +# +# (c) 2001 Bastien Chevreux + +# Index of exported commands +# - argp::registerArgs +# - argp::setArgDefaults +# - argp::setArgsNeeded +# - argp::parseArgs + +# Internal commands +# - argp::CheckValues + +# See end of file for an example on how to use + +package provide argp 0.2 + +namespace eval argp { + variable Optstore + variable Opttypes { + boolean integer double string + } + + namespace export {[a-z]*} +} + + +proc argp::registerArgs { func arglist } { + variable Opttypes + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + #puts $parentns + #puts $caller + #puts $cmangled + + set Optstore(keys,$cmangled) {} + set Optstore(deflist,$cmangled) {} + set Optstore(argneeded,$cmangled) {} + + foreach arg $arglist { + foreach {opt type default allowed} $arg { + set optindex [lsearch -glob $Opttypes $type*] + if { $optindex < 0} { + return -code error "$caller, unknown type $type while registering arguments.\nAllowed types: [string trim $Opttypes]" + } + set type [lindex $Opttypes $optindex] + + lappend Optstore(keys,$cmangled) $opt + set Optstore(type,$opt,$cmangled) $type + set Optstore(default,$opt,$cmangled) $default + set Optstore(allowed,$opt,$cmangled) $allowed + lappend Optstore(deflist,$cmangled) $opt $default + } + } + + if { [catch {CheckValues $caller $cmangled $Optstore(deflist,$cmangled)} res]} { + return -code error "Error in declaration of optional arguments.\n$res" + } +} + +proc argp::setArgDefaults { func arglist } { + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + set Optstore(deflist,$cmangled) {} + foreach {opt default} $arglist { + if {![info exists Optstore(default,$opt,$cmangled)]} { + return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)" + } + set Optstore(default,$opt,$cmangled) $default + } + + # set the new defaultlist + foreach opt $Optstore(keys,$cmangled) { + lappend Optstore(deflist,$cmangled) $opt $Optstore(default,$opt,$cmangled) + } +} + +proc argp::setArgsNeeded { func arglist } { + variable Optstore + + set parentns [string range [uplevel 1 [list namespace current]] 2 end] + if { $parentns != "" } { + append caller $parentns :: $func + } else { + set caller $func + } + set cmangled [string map {:: _} $caller] + + #append caller $parentns :: $func + #set cmangled ${parentns}_$func + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + + set Optstore(argneeded,$cmangled) {} + foreach opt $arglist { + if {![info exists Optstore(default,$opt,$cmangled)]} { + return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)" + } + lappend Optstore(argneeded,$cmangled) $opt + } +} + + +proc argp::parseArgs { args } { + variable Optstore + + if {[llength $args] == 0} { + upvar args a opts o + } else { + upvar args a [lindex $args 0] o + } + + if { [ catch { set caller [lindex [info level -1] 0]}]} { + set caller "main program" + set cmangled "" + } else { + set cmangled [string map {:: _} $caller] + } + + if {![info exists Optstore(deflist,$cmangled)]} { + return -code error "Arguments for $caller not registered yet." + } + + # set the defaults + array set o $Optstore(deflist,$cmangled) + + # but unset the needed arguments + foreach key $Optstore(argneeded,$cmangled) { + catch { unset o($key) } + } + + foreach {key val} $a { + if {![info exists Optstore(type,$key,$cmangled)]} { + return -code error "$caller, unknown option $key, must be one of: $Optstore(keys,$cmangled)" + } + switch -exact -- $Optstore(type,$key,$cmangled) { + boolean - + integer { + if { $val == "" } { + return -code error "$caller, $key empty string is not $Optstore(type,$key,$cmangled) value." + } + if { ![string is $Optstore(type,$key,$cmangled) $val]} { + return -code error "$caller, $key $val is not $Optstore(type,$key,$cmangled) value." + } + } + double { + if { $val == "" } { + return -code error "$caller, $key empty string is not double value." + } + if { ![string is double $val]} { + return -code error "$caller, $key $val is not double value." + } + if { [string is integer $val]} { + set val [expr {$val + .0}] + } + } + default { + } + } + set o($key) $val + } + + foreach key $Optstore(argneeded,$cmangled) { + if {![info exists o($key)]} { + return -code error "$caller, needed argument $key was not given." + } + } + + if { [catch { CheckValues $caller $cmangled [array get o]} err]} { + return -code error $err + } + + return +} + + +proc argp::CheckValues { caller cmangled checklist } { + variable Optstore + + #puts "Checking $checklist" + + foreach {key val} $checklist { + if { [llength $Optstore(allowed,$key,$cmangled)] > 0 } { + switch -exact -- $Optstore(type,$key,$cmangled) { + string { + if { [lsearch $Optstore(allowed,$key,$cmangled) $val] < 0} { + return -code error "$caller, $key $val is not in allowed values: $Optstore(allowed,$key,$cmangled)" + } + } + double - + integer { + set found 0 + foreach range $Optstore(allowed,$key,$cmangled) { + if {[llength $range] == 1} { + if { $val == [lindex $range 0] } { + set found 1 + break + } + } elseif {[llength $range] == 2} { + set low [lindex $range 0] + set high [lindex $range 1] + + if { ![string is integer $low] \ + && [string compare "-" $low] != 0} { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a lower value range that is not integer and not \u00b4-\u00b4: $range" + } + if { ![string is integer $high] \ + && [string compare "+" $high] != 0} { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a upper value range that is not integer and not \u00b4+\u00b4: $range" + } + if {[string compare "-" $low] == 0} { + if { [string compare "+" $high] == 0 \ + || $val <= $high } { + set found 1 + break + } + } + if { $val >= $low } { + if {[string compare "+" $high] == 0 \ + || $val <= $high } { + set found 1 + break + } + } + } else { + return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has an allowed value range containing more than 2 elements: $range" + } + } + if { $found == 0 } { + return -code error "$caller, $key $val is not covered by allowed ranges: $Optstore(allowed,$key,$cmangled)" + } + } + } + } + } +} diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/debug-1.0.6.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/debug-1.0.6.tm new file mode 100644 index 00000000..c2ee57be --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/debug-1.0.6.tm @@ -0,0 +1,306 @@ +# Debug - a debug narrative logger. +# -- Colin McCormack / originally Wub server utilities +# +# Debugging areas of interest are represented by 'tokens' which have +# independantly settable levels of interest (an integer, higher is more detailed) +# +# Debug narrative is provided as a tcl script whose value is [subst]ed in the +# caller's scope if and only if the current level of interest matches or exceeds +# the Debug call's level of detail. This is useful, as one can place arbitrarily +# complex narrative in code without unnecessarily evaluating it. +# +# TODO: potentially different streams for different areas of interest. +# (currently only stderr is used. there is some complexity in efficient +# cross-threaded streams.) + +# # ## ### ##### ######## ############# ##################### +## Requisites + +package require Tcl 8.5- + +namespace eval ::debug { + namespace export -clear \ + define on off prefix suffix header trailer \ + names 2array level setting parray pdict \ + nl tab hexl + namespace ensemble create -subcommands {} +} + +# # ## ### ##### ######## ############# ##################### +## API & Implementation + +proc ::debug::noop {args} {} + +proc ::debug::debug {tag message {level 1}} { + variable detail + if {$detail($tag) < $level} { + #puts stderr "$tag @@@ $detail($tag) >= $level" + return + } + + variable prefix + variable suffix + variable header + variable trailer + variable fds + + if {[info exists fds($tag)]} { + set fd $fds($tag) + } else { + set fd stderr + } + + # Assemble the shown text from the user message and the various + # prefixes and suffices (global + per-tag). + + set themessage "" + if {[info exists prefix(::)]} { append themessage $prefix(::) } + if {[info exists prefix($tag)]} { append themessage $prefix($tag) } + append themessage $message + if {[info exists suffix($tag)]} { append themessage $suffix($tag) } + if {[info exists suffix(::)]} { append themessage $suffix(::) } + + # Resolve variables references and command invokations embedded + # into the message with plain text. + set code [catch { + set smessage [uplevel 1 [list ::subst -nobackslashes $themessage]] + set sheader [uplevel 1 [list ::subst -nobackslashes $header]] + set strailer [uplevel 1 [list ::subst -nobackslashes $trailer]] + } __ eo] + + # And dump an internal error if that resolution failed. + if {$code} { + if {[catch { + set caller [info level -1] + }]} { set caller GLOBAL } + if {[string length $caller] >= 1000} { + set caller "[string range $caller 0 200]...[string range $caller end-200 end]" + } + foreach line [split $caller \n] { + puts -nonewline $fd "@@(DebugError from $tag ($eo): $line)" + } + return + } + + # From here we have a good message to show. We only shorten it a + # bit if its a bit excessive in size. + + if {[string length $smessage] > 4096} { + set head [string range $smessage 0 2048] + set tail [string range $smessage end-2048 end] + set smessage "${head}...(truncated)...$tail" + } + + foreach line [split $smessage \n] { + puts $fd "$sheader$tag | $line$strailer" + } + return +} + +# names - return names of debug tags +proc ::debug::names {} { + variable detail + return [lsort [array names detail]] +} + +proc ::debug::2array {} { + variable detail + set result {} + foreach n [lsort [array names detail]] { + if {[interp alias {} debug.$n] ne "::debug::noop"} { + lappend result $n $detail($n) + } else { + lappend result $n -$detail($n) + } + } + return $result +} + +# level - set level and fd for tag +proc ::debug::level {tag {level ""} {fd {}}} { + variable detail + # TODO: Force level >=0. + if {$level ne ""} { + set detail($tag) $level + } + + if {![info exists detail($tag)]} { + set detail($tag) 1 + } + + variable fds + if {$fd ne {}} { + set fds($tag) $fd + } + + return $detail($tag) +} + +proc ::debug::header {text} { variable header $text } +proc ::debug::trailer {text} { variable trailer $text } + +proc ::debug::define {tag} { + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +# Set a prefix/suffix to use for tag. +# The global (tag-independent) prefix/suffix is adressed through tag '::'. +# This works because colon (:) is an illegal character for user-specified tags. + +proc ::debug::prefix {tag {theprefix {}}} { + variable prefix + set prefix($tag) $theprefix + + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +proc ::debug::suffix {tag {theprefix {}}} { + variable suffix + set suffix($tag) $theprefix + + if {[interp alias {} debug.$tag] ne {}} return + off $tag + return +} + +# turn on debugging for tag +proc ::debug::on {tag {level ""} {fd {}}} { + variable active + set active($tag) 1 + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::debug $tag + return +} + +# turn off debugging for tag +proc ::debug::off {tag {level ""} {fd {}}} { + variable active + set active($tag) 1 + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::noop + return +} + +proc ::debug::setting {args} { + if {[llength $args] == 1} { + set args [lindex $args 0] + } + set fd stderr + if {[llength $args] % 2} { + set fd [lindex $args end] + set args [lrange $args 0 end-1] + } + foreach {tag level} $args { + if {$level > 0} { + level $tag $level $fd + interp alias {} debug.$tag {} ::debug::debug $tag + } else { + level $tag [expr {-$level}] $fd + interp alias {} debug.$tag {} ::debug::noop + } + } + return +} + +# # ## ### ##### ######## ############# ##################### +## Convenience commands. +# Format arrays and dicts as multi-line message. +# Insert newlines and tabs. + +proc ::debug::nl {} { return \n } +proc ::debug::tab {} { return \t } + +proc ::debug::parray {a {pattern *}} { + upvar 1 $a array + if {![array exists array]} { + error "\"$a\" isn't an array" + } + pdict [array get array] $pattern +} + +proc ::debug::pdict {dict {pattern *}} { + set maxl 0 + set names [lsort -dict [dict keys $dict $pattern]] + foreach name $names { + if {[string length $name] > $maxl} { + set maxl [string length $name] + } + } + set maxl [expr {$maxl + 2}] + set lines {} + foreach name $names { + set nameString [format (%s) $name] + lappend lines [format "%-*s = %s" \ + $maxl $nameString \ + [dict get $dict $name]] + } + return [join $lines \n] +} + +proc ::debug::hexl {data {prefix {}}} { + set r {} + + # Convert the data to hex and to characters. + binary scan $data H*@0a* hexa asciia + + # Replace non-printing characters in the data with dots. + regsub -all -- {[^[:graph:] ]} $asciia {.} asciia + + # Pad with spaces to a full multiple of 32/16. + set n [expr {[string length $hexa] % 32}] + if {$n < 32} { append hexa [string repeat { } [expr {32-$n}]] } + #puts "pad H [expr {32-$n}]" + + set n [expr {[string length $asciia] % 32}] + if {$n < 16} { append asciia [string repeat { } [expr {16-$n}]] } + #puts "pad A [expr {32-$n}]" + + # Reassemble formatted, in groups of 16 bytes/characters. + # The hex part is handled in groups of 32 nibbles. + set addr 0 + while {[string length $hexa]} { + # Get front group of 16 bytes each. + set hex [string range $hexa 0 31] + set ascii [string range $asciia 0 15] + # Prep for next iteration + set hexa [string range $hexa 32 end] + set asciia [string range $asciia 16 end] + + # Convert the hex to pairs of hex digits + regsub -all -- {..} $hex {& } hex + + # Add the hex and latin-1 data to the result buffer + append r $prefix [format %04x $addr] { | } $hex { |} $ascii |\n + incr addr 16 + } + + # And done + return $r +} + +# # ## ### ##### ######## ############# ##################### + +namespace eval debug { + variable detail ; # map: TAG -> level of interest + variable prefix ; # map: TAG -> message prefix to use + variable suffix ; # map: TAG -> message suffix to use + variable fds ; # map: TAG -> handle of open channel to log to. + variable header {} ; # per-line heading, subst'ed + variable trailer {} ; # per-line ending, subst'ed + + # Notes: + # - The tag '::' is reserved. "prefix" and "suffix" use it to store + # the global message prefix / suffix. + # - prefix and suffix are applied per message. + # - header and trailer are per line. And should not generate multiple lines! +} + +# # ## ### ##### ######## ############# ##################### +## Ready + +package provide debug 1.0.6 +return diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/flagfilter-0.3.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/flagfilter-0.3.tm new file mode 100644 index 00000000..1d37e215 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/flagfilter-0.3.tm @@ -0,0 +1,2714 @@ +#package provide flagfilter [namespace eval flagfilter {list [variable version 0.2.3]$version}] +#package provide [set ::pkg flagfilter-0.2.3] [namespace eval [lindex [split $pkg -] 0] {list [variable version [lindex [split $pkg -] 1][set ::pkg {}]]$version}] +# +#package provide [lindex [set pkg {flagfilter 0.2.3}] 0] [namespace eval [lindex $pkg 0] {list [variable version [lindex $pkg 1][set pkg {}]]$version}] +package provide [lassign {flagfilter 0.3} pkg ver]$pkg [namespace eval $pkg[set pkg {}] {list [variable version $::ver[set ::ver {}]]$version}] + +#Note: this is ugly.. particularly when trying to classify flags that are not fully specified i.e raw passthrough. +# - we can't know if a flag -x --x etc is expecting a parameter or not. +#0.2.2 2023-03 JN - added %match% placeholder support. Can be added to the dispatch command to tell it what command was actually matched. e.g tell xxx.tcl script that it was xxx.tcl when we matched on *.tcl + + +namespace eval flagfilter { + package require oolib ;# make 'oolib::collection new' available + + proc do_errorx {msg {code 1}} { + if {$::tcl_interactive} { + error $msg + } else { + puts stderr "|>err $msg" + exit $code + } + } + + proc do_error {msg {then error}} { + set levels [list debug info notice warn error critical alert emergency] + #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 syslog-like level descriptors + lassign $then type code + if {$code eq ""} { + set code 1 + } + set type [string tolower $type] + if {$type in [concat $levels exit]} { + puts -nonewline stderr "|$type> $msg\n" + } else { + puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '\n" + } + 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 -nonewline stderr " (exit suppressed due to tcl_interactive - raising error instead)\n" + if {![string is digit -strict $code]} { + puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '\n" + } + } + 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 -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '\n" + flush stderr + return -code error $msg + } + } + } + } + proc scriptdir {} { + set possibly_linked_script [file dirname [file normalize [file join [info script] ...]]] + if {[file isdirectory $possibly_linked_script]} { + return $possibly_linked_script + } else { + return [file dirname $possibly_linked_script] + } + } + +} + +package require overtype + + +namespace eval flagfilter { + namespace export get_one_flag_value + #review. Tcl can handle args like: {-a -val1 -b -val2} as long as they pair up. + #this will ignore flag-like values if they follow a -flag + # positional values that happen to start with - can still cause issues + #get_flagged_only can return an unpaired list if there are solos, or if it finds no value for the last flaglike element + # e.g from input {something -x -y -z} we will get {-x -y -z} + # + # + + #flagfilter::get_flagged_only may not always get things right when looking at a values list with command processors + #Even if all solos from commands are supplied in solodict - a flag might be solo only in the context of a particualar commandset + #The proper way to get flagged values from an arglist is to run the full parser. + #This then should be restricted to use for a specific subset of args where the supplied solodict is known to apply + proc get_flagged_only {arglist solodict} { + #solodict - solo flags with defaults + set solo_accumulator [dict create] ;#if multiple instances of solo flag found - append defaults to the value to form a list as long as the number of occurrences + #puts ">>>get_flagged_only input $arglist solodict:'$solodict'" + set result [list] + set last_was_flag 0 + set result [list] + set a_idx 0 + set end_of_options 0 + foreach a $arglist { + if {$a eq "--"} { + break + } + if {[dict exists $solodict $a]} { + set last_was_flag 0 + if {[dict exists $solo_accumulator $a]} { + set soloval [concat [dict get $solo_accumulator $a] [dict get $solodict $a]] + } else { + set soloval [dict get $solodict $a] + } + dict set solo_accumulator $a $soloval + #we need to keep order of first appearance + set idx [lsearch $result $a] + if {$idx < 0} { + lappend result $a $soloval + } else { + lset result $idx+1 $soloval + } + } else { + if {!$last_was_flag} { + if {$a eq "--"} { + + } else { + if {[lindex $arglist $a_idx-1] eq "--"} { + #end of options processing - none of the remaining are considered flags/options no matter what they look like + set last_was_flag 0 + break + } else { + if {[string match -* $a]} { + set last_was_flag 1 + lappend result $a ;#flag + } else { + #last wasnt, this isn't - don't output + set last_was_flag 0 + } + } + } + } else { + #we only look for single leading - in the value if last wasn't a flag - but we give -- and soloflags special treatment. + if {$a eq "--"} { + #last was flag + set last_was_flag 0 + } else { + lappend result $a ;#value + set last_was_flag 0 + } + } + } + incr a_idx + } + if {([llength $result] % 2) != 0} { + set last [lindex $result end] + if {[string match -* $last] && ($last ni [dict keys $solodict])} { + lappend result 1 + } + } + #puts ">>>get_flagged_only returning $result" + return $result + } + + + ## get_one_paired_flag_value + #best called with 'catch' unless flag known to be in arglist + #raises an error if no position available after the flag to retrieve value + #raises an error if flag not like -something + #raises an error if flag not found in list + proc get_one_paired_flag_value {arglist flag} { + if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $flag]} { + #regexp excludes plain - and -- + #if {![string match -* $flag]} {} + error "get_one_flag_value flag $flag does not look like a flag. Should be something like -$flag or --$flag" + } + set cindex [lsearch $arglist $flag] + if {$cindex >= 0} { + set valueindex [expr {$cindex + 1}] + if {$valueindex < [llength $arglist]} { + #puts stderr "++++++++++++++++++ get_one_flag_value flag '$flag' returning [lindex $arglist $valueindex]" + return [lindex $arglist $valueindex] + } else { + error "flagfilter::get_one_paired_flag_value no value corresponding to flag $flag (found flag, but reached end of list)" + } + } else { + error "flagfilter::get_one_paired_flag_value $flag not found in arglist: '$arglist'" + } + } +} + +namespace eval flagfilter::obj { + +} + + +namespace eval flagfilter { + variable run_counter 0 ;#Used by get_new_runid to form an id to represent run of main check_flags function. + #used as a basis for some object-instance names etc + proc get_new_runid {} { + variable run_counter + if {[catch {package require Thread}]} { + set tid 0 + } else { + set tid [thread::id] + } + return "ff-[pid]-${tid}-[incr run_counter]" + } + + namespace export check_flags + proc do_debug {lvl debugconfig msg} { + if {$lvl <= [dict get $debugconfig -debugargs]} { + foreach ln [split $msg \n] { + puts -nonewline stderr "|[dict get $debugconfig -source]> $ln\n" + flush stderr + } + } + } + + #---------------------------------------------------------------------- + # DO NOT RELY ON tcl::unsupported - it's named that for a reason and is not meant to be parsed + #wiki.tcl-lang.org/page/dict+tips+and+tricks + proc isdict {v} { + if {[string match "value is a list *" [::tcl::unsupported::representation $v]]} { + return [expr {!([llength $v] % 2)}] + } else { + return [string match "value is a dict *" [::tcl::unsupported::representation $v]] + } + } + + proc dict_format {dict} { + dictformat_rec $dict "" " " + } + proc dictformat_rec {dict indent indentstring} { + # unpack this dimension + set is_empty 1 + dict for {key value} $dict { + set is_empty 0 + if {[isdict $value]} { + append result "$indent[list $key]\n$indent\{\n" + append result "[dictformat_rec $value "$indentstring$indent" $indentstring]\n" + append result "$indent\}\n" + } else { + append result "$indent[list $key] [list $value]\n" + } + } + if {$is_empty} { + #experimental.. + append result "$indent\n" + #append result "" + } + return $result + } + #-------------------------------------------------------------------------- + + #solo 'category' includes longopts with value + #solo flags include the general list of -soloflags, and those specific to the current -commandprocessors spec (mashopts and singleopts) + proc is_this_flag_solo {f solos objp} { + if {![string match -* $f]} { + #not even flaglike + return 0 + } + + + if {$f in $solos} { + #review! - global -soloflags shouldn't override the requirements of a commandprocessor! + #but.. each commandprocessor needs to understand global solos occuring before our match so that we classify correctly.. + #todo - this may need to reference v_map and current position in scanlist to do properly + return 1 + } + if {$f eq "-"} { + #unless the caller declared it as a solo - treat this as a non flag element. (likely use is as a command match) + return 0 + } + if {$f eq "--"} { + #this is it's own type endofoptions + return 0 + } + + set p_opts [$objp get_combined_opts] + + set mashopts [dict get $p_opts mashopts] + set singleopts [dict get $p_opts singleopts] + set pairopts [dict get $p_opts pairopts] + set longopts [dict get $p_opts longopts] + + if {$f in $singleopts} { + return 1 + } + + #"any" keywords used by processors to consume anything - where we're not too worried about classifying a flagvalue vs an operand + #examine these last so that an explicit configuration of flags as pairopts,mashopts etc can still be classified correctly + if {"any" in $singleopts} { + return 1 + } + if {[string first "=" $f] >=1} { + if {"any" in $longopts} { + return 1 + } + #todo foreach longopt - split on = and search + } + + #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash - but if it's a pairopt, but not mashable - we can rule it out now + if {($f in $pairopts) && ($f ni $mashopts)} { + return 0 + } + #todo - suport mashes where one of the mashed flags takes an arg - review: only valid if it's last in the mash? + #(presumably so - unless we there was some other value delimiter such as isnumeric or capitalised flags vs lowercase values - but that seems a step too far - would require some sort of mashspec/mash-strategy config) + #last part of mash may actually be the value too. which complicates things + #linux ls seems to do this for example: + # ls -w 0 + # ls -lw 0 + # ls -lw0 + # also man.. e.g + # man -Tdvi + # man -Hlynx + # man -H + # - note this last one. '-H lynx' doesn't work - so it's a mashable opt that can take a value, but is not in pairopts! (-H with no value uses env value for browser) + # see also comments in is_this_flag_mash + # + + set flagletters [split [string range $f 1 end] ""] + set posn 1 + set is_solo 1 ;#default assumption to disprove + #trailing letters may legitimately not be in mashopts if they are part of a mashed value + #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing + foreach l $flagletters { + if {"-$l" ni $mashopts} { + #presumably an ordinary flag not-known to us + return 0 + } else { + if {"-$l" in $pairopts} { + if {$posn == [llength $flagletters]} { + #in pairopts and mash - but no value for it in the mash - thefore not a solo + return 0 + } else { + #entire tail is the value - this letter is effectively solo + return 1 + } + } elseif {"-$l" in $singleopts} { + #not allowed to take a value - keep processing letters + } else { + #can take a value! but not if at very end of mash. Either way This is a solo + return 1 + } + } + } + return $is_solo + } + #todo? support global (non-processor specific) mash list? -mashflags ? + proc is_this_flag_mash {f objp} { + if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $f]} { + #not even flaglike + return 0 + } + set optinfo [$objp get_combined_opts];#also applies to tail_processor - *usually* empty values for mashopts etc + + #we look at singleopts because even if the flag is in mashopts - when it is alone we don't classify it as a mash + set singleopts pdict get $optinfo singleopts] + if {$f in $singleopts} { + return 0 + } + + set pairopts [dict get $optinfo pairopts] + if {$f in [dict keys $pairopts]} { + #here, the entire arg (f) we are testing is in pairopts - it could still however appear as part of a mash, with or without a trailing value, and with or without other flags before it in the mash (but if neither prefixed,nor tailed then obviously not a mash) + return 0 + } + set mashopts [dict get $optinfo mashopts] + set flagletters [split [string range $f 1 end] ""] + set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash.. unless trailing one also takes a value + # .. in which case value could be at the tail of the mash.. or be the next arg in the list + # We will take absense from singleopts and pairopts to indicate the mashflag *optionally* takes a value + # (ie such a mashopt is a solo that can take a value only as a mashtail) + # presence in pairopts indicates a mashflag must have a value + # presense in singleopts indicates mashflag takes no value ever. + # mashopt cannot be in both singleopts and pairopts. (NAND) + foreach l $flagletters { + if {-$l in $pairopts} { + if {"-$l" in $mashopts} { + #need to consider any remainder in the mash as this value .. if no remainder - then this is a mash, but not 'solo' because this flag needs to consume the following arg. + # We are only concerned with mashness here so just stop processing mash elements when we hit the first one that is a pairopt + break + } else { + #we require the pairopt to explicitly be listed in mashopts as well as pairopts if it is to be allowed to be part of a mash + set is_mash 0 + } + } elseif {"-$l" in $singleopts} { + #singleopt & mashopt - cannot take a value, mashed or otherwise + if {"-$l" ni $mashopts} { + set is_mash 0 + } + } else { + if {"-$l" ni $mashopts} { + set is_mash 0 + } else { + #present only in mashopts - can take a value, but only immediately following in the mash + break + } + } + } + return $is_mash + } + proc is_this_flag_for_me {f objp cf_args} { + set processorname [$objp name] + set optinfo [$objp get_combined_opts] ;#also applies to tail_processor - *usually* empty values for mashopts etc + + if {$processorname in [list "tail_processor"]} { + return 1 + } + if {$processorname in [list "global"]} { + #todo - mashflags for global? + set defaults [dict get $cf_args -defaults] + set extras [dict get $cf_args -extras] + set soloflags [dict get $cf_args -soloflags] + if {$f in [concat $extras $soloflags [dict keys $defaults]]} { + return 1 + } + } + + set singleopts [dict get $optinfo singleopts] + if {"any" in [string tolower $singleopts]} { + #review semantics of 'all' here. does it mean any -xxx.. will match, or only if also in global -soloflags? + return 1 + } + set pairopts [dict get $optinfo pairopts] + set allopts [concat $singleopts [dict keys $pairopts]] + if {$f in $allopts} { + return 1 + } + + #process mashopts last + set mashopts [dict get $optinfo mashopts] + if {"any" in [string tolower $mashopts]} { + #if 'all' in mashopts - it can eat anything - review - is this even useful? + return 1 + } else { + set flagletters [split [string range $f 1 end] ""] + set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash + foreach l $flagletters { + if {"-$l" ni $mashopts} { + set is_mash 0 + } + } + return $is_mash + } + + return 0 + } + + + + proc add_dispatch_raw {recordvar parentname v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname raw] + lappend dispatchinfo $v + dict set drecord $parentname raw $dispatchinfo + } + } + proc add_dispatch_argument {recordvar parentname k v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname arguments] + lappend dispatchinfo $k $v ;#e.g -opt 1 + dict set drecord $parentname arguments $dispatchinfo + } + } + proc lsearch-all-stride-2 {l search} { + set posns [lmap i [lsearch -all $l $search] {expr {($i % 2) == 0 ? $i : [list x]}}] + return [lsearch -all -inline -not $posns x] + } + proc update_dispatch_argument {recordvar parentname k v} { + upvar $recordvar drecord + if {[dict exists $drecord $parentname]} { + set dispatchinfo [dict get $drecord $parentname arguments] + #can't assume there aren't repeat values e.g -v -v + #dict set dispatchinfo $k $v + if {[package vcompare [info tclversion] 8.7a5] >= 0} { + set posns [lsearch -all -stride 2 $dispatchinfo $k] + } else { + set posns [lsearch-all-stride-2 $dispatchinfo $k] + } + set lastitem [lindex $posns end] + if {[string length $lastitem]} { + set val_idx [expr {$lastitem + 1}] + set dispatchinfo [lreplace $dispatchinfo[set dispatchinfo {}] $val_idx $val_idx $v] ;# inlineK + dict set drecord $parentname arguments $dispatchinfo + } else { + error "Unable to update dispatch argument $k with value $v in dispatch record for $parentname" + } + #dict set drecord $parentname $dispatchinfo + } + } + + #Note the difference between this and is_command_match. + #Lack of a 'match' element does not cause a commandspec to skip allocating an operand it encounters + #Note that this isn't a general test to be applied to the entire argument list. + # - an arg may get matched by an earlier processor making it unavailable to be allocated by another processor + # so this test only applies during the ordered examination of args + proc can_this_commandspec_allocate_this_arg {flag cspec cf_args} { + set cmdinfo [lindex $cspec 1] + if {$cmdinfo eq "tail_processor"} { + return 1 + } + if {$cmdinfo eq "global"} { + set defaults [dict get $cf_args -defaults] + set soloflags [dict get $cf_args -soloflags] + set extras [dict get $cf_args -extras] + if {$flag in [concat $soloflags $extras [dict keys $defaults]]} { + return 1 + } + } + if {![dict exists $cmdinfo match]} { + return 1 + } + set matchspeclist [dict get $cmdinfo match] + foreach matchspec $matchspeclist { + if {[regexp -- $matchspec $flag]} { + return 1 + } + } + #only block it if there was a match pattern specified but it didn't match + return 0 + } + #Note - returns false for a cspec that has no match specified. + #A command/subcommand with no match specification is allowed to allocate any value - so be careful with this + # - it should not be used to *stop* an arg being allocated if the processor has no 'match' specified, or if it is another type of processor like 'tail_handler'. + proc is_command_match {flag cspec} { + set pinfo [lindex $cspec 1] + if {[dict exists $pinfo match]} { + set matchspeclist [dict get $pinfo match] + foreach matchspec $matchspeclist { + if {[regexp -- $matchspec $flag]} { + return 1 + } + } + return 0 + } else { + return 0 + } + } + proc is_command_match_any {f commandprocessors} { + foreach comspec $commandprocessors { + lassign $comspec cmdname cmdinfo + if {[dict exists $cmdinfo match]} { + set matchlist [dict get $cmdinfo match] + foreach matchspec $matchlist { + if {[regexp -- $matchspec $f]} { + #actually a command + return true + } + } + } + } + return false + } + + #determine if f is potentially a flag that takes a parameter from the next argument. + #e.g --x=y (longopt) does not consume following arg but --something *might* + proc is_candidate_toplevel_param_flag {f solos commandprocessors} { + if {[is_command_match_any $f $commandprocessors]} { + return false + } + if {$f in $solos} { + return 0 + } + if {$f in {- --}} { + return 0 + } + #longopts (--x=blah) and alternative --x blah + #possibly also -x=blah + if {[string match -* $f]} { + if {[string first "=" $f]>1} { + return 0 + } + } + return [expr {[string match -* $f]}] + } + + + + + + + + + + + + + + + + + + + #review - should we be using control::assert here? + #It depends if this is intended to raise error at runtime - would using control::assert and disabling assertions cause problems? + #todo - show caller info + proc assert_equal {a b} { + if {![expr {$a eq $b}]} { + error "assert_equal $a $b" + } + } + + + + + + #{1 unallocated 2 unallocated 3 unallocated 4 unallocated 5 unallocated 6 unallocated} ;#initial v_map + #1 2 3 4 5 6 ;#original list posns example + # 2 6 ;#map_remaining example (scanlist) + #1 3 4 5 ;#map_allocated example + #{1 {cmd1 operand} 2 unallocated 3 {cmd2 operand} 4 {cmd2 flag} 5 {cmd2 flagvalue} 6 unallocated} ;#v_map updated example + oo::class create class_vmap { + variable o_map + variable o_remaining + variable o_allocated + variable o_values + variable o_codemap + variable o_flagcategory + constructor {values} { + set o_codemap [dict create \ + operand op \ + flagvalue fv \ + soloflag so \ + flag fl \ + unallocated un \ + endofoptions eo \ + ] + set o_flagcategory [list "flag" "flagvalue" "soloflag"] + set o_values $values + #set o_remaining [lsearch -all $values *] ;#create a list of indices e.g 0 1 2 3 4 5 6 + #lsearch -all * is fast for very small lists - but lseq wins from size around 30+ + if {[llength $values]} { + if {[llength $values] < 30} { + #common case is short lists - but we don't want to penalize large lists + set o_remaining [lsearch -all $values *] + } else { + #punk::lib::range wraps lseq if available + set o_remaining [punk::lib::range 0 [llength $values]-1] + } + } else { + set o_remaining [list] + } + set o_allocated [list] + set o_map [list] + foreach posn $o_remaining { + lappend o_map $posn unallocated + } + } + method load {values rem alloc map} { + set o_values $values + set o_remaining $rem + set o_allocated $alloc + set o_map $map + } + method copy_to {obj} { + $obj load $o_values $o_remaining $o_allocated $o_map + } + method update_map_from {obj} { + #very basic sanity check first + if {[llength $o_values] ne [llength [$obj get_values]]} { + error "[self class].update_map_from cannot update. length of values mismatch" + } + + set newmap [$obj get_map] + } + + method get_codemap {} { + return $o_codemap + } + method get_values {} { + return $o_values + } + method get_remaining {} { + return $o_remaining + } + method get_allocated {} { + return $o_allocated + } + method get_map {} { + return $o_map + } + method argnum_from_remaining_posn {scanlist_posn} { + set vidx [lindex $o_remaining $scanlist_posn] + if {![string is digit -strict $vidx]} { + return -code error "[self class].argnum_from_remaining_posn cannot determine argnum from scanlist position:$scanlist_posn using unallocated list:'$o_remaining'" + } + return $vidx + } + + method allocate {objp argnum type value} { + set processorname [$objp name] + if {$processorname eq "tail_processor"} { + set owner "unallocated" + } else { + set owner [$objp parentname] + } + if {$argnum > [llength $o_values]-1} { + return -code error "[self class].allocate cannot allocate argnum:$argnum. Only [llength $o_values] items in value list" + } + if {$argnum in $o_allocated} { + return -code error "[self class].allocate already allocated '$processorname' argnum:'$argnum' type:'$type' val:'$value' remaining:$o_remaining allocated:$o_allocated map:$o_map" + } + lappend o_allocated $argnum + set o_allocated [lsort -dictionary $o_allocated] + dict set o_map $argnum [list $owner $type $value] + set scanlist_posn [lsearch $o_remaining $argnum] + set o_remaining [lreplace $o_remaining[set o_remaining {}] $scanlist_posn $scanlist_posn] ;#inlineK + + + + } + + method get_list_unflagged_by_class {classmatch} { + set resultlist [list] + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + switch -- $type { + flag - flagvalue - soloflag {} + default { + lappend resultlist $val + } + } + } + } + return $resultlist + } + + method get_list_flagged_by_class {classmatch} { + set list_flagged [list] + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + switch -- $type { + flag - flagvalue - soloflag { + lappend list_flagged $val + } + } + } + } + return $list_flagged + } + + method get_merged_flagged_by_class {classmatch} { + variable flagcategory + set all_flagged [list] + set seenflag [dict create] ;#key = -flagname val=earliest vindex + dict for {k vinfo} $o_map { + lassign $vinfo class type val + if {[string match $classmatch $class]} { + set a [llength $all_flagged] ;#index into all_flagged list we are building + switch -- $type { + soloflag { + if {[dict exists $seenflag $val]} { + set seenindex [dict get $seenflag $val] + set seenindexplus [expr {$seenindex+1}] + set existingvals [lindex $all_flagged $seenindexplus] + lappend existingvals 1 ;#1 indicating presence - stored as list rather than a count. todo: consider global or per-solo options to support incrementing instead? + lset all_flagged $seenindexplus $existingvals + } else { + dict set seenflag $val $a + lappend all_flagged $val 1 + } + } + flag { + if {![dict exists $seenflag $val]} { + dict set seenflag $val $a + lappend all_flagged $val + } + #no need to do anything if already seen - flagvalue must be next, and it will work out where to go. + } + flagvalue { + set idxflagfor [expr {$k -1}] + set flagforinfo [dict get $o_map $idxflagfor] + lassign $flagforinfo ffclass fftype ffval + #jn "--" following a flag could result in us getting here accidentaly.. review + set seenindex [dict get $seenflag $ffval] + if {$seenindex == [expr {$a-1}]} { + #usual case - this is a flagvalue following the first instance of the flag + lappend all_flagged $val + } else { + #write the value back to the seenindex+1 + set seenindexplus [expr {$seenindex+1}] + set existingvals [lindex $all_flagged $seenindexplus] + lappend existingvals $val ;#we keep multiples as a list + lset all_flagged $seenindexplus $existingvals + } + } + } + } + } + return $all_flagged + } + method typedrange_class_type_from_arg {argclass argtype} { + #set o_flagcategory [list "flag" "flagvalue" "soloflag"] + if {$argclass eq "unallocated"} { + switch -- $argtype { + flag - flagvalue - soloflag { + return [list unallocated flagtype] + } + default { + if {![string length $argtype]} { + #should only happen if something wrong with the tail_processor - rather than error out, for now at least make it stand out in the . + set argtype UNKNOWN + } + return [list unallocated ${argtype}type] ;#e.g unallocated_operand, unallocated_endofoptions + } + } + } else { + return [list $argclass argtype] ;# e.g command something + } + } + + method get_ranges_from_classifications {classifications} { + #puts stderr "get_ranges_from_classifications $classifications" + #examine classifications and create a list of ranges + set ranges [list];# e.g [list {unallocated 0 4} {cmd1 5 7} {unallocated 8 8} {cmd2 9 9} {cmd3 10 10} {unallocated 11 15}] + set seen_commands [list] + dict for {posn arginfo} $classifications { + set is_new_cmd 0 + set is_sub_cmd 0 + set is_continuation 0 + set rangename [lindex $ranges end 0] + set alloc [lindex $arginfo 0] ;#e.g of form 0 {unallocated operand} 1 {lscmd operand} 2 {lscmd soloflag} 3 {lscmd.dir operand} 4 {unallocated flag} + set cmdname "" + if {$alloc ne "unallocated"} { + if {$alloc ni $seen_commands} { + if {![llength $seen_commands]} { + set cmdname $alloc + set is_new_cmd 1 + } else { + set tail [lindex $seen_commands end] + if {$tail eq "unallocated"} { + set cmdname $alloc + set is_new_cmd 1 + } else { + if {[string first . $alloc] >= 0} { + set prefixcheck [lindex [split $alloc .] 0] + if {![string equal -length [string length $prefixcheck] $prefixcheck $tail]} { + #this is not unallocated, not a subcommand of the previous seen ie new command + set cmdname $alloc + set is_new_cmd 1 + } else { + set cmdname $prefixcheck + set is_sub_cmd 1 + set is_continuation 1 + } + } else { + set cmdname $alloc + set is_new_cmd 1 + } + } + } + } else { + set cmdname $alloc + set is_continuation 1 + } + if {$is_continuation} { + lassign [lindex $ranges end] _cmd n a b + set ranges [lrange $ranges 0 end-1] + lappend ranges [list command $n $a [incr b]] + flagfilter::assert_equal $b $posn + } elseif {$is_new_cmd} { + lappend seen_commands $alloc + if {$rangename eq ""} { + lappend ranges [list command $cmdname $posn $posn] + } else { + lassign [lindex $ranges end] _cmd n a b + lappend ranges [list command $cmdname [incr b] $posn] + flagfilter::assert_equal $b $posn + } + } else { + error "coding error during dispatch" + } + } else { + if {$rangename eq ""} { + lappend ranges [list unallocated mixed 0 0] + } else { + lassign [lindex $ranges end] class n a b + if {$class eq "unallocated"} { + #continuation - extend + set ranges [lrange $ranges 0 end-1] + lappend ranges [list unallocated mixed $a [incr b]] + } else { + #change from allocated to unallocated + lappend ranges [list unallocated mixed [incr b] $posn] + flagfilter::assert_equal $b $posn + } + } + } + } + set rangesbytype [list] + foreach oldrange $ranges { + lassign $oldrange oldrangeclass oldrangetype A B ;#A,B for original range bounds, a,b for bounds of sub-ranges we are creating + set last_type "" + set newrangelist [list] + set inner_range [list 0 0] + if {$oldrangeclass ne "unallocated"} { + #pass through - user can split commands further themselves by referencing the classifications map where each arg position is listed + set last_type $oldrangeclass ;#note the deliberate slight misuse - we are using the 'class' here rather than the type as we aren't looking at types within a command range + lappend rangesbytype $oldrange + } else { + #puts stdout "???????????????????????????????????????????????A$A B$B examining old range:'$oldrange'" + for {set i $A} {$i <= $B} {incr i} { + lassign [lindex $rangesbytype end] last_class last_type a b ;#enough just to use the type without the class + set a_info [dict get $classifications $i] + lassign $a_info argclass argtype v + lassign [my typedrange_class_type_from_arg $argclass $argtype] newrangeclass newrangetype + if {$last_type eq ""} { + lappend rangesbytype [list "unallocated" $newrangetype 0 0] + } else { + if {$last_type eq $newrangetype} { + set rangesbytype [lrange $rangesbytype 0 end-1] + lappend rangesbytype [list $last_class $last_type $a $i] + } else { + lappend rangesbytype [list $newrangeclass $newrangetype $i $i] + } + } + } + } + } + + return [list -ranges $ranges -rangesbytype $rangesbytype] + } + + method grid {} { + set posns [dict keys $o_map] + set col1 [string repeat " " 15] + set col [string repeat " " 4] + set pline "[overtype::left $col1 {var indices}] " + foreach p $posns { + append pline [overtype::left $col $p] + } + set remline "[overtype::left $col1 {unallocated}] " + foreach vidx $posns { + if {$vidx ni $o_remaining} { + append remline [overtype::left $col "."] + } else { + set tp [lindex [dict get $o_map $vidx] 1] + #set tp [string map $o_codemap $tp] + if {[dict exists $o_codemap $tp]} { + set tp [dict get $o_codemap $tp] + } + append remline [overtype::left $col $tp] + } + } + set cmdlist [list] + dict for {vidx info} $o_map { + if {[lindex $info 0] ne "unallocated"} { + set c [lindex [split [lindex $info 0] .] 0] + if {$c ni $cmdlist} { + lappend cmdlist $c + } + } + } + set clinelist [list] + foreach c $cmdlist { + set cline "[overtype::left $col1 $c] " + dict for {vidx info} $o_map { + lassign $info class type v + if {($c eq $class) || [string equal -length [string length "$c."] "$c." $class]} { + #set tp [string map $o_codemap $type] + if {[dict exists $o_codemap $type]} { + set tp [dict get $o_codemap $type] + } + append cline [overtype::left $col $tp] + } else { + append cline [overtype::left $col "."] + } + } + lappend clinelist $cline + } + + + set aline "[overtype::left $col1 {allocated}] " + foreach vidx $posns { + if {$vidx ni $o_allocated} { + append aline [overtype::left $col "."] + } else { + set tp [lindex [dict get $o_map $vidx] 1] + #set tp [string map $o_codemap $tp] + if {[dict exists $o_codemap $tp]} { + set tp [dict get $o_codemap $tp] + } + append aline [overtype::left $col $tp] + } + } + + return "$pline\n$remline\n[join $clinelist \n]\n$aline\n" + } + + } + + + #!todo - check if -commandprocessors members will collide with existing -flags in values before moving them + #!todo - skip optional sub-flag value if the next arg following its parent is a flag i.e proper handling of -commandprocessors {cmd {cmd sub "default}} when only cmd supplied. + #!important to fix. At the moment it could eat a further unflagged item in values later in the list which was intended for a different -commandprocessors member! + #add support for -commandprocessors {-cmd {-cmd -othercmd "default"}} to be a safe way to specify a linked -flag move that does the same. + proc allocate_arguments {PROCESSORS solos values cf_args caller} { + set runid [lindex [split [namespace tail $PROCESSORS] _] 1] ;# objname is of form PROCESSORS_pid-threadid-counter where "PROCESSORS_" is a literal + #puts stderr ">>>>>>> solos: $solos" + dict set debugc -debugargs [dict get $cf_args -debugargs] + dict set debugc -source "allocate_arguments $caller" + + set defaults [dict get $cf_args -defaults] + + set cmdprocessor_records [$PROCESSORS get_commandspecs] + + + set sep "\uFFFE" ;#argument-subargument separator (choose something else if this causes problems.. but we want something unlikely (or preferably impossible?) to be in a commandline, ideally a single character, and which at least shows something on screen during debug) + set sepstr "\\uFFFE" ;#for human readable error msg + #\u001E was tried and doesn't output on some terminals) + + set remaining_unflagged [dict create] + + set extra_flags_from_positionals [list] ;#values moved to -values + set moved_to_flagged [dict create] + + #implied_ are values supplied from defaults when a flag or operand was not found + set implied_flagged [list] + set implied_unflagged [list] + + + set dispatch [dict create] + #sanitize and raise error if sep somehow in values + if {[string first $sep $cmdprocessor_records] >= 0} { + do_error "allocate_arguments flags error separator '$sep' ($sepstr) found in values " + } + #-------------------------------------- + set VMAP [flagfilter::class_vmap create flagfilter::VMAP_$runid $values] + #-------------------------------------- + + set unconsumed_flags_and_values [list] + set unflagged [dict create] + + ###################### + #main -commandprocessors loop which scans the valuelist + set values_index 0 ;#track where we are up to as we allocate values to unflagged elements + set source_values $values ;#start with all including -flagged + + #todo - get rid of most of these flags last_x_was etc - and just do lookups into the v_map + # as this will probably involve *lots* of small functiona calls - keep this boolean version and check for performance issues. + set a_index 0 + set is_args_flag 0 + set last_arg_was_paramflag 0 ;#a flag that expects a parameter to follow + set last_arg_was_solo 0 + set solo_flags [dict keys $solos] ;#solos is a dict of -flag (preprocessed) + set end_of_options 0 + set end_of_options_index -1 ;#as later processors can rescan - we need to make sure they only look at arguments after this point + set last_p_found [dict create by "" index "" item ""] + set sequence 0 + set argerrors [list] ;#despite being a list - we will break out at first entry and return for now. + set parsestatus "ok" + + #set LAUNCHED [oolib::collection create col_processors_launched_$runid] + #set MATCHED [oolib::collection create col_processors_matched_$runid] + #oo::objdefine col_processors_matched_$runid { + # method test {} { + # return 1 + # } + #} + + #set objp [$PROCESSORS object_from_record $p] ;#temp convenience + + foreach objp [$PROCESSORS items] { + set objparent [$objp parent] + #$LAUNCHED add $objp [$objp name] + set p [$objp get_def] ;#individual record e.g {mycmd {match run singleopts {-x}}} or {tail_processor {}} + + lassign $p parentname pinfo + set is_sub [$objp is_sub] ;#is subargument - should look to see if last related spec got a value and abort if not. + set is_p_flag [$objp is_flag] ;#sub can be a flag even if parent isn't + set processorname [$objp name] + if {[$objp is_sub]} { + if {![[$objp parent] found_match]} { + continue + } + set p_sub [dict get $pinfo sub] + } + do_debug 3 $debugc " =========================>> p $p sequence:$sequence a_index $a_index" + + if {$processorname in [list "global" "tail_processor"]} { + dict set last_p_found by $processorname + #dict set last_p_found index $a_index + #dict set last_p_found item $a + } + # -format {x {sub y default "default"}} means y is dependent on x being present and shouldn't eat if the next value isn't flaglike + # -format {-x {sub -y}} does the same for moving positionals to the flagged list. + + + #set remaining_values [lrange $source_values $a_index end] + ##################################### + # full rescans for later processors + set remaining_values $source_values ;#source_values shrinks as commands take arguments + set a_index 0 + ##################################### + + do_debug 3 $debugc "-------->________>p '$processorname' remaining vals $remaining_values" + + #!todo - use v_map as an extra determinant to stop sequence for a command-set. (don't extend beyond where args have already been snipped by another command) + if {[$objp name] eq "tail_processor"} { + set mapcopy [flagfilter::class_vmap new {}] ;#no need to supply values as we are copying data from $VMAP + $VMAP copy_to $mapcopy + $objp set_map_object $mapcopy + } else { + $objp set_map_object $VMAP + } + foreach a $remaining_values { + set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index] + if {![string is integer -strict $argnum]} { + error "arg '$a' scan_index:$a_index - calculated argnum:'$argnum' is invalid" + + } + set sub_operand 0 + do_debug 3 $debugc "$argnum >eoptions_idx:$end_of_options_index a_index:$a_index __________________________________________________________a $a" + if {$end_of_options_index > -1} { + set end_of_options [expr {$a_index >= $end_of_options_index}] + } + + #review - data with leading - may be unintentionally interpreted as a flag + if {[string trim $a] eq "--"} { + #generally means end of options processing.. + #review - pass -- through?? + set last_arg_was_paramflag 0 ;#we don't treat first arg following end_of_options as belonging to the flag! - it is potentially an operand to the command + set is_solo_flag 0 + set end_of_options 1 + set end_of_options_index $a_index + #if {[lindex $p 0] eq "tail_processor"} { + $objp allocate $argnum "endofoptions" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + #} + } else { + if {($last_arg_was_paramflag) && ([$objp arg_is_defined_solo_to_me $a])} { + #last flag expecting param - but this flag *known* to be solo + #keep it simple and break out at first solo_flag related error ...unless it is trailing flag in the list + lappend argerrors [list flagerror solo_flag_following_non_solo_flag bad_flag $a] + set last_arg_was_solo 1 + break + } + #set is_solo_flag [expr {($a in $solo_flags)}] + #set is_solo_flag [is_this_flag_solo $a $solo_flags $objp] + set is_solo_flag [$objp arg_is_defined_solo_to_me $a] + + if {!$end_of_options} { + if {!$last_arg_was_paramflag} { + if {!$is_solo_flag} { + set is_args_flag [is_candidate_toplevel_param_flag $a $solo_flags $cmdprocessor_records] + #set is_args_flag [string match -* $a] + } + if {$is_args_flag || $is_solo_flag} { + if {[dict get $last_p_found by] eq $processorname} { + if {![is_this_flag_for_me $a $objp $cf_args]} { + if {$processorname ne "globalXXX"} { + do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit an unrecognized option: $a" + break + } + } + } + } + } else { + #last was flag expecting a param + set is_args_flag 0 + set is_solo_flag 0 + } + } else { + #end_of_options - ignore solo and other flags now. + set is_args_flag 0 + set is_solo_flag 0 + set last_arg_was_paramflag 0 + + } + + #puts stderr "!!!!!!!!!!!!!!!!!!1 here is_args_flag:$is_args_flag" + do_debug 3 $debugc " >________>________>is_p_flag: $is_p_flag last_arg_was_paramflag:$last_arg_was_paramflag is_args_flag:$is_args_flag is_solo: $is_solo_flag (soloflags:$solo_flags) a:$a " + if {!$is_args_flag && !$is_solo_flag } { + + if {!$last_arg_was_paramflag} { + if {[dict get $last_p_found by] eq $processorname} { + if {$processorname ne "tail_processor"} { + #we already found our unflagged value - and now we've hit another - time to break and hand it to a subcommand processor if any + do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit another value: $a" + break + } + } + set sequence_ok 1 ;#default assumption + set can_allocate [can_this_commandspec_allocate_this_arg $a $p $cf_args] + + if {$can_allocate} { + if {$is_sub} { + #!todo - use v_map as sequence terminator + #check if our find is in sequence + #we are only hunting non-flagged items and the the previous finder removes 1 from the source_values list + #therefore the a_index of our find should be the same if we are processing the very next argument. + #we have already checked that it was a related entity which found the last one. + #todo - review if it matters when parents/siblings don't eat all the way up to the next -flag. + #todo - implement a 'gather' key to keep eating in sequence and accumulate the values as a list + if {$a_index > [dict get $last_p_found index]} { + do_debug 3 $debugc "OUT OF SEQUENCE a_index:$a_index vs last_found index:[dict get $last_p_found index], $processorname disengaging - ignoring value $a and leaving it to the next processor" + set last_arg_was_paramflag 0 + do_debug 3 $debugc "<--- breaking --->" + break + } elseif {$a_index < [dict get $last_p_found index]} { + #too early.... found something before previous match + do_debug 3 $debugc "+++++++++++++++out of sequence $processorname - too early.. keeping scanning" + set sequence_ok 0 + } + if {$sequence_ok} { + set sub_operand 1 + } + } + } + + if {$can_allocate && $sequence_ok} { + #found a non-flagged value in the argumentlist to either reallocate to flagged values or to positional values + if {[dict exists $pinfo dispatch]} { + if {!$is_sub} { + #this must be the arg that caused the match + dict set dispatch $parentname [list command [dict get $pinfo dispatch] matched $a arguments [list] raw [list]] + } else { + #todo + lappend argerrors [list unsupported_dispatch $processorname] + } + } + if {$sub_operand} { + if {[dict exists $dispatch $parentname]} { + #todo - defaults? + add_dispatch_argument "dispatch" $parentname $processorname $a + add_dispatch_raw "dispatch" $parentname $a + } else { + #warning? + #lappend argerrors [list subcommand_unable_to_add_operand $processorname] + do_debug 3 $debugc "subcommand $processorname aborting scanning because parent command wasn't activated" + break + } + } + do_debug 2 $debugc " >+++++++>++++++++>++++++++>setting $processorname [if {$is_p_flag} {list -} {}]value $a" + if {$processorname eq "tail_processor"} { + set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index] + set argname arg$argnum + lappend remaining_unflagged $argname $a + lappend unconsumed_flags_and_values $a + dict set unflagged $argname $a + } elseif {$is_p_flag} { + $objp set_matched_argument $argnum $a + if {$is_sub} { + dict set extra_flags_from_positionals $p_sub $a + } else { + dict set extra_flags_from_positionals $parentname $a + } + lappend moved_to_flagged $processorname $a + #if has dependent commands ? - check for deep subcommand match? + } else { + $objp set_matched_argument $argnum $a + #lappend positional_values $a + dict set unflagged $processorname $a + } + do_debug 4 $debugc " >________>________>________>source_values :'$source_values'" + do_debug 3 $debugc " >________>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]" + + #---------------------------- + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + #------------------------------ + $objp allocate $argnum "operand" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr values_index ;#only increment when we allocate a value to one of the members of -commandprocessors + set last_arg_was_paramflag 0 + if {$processorname ne "tail_processor"} { + #don't break until we hit an unrecognized flag or another unflagged value + incr a_index -1 + #don't increment a_index before break, because we have shortened the list by 1. + #do_debug 3 $debugc "----breaking---" + #break + } else { + #decrement to compensate for shortened list because tail_processor continues to end + incr a_index -1 + } + } + + } else { + #last_arg_was_paramflag + set lastarg [dict get $last_p_found item] + #puts stdout "+++ lastarg: $lastarg source_values: [dict get $last_p_found source_values] a_index: $a_index" + if {$processorname eq "tail_processor"} { + lappend unconsumed_flags_and_values $a + } + if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $lastarg $objp $cf_args]} { + update_dispatch_argument "dispatch" $parentname $lastarg $a + add_dispatch_raw "dispatch" $parentname $a + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + $objp allocate $argnum "flagvalue" $a + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + } + set last_arg_was_paramflag 0 + } + } else { + # is a flag of some sort ({!$is_args_flag && !$is_solo_flag} = false) + if {$processorname eq "tail_processor"} { + lappend unconsumed_flags_and_values $a + } + if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $a $objp $cf_args]} { + if {$is_solo_flag} { + add_dispatch_argument "dispatch" $parentname $a 1 + add_dispatch_raw "dispatch" $parentname $a + set last_arg_was_solo 1 + set last_arg_was_paramflag 0 + $objp allocate $argnum "soloflag" $a + } else { + add_dispatch_argument "dispatch" $parentname $a "" + add_dispatch_raw "dispatch" $parentname $a + set last_arg_was_solo 0 + set last_arg_was_paramflag 1 + $objp allocate $argnum "flag" $a + } + dict set last_p_found by $processorname + dict set last_p_found index $a_index + dict set last_p_found item $a + do_debug 4 $debugc " >2_______>________>________>source_values :'$source_values'" + do_debug 3 $debugc " >2_______>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]" + set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK + incr a_index -1 + } else { + #auto alternate based on last value.. unless end_of_options + if {!$end_of_options} { + if {$a in $solo_flags} { + set last_arg_was_solo 1 + set last_arg_was_paramflag 0 + } else { + set last_arg_was_paramflag 1 + } + } + if {$a_index eq ([llength $source_values]-1)} { + #puts "XXXXXXXXXXXXXXXXXXX $a_index source_values:'$source_values'" + #if at end of list don't retain any last..was info. + set last_arg_was_solo 0 + set last_arg_was_paramflag 0 + } + #skip - don't eat + } + } + } + incr a_index + } + + if {![$objp found_match]} { + + #after break - we have retained vars: $parent, $sub_operand $pinfo $processorname etc + #didn't find an unflagged var - set a default if one was specified. + #do nothing otherwise - check_args will determine if it was -required etc. + #review - should only apply if parent cmd found something? + if {[dict exists $pinfo default]} { + set defaultval [dict get $pinfo default] + if {$is_p_flag} { + if {$is_sub} { + dict set extra_flags_from_positionals $p_sub $defaultval + } else { + dict set extra_flags_from_positionals $processorname $defaultval + } + #lappend moved_to_flagged $processorname $defaultval + lappend implied_flagged $processorname $defaultval + do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval implied_flagged: $implied_flagged " + } else { + lappend implied_unflagged $processorname $defaultval + dict set unflagged $processorname $defaultval + do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval moved_to_flagged: $moved_to_flagged " + } + + if {$is_sub && !$sub_operand} { + if {[dict exists $dispatch $parentname]} { + add_dispatch_argument "dispatch" $parentname $processorname $defaultval + } else { + lappend argerrors [list subcommand_unable_to_add_default_operand $processorname $defaultval] + } + } + } + } + + if {[$objp name] eq "tail_processor"} { + $VMAP update_map_from [$objp get_map_object] + } + + if {[llength $argerrors]} { + set parsestatus "error" + #abort processing at first error - we won't be able to make sense of the remaining args anyway + #even the tail_processor won't be able to classify reliably because flag meanings depend on the configured commands + break + } + } + + #assertion - should be none? + #set remaining_values [lrange $source_values $a_index end] + #do_debug 3 $debugc "-------->________>end of processing - remaining vals $remaining_values" + + do_debug 2 $debugc "========>=========>originals : $values" + do_debug 2 $debugc "[$VMAP get_map]" + do_debug 2 $debugc "========>=========>unconsumed: $unconsumed_flags_and_values" + + + + + + set all_flagged [$VMAP get_merged_flagged_by_class *] + set all_flagged_plus [concat $all_flagged $extra_flags_from_positionals] + + set all_flagged_list [$VMAP get_list_flagged_by_class *] + set all_flagged_list [concat $all_flagged_list $extra_flags_from_positionals] + + set remaining_flagged [$VMAP get_merged_flagged_by_class "unallocated"] + + set remaining_flagged_list [$VMAP get_list_flagged_by_class "unallocated"] + + + set unflagged_list_in_processing_order [dict values $unflagged] + set unflagged_list [$VMAP get_list_unflagged_by_class *] + + set unflagged_list_remaining [$VMAP get_list_unflagged_by_class "unallocated"] + + return [dict create \ + listremaining $unconsumed_flags_and_values \ + parseerrors $argerrors \ + parsestatus $parsestatus \ + flagged $all_flagged_plus \ + flaggedlist $all_flagged_list \ + flaggedremaining $remaining_flagged \ + flaggedlistremaining $remaining_flagged_list \ + unflagged $unflagged \ + unflaggedlist $unflagged_list \ + unflaggedremaining $remaining_unflagged \ + unflaggedlistremaining $unflagged_list_remaining \ + flaggednew $extra_flags_from_positionals \ + arglist [concat $unflagged_list_in_processing_order $all_flagged] \ + arglistremaining [concat $unflagged_list_remaining $remaining_flagged] \ + impliedflagged $implied_flagged \ + impliedunflagged $implied_unflagged \ + dispatch $dispatch \ + classifications [$VMAP get_map] \ + gridstring "\n[$VMAP grid]" \ + vmapobject "flagfilter::VMAP_$runid" \ + ] + } + + + + + + + + + + + + #specialisation for collection class to contain commandprocessors + # we expect to use only a single instance of this + oo::class create col_allprocessors { + superclass oolib::collection + variable o_commandspecs + method add_processor {p} { + my add $p [$p name] + if {[$p is_sub]} { + set parentname [$p parentname] + set obj_parent [my item $parentname] + set col_siblings [$obj_parent children] + $col_siblings add $p [$p name] + } + } + method set_commandspecs {cspecs} { + set o_commandspecs $cspecs + } + method get_commandspecs {} { + set o_commandspecs + } + #treating as singleton.. todo tidy + method name_from_record {rec} { + lassign $rec parentname pinfo + if {[dict exists $pinfo sub]} { + set name [join [list $parentname [dict get $pinfo sub]] .] + } else { + set name $parentname + } + return $name + } + method object_from_record {rec} { + set name [my name_from_record $rec] + return [my item $name] + } + #basic check if arg may consume the following one - not based on any specific info from processors + method arg_appears_standalone {f} { + if {(![string match "-*" $f]) && (![string match "/*" $f])} { + #not even flaglike + return 1 + } + if {$f in {- --}} { + return 1 + } + } + #does any processor define it as solo + method flag_can_be_solo {f} { + foreach objp [my items] { + if {[$objp arg_is_defined_solo_to_me $f]} { + return 1 + } + } + return 0 + } + } + oo::class create col_parents { + superclass oolib::collection + method add_parent {p} { + if {[$p is_sub]} { + error "cannot add a sub-processor to the main parents collection" + } + my add $p [$p name] + } + } + #each parent processor has a children collection which can only accept processors with sub defined. + oo::class create col_childprocessors { + superclass oolib::collection + variable o_ownername + method set_owner {parentname} { + set o_ownername $parentname + } + #owner of the collection (a parent processor) + method owner {} { + return $o_ownername + } + method add_processor {p} { + if {![$p is_sub]} { + error "processor must have 'sub' element to add to the parent's collection" + } + #check name matches this parent.. + + my add $p [$p name] + } + } + + oo::class create cprocessor { + variable o_runid + variable o_name + variable o_definition + variable o_pinfo + variable o_parentname + variable o_is_sub + variable o_col_children + variable o_mashopts + variable o_singleopts + variable o_pairopts + variable o_longopts + variable o_found_match ;#we directly matched a command trigger or positional argument + variable o_matched_argument + variable o_matched_argnum + variable o_matchspec + variable o_vmap + constructor {definition runid} { + set o_vmap "" + set o_definition $definition + set o_runid $runid + if {([llength $o_definition] < 2) || ([llength [lindex $o_definition 0]] != 1)} { + error "[self class].constructor Unable to interpret definition '$o_definition'" + } + lassign $o_definition o_parentname o_pinfo + if {([llength $o_pinfo] %2) != 0} { + error "[self class].constructor second element of definition '$o_definition' not a dict" + } + set o_is_sub [dict exists $o_pinfo sub] + if {!$o_is_sub} { + set o_name $o_parentname + set o_col_children [::flagfilter::col_childprocessors new] + $o_col_children set_owner $o_name + } else { + set o_name [join [list $o_parentname [dict get $o_pinfo sub]] .] + } + if {[dict exists $o_pinfo match]} { + set o_matchspec [dict get $o_pinfo match] + } else { + set o_matchspec {^[^-^/].*} ;#match anything that isn't flaglike + } + set o_found_match 0 + set o_matched_argument "" ;#need o_found_match to differentiate match of empty string + set o_matched_argnum -1 + #load mashopts etc at construction time as they're static + set o_mashopts [list] + set o_singleopts [list] + set o_pairopts [list] + set o_longopts [list] + if {[dict exists $o_pinfo mashopts]} { + lappend o_mashopts {*}[dict get $o_pinfo mashopts] + } + if {[dict exists $o_pinfo singleopts]} { + lappend o_singleopts {*}[dict get $o_pinfo singleopts] + } + if {[dict exists $o_pinfo pairopts]} { + lappend o_pairopts {*}[dict get $o_pinfo pairopts] + } + if {[dict exists $o_pinfo longopts]} { + lappend o_longopts {*}[dict get $o_pinfo longopts] + } + } + destructor { + catch {$o_vmap destroy} + if {!$o_is_sub} { + $o_col_children destroy + } + } + + method name {} { + return $o_name + } + #open things up during oo transition.. + method get_def {} { + return $o_definition + } + method is_flag {} { + if {[my is_sub]} { + #sub can be a flag even if parent isn't + set subname [dict get $o_pinfo sub] + return [string match -* $subname] + } else { + return [string match -* $o_name] + } + } + method has_same_parent {other} { + return [expr {[other parentname] eq $o_parentname}] + } + method is_sub {} { + return $o_is_sub + } + + method set_map_object {map} { + set o_vmap $map + } + method get_map_object {} { + return $o_vmap + } + method allocate {argnum type val} { + if {$o_vmap eq ""} { + error "[self class].allocate ($o_name) vmap is not set." + } + $o_vmap allocate [self object] $argnum $type $val + } + + method found_match {} { + return $o_found_match + } + method matched_argument {} { + return $o_matched_argument + } + method matched_argnum {} { + return $o_matched_argnum + } + method set_matched_argument {argnum a} { + #could be empty string + if {$o_found_match} { + error "[self object].set_matched_argument processor:$o_name already found match '$o_matched_argument' - cannot set again" + } + if {![my can_match $a]} { + error "error [self class].set_matched_argument processor:$o_name cannot match '$a' (matchspec: $o_matchspec)" + } + set o_found_match 1 + set o_matched_argument $a + set o_matched_argnum $argnum + } + method has_explicit_matchspec {} { + return [dict exists $o_pinfo match] + } + method matchspec {} { + return $o_matchspec + } + method can_match {a} { + if {!$o_found_match} { + foreach m $o_matchspec { + if {[regexp -- $m $a]} { + return 1 + } + } + return 0 + } else { + return 0 + } + } + #?? + method can_allocate_flags {} { + } + + + + + + #if we are a parent - this is own name + method parentname {} { + return $o_parentname + } + method parent {} { + return [::flagfilter::obj::PARENTS_$o_runid item $o_parentname] + } + method is_parent {} { + return [expr {!$o_is_sub}] + } + method children {} { + if {!$o_is_sub} { + return $o_col_children + } else { + #raise error? + return "" + } + } + method mashopts {} { + return $o_mashopts + } + method singleopts {} { + return $o_singleopts + } + method pairopts {} { + return $o_pairopts + } + method longopts {} { + return $o_longopts + } + + #whether flag categorized as solo by this processor + method arg_is_defined_solo_to_me {a} { + if {(![string match "-*" $a]) && (![string match "/*" $a])} { + #not even flaglike + return 0 + } + if {[my can_match $a]} { + return 0 + } + if {$a in {- --}} { + #specials not defined as solos + return 0 + } + + if {$o_name eq "global"} { + + } elseif {$o_name eq "tail_processor"} { + + } + + if {$a in $o_singleopts} { + return 1 + } + if {"any" in $o_singleopts} { + return 1 + } + set equalposn [string first "=" $a] + if {$equalposn >=1} { + if {"any" in $o_longopts} { + return 1 + } else { + set namepart [string range $a 0 $equalposn-1] + foreach lo $o_longopts { + if {[string match "${namepart}=*" $lo]} { + return 1 + } + } + } + } + #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash + #- but if it's a pairopt, but not mashable - we can rule it out now + if {($a in $o_pairopts) && ($a ni $o_mashopts)} { + return 0 + } + set flagletters [split [string range $a 1 end] ""] + set posn 1 + #trailing letters may legitimately not be in mashopts if they are part of a mashed value + #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing + foreach l $flagletters { + if {"-$l" ni $o_mashopts} { + #presumably an ordinary flag not-known to us + return 0 + } else { + if {"-$l" in $o_pairopts} { + if {$posn == [llength $flagletters]} { + #in pairopts and mash - but no value for it in the mash - thefore not a solo + return 0 + } else { + #entire tail is the value - this letter is effectively solo + return 1 + } + } elseif {"-$l" in $o_singleopts} { + #not allowed to take a value - keep processing letters + } else { + #can take a value! but not if at very end of mash. Either way This is a solo + return 1 + } + } + } + #This object should not treat the flag as a known solo + #- so if it is allowed to consume it, it may fall back on examining the subsequent argument's flaginess(?) + return 0 + } + + + method get_opts {} { + return [list mashopts $o_mashopts singleopts $o_singleopts pairopts $o_pairopts longopts $o_longopts] + } + #include parent opts + #we use the terminology 'option' for "-" prefixed items belonging to a -commandprocessors spec as opposed to more general -flags + #Note - this may also be called on the default "tail_processor", which will return empty sets, or an overridden tail_processor which may have data + method get_combined_opts {} { + set objparent [::flagfilter::obj::PARENTS_$o_runid item $o_parentname] + set parentopts [$objparent get_opts] + set mashopts [dict get $parentopts mashopts] + set singleopts [dict get $parentopts singleopts] + set pairopts [dict get $parentopts pairopts] + set longopts [dict get $parentopts longopts] + if {[my is_sub]} { + #this spec is a sub + set subopts [my get_opts] + #does order matter? could use struct::set union ? + foreach m [dict get $subopts mashopts] { + if {$m ni $mashopts} { + lappend mashopts $m + } + } + foreach s [dict get $subopts singleopts] { + if {$s ni $singleopts} { + lappend singleopts $s + } + } + foreach po [dict get $subopts pairopts] { + if {$po ni $pairopts} { + lappend pairopts $po + } + } + foreach lo [dict get $subopts longopts] { + if {$lo ni $longopts} { + lappend longopts $lo + } + } + + } + return [list mashopts $mashopts singleopts $singleopts pairopts $pairopts longopts $longopts] + } + + } + + + + + + + + + + + + proc get_command_info {cmdname cspecs} { + foreach item $cspecs { + lassign $item cmd specinfo + if {$cmd eq $cmdname && [dict exists $specinfo dispatch]} { + return $specinfo + } + } + return [list] + } + #### check_flags + # does not support unvalued flags - unless explicitly specified in -soloflags (global) or in -singleopts for a commandprocessor + #e.g not supported: v1 v2 -arg1 arg1val -debug -anotherflag anotherflagval + # - unless -soloflags is something like -soloflags {-debug} or -soloflags {{-debug 1}} where 1 is the default. In this case - we can no longer support accepting a value for -soloflags - the processor will not assign it an argument from the commandline. + #e.g not supported (unless -debug in -soloflags): v1 v2 -arg1 arg1val -anotherflag anotherflagval -debug + #e.g supported: v2 v2 -arg1 arg1val -debug 1 -anotherflag anotherflagval + # supports positional arguments - but only if specified in -commandprocessors + # todo + # - supports -- for treating following arg as value even if it looks like a flag + # - supports - for reading stdin + # expects at least -values + # other options -caller -defaults -required -extras -commandprocessors + # -soloflags (these are flags that *must* be solo - ie they cannot take an argument ) if no default specified they are boolean defaulting to 1, repeated instances in -values will be appended to a list. + # The only flag that can be a mix of solo or not, is the very last flag in the values list. In this case it must not be in the -soloflags list, but it will default to a boolean 1 to indicate presence. + proc check_flags {args} { + set runid [flagfilter::get_new_runid] + #################################################### + #puts "Entered checkflags, args $args" + set distanceToTop [info level] + set callerlist [list] + set was_dispatched_by_another 0 ;#used to + for {set i 1} {$i < $distanceToTop} {incr i} { + set callerlevel [expr {$distanceToTop - $i}] + set callerinfo [info level $callerlevel] + set firstword [lindex $callerinfo 0] + if {[string match "*check_flags*" $firstword]} { + set was_dispatched_by_another 1 + } + lappend callerlist $firstword + } + #puts stdout "callerlist: $callerlist" + + #first handle args for check_flags itself + if {[catch {lindex [info level -1] 0} caller]} { + set caller "" + } + #puts stderr ">>>>check_flags caller $caller" + get_one_paired_flag_value {-x 1} -x ;# + + #manually check for -caller even if unbalanced args + #we only need to use get_one_paired_flag_value because we haven't yet checked args is a properly formed paired list and if -caller is present we want to use it for clearer error messages. + #use normal dict operations to retrieve other flags. + #if failed to retrieve.. fall through to checks below + if {![catch {get_one_paired_flag_value $args -caller} flag_value_result]} { + set caller $flag_value_result + } + #puts stderr ">>>>check_flags caller $caller" + + + + + set cf_defaults [dict create\ + -caller $caller\ + -return [list arglistremaining]\ + -match [list]\ + -commandprocessors [list]\ + -soloflags [list]\ + -extras [list]\ + -defaults [list]\ + -required [list]\ + -values \uFFFF\ + -debugargs 0\ + ] + dict set cf_defaults -debugargsonerror 1 ;#error level to use when dispatch error occurs.. will not set lower than -debugargs + + + + if {([llength $args] % 2) != 0} { + do_error "check_flags error when called from '$caller' :check_flags must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $cf_defaults]' \n got: $args" + } + set cf_args $cf_defaults + foreach {k v} $args { + switch -- $k { + -caller - -return - -match - -commandprocessors - -soloflags - -extras - -defaults - -required - -values - -debugargs - -debugargsonerror { + dict set cf_args $k $v + } + default { + do_error "check_flags error when called from ${caller}: Unknown option '$k': must be one of '[dict keys $cf_defaults]' \nIf calling check_flags directly, put args being checked in -values {...}" + } + } + } + unset args + #################################################### + #now look at -values etc that check_flags is checking + + set caller [dict get $cf_args -caller] + + set debugargs [dict get $cf_args -debugargs] + dict set debugc -debugargs [dict get $cf_args -debugargs] + dict set debugc -source "check_flags $caller" + do_debug 1 $debugc "DEBUG-START $caller" + + set returnkey [dict get $cf_args -return] + set defaults [dict get $cf_args -defaults] + if {([llength $defaults] % 2) != 0} { + do_error "check_flags error when called from '$caller' :-defaults must be a list containing an even number of arguments of form: -flag value'" + } + set required [dict get $cf_args -required] + + + set acceptextra [dict get $cf_args -extras] + + set supplied [string trim [dict get $cf_args -values]] + set soloflags [dict get $cf_args -soloflags] ;#By their nature - solo flags are unlikely to be automatically 'required' - review + set solos_with_defaults [list] + foreach solo_spec $soloflags { + if {[llength $solo_spec] == 1} { + lappend solos_with_defaults $solo_spec 1 + } else { + lappend solos_with_defaults [lindex $solo_spec 0] [lindex $solo_spec 1] + } + + } + + if {$debugargs >= 3} { + set prefix "| $caller>" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + puts -nonewline stderr "$prefix input\n" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #puts stderr "$caller $cf_args" + dict for {k v} $cf_args { + if {$k ne "-commandprocessors"} { + puts -nonewline stderr "$prefix \[$k\]\n" + puts -nonewline stderr "$prefix $v\n" + } + } + if {$debugargs >=4} { + puts -nonewline stderr "$prefix \[-commandprocessors\]\n" + foreach record [dict get $cf_args -commandprocessors] { + puts -nonewline stderr "$prefix $record\n" + } + } + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #dict for {key val} $cf_args { + # puts stderr " $key" + # puts stderr " $val" + #} + } + + + ################################################################################################## + # allocate_arguments does the main work of processing non-flagged items in the main supplied argument list into flagged versions depending on the specs in -commandprocessors + # It sets defaults only for those arguments processed by a '-commandprocessors' spec. + # We must supply it with the -soloflags info because the solo flags affect what is considered an operand. + set command_specs [dict get $cf_args -commandprocessors] ;#may be empty list - that's ok - it will still populate the 'flagged' and 'arglist' return-dict members. + + #some of these are keys returned by allocate_arguments + # - some (e.g supplied) are added by check_flags + # This list is the list of -return values that can be used with check_args + set flaginfo_returns [list \ + parseerrors \ + parsestatus \ + flagged \ + flaggedremaining \ + flaggednew \ + unflagged \ + unflaggedremaining \ + unflaggedlistremaining \ + listremaining \ + arglist \ + arglistremaining \ + impliedunflagged \ + impliedflagged \ + classifications \ + gridstring \ + ranges \ + dispatch \ + dispatchstatuslist \ + dispatchresultlist \ + dispatchstatus \ + supplied \ + defaults \ + status \ + vmapobject \ + ] + + set PROCESSORS [col_allprocessors create ::flagfilter::obj::PROCESSORS_$runid] + set PARENTS [col_parents create ::flagfilter::obj::PARENTS_$runid] + + # + #set command_specs [concat [list {global {}}] $command_specs] + lappend command_specs {tail_processor {}} + + foreach cspec $command_specs { + set obj [cprocessor new $cspec $runid] ;#runid gives access to the context-objects PROCESSORS_runid & PARENTS_runid + if {[$obj is_parent]} { + $PARENTS add_parent $obj + } + #do_debug 1 $debugc "CONFIGURING OBJECT for commandprocessor [$obj name]" + $PROCESSORS add_processor $obj + } + do_debug 1 $debugc "ADDED [$PROCESSORS count] processors to main commandprocessor collection" + do_debug 1 $debugc "ADDED [$PARENTS count] processors to the parents collection" + $PROCESSORS set_commandspecs $command_specs + + #allocate_arguments uses the PROCESSORS object + set processed_arguments [allocate_arguments $PROCESSORS $solos_with_defaults $supplied $cf_args $caller] + #set processed_arguments [allocate_arguments {} $supplied] + + set newly_flagged_positionals [dict get $processed_arguments flaggednew] + set unflaggedremaining [dict get $processed_arguments unflaggedremaining] + set unflaggedlistremaining [dict get $processed_arguments unflaggedlistremaining] + set dispatch [dict get $processed_arguments dispatch] + set flaggedremaining [dict get $processed_arguments flaggedremaining] + set RETURNED_VMAP [dict get $processed_arguments vmapobject] + + + + if {$debugargs >= 3} { + set prefix "| $caller>" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + puts -nonewline stderr "$prefix output\n" + puts -nonewline stderr "$prefix [string repeat - 30]\n" + #puts stderr "processed_arguments: $processed_arguments" + dict for {key val} $processed_arguments { + puts -nonewline stderr "$prefix $key\n" + puts -nonewline stderr "$prefix $val\n" + } + puts -nonewline stderr "$prefix [string repeat - 30]\n" + } + + ################################################################################################## + + + + + + if {![llength $newly_flagged_positionals]} { + if {($supplied eq "\uFFFF") || ![llength $supplied]} { + #do_error "check_flags error when called from ${caller}: missing or empty -values" + } + } + + #probably not something to enforce... we might pass on unbalanced lists to other check_args etc. + #if {([llength $supplied] % 2) != 0} { + # do_error "${caller}: Error. $caller must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $defaults]'\n received values: $supplied" + #} + + + + set new_arg_list [dict get $processed_arguments arglistremaining] + set flagged_list [dict get $processed_arguments flagged] + #set suppliedkeys_with_extrakeys [concat [dict keys $supplied] [dict keys $newly_flagged_positionals]] + #puts stdout "suppliedkeys and new keys: $suppliedkeys_with_extrakeys" + + #todo - add flaggednew to required if all was specified? + #check invalid flags if not indicated in -extras , either explicitly or with 'extra' + set flags_from_required [get_flagged_only $required {}] + #set known_flags [lsort -unique -nocase [concat [dict keys $defaults] $flags_from_required $soloflags]] ;#why -nocase? why should -l and -L collapse to the uppercase version? + set known_flags [punk::lib::lunique_unordered [concat [dict keys $defaults] $flags_from_required $soloflags ]] + foreach spec $command_specs { + lassign $spec parentname pinfo + if {[string match -* $parentname] && $parentname ni $known_flags} { + lappend known_flags $parentname + } + if {[dict exists $pinfo sub]} { + if {[string match -* [dict get $pinfo sub]]} { + lappend known_flags [dict get $pinfo sub] + } + } + } + do_debug 2 $debugc "------------------->known_flags: $known_flags soloflags:$soloflags" + set invalid_flags [list] + if {"all" ni [string tolower $acceptextra]} { + if {"none" in [string tolower $acceptextra]} { + set ok_extras [list] + } elseif {[llength $acceptextra]} { + set ok_extras $acceptextra + } + #todo + #puts stderr " check_flags - temporary disable of checking for invalid flags" + set pairflagged $flagged_list + foreach {f v} $pairflagged { + if {$f ni $acceptextra && $f ni $known_flags} { + lappend invalid_flags $f + } + } + } + if {[llength $invalid_flags]} { + do_error "check_flags $caller error when called from ${caller}: unknown flags '$invalid_flags'" + } + + set calc_required [list] + set keywords_in_required [lsearch -inline -all -not $required -*] + set bad_keywords_in_required [lsearch -regexp -nocase -all -inline -not $keywords_in_required "all|none"] + if {[llength $bad_keywords_in_required]} { + do_error "check_flags error when called from ${caller}: bad flags in '-required' it must be a list of flags of the form -flagname or ONLY one of the keywords 'none' or 'all'" + } + #keywords_in_required now known to be only comprised of (possibly case variant) values of all|none + if {[llength $keywords_in_required] > 1} { + do_error "check_flags error when called from ${caller}: specifying both 'none' and 'all' in -required is not valid, and repeated values are not valid." + } + if {"none" eq [string tolower [lindex $keywords_in_required 0]]} { + set calc_required [list] + } + set flags [lsearch -inline -all $required -*] + + if {[llength $required]} { + if {[lsearch -nocase $keywords_in_required "all"] >= 0} { + #'all' can be present with other flags - and indicates we also require all the flags from -defaults + dict for {k -} $defaults { + if {$k ni $calc_required} { + lappend calc_required $k + } + } + } + } + + set classifications [dict get $processed_arguments classifications] ;#assertion - ordered by numerically increasing key representing positions in supplied argument list + set rangesets [$RETURNED_VMAP get_ranges_from_classifications $classifications] + set ranges [dict get $rangesets -ranges] + set rangesbytype [dict get $rangesets -rangesbytype] ;#unallocated are split into flag,operand and endofoptions - further splitting is easy enough to do by looking up the classifications list for each position in the supplied arg list. + #tailflags are the same for all dispatch items + set tailflagspaired [tailflagspaired $defaults $supplied $classifications $rangesbytype] + + + set dict_supplied [dict create supplied $supplied] + set dict_defaults [dict create defaults $defaults] + set dict_ranges [dict create ranges $ranges] + set dict_rangesbytype [dict create rangesbytype $rangesbytype] + set raise_dispatch_error_instead_of_return "" + set dict_dispatch_results [list dispatchstatuslist [list] dispatchresultlist [list] dispatchstatus "ok"] + #todo - only dispatch if no unallocated args (must get tail_processor to allocate known flags to 'global') + if {[llength $dispatch]} { + set dispatchstatuslist [list] + set dispatchresultlist [list] + set dispatchstatus "ok" + #each dispatch entry is a commandname and dict + #set dispatchrecord [lrange $dispatch 0 1] + set re_argnum {%arg([0-9^%]+)%} + set re_argtake {%argtake([0-9^%]+)%} + set re_dquotedparts {(?:(?:\"[^\"]*\")|(?:\"[^\"]*"))|(?:\S*[^ \"])} ;#for use with regexp -all -inline + #e.g {"a b" 'b x' "x cd "e f" g a} -> {"a b"} 'b x' {"x cd "} e f {" g a} + #dumb-editor rebalancing quote for above comment " + foreach {parentname dispatchrecord} $dispatch { + set commandinfo [get_command_info $parentname $command_specs] + + do_debug 1 $debugc ">>>>>DISPATCHRECORD: $dispatchrecord" + + # e.g lscmd lscmd natsortcommandline_ls lscmd.dir x + + do_debug 2 $debugc "commandinfo for $parentname: $commandinfo" + set command [dict get $dispatchrecord command] + #support for %x% placeholders in dispatchrecord command + set command [string map {%match% %matched%} $command] ;#alias + set command [string map [list %matched% [dict get $dispatchrecord matched]] $command] + + set argnum_indices [regexp -indices -all -inline $re_argnum $command] + if {[llength $argnum_indices]} { + foreach {argx_indices x_indices} $argnum_indices { + #argx eg %arg12% + set argx [string range $command {*}$argx_indices] + set x [string range $command {*}$x_indices] + set command [string map [list $argx [lindex [dict get $dispatchrecord arguments] $x]] $command] + } + } + + set argsreduced [dict get $dispatchrecord arguments] + #set rawparts [regexp -all -inline $re_dquotedparts [dict get $dispatchrecord raw]] + + #review! + #how will this behave differently on unix + package require punk::winrun + set rawparts [punk::winrun::unquote_wintcl [dict get $dispatchrecord raw]] + #set argtake_indices [regexp -indices -all -inline $re_argtake $command] + + + set start 0 + while {[regexp -start $start -indices $re_argtake $command argx_indices x_indices]} { + #argx eg %argtake12% + set argx [string range $command {*}$argx_indices] + set x [string range $command {*}$x_indices] + set argval [lindex [dict get $dispatchrecord arguments] $x] + set replacementlen [string length $argval] + set command [string map [list $argx $argval] $command] + set start [expr {[lindex $argx_indices 0] + $replacementlen}] + set argsreduced [lremove $argsreduced $x] + set rawparts [lremove $rawparts $x] + } + dict set dispatchrecord arguments $argsreduced + if {$start > 0} { + set rawreduced [join $rawparts] + dict set dispatchrecord raw $rawreduced + } + + set argvals [dict get $dispatchrecord arguments] + set matched_operands [list] + set matched_opts [list] + set matched_in_order [list] + set prefix "${parentname}." + set prefixlen [string length $prefix] + foreach {k v} $argvals { + #puts "$$$$ $k" + if {[string equal -length $prefixlen $prefix $k]} { + #key is prefixed with "commandname." + set k [string replace $k 0 $prefixlen-1] + } + #todo - -- ? + if {[string match -* $k]} { + lappend matched_opts $k $v + lappend matched_in_order $k $v + } else { + set kparts [split $k .] + lappend matched_operands $v + lappend matched_in_order $v + } + } + + if {![dict exists $commandinfo dispatchtype]} { + set dispatchtype tcl + } else { + set dispatchtype [dict get $commandinfo dispatchtype] + } + if {![dict exists $commandinfo dispatchglobal]} { + if {$dispatchtype eq "tcl"} { + set dispatchglobal 1 + } else { + set dispatchglobal 0 + } + } else { + set dispatchglobal [dict get $commandinfo dispatchglobal] + } + #generally we only want to dispatch remaining flagged, and only at the tail end.(as opposed to flags occurring between command groups) + # -It doesn't usually make much sense to dispatch remaining unflagged items, and it would be rare to require flags occurring before the command. + #however - there are potential commands such as help, dryrun or maybe an analysis command that may need to see unconsumed operands or even look 'back' at prior items + ##update 2023-03 - we definitely want to look back to prior non-matches when we match on a script e.g tclsh8.6 -someflag etc xxx.tcl scriptarg1 -etc + # if we match and dispatch on *.tcl - then we may need 'tclsh8.6 -someflag etc' as the interpreter (possibly with arguments) to use. + # we may need a 'script' dispatchtype (as well as the option to just pass these prior arguments as additional options for some other dispatchtypes) + # + # todo - add supported dispatchglobal values such as all, pre, post, allpre, allpost, and classifications + # where pre & post are only those occurring directly before and after the command and its args, i.e not extending beyond any prior or subsequent other command. + # classifications would be flagged as -classifications $classifications whereas pre and post would be added directly if specified singly, or flagged with -pre, -post etc if multiple are specified + # Those beginning with 'all' should also be wrapped in flags, because potentially they come from disjointed sections of the argumentlist + # - and we generally shouldn't supply arguments next to each other that weren't contiguous in the original list + # The 1,true,yes,tailflagspaired value is designed for the usecase where a common set of tail flags e.g -debug can apply to any commands matched by the filter. + # tail = all unallocated args after final command, including operands and end-of-options '--' (todo) + # tailflags = all unallocated *contiguous* flags after the final command and final operands. (ie it will deliberately miss flags following last command if there is a later operand) (todo) + # tailflagspaired = same as tailflags, but any solo-flags are defaulted to 1 (flags not merged, so there might be duplicate keys) so that it's a fully paired list + # In other situations - post may make sense to get the very next set of unconsumed arguments. + if {[string tolower $dispatchglobal] in [list 1 true yes tailflagspaired]} { + set command_range_posn [lsearch -index 1 $ranges $parentname] + set extraflags $tailflagspaired + } else { + set extraflags [list] + } + + #jn concat allows $command to itself be a list + ##tcl dispatchtype + dict set dispatchrecord dispatchtype $dispatchtype + switch -- $dispatchtype { + tcl { + do_debug 1 $debugc "DISPATCHING with tcl arg order: $command $matched_operands $matched_opts $extraflags" + #set commandline [list $command {*}$matched_operands {*}$matched_opts {*}$extraflags] + set commandline [concat $command $matched_operands $matched_opts $extraflags] + } + raw { + do_debug 1 $debugc "DISPATCHING with raw args : $command [dict get $dispatchrecord raw]" + #set commandline [list $command {*}[dict get $dispatchrecord raw] {*}$extraflags] + set commandline [concat $command [dict get $dispatchrecord raw] $extraflags] + } + shell { + do_debug 1 $debugc "DISPATCHING with shell args : $command [dict get $dispatchrecord raw]" + #assume the shell arguments are in one quoted string? + set commandline [concat $command [list [dict get $dispatchrecord raw]] $extraflags] + } + default { + #non quoted shell? raw + defaults? + do_debug 1 $debugc "DISPATCHING with given arg order: $command $matched_in_order $extraflags" + #set commandline [list $command {*}$matched_in_order {*}$extraflags] + set commandline [concat $command $matched_in_order $extraflags] + } + } + + dict set dispatchrecord asdispatched $commandline + set dispatchresult "" + set dispatcherror "" + if {![catch {{*}$commandline} cmdresult]} { + set dispatchresult $cmdresult + lappend dispatchstatuslist [list status ok cmd $parentname outputlength [string length $cmdresult]] + lappend dispatchresultlist $cmdresult + } else { + set dispatchstatus "error" + set dispatcherror $cmdresult + #don't add to dispatchresultlist + lappend dispatchstatuslist [list status err cmd $parentname outputlength 0 error $cmdresult] + if {!$was_dispatched_by_another} { + #this is the first (or a direct) call to check_flags - so make sure error gets raised in this proc rather than just storing the error in the data and returning + set raise_dispatch_error_instead_of_return "dispatchstatuslist:\n[join $dispatchstatuslist \n] \nerrinfo:\n $::errorInfo" + dict set dispatchrecord result $dispatchresult + dict set dispatchrecord error $dispatcherror + dict set dispatch $parentname $dispatchrecord + + break + #return -code error "check_flags error during command dispatch:\n$cmdresult" + } + #we've been dispatched from another check_flags - so ok to propagate the error up via the dispatchrecord/dispatchstatuslist + } + dict set dispatchrecord result $dispatchresult + dict set dispatchrecord error $dispatcherror + dict set dispatch $parentname $dispatchrecord + } + + set dict_dispatch_results [list dispatchcaller $caller dispatchstatuslist $dispatchstatuslist dispatchresultlist $dispatchresultlist dispatchstatus $dispatchstatus] + } + #end llength $dispatch + + + set combined [dict merge $dict_defaults $dict_supplied $processed_arguments $dict_ranges $dict_rangesbytype $dict_dispatch_results] + dict set combined dispatch $dispatch ;#update with asdispatched info + if {([dict get $combined parsestatus] eq "ok") && ([dict get $combined dispatchstatus] eq "ok")} { + dict set combined status "ok" + } else { + dict set combined status "error" + } + do_debug 1 $debugc "COMBINED:$combined" + + + set returnkey [string tolower $returnkey] + if {"all" in $returnkey} { + set returnval $combined + #set returnval [dict merge $combined $dict_dispatch_results] + } else { + if {[llength $returnkey] == 1} { + set invalid 0 + #todo - support multiple merge? + set right "" + if {[regexp -all {\|} $returnkey] == 1} { + lassign [split $returnkey |] left right + set joinparts [split $left ,] + } else { + set joinparts [split $returnkey ,] + } + foreach j [concat $joinparts $right] { + if {$j ni $flaginfo_returns} { + set invalid 1 + } + } + set returnval [list] + if {!$invalid} { + foreach j $joinparts { + lappend returnval {*}[dict get $combined $j] + } + if {[string length $right]} { + set returnval [dict merge $returnval $defaults $returnval] + } + } else { + set returnval [list callerrors [list "-return '$returnkey' not valid"]] + } + } else { + set callerrors [list] + set returnval [dict create] + foreach rk $returnkey { + if {$returnkey in $flaginfo_returns} { + dict set returnval $rk [dict get $combined $returnkey] + } else { + lappend callerrors [list "-return '$returnkey' not valid"] + } + } + if {[llength $callerrors]} { + dict set returnval callerrors $callerrors + } + } + } + + do_debug 1 $debugc "[string repeat = 40]" + do_debug 1 $debugc "dispatch_results: $dict_dispatch_results" + do_debug 1 $debugc "[string repeat - 40]" + + if {[string length $raise_dispatch_error_instead_of_return]} { + set errdebug [dict get $cf_args -debugargsonerror] + if {$errdebug > [dict get $cf_args -debugargs]} { + dict set debugc -debugargs $errdebug + } + } + + set debuglevel_return 2 + set debugdict [concat {*}[lmap k [dict keys $combined] {list $k $debuglevel_return}]] ;#create a dict of keys from combined, all defaulted to $debuglevel_return + if {[llength [dict get $combined parseerrors]]} { + dict set debugdict "parseerrors" 0 + } else { + dict set debugdict "parseerrors" 2 + } + dict set debugdict "defaults" 1 + dict set debugdict "supplied" 1 + dict set debugdict "dispatch" 1 + dict set debugdict "ranges" 1 + dict set debugdict "rangesbytype" 1 + dict set debugdict "dispatchstatus" 1 + if {[dict get $combined "status"] eq "ok"} { + dict set debugdict "status" 1 + } else { + dict set debugdict "status" 0 + } + + do_debug 1 $debugc "returning '$returnkey'" + do_debug 1 $debugc "returnval '$returnval'" + if {([llength $returnval] % 2) == 0} { + do_debug 1 $debugc "returnkeys '[dict keys $returnval]'" + } + do_debug 1 $debugc "[string repeat = 40]" + dict for {k v} $combined { + set dlev [dict get $debugdict $k] + switch -- $k { + dispatch { + set col1 [string repeat " " 12] + #process as paired list rather than dict (support repeated commands) + set i 0 + foreach {cmdname cmdinfo} $v { + set field1 [string repeat " " [expr {[string length $cmdname]}]] + set col2_dispatch [string repeat " " [expr {[string length $cmdname] + 15}]] + set j 0 + foreach {ckey cval} $cmdinfo { + + if {$i == 0 && $j == 0} { + set c1 [overtype::left $col1 "dispatch"] + } else { + set c1 [overtype::left $col1 { ... }] + } + + if {$j == 0} { + set f1 [overtype::left $field1 $cmdname] + set c2 [overtype::left $col2_dispatch "$f1 $ckey"] + } else { + set f1 [overtype::left $field1 ...] + set c2 [overtype::left $col2_dispatch "$f1 $ckey"] + } + #leave at debug level 1 - because dispatch is generally important + do_debug $dlev $debugc "${c1}${c2} $cval" + + incr j + } + incr i + } + + #do_debug 1 $debugc "[overtype::left $col1 $k] [lindex $v 0] [list [lindex $v 1]]" + #foreach {nm rem} [lrange $v 2 end] { + # do_debug 1 $debugc "[overtype::left $col1 { ... }] $nm [list $rem]" + #} + } + dispatchresultlist { + set col1 [string repeat " " 25] + set i 0 + foreach dresult $v { + if {$i == 0} { + set c1 [overtype::left $col1 $k] + } else { + set c1 [overtype::left $col1 { ... }] + } + do_debug $dlev $debugc "$c1 $dresult" + incr i + } + } + classifications { + set col1 [string repeat " " 25] + set len [dict size $v] + if {$len == 0} { + do_debug $dlev $debugc "[overtype::left $col1 $k]" + continue + } + set max [expr {$len -1}] + set numlines [expr $len / 3 + 1] + if {($len % 3) == 0} { + incr numlines -1 + } + set j 0 + for {set ln 0} {$ln < $numlines} {incr ln} { + if {$ln == 0} { + set c1 "[overtype::left $col1 $k]" + } else { + set c1 "[overtype::left $col1 { ... }]" + } + set line "" + for {set col 0} {$col < 3} {incr col} { + if {$j <= $max} { + append line "$j [list [dict get $v $j]] " + } + incr j + } + do_debug $dlev $debugc "$c1 [string trim $line]" + } + } + gridstring { + set col1 [string repeat " " 25] + set i 0 + foreach ln [split $v \n] { + if {$i == 0} { + set c1 [overtype::left $col1 $k] + } else { + set c1 [overtype::left $col1 { ... }] + } + do_debug $dlev $debugc "$c1 $ln" + incr i + } + } + default { + set col1 [string repeat " " 25] + do_debug $dlev $debugc "[overtype::left $col1 $k] $v" + } + } + } + + + # --------------------------------- + foreach obj [$PARENTS items] { + catch {$obj destroy} + } + $PARENTS destroy + #puts "PROCESSORS: $PROCESSORS" + foreach obj [$PROCESSORS items] { + catch {$obj destroy} + } + $PROCESSORS destroy + catch {$RETURNED_VMAP destroy} + # --------------------------------- + + do_debug 1 $debugc "[string repeat = 40]" + do_debug 1 $debugc "DEBUG-END $caller" + if {[string length $raise_dispatch_error_instead_of_return]} { + return -code error $raise_dispatch_error_instead_of_return + } + + + return $returnval + } + + proc tailflagspaired {defaults supplied classifications rangesbytype} { + lassign [lindex $rangesbytype end] c tp a b + if {($c eq "unallocated") && ($tp eq "flagtype")} { + set tail_unallocated [lrange $supplied $a $b] + } else { + set tail_unallocated [list] + } + #set extraflags [list] + set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated] + #dict merge based operation can't work if there are solo_flags? + #review + if {[llength $tail_unallocated]} { + for {set i $a} {$i <=$b} {incr i} { + set arginfo [dict get $classifications $i] + lassign $arginfo class ftype v + switch -- $ftype { + flag - flagvalue { + lappend extraflags $v + } + soloflag { + lappend extraflags $v + if {[dict exists $defaults $v]} { + lappend extraflags [dict get $defaults $v] + } else { + lappend extraflags 1 + } + } + } + } + foreach {k v} [dict get $defaults] { + if {$k ni $extraflags} { + lappend extraflags $k $v + } + } + } else { + set extraflags $defaults + } + return $extraflags + } + + proc tailflagspaired1 {defaults supplied classifications rangesbytype} { + lassign [lindex $rangesbytype end] c tp a b + if {($c eq "unallocated") && ($tp eq "flagtype")} { + set tail_unallocated [lrange $supplied $a $b] + } else { + set tail_unallocated [list] + } + #set all_post_unallocated_ranges [lsearch -all -inline -index 0 [lrange $rangesbytype $command_range_posn end] "unallocated"] + + set extraflags [list] + + #set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated] + #dict merge based operation can't work if there are solo_flags with no value set + if {[llength $tail_unallocated]} { + for {set i $a} {$i <=$b} {incr i} { + set arginfo [dict get $classifications $i] + lassign $arginfo class ftype v + switch -- $ftype { + flag - flagvalue { + lappend extraflags $v + } + soloflag { + lappend extraflags $v + if {[dict exists $defaults $v]} { + lappend extraflags [dict get $defaults $v] + } else { + lappend extraflags 1 + } + } + } + } + foreach {k v} [dict get $defaults] { + if {$k ni $extraflags} { + lappend extraflags $k $v + } + } + } else { + set extraflags $defaults + } + + } + + + +} + + +namespace eval flagfilter { + + #punk::lib::dict_merge_ordered + + + + #retrieve *only* names that are dependant on the provided namekey - not the key itself + # (query is sorted by the trailing numerical index which represents order the arguments were processed) + proc flag_array_get_sorted_subs {arrname sep namekey} { + upvar $arrname arr + set allsubs [array names arr ${namekey}.*${sep}name,*] + set rnames [lmap nm $allsubs {string reverse $nm}] + set sorted_rnames [lsort -dictionary $rnames] + set ordered [lmap nm $sorted_rnames {string reverse $nm}] + return $ordered + } + + proc flag_array_get_sorted_siblings {arrname sep namekey} { + #determine parent by looking at dot - but confirm parent name is in array. + + } + + + + #dictionary based lsort of reversed names which are presumed to have a trailing separator of some sort and a number e.g: name,0 name,1 ... name,10 etc. + #use -dictionary to ensure embedded numbers are sorted as integers + proc array_names_sorted_by_tail {arrname nameglob} { + upvar $arrname arr + set matched_names [array names arr $nameglob] + set rnames [lmap nm $matched_names {string reverse $nm}] + set sorted_rnames [lsort -dictionary $rnames] + return [lmap nm $sorted_rnames {string reverse $nm}] + } + + +} + + + + + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/funcl-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/funcl-0.1.tm new file mode 100644 index 00000000..ccdc9d99 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/funcl-0.1.tm @@ -0,0 +1,322 @@ +package provide funcl [namespace eval funcl { + variable version + set version 0.1 +}] +#funcl = function list (nested call structure) +# +#a basic functional composition o combinator +#o(f,g)(x) == f(g(x)) + +namespace eval funcl { + + #from punk + proc arg_is_script_shaped {arg} { + if {[string first " " $arg] >= 0} { + return 1 + } elseif {[string first \n $arg] >= 0} { + return 1 + } elseif {[string first ";" $arg] >= 0} { + return 1 + } elseif {[string first \t $arg] >= 0} { + return 1 + } else { + return 0 + } + } + + + proc o args { + set closing [string repeat {]} [expr [llength $args]-1]] + set body "[join $args { [}] \$data $closing" + return $body + } + + proc o_ args { + set body "" + set tails [lrepeat [llength $args] ""] + puts stdout "tails: $tails" + + set end [lindex $args end] + if {[llength $end] == 1 && [arg_is_script_shaped $end]} { + set endfunc [string map " $end" {uplevel 1 [list if 1 ]}] + } else { + set endfunc $end + } + if {[llength $args] == 1} { + return $endfunc + } + + set wrap { [} + append wrap $endfunc + append wrap { ]} + + set i 0 + foreach cmdlist [lrange $args 0 end-1] { + set is_script 0 + if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} { + set is_script 1 + set script [lindex $cmdlist 0] + } + set t "" + if {$i > 0} { + append body { [} + } + set posn [lsearch $cmdlist _] + if {$posn <= 0} { + append body $cmdlist + if {$i == [expr {[llength $args] -2}]} { + #append body " \$data" + append body " $wrap" + } + if {$i > 0} { + set t {]} + } + } else { + append body [lrange $cmdlist 0 $posn-1] + if {$i == [expr {[llength $args] -2}]} { + #append body " \$data" + append body " $wrap" + } + set t [lrange $cmdlist $posn+1 end] + if {$i > 0} { + append t { ]} + } + } + lset tails $i $t + incr i + } + append body [join [lreverse $tails] " "] + puts stdout "tails: $tails" + + return $body + } + + #review - consider _call -- if count > 1 then they must all be callable cmdlists(?) + # what does it mean to have additional _fn wrapper with no other elements? (no actual function) + #e.g _fn 2 5 6 somefunc {_fn 1 3 {_call 1 3 xxx}} {_fn 1 4 command {_fn ...}} + # what type indicates running subtrees in parallel vs sequentially? + # any reason to have _call count other than 1? Presumably the parent node indicates the parallelism/sequentialism etc. + # + # + # accept or return a funcl (or funcltree if multiple funcls in one commandlist) + # also accept/return a call - return empty list if passed a call + proc next_funcl {funcl_or_tree} { + if {[lindex $funcl_or_tree 0] eq "_call"} { + return [list] + } + if {[lindex $funcl_or_tree 0] in [list "_fn" "_call"]} { + set funcl $funcl_or_tree + } else { + error "funcltree not implemented" + } + + + set count [lindex $funcl 1] + if {$count == 0} { + #null funcl.. what is it? metadata/placeholder? + return $funcl + } + set indices [lrange $funcl 2 [expr {1 + $count}]] + set i 0 + foreach idx $indices { + if {$i > 0} { + #todo - return a funcltree + error "multi funcl not implemented" + } + set next [lindex $funcl $idx] + incr i + } + + return $next + + } + + #convert a funcl to a tcl script + proc funcl_script {funcl} { + if {![llength $funcl]} { + return "" + } + set body "" + set tails [list] + + set type [lindex $funcl 0] + if {$type ni [list "_fn" "_call"]} { + #todo - handle funcltree + error "type $type not implemented" + } + + + #only count of 1 with index 3 supported(?) + if {$type eq "_call"} { + #leaf + set cmdlist [lindex $funcl 3] + return $cmdlist + } + + #we will use next_funcl to walk the nodes.. todo support treefuncl response from next_funcl which could branch multiple times. + #by continually passing back the resulting treefuncl/funcl to next_funcl we can process in correct order (?) + # we would still need to maintain state to stitch it back together once returned from a subtree.. + # ie multiple tail parts + set count [lindex $funcl 1] + + if {$count == 1} { + set idx [lindex $funcl 2] + if {$idx == 3} { + set cmdlist_pre [list] + } else { + set cmdlist_pre [lrange $funcl 3 $idx-1] + } + append body $cmdlist_pre + set t [lrange $funcl $idx+1 end] + lappend tails $t + } else { + #?? + error "funcl_script branching not yet supported" + } + + + set get_next 1 + set i 1 + while {$get_next} { + set funcl [next_funcl $funcl] + if {![llength $funcl]} { + set get_next 0 + } + lassign $funcl type count idx ;#todo support count > 1 + if {$type eq "_call"} { + set get_next 0 + } + set t "" + if {$type eq "_call"} { + append body { [} + append body [lindex $funcl $idx] + append body { ]} + } else { + append body { [} + if {$idx == 3} { + set cmdlist_pre [list] + } else { + set cmdlist_pre [lrange $funcl 3 $idx-1] + } + append body $cmdlist_pre + set t [lrange $funcl $idx+1 end] + lappend tails $t + lappend tails { ]} + } + incr i + } + append body [join [lreverse $tails] " "] + #puts stdout "tails: $tails" + + return $body + } + + + interp alias "" o_of "" funcl::o_of_n 1 + + #o_of_n + #tcl list rep o combinator + # + # can take lists of ordinary commandlists, scripts and funcls + # _fn 1 x where 1 indicates number of subfuncls and where x indicates next funcl position (_fn list or _arg) + # _fn 0 indicates next item is an unwrapped commandlist (terminal command) + # + #o_of is equivalent to o_of_n 1 (1 argument o combinator) + #last n args are passed to the prior function + #e.g for n=1 f a b = f(a(b)) + #e.g for n=2, e f a b = e(f(a b)) + proc o_of_n {n args} { + puts stdout "o_of_n '$args'" + if {$n != 1} { + error "o_of_n only implemented for 1 sub-funcl" + } + set comp [list] ;#composition list + set end [lindex $args end] + if {[lindex $end 0] in {_fn _call}]} { + #is_funcl + set endfunc [lindex $args end] + } else { + if {[llength $end] == 1 && [arg_is_script_shaped $end]} { + #set endfunc [string map [list $end] {uplevel 1 [list if 1 ]}] + set endfunc [list _call 1 3 [list uplevel 1 [list if 1 [lindex $end 0]]]] + } else { + set endfunc [list _call 1 3 [list {*}$end]] + } + } + + if {[llength $args] == 1} { + return $endfunc + } + set comp $endfunc + set revlist [lreverse [lrange $args 0 end-1]] + foreach cmdlist $revlist { + puts stderr "o_of_n >>-- $cmdlist" + if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} { + set is_script 1 + set script [lindex $cmdlist 0] + set arglist [list data] + + set comp [list _fn 1 6 call_script $script $arglist $comp] + } else { + set posn1 [expr {[llength $cmdlist] + 2 + $n}] + set comp [list _fn $n $posn1 {*}$cmdlist $comp] + } + } + return $comp + } + proc call_script {script argnames args} { + uplevel 3 [list if 1 [list apply [list $argnames $script] {*}$args]] + } + proc funcl_script_test {scr} { + do_funcl_script_test $scr + } + proc do_funcl_script_test {scr} { + #set j "in do_funcl_script_test" + #set data "xxx" + #puts '$scr' + if 1 $scr + } + + #standard o_ with no script-handling + proc o_plain args { + set body "" + set i 0 + set tails [lrepeat [llength $args] ""] + #puts stdout "tails: $tails" + foreach cmdlist $args { + set t "" + if {$i > 0} { + append body { [} + } + set posn [lsearch $cmdlist _] + if {$posn <= 0} { + append body $cmdlist + if {$i == [expr {[llength $args] -1}]} { + append body " \$data" + } + if {$i > 0} { + set t {]} + } + } else { + append body [lrange $cmdlist 0 $posn-1] + if {$i == [expr {[llength $args] -1}]} { + append body " \$data" + } + set t [lrange $cmdlist $posn+1 end] + if {$i > 0} { + append t { ]} + } + } + lset tails $i $t + incr i + } + append body [join [lreverse $tails] " "] + #puts stdout "tails: $tails" + + return $body + } + #timings suggest no faster to split out the first item from the cmdlist loop +} + + + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config index c79eb6da..922ff786 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config @@ -2,18 +2,29 @@ #bootsupport modules can be pulled in from within other areas of src or from the built module folders of the project #They must be already built, so generally shouldn't come directly from src/modules. +#we want showdict - but it needs punk pipeline notation. +#this requires pulling in punk - which brings in lots of other stuff +#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 set bootsupport_modules [list\ src/vendormodules commandstack\ src/vendormodules cksum\ + src/vendormodules debug\ src/vendormodules dictutils\ src/vendormodules fauxlink\ src/vendormodules fileutil\ src/vendormodules http\ src/vendormodules md5\ + src/vendormodules metaface\ src/vendormodules modpod\ src/vendormodules oolib\ src/vendormodules overtype\ + src/vendormodules pattern\ + src/vendormodules patterncmd\ + src/vendormodules patternlib\ + src/vendormodules patternpredator2\ src/vendormodules sha1\ src/vendormodules tomlish\ src/vendormodules test::tomlish\ @@ -25,8 +36,15 @@ set bootsupport_modules [list\ src/vendormodules textutil::trim\ src/vendormodules textutil::wcswidth\ src/vendormodules uuid\ - modules punkcheck\ + modules argp\ + modules flagfilter\ + modules funcl\ modules natsort\ + modules punk\ + modules punkapp\ + modules punkcheck\ + modules punkcheck::cli\ + modules punk::aliascore\ modules punk::ansi\ modules punk::assertion\ modules punk::args\ @@ -35,6 +53,7 @@ set bootsupport_modules [list\ modules punk::cap::handlers::scriptlibs\ modules punk::cap::handlers::templates\ modules punk::char\ + modules punk::config\ modules punk::console\ modules punk::du\ modules punk::encmime\ @@ -46,6 +65,7 @@ set bootsupport_modules [list\ modules punk::mix::cli\ modules punk::mix::util\ modules punk::mix::templates\ + modules punk::repl::codethread\ modules punk::mix::commandset::buildsuite\ modules punk::mix::commandset::debug\ modules punk::mix::commandset::doc\ @@ -55,14 +75,18 @@ set bootsupport_modules [list\ modules punk::mix::commandset::project\ modules punk::mix::commandset::repo\ modules punk::mix::commandset::scriptwrap\ + modules punk::mod\ + modules punk::nav::fs\ modules punk::ns\ modules punk::overlay\ modules punk::path\ modules punk::packagepreference\ modules punk::repo\ modules punk::tdl\ + modules punk::unixywindows\ modules punk::zip\ modules punk::winpath\ + modules shellfilter\ modules textblock\ modules natsort\ modules oolib\ diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/metaface-1.2.5.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/metaface-1.2.5.tm new file mode 100644 index 00000000..4c88cb16 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/metaface-1.2.5.tm @@ -0,0 +1,6411 @@ +package require dictutils +package provide metaface [namespace eval metaface { + variable version + set version 1.2.5 +}] + + + + +#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 vinfo $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 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 {}} +proc ::p::-1::M {_ID_} { + 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 { + foreach m [dict keys [set ::p::${IID}::_iface::o_methods]] { + lappend members $m + } + } + return $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 {}} +proc ::p::-1::P {_ID_} { + 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 { + foreach prop [dict keys [set ::p::${IID}::_iface::o_properties]] { + lappend members $prop + } + } + 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_} { + 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/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm new file mode 100644 index 00000000..5d76af04 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm @@ -0,0 +1,1285 @@ +#PATTERN +# - A prototype-based Object system. +# +# Julian Noble 2003 +# License: Public domain +# + +# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. +# +# +# Pattern uses a mixture of class-based and prototype-based object instantiation. +# +# A pattern object has 'properties' and 'methods' +# The system makes a distinction between them with regards to the access syntax for write operations, +# and yet provides unity in access syntax for read operations. +# e.g >object . myProperty +# will return the value of the property 'myProperty' +# >ojbect . myMethod +# will return the result of the method 'myMethod' +# contrast this with the write operations: +# set [>object . myProperty .] blah +# >object . myMethod blah +# however, the property can also be read using: +# set [>object . myProperty .] +# Note the trailing . to give us a sort of 'reference' to the property. +# this is NOT equivalent to +# set [>object . myProperty] +# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property +# i.e it is equivalent in this case to: set blah + +#All objects are represented by a command, the name of which contains a leading ">". +#Any commands in the interp which use this naming convention are assumed to be a pattern object. +#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) + +#All user-added properties & methods of the wrapped object are accessed +# using the separator character "." +#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." +# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) +# you would use the 'Create' metamethod on the pattern object like so: +# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject +# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties +# of the object it was created from. ( + + +#The use of the access-syntax separator character "." allows objects to be kept +# 'clean' in the sense that the only methods &/or properties that can be called this way are ones +# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax +# so you are free to implement your own 'Create' method on your object that doesn't conflict with +# the metamethod. + +#Chainability (or how to violate the Law of Demeter!) +#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other +# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference +# structure, without the need to regress to enter matching brackets as is required when using +# standard TCL command syntax. +# ie instead of: +# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething +# we can use: +# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething +# +# This separates out the object-traversal syntax from the TCL command syntax. + +# . is the 'traversal operator' when it appears between items in a commandlist +# . is the 'reference operator' when it is the last item in a commandlist +# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. +# It marks breaks in the multidimensional structure that correspond to how the data is stored. +# e.g obj . arraydata x y , x1 y1 z1 +# represents an element of a 5-dimensional array structured as a plane of cubes +# e.g2 obj . arraydata x y z , x1 y1 +# represents an element of a 5-dimensional array structured as a cube of planes +# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 +# .. is the 'meta-traversal operator' when it appears between items in a commandlist +# .. is the 'meta-info operator'(?) when it is the last item in a commandlist + + +#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing +# implement iStacks & pStacks (interface stacks & pattern stacks) + +#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 + + +#------------------------------------------------------------ +# System objects. +#------------------------------------------------------------ +#::p::-1 ::p::internals::>metaface +#::p::0 ::p::ifaces::>null +#::p::1 ::>pattern +#------------------------------------------------------------ + +#TODO + +#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) + + +#CHANGES +#2018-09 - v 1.2.2 +# varied refactoring +# Changed invocant datastructure curried into commands (the _ID_ structure) +# Changed MAP structure to dict +# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) +# updated test suites +#2018-08 - v 1.2.1 +# split ::p::predatorX functions into separate files (pkgs) +# e.g patternpredator2-1.0.tm +# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken +# +#2017-08 - v 1.1.6 Fairly big overhaul +# New predator function using coroutines +# Added bang operator ! +# Fixed Constructor chaining +# Added a few tests to test::pattern +# +#2008-03 - preserve ::errorInfo during var writes + +#2007-11 +#Major overhaul + new functionality + new tests v 1.1 +# new dispatch system - 'predator'. +# (preparing for multiple interface stacks, multiple invocants etc) +# +# +#2006-05 +# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. +# +#2005-12 +# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. +# +# Fixed so that PatternVariable default applied on Create. +# +# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: +# - heading towards multiple-interface objects +# +#2005-10-28 +# 1.0.8.1 passes 80/80 tests +# >object .. Destroy - improved cleanup of interfaces & namespaces. +# +#2005-10-26 +# fixes to refsync (still messy!) +# remove variable traces on REF vars during .. Destroy +# passes 76/76 +# +#2005-10-24 +# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. +# 1.0.8.0 now passes 75/76 +# +#2005-10-19 +# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) +# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) +# 1.0.8.0 (passes 74/76) +# tests now in own package +# usage: +# package require test::pattern +# test::p::list +# test::p::run ?nameglob? ?-version ? +# +#2005-09?-12 +# +# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. +# fixed @next@ so that destination method resolved at interface compile time instead of call time +# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. +# (before, the overlay only occured when '.. Method' was used to override.) +# +# +# miscellaneous tidy-ups +# +# 1.0.7.8 (passes 71/73) +# +#2005-09-10 +# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value +# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. +# +#2005-09-07 +# bugfix indexed write to list property +# bugfix Variable default value +# 1.0.7.7 (passes 70/72) +# fails: +# arrayproperty.test - array-entire-reference +# properties.test - property_getter_filter_via_ObjectRef +# +#2005-04-22 +# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) +# +# 1.0.7.4 +# +#2004-11-05 +# basic PropertyRead implementation (non-indexed - no tests!) +# +#2004-08-22 +# object creation speedups - (pattern::internals::obj simplified/indirected) +# +#2004-08-17 +# indexed property setter fixes + tests +# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) +# +#2004-08-16 +# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) +# +#2004-08-15 +# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) +# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger +# - also trigger on curried traces to indexed properties i.e list and array elements. +# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. +# +# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] +# +#2004-08-05 +# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) +# +# fix + add tests to support method & property of same name. (method precedence) +# +#2004-08-04 +# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) +# +# 1.0.7.1 +# use objectref array access to read properties even when some props unset; + test +# unset property using array access on object reference; + test +# +# +#2004-07-21 +# object reference changes - array property values appear as list value when accessed using upvared array. +# bugfixes + tests - properties containing lists (multidimensional access) +# +#1.0.7 +# +#2004-07-20 +# fix default property value append problem +# +#2004-07-17 +# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods +# ( +# +#2004-06-18 +# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. +# +#2004-06-05 +# change argsafety operator to be anything with leading - +# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' +# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, +# the entire dash-prefixed operator is also passed in as an argument. +# e.g >object . doStuff -window . +# will call the doStuff method with the 2 parameters -window . +# >object . doStuff - . +# will call doStuff with single parameter . +# >object . doStuff - -window . +# will result in a reference to the doStuff method with the argument -window 'curried' in. +# +#2004-05-19 +#1.0.6 +# fix so custom constructor code called. +# update Destroy metamethod to unset $self +# +#1.0.4 - 2004-04-22 +# bug fixes regarding method specialisation - added test +# +#------------------------------------------------------------ + +package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] + + +namespace eval pattern::util { + + # Generally better to use 'package require $minver-' + # - this only gives us a different error + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } +} + +package require patterncmd 1.2.4- +package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) + + + +#package require cmdline +package require overtype + +#package require md5 ;#will be loaded if/when needed +#package require md4 +#package require uuid + + + + + +namespace eval pattern { + variable initialised 0 + + + if 0 { + if {![catch {package require twapi_base} ]} { + #twapi is a windows only package + #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. + # If available - windows seems to provide a fast uuid generator.. + #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) + # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) + interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok + } else { + #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) + # (e.g 200usec 2018 corei9) + #(with or without tcllibc?) + #very first call is extremely slow though - 3.5seconds on 2018 corei9 + package require uuid + interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate + } + #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) + } + + +} + + + + + + +namespace eval p { + #this is also the interp alias namespace. (object commands created here , then renamed into place) + #the object aliases are named as incrementing integers.. !todo - consider uuids? + variable ID 0 + namespace eval internals {} + + + #!?? + #namespace export ?? + variable coroutine_instance 0 +} + +#------------------------------------------------------------------------------------- +#review - what are these for? +#note - this function is deliberately not namespaced +# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features +proc process_pattern_aliases {object args} { + set o [namespace tail $object] + interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] + interp alias {} process_method_$o {} [$object .. Method .] + interp alias {} process_constructor_$o {} [$object .. Constructor .] +} +#------------------------------------------------------------------------------------- + + + + +#!store all interface objects here? +namespace eval ::p::ifaces {} + + + +#K combinator - see http://wiki.tcl.tk/1923 +#proc ::p::K {x y} {set x} +#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] + + + + + + + + +proc ::p::internals::(VIOLATE) {_ID_ violation_script} { + #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] + set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] + + if {![dict get $processed explicitvars]} { + #no explicit var statements - we need the implicit ones + set self [set ::p::${_ID_}::(self)] + set IFID [lindex [set $self] 1 0 end] + #upvar ::p::${IFID}:: self_IFINFO + + + set varDecls {} + set vlist [array get ::p::${IFID}:: v,name,*] + set _k ""; set v "" + if {[llength $vlist]} { + append varDecls "upvar #0 " + foreach {_k v} $vlist { + append varDecls "::p::\${_ID_}::$v $v " + } + append varDecls "\n" + } + + #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] + set violation_script $varDecls\n[dict get $processed body] + + #tidy up + unset processed varDecls self IFID _k v + } else { + set violation_script [dict get $processed body] + } + unset processed + + + + + #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. + eval "unset violation_script;$violation_script" +} + + +proc ::p::internals::DestroyObjectsBelowNamespace {ns} { + #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" + + set nsparts [split [string trim [string map {:: :} $ns] :] :] + if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { + #ns not of form ::p::?::_ref + + foreach obj [info commands ${ns}::>*] { + #catch {::p::meta::Destroy $obj} + #puts ">>found object $obj below ns $ns - destroying $obj" + $obj .. Destroy + } + } + + #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] + #foreach tinfo $traces { + # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo + #} + #unset -nocomplain ${ns}::-->PATTERN_ANCHOR + + foreach sub [namespace children $ns] { + ::p::internals::DestroyObjectsBelowNamespace $sub + } +} + + + + +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# + + + + + + + + + +proc ::p::get_new_object_id {} { + tailcall incr ::p::ID + #tailcall ::pattern::new_uuid +} + +#create a new minimal object - with no interfaces or patterns. + +#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} +proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { + + #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" + + if {$OID eq "-2"} { + set OID [::p::get_new_object_id] + #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) + #set OID [pattern::new_uuid] + } + #if $wrapped provided it is assumed to be an existing namespace. + #if {[string length $wrapped]} { + # #??? + #} + + #sanity check - alias must not exist for this OID + if {[llength [interp alias {} ::p::$OID]]} { + error "Object alias '::p::$OID' already exists - cannot create new object with this id" + } + + #system 'varspaces' - + + #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://wiki.tcl.tk/1030 'Dangers of creative writing') + #set o_open 1 - every object is initially also an open interface (?) + #NOTE! comments within namespace eval slow it down. + namespace eval ::p::$OID { + #namespace ensemble create + namespace eval _ref {} + namespace eval _meta {} + namespace eval _iface { + variable o_usedby; + variable o_open 1; + array set o_usedby [list]; + variable o_varspace "" ; + variable o_varspaces [list]; + variable o_methods [dict create]; + variable o_properties [dict create]; + variable o_variables; + variable o_propertyunset_handlers; + set o_propertyunset_handlers [dict create] + } + } + + #set alias ::p::$OID + + #objectid alis default_method object_command wrapped_namespace + set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] + + #MAP is a dict + set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] + + + + #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token + #we've already checked that ::p::$OID doesn't pre-exist + # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias + #interp alias {} ::p::$OID {} ::p::internals::predator $MAP + + + # _ID_ structure + set invocants_dict [dict create this [list $INVOCANTDATA] ] + #puts stdout "New _ID_structure: $interfaces_dict" + set _ID_ [dict create i $invocants_dict context ""] + + + interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ + #rename the command into place - thus the alias & the command name no longer match! + rename ::p::$OID $cmd + + set ::p::${OID}::_meta::map $MAP + + # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something + interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ + + #set p2 [string map {> ?} $cmd] + #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ + + + #trace add command $cmd delete "$cmd .. Destroy ;#" + #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" + + trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" + #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) + + #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" + + + #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" + #trace add command $cmd delete "puts deleting$cmd ;#" + #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" + + + #puts "--> new_object returning map $MAP" + return $MAP +} + + + + +#>x .. Create >y +# ".." is special case equivalent to "._." +# (whereas in theory it would be ".default.") +# "." is equivalent to ".default." is equivalent to ".default.default." (...) + +#>x ._. Create >y +#>x ._.default. Create >y ??? +# +# + +# create object using 'blah' as source interface-stack ? +#>x .blah. .. Create >y +#>x .blah,_. ._. Create .iStackDestination. >y + + + +# +# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] +# the 1st item, blah in this case becomes the 'default' iStack. +# +#>x .*. +# cast to object with all iStacks +# +#>x .*,!_. +# cast to object with all iStacks except _ +# +# --------------------- +#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' +# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. +# +#eg1: >x & >y . some_multi_method arg arg +# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) +# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' +# The invocant signature is thus {these 2} +# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) +# Invocation roles can be specified in the call using the @ operator. +# e.g >x & >y @ points . some_multi_method arg arg +# The invocant signature for this is: {points 2} +# +#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path +# This has the signature {objects n plane 1} where n depends on the length of the list $objects +# +# +# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. +# e.g set pointset [>x & >y .] +# We can now call multimethods on $pointset +# + + + + + + +#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) +proc ::pattern::predatorversion {{ver ""}} { + variable active_predatorversion + set allowed_predatorversions {1 2} + set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions + + if {![info exists active_predatorversion]} { + set first_time_set 1 + } else { + set first_time_set 0 + } + + if {$ver eq ""} { + #get version + if {$first_time_set} { + set active_predatorversions $default_predatorversion + } + return $active_predatorversion + } else { + #set version + if {$ver ni $allowed_predatorversions} { + error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" + } + + if {!$first_time_set} { + if {$active_predatorversion eq $ver} { + #puts stderr "Active predator version is already '$ver'" + #ok - nothing to do + return $active_predatorversion + } else { + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + rename ::p::internals::predator ::p::predator$active_predatorversion + } + } + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + + rename ::p::predator$ver ::p::internals::predator + set active_predatorversion $ver + + return $active_predatorversion + } +} +::pattern::predatorversion 2 + + + + + + + + + + + + +# >pattern has object ID 1 +# meta interface has object ID 0 +proc ::pattern::init args { + + if {[set ::pattern::initialised]} { + if {[llength $args]} { + #if callers want to avoid this error, they can do their own check of $::pattern::initialised + error "pattern package is already initialised. Unable to apply args: $args" + } else { + return 1 + } + } + + #this seems out of date. + # - where is PatternPropertyRead? + # - Object is obsolete + # - Coinjoin, Combine don't seem to exist + array set ::p::metaMethods { + Clone object + Conjoin object + Combine object + Create object + Destroy simple + Info simple + Object simple + PatternProperty simple + PatternPropertyWrite simple + PatternPropertyUnset simple + Property simple + PropertyWrite simple + PatternMethod simple + Method simple + PatternVariable simple + Variable simple + Digest simple + PatternUnknown simple + Unknown simple + } + array set ::p::metaProperties { + Properties object + Methods object + PatternProperties object + PatternMethods object + } + + + + + + #create metaface - IID = -1 - also OID = -1 + # all objects implement this special interface - accessed via the .. operator. + + + + + + set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface + + + #OID = 0 + ::p::internals::new_object ::p::ifaces::>null "" 0 + + #? null object has itself as level0 & level1 interfaces? + #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] + + #null interface should always have 'usedby' members. It should never be extended. + array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array + set ::p::0::_iface::o_open 0 + + set ::p::0::_iface::o_constructor [list] + set ::p::0::_iface::o_variables [list] + set ::p::0::_iface::o_properties [dict create] + set ::p::0::_iface::o_methods [dict create] + set ::p::0::_iface::o_varspace "" + set ::p::0::_iface::o_varspaces [list] + array set ::p::0::_iface::o_definition [list] + set ::p::0::_iface::o_propertyunset_handlers [dict create] + + + + + ############################### + # OID = 1 + # >pattern + ############################### + ::p::internals::new_object ::>pattern "" 1 + + #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] + + + array set ::p::1::_iface::o_usedby [list] ;#'usedby' array + + set _self ::pattern + + #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 + #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 + + + + #1)this object references its interfaces + #lappend ID $IFID $IFID_1 + #lset SELFMAP 1 0 $IFID + #lset SELFMAP 2 0 $IFID_1 + + + #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] + #proc ::>pattern args $body + + + + + ####################################################################################### + #OID = 2 + # >ifinfo interface for accessing interfaces. + # + ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object + set ::p::2::_iface::o_constructor [list] + set ::p::2::_iface::o_variables [list] + set ::p::2::_iface::o_properties [dict create] + set ::p::2::_iface::o_methods [dict create] + set ::p::2::_iface::o_varspace "" + set ::p::2::_iface::o_varspaces [list] + array set ::p::2::_iface::o_definition [list] + set ::p::2::_iface::o_open 1 ;#open for extending + + ::p::ifaces::>2 .. AddInterface 2 + + #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations + #(bootstrap because we can't yet use metaface methods on it) + + + + proc ::p::2::_iface::isOpen.1 {_ID_} { + return $::p::2::_iface::o_open + } + interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 + + proc ::p::2::_iface::isClosed.1 {_ID_} { + return [expr {!$::p::2::_iface::o_open}] + } + interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 + + proc ::p::2::_iface::open.1 {_ID_} { + set ::p::2::_iface::o_open 1 + } + interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 + + proc ::p::2::_iface::close.1 {_ID_} { + set ::p::2::_iface::o_open 0 + } + interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 + + + #proc ::p::2::_iface::(GET)properties.1 {_ID_} { + # set ::p::2::_iface::o_properties + #} + #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 + + #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties + + + #proc ::p::2::_iface::(GET)methods.1 {_ID_} { + # set ::p::2::_iface::o_methods + #} + #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 + #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods + + + + + + #link from object to interface (which in this case are one and the same) + + #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] + #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] + #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] + #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] + + interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen + interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed + interp alias {} ::p::2::open {} ::p::2::_iface::open + interp alias {} ::p::2::close {} ::p::2::_iface::close + + + #namespace eval ::p::2 "namespace export $method" + + ####################################################################################### + + + + + + + set ::pattern::initialised 1 + + + ::p::internals::new_object ::p::>interface "" 3 + #create a convenience object on which to manipulate the >ifinfo interface + #set IF [::>pattern .. Create ::p::>interface] + set IF ::p::>interface + + + #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? + # (or is forcing end user to add their own pStack/iStack ok .. ?) + # + ::p::>interface .. AddPatternInterface 2 ;# + + ::p::>interface .. PatternVarspace _iface + + ::p::>interface .. PatternProperty methods + ::p::>interface .. PatternPropertyRead methods {} { + varspace _iface + var {o_methods alias} + return $alias + } + ::p::>interface .. PatternProperty properties + ::p::>interface .. PatternPropertyRead properties {} { + varspace _iface + var o_properties + return $o_properties + } + ::p::>interface .. PatternProperty variables + + ::p::>interface .. PatternProperty varspaces + + ::p::>interface .. PatternProperty definition + + ::p::>interface .. Constructor {{usedbylist {}}} { + #var this + #set this @this@ + #set ns [$this .. Namespace] + #puts "-> creating ns ${ns}::_iface" + #namespace eval ${ns}::_iface {} + + varspace _iface + var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces + + set o_constructor [list] + set o_variables [list] + set o_properties [dict create] + set o_methods [dict create] + set o_varspaces [list] + array set o_definition [list] + + foreach usedby $usedbylist { + set o_usedby(i$usedby) 1 + } + + + } + ::p::>interface .. PatternMethod isOpen {} { + varspace _iface + var o_open + + return $o_open + } + ::p::>interface .. PatternMethod isClosed {} { + varspace _iface + var o_open + + return [expr {!$o_open}] + } + ::p::>interface .. PatternMethod open {} { + varspace _iface + var o_open + set o_open 1 + } + ::p::>interface .. PatternMethod close {} { + varspace _iface + var o_open + set o_open 0 + } + ::p::>interface .. PatternMethod refCount {} { + varspace _iface + var o_usedby + return [array size o_usedby] + } + + set ::p::2::_iface::o_open 1 + + + + + uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} + #uplevel #0 {package require patternlib} + return 1 +} + + + +proc ::p::merge_interface {old new} { + #puts stderr " ** ** ** merge_interface $old $new" + set ns_old ::p::$old + set ns_new ::p::$new + + upvar #0 ::p::${new}:: IFACE + upvar #0 ::p::${old}:: IFACEX + + if {![catch {set c_arglist $IFACEX(c,args)}]} { + #constructor + #for now.. just add newer constructor regardless of any existing one + #set IFACE(c,args) $IFACEX(c,args) + + #if {![info exists IFACE(c,args)]} { + # #target interface didn't have a constructor + # + #} else { + # # + #} + } + + + set methods [::list] + foreach nm [array names IFACEX m-1,name,*] { + lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) + } + + #puts " *** merge interface $old -> $new ****merging-in methods: $methods " + + foreach method $methods { + if {![info exists IFACE(m-1,name,$method)]} { + #target interface doesn't yet have this method + + set THISNAME $method + + if {![string length [info command ${ns_new}::$method]]} { + + if {![set ::p::${old}::_iface::o_open]} { + #interp alias {} ${ns_new}::$method {} ${ns_old}::$method + #namespace eval $ns_new "namespace export [namespace tail $method]" + } else { + #wait to compile + } + + } else { + error "merge interface - command collision " + } + #set i 2 ??? + set i 1 + + } else { + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + + set i [incr IFACE(m-1,chain,$method)] + + set THISNAME ___system___override_${method}_$i + + #move metadata using subindices for delegated methods + set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) + set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) + set IFACE(mp-$i,$method) $IFACE(mp-1,$method) + + set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) + set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) + + + #set next [::p::next_script $IFID0 $method] + if {![string length [info command ${ns_new}::$THISNAME]]} { + if {![set ::p::${old}::_iface::o_open]} { + interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method + namespace eval $ns_new "namespace export $method" + } else { + #wait for compile + } + } else { + error "merge_interface - command collision " + } + + } + + array set IFACE [::list \ + m-1,chain,$method $i \ + m-1,body,$method $IFACEX(m-1,body,$method) \ + m-1,args,$method $IFACEX(m-1,args,$method) \ + m-1,name,$method $THISNAME \ + m-1,iface,$method $old \ + ] + + } + + + + + + #array set ${ns_new}:: [array get ${ns_old}::] + + + #!todo - review + #copy everything else across.. + + foreach {nm v} [array get IFACEX] { + #puts "-.- $nm" + if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { + set IFACE($nm) $v + } + } + + #!todo -write a test + set ::p::${new}::_iface::o_open 1 + + #!todo - is this done also when iface compiled? + #namespace eval ::p::$new {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place + + return +} + + + + +#detect attempt to treat a reference to a method as a property +proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { +#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" + lassign [lrange $args end-2 end] vtraced vidx op + #NOTE! cannot rely on vtraced as it may have been upvared + + switch -- $op { + write { + error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + unset { + #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace + #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #!todo - don't use vtraced! + trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #pointless raising an error as "Any errors in unset traces are ignored" + #error "cannot unset. $field is a method not a property" + } + read { + error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + array { + error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" + #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" + } + } + + return +} + + + + +#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. +# +# The 'dispatcher' is an object instance's underlying object command. +# + +#proc ::p::make_dispatcher {obj ID IFID} { +# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { +# ::p::@IID@ $methprop @oid@ {*}$args +# }] +# return +#} + + + + +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +#aliased from ::p::${OID}:: +# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something +proc ::p::internals::no_default_method {_ID_ args} { + puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped + tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" +} + +#force 1 will extend an interface even if shared. (??? why is this necessary here?) +#if IID empty string - create the interface. +proc ::p::internals::expand_interface {IID {force 0}} { + #puts stdout ">>> expand_interface $IID [info level -1]<<<" + if {![string length $IID]} { + #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) + set iid [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$iid + return $iid + } else { + if {[set ::p::${IID}::_iface::o_open]} { + #interface open for extending - shared or not! + return $IID + } + + if {[array size ::p::${IID}::_iface::o_usedby] > 1} { + #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby + + #oops.. shared interface. Copy before specialising it. + set prev_IID $IID + + #set IID [::p::internals::new_interface] + set IID [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$IID + + ::p::internals::linkcopy_interface $prev_IID $IID + #assert: prev_usedby contains at least one other element. + } + + #whether copied or not - mark as open for extending. + set ::p::${IID}::_iface::o_open 1 + return $IID + } +} + +#params: old - old (shared) interface ID +# new - new interface ID +proc ::p::internals::linkcopy_interface {old new} { + #puts stderr " ** ** ** linkcopy_interface $old $new" + set ns_old ::p::${old}::_iface + set ns_new ::p::${new}::_iface + + + + foreach nsmethod [info commands ${ns_old}::*.1] { + #puts ">>> adding $nsmethod to iface $new" + set tail [namespace tail $nsmethod] + set method [string range $tail 0 end-2] ;#strip .1 + + if {![llength [info commands ${ns_new}::$method]]} { + + set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 + + #link from new interface namespace to existing one. + #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) + #!todo? verify? + #- actual link is chainslot to chainslot + interp alias {} ${ns_new}::$method.1 {} $oldhead + + #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? + + + #chainhead pointer within new interface + interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 + + namespace eval $ns_new "namespace export $method" + + #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { + # lappend ${ns_new}::o_methods $method + #} + } else { + if {$method eq "(VIOLATE)"} { + #ignore for now + #!todo + continue + } + + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + #warning - existing chainslot will be completely shadowed by linked method. + # - existing one becomes unreachable. #!todo review!? + + + error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" + + } + } + + + #foreach propinf [set ${ns_old}::o_properties] { + # lassign $propinf prop _default + # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop + # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop + # lappend ${ns_new}::o_properties $propinf + #} + + + set ${ns_new}::o_variables [set ${ns_old}::o_variables] + set ${ns_new}::o_properties [set ${ns_old}::o_properties] + set ${ns_new}::o_methods [set ${ns_old}::o_methods] + set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] + + + set ::p::${old}::_iface::o_usedby(i$new) linkcopy + + + #obsolete.? + array set ::p::${new}:: [array get ::p::${old}:: ] + + + + #!todo - is this done also when iface compiled? + #namespace eval ::p::${new}::_iface {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' + + return +} +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +pattern::init + +return $::pattern::version diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm new file mode 100644 index 00000000..4107b8af --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm @@ -0,0 +1,645 @@ +package provide patterncmd [namespace eval patterncmd { + variable version + + set version 1.2.4 +}] + + +namespace eval pattern { + variable idCounter 1 ;#used by pattern::uniqueKey + + namespace eval cmd { + namespace eval util { + package require overtype + variable colwidths_lib [dict create] + variable colwidths_lib_default 15 + + dict set colwidths_lib "library" [list ch " " num 21 head "|" tail ""] + dict set colwidths_lib "version" [list ch " " num 7 head "|" tail ""] + dict set colwidths_lib "type" [list ch " " num 9 head "|" tail ""] + dict set colwidths_lib "note" [list ch " " num 31 head "|" tail "|"] + + proc colhead {type args} { + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + set line "" + foreach colname [dict keys $colwidths] { + append line "[col $type $colname [string totitle $colname] {*}$args]" + } + return $line + } + proc colbreak {type} { + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + set line "" + foreach colname [dict keys $colwidths] { + append line "[col $type $colname {} -backchar - -headoverridechar + -tailoverridechar +]" + } + return $line + } + proc col {type col val args} { + # args -head bool -tail bool ? + #---------------------------------------------------------------------------- + set known_opts [list -backchar -headchar -tailchar -headoverridechar -tailoverridechar -justify] + dict set default -backchar "" + dict set default -headchar "" + dict set default -tailchar "" + dict set default -headoverridechar "" + dict set default -tailoverridechar "" + dict set default -justify "left" + if {([llength $args] % 2) != 0} { + error "(pattern::cmd::util::col) ERROR: uneven options supplied - must be of form '-option value' " + } + foreach {k v} $args { + if {$k ni $known_opts} { + error "((pattern::cmd::util::col) ERROR: option '$k' not in known options: '$known_opts'" + } + } + set opts [dict merge $default $args] + set backchar [dict get $opts -backchar] + set headchar [dict get $opts -headchar] + set tailchar [dict get $opts -tailchar] + set headoverridechar [dict get $opts -headoverridechar] + set tailoverridechar [dict get $opts -tailoverridechar] + set justify [dict get $opts -justify] + #---------------------------------------------------------------------------- + + + + upvar #0 ::pattern::cmd::util::colwidths_$type colwidths + #calculate headwidths + set headwidth 0 + set tailwidth 0 + foreach {key def} $colwidths { + set thisheadlen [string length [dict get $def head]] + if {$thisheadlen > $headwidth} { + set headwidth $thisheadlen + } + set thistaillen [string length [dict get $def tail]] + if {$thistaillen > $tailwidth} { + set tailwidth $thistaillen + } + } + + + set spec [dict get $colwidths $col] + if {[string length $backchar]} { + set ch $backchar + } else { + set ch [dict get $spec ch] + } + set num [dict get $spec num] + set headchar [dict get $spec head] + set tailchar [dict get $spec tail] + + if {[string length $headchar]} { + set headchar $headchar + } + if {[string length $tailchar]} { + set tailchar $tailchar + } + #overrides only apply if the head/tail has a length + if {[string length $headchar]} { + if {[string length $headoverridechar]} { + set headchar $headoverridechar + } + } + if {[string length $tailchar]} { + if {[string length $tailoverridechar]} { + set tailchar $tailoverridechar + } + } + set head [string repeat $headchar $headwidth] + set tail [string repeat $tailchar $tailwidth] + + set base [string repeat $ch [expr {$headwidth + $num + $tailwidth}]] + if {$justify eq "left"} { + set left_done [overtype::left $base "$head$val"] + return [overtype::right $left_done "$tail"] + } elseif {$justify in {centre center}} { + set mid_done [overtype::centre $base $val] + set left_mid_done [overtype::left $mid_done $head] + return [overtype::right $left_mid_done $tail] + } else { + set right_done [overtype::right $base "$val$tail"] + return [overtype::left $right_done $head] + } + + } + + } + } + +} + +#package require pattern + +proc ::pattern::libs {} { + set libs [list \ + pattern {-type core -note "alternative:pattern2"}\ + pattern2 {-type core -note "alternative:pattern"}\ + patterncmd {-type core}\ + metaface {-type core}\ + patternpredator2 {-type core}\ + patterndispatcher {-type core}\ + patternlib {-type core}\ + patterncipher {-type optional -note optional}\ + ] + + + + package require overtype + set result "" + + append result "[cmd::util::colbreak lib]\n" + append result "[cmd::util::colhead lib -justify centre]\n" + append result "[cmd::util::colbreak lib]\n" + foreach libname [dict keys $libs] { + set libinfo [dict get $libs $libname] + + append result [cmd::util::col lib library $libname] + if {[catch [list package present $libname] ver]} { + append result [cmd::util::col lib version "N/A"] + } else { + append result [cmd::util::col lib version $ver] + } + append result [cmd::util::col lib type [dict get $libinfo -type]] + + if {[dict exists $libinfo -note]} { + set note [dict get $libinfo -note] + } else { + set note "" + } + append result [cmd::util::col lib note $note] + append result "\n" + } + append result "[cmd::util::colbreak lib]\n" + return $result +} + +proc ::pattern::record {recname fields} { + if {[uplevel 1 [list namespace which $recname]] ne ""} { + error "(pattern::record) Can't create command '$recname': A command of that name already exists" + } + + set index -1 + set accessor [list ::apply { + {index rec args} + { + if {[llength $args] == 0} { + return [lindex $rec $index] + } + if {[llength $args] == 1} { + return [lreplace $rec $index $index [lindex $args 0]] + } + error "Invalid number of arguments." + } + + }] + + set map {} + foreach field $fields { + dict set map $field [linsert $accessor end [incr index]] + } + uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec] +} +proc ::pattern::record2 {recname fields} { + if {[uplevel 1 [list namespace which $recname]] ne ""} { + error "(pattern::record) Can't create command '$recname': A command of that name already exists" + } + + set index -1 + set accessor [list ::apply] + + set template { + {rec args} + { + if {[llength $args] == 0} { + return [lindex $rec %idx%] + } + if {[llength $args] == 1} { + return [lreplace $rec %idx% %idx% [lindex $args 0]] + } + error "Invalid number of arguments." + } + } + + set map {} + foreach field $fields { + set body [string map [list %idx% [incr index]] $template] + dict set map $field [list ::apply $body] + } + uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec] +} + +proc ::argstest {args} { + package require cmdline + +} + +proc ::pattern::objects {} { + set result [::list] + + foreach ns [namespace children ::pp] { + #lappend result [::list [namespace tail $ns] [set ${ns}::(self)]] + set ch [namespace tail $ns] + if {[string range $ch 0 2] eq "Obj"} { + set OID [string range $ch 3 end] ;#OID need not be digits (!?) + lappend result [::list $OID [list OID $OID object_command [set pp::${ch}::v_object_command] usedby [array names ${ns}::_iface::o_usedby]]] + } + } + + + + + return $result +} + + + +proc ::pattern::name {num} { + #!todo - fix + #set ::p::${num}::(self) + + lassign [interp alias {} ::p::$num] _predator info + if {![string length $_predator$info]} { + error "No object found for num:$num (no interp alias for ::p::$num)" + } + set invocants [dict get $info i] + set invocants_with_role_this [dict get $invocants this] + set invocant_this [lindex $invocants_with_role_this 0] + + + #lassign $invocant_this id info + #set map [dict get $info map] + #set fields [lindex $map 0] + lassign $invocant_this _id _ns _defaultmethod name _etc + return $name +} + + +proc ::pattern::with {cmd script} { + foreach c [info commands ::p::-1::*] { + interp alias {} [namespace tail $c] {} $c $cmd + } + interp alias {} . {} $cmd . + interp alias {} .. {} $cmd .. + + return [uplevel 1 $script] +} + + + + + +#system diagnostics etc + +proc ::pattern::varspace_list {IID} { + namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_variables o_variables + + set varspaces [list] + dict for {vname vdef} $o_variables { + set vs [dict get $vdef varspace] + if {$vs ni $varspaces} { + lappend varspaces $vs + } + } + if {$o_varspace ni $varspaces} { + lappend varspaces $o_varspace + } + return $varspaces +} + +proc ::pattern::check_interfaces {} { + foreach ns [namespace children ::p] { + set IID [namespace tail $ns] + if {[string is digit $IID]} { + foreach ref [array names ${ns}::_iface::o_usedby] { + set OID [string range $ref 1 end] + if {![namespace exists ::p::${OID}::_iface]} { + puts -nonewline stdout "\r\nPROBLEM!!!!!!!!! nonexistant/invalid object $OID referenced by Interface $IID\r\n" + } else { + puts -nonewline stdout . + } + + + #if {![info exists ::p::${OID}::(self)]} { + # puts "PROBLEM!!!!!!!!! nonexistant object $OID referenced by Interface $IID" + #} + } + } + } + puts -nonewline stdout "\r\n" +} + + +#from: http://wiki.tcl.tk/8766 (Introspection on aliases) +#usedby: metaface-1.1.6+ +#required because aliases can be renamed. +#A renamed alias will still return it's target with 'interp alias {} oldname' +# - so given newname - we require which_alias to return the same info. + proc ::pattern::which_alias {cmd} { + uplevel 1 [list ::trace add execution $cmd enterstep ::error] + catch {uplevel 1 $cmd} res + uplevel 1 [list ::trace remove execution $cmd enterstep ::error] + #puts stdout "which_alias $cmd returning '$res'" + return $res + } +# [info args] like proc following an alias recursivly until it reaches +# the proc it originates from or cannot determine it. +# accounts for default parameters set by interp alias +# + + + +proc ::pattern::aliasargs {cmd} { + set orig $cmd + + set defaultargs [list] + + # loop until error or return occurs + while {1} { + # is it a proc already? + if {[string equal [info procs $cmd] $cmd]} { + set result [info args $cmd] + # strip off the interp set default args + return [lrange $result [llength $defaultargs] end] + } + # is it a built in or extension command we can get no args for? + if {![string equal [info commands $cmd] $cmd]} { + error "\"$orig\" isn't a procedure" + } + + # catch bogus cmd names + if {[lsearch [interp aliases {}] $cmd]==-1} { + if {[catch {::pattern::which_alias $cmd} alias]} { + error "\"$orig\" isn't a procedure or alias or command" + } + #set cmd [lindex $alias 0] + if {[llength $alias]>1} { + set cmd [lindex $alias 0] + set defaultargs [concat [lrange $alias 1 end] $defaultargs] + } else { + set cmd $alias + } + } else { + + if {[llength [set cmdargs [interp alias {} $cmd]]]>0} { + # check if it is aliased in from another interpreter + if {[catch {interp target {} $cmd} msg]} { + error "Cannot resolve \"$orig\", alias leads to another interpreter." + } + if {$msg != {} } { + error "Not recursing into slave interpreter \"$msg\".\ + \"$orig\" could not be resolved." + } + # check if defaults are set for the alias + if {[llength $cmdargs]>1} { + set cmd [lindex $cmdargs 0] + set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs] + } else { + set cmd $cmdargs + } + } + } + } + } +proc ::pattern::aliasbody {cmd} { + set orig $cmd + + set defaultargs [list] + + # loop until error or return occurs + while {1} { + # is it a proc already? + if {[string equal [info procs $cmd] $cmd]} { + set result [info body $cmd] + # strip off the interp set default args + return $result + #return [lrange $result [llength $defaultargs] end] + } + # is it a built in or extension command we can get no args for? + if {![string equal [info commands $cmd] $cmd]} { + error "\"$orig\" isn't a procedure" + } + + # catch bogus cmd names + if {[lsearch [interp aliases {}] $cmd]==-1} { + if {[catch {::pattern::which_alias $cmd} alias]} { + error "\"$orig\" isn't a procedure or alias or command" + } + #set cmd [lindex $alias 0] + if {[llength $alias]>1} { + set cmd [lindex $alias 0] + set defaultargs [concat [lrange $alias 1 end] $defaultargs] + } else { + set cmd $alias + } + } else { + + if {[llength [set cmdargs [interp alias {} $cmd]]]>0} { + # check if it is aliased in from another interpreter + if {[catch {interp target {} $cmd} msg]} { + error "Cannot resolve \"$orig\", alias leads to another interpreter." + } + if {$msg != {} } { + error "Not recursing into slave interpreter \"$msg\".\ + \"$orig\" could not be resolved." + } + # check if defaults are set for the alias + if {[llength $cmdargs]>1} { + set cmd [lindex $cmdargs 0] + set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs] + } else { + set cmd $cmdargs + } + } + } + } + } + + + + + +proc ::pattern::uniqueKey2 {} { + #!todo - something else?? + return [clock seconds]-[incr ::pattern::idCounter] +} + +#used by patternlib package +proc ::pattern::uniqueKey {} { + return [incr ::pattern::idCounter] + #uuid with tcllibc is about 30us compared with 2us + # for large datasets, e.g about 100K inserts this would be pretty noticable! + #!todo - uuid pool with background thread to repopulate when idle? + #return [uuid::uuid generate] +} + + + +#------------------------------------------------------------------------------------------------------------------------- + +proc ::pattern::test1 {} { + set msg "OK" + + puts stderr "next line should say:'--- saystuff:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. PatternMethod saystuff args { + puts stderr "--- saystuff: $args" + } + ::>thing .. Create ::>jjj + + ::>jjj . saystuff $msg + ::>jjj .. Destroy + ::>thing .. Destroy +} + +proc ::pattern::test2 {} { + set msg "OK" + + puts stderr "next line should say:'--- property 'stuff' value:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. PatternProperty stuff $msg + + ::>thing .. Create ::>jjj + + puts stderr "--- property 'stuff' value:[::>jjj . stuff]" + ::>jjj .. Destroy + ::>thing .. Destroy +} + +proc ::pattern::test3 {} { + set msg "OK" + + puts stderr "next line should say:'--- property 'stuff' value:$msg" + ::>pattern .. Create ::>thing + + ::>thing .. Property stuff $msg + + puts stderr "--- property 'stuff' value:[::>thing . stuff]" + ::>thing .. Destroy +} + +#--------------------------------- +#unknown/obsolete + + + + + + + + +#proc ::p::internals::showargs {args {ch stdout}} {puts $ch $args} +if {0} { + proc ::p::internals::new_interface {{usedbylist {}}} { + set OID [incr ::p::ID] + ::p::internals::new_object ::p::ifaces::>$OID "" $OID + puts "obsolete >> new_interface created object $OID" + foreach usedby $usedbylist { + set ::p::${OID}::_iface::o_usedby(i$usedby) 1 + } + set ::p::${OID}::_iface::o_varspace "" ;#default varspace is the object's namespace. (varspace is absolute if it has leading :: , otherwise it's a relative namespace below the object's namespace) + #NOTE - o_varspace is only the default varspace for when new methods/properties are added. + # it is possible to create some methods/props with one varspace value, then create more methods/props with a different varspace value. + + set ::p::${OID}::_iface::o_constructor [list] + set ::p::${OID}::_iface::o_variables [list] + set ::p::${OID}::_iface::o_properties [dict create] + set ::p::${OID}::_iface::o_methods [dict create] + array set ::p::${OID}::_iface::o_definition [list] + set ::p::${OID}::_iface::o_open 1 ;#open for extending + return $OID + } + + + #temporary way to get OID - assumes single 'this' invocant + #!todo - make generic. + proc ::pattern::get_oid {_ID_} { + #puts stderr "#* get_oid: [lindex [dict get $_ID_ i this] 0 0]" + return [lindex [dict get $_ID_ i this] 0 0] + + #set invocants [dict get $_ID_ i] + #set invocant_roles [dict keys $invocants] + #set role_members [dict get $invocants this] + ##set this_invocant [lindex $role_members 0] ;#for the role 'this' we assume only one invocant in the list. + #set this_invocant [lindex [dict get $_ID_ i this] 0] ; + #lassign $this_invocant OID this_info + # + #return $OID + } + + #compile the uncompiled level1 interface + #assert: no more than one uncompiled interface present at level1 + proc ::p::meta::PatternCompile {self} { + ???? + + upvar #0 $self SELFMAP + set ID [lindex $SELFMAP 0 0] + + set patterns [lindex $SELFMAP 1 1] ;#list of level1 interfaces + + set iid -1 + foreach i $patterns { + if {[set ::p::${i}::_iface::o_open]} { + set iid $i ;#found it + break + } + } + + if {$iid > -1} { + #!todo + + ::p::compile_interface $iid + set ::p::${iid}::_iface::o_open 0 + } else { + #no uncompiled interface present at level 1. Do nothing. + return + } + } + + + proc ::p::meta::Def {self} { + error ::p::meta::Def + + upvar #0 $self SELFMAP + set self_ID [lindex $SELFMAP 0 0] + set IFID [lindex $SELFMAP 1 0 end] + + set maxc1 0 + set maxc2 0 + + set arrName ::p::${IFID}:: + + upvar #0 $arrName state + + array set methods {} + + foreach nm [array names state] { + if {[regexp {^m-1,name,(.+)} $nm _match mname]} { + set methods($mname) [set state($nm)] + + if {[string length $mname] > $maxc1} { + set maxc1 [string length $mname] + } + if {[string length [set state($nm)]] > $maxc2} { + set maxc2 [string length [set state($nm)]] + } + } + } + set bg1 [string repeat " " [expr {$maxc1 + 2}]] + set bg2 [string repeat " " [expr {$maxc2 + 2}]] + + + set r {} + foreach nm [lsort -dictionary [array names methods]] { + set arglist $state(m-1,args,$nm) + append r "[overtype::left $bg1 $nm] : [overtype::left $bg2 $methods($nm)] [::list $arglist]\n" + } + return $r + } + + + +} \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patternlib-1.2.6.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patternlib-1.2.6.tm new file mode 100644 index 00000000..bd4b3e59 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patternlib-1.2.6.tm @@ -0,0 +1,2590 @@ +#JMN 2004 +#public domain + + +package provide patternlib [namespace eval patternlib { + + variable version + set version 1.2.6 +}] + + + +#Change History +#------------------------------------------------------------------------------- +#2022-05 +# added . search and . itemKeys methods to >collection to enable lookups by value +#2021-09 +# Add >keyvalprotector - an object to overload various collection methods such as 'remove' to stop deletion of specific items. +# +#2006-05 +# deprecate 'del' in favour of 'remove' - 'del' still there but delegated to 'remove'. todo - emit deprecation warnings. +# +#2005-04 +# remove 'name' method - incorporate indexed retrieval into 'names' method +# !todo? - adjust key/keys methods for consistency? +# +#2004-10 +# initial key aliases support +# fix negative index support on some methods e.g remove +#2004-08 +# separated >collection predicate methods out onto separate 'mixin' object >predicatedCollection +# added $posn $result variables to predicate methods, changed varnames from $k $v to $key $value +# +#2004-06-05 +# added 'sort' method to sort on values. +# fixed 'keySort' method to accept multiple sort options +# added predicate methods 'all' 'allKeys' 'collectAll' +#2004-06-01 +# '>collection . names' method now accepts optional 'glob' parameter to filter result +#2004-05-19 +#fix '>collection . clear' method so consecutive calls don't raise an error +#------------------------------------------------------------------------------- + +namespace eval ::patternlib::util { + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } + + #bloom filter experiment https://wiki.tcl-lang.org/page/A+Simple+Bloom+Filter + # k-hashes + # m-bits + # n-elements + # optimal value of k: (m/n)ln(2) + #proc bloom_optimalNumHashes {capacity_n bitsize_m} { + # expr { round((double($bitsize_m) / $capacity_n) * log(2))} + #} + #proc bloom_optimalNumBits {capacity fpp} { + # expr {entier(-$capacity * log($fpp) / (log(2) * log(2)))} + #} + +} +::patternlib::util::package_require_min pattern 1.2.4 +#package require pattern +::pattern::init ;# initialises (if not already) + + +namespace eval ::patternlib {namespace export {[a-z]*} + namespace export {[>]*} + + variable keyCounter 0 ;#form part of unique keys for collections when items added without any key specified + proc uniqueKey {} { + return [incr ::patternlib::keyCounter] + } + +#!todo - multidimensional collection +# - o_list as nested list +# - o_array with compound keys(?) how will we unambiguously delimit dimensions in a concatenated key? +# - perhaps a key is always a list length n where n is the number of dimensions? +# - therefore we'll need an extra level of nesting for the current base case n=1 +# +# - how about a nested dict for each key-structure (o_list & o_array) ? + +#COLLECTION +# +#!todo? - consider putting the actual array & list vars in the objects namespace, and using the instancevars to hold their names +# - consider array-style access using traced var named same as collection. +# would this defeat the purpose ? if it was faster, would users always use array syntax in preference.. in which case they may as well just use arrays..? +#!todo - add boolean property to force unique values as well as keys + + +#::pattern::create >collection + + + + +::>pattern .. Create >collection +set COL >collection +#process_pattern_aliases [namespace origin >collection] +#process_pattern_aliases ::patternlib::>collection +$COL .. Property version 1.0 +$COL .. PatternDefaultMethod item + +set PV [$COL .. PatternVariable .] + +$PV o_data +#$PV o_array +#$PV o_list +$PV o_alias +$PV this + +#for invert method +$PV o_dupes 0 + + +$COL .. PatternProperty bgEnum + + +#PV o_ns + +$PV m_i_filteredCollection + +#set ID [lindex [set >collection] 0 0] ;#context ID +#set IID [lindex [set >collection] 1 0] ;#level 1 base-interface ID + +$COL .. Constructor {args} { + var o_data m_i_filteredCollection o_count o_bgEnum + + var this + set this @this@ + + set m_i_filteredCollection 0 + if {![llength $args]} { + set o_data [dict create] + #array set o_array [list] + #set o_list [list] + set o_count 0 + } elseif {[llength $args] == 1} { + set o_data [dict create] + set pairs [lindex $args 0] + if {[llength $pairs] % 2} { + error "patternllib::>collection - if an argument given to constructor, it must have an even number of elements. Bad args: $args" + } + set keys_seen [list] + foreach key [dict keys $pairs] { + if {[string is integer -strict $key] } { + error ">collection key must be non-integer. Bad key: $key. No items added." + } + if {$key in $keys_seen} { + error "key '$key' already exists in this collection. No items added." + } + lappend keys_seen $key + } + unset keys_seen + #rely on dict ordering guarantees (post 8.5? preserves order?) + set o_data [dict merge $o_data[set o_data {}] $pairs] + set o_count [dict size $o_data] + } else { + error "patternlib::>collection constructor did not understand arguments supplied. Try a dict as a single argument." + } + array set o_alias [list] + + array set o_bgEnum [list] + @next@ +} +#comment block snipped from collection Constructor + #--------------------------------------------- + #set o_selfID [lindex [set $o_this] 0] ;#object id always available in methods as $_ID_ anyway + # + #### OBSOLETE - left as example of an approach + #make count property traceable (e.g so property ref can be bound to Tk widgets) + #!todo - manually update o_count in relevant methods faster?? + # should avoid trace calls for addList methods, shuffle etc + # + #set handler ::p::${_ID_}::___count_TraceHandler + #proc $handler {_ID_ vname vidx op} { + # #foreach {vname vidx op} [lrange $args end-2 end] {break} + # #! we shouldn't trust this vname - it may be that we are being accessed via upvar so it is a different name + # + # #this is only a 'write' handler + # set ::p::[lindex ${_ID_} 0 0]::o_count [llength [set ::p::[lindex ${_ID_} 0 0]::o_list]] + # return + #} + #trace add variable o_list {write} [list $handler $_ID_] + #### + # + # + #puts "--->collection constructor id: $_ID_" + + + + +set PM [$COL .. PatternMethod .] + + +#!review - why do we need the count method as well as the property? +#if needed - document why. +# read traces on count property can be bypassed by method call... shouldn't we avoid that? +#2018 - in theory write traces on the . count property are very useful from an application-writer's perpective. +# +$COL .. PatternMethod count {} { + #we don't require any instance vars to be upvar'ed - argless [var] stops them automatically being added. + #we directly refer to the ::O:: var if only accessing a few times rather than upvar'ing. + var o_data + dict size $o_data +} + +$COL .. PatternProperty count +$COL .. PatternPropertyWrite count {_val} { + var + error "count property is read-only" +} + +$COL .. PatternPropertyUnset count {} { + var +} ;#cannot raise error's in unset trace handlers - simply fail to unset silently + +$COL .. PatternMethod isEmpty {} { + #var o_list + #return [expr {[llength $o_list] == 0}] + var o_data + expr {[dict size $o_data] == 0} +} + +$COL .. PatternProperty inverted 0 + + + +###### +# item +###### +#defaults to fifo when no idx supplied (same as 'pair' method). !review? is lifo more logical/intuitive/useful? +# i.e [>obj . item] returns the 1st element in the list +#[>obj . item -1] returns the last element (equiv to "end" keyword used by Tcl list commands) +#[>obj . item -2] returns 2nd last element (equiv to "end-1") + + +$COL .. PatternMethod item {{idx 0}} { + #with pattern::0::$OID access.. was measured faster than item2 : approx 110us vs 140us for 26element collection accessed via string (time {>col $key} 10000) + # (still at least 20 times slower than a plain array... at <5us) + var o_data o_alias + + #!todo - review 'string is digit' vs 'string is integer' ?? + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + set keys [dict keys $o_data] + if {[catch {dict get $o_data [lindex $keys $idx]} result]} { + var this + error "no such index : '$idx' in collection: $this" + } else { + return $result + } + } else { + if {[catch {dict get $o_data $idx} result]} { + if {[catch {set o_alias($idx)} nextIdx ]} { + var this + error "no such index: '$idx' in collection: $this" + } else { + #try again + #return $o_array($nextIdx) + #tailcall? + #item $_ID_ $nextIdx + #puts stdout "\n\n\n !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! about to call tailcall item $_ID_ $nextIdx \n\n\n" + tailcall item $_ID_ $nextIdx + } + } else { + return $result + } + } +} + + + +if {0} { +#leave this here for comparison. +$COL .. PatternMethod item2 {{idx 0}} { + var o_array o_list o_alias this + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + if {[catch {set o_array([lindex $o_list $idx])} result]} { + error "no such index : '$idx' in collection: $this" + } else { + return $result + } + } else { + if {[catch {set o_array($idx)} result]} { + + if {[catch {set o_alias($idx)} nextIdx ]} { + error "no such index: '$idx' in collection: $this" + } else { + #try again + #return $o_array($nextIdx) + item $_ID_ $nextIdx + } + } else { + return $result + } + } + +} +} + +#simple no-frills access for speed.. (timed at 43us vs 63us for item (depending on dispatch method!)) +$COL .. PatternMethod itemNamed {idx} { + var o_data + dict get $o_data $idx +} +$COL .. PatternMethod in {idx} { + var o_data + dict get $o_data $idx +} + +$COL .. PatternMethod itemAt {idx} { + var o_data + dict get $o_data [lindex [dict keys $o_data] $idx] +} + +$COL .. PatternMethod replace {idx val} { + var o_data o_alias this + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + if {[catch {dict set o_data [lindex [dict keys $o_data] $idx] $val}]} { + error "no such index: '$idx' in collection: $this" + } else { + return $val + } + } else { + if {[catch {dict set o_data $idx $val}]} { + if {[catch {set o_alias($idx)} nextIdx ]} { + error "no such index: '$idx' in collection: $this" + } else { + #try again + tailcall replace $_ID_ $nextIdx $val + } + + } else { + return $val + } + } +} + +#if the supplied index is an alias, return the underlying key; else return the index supplied. +$COL .. PatternMethod realKey {idx} { + var o_alias + + if {[catch {set o_alias($idx)} key]} { + return $idx + } else { + return $key + } +} + +#note alias feature is possibly ill-considered. +#if we delete an item - should we delete corresponding alias? If not - we then would need to allow adding under an alias only if the corresponding key is missing. +$COL .. PatternMethod alias {newAlias existingKeyOrAlias} { + var o_alias + + #set existingKey [realKey $_ID_ $existingKeyOrAlias] + #alias to the supplied KeyOrAlias - not the underlying key + + if {[string is integer -strict $newAlias]} { + error "collection key alias cannot be integer" + } + + if {[string length $existingKeyOrAlias]} { + set o_alias($newAlias) $existingKeyOrAlias + } else { + unset o_alias($newAlias) + } +} +$COL .. PatternMethod aliases {{key ""}} { + var o_alias + + if {[string length $key]} { + set result [list] + #lsearch -stride? + foreach {n v} [array get o_alias] { + if {$v eq $key} { + lappend result $n $v + } + } + + return $result + } else { + return [array get o_alias] + } +} + +#'pop' & 'unshift' methods !todo - optimize so lsearch not called when numerical idx/posn already supplied + +#default to removing item from the end, otherwise from supplied index (position or key) +#!todo - accept alias indices +#!todo - review.. should any corresponding alias be destroyed when the corresponding item is popped (or removed in any way?) +#!todo - review.. for performance.. shouldn't pop NOT accept an index? +#if we need to pop from other than the end.. this could be a separate function. Do other langs use pop with an index?? +$COL .. PatternMethod pop {{idx ""}} { + var o_data o_count + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] end] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + set posn [lsearch -exact [dict keys $o_data] $key] + + if {($posn >= 0) && ($posn < [dict size $o_data])} { + set result [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } +} +$COL .. PatternMethod poppair {} { + var o_data o_count + set key [lindex [dict keys $o_data] end] + set val [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return [list $key $val] +} + + + +#!todo - add 'push' method... (basically specialized versions of 'add') +#push - add at end (effectively an alias for add) +#shift - add at start ???bad name? this is completely at odds with for example the common Perl shift function, which returns and removes the first element of an array. +#add - add at end + +#ordered +$COL .. PatternMethod items {} { + var o_data + + dict values $o_data +} + + + + +#### +#pair +#### +#fifo-style accesss when no idx supplied (likewise with 'add' method) +$COL .. PatternMethod pair {{idx 0}} { + var o_data + + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + + if {[catch {dict get $o_data $key} val]} { + error "no such index: '$idx'" + } else { + return [list $key $val] + } +} +$COL .. PatternMethod pairs {} { + var o_data + set o_data +} + +$COL .. PatternMethod get {} { + var o_data + set o_data +} +#todo - fix >pattern so that methods don't collide with builtins +#may require change to use oo - or copy 'my' mechanism to call own methods +$COL .. PatternMethod Info {} { + var o_data + return [dict info $o_data] +} +#2006-05-21.. args to add really should be in key, value order? +# - this the natural order in array-like lists +# - however.. key should be optional. + +$COL .. PatternMethod add {val args} { + #(using args instead of {key ""} enables use of empty string as a key ) + + var o_data o_alias o_count this + + if {![llength $args]} { + set key "_[::patternlib::uniqueKey]_" + } else { + #!todo - could we handle multiple val,key pairs without impacting performance of the common case? + if {[llength $args] > 1} { + error "add method expected 'val' and optional 'key' - got: $val $args" + + } + + set key [lindex $args 0] + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + + if {[dict exists $o_data $key]} { + #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]" + error "key '$key' already exists in collection $this" + } + if {[info exists o_alias($key)]} { + if {[dict exists $o_data $o_alias($key)]} { + #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias + error "key '$key' already exists as an alias for $o_alias($key) in collection $this" + } + } + + dict set o_data $key $val + + + set posn $o_count + incr o_count + + return $posn +} + + +#should the 'stack' methods such as shift,push,pop,peek actually be on a separate interface? +#what then of methods like 'count' which apply equally well to collections and stacks? + +#Alias for 'add' - is there a way to alias this to add implementation with zero overhead?? +$COL .. PatternMethod push {val args} { + #(using args instead of {key ""} enables use of empty string as a key ) + + var o_data o_alias o_count this + + if {![llength $args]} { + set key "_[::patternlib::uniqueKey]_" + } else { + #!todo - could we handle multiple val,key pairs without impacting performance of the common case? + if {[llength $args] > 1} { + error "add method expected 'val' and optional 'key' - got: $val $args" + + } + + set key [lindex $args 0] + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + + if {[dict exists $o_data $key]} { + #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]" + error "key '$key' already exists in collection $this" + } + if {[info exists o_alias($key)]} { + if {[dict exists $o_data $o_alias($key)]} { + #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias + error "key '$key' already exists as an alias for $o_alias($key) in collection $this" + } + } + + dict set o_data $key $val + + + set posn $o_count + incr o_count + + return $posn +} + + +#shift/unshift - roughly analogous to those found in Perl & PHP +#unshift adds 1 or more values to the beginning of the collection. +$COL .. PatternMethod unshift {values {keys ""}} { + var o_data o_count + + if {![llength $keys]} { + for {set i 0} {$i < [llength $values]} {incr i} { + lappend keys "_[::patternlib::uniqueKey]_" + } + } else { + #check keys before we insert any of them. + foreach newkey $keys { + if {[string is integer -strict $newkey]} { + error "cannot accept key '$newkey', >collection keys must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + } + } + if {[llength $values] != [llength $keys]} { + error "unshift requires same number of keys as values. (or no keys for auto-generated keys) Received [llength $values] values, [llength $keys] keys" + } + + #separate loop through keys because we want to fail the whole operation if any are invalid. + + set existing_keys [dict keys $o_data] + foreach newkey $keys { + if {$newkey in $exisint_keys} { + #puts stderr "==============> key $key already exists in this collection" + error "key '$newkey' already exists in this collection" + } + } + + + #ok - looks like entire set can be inserted. + set newpairs [list] + foreach val $values key $keys { + lappend newpairs $key $val + } + set o_data [concat $newpairs $o_data[set o_data {}]] + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#default to removing item from the beginning, otherwise from supplied index (position or key) +#!todo - accept alias indices +$COL .. PatternMethod shift {{idx ""}} { + var o_data o_count + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] 0] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + set posn [lsearch -exact [dict keys $o_data] $key] + + if {($posn >= 0) && (($posn/2) < [dict size $o_data])} { + set result [dict get $o_data $key] + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } +} + + +$COL .. PatternMethod peek {} { + var o_data + + #set o_array([lindex $o_list end]) + + #dict get $o_data [lindex [dict keys $o_data] end] + lindex $o_data end +} + +$COL .. PatternMethod peekKey {} { + var o_data + #lindex $o_list end + lindex $o_data end-1 +} + + +$COL .. PatternMethod insert {val args} { + var o_data o_count + + set idx 0 + set key "" + + if {[llength $args] <= 2} { + #standard arg (ordered) style: + #>obj . insert $value $position $key + + lassign $args idx key + } else { + #allow for literate programming style: + #e.g + # >obj . insert $value at $listPosition as $key + + if {[catch {array set iargs $args}]} { + error "insert did not understand argument list. +usage: +>obj . insert \$val \$position \$key +>obj . insert \$val at \$position as \$key" + } + if {[info exists iargs(at)]} { + set idx $iargs(at) + } + if {[info exists iargs(as)]} { + set key $iargs(as) + } + } + + if {![string length $key]} { + set key "_[::patternlib::uniqueKey]_" + } + + if {[string is integer -strict $key]} { + error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys" + } + + + if {[dict exists $o_data $key]} { + #puts stderr "==============> key $key already exists in this collection" + error "key '$key' already exists in this collection" + } + + if {$idx eq "end"} { + #lappend o_list $key + #standard dict set will add it to the end anyway + dict set o_data $key $val + + } else { + #set o_list [linsert $o_list $idx $key] + + #treat dict as list + set o_data [linsert $o_data[set o_data {}] [expr {$idx*2}] $key $val] + } + + + #set o_array($key) $val + + + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#!todo - deprecate and give it a better name! addDict addPairs ? +$COL .. PatternMethod addArray {list} { + var + puts stderr "patternlib::>collection WARNING: addArray deprecated - call addPairs with same argument instead" + tailcall addPairs $_ID_ $list +} +$COL .. PatternMethod addPairs {list} { + var o_data o_alias o_count + if {[llength $list] % 2} { + error "must supply an even number of elements" + } + + set aliaslist [array names o_alias] + #set keylist [dict keys $o_data] + foreach newkey [dict keys $list] { + if {[string is integer -strict $newkey] } { + error ">collection key must be non-integer. Bad key: $newkey. No items added." + } + + #if {$newkey in $keylist} {} + #for small to medium collections - testing for newkey in $keylist is probably faster, + # but we optimise here for potentially large existing collections, where presumably a dict exists lookup will be more efficient. + if {[dict exists $o_data $newkey]} { + error "key '$newkey' already exists in this collection. No items added." + } + #The assumption is that there are in general relatively few aliases - so a list test is appropriate + if {$newkey in $aliaslist} { + if {[dict exists $o_data $o_alias($newkey)]} { + error "key '$newkey' already exists as an alias for $o_alias($newkey) in collection. No items added " + } + } + #! check if $list contains dups? + #- slows method down - for little benefit? + } + #!todo - test? (but we need a loop to test for integer keys.. so what's the point?) + #set intersection [struct::set intersect [dict keys $list] [dict keys $o_data]] + #if {[llength $intersection]} { + # error "keys '$intersection' already present in this collection. No items added." + #} + + + #rely on dict ordering guarantees (post 8.5? preserves order?) + set o_data [dict merge $o_data[set o_data {}] $list] + + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} +$COL .. PatternMethod addList {list} { + var o_data o_count + + foreach val $list { + dict set o_data "_[::patternlib::uniqueKey]_" $val + #!todo - test. Presumably lappend faster because we don't need to check existing keys.. + #..but.. is there shimmering involved in treating o_data as a list? + #lappend o_data _[::patternlib::uniqueKey]_ $val + + #tested 2008-06 tcl8.6a0 lappend was slower as the gain is lost (and more!) during subsequent [dict size $o_data] + } + set o_count [dict size $o_data] + + return [expr {$o_count - 1}] +} + +#'del' is not a very good name... as we're not really 'deleting' anything. +# 'remove' seems better, and appears to be more consistent with other languages' collection implementations. +#!todo - handle 'endRange' parameter for removing ranges of items. +$COL .. PatternMethod del {idx {endRange ""}} { + var + #!todo - emit a deprecation warning for 'del' + tailcall remove $_ID_ $idx $endRange +} + +$COL .. PatternMethod remove {idx {endRange ""}} { + var o_data o_count o_alias this + + if {[string length $endRange]} { + 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} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx' in collection: $this" + } else { + #try with next key in alias chain... + #return [remove $_ID_ $nextKey] + tailcall remove $_ID_ $nextKey + } + } + } + + dict unset o_data $key + + set o_count [dict size $o_data] + return +} + +#ordered +$COL .. PatternMethod names {{globOrIdx {}}} { + var o_data + + if {[llength $globOrIdx]} { + if {[string is integer -strict $globOrIdx]} { + #Idx + set idx $globOrIdx + + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + + + + if {[catch {lindex [dict keys $o_data] $idx} result]} { + error "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] + } +} + +#ordered +$COL .. PatternMethod keys {} { + #like 'names' but without globbing + var o_data + dict keys $o_data +} + +#Unfortunately the string 'name' is highly collidable when mixing in a collection over existing objects +# - !todo - review. Is it worth adjusting the collection methodnames to avoid a few common collision cases? +# - some sort of resolution order/interface-selection is clearly required anyway +# so perhaps it's generally best not to bother being 'polite' here, and implement a robust understandable resolution mechanism. +# In the mean time however... we'll at least avoid 'name'! +# +#$PM name {{posn 0}} { +# var o_array o_list +# +# if {$posn < 0} { +# set posn "end-[expr {abs($posn + 1)}]" +# } +# +# if {[catch {lindex $o_list $posn} result]} { +# error "no such index : '$posn'" +# } else { +# return $result +# } +#} + +$COL .. PatternMethod key {{posn 0}} { + var o_data + + if {$posn < 0} { + set posn "end-[expr {abs($posn + 1)}]" + } + + if {[catch {lindex [dict keys $o_data] $posn} result]} { + error "no such index : '$posn'" + } else { + return $result + } +} + + +#!todo - consider use of 'end-x' syntax for 'to', and implications re consistency with other commands. +$COL .. PatternMethod setPosn {idx to} { + var o_data + + if {![string is integer -strict $to]} { + error "destination position must be numeric, consider reKey method if you are trying to change the string key under which this value is stored" + } + + if {[string is integer -strict $idx]} { + set idx [expr {$idx % [dict size $o_data]}] + + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + } + + set to [expr {$to % [dict size $o_data]}] + + + set val [dict get $o_data $key] + dict unset o_data $key + + #treat dict as list + set o_data [linsert $o_data[set o_data {}] [expr {$posn*2}] $key $val] + + #set o_list [lreplace $o_list $posn $posn] + #set o_list [linsert $o_list $to $key] + + return $to +} +#!todo - improve efficiency of calls to other functions on this object.. 'inline'?? +#presumably the collection object functionality will be long-term stable because it's purpose is to be a core datastructure; therefore it should be reasonable to favour efficiency over maintainability. +$COL .. PatternMethod incrPosn {idx {by 1}} { + var o_data + if {[string is integer -strict $idx]} { + set idx [expr {$idx % [dict size $o_data]}] + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + } + + set newPosn [expr {($posn + $by) % [dict size $o_data]}] + + setPosn $_ID_ $posn $newPosn + return $newPosn +} +$COL .. PatternMethod decrPosn {idx {by 1}} { + var + return [incrPosn $_ID_ $idx [expr {- $by}]] +} +$COL .. PatternMethod move {idx to} { + var + return [setPosn $_ID_ $idx $to] +} +$COL .. PatternMethod posn {key} { + var o_data + return [lsearch -exact [dict keys $o_data] $key] +} + +#!todo? - disallow numeric values for newKey so as to be consistent with add +#!note! - item can be reKeyed out from under an alias such that the alias chain no longer points to anything +# - this is ok. +$COL .. PatternMethod reKey {idx newKey} { + var o_data o_alias + + + if {[dict exists $o_data $newKey]} { + #puts stderr "==============> reKey collision, key $newKey already exists in this collection" + error "reKey collision, key '$newKey' already exists in this collection" + } + if {[info exists o_alias($newKey)]} { + if {[dict exists $o_data $o_alias($newKey)]} { + error "reKey collision, key '$newKey' already present as an alias in this collection" + } else { + set newKey $o_alias($newKey) + } + } + + + + 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} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx'" + } else { + #try with next key in alias chain... + #return [reKey $_ID_ $nextKey $newKey] + tailcall reKey $_ID_ $nextKey $newKey + } + } + } + + #set o_list [lreplace $o_list $posn $posn $newKey] + ##atomic? (traces on array?) + #set o_array($newKey) $o_array($key) + #unset o_array($key) + + dict set o_data $newKey [dict get $o_data $key] + dict unset o_data $key + + return +} +$COL .. PatternMethod hasKey {key} { + var o_data + dict exists $o_data $key +} +$COL .. PatternMethod hasAlias {key} { + var o_alias + info exists o_alias($key) +} + +#either key or alias +$COL .. PatternMethod hasIndex {key} { + var o_data o_alias + if {[dict exists $o_data $key]} { + return 1 + } else { + return [info exists o_alias($key)] + } +} + + +#Shuffle methods from http://mini.net/tcl/941 +$COL .. PatternMethod shuffleFast {} { + #shuffle6 - fast, but some orders more likely than others. + + var o_data + + set keys [dict keys $o_data] + + set n [llength $keys] + for { set i 1 } { $i < $n } { incr i } { + set j [expr { int( rand() * $n ) }] + set temp [lindex $keys $i] + lset keys $i [lindex $keys $j] + lset keys $j $temp + } + + #rebuild dict in new order + #!todo - can we do the above 'in place'? + set newdata [dict create] + foreach k $keys { + dict set newdata $k [dict get $o_data $k] + } + set o_data $newdata + + return +} +$COL .. PatternMethod shuffle {} { + #shuffle5a + + var o_data + + set n 1 + set keys [list] ;#sorted list of keys + foreach k [dict keys $o_data] { + #set index [expr {int(rand()*$n)}] + + #set slist [linsert [::pattern::K $keys [set keys {}]] $index $k] + + #faster alternative.. 'inline K' [lindex [list a b] 0] ~ [K a b] + set keys [linsert [lindex [list $keys [set keys {}]] 0] [expr {int(rand()*$n)}] $k] + incr n + } + + #rebuild dict in new order + #!todo - can we do the above 'in place'? + set newdata [dict create] + foreach k $keys { + dict set newdata $k [dict get $o_data $k] + } + set o_data $newdata + + return +} + + +#search is a somewhat specialised form of 'itemKeys' +$COL .. PatternMethod search {value args} { + var o_data + #only search on values as it's possible for keys to match - especially with options such as -glob + 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 + } +} + +#inverse lookup +$COL .. PatternMethod itemKeys {value} { + var o_data + #only search on values as it's possible for keys to match + 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 +} + +#invert: +#change collection to be indexed by its values with the old keys as new values. +# - keys of duplicate values become a list keyed on the value. +#e.g the array equivalent is: +# arr(a) 1 +# arr(b) 2 +# arr(c) 2 +#becomes +# inv(1) a +# inv(2) {b c} +#where the order of duplicate-value keys is not defined. +# +#As the total number of keys may change on inversion - order is not preserved if there are ANY duplicates. +# + + +#!todo - try just [lreverse $o_data] ?? + + +$COL .. PatternMethod invert {{splitvalues ""}} { + + var o_data o_count o_dupes o_inverted + + + if {$splitvalues eq ""} { + #not overridden - use o_dupes from last call to determine if values are actually keylists. + if {$o_dupes > 0} { + set splitvalues 1 + } else { + set splitvalues 0 + } + } + + + #set data [array get o_array] + set data $o_data + + if {$o_count > 500} { + #an arbitrary optimisation for 'larger' collections. + #- should theoretically keep the data size and save some reallocations. + #!todo - test & review + # + foreach nm [dict keys $o_data] { + dict unset o_data $nm + } + } else { + set o_data [dict create] + } + + if {!$splitvalues} { + dict for {k v} $data { + dict set o_data $v $k + } + } else { + dict for {k v} $data { + #we're splitting values because each value is a list of keys + #therefore sub should be unique - no need for lappend in this branch. + foreach sub $v { + #if {[info exists o_array($sub)]} { + # puts stderr "---here! v:$v sub:$sub k:$k" + # lappend o_array($sub) $k + #} else { + dict set o_data $sub $k + #} + } + } + } + + + if {[dict size $o_data] != $o_count} { + #must have been some dupes + + set o_dupes [expr {$o_count - [dict size $o_data]}] + #update count to match inverted collection + set o_count [dict size $o_data] + } else { + set o_dupes 0 + } + + set o_inverted [expr {!$o_inverted}] + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $o_dupes +} + + + + + + +#NOTE: values are treated as lists and split into separate keys for inversion only if requested! +# To treat values as keylists - set splitvalues 1 +# To treat each value atomically - set splitvalues 0 +# i.e only set splitvalues 1 if you know the values represent duplicate keys from a previous call to invert! +# +# +#Initially call invert with splitvalues = 0 +#To keep calling invert and get back where you started.. +# The rule is... if the previous call to invert returned > 0... pass 1 on the next call. +# +$COL .. PatternMethod invert_manual {{splitvalues 0}} { + #NOTE - the list nesting here is *tricky* - It probably isn't broken. + + var o_list o_array o_count + + set data [array get o_array] + + if {$o_count > 500} { + #an arbitrary optimisation for 'large' collections. + #- should theoretically keep the array size and save some reallocations. + #!todo - test & review + # + foreach nm [array names o_array] { + unset o_array($nm) + } + } else { + array unset o_array + } + + if {!$splitvalues} { + foreach {k v} $data { + lappend o_array($v) $k + } + } else { + foreach {k v} $data { + #we're splitting values because each value is a list of keys + #therefore sub should be unique - no need for lappend in this branch. + foreach sub $v { + #if {[info exists o_array($sub)]} { + # puts stderr "---here! v:$v sub:$sub k:$k" + # lappend o_array($sub) $k + #} else { + set o_array($sub) $k + #} + } + } + } + + + if {[array size o_array] != $o_count} { + #must have been some dupes + set o_list [array names o_array] + + + set dupes [expr {$o_count - [array size o_array]}] + #update count to match inverted collection + set o_count [array size o_array] + } else { + #review - are these machinations worthwhile for order preservation? what speed penalty do we pay? + array set prev $data + set i -1 + if {$splitvalues} { + #values are lists of length one. Take lindex 0 so list values aren't overnested. + foreach oldkey $o_list { + lset o_list [incr i] [lindex $prev($oldkey) 0] + } + } else { + foreach oldkey $o_list { + lset o_list [incr i] $prev($oldkey) + } + } + + set dupes 0 + } + + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $dupes +} + + + +#Note that collections cannot be inverted without loss of information if they have duplicates AND compound keys +# (keys that are lists) +$COL .. PatternMethod invert_lossy {{splitvalues 1}} { + var o_list o_array o_count + + set data [array get o_array] + + if {$o_count > 500} { + #an arbitrary optimisation for 'large' collections. + #- should theoretically keep the array size and save some reallocations. + #!todo - test & review + # + foreach nm [array names o_array] { + unset o_array($nm) + } + } else { + array unset o_array + } + + if {!$splitvalues} { + foreach {k v} $data { + #note! we must check for existence and use 'set' for first case. + #using 'lappend' only will result in deeper nestings on each invert! + #If you don't understand this - don't change it! + if {[info exists o_array($v)]} { + lappend o_array($v) $k + } else { + set o_array($v) $k + } + } + } else { + foreach {k v} $data { + #length test necessary to avoid incorrect 'un-nesting' + #if {[llength $v] > 1} { + foreach sub $v { + if {[info exists o_array($sub)]} { + lappend o_array($sub) $k + } else { + set o_array($sub) $k + } + } + #} else { + # if {[info exists o_array($v)]} { + # lappend o_array($v) $k + # } else { + # set o_array($v) $k + # } + #} + } + } + + + if {[array size o_array] != $o_count} { + #must have been some dupes + set o_list [array names o_array] + + + set dupes [expr {$o_count - [array size o_array]}] + #update count to match inverted collection + set o_count [array size o_array] + } else { + #review - are these machinations worthwhile for order preservation? what speed penalty do we pay? + array set prev $data + set i -1 + foreach oldkey $o_list { + lset o_list [incr i] $prev($oldkey) + } + set dupes 0 + } + + + #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes' + return $dupes +} + +$COL .. PatternMethod reverse {} { + var o_data + + 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 +} + +$COL .. PatternMethod keySort {{options -ascii}} { + var o_data + + set keys [lsort {*}$options [dict keys $o_data]] + + set dictnew [dict create] + foreach k $keys { + dict set dictnew $k [dict get $o_data $k] + } + set o_data $dictnew + + return +} + +#!todo - allow simple options in combination with options such as -command and -object. Redo args handling completely for more complex sorting. +$COL .. PatternMethod sort {args} { + var o_data + + #defaults + set options [dict create -index 1] ;#values always in subelement 1 of name-value pair list for sorting. + + set options_simple [list] + + + for {set i 0} {$i < [llength $args]} {incr i} { + set a [lindex $args $i] + switch -- $a { + -indices - + -ascii - + -dictionary - + -integer - + -real - + -increasing - + -decreasing { + #dict set options $a 1 + lappend options_simple $a + } + -unique { + #not a valid option + #this would stuff up the data... + #!todo? - remove dups from collection if this option used? - alias the keys? + } + -object { + #!todo - treat value as object and allow sorting by sub-values .eg >col1 . sort -object ". sub . property" -increasing + #may be slow - but handy. Consider -indexed property to store/cache these values on first run + } + -command { + dict set options $a [lindex $args [incr i]] + } + -index { + #allow sorting on subindices of the value. + dict set options -index [concat [dict get $options -index] [lindex $args [incr i]] ] + } + default { + #unrecognised option - print usage? + } + } + } + + + + if {[set posn [lsearch -exact $options_simple "-indices"]] >= 0} { + + var o_array + + set slist [list] + foreach k [dict keys $o_data] { + lappend slist [list $k [dict get $o_data $k]] + } + return [lsort {*}$options_simple {*}$options $slist] + + + + #set options_simple [lreplace $options_simple $posn $posn] ;# + #set slist [list] + #foreach {n v} [array get ::p::[lindex ${_ID_} 0 0]::o_array] { + # lappend slist [list $n $v] + #} + #set slist [lsort {*}$options_simple {*}$options $slist] + #foreach i $slist { + # #determine the position in the collections list + # lappend result {*}[lsearch -exact $o_list [lindex $i 0]] + #} + #return $result + } else { + set slist [list] + dict for {k v} $o_data { + lappend slist [list $k $v] + } + #set slist [lsort {*}$options_simple {*}$options $slist] + set slist [lsort {*}$options_simple {*}$options $slist[set slist {}]] ;#K combinator for efficiency + + + #set o_list [lsearch -all -inline -subindices -index 0 $slist *] + + set o_data [dict create] + foreach pair $slist { + dict set o_data [lindex $pair 0] [lindex $pair 1] + } + + + + return + } + +} + + +$COL .. PatternMethod clear {} { + var o_data o_count + + set o_data [dict create] + set o_count 0 + #aliases? + return +} + +#see http://wiki.tcl.tk/15271 - A generic collection traversal interface +# +#!todo - options: -progresscommand -errorcommand (-granularity ?) (-self ? (to convert to an iterator?)) +#!todo? - lazy retrieval of items so that all changes to the collection are available to a running asynch enumeration? +# - should this be an option? which mechanism should be the default? +# - currently only the keylist is treated in 'snapshot' fashion +# so values could be changed and the state could be invalidated by other code during an enumeration +# +$COL .. PatternMethod enumerate {args} { + #---------- + lassign [lrange $args end-1 end] cmd seed + set optionlist [list] + foreach a [lrange $args 0 end-2] { + lappend optionlist $a + } + set opt(-direction) left + set opt(-completioncommand) "" + array set opt $optionlist + #---------- + var o_data + + if {[string tolower [string index $opt(-direction) 0]] eq "r"} { + #'right' 'RIGHT' 'r' etc. + set list [lreverse [dict keys $o_data]] + } else { + #normal left-right order + set list [dict keys $o_data] + } + + if {![string length $opt(-completioncommand)]} { + #standard synchronous processing + foreach k $list { + set seed [uplevel #0 [list {*}$cmd $seed [dict get $o_data $k]]] + } + return $seed + } else { + #ASYNCHRONOUS enumeration + var this o_bgEnum + #!todo - make id unique + #!todo - facility to abort running enumeration. + set enumID enum[array size o_bgEnum] + + set seedvar [$this . bgEnum $enumID .] + set $seedvar $seed + + after 0 [list $this . _doBackgroundEnum $enumID $list $cmd $seedvar $opt(-completioncommand)] + return $enumID + } +} + +#!todo - make private? - put on a separate interface? +$COL .. PatternMethod _doBackgroundEnum {enumID slice cmd seedvar completioncommand} { + var this o_data + + + #Note that we don't post to the eventqueue using 'foreach s $slice' + # we only schedule another event after each item is processed + # - otherwise we would be spamming the eventqueue with items. + + #!todo? - accept a -granularity option to allow handling of n list-items per event? + + if {[llength $slice]} { + set slice [lassign $slice head] + + set script [string map [list %cmd% $cmd %seedvar% $seedvar %val% [dict get $o_data $head]] { + %cmd% [set %seedvar%] %val% + }] + + #post to eventqueue and re-enter _doBackgroundEnum + # + after idle [list after 0 [subst {set $seedvar \[uplevel #0 [list $script] \]; $this . _doBackgroundEnum $enumID [list $slice] [list $cmd] $seedvar [list $completioncommand]}]] + + } else { + #done. + + set script [string map [list %cmd% $completioncommand %seedvar% $seedvar] { + lindex [list [%cmd% [set %seedvar%]] [unset %seedvar%]] 0 + }] + + after idle [list after 0 [list uplevel #0 $script]] + } + + return +} + +$COL .. PatternMethod enumeratorstate {} { + var o_bgEnum + parray o_bgEnum +} + +#proc ::bgerror {args} { +# puts stderr "=bgerror===>$args" +#} + + +#map could be done in terms of the generic 'enumerate' method.. but it's slower. +# +#$PM map2 {proc} { +# var +# enumerate $_ID_ [list ::map-helper $proc] [list] +#} +#proc ::map-helper {proc accum item} { +# lappend accum [uplevel #0 [list {*}$proc $item]] +#} + +$COL .. PatternMethod map {cmd} { + var o_data + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list {*}$cmd $v]] + } + + return $seed +} +$COL .. PatternMethod objectmap {cmd} { + var o_data + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list $v {*}$cmd]] + } + + return $seed +} + + +#End core collection functionality. +#collection 'mixin' interfaces + +>pattern .. Create >keyvalprotector +>keyvalprotector .. PatternVariable o_protectedkeys +>keyvalprotector .. PatternVariable o_protectedvals + +#!todo - write test regarding errors in Constructors for mixins like this +# - an error (e.g from bad args) can cause errors with vars after it's re-run with correct args +>keyvalprotector .. Constructor {args} { + var this o_protectedkeys o_protectedvals + set this @this@ + #---------------------------------------------------------------------------- + set known_opts [list -keys -vals ] + dict set default -keys [list] + dict set default -vals [list] + if {([llength $args] % 2) != 0} { + error "(>keyvalprotector .. Constructor) ERROR: uneven options supplied - must be of form '-option value' " + } + foreach {k v} $args { + if {$k ni $known_opts} { + error "(>keyvalprotector .. Constructor) ERROR: option '$k' not in known options: '$known_opts'" + } + } + set opts [dict merge $default $args] + set o_protectedkeys [dict get $opts -keys] + set o_protectedvals [dict get $opts -vals] + #---------------------------------------------------------------------------- + set protections [concat $o_protectedkeys $o_protectedvals] + if {![llength $protections]} { + error "(>keyvalprotector .. Constructor) ERROR: must supply at least one argument to -vals or -keys" + } + +} +>keyvalprotector .. PatternMethod clear {} { + error "(>keyvalprotector . clear) ERROR: This collection is protected by a >keyvalprotector mixin. Cannot clear" +} +>keyvalprotector .. PatternMethod pop {{idx ""}} { + var o_data o_count o_protectedkeys o_protectedvals + + if {$idx eq ""} { + set key [lindex [dict keys $o_data] end] + } else { + if {[string is integer -strict $idx]} { + set key [lindex [dict keys $o_data] $idx] + } else { + set key $idx + } + } + + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . pop) ERROR: Cannot pop object with index '$idx', key '$key' from collection." + } + set posn [lsearch -exact [dict keys $o_data] $key] + if {($posn >= 0) && ($posn < [dict size $o_data])} { + set result [dict get $o_data $key] + if {$result in $o_protectedvals} { + error "(>keyvalprotector . pop) ERROR: Cannot pop object '$result' with index '$idx', key '$key' from collection." + } + dict unset o_data $key + set o_count [dict size $o_data] + return $result + } else { + error "no such index: '$idx'" + } + +} +>keyvalprotector .. PatternMethod remove {idx {endRange ""}} { + var this o_data o_count o_alias o_protectedkeys o_protectedvals + + if {[string length $endRange]} { + 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] + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' key '$key' from collection" + } + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + if {$posn < 0} { + if {[catch {set o_alias($key)} nextKey]} { + error "no such index: '$idx' in collection: $this" + } else { + if {$key in $o_protectedkeys} { + error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' from collection" + } + #try with next key in alias chain... + #return [remove $_ID_ $nextKey] + tailcall remove $_ID_ $nextKey + } + } + } + + dict unset o_data $key + + set o_count [dict size $o_data] + return +} + +#1) +#predicate methods (order preserving) +#usage: +# >collection .. Create >c1 +# >predicatedCollection .. Create >c1 ;#overlay predicate methods on existing collection + +#e.g >col1 . all {$val > 14} +#e.g >col1 . filterToCollection {$val > 19} . count +#e.g >col1 . filter {[string match "x*" $key]} +#!todo - fix. currying fails.. + +::>pattern .. Create >predicatedCollection +#process_pattern_aliases ::patternlib::>predicatedCollection + +set PM [>predicatedCollection .. PatternMethod .] + +>predicatedCollection .. PatternMethod filter {predicate} { + var this o_list o_array + set result [list] + + #!note (jmn 2004) how could we do smart filtering based on $posn? + #i.e it would make sense to lrange $o_list based on $posn... + #but what about complicated expressions where $posn is a set of ranges and/or combined with tests on $key & $val ?? + #Seems better to provide an alternative efficient means of generating subcolllections/ranges to perform predicate operations upon. + #given this, is $posn even useful? + + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $val + } + incr posn + } + set result +} +>predicatedCollection .. PatternMethod filterToKeys {predicate} { + var this o_list o_array + set result [list] + + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $key + } + incr posn + } + set result +} +>predicatedCollection .. PatternMethod filterToCollection {predicate {destCollection {}}} { + #!todo - collection not in subordinate namespace? -> if subordinate, should imply modification of sub's contents will be reflected in parent? + #!todo - implement as 'view' on current collection object.. extra o_list variables? + #!todo - review/document 'expected' key collision behaviour - source keys used as dest keys.. -autokey option required? + var this o_list o_array m_i_filteredCollection + + incr m_i_filteredCollection + if {![string length $destCollection]} { + #!todo? - implement 'one-shot' object (similar to RaTcl) + set result [::patternlib::>collection .. Create [$this .. Namespace]::>filteredCollection-$m_i_filteredCollection] + } else { + set result $destCollection + } + + #### + #externally manipulate new collection + #set ADD [$c . add .] + #foreach key $o_list { + # set val $o_array($key) + # if $predicate { + # $ADD $val $key + # } + #} + ### + + #internal manipulation faster + #set cID [lindex [set $result] 0] + set cID [lindex [$result --] 0] + + #use list to get keys so as to preserve order + set posn 0 + upvar #0 ::p::${cID}::o_array cARRAY ::p::${cID}::o_list cLIST + foreach key $o_list { + set val $o_array($key) + if $predicate { + if {[info exists cARRAY($key)]} { + error "key '$key' already exists in this collection" + } + lappend cLIST $key + set cARRAY($key) $val + } + incr posn + } + + return $result +} + +#NOTE! unbraced expr/if statements. We want to evaluate the predicate. +>predicatedCollection .. PatternMethod any {predicate} { + var this o_list o_array + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + return 1 + } + incr posn + } + return 0 +} +>predicatedCollection .. PatternMethod all {predicate} { + var this o_list o_array + set posn 0 + foreach key $o_list { + set val $o_array($key) + if !($predicate) { + return 0 + } + incr posn + } + return 1 +} +>predicatedCollection .. PatternMethod dropWhile {predicate} { + var this o_list o_array + set result [list] + set _idx 0 + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + incr _idx + } else { + break + } + incr posn + } + set remaining [lrange $o_list $_idx end] + foreach key $remaining { + set val $o_array($key) + lappend result $val + } + return $result +} +>predicatedCollection .. PatternMethod takeWhile {predicate} { + var this o_list o_array + set result [list] + set posn 0 + foreach key $o_list { + set val $o_array($key) + if $predicate { + lappend result $val + } else { + break + } + incr posn + } + set result +} + + + +#end >collection mixins +###################################### + + + + +#----------------------------------------------------------- +#!TODO - methods for converting an arrayHandle to & from a hashMap efficiently? +# Why do we need both? apart from the size variable, what is the use of hashMap? +#----------------------------------------------------------- +#::pattern::create >hashMap +::>pattern .. Create >hashMap + +>hashMap .. PatternVariable o_size +>hashMap .. PatternVariable o_array + +>hashMap .. Constructor {args} { + var o_array o_size + array set o_array [list] + set o_size 0 +} +>hashMap .. PatternDefaultMethod "item" +>hashMap .. PatternMethod item {key} { + var o_array + set o_array($key) +} +>hashMap .. PatternMethod items {} { + var o_array + + set result [list] + foreach nm [array names o_array] { + lappend result $o_array($nm) + } + return $result +} +>hashMap .. PatternMethod pairs {} { + var o_array + + array get o_array +} +>hashMap .. PatternMethod add {val key} { + var o_array o_size + + set o_array($key) $val + incr o_size + return $key +} + +>hashMap .. PatternMethod del {key} { + var + puts stderr "warning: 'del' method of >hashMap deprecated. Use 'remove' instead." + remove $_ID_ $key +} +>hashMap .. PatternMethod remove {key} { + var o_array o_size + unset o_array($key) + incr o_size -1 + return $key +} +>hashMap .. PatternMethod count {} { + var o_size + #array size o_array + return $o_size +} +>hashMap .. PatternMethod count2 {} { + var o_array + #array size o_array ;#slow, at least for TCLv8.4.4 + #even array statistics is faster than array size ! + #e.g return [lindex [array statistics o_array] 0] + #but.. apparently there are circumstances where array statistics doesn't report the correct size. + return [array size o_array] +} +>hashMap .. PatternMethod names {} { + var o_array + array names o_array +} +>hashMap .. PatternMethod keys {} { + #synonym for names + var o_array + array names o_array +} +>hashMap .. PatternMethod hasKey {key} { + var o_array + return [info exists o_array($key)] +} +>hashMap .. PatternMethod clear {} { + var o_array o_size + unset o_array + set o_size 0 + return +} +#>hashMap .. Ready 1 + + + + + + + + + + + + + + + +#explicitly create metadata. Not required for user-defined patterns. +# this is only done here because this object is used for the metadata of all objects +# so the object must have all it's methods/props before its own metadata structure can be built. +#uplevel 1 "::pattern::object ::pattern::>_nullMeta createMetadata >collection" +#uplevel 1 "::patternlib::>collection .. CreateMetadata ::patternlib::>collection" + + + + +if 0 { + + +#----------------------------------------------------------- +#::pattern::create >arrayHandle { +# variable o_arrayName +# variable this +#} +::>pattern .. Create >arrayHandle + +>arrayHandle .. PatternVariable o_arrayName +>arrayHandle .. PatternVariable this + +>arrayHandle .. Constructor {args} { + var o_arrayName this + set this @this@ + + + set o_arrayName [$this .. Namespace]::array + + upvar #0 $o_arrayName $this + #? how to automatically update this after a namespace import? + + array set $o_arrayName [list] + +} +>arrayHandle .. PatternMethod array {} { + var o_arrayName + return $o_arrayName +} + +#------------------------------------------------------- +#---- some experiments +>arrayHandle .. PatternMethod up {varname} { + var o_arrayName + + #is it dodgy to hard-code the calling depth? + #will it be different for different object systems? + #Will it even be consistent for the same object. + # Is this method necessary anyway? - + # - users can always instead do: + # upvar #0 [>instance . array] var + + uplevel 3 [list upvar 0 $o_arrayName $varname] + + return +} +>arrayHandle .. PatternMethod global {varname} { + var o_arrayName + # upvar #0 [>instance . array] var + + if {![string match ::* $varname]} { + set varname ::$varname + } + + upvar #0 $o_arrayName $varname + + return +} +>arrayHandle .. PatternMethod depth {} { + var o_arrayName + # + for {set i 0} {$i < [info level]} { + puts "${i}: [uplevel $i [list namespace current] , [info level $i]]" + } + +} + # -------------------------------------------- + + +>arrayHandle .. PatternMethod item {key} { + var o_arrayName + set ${o_arrayName}($key) +} +>arrayHandle .. PatternMethod items {} { + var o_arrayName + + set result [list] + foreach nm [array names $o_arrayName] { + lappend result [set ${o_arrayName}($nm)] + } + return $result +} +>arrayHandle .. PatternMethod pairs {} { + var o_arrayName + + array get $o_arrayName +} +>arrayHandle .. PatternMethod add {val key} { + var o_arrayName + + set ${o_arrayName}($key) $val + return $key +} +>arrayHandle .. PatternMethod del {key} { + puts stderr "Warning: 'del' method of >arrayHandle deprecated. Use 'remove' instead." + remove $_ID_ $key +} +>arrayHandle .. PatternMethod remove {key} { + var o_arrayName + unset ${o_arrayName}($key) + return $key +} +>arrayHandle .. PatternMethod size {} { + var o_arrayName + return [array size $o_arrayName] +} +>arrayHandle .. PatternMethod count {} { + #alias for size + var o_arrayName + return [array size $o_arrayName] +} +>arrayHandle .. PatternMethod statistics {} { + var o_arrayName + return [array statistics $o_arrayName] +} +>arrayHandle .. PatternMethod names {} { + var o_arrayName + array names $o_arrayName +} +>arrayHandle .. PatternMethod keys {} { + #synonym for names + var o_arrayName + array names $o_arrayName +} +>arrayHandle .. PatternMethod hasKey {key} { + var o_arrayName + + return [info exists ${o_arrayName}($key)] +} +>arrayHandle .. PatternMethod clear {} { + var o_arrayName + unset $o_arrayName + array set $o_arrayName [list] + + return +} +#>arrayHandle .. Ready 1 + + + + +::>pattern .. Create >matrix + +>matrix .. PatternVariable o_array +>matrix .. PatternVariable o_size + +>matrix .. Constructor {args} { + var o_array o_size + + array set o_array [list] + set o_size 0 +} + + +#process_pattern_aliases ::patternlib::>matrix + +set PM [>matrix .. PatternMethod .] + +>matrix .. PatternMethod item {args} { + var o_array + + if {![llength $args]} { + error "indices required" + } else { + + } + if [info exists o_array($args)] { + return $o_array($args) + } else { + error "no such index: '$args'" + } +} +>matrix .. PatternMethod items {} { + var o_array + + set result [list] + foreach nm [array names o_array] { + lappend result $o_array($nm) + } + return $result +} +>matrix .. PatternMethod pairs {} { + var o_array + + array get o_array +} +>matrix .. PatternMethod slice {args} { + var o_array + + if {"*" ni $args} { + lappend args * + } + + array get o_array $args +} +>matrix .. PatternMethod add {val args} { + var o_array o_size + + if {![llength $args]} { + error "indices required" + } + + set o_array($args) $val + incr o_size + + #return [array size o_array] + return $o_size +} +>matrix .. PatternMethod names {} { + var o_array + array names o_array +} +>matrix .. PatternMethod keys {} { + #synonym for names + var o_array + array names o_array +} +>matrix .. PatternMethod hasKey {args} { + var o_array + + return [info exists o_array($args)] +} +>matrix .. PatternMethod clear {} { + var o_array o_size + unset o_array + set o_size 0 + return +} +>matrix .. PatternMethod count {} { + var o_size + return $o_size +} +>matrix .. PatternMethod count2 {} { + var o_array + #see comments for >hashMap count2 + return [array size o_array] +} +#>matrix .. Ready 1 + +#-------------------------------------------------------- +#tree data structure (based *loosely* on API at http://www.msen.com/%7Eclif/treeNobj.html - discussed in Clif Flynts book Tcl programming) +#!todo - compare API to http://tcllib.sourceforge.net/doc/tree.html +#!todo - create an >itree (inheritance tree) where node data is readable/writable on children unless overridden. +::>pattern .. Create >tree + +set _NODE [::>pattern .. Create [>tree .. Namespace]::>node] +set _TREE_NODE $_NODE +#process_pattern_aliases $_TREE_NODE + +$_NODE .. PatternVariable o_treens ;#tree namespace +$_NODE .. PatternVariable o_idref +$_NODE .. PatternVariable o_nodePrototype + +#$_NODE .. PatternProperty data +$_NODE .. PatternProperty info + +$_NODE .. PatternProperty tree +$_NODE .. PatternProperty parent +$_NODE .. PatternProperty children +$_NODE .. PatternMethod addNode {} { + set nd_id [incr $o_idref] + set nd [$o_nodePrototype .. Create ${o_treens}::>n-$nd_id -tree $o_tree -parent @this@] + @this@ . add $nd n-$nd_id + + return n-$nd_id +} +#flat list of all nodes below this +#!todo - something else? ad-hoc collections? +#!todo - non-recursive version? tail-call opt? +$_NODE .. PatternMethod nodes {} { + set result [list] + + #use(abuse?) our knowledge of >collection internals + foreach n $o_list { + #eval lappend result $n [$o_array($n) . nodes] + #!todo - test + lappend result $n {*}[$o_array($n) . nodes] + } + return $result +} +#count of number of descendants +#!todo - non-recursive version? tail-call opt? +$_NODE .. PatternMethod size {} { + set result 0 + #use(abuse?) our knowledge of >collection internals + foreach n $o_list { + incr result [expr {1 + [$o_array($n) . size]}] + } + return $result +} +$_NODE .. PatternMethod isLeaf {} { + #!todo - way to stop unused vars being uplevelled? + var o_tree + + #tailcall isEmpty $_ID_ ;#fails. because isEmpty is from >collection interface - so different ns? + tailcall [@this@ . isEmpty .] +} +$_NODE .. Constructor {args} { + array set A $args + + set o_tree $A(-tree) + set o_parent $A(-parent) + + #array set o_data [list] + array set o_info [list] + + set o_nodePrototype [::patternlib::>tree .. Namespace]::>node + set o_idref [$o_tree . nodeID .] + set o_treens [$o_tree .. Namespace] + #set o_children [::patternlib::>collection .. Create [@this@ .. Namespace]::>children] + + #overlay children collection directly on the node + set o_children [::patternlib::>collection .. Create @this@] + + return +} + +>tree .. PatternProperty test blah +>tree .. PatternProperty nodeID 0 ;#public only so node can access.. need 'friend' concept? +>tree .. PatternVariable o_ns +>tree .. Constructor {args} { + set o_ns [@this@ .. Namespace] + + #>tree is itself also a node (root node) + #overlay new 'root' node onto existing tree, pass tree to constructor + [::patternlib::>tree .. Namespace]::>node .. Create @this@ -tree @this@ -parent "" +} + + + + +unset _NODE + + + + +#-------------------------------------------------------- +#a basic binary search tree experiment +# - todo - 'scheme' property to change behaviour? e.g balanced tree +::>pattern .. Create >bst +#process_pattern_aliases ::patternlib::>bst +>bst .. PatternVariable o_NS ;#namespace +>bst .. PatternVariable o_this ;#namespace +>bst .. PatternVariable o_nodeID + +>bst .. PatternProperty root "" +>bst .. Constructor {args} { + set o_this @this@ + set o_NS [$o_this .. Namespace] + namespace eval ${o_NS}::nodes {} + puts stdout ">bst constructor" + set o_nodeID 0 +} +>bst .. PatternMethod insert {key args} { + set newnode [::patternlib::>bstnode .. Create ${o_NS}::nodes::>n-[incr o_nodeID]] + set [$newnode . key .] $key + if {[llength $args]} { + set [$newnode . value .] $args + } + if {![string length $o_root]} { + set o_root $newnode + set [$newnode . parent .] $o_this + } else { + set ipoint {} ;#insertion point + set tpoint $o_root ;#test point + set side {} + while {[string length $tpoint]} { + set ipoint $tpoint + if {[$newnode . key] < [$tpoint . key]} { + set tpoint [$tpoint . left] + set side left + } else { + set tpoint [$tpoint . right] + set side right + } + } + set [$newnode . parent .] $ipoint + set [$ipoint . $side .] $newnode + } + return $newnode +} +>bst .. PatternMethod item {key} { + if {![string length $o_root]} { + error "item $key not found" + } else { + set tpoint $o_root + while {[string length $tpoint]} { + if {[$tpoint . key] eq $key} { + return $tpoint + } else { + if {$key < [$tpoint . key]} { + set tpoint [$tpoint . left] + } else { + set tpoint [$tpoint . right] + } + } + } + error "item $key not found" + } +} +>bst .. PatternMethod inorder-walk {} { + if {[string length $o_root]} { + $o_root . inorder-walk + } + puts {} +} +>bst .. PatternMethod view {} { + array set result [list] + + if {[string length $o_root]} { + array set result [$o_root . view 0 [list]] + } + + foreach depth [lsort [array names result]] { + puts "$depth: $result($depth)" + } + +} +::>pattern .. Create >bstnode +#process_pattern_aliases ::patternlib::>bstnode +>bstnode .. PatternProperty parent +>bstnode .. PatternProperty left "" +>bstnode .. PatternProperty right "" +>bstnode .. PatternProperty key +>bstnode .. PatternProperty value + +>bstnode .. PatternMethod inorder-walk {} { + if {[string length $o_left]} { + $o_left . inorder-walk + } + + puts -nonewline "$o_key " + + if {[string length $o_right]} { + $o_right . inorder-walk + } + + return +} +>bstnode .. PatternMethod view {depth state} { + #!todo - show more useful representation of structure + set lower [incr depth] + + if {[string length $o_left]} { + set state [$o_left . view $lower $state] + } + + if {[string length $o_right]} { + set state [$o_right . view $lower $state] + } + + + array set s $state + lappend s($depth) $o_key + + return [array get s] +} + + +#-------------------------------------------------------- +#::pattern::create ::pattern::>metaObject +#::pattern::>metaObject PatternProperty methods +#::pattern::>metaObject PatternProperty properties +#::pattern::>metaObject PatternProperty PatternMethods +#::pattern::>metaObject PatternProperty patternProperties +#::pattern::>metaObject Constructor args { +# set this @this@ +# +# set [$this . methods .] [::>collection create [$this namespace]::methods] +# set [$this . properties .] [::>collection create [$this namespace]::properties] +# set [$this . PatternMethods .] [::>collection create [$this namespace]::PatternMethods] +# set [$this . patternProperties .] [::>collection create [$this namespace]::patternProperties] +# +#} + + + + #tidy up + unset PV + unset PM + + + +#-------------------------------------------------------- +::>pattern .. Create >enum +#process_pattern_aliases ::patternlib::>enum +>enum .. PatternMethod item {{idx 0}} { + var o_array o_list + + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + if {[catch {set o_array([lindex $o_list $idx])} result]} { + error "no such index : '$idx'" + } else { + return $result + } + } else { + if {[catch {set o_array($idx)} result]} { + error "no such index: '$idx'" + } else { + return $result + } + } +} + + + +#proc makeenum {type identifiers} { +# #!!todo - make generated procs import into whatever current system context? +# +# upvar #0 wbpbenum_${type}_number a1 wbpbenum_number_${type} a2 +# +# #obliterate any previous enum for this type +# catch {unset a1} +# catch {unset a2} +# +# set n 0 +# foreach id $identifiers { +# set a1($id) $n +# set a2($n) $id +# incr n +# } +# proc ::${type}_to_number key [string map [list @type@ $type] { +# upvar #0 wbpbenum_@type@_number ary +# if {[catch {set ary($key)} num]} { +# return -code error "unknown @type@ '$key'" +# } +# return $num +# }] +# +# proc ::number_to_${type} {number} [string map [list @type@ $type] { +# upvar #0 wbpbenum_number_@type@ ary +# if {[catch {set ary($number)} @type@]} { +# return -code error "no @type@ for '$number'" +# } +# return $@type@ +# }] +# +# #eval "namespace eval ::sysnexus {namespace export number_to_${type}; namespace export ${type}_to_number}" +# #eval "namespace eval :: {namespace import -force sysnexus::number_to_${type} sysnexus::${type}_to_number}" +#} +# +#-------------------------------------------------------- +::>pattern .. Create >nest +>nest .. PatternVariable THIS +>nest .. PatternProperty data -autoclone +>nest .. Constructor {args} { + var o_data + var THIS + set THIS @this@ + array set o_data [list] +} +>nest .. PatternMethod item {args} { + set THIS @this@ + return [$THIS . data [join $args ,]] +} + +# +# e.g +# set [>nest a , b . data c .] blah +# >nest a , b , c +# +# set [>nest w x , y . data z .] etc +# >nest w x , y , z +#-------------------------------------------------------- + +} + +} + + +#package require patternlibtemp diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm new file mode 100644 index 00000000..457d5742 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm @@ -0,0 +1,754 @@ +package provide patternpredator2 1.2.4 + +proc ::p::internals::jaws {OID _ID_ args} { + #puts stderr ">>>(patternpredator2 lib)jaws called with _ID_:$_ID_ args: $args" + #set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + yield + set w 1 + + set stack [list] + set wordcount [llength $args] + set terminals [list . .. , # @ !] ;#tokens which require the current stack to be evaluated first + set unsupported 0 + set operator "" + set operator_prev "" ;#used only by argprotect to revert to previous operator + + + if {$OID ne "null"} { + #!DO NOT use upvar here for MAP! (calling set on a MAP in another iteration/call will overwrite a map for another object!) + #upvar #0 ::p::${OID}::_meta::map MAP + set MAP [set ::p::${OID}::_meta::map] + } else { + # error "jaws - OID = 'null' ???" + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] ;#MAP taken from _ID_ will be missing 'interfaces' key + } + set invocantdata [dict get $MAP invocantdata] + lassign $invocantdata OID alias default_method object_command wrapped + + set finished_args 0 ;#whether we've completely processed all args in the while loop and therefor don't need to peform the final word processing code + + #don't use 'foreach word $args' - we sometimes need to backtrack a little by manipulating $w + while {$w < $wordcount} { + set word [lindex $args [expr {$w -1}]] + #puts stdout "w:$w word:$word stack:$stack" + + if {$operator eq "argprotect"} { + set operator $operator_prev + lappend stack $word + incr w + } else { + if {[llength $stack]} { + if {$word in $terminals} { + set reduction [list 0 $_ID_ {*}$stack ] + #puts stderr ">>>jaws yielding value: $reduction triggered by word $word in position:$w" + + + set _ID_ [yield $reduction] + set stack [list] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#Do not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces [list level0 {} level1 {}]] + #puts stderr "WARNING REVIEW: jaws-branch - leave empty??????" + } + + #review - 2018. switched to _ID_ instead of MAP + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command + #lassign [dict get $MAP invocantdata] OID alias default_method object_command + + + #puts stdout "---->>> yielded _ID_: $_ID_ OID:$OID alias:$alias default_method:$default_method object_command:$object_command" + set operator $word + #don't incr w + #incr w + } else { + if {$operator eq "argprotect"} { + set operator $operator_prev + set operator_prev "" + lappend stack $word + } else { + #only look for leading argprotect chacter (-) if we're not already in argprotect mode + if {$word eq "--"} { + set operator_prev $operator + set operator "argprotect" + #Don't add the plain argprotector to the stack + } elseif {[string match "-*" $word]} { + #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument) + set operator_prev $operator + set operator "argprotect" + lappend stack $word + } else { + lappend stack $word + } + } + + + incr w + } + } else { + #no stack + switch -- $word {.} { + + if {$OID ne "null"} { + #we know next word is a property or method of a pattern object + incr w + set nextword [lindex $args [expr {$w - 1}]] + set command ::p::${OID}::$nextword + set stack [list $command] ;#2018 j + set operator . + if {$w eq $wordcount} { + set finished_args 1 + } + } else { + # don't incr w + #set nextword [lindex $args [expr {$w - 1}]] + set command $object_command ;#taken from the MAP + set stack [list "_exec_" $command] + set operator . + } + + + } {..} { + incr w + set nextword [lindex $args [expr {$w -1}]] + set command ::p::-1::$nextword + #lappend stack $command ;#lappend a small number of items to an empty list is slower than just setting the list. + set stack [list $command] ;#faster, and intent is clearer than lappend. + set operator .. + if {$w eq $wordcount} { + set finished_args 1 + } + } {,} { + #puts stdout "Stackless comma!" + + + if {$OID ne "null"} { + set command ::p::${OID}::$default_method + } else { + set command [list $default_method $object_command] + #object_command in this instance presumably be a list and $default_method a list operation + #e.g "lindex {A B C}" + } + #lappend stack $command + set stack [list $command] + set operator , + } {--} { + set operator_prev $operator + set operator argprotect + #no stack - + } {!} { + set command $object_command + set stack [list "_exec_" $object_command] + #puts stdout "!!!! !!!! $stack" + set operator ! + } default { + if {$operator eq ""} { + if {$OID ne "null"} { + set command ::p::${OID}::$default_method + } else { + set command [list $default_method $object_command] + } + set stack [list $command] + set operator , + lappend stack $word + } else { + #no stack - so we don't expect to be in argprotect mode already. + if {[string match "-*" $word]} { + #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument) + set operator_prev $operator + set operator "argprotect" + lappend stack $word + } else { + lappend stack $word + } + + } + } + incr w + } + + } + } ;#end while + + #process final word outside of loop + #assert $w == $wordcount + #trailing operators or last argument + if {!$finished_args} { + set word [lindex $args [expr {$w -1}]] + if {$operator eq "argprotect"} { + set operator $operator_prev + set operator_prev "" + + lappend stack $word + incr w + } else { + + + switch -- $word {.} { + if {![llength $stack]} { + #set stack [list "_result_" [::p::internals::ref_to_object $_ID_]] + yieldto return [::p::internals::ref_to_object $_ID_] + error "assert: never gets here" + + } else { + #puts stdout "==== $stack" + #assert - whenever _ID_ changed in this proc - we have updated the $OID variable + yieldto return [::p::internals::ref_to_stack $OID $_ID_ $stack] + error "assert: never gets here" + } + set operator . + + } {..} { + #trailing .. after chained call e.g >x . item 0 .. + #puts stdout "$$$$$$$$$$$$ [list 0 $_ID_ {*}$stack] $$$$" + #set reduction [list 0 $_ID_ {*}$stack] + yieldto return [yield [list 0 $_ID_ {*}$stack]] + } {#} { + set unsupported 1 + } {,} { + set unsupported 1 + } {&} { + set unsupported 1 + } {@} { + set unsupported 1 + } {--} { + + #set reduction [list 0 $_ID_ {*}$stack[set stack [list]]] + #puts stdout " -> -> -> about to call yield $reduction <- <- <-" + set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]] ] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}} ] + } + yieldto return $MAP + } {!} { + #error "untested branch" + set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]]] + #set OID [::pattern::get_oid $_ID_] + set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + if {$OID ne "null"} { + set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here! + } else { + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] + } + lassign [dict get $MAP invocantdata] OID alias default_command object_command + set command $object_command + set stack [list "_exec_" $command] + set operator ! + } default { + if {$operator eq ""} { + #error "untested branch" + lassign [dict get $MAP invocantdata] OID alias default_command object_command + #set command ::p::${OID}::item + set command ::p::${OID}::$default_command + lappend stack $command + set operator , + + } + #do not look for argprotect items here (e.g -option) as the final word can't be an argprotector anyway. + lappend stack $word + } + if {$unsupported} { + set unsupported 0 + error "trailing '$word' not supported" + + } + + #if {$operator eq ","} { + # incr wordcount 2 + # set stack [linsert $stack end-1 . item] + #} + incr w + } + } + + + #final = 1 + #puts stderr ">>>jaws final return value: [list 1 $_ID_ {*}$stack]" + + return [list 1 $_ID_ {*}$stack] +} + + + +#trailing. directly after object +proc ::p::internals::ref_to_object {_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 object_command + set refname ::p::${OID}::_ref::__OBJECT + + array set $refname [list] ;#important to initialise the variable as an array here - or initial read attempts on elements will not fire traces + + set traceCmd [list ::p::predator::object_read_trace $OID $_ID_] + if {[list {read} $traceCmd] ni [trace info variable $refname]} { + #puts stdout "adding read trace on variable '$refname' - traceCmd:'$traceCmd'" + trace add variable $refname {read} $traceCmd + } + set traceCmd [list ::p::predator::object_array_trace $OID $_ID_] + if {[list {array} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {array} $traceCmd + } + + set traceCmd [list ::p::predator::object_write_trace $OID $_ID_] + if {[list {write} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {write} $traceCmd + } + + set traceCmd [list ::p::predator::object_unset_trace $OID $_ID_] + if {[list {unset} $traceCmd] ni [trace info variable $refname]} { + trace add variable $refname {unset} $traceCmd + } + return $refname +} + + +proc ::p::internals::create_or_update_reference {OID _ID_ refname command} { + #if {[lindex $fullstack 0] eq "_exec_"} { + # #strip it. This instruction isn't relevant for a reference. + # set commandstack [lrange $fullstack 1 end] + #} else { + # set commandstack $fullstack + #} + #set argstack [lassign $commandstack command] + #set field [string map {> __OBJECT_} [namespace tail $command]] + + + + set reftail [namespace tail $refname] + set argstack [lassign [split $reftail +] field] + set field [string map {> __OBJECT_} [namespace tail $command]] + + #puts stderr "refname:'$refname' command: $command field:$field" + + + if {$OID ne "null"} { + upvar #0 ::p::${OID}::_meta::map MAP + } else { + #set map [dict get [lindex [dict get $_ID_ i this] 0 1] map] + set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}}] + } + lassign [dict get $MAP invocantdata] OID alias default_method object_command + + + + if {$OID ne "null"} { + interp alias {} $refname {} $command $_ID_ {*}$argstack + } else { + interp alias {} $refname {} $command {*}$argstack + } + + + #set iflist [lindex $map 1 0] + set iflist [dict get $MAP interfaces level0] + #set iflist [dict get $MAP interfaces level0] + set field_is_property_like 0 + foreach IFID [lreverse $iflist] { + #tcl (braced) expr has lazy evaluation for &&, || & ?: operators - so this should be reasonably efficient. + if {[llength [info commands ::p::${IFID}::_iface::(GET)$field]] || [llength [info commands ::p::${IFID}::_iface::(SET)$field]]} { + set field_is_property_like 1 + #There is a setter or getter (but not necessarily an entry in the o_properties dict) + break + } + } + + + + + #whether field is a property or a method - remove any commandrefMisuse_TraceHandler + foreach tinfo [trace info variable $refname] { + #puts "-->removing traces on $refname: $tinfo" + if {[lindex $tinfo 1 0] eq "::p::internals::commandrefMisuse_TraceHandler"} { + trace remove variable $refname {*}$tinfo + } + } + + if {$field_is_property_like} { + #property reference + + + set this_invocantdata [lindex [dict get $_ID_ i this] 0] + lassign $this_invocantdata OID _alias _defaultmethod object_command + #get fully qualified varspace + + # + set propdict [$object_command .. GetPropertyInfo $field] + if {[dict exist $propdict $field]} { + set field_is_a_property 1 + set propinfo [dict get $propdict $field] + set varspace [dict get $propinfo varspace] + if {$varspace eq ""} { + set full_varspace ::p::${OID} + } else { + if {[::string match "::*" $varspace]} { + set full_varspace $varspace + } else { + set full_varspace ::p::${OID}::$varspace + } + } + } else { + set field_is_a_property 0 + #no propertyinfo - this field was probably established as a PropertyRead and/or PropertyWrite without a Property + #this is ok - and we still set the trace infrastructure below (app may convert it to a normal Property later) + set full_varspace ::p::${OID} + } + + + + + + #We only trace on entire property.. not array elements (if references existed to both the array and an element both traces would be fired -(entire array trace first)) + set Hndlr [::list ::p::predator::propvar_write_TraceHandler $OID $field] + if { [::list {write} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} { + trace add variable ${full_varspace}::o_${field} {write} $Hndlr + } + set Hndlr [::list ::p::predator::propvar_unset_TraceHandler $OID $field] + if { [::list {unset} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} { + trace add variable ${full_varspace}::o_${field} {unset} $Hndlr + } + + + #supply all data in easy-access form so that propref_trace_read is not doing any extra work. + set get_cmd ::p::${OID}::(GET)$field + set traceCmd [list ::p::predator::propref_trace_read $get_cmd $_ID_ $refname $field $argstack] + + if {[list {read} $traceCmd] ni [trace info variable $refname]} { + set fieldvarname ${full_varspace}::o_${field} + + + #synch the refvar with the real var if it exists + #catch {set $refname [$refname]} + if {[array exists $fieldvarname]} { + if {![llength $argstack]} { + #unindexed reference + array set $refname [array get $fieldvarname] + #upvar $fieldvarname $refname + } else { + set s0 [lindex $argstack 0] + #refs to nonexistant array members common? (catch vs 'info exists') + if {[info exists ${fieldvarname}($s0)]} { + set $refname [set ${fieldvarname}($s0)] + } + } + } else { + #refs to uninitialised props actually should be *very* common. + #If we use 'catch', it means retrieving refs to non-initialised props is slower. Fired catches can be relatively expensive. + #Because it's common to get a ref to uninitialised props (e.g for initial setting of their value) - we will use 'info exists' instead of catch. + + #set errorInfo_prev $::errorInfo ;#preserve errorInfo across catches! + + #puts stdout " ---->>!!! ref to uninitialised prop $field $argstack !!!<------" + + + if {![llength $argstack]} { + #catch {set $refname [set ::p::${OID}::o_$field]} + if {[info exists $fieldvarname]} { + set $refname [set $fieldvarname] + #upvar $fieldvarname $refname + } + } else { + if {[llength $argstack] == 1} { + #catch {set $refname [lindex [set ::p::${OID}::o_$field] [lindex $argstack 0]]} + if {[info exists $fieldvarname]} { + set $refname [lindex [set $fieldvarname] [lindex $argstack 0]] + } + + } else { + #catch {set $refname [lindex [set ::p::${OID}::o_$field] $argstack]} + if {[info exists $fieldvarname]} { + set $refname [lindex [set $fieldvarname] $argstack] + } + } + } + + #! what if someone has put a trace on ::errorInfo?? + #set ::errorInfo $errorInfo_prev + } + trace add variable $refname {read} $traceCmd + + set traceCmd [list ::p::predator::propref_trace_write $_ID_ $OID $full_varspace $refname] + trace add variable $refname {write} $traceCmd + + set traceCmd [list ::p::predator::propref_trace_unset $_ID_ $OID $refname] + trace add variable $refname {unset} $traceCmd + + + set traceCmd [list ::p::predator::propref_trace_array $_ID_ $OID $refname] + # puts "**************** installing array variable trace on ref:$refname - cmd:$traceCmd" + trace add variable $refname {array} $traceCmd + } + + } else { + #puts "$refname ====> adding refMisuse_traceHandler $alias $field" + #matching variable in order to detect attempted use as property and throw error + + #2018 + #Note that we are adding a trace on a variable (the refname) which does not exist. + #this is fine - except that the trace won't fire for attempt to write it as an array using syntax such as set $ref(someindex) + #we could set the ref to an empty array - but then we have to also undo this if a property with matching name is added + ##array set $refname {} ;#empty array + # - the empty array would mean a slightly better error message when misusing a command ref as an array + #but this seems like a code complication for little benefit + #review + + trace add variable $refname {read write unset array} [list ::p::internals::commandrefMisuse_TraceHandler $OID $field] + } +} + + + +#trailing. after command/property +proc ::p::internals::ref_to_stack {OID _ID_ fullstack} { + if {[lindex $fullstack 0] eq "_exec_"} { + #strip it. This instruction isn't relevant for a reference. + set commandstack [lrange $fullstack 1 end] + } else { + set commandstack $fullstack + } + set argstack [lassign $commandstack command] + set field [string map {> __OBJECT_} [namespace tail $command]] + + + #!todo? + # - make every object's OID unpredictable and sparse (UUID) and modify 'namespace child' etc to prevent iteration/inspection of ::p namespace. + # - this would only make sense for an environment where any meta methods taking a code body (e.g .. Method .. PatternMethod etc) are restricted. + + + #references created under ::p::${OID}::_ref are effectively inside a 'varspace' within the object itself. + # - this would in theory allow a set of interface functions on the object which have direct access to the reference variables. + + + set refname ::p::${OID}::_ref::[join [concat $field $argstack] +] + + if {[llength [info commands $refname]]} { + #todo - review - what if the field changed to/from a property/method? + #probably should fix that where such a change is made and leave this short circuit here to give reasonable performance for existing refs + return $refname + } + ::p::internals::create_or_update_reference $OID $_ID_ $refname $command + return $refname +} + + +namespace eval pp { + variable operators [list .. . -- - & @ # , !] + variable operators_notin_args "" + foreach op $operators { + append operators_notin_args "({$op} ni \$args) && " + } + set operators_notin_args [string trimright $operators_notin_args " &"] ;#trim trailing spaces and ampersands + #set operators_notin_args {({.} ni $args) && ({,} ni $args) && ({..} ni $args)} +} +interp alias {} strmap {} string map ;#stop code editor from mono-colouring our big string mapped code blocks! + + + + + +# 2017 ::p::predator2 is the development version - intended for eventual use as the main dispatch mechanism. +#each map is a 2 element list of lists. +# form: {$commandinfo $interfaceinfo} +# commandinfo is of the form: {ID Namespace defaultmethod commandname _?} + +#2018 +#each map is a dict. +#form: {invocantdata {ID Namespace defaultmethod commandname _?} interfaces {level0 {} level1 {}}} + + +#OID = Object ID (integer for now - could in future be a uuid) +proc ::p::predator2 {_ID_ args} { + #puts stderr "predator2: _ID_:'$_ID_' args:'$args'" + #set invocants [dict get $_ID_ i] + #set invocant_roles [dict keys $invocants] + + #For now - we are 'this'-centric (single dispatch). todo - adapt for multiple roles, multimethods etc. + #set this_role_members [dict get $invocants this] + #set this_invocant [lindex [dict get $_ID_ i this] 0] ;#for the role 'this' we assume only one invocant in the list. + #lassign $this_invocant this_OID this_info_dict + + set this_OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid + + + set cheat 1 ;# + #------- + #Optimise the next most common use case. A single . followed by args which contain no other operators (non-chained call) + #(it should be functionally equivalent to remove this shortcut block) + if {$cheat} { + if { ([lindex $args 0] eq {.}) && ([llength $args] > 1) && ([llength [lsearch -all -inline $args .]] == 1) && ({,} ni $args) && ({..} ni $args) && ({--} ni $args) && ({!} ni $args)} { + + set remaining_args [lassign $args dot method_or_prop] + + #how will we do multiple apis? (separate interface stacks) apply? apply [list [list _ID_ {*}$arglist] ::p::${stackid?}::$method_or_prop ::p::${this_OID}] ??? + set command ::p::${this_OID}::$method_or_prop + #REVIEW! + #e.g what if the method is named "say hello" ?? (hint - it will break because we will look for 'say') + #if {[llength $command] > 1} { + # error "methods with spaces not included in test suites - todo fix!" + #} + #Dont use {*}$command - (so we can support methods with spaces) + #if {![llength [info commands $command]]} {} + if {[namespace which $command] eq ""} { + if {[namespace which ::p::${this_OID}::(UNKNOWN)] ne ""} { + #lset command 0 ::p::${this_OID}::(UNKNOWN) ;#seems wrong - command could have spaces + set command ::p::${this_OID}::(UNKNOWN) + #tailcall {*}$command $_ID_ $cmdname {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + tailcall $command $_ID_ $method_or_prop {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "(::p::predator2) error running command:'$command' argstack:'[lrange $args 2 end]'\n - command not found and no 'unknown' handler" "method '$method_or_prop' not found" + } + } else { + #tailcall {*}$command $_ID_ {*}$remaining_args + tailcall $command $_ID_ {*}$remaining_args + } + } + } + #------------ + + + if {([llength $args] == 1) && ([lindex $args 0] eq "..")} { + return $_ID_ + } + + + #puts stderr "pattern::predator (test version) called with: _ID_:$_ID_ args:$args" + + + + #puts stderr "this_info_dict: $this_info_dict" + + + + + if {![llength $args]} { + #should return some sort of public info.. i.e probably not the ID which is an implementation detail + #return cmd + return [lindex [dict get [set ::p::${this_OID}::_meta::map] invocantdata] 0] ;#Object ID + + #return a dict keyed on object command name - (suitable as use for a .. Create 'target') + #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method object_command wrapped + #return [list $object_command [list -id $this_OID ]] + } elseif {[llength $args] == 1} { + #short-circuit the single index case for speed. + if {[lindex $args 0] ni {.. . -- - & @ # , !}} { + #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method + lassign [lindex [dict get $_ID_ i this] 0] this_OID alias default_method + + tailcall ::p::${this_OID}::$default_method $_ID_ [lindex $args 0] + } elseif {[lindex $args 0] eq {--}} { + + #!todo - we could hide the invocant by only allowing this call from certain uplevel procs.. + # - combined with using UUIDs for $OID, and a secured/removed metaface on the object + # - (and also hiding of [interp aliases] command so they can't iterate and examine all aliases) + # - this could effectively hide the object's namespaces,vars etc from the caller (?) + return [set ::p::${this_OID}::_meta::map] + } + } + + + + #upvar ::p::coroutine_instance c ;#coroutine names must be unique per call to predator (not just per object - or we could get a clash during some cyclic calls) + #incr c + #set reduce ::p::reducer${this_OID}_$c + set reduce ::p::reducer${this_OID}_[incr ::p::coroutine_instance] + #puts stderr "..................creating reducer $reduce with args $this_OID _ID_ $args" + coroutine $reduce ::p::internals::jaws $this_OID $_ID_ {*}$args + + + set current_ID_ $_ID_ + + set final 0 + set result "" + while {$final == 0} { + #the argument given here to $reduce will be returned by 'yield' within the coroutine context (jaws) + set reduction_args [lassign [$reduce $current_ID_[set current_ID_ [list]] ] final current_ID_ command] + #puts stderr "..> final:$final current_ID_:'$current_ID_' command:'$command' reduction_args:'$reduction_args'" + #if {[string match *Destroy $command]} { + # puts stdout " calling Destroy reduction_args:'$reduction_args'" + #} + if {$final == 1} { + + if {[llength $command] == 1} { + if {$command eq "_exec_"} { + tailcall {*}$reduction_args + } + if {[llength [info commands $command]]} { + tailcall {*}$command $current_ID_ {*}$reduction_args + } + set cmdname [namespace tail $command] + set this_OID [lindex [dict get $current_ID_ i this] 0 0] + if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} { + lset command 0 ::p::${this_OID}::(UNKNOWN) + tailcall {*}$command $current_ID_ $cmdname {*}$reduction_args ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "1)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found" + } + + } else { + #e.g lindex {a b c} + tailcall {*}$command {*}$reduction_args + } + + + } else { + if {[lindex $command 0] eq "_exec_"} { + set result [uplevel 1 [list {*}[lrange $command 1 end] {*}$reduction_args]] + + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {} ] + } else { + if {[llength $command] == 1} { + if {![llength [info commands $command]]} { + set cmdname [namespace tail $command] + set this_OID [lindex [dict get $current_ID_ i this] 0 0] + if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} { + + lset command 0 ::p::${this_OID}::(UNKNOWN) + set result [uplevel 1 [list {*}$command $current_ID_ $cmdname {*}$reduction_args]] ;#delegate to UNKNOWN, along with original commandname as 1st arg. + } else { + return -code error -errorinfo "2)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found" + } + } else { + #set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]] + set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]] + + } + } else { + set result [uplevel 1 [list {*}$command {*}$reduction_args]] + } + + if {[llength [info commands $result]]} { + if {([llength $result] == 1) && ([string first ">" [namespace tail $result]] == 0)} { + #looks like a pattern command + set current_ID_ [$result .. INVOCANTDATA] + + + #todo - determine if plain .. INVOCANTDATA is sufficient instead of .. UPDATEDINVOCANTDATA + #if {![catch {$result .. INVOCANTDATA} result_invocantdata]} { + # set current_ID_ $result_invocantdata + #} else { + # return -code error -errorinfo "3)error running command:'$command' argstack:'$reduction_args'\n - Failed to access result:'$result' as a pattern object." "Failed to access result:'$result' as a pattern object" + #} + } else { + #non-pattern command + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}] + } + } else { + set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}] + #!todo - allow further operations on non-command values. e.g dicts, lists & strings (treat strings as lists) + + } + } + + } + } + error "Assert: Shouldn't get here (end of ::p::predator2)" + #return $result +} diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm new file mode 100644 index 00000000..2d6e61da --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm @@ -0,0 +1,7806 @@ +#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 +} + + + +#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 withe punk repl - todo - tone it down. +#namespace eval ::punk::repl::codethread { +# variable running 0 +#} +package require punk::lib +package require punk::ansi +#require aliascore after punk::lib & punk::ansi are loaded +package require punk::aliascore ;#mostly punk::lib aliases +punk::aliascore::init + +package require punk::repl::codethread +package require punk::config +#package require textblock +if {![llength [info commands ::ansistring]]} { + namespace import punk::ansi::ansistring +} +package require punk::console +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 +if {[catch { + package require punk::packagepreference +} errM]} { + puts stderr "Failed to load punk::packagepreference" +} +punk::packagepreference::install + +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]} { + 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]} { + 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 objclone {obj} { + append obj2 $obj {} + } + interp alias "" strlen "" ::punk::strlen + interp alias "" str_len "" ::punk::strlen + interp alias "" objclone "" ::punk::objclone + #proc ::strlen {str} { + # string length [append str2 $str {}] + #} + #proc ::objclone {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} + + 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 + } + + #review - there are various type of uuid - we should use something consistent across platforms + #twapi is used on windows because it's about 5 times faster - but is this more important than consistency? + #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway + #(counterpoint: in the case of punk - we currently need twapi anyway on windows) + #does tcllib's uuid use the same mechanisms on different platforms anyway? + proc ::punk::uuid {} { + set has_twapi 0 + if 0 { + if {"windows" eq $::tcl_platform(platform)} { + if {![catch { + set loader [zzzload::pkg_wait twapi] + } errM]} { + if {$loader in [list failed loading]} { + puts stderr "Unexpected problem during thread-load of pkg twapi - zzload::pkg_wait returned $loader" + } + } else { + package require twapi + } + if {[package provide twapi] ne ""} { + set has_twapi 1 + } + } + } + if {!$has_twapi} { + if {[catch {package require uuid} errM]} { + error "Unable to load a package for uuid on this platform. Try installing tcllib's uuid (any platform) - or twapi for windows" + } + return [uuid::uuid generate] + } else { + return [twapi::new_uuid] + } + } + + #get last command result that was run through the repl + proc ::punk::get_runchunk {args} { + set argd [punk::args::get_dict { + *opts + -1 -optional 1 -type none + -2 -optional 1 -type none + *values -min 0 -max 0 + } $args] + #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 + } + #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + # + #we can't provide a float comparison suitable for every situation, + #but we pick something reasonable, keep it stable, and document it. + proc float_almost_equal {a b} { + package require math::constants + set diff [expr {abs($a - $b)}] + if {$diff <= $math::constants::eps} { + return 1 + } + set A [expr {abs($a)}] + set B [expr {abs($b)}] + set largest [expr {($B > $A) ? $B : $A}] + return [expr {$diff <= $largest * $math::constants::eps}] + } + + #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. + proc boolean_equal {a b} { + #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. + expr {($a && 1) == ($b && 1)} + } + #debatable whether boolean_almost_equal is likely to be surprising or helpful. + #values from a calculation that are extremely close to zero but aren't false could also be surprising - especially if they compare equal numerically + #perhaps a fuzzy-boolean is a step too far for a default - but it's inline with float-comparison for pattern-matching. use an even more complex classifier? (^&~) ? + proc boolean_almost_equal {a b} { + if {[string is double -strict $a]} { + if {[float_almost_equal $a 0]} { + set a 0 + } + } + if {[string is double -strict $b]} { + if {[float_almost_equal $b 0]} { + set b 0 + } + } + #must handle true,no etc. + expr {($a && 1) == ($b && 1)} + } + + + 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} " "<"] ;# (> required for insertionspecs at rhs of = & .= ) + #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname + + #except when prefixed directly by pin classifier ^ + set protect_terminals [list "^"] ;# e.g sequence ^# + #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string + #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' + set in_brackets 0 ;#count depth + set in_atom 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + set indq 0 + set inesc 0 ;#whether last char was backslash (see also punk::escv) + set prevc "" + set char_index 0 + foreach c [split $varspecs ""] { + if {$indq} { + if {$inesc} { + #puts stderr "inesc adding '$c'" + append token $c + } else { + if {$c eq {"}} { + set indq 0 + } else { + append token $c + } + } + } elseif {$in_atom} { + #ignore dquotes/brackets in atoms - pass through + append token $c + #set nextc [lindex $chars $char_index+1] + if {$c eq "'"} { + set in_atom 0 + } + } elseif {$in_brackets > 0} { + append token $c + if {$c eq ")"} { + incr in_brackets -1 + } + } else { + if {$c eq {"} && !$inesc} { + set indq 1 + } elseif {$c eq ","} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. + #lassign [scan $token %${first_term}s%s] var spec + set var [string range $token 0 $first_term-1] + set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list [string trim $var] [string trim $spec]] + set token "" + set token_index -1 ;#reduce by 1 because , not included in next token + set first_term -1 + } else { + append token $c + switch -exact -- $c { + ' { + set in_atom 1 + } + ( { + incr in_brackets + } + default { + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } + } + } + } + } + set prevc $c + if {$c eq "\\"} { + #review + if {$inesc} { + set inesc 0 + } else { + set token [string range $token 0 end-1] + set inesc 1 + } + } else { + set inesc 0 + } + incr token_index + incr char_index + } + if {[string length $token]} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + #lassign [scan $token %${first_term}s%s] var spec + set var [string range $token 0 $first_term-1] + set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list [string trim $var] [string trim $spec]] + } + return $varlist + } + proc _split_var_key_at_unbracketed_comma {varspecs} { + set varlist [list] + set var_terminals [list "@" "/" "#" "!"] + #except when prefixed directly by pin classifier ^ + set protect_terminals [list "^"] ;# e.g sequence ^# + #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string + #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' + set in_brackets 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + set prevc "" + foreach c [split $varspecs ""] { + if {$in_brackets} { + append token $c + if {$c eq ")"} { + set in_brackets 0 + } + } else { + if {$c eq ","} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + lassign [scan $token %${first_term}s%s] var spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list $var $spec] + set token "" + set token_index -1 ;#reduce by 1 because , not included in next token + set first_term -1 + } else { + append token $c + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } elseif {$c eq "("} { + set in_brackets 1 + } + } + } + set prevc $c + incr token_index + } + if {[string length $token]} { + #lappend varlist [splitstrposn $token $first_term] + set var $token + set spec "" + if {$first_term > 0} { + lassign [scan $token %${first_term}s%s] var spec + } else { + if {$first_term == 0} { + set var "" + set spec $token + } + } + lappend varlist [list $var $spec] + } + return $varlist + } + proc _split_var_key_at_unbracketed_comma1 {varspecs} { + set varlist [list] + set var_terminals [list "@" "/" "#" "!"] + set in_brackets 0 + #set varspecs [string trimleft $varspecs ,] + set token "" + #if {[string first "," $varspecs] <0} { + # return $varspecs + #} + set first_term -1 + set token_index 0 ;#index of terminal char within each token + foreach c [split $varspecs ""] { + if {$in_brackets} { + if {$c eq ")"} { + set in_brackets 0 + } + append token $c + } else { + if {$c eq ","} { + 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 ""] + } + 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 + } + } + #? { + 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! + 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 + 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 (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 { + 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 + } + %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 { + 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 {$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 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 globvalue-get-keys-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 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 globvalue-get-pairs-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 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 globvalue-get-values-not + append script \n [string map [list $keyglob] { + # 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 $keyglob] { + # 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 + } + } + }] + } + + error "globkeyvalue-get-pairs todo" + } + @* { + 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 + }] + } + %* { + 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 {[string first "end" $index] >=0} { + 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]} { + 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} { + #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"} { + if {[string match *-* $index]} { + lappend INDEX_OPERATIONS string-range + 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 "*"} { + 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 + } + + #todo - recurse into bracketed sub parts + #JMN3 + #e.g @*/(x@0,y@2) + proc _var_classify {multivar} { + set cmdname ::punk::pipecmds::var_classify::_[pipecmd_namemapping $multivar] + if {[info commands $cmdname] ne ""} { + return [$cmdname] + } + + + #comma seems a natural choice to split varspecs, + #but also for list and dict subelement access + #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) + #so / will indicate subelements e.g @0/1 for lindex $list 0 1 + #set valsource_key_list [_split_var_key_at_unbracketed_comma $multivar] + set valsource_key_list [_split_patterns_memoized $multivar] + + + + #mutually exclusive - atom/pin + #set map [list "" ' ^ &] ;#0 = default/var/not-yet-determined 1 = atom 2 = pin + #set var_class [lmap var $valsource_key_list {expr {([set m [lsearch $map [string index [lindex $var 0] 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] + #0 - novar + #1 - atom ' + #2 - pin ^ + #3 - boolean & + #4 - integer + #5 - double + #6 - var + #7 - glob (no classifier and contains * or ?) + #8 - numeric + #9 - > (+) + #10 - < (-) + + set var_names [list] + set var_class [list] + set varspecs_trimmed [list] ;#raw varspecs without pin/atom modifiers - or empty string for glob + + + set leading_classifiers [list "'" "&" "^" ] + set trailing_classifiers [list + -] + set possible_number_start [list - + . 0 1 2 3 4 5 6 7 8 9 > <] + + foreach v_key $valsource_key_list { + lassign $v_key v key + set vname $v ;#default + set classes [list] + if {$v eq ""} { + lappend var_class [list $v_key 0] + lappend varspecs_trimmed $v_key + } else { + set lastchar [string index $v end] + switch -- $lastchar { + + { + lappend classes 9 + set vname [string range $v 0 end-1] + } + - { + lappend classes 10 + set vname [string range $v 0 end-1] + } + } + set firstchar [string index $v 0] + switch -- $firstchar { + ' { + lappend var_class [list $v_key 1] + #set vname [string range $v 1 end] + lappend varspecs_trimmed [list $vname $key] + } + ^ { + lappend classes [list 2] + #use vname - may already have trailing +/- stripped + set vname [string range $vname 1 end] + set secondclassifier [string index $v 1] + switch -- $secondclassifier { + "&" { + #pinned boolean + lappend classes 3 + set vname [string range $v 2 end] + } + "#" { + #pinned numeric comparison instead of string comparison + #e.g set x 2 + # this should match: ^#x.= list 2.0 + lappend classes 8 + set vname [string range $vname 1 end] + } + "*" { + #pinned glob + lappend classes 7 + set vname [string range $v 2 end] + } + } + #todo - check for second tag - & for pinned boolean? + #consider requiring ^# for numeric comparisons. currently no way to do a strictly string comparison on pinned variables.... default ^var really shouldn't be doing any magic. + #while we're at it.. pinned glob would be nice. ^* + #maybe even pinned scan ^% ? regex? ^/ or ^? these would be hard to have corresponding literals in the pattern mini-lang. + #These all limit the range of varnames permissible - which is no big deal. + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed [list $vname $key] + } + & { + #we require boolean literals to be single-quoted so we can use cross-binding on boolean vars. + #ie &true is the variable true whereas &'true' or &'1' &'t' etc are literal booleans + #allow exception of &1 &0 to be literal booleans - because we disallow 0 & 1 as varnames in other contexts anyway - so it would be more consistent not to treat as varnames here. + lappend var_class [list $v_key 3] + set vname [string range $v 1 end] + lappend varspecs_trimmed [list $vname $key] + } + default { + if {([string first ? $v]) >=0 || ([string first * $v] >=0)} { + lappend var_class [list $v_key 7] ;#glob + #leave vname as the full glob + lappend varspecs_trimmed [list "" $key] + } else { + #scan vname not v - will either be same as v - or possibly stripped of trailing +/- + set numtestv [join [scan $vname %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, decimal points and sci notation - fails to handle leading dot e.g .5 + #leading . still need to test directly for double + if {[string is double -strict $vname] || [string is double -strict $numtestv]} { + if {[string is integer -strict $numtestv]} { + #this will pick up boolean 1 or 0 - but that's ok - they require "&" marker if boolean comparison desired + #integer test before double.. + #note there is also string is wide (string is wideinteger) for larger ints.. + lappend classes 4 + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed $v_key + } else { + #double + #sci notation 1e123 etc + #also large numbers like 1000000000 - even without decimal point - (tcl bignum) + lappend classes 5 + lappend var_class [list $v_key $classes] + lappend varspecs_trimmed $v_key + } + } else { + lappend var_class [list $v_key 6] ;#var + lappend varspecs_trimmed $v_key + } + } + } + } + } + lappend var_names $vname + } + + set result [list var_names $var_names var_class $var_class varspecs_trimmed $varspecs_trimmed] + + proc $cmdname {} [list return $result] + debug.punk.pipe.compile {proc $cmdname} + return $result + } + + + + #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 [_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::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::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::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::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 + #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 var_display_names [list] + foreach v $var_names { + if {$v eq ""} { + lappend var_display_names {{}} + } else { + lappend var_display_names $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::objclone $args] + set nscaller [uplevel 1 [list 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::objclone $args] + set nscaller [uplevel 1 [list namespace current]] + tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller] + } + + #map rhs to names suitable to use in pipemcd proc name (whitespace mapping) + # (for .= and = pipecmds) + proc pipecmd_namemapping {rhs} { + #used to build a command from a pattern which could contain :: - we need to map it to keep it a single command in the desired namespace. + #glob chars will prevent us using simple test {[info commands $cmd] eq ""} to test existence + #we could use the test {$cmd in [info commands]} - but mapping away globchars is more robust, allowing the simpler test + set rhs [string trim $rhs];#ignore all leading & trailing whitespace + set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token + set rhs [tcl::string::map {: ? * } $rhs] + #review - we don't expect other command-incompatible chars such as colon? + return $rhs + } + + #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 [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 ::namespace path]] + if {$cmdns ni $existing_path} { + uplevel 1 [list ::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 [_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 ::namespace path]] + if {$cmdns ni $existing_path} { + uplevel 1 [list ::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? + } + 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 ] + 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 + } + + + + + #todo - consider whether we can use < for insertion/iteration combinations + # =a<,b< iterate once through + # =a><,b>< cartesian product + # =a<>,b<> ??? zip ? + # + # ie = {a b c} |> .=< inspect + # would call inspect 3 times, once for each argument + # .= list {a b c} {x y z} |a/0,b/1> .=a><,b>< list + # would produce list of cartesian pairs? + # + proc _split_equalsrhs {insertionpattern} { + #map the insertionpattern so we can use faster globless info command search + set name_mapped [pipecmd_namemapping $insertionpattern] + set cmdname ::punk::pipecmds::split_rhs::_$name_mapped + if {[info commands $cmdname] ne ""} { + return [$cmdname] + } + + set lst_var_indexposition [punk::_split_patterns_memoized $insertionpattern] + set i 0 + set return_triples [list] + foreach v_pos $lst_var_indexposition { + lassign $v_pos v index_and_position + #e.g varname@@data/ok>0 varname/1/0>end + #ensure only one ">" is detected + if {![string length $index_and_position]} { + set indexspec "" + set positionspec "" + } else { + set chars [split $index_and_position ""] + set posns [lsearch -all $chars ">"] + if {[llength $posns] > 1} { + error "pipesyntax error in segment insertion pattern '$insertionpattern' -v '$v' multiple '>' characters. Pattern not understood." "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] + } + if {![llength $posns]} { + set indexspec $index_and_position + set positionspec "" + } else { + set splitposn [lindex $posns 0] + set indexspec [string range $index_and_position 0 $splitposn-1] + set positionspec [string range $index_and_position $splitposn+1 end] + } + } + + #review - + if {($positionspec in [list "*" "/*" "@*" "/" "@"]) || ($v eq "*" && $positionspec eq "")} { + set star "" + if {$v eq "*"} { + set v "" + set star "*" + } + if {[string index $positionspec end] eq "*"} { + set star "*" + } + #it is always possible to insert at end of list regardless of current length - so /end* and @end* are equivalent + #as are /end and @end + #lset lst_var_indexposition $i [list $v "/end$star"] + set triple [list $v $indexspec "/end$star"] + } else { + if {$positionspec eq ""} { + #e.g just =varname + #lset lst_var_indexposition $i [list $v "/end"] + set triple [list $v $indexspec "/end"] + #error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' missing position spec e.g /0" + } else { + if {[string index $indexspec 0] ni [list "" "/" "@"]} { + error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' bad index spec '$indexspec'" "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] + } + set triple [list $v $indexspec $positionspec] + } + } + lappend return_triples $triple + incr i + } + proc $cmdname {} [list return $return_triples] + return $return_triples + } + + 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 + } + + # + # + # relatively slow on even small sized scripts + proc arg_is_script_shaped2 {arg} { + set re {^(\s|;|\n)$} + set chars [split $arg ""] + if {[lsearch -regex $chars $re] >=0} { + return 1 + } else { + return 0 + } + } + + #exclude quoted whitespace + proc arg_is_script_shaped {arg} { + if {[tcl::string::first \n $arg] >= 0} { + return 1 + } elseif {[tcl::string::first ";" $arg] >= 0} { + return 1 + } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { + lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found + return [expr {$part2 ne ""}] + } else { + return 0 + } + } + proc _rhs_tail_split {fullrhs} { + set inq 0; set indq 0 + set equalsrhs "" + set i 0 + foreach ch [split $fullrhs ""] { + if {$inq} { + append equalsrhs $ch + if {$ch eq {'}} { + set inq 0 + } + } elseif {$indq} { + append equalsrhs $ch + if {$ch eq {"}} { + set indq 0 + } + } else { + switch -- $ch { + {'} { + set inq 1 + } + {"} { + set indq 1 + } + " " { + #whitespace outside of quoting + break + } + 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z {} + default { + #\t not a literal for switch jumpTable bytecompile - review - can we do it without for example string mapping to (and without a literal binary tab in source file)? + #we can't (reliably?) put \t as one of our switch keys + # + if {$ch eq "\t"} { + break + } + } + } + append equalsrhs $ch + } + incr i + } + set tail [tcl::string::range $fullrhs $i end] + return [list $equalsrhs $tail] + } + + # -- + #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 + + switch -- $next1 { + pipematch { + 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 first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { + #*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 {[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 [_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 [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 <| + #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 $getv]{ + # + #}] + #maintenance - index logic should be similar identical? to to match_assign - which only needs to process atoms because it (for now?) delegates all pipeline ops here, so no vars available (single segment assign) + #tag: positionspechandler + + + #puts stdout "=== list_insertion_script '$positionspec' segmenttail " + set script2 [punk::list_insertion_script $positionspec segmenttail ] + set script2 [string map [list "\$insertion_data" ] $script2] + append insertion_script \n $script2 + + } + append insertion_script \n {set segmenttail} + append insertion_script \n "}" + #puts stderr "$insertion_script" + debug.punk.pipe.compile {creating proc ::punk::pipecmds::insertion::_$rhsmapped } 4 + eval $insertion_script + } + + set segment_members_filled [::punk::pipecmds::insertion::_$rhsmapped $dict_tagval [lindex [list $segmenttail [unset segmenttail]] 0] ] + + #set segment_members_filled $segmenttail + #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) + + } + set rhs [string map $dict_tagval $rhs] ;#obsolete? + + debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 + + + # script index could have changed!!! todo fix! + + #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) + if {(!$segment_first_is_script ) && $segment_op eq ".="} { + #no scriptiness detected + + #debug.punk.pipe.rep {[a yellow bold][rep_listname segment_members_filled][a]} 4 + + set cmdlist_result [uplevel 1 $segment_members_filled] + #debug.punk.pipe {[a green bold]forward_result: $forward_result[a]} 4 + #debug.punk.pipe.rep {[a yellow bold]forward_result REP: [rep $forward_result][a]} 4 + + #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] + + set segment_result [_handle_bind_result $d] + #puts stderr ">>forward_result: $forward_result segment_result $segment_result" + + + } elseif {$segment_op eq "="} { + #slightly different semantics for assigment! + #We index into the DATA - not the position within the segment! + #(an = segment must take a single argument, as opposed to a .= segment) + #(This was a deliberate design choice for consistency with set, and to reduce errors.) + #(we could have allowed multiple args to = e.g to form a list, but it was tried, and the edge-cases were unintuitive and prone to user error) + #(The choice to restrict to single argument, but allow insertion and appending via insertion-specs is more explicit and reliable even though the insertion-specs operate differently to those of .=) + # + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = + # must return: {a b c} not a b c + # + if {!$segment_has_insertions} { + set segment_members_filled $segment_members + if {[dict exists $dict_tagval data]} { + if {![llength $segment_members_filled]} { + set segment_members_filled [dict get $dict_tagval data] + } else { + lappend segment_members_filled [dict get $dict_tagval data] + } + } + } + + set d [_multi_bind_result $returnvarspec [lindex [list $segment_members_filled [unset segment_members_filled ]] 0]] + set segment_result [_handle_bind_result $d] + + + } elseif {$segment_first_is_script || $segment_op eq "script"} { + #script + debug.punk.pipe {[a+ cyan bold].. evaluating as script[a]} 2 + + set script [lindex $segment_members 0] + + #build argument lists for 'apply' + set segmentargnames [list] + set segmentargvals [list] + foreach {k val} $dict_tagval { + if {$k eq "args"} { + #skip args - it is manually added at the end of the apply list if it's a valid tcl list + continue + } + lappend segmentargnames $k + lappend segmentargvals $val + } + + set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list + #puts "------> rep prevr argsdatalist: [rep $argsdatalist]" + set add_argsdata 0 + if {[dict exists $dict_tagval "args"]} { + set argsdatalist [dict get $dict_tagval "args"] + #see if the raw result can be treated as a list + if {[catch {lindex $argsdatalist 0}]} { + #we cannot supply 'args' + set pre_script "" + #todo - only add trace if verbose warnings enabled? + append pre_script "trace add variable args read punk::pipeline_args_read_trace_error\n" + set script $pre_script + append script $segment_first_word + set add_argsdata 0 + } else { + set add_argsdata 1 + } + } + + debug.punk.pipe.rep {>> [rep_listname segmentargvals]} 4 + set ns [uplevel 1 {::namespace current}] + if {!$add_argsdata} { + debug.punk.pipe {APPLY1: (args not set; not a list) segment vars:$segmentargnames} 4 + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals" + set evaluation [uplevel 1 [list ::apply [::list $segmentargnames $script $ns] {*}$segmentargvals]] + } else { + debug.punk.pipe {APPLY2: (args is set)segment vars:$segmentargnames} 4 + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals $argsdatalist" + #pipeline script context should be one below calling context - so upvar v v will work + #ns with leading colon will fail with apply + set evaluation [uplevel 1 [list ::apply [::list [::concat $segmentargnames args] $script $ns] {*}$segmentargvals {*}$argsdatalist]] + } + + debug.punk.pipe.rep {script result, evaluation: [rep_listname evaluation]} 4 + #puts "---> rep script evaluation result: [rep $evaluation]" + #set d [_multi_bind_result $returnvarspec [punk::K $evaluation [unset evaluation]]] + + #trailing segment_members are *pipedata* scripts - as opposed to ordinary pipeline scripts! + set tail_scripts [lrange $segment_members 1 end] + if {[llength $tail_scripts]} { + set r [pipedata $evaluation {*}$tail_scripts] + } else { + set r $evaluation + } + set d [_multi_bind_result $returnvarspec [lindex [list $r [unset r]] 0]] + set segment_result [_handle_bind_result $d] + } else { + #tags ? + #debug.punk.pipe {>>raw commandline: [concat $rhs $segment_members_filled]} 5 + if 0 { + + + + #set s [list uplevel 1 [concat $rhs $segment_members_filled]] + if {![info exists pscript]} { + upvar ::_pipescript pscript + } + if {![info exists pscript]} { + #set pscript $s + set pscript [funcl::o_of_n 1 $segment_members] + } else { + #set pscript [string map [list

$pscript] {uplevel 1 [concat $rhs $segment_members_filled [

]]}] + #set snew "set pipe_$i \[uplevel 1 \[list $rhs $segment_members_filled " + #append snew "set pipe_[expr $i -1]" + #append pscript $snew + set pscript [funcl::o_of_n 1 $segment_members $pscript] + + } + } + set cmdlist_result [uplevel 1 $segment_members_filled] + #set d [_multi_bind_result $returnvarspec [punk::K $segment_members_filled [unset segment_members_filled]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result]] 0 ]] + + #multi_bind_result needs to return a funcl for rhs of: + #lindex [list [set syncvar [main pipeline.. ]] [rhs binding funcl...] 1 ] + #which uses syncvar + # + #The lhs of 'list' runs first so now syncvar can be the root level of the rhs function list and bind the necessary vars. + #NOTE: unintuitively, we are returning the value of rhs to the main pipleline! (leftmost binding) this is because the leftmost binding determines what goes back to the pipeline result + + set segment_result [_handle_bind_result $d] + } + #the subresult doesn't need to go backwards - as the final assignment can emit the result into a variable + #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section + #It may however make a good debug point + #puts stderr "segment $i segment_result:$segment_result" + + debug.punk.pipe.rep {[rep_listname segment_result]} 3 + + + + + + #examine tailremaining. + # either x x x |?> y y y ... + # or just y y y + #we want the x side for next loop + + #set up the conditions for the next loop + #|> x=y args + # inpipespec - contents of previous piper |xxx> + # outpipespec - empty or content of subsequent piper |xxx> + # previous_result + # assignment (x=y) + + + set pipespec($j,in) $pipespec($i,out) + set outpipespec "" + set tailmap "" + set next_pipe_posn -1 + if {[llength $tailremaining]} { + + #set tailmap [lmap v $tailremaining {lreplace [split $v {}] 1 end-1}] + ##e.g for: a b c |> e f g |> h + #set next_pipe_posn [lsearch $tailmap {| >}] + set next_pipe_posn [lsearch $tailremaining "|*>"] + + set outpipespec [string range [lindex $tailremaining $next_pipe_posn] 1 end-1] + } + set pipespec($j,out) $outpipespec + + + set script_like_first_word 0 + if {[llength $tailremaining] || $next_pipe_posn >= 0} { + + if {$next_pipe_posn >=0} { + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for + set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] + + } else { + set next_all_members $tailremaining + set tailremaining [list] + } + + + #assignment is the arg immediately following |> operator e.g x.=blah or x=etc (or a normal commandlist or script!) + set segment_first_word "" + set returnvarspec "" ;# the lhs of x=y + set segment_op "" + set rhs "" + set segment_first_is_script 0 + if {[llength $next_all_members]} { + if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + set segment_first_word [lindex $next_all_members 0] + set segment_first_is_script 1 + set segment_op "" + set segment_members $next_all_members + } else { + set possible_assignment [lindex $next_all_members 0] + #set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} + if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { + set segment_op ".=" + set segment_first_word [lindex $next_all_members 1] + set script_like_first_word [arg_is_script_shaped $segment_first_word] + if {$script_like_first_word} { + set segment_first_is_script 1 ;#relative to segment_members which no longer includes the .= + } + set segment_members [lrange $next_all_members 1 end] + } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { + set segment_op "=" + #never scripts + #must be at most a single element after the = ! + if {[llength $next_all_members] > 2} { + #raise this as pipesyntax as opposed to pipedata? + error "pipesyntax - at most one element can follow = (got [lrange $next_all_members 1 end])" "pipeline $segment_op $returnvarspec $rhs [lrange $next_all_members 1 end]" [list pipesyntax too_many_elements] + } + set segment_first_word [lindex $next_all_members 1] + if {[catch {llength $segment_first_word}]} { + set segment_is_list 0 ;#only used for segment_op = + } else { + set segment_is_list 1 ;#only used for segment_op = + } + + set segment_members $segment_first_word + } else { + #no assignment operator and not script shaped + set segment_op "" + set returnvarspec "" + set segment_first_word [lindex $next_all_members 0] + set segment_first_word [lindex $next_all_members 1] + set segment_members $next_all_members + #puts stderr ">>3 no-operator segment_first_word: '$segment_first_word'" + } + } + + + } else { + #?? two pipes in a row ? + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 + set segment_members return + set segment_first_word return + } + + #set forward_result $segment_result + #JMN2 + set previous_result $segment_result + #set previous_result [join $segment_result] + } else { + debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a]} 4 + #output pipe spec at tail of pipeline + + set pipedvars [dict create] + if {[string length $pipespec($i,out)]} { + set d [apply {{mv res} { + punk::_multi_bind_result $mv $res -levelup 1 + }} $pipespec($i,out) $segment_result] + set segment_result [_handle_bind_result $d] + set pipedvars [dict get $d setvars] + } + + set more_pipe_segments 0 + } + + #the segment_result is based on the leftmost var on the lhs of the .= + #whereas forward_result is always the entire output of the segment + #JMN2 + #lappend segment_result_list [join $segment_result] + lappend segment_result_list $segment_result + incr i + incr j + } ;# end while + + return [lindex $segment_result_list end] + #JMN2 + #return $segment_result_list + #return $forward_result + } + + + #just an experiment + #what advantage/difference versus [llength [lrange $data $start $end]] ??? + proc data_range_length {data start end} { + set datalen [llength $data] + + #normalize to s and e + if {$start eq "end"} { + set s [expr {$datalen - 1}] + } elseif {[string match end-* $start]} { + set stail [string range $start 4 end] + set posn [expr {$datalen - $stail -1}] + if {$posn < 0} { + return 0 + } + set s $posn + } else { + #int + if {($start < 0) || ($start > ($datalen -1))} { + return 0 + } + set s $start + } + if {$end eq "end"} { + set e [expr {$datalen - 1}] + } elseif {[string match end-* $end]} { + set etail [string range $end 4 end] + set posn [expr {$datalen - $etail -1}] + if {$posn < 0} { + return 0 + } + set e $posn + } else { + #int + if {($end < 0)} { + return 0 + } + set e $end + } + if {$s > ($datalen -1)} { + return 0 + } + if {$e > ($datalen -1)} { + set e [expr {$datalen -1}] + } + + + + if {$e < $s} { + return 0 + } + + return [expr {$e - $s + 1}] + } + + # unknown -- + # This procedure is called when a Tcl command is invoked that doesn't + # exist in the interpreter. It takes the following steps to make the + # command available: + # + # 1. See if the autoload facility can locate the command in a + # Tcl script file. If so, load it and execute it. + # 2. If the command was invoked interactively at top-level: + # (a) see if the command exists as an executable UNIX program. + # If so, "exec" the command. + # (b) see if the command requests csh-like history substitution + # in one of the common forms !!, !, or ^old^new. If + # so, emulate csh's history substitution. + # (c) see if the command is a unique abbreviation for another + # command. If so, invoke the command. + # + # Arguments: + # args - A list whose elements are the words of the original + # command, including the command name. + + #review - we shouldn't really be doing this + #We need to work out if we can live with the real default unknown and just inject some special cases at the beginning before falling-back to the normal one + + proc ::unknown args { + #puts stderr "unk>$args" + variable ::tcl::UnknownPending + global auto_noexec auto_noload env tcl_interactive errorInfo errorCode + + if {[info exists errorInfo]} { + set savedErrorInfo $errorInfo + } + if {[info exists errorCode]} { + set savedErrorCode $errorCode + } + + set name [lindex $args 0] + if {![info exists auto_noload]} { + # + # Make sure we're not trying to load the same proc twice. + # + if {[info exists UnknownPending($name)]} { + return -code error "self-referential recursion\ + in \"unknown\" for command \"$name\"" + } + set UnknownPending($name) pending + set ret [catch { + auto_load $name [uplevel 1 {::namespace current}] + } msg opts] + unset UnknownPending($name) + if {$ret != 0} { + dict append opts -errorinfo "\n (autoloading \"$name\")" + return -options $opts $msg + } + if {![array size UnknownPending]} { + unset UnknownPending + } + if {$msg} { + if {[info exists savedErrorCode]} { + set ::errorCode $savedErrorCode + } else { + unset -nocomplain ::errorCode + } + if {[info exists savedErrorInfo]} { + set errorInfo $savedErrorInfo + } else { + unset -nocomplain errorInfo + } + set code [catch {uplevel 1 $args} msg opts] + if {$code == 1} { + # + # Compute stack trace contribution from the [uplevel]. + # Note the dependence on how Tcl_AddErrorInfo, etc. + # construct the stack trace. + # + set errInfo [dict get $opts -errorinfo] + set errCode [dict get $opts -errorcode] + set cinfo $args + if {[string length [encoding convertto utf-8 $cinfo]] > 150} { + set cinfo [string range $cinfo 0 150] + while {[string length [encoding convertto utf-8 $cinfo]] > 150} { + set cinfo [string range $cinfo 0 end-1] + } + append cinfo ... + } + set tail "\n (\"uplevel\" body line 1)\n invoked\ + from within\n\"uplevel 1 \$args\"" + set expect "$msg\n while executing\n\"$cinfo\"$tail" + if {$errInfo eq $expect} { + # + # The stack has only the eval from the expanded command + # Do not generate any stack trace here. + # + dict unset opts -errorinfo + dict incr opts -level + return -options $opts $msg + } + # + # Stack trace is nested, trim off just the contribution + # from the extra "eval" of $args due to the "catch" above. + # + set last [string last $tail $errInfo] + if {$last + [string length $tail] != [string length $errInfo]} { + # Very likely cannot happen + return -options $opts $msg + } + set errInfo [string range $errInfo 0 $last-1] + set tail "\"$cinfo\"" + set last [string last $tail $errInfo] + if {$last < 0 || $last + [string length $tail] != [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo $errInfo $msg + } + set errInfo [string range $errInfo 0 $last-1] + set tail "\n invoked from within\n" + set last [string last $tail $errInfo] + if {$last + [string length $tail] == [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo [string range $errInfo 0 $last-1] $msg + } + set tail "\n while executing\n" + set last [string last $tail $errInfo] + if {$last + [string length $tail] == [string length $errInfo]} { + return -code error -errorcode $errCode \ + -errorinfo [string range $errInfo 0 $last-1] $msg + } + return -options $opts $msg + } else { + dict incr opts -level + return -options $opts $msg + } + } + } + #set isrepl [expr {[file tail [file rootname [info script]]] eq "repl"}] + set isrepl [punk::repl::codethread::is_running] ;#may not be reading though + if {$isrepl} { + #set ::tcl_interactive 1 + } + if {$isrepl || (([info level] == 1) && (([info script] eq "" ) ) + && ([info exists tcl_interactive] && $tcl_interactive))} { + if {![info exists auto_noexec]} { + set new [auto_execok $name] + if {$new ne ""} { + set redir "" + if {[namespace which -command console] eq ""} { + set redir ">&@stdout <@stdin" + } + + + #windows experiment todo - use twapi and named pipes + #twapi::namedpipe_server {\\.\pipe\something} + #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones + #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc + # + + if {[string first " " $new] > 0} { + set c1 $name + } else { + set c1 $new + } + + # -- --- --- --- --- + set idlist_stdout [list] + set idlist_stderr [list] + #set shellrun::runout "" + #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks + #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] + #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] + + if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it + #not a trivial task + + #This runs external executables in a context in which they are not attached to a terminal + #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output + #ctrl-c propagation also needs to be considered + + set teehandle punksh + uplevel 1 [list ::catch \ + [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + + if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { + dict set ::tcl::UnknownOptions -code error + set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" + } else { + #no point returning "exitcode 0" if that's the only non-error return. + #It is misleading. Better to return empty string. + set ::tcl::UnknownResult "" + } + } else { + set repl_runid [punk::get_repl_runid] + #set ::punk::last_run_display [list] + + set redir ">&@stdout <@stdin" + uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr + #todo - there is probably no way around this but to somehow exec in the context of a completely separate console + #This is probably a tricky problem - especially to do cross-platform + # + # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit + if {[dict get $::tcl::UnknownOptions -code] == 0} { + set c green + set m "ok" + } else { + set c yellow + set m "errorCode $::errorCode" + } + set chunklist [list] + lappend chunklist [list "info" "[a $c]$m[a] " ] + if {$repl_runid != 0} { + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + + } + + foreach id $idlist_stdout { + shellfilter::stack::remove stdout $id + } + foreach id $idlist_stderr { + shellfilter::stack::remove stderr $id + } + # -- --- --- --- --- + + + #uplevel 1 [list ::catch \ + # [concat exec $redir $new [lrange $args 1 end]] \ + # ::tcl::UnknownResult ::tcl::UnknownOptions] + + #puts "===exec with redir:$redir $::tcl::UnknownResult ==" + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + } + + if {$name eq "!!"} { + set newcmd [history event] + } elseif {[regexp {^!(.+)$} $name -> event]} { + set newcmd [history event $event] + } elseif {[regexp {^\^([^^]*)\^([^^]*)\^?$} $name -> old new]} { + set newcmd [history event -1] + catch {regsub -all -- $old $newcmd $new newcmd} + } + if {[info exists newcmd]} { + tclLog $newcmd + history change $newcmd 0 + uplevel 1 [list ::catch $newcmd \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + + set ret [catch {set candidates [info commands $name*]} msg] + if {$name eq "::"} { + set name "" + } + if {$ret != 0} { + dict append opts -errorinfo \ + "\n (expanding command prefix \"$name\" in unknown)" + return -options $opts $msg + } + # Filter out bogus matches when $name contained + # a glob-special char [Bug 946952] + if {$name eq ""} { + # Handle empty $name separately due to strangeness + # in [string first] (See RFE 1243354) + set cmds $candidates + } else { + set cmds [list] + foreach x $candidates { + if {[string first $name $x] == 0} { + lappend cmds $x + } + } + } + + #punk - disable prefix match search + set default_cmd_search 0 + if {$default_cmd_search} { + if {[llength $cmds] == 1} { + uplevel 1 [list ::catch [lreplace $args 0 0 [lindex $cmds 0]] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } + if {[llength $cmds]} { + return -code error "ambiguous command name \"$name\": [lsort $cmds]" + } + } else { + #punk hacked version - report matches but don't run + if {[llength $cmds]} { + return -code error "unknown command name \"$name\": possible match(es) [lsort $cmds]" + } + + } + + + } + return -code error -errorcode [list TCL LOOKUP COMMAND $name] "invalid command name $name" + } + + proc know {cond body} { + set existing [info body ::unknown] + #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) + ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##if {$body ni $existing} { + package require base64 + set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered + #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { + #--------------------------------------- + if {![catch {expr {@c@}} res] && $res} { + debug.punk.unknown {HANDLED BY: punk unknown_handler ([llength $args]) args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + return [eval {@b@}] + } else { + debug.punk.unknown {skipped: punk unknown_handler ([llength $args]) args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + } + #--------------------------------------- + }]$existing + #} + } + + proc know? {{len 2000}} { + puts [string range [info body ::unknown] 0 $len] + } + proc decodescript {b64} { + if {[ catch { + package require base64 + base64::decode $b64 + } scr]} { + return "" + } else { + return "($scr)" + } + } + + # --------------------------- + # commands that should be aliased in safe interps that need to use punk repl + # + proc get_repl_runid {} { + if {[interp issafe]} { + if {[info commands ::tsv::exists] eq ""} { + puts stderr "punk::get_repl_runid cannot operate directly in safe interp - install the appropriate punk aliases" + error "punk::get_repl_runid punk repl aliases not installed" + } + #if safe interp got here - there must presumably be a direct set of aliases on tsv::* commands + } + if {[tsv::exists repl runid]} { + return [tsv::get repl runid] + } else { + return 0 + } + } + #ensure we don't get into loop in unknown when in safe interp - which won't have tsv + proc set_repl_last_unknown {args} { + if {[interp issafe]} { + if {[info commands ::tsv::set] eq ""} { + puts stderr "punk::set_repl_last_unknown cannot operate directly in safe interp - install an alias to tsv::set repl last_unknown" + return + } + #tsv::* somehow working - possibly custom aliases for tsv functionality ? review + } + if {[info commands ::tsv::set] eq ""} { + puts stderr "set_repl_last_unknown - tsv unavailable!" + return + } + tsv::set repl last_unknown {*}$args + } + # --------------------------- + + + proc configure_unknown {} { + #----------------------------- + #these are critical e.g core behaviour or important for repl displaying output correctly + + #---------------- + #for var="val {a b c}" + #proc ::punk::val {{v {}}} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version + proc ::punk::val [list [list v [purelist]]] {return $v} + #---------------- + + #can't use know - because we don't want to return before original unknown body is called. + proc ::unknown {args} [string cat { + package require base64 + #set ::punk::last_run_display [list] + #set ::repl::last_unknown [lindex $args 0] ;#jn + #tsv::set repl last_unknown [lindex $args 0] ;#REVIEW + punk::set_repl_last_unknown [lindex $args 0] + }][info body ::unknown] + + + #handle process return dict of form {exitcode num etc blah} + #ie when the return result as a whole is treated as a command + #exitcode must be the first key + know {[lindex $args 0 0] eq "exitcode"} { + uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]] + } + + + #----------------------------- + # + # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. + + #todo - repl output info that it was evaluated as an expression + #know {[expr $args] || 1} {expr $args} + know {[expr $args] || 1} {tailcall expr $args} + + #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc + #punk::lib::range is defined as a wrapper to lseq if it is available (8.7+) + know {[regexp {^([+-]*[0-9_]+)\.\.([+-]*[0-9_]+)$} [lindex $args 0 0] -> from to]} {punk::lib::range $from $to} + + + #NOTE: + #we don't allow setting namespace qualified vars in the lhs assignment pattern. + #The principle is that we shouldn't be setting vars outside of the immediate calling scope. + #(It would also be difficult and error-prone and generally make the pipelines less re-usable and reliable) + #Therefore ::nswhatever::blah= x is the pipeline: blah= x - where the corresponding command, if any is first resolved in ::nswhatever + #We will require that the namespace already exists - which is consistent with if the command were to be run without unknown + proc ::punk::_unknown_assign_dispatch {matchedon pattern equalsrhs args} { + set tail [lassign $args hd] + #puts "-> _unknown_assign_dispatch '$partzerozero' pattern:'$pattern' equalsrhs:'$equalsrhs' args:'$args' argshd:'$hd' argstail:'$tail'" + if {$hd ne $matchedon} { + if {[llength $tail]} { + error "unknown_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $tail" + } + #regexp $punk::re_assign $hd _ pattern equalsrhs + #we assume the whole pipeline has been provided as the head + #regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail + regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs tail + } + #NOTE: - it doesn't make sense to call 'namespace' qualifiers or 'namespace tail' on a compound hd such as v,::etc= blah + # we only look at leftmost namespace-like thing and need to take account of the pattern syntax + # e.g for ::etc,'::x'= + # the ns is :: and the tail is etc,'::x'= + # (Tcl's namespace qualifiers/tail won't help here) + if {[string match ::* $hd]} { + set patterns [punk::_split_patterns_memoized $hd] + #get a pair-list something like: {::x /0} {etc {}} + set ns [namespace qualifiers [lindex $patterns 0 0]] + set nslen [string length $ns] + set patterntail [string range $ns $nslen end] + } else { + set ns "" + set patterntail $pattern + } + if {[string length $ns] && ![namespace exists $ns]} { + error "unknown_assign_dispatch: namespace '$ns' not found. (Note that pipeline lhs variables cannot be namespaced)" + } else { + set nscaller [uplevel 1 [list ::namespace current]] + #jmn + set rhsmapped [pipecmd_namemapping $equalsrhs] + set commands [uplevel 1 [list ::info commands $pattern=$rhsmapped]] ;#uplevel - or else we are checking from perspective of this namespace ::punk + #we must check for exact match of the command in the list - because command could have glob chars. + if {"$pattern=$rhsmapped" in $commands} { + puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'" + #we call the namespaced function - we don't evaluate it *in* the namespace. + #REVIEW + #warn for now...? + #tailcall $pattern=$equalsrhs {*}$args + tailcall $pattern=$rhsmapped {*}$tail + } + } + #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" + #ignore the namespace.. + #We could interpret the fact that the nonexistant pipe was called with a namespace to indicate that's where the pipecommand should be created.. + #But.. we would need to ensure 1st (compiling) invocation runs the same way as subsequent invocations. + #namespace evaling match_assign here probably wouldn't accomplish that and may create surprises with regards to where lhs vars(if any) are created + tailcall ::punk::match_assign $patterntail $equalsrhs {*}$tail + #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] + } + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) + #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list + #e.g x=a\nb c + #x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained + # + #know {[regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + #know {[regexp {^{([^\t\r\n=]*)\=([^ \t\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + + + + proc ::punk::_unknown_compare {val1 val2 args} { + if {![string length [string trim $val2]]} { + if {[llength $args] > 1} { + #error "Extra args after comparison operator ==. usage e.g : \$var1==\$var2 or \$var1==\$var2 + 2" + set val2 [string cat {*}[lrange $args 1 end]] + return [expr {$val1 eq $val2}] + } + return $val1 + } elseif {[llength $args] == 1} { + #simple comparison + if {[string is digit -strict $val1$val2]} { + return [expr {$val1 == $val2}] + } else { + return [string equal $val1 $val2] + } + } elseif {![catch {expr $val2 {*}[lrange $args 1 end]} evaluated]} { + if {[string is digit -strict $val1$evaluated]} { + return [expr {$val1 == $evaluated}] + } else { + return [expr {$val1 eq $evaluated}] + } + } else { + set evaluated [uplevel 1 [list {*}$val2 {*}[lrange $args 1 end]]] + if {[string is digit -strict $val1$evaluated]} { + return [expr {$val1 == $evaluated}] + } else { + return [expr {$val1 eq $evaluated}] + } + } + } + #ensure == is after = in know sequence + #.* on left is pretty broad - todo: make it a little more specific to avoid unexpected interactions + know {[regexp {(.*)==(.*)} [lindex $args 0] _ val1 val2]} {tailcall ::punk::_unknown_compare $val1 $val2 {*}$args} + #.= must come after = here to ensure it comes before = in the 'unknown' proc + #set punk::re_dot_assign {([^=]*)\.=(.*)} + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] _ varspecs rhs]} { + # set tail [expr {([lindex $args 0] eq [lindex $args 0 0]) ? [lrange $args 1 end] : [concat [lrange [lindex $args 0] 1 end] [lrange $args 1 end] ] }] + # tailcall ::punk::match_exec $varspecs $rhs {*}$tail + # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] + # } + # + + + + proc ::punk::_unknown_dot_assign_dispatch {partzerozero pattern equalsrhs args} { + #puts stderr ". unknown dispatch $partzerozero" + set argstail [lassign $args hd] + + #this equates to auto-flattening the head.. which seems like a bad idea, the structure was there for a reason. + #we should require explicit {*} expansion if the intention is for the args to be joined in at that level. + #expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } + + if {$hd ne $partzerozero} { + if {[llength $argstail]} { + error "unknown_dot_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $argstail" + } + #regexp $punk::re_assign $hd _ pattern equalsrhs + #we assume the whole pipeline has been provided as the head + #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + + regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs argstail + } + #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail + + + return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]] + + } + + # + know {[regexp {^([^\t\r\n=]*)\=([^\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + know {[regexp {^{([^\t\r\n=]*)\=([^\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + + #variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + + #add escaping backslashes to a value + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g + #set ktest {a"b} + #@@[escv $ktest].= list a"b val + #without escv: + #@@"a\\"b".= list a"b val + #with more backslashes in keys the escv use becomes more apparent: + #set ktest {\\x} + #@@[escv $ktest].= list $ktest val + #without escv we would need: + #@@\\\\\\\\x.= list $ktest val + proc escv {v} { + #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically + #thanks to DKF + regsub -all {\W} $v {\\&} + } + interp alias {} escv {} punk::escv + #review + #set v "\u2767" + # + #escv $v + #\ + #the + + + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { + # set argstail [lassign $args hd] + # #set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable! + # #avoid using the return from expr and it works: + # expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } + # + # tailcall ::punk::match_exec $varspecs $rhs {*}$tail + # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] + #} + + } + configure_unknown + #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. + # + + #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc + #Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation. + proc % {args} { + set arglist [lassign $args assign] ;#tail, head + if {$assign eq ".="} { + tailcall {*}[list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] + } + + set is_script [punk::arg_is_script_shaped $assign] + + if {!$is_script && [string index $assign end] eq "="} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + #set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + #set dumbeditor {\}} + if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=\{]*)=$} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax punk::% unable to interpret pipeline '$args'" "% $args" [list pipesyntax unable_to_interpret] + } + } else { + if {$is_script} { + set cmdlist [list ::punk::pipeline "script" "" "" {*}$args] + } else { + set cmdlist [list ::punk::pipeline ".=" "" "" {*}$args] + } + } + tailcall {*}$cmdlist + + + #result-based mismatch detection can probably never work nicely.. + #we need out-of-band method to detect mismatch. Otherwise we can't match on mismatch results! + # + set result [uplevel 1 $cmdlist] + #pipeline result not guaranteed to be a proper list so we can't use list methods to directly look for 'binding mismatch' + #.. but if we use certain string methods - we shimmer the case where the main result is a list + #string match doesn't seem to change the rep.. though it does generate a string rep. + #puts >>1>[rep $result] + if {[catch {lrange $result 0 1} first2wordsorless]} { + #if we can't get as a list then it definitely isn't the semi-structured 'binding mismatch' + return $result + } else { + if {$first2wordsorless eq {binding mismatch}} { + error $result + } else { + #puts >>2>[rep $result] + return $result + } + } + } + + proc ispipematch {args} { + expr {[lindex [uplevel 1 [list pipematch {*}$args]] 0] eq "ok"} + } + + #pipe initiator which will never raise an error *except for pipesyntax* , but always returns {ok {result something}} or {error {mismatch something}} or, for tcl errors {error {reason something}} + proc pipematch {args} { + #debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2 + variable re_dot_assign + variable re_assign + + set arglist [lassign $args assign] + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + # set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + # set dumbeditor {\}} + if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=]*)=.*} $assign _ returnvarspecs]} { + set cmdlist [list $assign {*}$arglist] + #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax punk::pipematch unable to interpret pipeline '$args'" "pipematch $args" [pipesyntax unable_to_interpret] + } + } else { + set cmdlist $args + #script? + #set cmdlist [list ::punk::pipeline .= "" "" {*}$args] + } + + if {[catch {uplevel 1 $cmdlist} result erroptions]} { + #puts stderr "pipematch erroptions:$erroptions" + #debug.punk.pipe {pipematch error $result} 4 + set ecode [dict get $erroptions -errorcode] + switch -- [lindex $ecode 0] { + binding { + if {[lindex $ecode 1] eq "mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + #return [dict create error [dict create mismatch $result]] + #puts stderr "pipematch converting error to {error {mismatch }}" + return [list error [list mismatch $result]] + } + } + pipesyntax { + #error $result + return -options $erroptions $result + } + casematch { + return $result + } + } + #return [dict create error [dict create reason $result]] + return [list error [list reason $result]] + } else { + return [list ok [list result $result]] + #debug.punk.pipe {pipematch result $result } 4 + #return [dict create ok [dict create result $result]] + } + } + + proc pipenomatchvar {varname args} { + if {[string first = $varname] >=0} { + #first word "pipesyntax" is looked for by pipecase + error "pipesyntax pipenomatch expects a simple varname as first argument" "pipenomatchvar $varname $args" [list pipesyntax expected_simple_varname] + } + #debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2 + + set assign [lindex $args 0] + set arglist [lrange $args 1 end] + if {[string first = $assign] >= 0} { + variable re_dot_assign + variable re_assign + #what if we get passed a script block containing = ?? e.g {error x=a} + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + } elseif {[regexp $re_dot_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs $rhs {*}$arglist] + } elseif {[regexp $re_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::pipeline = $returnvarspecs $rhs {*}$arglist] + } else { + debug.punk.pipe {[a+ yellow bold] Unexpected arg following pipenomatchvar variable [a]} 0 + set cmdlist $args + #return [dict create error [dict create reason [dict create pipematch bad_first_word value $assign pipeline [list pipematch $assign {*}$args]]]] + } + } else { + set cmdlist $args + } + + upvar 1 $varname nomatchvar + if {[catch {uplevel 1 $cmdlist} result erroptions]} { + set ecode [dict get $erroptions -errorcode] + debug.punk.pipe {[a+ yellow bold]pipematchnomatch error $result[a]} 3 + if {[lindex $ecode 0] eq "pipesyntax"} { + set errordict [dict create error [dict create pipesyntax $result]] + set nomatchvar $errordict + return -options $erroptions $result + } + if {[lrange $ecode 0 1] eq "binding mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + set errordict [dict create error [dict create mismatch $result]] + set nomatchvar $errordict + return -options $erroptions $result + } + set errordict [dict create error [dict create reason $result]] + set nomatchvar $errordict + #re-raise the error for pipeswitch to deal with + return -options $erroptions $result + } else { + debug.punk.pipe {pipematchnomatch result $result } 4 + set nomatchvar "" + #uplevel 1 [list set $varname ""] + #return raw result only - to pass through to pipeswitch + return $result + #return [dict create ok [dict create result $result]] + } + } + + #should only raise an error for pipe syntax errors - all other errors should be wrapped + proc pipecase {args} { + #debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9 + set arglist [lassign $args assign] + if {$assign eq ".="} { + set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] + } elseif {$assign eq "="} { + #set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] + set cmdlist [list ::= {*}$arglist] + } elseif {![punk::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { + #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} + #set dumbeditor {\}} + #set re_equals {^([^ \t\r\n=\{]*)=$} + #set dumbeditor {\}} + + if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { + set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] + } elseif {[regexp {^([^ \t\r\n=]*)=.*} $assign _ returnvarspecs]} { + set cmdlist [list $assign {*}$arglist] + #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] + } else { + error "pipesyntax pipecase unable to interpret pipeline '$args'" + } + #todo - account for insertion-specs e.g x=* x.=/0* + } else { + #script? + set cmdlist [list ::punk::pipeline .= "" "" {*}$args] + } + + + if {[catch {uplevel 1 [list ::if 1 $cmdlist]} result erroptions]} { + #puts stderr "====>>> result: $result erroptions" + set ecode [dict get $erroptions -errorcode] + switch -- [lindex $ecode 0] { + pipesyntax { + #error $result + return -options $erroptions $result + } + casenomatch { + return -options $erroptions $result + } + binding { + if {[lindex $ecode 1] eq "mismatch"} { + #error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch + #return [dict create error [dict create mismatch $result]] + # + #NOTE: casemismatch is part of the api for pipecase. It is a casemismatch rather than an error - because for a pipecase - a casemismatch is an expected event (many casemismatches - one match) + return [dict create casemismatch $result] + } + } + } + + #we can't always treat $result as a list - may be an error string which can't be represented as a list, and there may be no useful errorCode + #todo - use errorCode instead + if {[catch {lindex $result 0} word1]} { + #tailcall error $result + return -options $erroptions $result + } else { + switch -- $word1 { + switcherror - funerror { + error $result "pipecase [lsearch -all -inline $args "*="]" + } + resultswitcherror - resultfunerror { + #recast the error as a result without @@ok wrapping + #use the tailcall return to stop processing other cases in the switch! + tailcall return [dict create error $result] + } + ignore { + #suppress error, but use normal return + return [dict create error [dict create suppressed $result]] + } + default { + #normal tcl error + #return [dict create error [dict create reason $result]] + tailcall error $result "pipecase $args" [list caseerror] + } + } + } + } else { + tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] + } + + } + + #note that pipeswitch deliberately runs in callers scope to have direct access to variables - it is akin to a control structure. + #It also - somewhat unusually accepts args - which we provide as 'switchargs' + #This is unorthodox/risky in that it will clobber any existing var of that name in callers scope. + #Solve using documentation.. consider raising error if 'switchargs' already exists, which would require user to unset switchargs in some circumstances. + proc pipeswitch {pipescript args} { + #set nextargs $args + #unset args + #upvar args upargs + #set upargs $nextargs + upvar switchargs switchargs + set switchargs $args + uplevel 1 [::list ::if 1 $pipescript] + } + #static-closure version - because we shouldn't be writing back to calling context vars directly + #Tcl doesn't (2023) have mutable closures - but for functional pipeline composition - we probably don't want that anyway! + #pipeswitchc is preferable to pipeswitch in that we can access context without risk of affecting it, but is less performant. (particularly in global scope.. but that probably isn't an important usecase) + proc pipeswitchc {pipescript args} { + set binding {} + if {[info level] == 1} { + #up 1 is global + set get_vars [list info vars] + } else { + set get_vars [list info locals] + } + set vars [uplevel 1 {*}$get_vars] + set posn [lsearch $vars switchargs] + set vars [lreplace $vars $posn $posn] + foreach v $vars { + upvar 1 $v var + if {(![array exists var]) && [info exists var]} { + lappend binding [list $v $var] ;#values captured as defaults for apply args. + } + } + lappend binding [list switchargs $args] + apply [list $binding $pipescript [uplevel 1 {::namespace current}]] + } + + proc pipedata {data args} { + #puts stderr "'$args'" + set r $data + for {set i 0} {$i < [llength $args]} {incr i} { + set e [lindex $args $i] + #review: string is list is as slow as catch {llength $e} - and also affects ::errorInfo unlike other string is commands. bug/enhancement report? + if {![string is list $e]} { + #not a list - assume script and run anyway + set r [apply [list {data} $e] $r] + } else { + if {[llength $e] == 1} { + switch -- $e { + > { + #output to calling context. only pipedata return value and '> varname' should affect caller. + incr i + uplevel 1 [list set [lindex $args $i] $r] + } + % - pipematch - ispipematch { + incr i + set e2 [lindex $args $i] + #set body [list $e {*}$e2] + #append body { $data} + + set body [list $e {*}$e2] + append body { {*}$data} + + + set applylist [list {data} $body] + #puts stderr $applylist + set r [apply $applylist $r] + } + pipeswitch - pipeswitchc { + #pipeswitch takes a script not a list. + incr i + set e2 [lindex $args $i] + set body [list $e $e2] + #pipeswitch takes 'args' - so expand $data when in pipedata context + append body { {*}$data} + #use applylist instead of uplevel when in pipedata context! + #can use either switchdata/data but not vars in calling context of 'pipedata' command. + #this is consistent with pipeswitch running in a % / .= pipeline which can only access vars in immediate calling context. + set applylist [list {data} $body] + #puts stderr $applylist + set r [apply $applylist $r] + } + default { + #puts "other single arg: [list $e $r]" + append e { $data} + set r [apply [list {data} $e] $r] + } + } + } elseif {[llength $e] == 0} { + #do nothing - pass data through + #leave r as is. + } else { + set r [apply [list {data} $e] $r] + } + } + } + return $r + } + + + proc scriptlibpath {{shortname {}} args} { + upvar ::punk::config::running running_config + set scriptlib [dict get $running_config scriptlib] + if {[string match "lib::*" $shortname]} { + set relpath [string map [list "lib::" "" "::" "/"] $shortname] + set relpath [string trimleft $relpath "/"] + set fullpath $scriptlib/$relpath + } else { + set shortname [string trimleft $shortname "/"] + set fullpath $scriptlib/$shortname + } + return $fullpath + } + + + #useful for aliases e.g treemore -> xmore tree + proc xmore {args} { + if {[llength $args]} { + uplevel #0 [list {*}$args | more] + } else { + error "usage: punk::xmore args where args are run as {*}\$args | more" + } + } + + + #environment path as list + # + #return *appendable* pipeline - i.e no args via <| + proc path_list_pipe {{glob *}} { + if {$::tcl_platform(platform) eq "windows"} { + set sep ";" + } else { + # : ok for linux/bsd ... mac? + set sep ":" + } + set cond [string map [list $glob] {expr {[string length $item] && [string match $item]}}] + #env members such as ''path' not case sensitive on windows - but are on some other platforms (at least FreeBSD) + return [list .= set ::env(PATH) |> .=>2 string trimright $sep |> .=>1 split $sep |> list_filter_cond $cond ] + } + proc path_list {{glob *}} { + set pipe [punk::path_list_pipe $glob] + {*}$pipe + } + proc path {{glob *}} { + set pipe [punk::path_list_pipe $glob] + {*}$pipe |> list_as_lines + } + + #------------------------------------------------------------------- + #sh 'test' equivalent - to be used with exitcode of process + # + + #single evaluation to get exitcode + proc sh_test {args} { + set a1 [lindex $args 0] + if {$a1 in [list -b -c -d -e -f -h -L -s -S -x -w]} { + set a2 [lindex $args 1] + if {![catch { + set attrinfo [file attributes $a2] + } errM]} { + if {[dict exists $attrinfo -vfs] && [dict get $attrinfo -vfs] == 1} { + puts stderr "WARNING: external 'test' being called on vfs path. External command will probably not have access to the vfs. Use 'TEST' for Tcl view of vfs mounted filesystems." + } + } + } + tailcall run test {*}$args + } + + #whether v is an integer from perspective of unix test command. + #can be be bigger than a tcl int or wide ie bignum - but must be whole number + #test doesn't handle 1.0 - so we shouldn't auto-convert + proc is_sh_test_integer {v} { + if {[string first . $v] >=0 || [string first e $v] >= 0} { + return false + } + #if it is double but not sci notation and has no dots - then we can treat as a large integer for 'test' + if {[string is double -strict $v]} { + return true + } else { + return false + } + } + #can use double-evaluation to get true/false + #faster tcl equivalents where possible to accuratley provide, and fallthrough to sh for compatibility of unimplemented + #The problem with fallthrough is that sh/bash etc have a different view of existant files + #e.g unix files such as /dev/null vs windows devices such as CON,PRN + #e.g COM1 is mapped as /dev/ttyS1 in wsl (?) + #Note also - tcl can have vfs mounted file which will appear as a directory to Tcl - but a file to external commands! + #We will stick with the Tcl view of the file system. + #User can use their own direct calls to external utils if + #Note we can't support $? directly in Tcl - script would have to test ${?} or use [set ?] + proc sh_TEST {args} { + upvar ? lasterr + set lasterr 0 + set a1 [lindex $args 0] + set a2 [lindex $args 1] + set a3 [lindex $args 2] + set fileops [list -b -c -d -e -f -h -L -s -S -x -w] + if {[llength $args] == 1} { + #equivalent of -n STRING + set boolresult [expr {[string length $a1] != 0}] + } elseif {[llength $args] == 2} { + if {$a1 in $fileops} { + if {$::tcl_platform(platform) eq "windows"} { + #e.g trailing dot or trailing space + if {[punk::winpath::illegalname_test $a2]} { + #protect with \\?\ to stop windows api from parsing + #will do nothing if already prefixed with \\?\ + + set a2 [punk::winpath::illegalname_fix $a2] + } + } + } + switch -- $a1 { + -b { + #dubious utility on FreeBSD, windows? + #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' + #Linux apparently uses them though + if{[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "blockSpecial"}] + } else { + set boolresult false + } + } + -c { + #e.g on windows CON,NUL + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "characterSpecial"}] + } else { + set boolresult false + } + } + -d { + set boolresult [file isdirectory $a2] + } + -e { + set boolresult [file exists $a2] + } + -f { + #e.g on windows CON,NUL + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "file"}] + } else { + set boolresult false + } + } + -h - + -L { + set boolresult [expr {[file type $a2] eq "link"}] + } + -s { + set boolresult [expr {[file exists $a2] && ([file size $a2] > 0 )}] + } + -S { + if {[file exists $a2]} { + set boolresult [expr {[file type $a2] eq "socket"}] + } else { + set boolresult false + } + } + -x { + set boolresult [expr {[file exists $a2] && [file executable $a2]}] + } + -w { + set boolresult [expr {[file exists $a2] && [file writable $a2]}] + } + -z { + set boolresult [expr {[string length $a2] == 0}] + } + -n { + set boolresult [expr {[string length $a2] != 0}] + } + default { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + } + } + } elseif {[llength $args] == 3} { + switch -- $a2 { + "=" { + #test does string comparisons + set boolresult [string equal $a1 $a3] + } + "!=" { + #string comparison + set boolresult [expr {$a1 ne $a3}] + } + "-eq" { + #test expects a possibly-large integer-like thing + #shell scripts will + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 == $a3}] + } + "-ge" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 >= $a3}] + } + "-gt" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 > $a3}] + } + "-le" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 <= $a3}] + } + "-lt" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 < $a3}] + } + "-ne" { + if {![is_sh_test_integer $a1]} { + puts stderr "sh_TEST: invalid integer '$a1'" + set lasterr 2 + return false + } + if {![is_sh_test_integer $a3]} { + puts stderr "sh_TEST: invalid integer '$a3'" + set lasterr 2 + return false + } + set boolresult [expr {$a1 != $a3}] + } + default { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + + } + } + } else { + puts stderr "sh_TEST: delegating 'test $args' to external 'test' command" + #set boolresult [apply {arglist {uplevel #0 [runx test {*}$arglist]} ::} $args] + set callinfo [runx test {*}$args] + set errinfo [dict get $callinfo stderr] + set exitcode [dict get $callinfo exitcode] + if {[string length $errinfo]} { + puts stderr "sh_TEST error in external call to 'test $args': $errinfo" + set lasterr $exitcode + } + if {$exitcode == 0} { + set boolresult true + } else { + set boolresult false + } + } + + #normalize 1,0 etc to true,false + #we want to make it obvious we are not just reporting exitcode 0 for example - which represents true in tcl. + if {$boolresult} { + return true + } else { + if {$lasterr == 0} { + set lasterr 1 + } + return false + } + + + } + proc sh_echo {args} { + tailcall run echo {*}$args + } + proc sh_ECHO {args} { + #execute the result of the run command - which is something like: 'exitcode n' - to get true/false + tailcall apply {arglist {uplevel #0 [run echo {*}$arglist]} ::} $args + } + + + #sh style true/false for process exitcode. 0 is true - everything else false + proc exitcode {args} { + set c [lindex $args 0] + if {[string is integer -strict $c]} { + #return [expr {$c == 0}] + #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true + if {$c == 0} { + return true + } else { + return false + } + } else { + return false + } + } + #------------------------------------------------------------------- + + namespace export help aliases alias exitcode % pipedata pipecase pipeline pipematch pipeswitch pipeswitchc pipecase linelist linesort inspect list_as_lines val treemore + + #namespace ensemble create + + + + + #tilde + #These aliases work fine for interactive use - but the result is always a string int-rep + #interp alias {} ~ {} file join $::env(HOME) ;#HOME must be capitalized to work cross platform (lowercase home works on windows - but probably not elsewhere) + #interp alias {} ~ {} apply {args {file join $::env(HOME) $args}} + proc ~ {args} { + set hdir [punk::objclone $::env(HOME)] + file pathtype $hdir + set d $hdir + #use the file join 2-arg optimisation to avoid losing path-rep - probably doesn't give any advantage on all Tcl versions + foreach a $args { + set d [file join $d $a] + } + file pathtype $d + return [punk::objclone $d] + } + interp alias {} ~ {} punk::~ + + + #maint - punk::args has similar + #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args + #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? + #JMN + #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values + #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. + #only supports -flag val pairs, not solo options + #If an option is supplied multiple times - only the last value is used. + #TODO - remove + proc get_leading_opts_and_values {defaults rawargs args} { + if {[llength $defaults] %2 != 0} { + error "get_leading_opts_and_values expected first argument 'defaults' to be a dictionary" + } + dict for {k v} $defaults { + if {![string match -* $k]} { + error "get_leading_opts_and_values problem with supplied defaults. Expect each key to begin with a dash. Got key '$k'" + } + } + #puts "--> [info frame -2] <--" + set cmdinfo [dict get [info frame -2] cmd] + #we can't treat cmdinfo as a list - it may be something like {command {*}$args} in which case lindex $cmdinfo 0 won't work + #hopefully first word is a plain proc name if this function was called in the normal manner - directly from a proc + #we will break at first space and assume the lhs of that will give enough info to be reasonable - (alternatively we could use entire cmdinfo - but it might be big and ugly) + set caller [regexp -inline {\S+} $cmdinfo] + + #if called from commandline or some other contexts such as outside of a proc in a namespace - caller may just be "namespace" + if {$caller eq "namespace"} { + set caller "get_leading_opts_and_values called from namespace" + } + + # ------------------------------ + if {$caller ne "get_leading_opts_and_values"} { + #check our own args + lassign [get_leading_opts_and_values {-anyopts 0 -minvalues 0 -maxvalues -1} $args] _o ownopts _v ownvalues + if {[llength $ownvalues] > 0} { + error "get_leading_opts_and_values expected: a dictionary of defaults, a list of args and at most two option pairs -minvalues and -maxvalues - got extra arguments: '$ownvalues'" + } + set opt_minvalues [dict get $ownopts -minvalues] + set opt_maxvalues [dict get $ownopts -maxvalues] + set opt_anyopts [dict get $ownopts -anyopts] + } else { + #don't check our own args if we called ourself + set opt_minvalues 0 + set opt_maxvalues 0 + set opt_anyopts 0 + } + # ------------------------------ + + if {[set eopts [lsearch $rawargs "--"]] >= 0} { + set values [lrange $rawargs $eopts+1 end] + set arglist [lrange $rawargs 0 $eopts-1] + } else { + if {[lsearch $rawargs -*] >= 0} { + #to support option values with leading dash e.g -offset -1 , we can't just take the last flagindex + set i 0 + foreach {k v} $rawargs { + if {![string match -* $k]} { + break + } + if {$i+1 >= [llength $rawargs]} { + #no value for last flag + error "bad options for $caller. No value supplied for last option $k" + } + incr i 2 + } + set arglist [lrange $rawargs 0 $i-1] + set values [lrange $rawargs $i end] + } else { + set values $rawargs ;#no -flags detected + set arglist [list] + } + } + if {$opt_maxvalues == -1} { + #only check min + if {[llength $values] < $opt_minvalues} { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected at least $opt_minvalues" + } + } else { + if {[llength $values] < $opt_minvalues || [llength $values] > $opt_maxvalues} { + if {$opt_minvalues == $opt_maxvalues} { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected exactly $opt_minvalues" + } else { + error "bad number of trailing values for $caller. Got [llength $values] values. Expected between $opt_minvalues and $opt_maxvalues inclusive" + } + } + } + + if {!$opt_anyopts} { + set checked_args [dict create] + for {set i 0} {$i < [llength $arglist]} {incr i} { + #allow this to error out with message indicating expected flags + dict set checked_args [tcl::prefix match -message "options for $caller. Unexpected option" [dict keys $defaults] [lindex $arglist $i]] [lindex $arglist $i+1] + incr i ;#skip val + } + } else { + set checked_args $arglist + } + set opts [dict merge $defaults $checked_args] + + #maintain order of opts $opts values $values as caller may use lassign. + return [dict create opts $opts values $values] + } + + + + + + + + + + #-------------------------------------------------- + #some haskell-like operations + #group equivalent + #http://zvon.org/other/haskell/Outputlist/group_f.html + #as we can't really distinguish a single element list from a string we will use 2 functions + proc group_list1 {lst} { + set out [list] + set prev [lindex $lst 0] + set g [list] + foreach i $lst { + if {$i eq $prev} { + lappend g $i + } else { + lappend out $g + set g [list $i] + } + set prev $i + } + lappend out $g + return $out + } + proc group_list {lst} { + set out [list] + set next [lindex $lst 1] + set tail [lassign $lst x] + set g [list $x] + set y [lindex $tail 0] + set last_condresult [expr {$x}] + set n 1 ;#start at one instead of zero for lookahead + foreach x $tail { + set y [lindex $tail $n] + set condresult [expr {$x}] + if {$condresult eq $last_condresult} { + lappend g $x + } else { + lappend out $g + set g [list $x] + set last_condresult $condresult + } + incr n + } + lappend out $g + return $out + } + + #NOT attempting to match haskell other than in overall concept. + # + #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. + #Haskell seems to take an entire lambda so varnames can be user-specified - but the 'magic' there is in it's choice of submitting 2 elements at a time + #We could do similar .. but we'll focus on comprehensibility for the basic cases - especially as begginning and end of list issues could be confusing. + # + #vars: index prev, prev0, prev1, item, next, next0, next1,nextr, cond + #(nextr is a bit obscure - but basically means next-repeat ie if no next - use same value. just once though.) + #group by cond result or first 3 wordlike parts of error + #e.g group_list_by {[lindex $item 0]} {{a 1} {a 2} {b 1}} + proc group_list_by {cond lst} { + set out [list] + set prev [list] + set next [lindex $lst 1] + set tail [lassign $lst item] + set g [list $item] + set next [lindex $tail 0] + if {$prev eq ""} { + set prev0 0 + set prev1 1 + set prevr $item + } else { + set prev0 $prev + set prev1 $prev + set prevr $prev + } + if {$next eq ""} { + set next0 0 + set next1 1 + set nextr $item + } else { + set next0 $next + set next1 $next + set nextr $next + } + set last_condresult [apply {{index cond prev prev0 prev1 prevr item next next0 next1 nextr} { + if {[catch {expr $cond} r]} { + puts stderr "index: 0 ERROR $r" + set wordlike_parts [regexp -inline -all {\S+} $r] + set r [list ERROR {*}[lrange $wordlike_parts 0 2]] + } + set r + } + } 0 $cond $prev $prev0 $prev1 $prevr $item $next $next0 $next1 $nextr] + set n 1 ;#start at one instead of zero for lookahead + #note - n also happens to matchi zero-based index of original list + set prev $item + foreach item $tail { + set next [lindex $tail $n] + if {$prev eq ""} { + set prev0 0 + set prev1 1 + set prevr $item + } else { + set prev0 $prev + set prev1 $prev + set prevr $prev + } + if {$next eq ""} { + set next0 0 + set next1 1 + set nextr $item + } else { + set next0 $next + set next1 $next + set nextr $next + } + set condresult [apply {{index cond prev prev0 prev1 prevr item next next0 next1 nextr} { + if {[catch {expr $cond} r]} { + puts stderr "index: $index ERROR $r" + set wordlike_parts [regexp -inline -all {\S+} $r] + set r [list ERROR {*}[lrange $wordlike_parts 0 2]] + } + set r + } + } $n $cond $prev $prev0 $prev1 $prevr $item $next $next0 $next1 $nextr] + if {$condresult eq $last_condresult} { + lappend g $item + } else { + lappend out $g + set g [list $item] + set last_condresult $condresult + } + incr n + set prev $item + } + lappend out $g + return $out + } + + #group_numlist ? preserve representation of numbers rather than use string comparison? + + + # - group_string + #.= punk::group_string "aabcccdefff" + # aa b ccc d e fff + proc group_string {str} { + lmap v [group_list [split $str ""]] {string cat {*}$v} + } + + #lists may be of unequal lengths + proc transpose_lists {list_rows} { + set res {} + #set widest [pipedata $list_rows {lmap v $data {llength $v}} {tcl::mathfunc::max {*}$data}] + set widest [tcl::mathfunc::max {*}[lmap v $list_rows {llength $v}]] + for {set j 0} {$j < $widest} {incr j} { + set newrow {} + foreach oldrow $list_rows { + if {$j >= [llength $oldrow]} { + continue + } else { + lappend newrow [lindex $oldrow $j] + } + } + lappend res $newrow + } + return $res + } + proc transpose_strings {list_of_strings} { + set charlists [lmap v $list_of_strings {split $v ""}] + set tchars [transpose_lists $charlists] + lmap v $tchars {string cat {*}$v} + } + + package require struct::matrix + #transpose a serialized matrix using the matrix command + #Note that we can have missing row values below and to right + #e.g + #a + #a b + #a + proc transpose_matrix {matrix_rows} { + set mcmd [struct::matrix] + #serialization format: numcols numrows rowlist + set widest [tcl::mathfunc::max {*}[lmap v $matrix_rows {llength $v}]] + $mcmd deserialize [list [llength $matrix_rows] $widest $matrix_rows] + $mcmd transpose + set result [lindex [$mcmd serialize] 2] ;#strip off dimensions + $mcmd destroy + return $result + } + + set objname [namespace current]::matrixchain + if {$objname ni [info commands $objname]} { + oo::class create matrixchain { + variable mcmd + constructor {matrixcommand} { + puts "wrapping $matrixcommand with [self]" + set mcmd $matrixcommand + } + destructor { + puts "matrixchain destructor called for [self] (wrapping $mcmd)" + $mcmd destroy + } + method unknown {args} { + if {[llength $args]} { + switch -- [lindex $args 0] { + add - delete - insert - transpose - sort - set - swap { + $mcmd {*}$args + return [self] ;#result is the wrapper object for further chaining in pipelines + } + default { + tailcall $mcmd {*}$args + } + } + } else { + #will error.. but we should pass that on + tailcall $mcmd + } + } + } + } + + #review + #how do we stop matrix pipelines from leaving commands around? i.e how do we call destroy on the matrixchain wrapper if not explicitly? + #Perhaps will be solved by: Tip 550: Garbage collection for TclOO + #Theoretically this should allow tidy up of objects created within the pipeline automatically + #If the object name is placed in the pipeline variable dict then it should survive across segment apply scripts and only go out of scope at the end. + proc matrix_command_from_rows {matrix_rows} { + set mcmd [struct::matrix] + set widest [tcl::mathfunc::max {*}[lmap v $matrix_rows {llength $v}]] + $mcmd deserialize [list [llength $matrix_rows] $widest $matrix_rows] + #return $mcmd + set wrapper [punk::matrixchain new $mcmd] + } + + #-------------------------------------------------- + + proc list_filter_cond {itemcond listval} { + set filtered_list [list] + set binding {} + if {[info level] == 1} { + #up 1 is global + set get_vars [list ::info vars] + } else { + set get_vars [list ::info locals] + } + set vars [uplevel 1 {*}$get_vars] + set posn [lsearch $vars item] + set vars [lreplace $vars $posn $posn] + foreach v $vars { + upvar 1 $v var + if {(![array exists var]) && [info exists var]} { + lappend binding [list $v $var] ;#values captured as defaults for apply args. + } + } + #lappend binding [list item $args] + + #puts stderr "binding: [join $binding \n]" + #apply [list $binding $pipescript [uplevel 1 ::namespace current]] + foreach item $listval { + set bindlist [list {*}$binding [list item $item]] + if {[apply [list $bindlist $itemcond [uplevel 1 ::namespace current]] ]} { + lappend filtered_list $item + } + } + return $filtered_list + } + + + proc ls {args} { + if {![llength $args]} { + set args [list [pwd]] + } + if {[llength $args] ==1} { + return [glob -nocomplain -tails -dir [lindex $args 0] *] + } else { + set result [dict create] + foreach a $args { + set k [file normalize $a] + set contents [glob -nocomplain -tails -dir $a *] + dict set result $k $contents + } + return $result + } + } + + + + #linelistraw is essentially split $text \n so is only really of use for pipelines, where the argument order is more convenient + #like linelist - but keeps leading and trailing empty lines + #single \n produces {} {} + #the result can be joined to reform the arg if a single arg supplied + # + proc linelistraw {args} { + set linelist [list] + foreach {a} $args { + set nlsplit [split $a \n] + lappend linelist {*}$nlsplit + } + #return [split $text \n] + return $linelist + } + proc linelist1 {args} { + set linelist [list] + foreach {a} $args { + set nlsplit [split $a \n] + set start 0 + set end "end" + + if {[lindex $nlsplit 0] eq ""} { + set start 1 + } + if {[lindex $nlsplit end] eq ""} { + set end "end-1" + } + set alist [lrange $nlsplit $start $end] + lappend linelist {*}$alist + } + return $linelist + } + + + #An implementation of a notoriously controversial metric. + proc LOC {args} { + set argspecs [subst { + -dir -default "\uFFFF" + -exclude_dupfiles -default 1 -type boolean + -exclude_punctlines -default 1 -type boolean + -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } + }] + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + + # -- --- --- --- --- --- + set opt_dir [dict get $opts -dir] + if {$opt_dir eq "\uFFFF"} { + set opt_dir [pwd] ;#pwd can take over a ms on windows in a not terribly deep path even with SSDs - so as a general rule we don't use it in the original defaults list + } + # -- --- --- --- --- --- + set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] + set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars + set opt_punctchars [dict get $opts -punctchars] + # -- --- --- --- --- --- + + + set filepaths [punk::path::treefilenames -dir $opt_dir {*}$searchspecs] + set loc 0 + set dupfileloc 0 + set seentails [list] + set dupfilecount 0 + set extensions [list] + set purepunctlines 0 + foreach fpath $filepaths { + set isdupfile 0 + set floc 0 + set fpurepunctlines 0 + set ext [file extension $fpath] + if {$ext ni $extensions} { + lappend extensions $ext + } + if {!$opt_exclude_punctlines} { + set floc [llength [linelist -line {trimright} -block {trimall} [fcat $fpath]]] + } else { + set lines [linelist -line {trimright} -block {trimall} [fcat $fpath]] + set mapawaypunctuation [list] + foreach p $opt_punctchars empty {} { + lappend mapawaypunctuation $p $empty + } + foreach ln $lines { + if {[string length [string trim [string map $mapawaypunctuation $ln]]] > 0} { + incr floc + } else { + incr fpurepunctlines + } + } + } + if {[file tail $fpath] in $seentails} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + } + if {!$isdupfile || ($isdupfile && !$opt_exclude_dupfiles)} { + incr loc $floc + incr purepunctlines $fpurepunctlines + } + + lappend seentails [file tail $fpath] + } + if {$opt_exclude_punctlines} { + return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions purepunctuationlines $purepunctlines] + } + return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions] + } + + + + #!!!todo fix - linedict is unfinished and non-functioning + #linedict based on indents + proc linedict {args} { + set data [lindex $args 0] + set opts [lrange $args 1 end] ;#todo + set nlsplit [split $data \n] + set rootindent -1 + set stepindent -1 + + #set wordlike_parts [regexp -inline -all {\S+} $lastitem] + set d [dict create] + set keys [list] + set i 1 + set firstkeyline "N/A" + set firststepline "N/A" + foreach ln $nlsplit { + if {![string length [string trim $ln]]} { + incr i + continue + } + set is_rootkey 0 + regexp {(\s*)(.*)} $ln _ space linedata + puts stderr ">>line:'$ln' [string length $space] $linedata" + set this_indent [string length $space] + if {$rootindent < 0} { + set firstkeyline $ln + set rootindent $this_indent + } + if {$this_indent == $rootindent} { + set is_rootkey 1 + } + if {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline" + } + if {$is_rootkey} { + dict set d $linedata {} + lappend keys $linedata + } else { + if {$stepindent < 0} { + set stepindent $this_indent + set firststepline $ln + } + if {$this_indent == $stepindent} { + dict set d [lindex $keys end] $ln + } else { + if {($this_indent % $stepindent) != 0} { + error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline" + } + + #todo fix! + set parentkey [lindex $keys end] + lappend keys [list $parentkey $ln] + set oldval [dict get $d $parentkey] + if {[string length $oldval]} { + set new [dict create $oldval $ln] + } else { + dict set d $parentkey $ln + } + + } + } + incr i + } + return $d + } + proc dictline {d} { + puts stderr "unimplemented" + set lines [list] + + return $lines + } + + + proc ooinspect {obj} { + set obj [uplevel 1 [list namespace which -command $obj]] + set isa [lmap type {object class metaclass} { + if {![info object isa $type $obj]} continue + set type + }] + foreach tp $isa { + switch -- $tp { + class { + lappend info {class superclasses} {class mixins} {class filters} + lappend info {class methods} {class methods} + lappend info {class variables} {class variables} + } + object { + lappend info {object class} {object mixins} {object filters} + lappend info {object methods} {object methods} + lappend info {object variables} {object variables} + lappend info {object namespace} {object vars} ;#{object commands} + } + } + } + + set result [dict create isa $isa] + foreach args $info { + dict set result $args [info {*}$args $obj] + foreach opt {-private -all} { + catch { + dict set result [list {*}$args $opt] [info {*}$args $obj $opt] + } + } + } + dict filter $result value {?*} + } + + + #pipeline inspect + #e.g + #= {a z c} |> inspect -label input_dict |> lsort |> {inspect $data} + proc inspect {args} { + set defaults [list -label "" -limit 20 -channel stderr -showcount 1 -ansi 1] + set flags [list] + set endoptsposn [lsearch $args --] ;#first -- if data expected to contain --, then should always be called with --. e.g inspect -- + if {$endoptsposn >= 0} { + set flags [lrange $args 0 $endoptsposn-1] + set pipeargs [lrange $args $endoptsposn+1 end] + } else { + #no explicit end of opts marker + #last trailing elements of args after taking *known* -tag v pairs is the value to inspect + for {set i 0} {$i < [llength $args]} {incr i} { + set k [lindex $args $i] + if {$k in [dict keys $defaults]} { + lappend flags {*}[lrange $args $i $i+1] + incr i + } else { + #first unrecognised option represents end of flags + break + } + } + set pipeargs [lrange $args $i end] + } + foreach {k v} $flags { + if {$k ni [dict keys $defaults]} { + error "inspect: unknown option $k. Known options: [dict keys $defaults]. If data contains flaglike elements, consider calling with end-of-opts marker. e.g inspect --" + } + } + set opts [dict merge $defaults $flags] + # -- --- --- --- --- + set label [dict get $opts -label] + set channel [dict get $opts -channel] + set showcount [dict get $opts -showcount] + if {[string length $label]} { + set label "${label}: " + } + set limit [dict get $opts -limit] + set opt_ansi [dict get $opts -ansi] + switch -- [string tolower $opt_ansi] { + 0 - 1 - 2 {} + view {set opt_ansi 2} + default { + error "inspect -ansi 0|1|2|view - received -ansi $opt_ansi" + } + } + # -- --- --- --- --- + + set more "" + if {[llength $pipeargs] == 1} { + #usual case is data as a single element + set val [lindex $pipeargs 0] + set count 1 + } else { + #but the pipeline segment could have an insertion-pattern ending in * + set val $pipeargs + set count [llength $pipeargs] + } + switch -- [string tolower $channel] { + nul - null - /dev/null { + return $val + } + } + set displayval $val ;#default - may be overridden based on -limit + + if {$count > 1} { + #val is a list + set llen [llength $val] + if {$limit > 0 && ($limit < $llen)} { + set displayval [lrange $val 0 $limit-1] + if {$llen > $limit} { + set more "..." + } + } + } else { + #not a valid tcl list - limit by lines + if {$limit > 0} { + set rawlines [split $val \n] + set llen [llength $rawlines] + set displaylines [lrange $rawlines 0 $limit-1] + set displayval [join $displaylines "\n"] + if {$llen > $limit} { + set more "\n..." + } + } + + } + if {$showcount} { + set displaycount "[a purple bold]($count)[a] " + if {$showcount} { + set countspace [expr {[string length $count] + 3}] ;#lhs margin size of count number plus brackets and one space + set margin [string repeat " " $countspace] + set displayval [string map [list \r "" \n "\n$margin"] $displayval] + } + } else { + set displaycount "" + } + if {$opt_ansi == 0} { + set displayval [punk::ansi::ansistrip $displayval] + } elseif {$opt_ansi == 2} { + set displayval [ansistring VIEW $displayval] + } + if {![string length $more]} { + puts $channel "$displaycount$label[a green bold]$displayval[a]" + } else { + puts $channel "$displaycount$label[a green bold]$displayval[a yellow bold]$more[a]" + } + return $val + } + + + + #return list of {chan chunk} elements + proc help_chunks {args} { + set chunks [list] + set linesep [string repeat - 76] + set mascotblock "" + catch { + package require patternpunk + #lappend chunks [list stderr [>punk . rhs]] + append mascotblock [textblock::frame [>punk . banner -title "Punk Shell" -left Tcl -right [package provide Tcl]]] + } + + set topic [lindex $args end] + set argopts [lrange $args 0 end-1] + + + set text "" + append text "Punk core navigation commands:\n" + + #todo - load from source code annotation? + set cmdinfo [list] + lappend cmdinfo [list help "This help. To see available subitems type: help topics"] + lappend cmdinfo [list dev "(ensemble command to make new projects/modules and to generate docs)"] + lappend cmdinfo [list a? "view ANSI colours\n e.g a? web"] + lappend cmdinfo [list ./ "view/change directory"] + lappend cmdinfo [list ../ "go up one directory"] + lappend cmdinfo [list ./new "make new directory and switch to it"] + lappend cmdinfo [list n/ "view/change namespace (accepts ns path globs e.g **::*get* to match comands at any level )"] + lappend cmdinfo [list n// "view/change namespace (with command listing)"] + lappend cmdinfo [list nn/ "go up one namespace"] + lappend cmdinfo [list n/new "make child namespace and switch to it"] + + set cmds [lsearch -all -inline -index 0 -subindices $cmdinfo *] + set descr [lsearch -all -inline -index 1 -subindices $cmdinfo *] + set widest1 [tcl::mathfunc::max {*}[lmap v $cmds {string length $v}]] + set widest2 [tcl::mathfunc::max {*}[lmap v $descr {string length $v}]] + set t [textblock::class::table new -show_seps 0] + foreach c $cmds d $descr { + #append text " [overtype::left $col1 $c][overtype::left $col2 $d]" \n + $t add_row [list $c $d] + } + set widest1 [$t column_datawidth 0] + $t configure_column 0 -minwidth [expr {$widest1 + 2}] + set widest2 [$t column_datawidth 1] + $t configure_column 1 -minwidth [expr {$widest2 + 1}] + append text [$t print] + + + set warningblock "" + + if {[catch {package require textblock} errM]} { + set introblock $mascotblock + append introblock \n $text + append warningblock \n "WARNING: textblock package couldn't be loaded. Side-by-side display not available" + + } else { + set introblock [textblock::join -- " " \n$mascotblock " " $text] + } + + + lappend chunks [list stdout $introblock] + + + if {$topic in [list tcl]} { + if {[punk::lib::system::has_script_var_bug]} { + append warningblock \n "minor warning: punk::lib::system::has_script_var_bug returned true! (string rep for list variable in script generated when script changed)" + } + if {[punk::lib::system::has_safeinterp_compile_bug]} { + set indent " " + append warningblock \n "[a+ web-red]warning: punk::lib::system::has_safeinterp_compile_bug returned true!" \n + append warningblock "${indent}(ensemble commands not compiled in safe interps - heavy performance impact in safe interps)" \n + append warningblock "${indent}see https://core.tcl-lang.org/tcl/tktview/1095bf7f75" + append warningblock [a] + } + } + + set text "" + if {$topic in [list env environment]} { + #todo - move to punk::config? + upvar ::punk::config::punk_env_vars_config punkenv_config + upvar ::punk::config::other_env_vars_config otherenv_config + + set known_punk [dict keys $punkenv_config] + set known_other [dict keys $otherenv_config] + append text \n + set usetable 1 + if {$usetable} { + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + if {"windows" eq $::tcl_platform(platform)} { + #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. + #The Tcl ::env array is linked to the underlying process view of the environment + #- but info exists ::env(var) can misreport as true if it has been deleted by setting to empty string rather than using unset. + #an 'array get' will resynchronise. + #Even if an env variable didn't exist before - setting it to empty string can get it to this inconsistent state. + array get ::env + } + #do an array read on ::env + foreach {v vinfo} $punkenv_config { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + set help "" + if {[dict exists $vinfo help]} { + set help [dict get $vinfo help] + } + $t add_row [list $v $c2 $help] + } + $t configure_column 0 -headers [list "Punk environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} + + set punktable [$t print] + $t destroy + + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + foreach {v vinfo} $otherenv_config { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + $t add_row [list $v $c2] + } + $t configure_column 0 -headers [list "Other environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} + + set othertable [$t print] + $t destroy + append text [textblock::join -- $punktable " " $othertable]\n + } else { + + append text $linesep\n + append text "punk environment vars:\n" + append text $linesep\n + set col1 [string repeat " " 25] + set col2 [string repeat " " 50] + foreach v $known_punk { + set c1 [overtype::left $col1 $v] + if {[info exists ::env($v)]} { + set c2 [overtype::left $col2 [set ::env($v)] + } else { + set c2 [overtype::right $col2 "(NOT SET)"] + } + append text "$c1 $c2\n" + } + append text $linesep\n + } + + lappend chunks [list stdout $text] + } + + if {$topic in [list console terminal]} { + lappend cstring_tests [dict create\ + type "PM "\ + msg "PRIVACY MESSAGE"\ + f7 punk::ansi::controlstring_PM\ + f7desc "7bit ESC ^"\ + f8 punk::ansi::controlstring_PM8\ + f8desc "8bit \\x9e"\ + ] + lappend cstring_tests [dict create\ + type SOS\ + msg "STRING"\ + f7 punk::ansi::controlstring_SOS\ + f7desc "7bit ESC X"\ + f8 punk::ansi::controlstring_SOS8\ + f8desc "8bit \\x98"\ + ] + lappend cstring_tests [dict create\ + type APC\ + msg "APPLICATION PROGRAM COMMAND"\ + f7 punk::ansi::controlstring_APC\ + f7desc "7bit ESC _"\ + f8 punk::ansi::controlstring_APC8\ + f8desc "8bit \\x9f"\ + ] + + foreach test $cstring_tests { + set m [[dict get $test f7] [dict get $test msg]] + set hidden_width_m [punk::console::test_char_width $m] + set m8 [[dict get $test f8] [dict get $test msg]] + set hidden_width_m8 [punk::console::test_char_width $m8] + if {$hidden_width_m != 0 || $hidden_width_m8 != 0} { + if {$hidden_width_m == 0} { + set d "[a+ green bold][dict get $test f7desc] [a red]${m}[a]" + } else { + set d "[a+ yellow bold][dict get $test f7desc] [a red]$m[a]" + } + if {$hidden_width_m8 == 0} { + set d8 "[a+ green ][dict get $test f8desc] [a red]$m8[a]" + } else { + set d8 "[a+ yellow bold][dict get $test f8desc] [a red]$m8[a]" + } + append warningblock \n "WARNING: terminal doesn't hide all [dict get $test type] control strings: $d $d8" + } + } + } + + lappend chunks [list stderr $warningblock] + if {$topic in [list topics help]} { + set text "" + set topics [dict create\ + "topics|help" "List help topics"\ + "tcl" "Tcl version warnings"\ + "env|environment" "punkshell environment vars"\ + "console|terminal" "Some console behaviour tests and warnings"\ + ] + + set t [textblock::class::table new -show_seps 0] + $t add_column -headers [list "Topic"] + $t add_column + foreach {k v} $topics { + $t add_row [list $k $v] + } + set widest0 [$t column_datawidth 0] + $t configure_column 0 -minwidth [expr {$widest0 + 4}] + append text \n[$t print] + + lappend chunks [list stdout $text] + } + + return $chunks + } + proc help {args} { + set chunks [help_chunks {*}$args] + foreach chunk $chunks { + lassign $chunk chan text + puts -nonewline $chan $text + } + } + proc mode {{raw_or_line query}} { + package require punk::console + tailcall ::punk::console::mode $raw_or_line + } + + #this hides windows cmd's mode command - probably no big deal - anyone who needs it will know how to exec it. + interp alias {} mode {} punk::mode + + + #NOTE: an alias may match in a namespace - but not have a corresponding command that matches that name (alias renamed) + proc aliases {{glob *}} { + set ns [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command + set ns_mapped [string map {:: \uFFFF} $ns] + #puts stderr "aliases ns: $ns_mapped" + set segments [split $ns_mapped \uFFFF] ;#include empty string before leading :: + if {![string length [lindex $segments end]]} { + #special case for :: only include leading segment rather thatn {} {} + set segments [lrange $segments 0 end-1] + } + set segcount [llength $segments] ;#only match number of segments matching current ns + + + set all_aliases [interp aliases {}] + set matched [list] + foreach a $all_aliases { + #normalize with leading :: + if {![string match ::* $a]} { + set abs ::$a + } else { + set abs $a + } + + set asegs [split [string map {:: \uFFFF} $abs] \uFFFF] + set acount [llength $asegs] + #puts "alias $abs acount:$acount asegs:$asegs segcount:$segcount segments: $segments" + if {[expr {$acount - 1}] == $segcount} { + if {[lrange $asegs 0 end-1] eq $segments} { + if {[string match $glob [lindex $asegs end]]} { + #report this alias in the current namespace - even though there may be no matching command + lappend matched $a ;#add raw alias token which may or may not have leading :: + } + } + } + } + #set matched_abs [lsearch -all -inline $all_aliases $glob] + + return $matched + } + + proc alias {{aliasorglob ""} args} { + set nsthis [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command + if {[llength $args]} { + if {$aliasorglob in [interp aliases ""]} { + set existing [interp alias "" $aliasorglob] + puts stderr "Overwriting existing alias $aliasorglob -> $existing with $aliasorglob -> $args (in current session only)" + } + if {([llength $args] ==1) && [string trim [lindex $args 0]] eq ""} { + #use empty string/whitespace as intention to delete alias + return [interp alias "" $aliasorglob ""] + } + return [interp alias "" $aliasorglob "" {*}$args] + } else { + if {![string length $aliasorglob]} { + set aliaslist [punk::aliases] + puts -nonewline stderr $aliaslist + return + } + #we need to first check for exact match of alias that happens to have glob chars i.e the supplied aliasorglob looks like a glob but is actually directly an alias + set target [interp alias "" $aliasorglob] + if {[llength $target]} { + return $target + } + + if {([string first "*" $aliasorglob] >= 0) || ([string first "?" $aliasorglob] >= 0)} { + set aliaslist [punk::aliases $aliasorglob] + puts -nonewline stderr $aliaslist + return + } + return [list] + } + } + + #pipeline-toys - put in lib/scriptlib? + ##geometric mean + #alias gmean .=> llength |> expr 1.0 / |e> .=i>* tcl::mathop::* |> .=>1,e>3 expr ** {::tcl::tm::add {*}$data; set path} |> inspect -label added_to_module_path <0/#| + + + + + + #interp alias {} c {} clear ;#external executable 'clear' may not always be available + #todo - review + interp alias {} clear {} ::punk::reset + interp alias {} c {} ::punk::reset + proc reset {} { + if {[llength [info commands ::punk::repl::reset_terminal]]} { + #punk::repl::reset_terminal notifies prompt system of reset + punk::repl::reset_terminal + } else { + puts -nonewline stdout [punk::ansi::reset] + } + } + + + + #fileutil::cat except with checking for windows illegal path names (when on windows platform) + interp alias {} fcat {} punk::mix::util::fcat + + #---------------------------------------------- + interp alias {} linelistraw {} punk::linelistraw + + # 'path' collides with kettle path in kettle::doc function - todo - patch kettle? + interp alias {} PATH {} punk::path + + interp alias {} path_list {} punk::path_list + interp alias {} list_filter_cond {} punk::list_filter_cond + + + interp alias {} inspect {} punk::inspect + interp alias {} ooinspect {} punk::ooinspect + + interp alias {} linedict {} punk::linedict + interp alias {} dictline {} punk::dictline + + #todo - pipepure - evaluate pipeline in a slave interp without commands that have side-effects. (safe interp?) + interp alias {} % {} punk::% + interp alias {} pipeswitch {} punk::pipeswitch + interp alias {} pipeswitchc {} punk::pipeswitchc ;#closure version - more correct + interp alias {} pipecase {} punk::pipecase + interp alias {} pipematch {} punk::pipematch + interp alias {} ispipematch {} punk::ispipematch + interp alias {} pipenomatchvar {} punk::pipenomatchvar + interp alias {} pipedata {} punk::pipedata + interp alias {} pipeset {} punk::pipeset + interp alias {} pipealias {} punk::pipealias + interp alias {} listset {} punk::listset ;#identical to pipeset + + + #non-core aliases + interp alias {} is_list_all_in_list {} punk::lib::is_list_all_in_list + interp alias {} is_list_all_ni_list {} punk::libis_list_all_ni_list + + + + #interp alias {} = {} ::punk::pipeline = "" "" + #interp alias {} = {} ::punk::match_assign "" "" + interp alias {} .= {} ::punk::pipeline .= "" "" + #proc .= {args} { + # #uplevel 1 [list ::punk::pipeline .= "" "" {*}$args] + # tailcall ::punk::pipeline .= "" "" {*}$args + #} + + + interp alias {} rep {} ::tcl::unsupported::representation + interp alias {} dis {} ::tcl::unsupported::disassemble + + + + # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion + interp alias {} l {} sh_runout -n ls -A ;#plain text listing + #interp alias {} ls {} sh_runout -n ls -AF --color=always + interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less + #note that shell globbing with * won't work on unix systems when using unknown/exec + interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) + interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. + # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? + #interp alias {} lw {} ls -aFv --color=always + + interp alias {} dir {} shellrun::runconsole dir + + # punk::nav::fs + package require punk::nav::fs + interp alias {} ./ {} punk::nav::fs::d/ + interp alias {} ../ {} punk::nav::fs::dd/ + interp alias {} d/ {} punk::nav::fs::d/ + interp alias {} dd/ {} punk::nav::fs::dd/ + + interp alias {} vwd {} punk::nav::fs::vwd ;#return punk::nav::fs::VIRTUAL_CWD - and report to stderr pwd if different + interp alias {} dirlist {} punk::nav::fs::dirlist + interp alias {} dirfiles {} punk::nav::fs::dirfiles + interp alias {} dirfiles_dict {} punk::nav::fs::dirfiles_dict + + interp alias {} ./new {} punk::nav::fs::d/new + interp alias {} d/new {} punk::nav::fs::d/new + interp alias {} ./~ {} punk::nav::fs::d/~ + interp alias {} d/~ {} punk::nav::fs::d/~ + interp alias "" x/ "" punk::nav::fs::x/ + + + if {$::tcl_platform(platform) eq "windows"} { + set has_powershell 1 + interp alias {} dl {} dir /q + interp alias {} dw {} dir /W/D + } else { + #todo - natsorted equivalent + #interp alias {} dl {} + interp alias {} dl {} puts stderr "not implemented" + interp alias {} dw {} puts stderr "not implemented" + #todo - powershell detection on other platforms + set has_powershell 0 + } + if {$has_powershell} { + #see also powershell runspaces etc: + # powershell runspaces e.g $rs=[RunspaceFactory]::CreateRunspace() + # $ps = [Powershell]::Create() + + interp alias {} ps {} exec >@stdout pwsh -nolo -nop -c + interp alias {} psx {} runx -n pwsh -nop -nolo -c + interp alias {} psr {} run -n pwsh -nop -nolo -c + interp alias {} psout {} runout -n pwsh -nop -nolo -c + interp alias {} pserr {} runerr -n pwsh -nop -nolo -c + interp alias {} psls {} shellrun::runconsole pwsh -nop -nolo -c ls + interp alias {} psps {} shellrun::runconsole pwsh -nop -nolo -c ps + } else { + set ps_missing "powershell missing (powershell is open source and can be installed on windows and most unix-like platforms)" + interp alias {} ps {} puts stderr $ps_missing + interp alias {} psx {} puts stderr $ps_missing + interp alias {} psr {} puts stderr $ps_missing + interp alias {} psout {} puts stderr $ps_missing + interp alias {} pserr {} puts stderr $ps_missing + interp alias {} psls {} puts stderr $ps_missing + interp alias {} psps {} puts stderr $ps_missing + } + proc psencode {cmdline} { + + } + proc psdecode {encodedcmd} { + + } + + proc repl {startstop} { + switch -- $startstop { + stop { + if {[punk::repl::codethread::is_running]} { + puts stdout "Attempting repl stop. Try ctrl-c or exit command to leave interpreter" + set ::repl::done 1 + } + } + start { + if {[punk::repl::codethread::is_running]} { + repl::start stdin + } + } + default { + error "repl unknown action '$startstop' - must be start or stop" + } + } + } + +} + + +# -- --- --- --- +#Load decks. commandset packages are not loaded until the deck is called. +# -- --- --- --- +package require punk::mod +#punk::mod::cli set_alias pmod +punk::mod::cli set_alias app + +#todo - change to punk::dev +package require punk::mix +punk::mix::cli set_alias dev +punk::mix::cli set_alias deck ;#deprecate! + +#todo - add punk::deck for managing cli modules and commandsets + +package require punkcheck::cli +punkcheck::cli set_alias pcheck +punkcheck::cli set_alias punkcheck +# -- --- --- --- + +package provide punk [namespace eval punk { + #FUNCTL + variable version + set version 0.1 +}] + + + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm new file mode 100644 index 00000000..83c02d0b --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -0,0 +1,272 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.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) 2024 +# +# @@ Meta Begin +# Application punk::aliascore 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::aliascore 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::aliascore] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::aliascore +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::aliascore +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::aliascore::class { +# #*** !doctools +# #[subsection {Namespace punk::aliascore::class}] +# #[para] class definitions +# if {[info commands [namespace current]::interface_sample1] eq ""} { +# #*** !doctools +# #[list_begin enumerated] +# +# # oo::class create interface_sample1 { +# # #*** !doctools +# # #[enum] CLASS [class interface_sample1] +# # #[list_begin definitions] +# +# # method test {arg1} { +# # #*** !doctools +# # #[call class::interface_sample1 [method test] [arg arg1]] +# # #[para] test method +# # puts "test: $arg1" +# # } +# +# # #*** !doctools +# # #[list_end] [comment {-- end definitions interface_sample1}] +# # } +# +# #*** !doctools +# #[list_end] [comment {--- end class enumeration ---}] +# } +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::aliascore { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + variable aliases + #use absolute ns ie must be prefixed with :: + #single element commands are imported if source command already exists, otherwise aliased. multi element commands are aliased + + #functions must be in export list of their source namespace + set aliases [tcl::dict::create\ + tstr ::punk::lib::tstr\ + list_as_lines ::punk::lib::list_as_lines\ + lines_as_list ::punk::lib::lines_as_list\ + linelist ::punk::lib::linelist\ + linesort ::punk::lib::linesort\ + pdict ::punk::lib::pdict\ + plist {::punk::lib::pdict -roottype list}\ + showlist {::punk::lib::showdict -roottype list}\ + showdict ::punk::lib::showdict\ + ansistrip ::punk::ansi::ansistrip\ + stripansi ::punk::ansi::ansistrip\ + ansiwrap ::punk::ansi::ansiwrap\ + colour ::punk::console::colour\ + ansi ::punk::console::ansi\ + color ::punk::console::colour\ + a+ ::punk::console::code_a+\ + A+ {::punk::console::code_a+ forcecolour}\ + a ::punk::console::code_a\ + A {::punk::console::code_a forcecolour}\ + a? ::punk::console::code_a?\ + A? {::punk::console::code_a? forcecolor}\ + smcup ::punk::console::enable_alt_screen\ + rmcup ::punk::console::disable_alt_screen\ + ] + + #*** !doctools + #[subsection {Namespace punk::aliascore}] + #[para] Core API functions for punk::aliascore + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + #todo - options as to whether we should raise an error if collisions found, undo aliases etc? + proc init {args} { + set defaults {-force 0} + set opts [dict merge $defaults $args] + set opt_force [dict get $opts -force] + + variable aliases + if {!$opt_force} { + set existing [list] + set conflicts [list] + foreach {a cmd} $aliases { + if {[tcl::info::commands ::$a] ne ""} { + lappend existing $a + if {[llength $cmd] > 1} { + #use alias mechanism + set existing_target [interp alias "" $a] + } else { + #using namespace import + #check origin + set existing_target [tcl::namespace::origin $cmd] + } + if {$existing_target ne $cmd} { + #command exists in global ns but doesn't match our defined aliases/imports + lappend conflicts $a + } + } + } + if {[llength $conflicts]} { + error "punk::aliascore::init declined to create any aliases or imports because -force == 0 and conflicts found:$conflicts" + } + } + set tempns ::temp_[info cmdcount] ;#temp ns for renames + dict for {a cmd} $aliases { + #puts "aliascore $a -> $cmd" + if {[llength $cmd] > 1} { + interp alias {} $a {} {*}$cmd + } else { + if {[tcl::info::commands $cmd] ne ""} { + #todo - ensure exported? noclobber? + if {[tcl::namespace::tail $a] eq [tcl::namespace::tail $cmd]} { + #puts stderr "importing $cmd" + tcl::namespace::eval :: [list namespace import $cmd] + } else { + #target command name differs from exported name + #e.g stripansi -> punk::ansi::ansistrip + #import and rename + #puts stderr "importing $cmd (with rename to ::$a)" + tcl::namespace::eval $tempns [list namespace import $cmd] + catch {rename ${tempns}::[namespace tail $cmd] ::$a} + } + } else { + interp alias {} $a {} {*}$cmd + } + } + } + #tcl::namespace::delete $tempns + return [dict keys $aliases] + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::aliascore ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#interp alias {} list_as_lines {} punk::lib::list_as_lines +#interp alias {} lines_as_list {} punk::lib::lines_as_list +#interp alias {} ansistrip {} punk::ansi::ansistrip ;#review +#interp alias {} linelist {} punk::lib::linelist ;#critical for = assignment features +#interp alias {} linesort {} punk::lib::linesort + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::aliascore::lib { + namespace export {[a-z]*} ;# Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace punk::aliascore::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::aliascore::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval punk::aliascore::system { + #*** !doctools + #[subsection {Namespace punk::aliascore::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::aliascore [namespace eval punk::aliascore { + variable pkg punk::aliascore + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm new file mode 100644 index 00000000..206b560b --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm @@ -0,0 +1,475 @@ + +tcl::namespace::eval punk::config { + variable loaded + variable startup ;#include env overrides + variable running + variable punk_env_vars + variable other_env_vars + + variable vars + + namespace export {[a-z]*} + + #todo - XDG_DATA_HOME etc + #https://specifications.freedesktop.org/basedir-spec/latest/ + # see also: http://hiphish.github.io/blog/2020/08/30/dotfiles-were-a-mistake/ + + proc init {} { + variable defaults + variable startup + variable running + variable punk_env_vars + variable punk_env_vars_config + variable other_env_vars + variable other_env_vars_config + + set exename "" + catch { + #catch for safe interps + #safe base will return empty string, ordinary safe interp will raise error + set exename [tcl::info::nameofexecutable] + } + if {$exename ne ""} { + set exefolder [file dirname $exename] + #default file logs to logs folder at same level as exe if writable, or empty string + set log_folder [file normalize $exefolder/../logs] + #tcl::dict::set startup scriptlib $exefolder/scriptlib + #tcl::dict::set startup apps $exefolder/../../punkapps + + #todo - use punk main.tcl location instead - exefolder doesn't work if system tclsh used etc + set default_scriptlib $exefolder/scriptlib + set default_apps $exefolder/../../punkapps + if {[file isdirectory $log_folder] && [file writable $log_folder]} { + #tcl::dict::set startup logfile_stdout $log_folder/repl-exec-stdout.txt + #tcl::dict::set startup logfile_stderr $log_folder/repl-exec-stderr.txt + set default_logfile_stdout $log_folder/repl-exec-stdout.txt + set default_logfile_stderr $log_folder/repl-exec-stderr.txt + } else { + set default_logfile_stdout "" + set default_logfile_stderr "" + } + } else { + #probably a safe interp - which cannot access info nameofexecutable even if access given to the location via punk::island + #review - todo? + #tcl::dict::set startup scriptlib "" + #tcl::dict::set startup apps "" + set default_scriptlib "" + set default_apps "" + set default_logfile_stdout "" + set default_logfile_stderr "" + } + + # auto_exec_mechanism ;#whether to use exec instead of experimental shellfilter::run + + #optional channel transforms on stdout/stderr. + #can sometimes be useful to distinguish eventloop stdout/stderr writes compared to those triggered directly from repl commands + #If no distinction necessary - should use default_color_ + #The counterpart: default_color__repl is a transform that is added and removed with each repl evaluation. + #startup color_stdout - parameters as suitable for punk::ansi::a+ (test with 'punk::ansi::a?') e.g "cyan bold" ;#not a good idea to default + set default_color_stdout brightwhite ;#stdout colour including background calls (after etc) + set default_color_stdout_repl "" ;#stdout colour applied during direct repl call only + #This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful. + #set default_color_stderr "red bold" + #set default_color_stderr "web-lightsalmon" + set default_color_stderr yellow ;#limit to basic colours for wider terminal support. yellow = term-olive + set default_color_stderr_repl "" ;#during repl call only + + set homedir "" + if {[catch { + #depending on which build of tcl - some safe interps prior to bugfix https://core.tcl-lang.org/tcl/info/3aa487993f will return a homedir value in an unmodified safe interp + #other 'safe' interps may have explicitly made this available - we shouldn't override that decision here using interp issafe so we can't compensate for versions which shouldn't really be returning this in the safe interp + set homedir [file home] + } errM]} { + #tcl 8.6 doesn't have file home.. try again + if {[info exists ::env(HOME)]} { + set homedir $::env(HOME) + } + } + + + # per user xdg vars + # --- + set default_xdg_config_home "" ;#config data - portable + set default_xdg_data_home "" ;#data the user likely to want to be portable + set default_xdg_cache_home "" ;#local cache + set default_xdg_state_home "" ;#persistent user data such as logs, but not as important or as portable as those in xdg_data_home + # --- + set default_xdg_data_dirs "" ;#non-user specific + #xdg_config_dirs ? + #xdg_runtime_dir ? + + + #review. we are assuming if we can't get a home dir - then all the xdg vars including xdg_data_dirs aren't likely to be useful (as presumably filesystem access is absent) + #(safe interp generally won't have access to ::env either) + #This coupling doesn't necessarily hold - its possible the relevant env vars were copied to a safe interp - although that would be a policy that would make disabling 'info home' inconsistent. + if {$homedir ne ""} { + if {"windows" eq $::tcl_platform(platform)} { + #as much as I'd prefer to use ~/.local/share and ~/.config to keep them more consistent with unixlike platforms - the vast majority of apps put them where microsoft wants them. + #we have a choice of LOCALAPPDATA vs APPDATA (local to machine vs potentially roaming/redirected in a corporate environment) + #using the roaming location should not impact users who aren't using a domain controller but is potentially much more convenient for those who do. + if {[info exists ::env(APPDATA)]} { + set default_xdg_config_home $::env(APPDATA) + set default_xdg_data_home $::env(APPDATA) + } + + #The xdg_cache_home should be kept local + if {[info exists ::env(LOCALAPPDATA)]} { + set default_xdg_cache_home $::env(LOCALAPPDATA) + set default_xdg_state_home $::env(LOCALAPPDATA) + } + + if {[info exists ::env(PROGRAMDATA)]} { + #- equiv env(ALLUSERSPROFILE) ? + set default_xdg_data_dirs $::env(PROGRAMDATA) + } + + } else { + #follow defaults as specified on freedesktop.org e.g https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html + set default_xdg_config_home [file join $homedir .config] + set default_xdg_data_home [file join $homedir .local share] + set default_xdg_cache_home [file join $homedir .cache] + set default_xdg_state_home [file join $homedir .local state] + set default_xdg_data_dirs /usr/local/share + } + } + + set defaults [dict create\ + apps $default_apps\ + config ""\ + configset ".punkshell"\ + scriptlib $default_scriptlib\ + color_stdout $default_color_stdout\ + color_stdout_repl $default_color_stdout_repl\ + color_stderr $default_color_stderr\ + color_stderr_repl $default_color_stderr_repl\ + logfile_stdout $default_logfile_stdout\ + logfile_stderr $default_logfile_stderr\ + logfile_active 0\ + syslog_stdout "127.0.0.1:514"\ + syslog_stderr "127.0.0.1:514"\ + syslog_active 0\ + auto_exec_mechanism exec\ + auto_noexec 0\ + xdg_config_home $default_xdg_config_home\ + xdg_data_home $default_xdg_data_home\ + xdg_cache_home $default_xdg_cache_home\ + xdg_state_home $default_xdg_state_home\ + xdg_data_dirs $default_xdg_data_dirs\ + theme_posh_override ""\ + posh_theme ""\ + posh_themes_path ""\ + ] + + set startup $defaults + #load values from saved config file - $xdg_config_home/punk/punk.config ? + #typically we want env vars to override the stored config - as env vars conventionally used on some commandlines. + #that's possibly ok for the PUNK_ vars + #however.. others like the xdg vars and NOCOLOR may apply to other apps.. and we may want to override them from the saved config? + #making some env vars override saved config values and some not would be potentially confusing. may need one/more specific settings or env vars to determine which takes precedence? + #simpler is probably just to let env vars take precedence - and warn when saving or viewing config that the saved values are being overridden + #- requiring user to manually unset any unwanted env vars when launching? + + #we are likely to want the saved configs for subshells/decks to override them however. + + #todo - load/save config file + + #todo - define which configvars are settable in env + #list of varname varinfo where varinfo is a sub dictionary (type key is mandatory, with value from: string,pathlist,boolean) + set punk_env_vars_config [dict create \ + PUNK_APPS {type pathlist}\ + PUNK_CONFIG {type string}\ + PUNK_CONFIGSET {type string}\ + PUNK_SCRIPTLIB {type string}\ + PUNK_AUTO_EXEC_MECHANISM {type string}\ + PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"}\ + PUNK_COLOR_STDERR {type string help "stderr colour transform. Use 'punk::ansi::a?' to see colour names"}\ + PUNK_COLOR_STDERR_REPL {type string help "stderr colour transform only while command running (not active during 'after')"}\ + PUNK_COLOR_STDOUT {type string help "stdout colour transform. Use 'punk::ansi::a?' to see colour names"}\ + PUNK_COLOR_STDOUT_REPL {type string help "stdout colour transform only while command running (not active during 'after')"}\ + PUNK_LOGFILE_STDOUT {type string}\ + PUNK_LOGFILE_STDERR {type string}\ + PUNK_LOGFILE_ACTIVE {type string}\ + PUNK_SYSLOG_STDOUT {type string}\ + PUNK_SYSLOG_STDERR {type string}\ + PUNK_SYSLOG_ACTIVE {type string}\ + PUNK_THEME_POSH_OVERRIDE {type string}\ + ] + set punk_env_vars [dict keys $punk_env_vars_config] + + #override with env vars if set + foreach {evar varinfo} $punk_env_vars_config { + if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] + set f [set ::env($evar)] + if {$f ne "default"} { + #e.g PUNK_SCRIPTLIB -> scriptlib + set varname [tcl::string::tolower [tcl::string::range $evar 5 end]] + if {$vartype eq "pathlist"} { + #colon vs semicolon path sep is problematic for windows environments where unix-like systems such as cygwin/wsl are used and a variable may be set for either the native path separator or the unix-like system + #Even without the colon vs semicolon issue, native vs unix-like paths on windows mixed environment systems can cause grief. + #For now at least, we will simply respect the platform pathSeparator and hope the user manages the environment variables appropriately. + #some programs do automatic translation - which is a nice idea in principle - but is also prone to error as we don't know if it's already occurred or not depending on how things are launched. + #An example of where this sort of thing can go wrong is env(TCLLIBPATH) - which is defined as a space separated list not requiring further splitting + # - but some programs have been known to split this value on colon anyway, which breaks things on windows. + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } + } + } + } + + # https://no-color.org + #if {[info exists ::env(NO_COLOR)]} { + # if {$::env(NO_COLOR) ne ""} { + # set colour_disabled 1 + # } + #} + set other_env_vars_config [dict create\ + NO_COLOR {type string}\ + XDG_CONFIG_HOME {type string}\ + XDG_DATA_HOME {type string}\ + XDG_CACHE_HOME {type string}\ + XDG_STATE_HOME {type string}\ + XDG_DATA_DIRS {type pathlist}\ + POSH_THEME {type string}\ + POSH_THEMES_PATH {type string}\ + TCLLIBPATH {type string}\ + ] + lassign [split [info tclversion] .] tclmajorv tclminorv + #don't rely on lseq or punk::lib for now.. + set relevant_minors [list] + for {set i 0} {$i <= $tclminorv} {incr i} { + lappend relevant_minors $i + } + foreach minor $relevant_minors { + set vname TCL${tclmajorv}_${minor}_TM_PATH + if {$minor eq $tclminorv || [info exists ::env($vname)]} { + dict set other_env_vars_config $vname {type string} + } + } + set other_env_vars [dict keys $other_env_vars_config] + + foreach {evar varinfo} $other_env_vars_config { + if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] + set f [set ::env($evar)] + if {$f ne "default"} { + set varname [tcl::string::tolower $evar] + if {$vartype eq "pathlist"} { + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } + } + } + } + + + #unset -nocomplain vars + + #todo + set running [tcl::dict::create] + set running [tcl::dict::merge $running $startup] + } + init + + #todo + proc Apply {config} { + puts stderr "punk::config::Apply partially implemented" + set configname [string map {-config ""} $config] + if {$configname in {startup running}} { + upvar ::punk::config::$configname applyconfig + + if {[dict exists $applyconfig auto_noexec]} { + set auto [dict get $applyconfig auto_noexec] + if {![string is boolean -strict $auto]} { + error "config::Apply error - invalid data for auto_noexec:'$auto' - expected boolean" + } + if {$auto} { + set ::auto_noexec 1 + } else { + #puts "auto_noexec false" + unset -nocomplain ::auto_noexec + } + } + + } else { + error "no config named '$config' found" + } + return "apply done" + } + Apply startup + + #todo - consider how to divide up settings, categories, 'devices', decks etc + proc get_running_global {varname} { + variable running + if {[dict exists $running $varname]} { + return [dict get $running $varname] + } + error "No such global configuration item '$varname' found in running config" + } + proc get_startup_global {varname} { + variable startup + if {[dict exists $startup $varname]} { + return [dict get $startup $varname] + } + error "No such global configuration item '$varname' found in startup config" + } + + proc get {whichconfig {globfor *}} { + variable startup + variable running + switch -- $whichconfig { + config - startup - startup-config - startup-configuration { + #show *startup* config - different behaviour may be confusing to those used to router startup and running configs + set configdata $startup + } + running - running-config - running-configuration { + set configdata $running + } + default { + error "Unknown config name '$whichconfig' - try startup or running" + } + } + if {$globfor eq "*"} { + return $configdata + } else { + set keys [dict keys $configdata [string tolower $globfor]] + set filtered [dict create] + foreach k $keys { + dict set filtered $k [dict get $configdata $k] + } + return $filtered + } + } + + proc configure {args} { + set argd [punk::args::get_dict { + + whichconfig -type string -choices {startup running} + } $args] + + } + + proc show {whichconfig {globfor *}} { + #todo - tables for console + set configdata [punk::config::get $whichconfig $globfor] + return [punk::lib::showdict $configdata] + } + + #e.g + # copy running-config startup-config + # copy startup-config test-config.cfg + # copy backup-config.cfg running-config + #review - consider the merge vs overwrite feature of some routers.. where copy to running-config does a merge rather than an overwrite + #This is to allow partial configs to be loaded to running, whereas a save of running to any target is always a complete configuration + proc copy {args} { + set argd [punk::args::get_dict { + *proc -name punk::config::copy -help "Copy a partial or full configuration from one config to another + If a target config has additional settings, then the source config can be considered to be partial with regards to the target. + " + -type -default "" -choices {replace merge} -help "Defaults to merge when target is running-config + Defaults to replace when source is running-config" + *values -min 2 -max 2 + fromconfig -help "running or startup or file name (not fully implemented)" + toconfig -help "running or startup or file name (not fully implemented)" + } $args] + set fromconfig [dict get $argd values fromconfig] + set toconfig [dict get $argd values toconfig] + set fromconfig [string map {-config ""} $fromconfig] + set toconfig [string map {-config ""} $toconfig] + + set copytype [dict get $argd opts -type] + + + #todo - warn & prompt if doing merge copy to startup + switch -exact -- $fromconfig-$toconfig { + running-startup { + if {$copytype eq ""} { + set copytype replace ;#full configuration + } + if {$copytype eq "replace"} { + error "punk::config::copy error. full configuration copy from running to startup config not yet supported" + } else { + error "punk::config::copy error. merge configuration copy from running to startup config not yet supported" + } + } + startup-running { + #default type merge - even though it's not always what is desired + if {$copytype eq ""} { + set copytype merge ;#load in a partial configuration + } + + #warn/prompt either way + if {$copytype eq "replace"} { + #some routers require use of a separate command for this branch. + #presumably to ensure the user doesn't accidentally load partials onto a running system + # + error "punk::config::copy error. full configuration copy from startup to overwrite running config not supported" + } else { + error "punk::config::copy error. merge copy from possibly partial configuration: startup to running config not currently supported" + } + } + default { + error "punk::config::copy error. copy must from running to startup or startup to running. File sources/targets not yet supported" + } + } + } + + + + + +} + + + + + +#todo - move to cli? +::tcl::namespace::eval punk::config { + #todo - something better - 'previous' rather than reverting to startup + proc channelcolors {{onoff {}}} { + variable running + variable startup + + if {![string length $onoff]} { + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } else { + if {![string is boolean $onoff]} { + error "channelcolors: invalid value $onoff - expected boolean: true|false|on|off|1|0|yes|no" + } + if {$onoff} { + dict set running color_stdout [dict get $startup color_stdout] + dict set running color_stderr [dict get $startup color_stderr] + } else { + dict set running color_stdout "" + dict set running color_stderr "" + } + } + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } +} + +package provide punk::config [tcl::namespace::eval punk::config { + variable version + set version 0.1 + +}] \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mod-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mod-0.1.tm new file mode 100644 index 00000000..58906c88 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mod-0.1.tm @@ -0,0 +1,164 @@ +#punkapps app manager +# deck cli + +namespace eval punk::mod::cli { + namespace export help list run + namespace ensemble create + + # namespace ensemble configure [namespace current] -unknown punk::mod::cli::_unknown + if 0 { + proc _unknown {ns args} { + puts stderr "punk::mod::cli::_unknown '$ns' '$args'" + puts stderr "punk::mod::cli::help $args" + puts stderr "arglen:[llength $args]" + punk::mod::cli::help {*}$args + } + } + + #cli must have _init method - usually used to load commandsets lazily + # + variable initialised 0 + proc _init {args} { + variable initialised + if {$initialised} { + return + } + #... + set initialised 1 + } + + proc help {args} { + set basehelp [punk::mix::base help {*}$args] + #namespace export + return $basehelp + } + proc getraw {appname} { + upvar ::punk::config::running running_config + set app_folders [dict get $running_config apps] + #todo search each app folder + set bases [::list] + set versions [::list] + set mains [::list] + set appinfo [::list bases {} mains {} versions {}] + + foreach containerfolder $app_folders { + lappend bases $containerfolder + if {[file exists $containerfolder]} { + if {[file exists $containerfolder/$appname/main.tcl]} { + #exact match - only return info for the exact one specified + set namematches $appname + set parts [split $appname -] + } else { + set namematches [glob -nocomplain -dir $containerfolder -type d -tail ${appname}-*] + set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort? + } + foreach nm $namematches { + set mainfile $containerfolder/$nm/main.tcl + set parts [split $nm -] + if {[llength $parts] == 1} { + set ver "" + } else { + set ver [lindex $parts end] + } + if {$ver ni $versions} { + lappend versions $ver + lappend mains $ver $mainfile + } else { + puts stderr "punk::apps::app version '$ver' of app '$appname' already encountered at $mainfile. (will use earliest encountered in running-config apps and ignore others of same version)" + } + } + } else { + puts stderr "punk::apps::app missing apps_folder:'$containerfolder' Ensure apps_folder is set in punk::config" + } + } + dict set appinfo versions $versions + #todo - natsort! + set sorted_versions [lsort $versions] + set latest [lindex $sorted_versions 0] + if {$latest eq "" && [llength $sorted_versions] > 1} { + set latest [lindex $sorted_versions 1 + } + dict set appinfo latest $latest + + dict set appinfo bases $bases + dict set appinfo mains $mains + return $appinfo + } + + proc list {{glob *}} { + upvar ::punk::config::running running_config + set apps_folder [dict get $running_config apps] + if {[file exists $apps_folder]} { + if {[file exists $apps_folder/$glob]} { + #tailcall source $apps_folder/$glob/main.tcl + return $glob + } + set apps [glob -nocomplain -dir $apps_folder -type d -tail $glob] + if {[llength $apps] == 0} { + if {[string first * $glob] <0 && [string first ? $glob] <0} { + #no glob chars supplied - only launch if exact match for name part + set namematches [glob -nocomplain -dir $apps_folder -type d -tail ${glob}-*] + set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort? + if {[llength $namematches] > 0} { + set latest [lindex $namematches end] + lassign $latest nm ver + #tailcall source $apps_folder/$latest/main.tcl + } + } + } + + return $apps + } + } + + #todo - way to launch as separate process + # solo-opts only before appname - args following appname are passed to the app + proc run {args} { + set nameposn [lsearch -not $args -*] + if {$nameposn < 0} { + error "punkapp::run unable to determine application name" + } + set appname [lindex $args $nameposn] + set controlargs [lrange $args 0 $nameposn-1] + set appargs [lrange $args $nameposn+1 end] + + set appinfo [punk::mod::cli::getraw $appname] + if {[llength [dict get $appinfo versions]]} { + set ver [dict get $appinfo latest] + puts stdout "info: $appinfo" + set ::argc [llength $appargs] + set ::argv $appargs + source [dict get $appinfo mains $ver] + if {"-hideconsole" in $controlargs} { + puts stderr "attempting console hide" + #todo - something better - a callback when window mapped? + after 500 {::punkapp::hide_console} + } + return $appinfo + } else { + error "punk::mod::cli unable to run '$appname'. main.tcl not found in [dict get $appinfo bases]" + } + } + + +} + +namespace eval punk::mod::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command help + package require punk::mix::base + package require punk::overlay + punk::overlay::custom_from_base [namespace current] ::punk::mix::base +} + +package provide punk::mod [namespace eval punk::mod { + variable version + set version 0.1 + +}] + + + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm new file mode 100644 index 00000000..fdffa091 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -0,0 +1,1373 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.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) 2024 +# +# @@ Meta Begin +# Application punk::nav::fs 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::nav::fs 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::nav::fs] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::nav::fs +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::nav::fs +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::lib +package require punk::args +package require punk::ansi +package require punk::winpath +package require punk::du +package require commandstack +#*** !doctools +#[item] [package {Tcl 8.6}] +#[item] [package {punk::lib}] +#[item] [package {punk::args}] +#[item] [package {punk::winpath}] +#[item] [package {punk::du}] +#[item] [package {punk::commandstack}] + +if {"windows" eq $::tcl_platform(platform)} { + catch {package require punk::unixywindows} +} +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::nav::fs::class { + #*** !doctools + #[subsection {Namespace punk::nav::fs::class}] + #[para] class definitions + #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::nav::fs { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + + #Both tcl's notion of pwd and VIRTUAL_CWD can be out of sync with the process CWD. This happens when in a VFS. + #We can also have VIRTUAL_CWD navigate to spaces that Tcl's cd can't - review + + variable VIRTUAL_CWD ;#cwd that tracks pwd except when in zipfs locations that are not at or below a mountpoint + if {![interp issafe]} { + set VIRTUAL_CWD [pwd] + } else { + set VIRTUAL_CWD "" + } + proc vwd {} { + variable VIRTUAL_CWD + set cwd [pwd] + if {$cwd ne $VIRTUAL_CWD} { + puts stderr "pwd: $cwd" + } + return $::punk::nav::fs::VIRTUAL_CWD + } + + #TODO - maintain per 'volume/server' CWD + #e.g cd and ./ to: + # d: + # //zipfs: + # //server + # https://example.com + # should return to the last CWD for that volume/server + + #VIRTUAL_CWD follows pwd when changed via cd + set stackrecord [commandstack::rename_command -renamer punk::nav::fs cd {args} { + if {![catch { + $COMMANDSTACKNEXT {*}$args + } errM]} { + set ::punk::nav::fs::VIRTUAL_CWD [pwd] + } else { + error $errM + } + }] + + #*** !doctools + #[subsection {Namespace punk::nav::fs}] + #[para] Core API functions for punk::nav::fs + #[list_begin definitions] + + + #NOTE - as we expect to run other apps (e.g Tk) in the same process, but possibly different threads - we should be careful about use of cd which is per-process not per-thread. + #As this function recurses and calls cd multiple times - it's not thread-safe. + #Another thread could theoretically cd whilst this is running. + #Most likely this will then just error-out - but there is a possibility we could end up in the wrong directory, or cause the same problems in the other thread. + #REVIEW - consider looking at current directory only at the beginning and do a single cd to an absolute path. + #currently this allows ./ subdir subdir2 nonexistant and we cd to subdir/subdir2 even though an error is produced at the end. + #This offers a convenience for repl useage at the slight cost of more potential cross-thread cd interference + #- although presumably most library code shouldn't be changing CWD anyway. + #Ideally the user/repl should be in control of the processes working directory and we shouldn't have to worry too much here. + #Notably for example tcltest-2.5.5 at least uses cd - so this is something that may be best run in a separate process (for each test suite?) + #This seems unfortunate - as a multithreaded set of test runs might otherwise have made some sense.. but perhaps for tests more serious isolation is a good idea. + #It also seems common to cd when loading certain packages e.g tls from starkit. + #While in most/normal cases the library will cd back to the remembered working directory after only a brief time - there seem to be many opportunities for issues + #if the repl is used to launch/run a number of things in the one process + proc d/ {args} { + variable VIRTUAL_CWD + + set is_win [expr {"windows" eq $::tcl_platform(platform)}] + + set repl_runid 0 + if {[info commands ::punk::get_repl_runid] ne ""} { + set repl_runid [punk::get_repl_runid] + } + #set ::punk::last_run_display [list] + + if {([llength $args]) && ([lindex $args 0] eq "")} { + set args [lrange $args 1 end] + } + + + if {![llength $args]} { + #ls is too slow even over a fairly low-latency network + #set out [runout -n ls -aFC] + if {[string match //zipfs:/* $VIRTUAL_CWD]} { + if {[Zipfs_path_within_zipfs_mounts $VIRTUAL_CWD]} { + if {[pwd] ne $VIRTUAL_CWD} { + commandstack::basecall cd $VIRTUAL_CWD + } + } + set matchinfo [dirfiles_dict -searchbase $VIRTUAL_CWD] + } else { + if {[pwd] ne $VIRTUAL_CWD} { + commandstack::basecall cd $VIRTUAL_CWD + } + set matchinfo [dirfiles_dict -searchbase [pwd]] + } + set dircount [llength [dict get $matchinfo dirs]] + set filecount [llength [dict get $matchinfo files]] + set symlinkcount [llength [dict get $matchinfo links]] ;#doesn't include windows shelllinks (.lnk) + #set location [file normalize [dict get $matchinfo location]] + set location [dict get $matchinfo location] + + + #result for glob is count of matches - use dirfiles etc for script access to results + set result [list location $location dircount $dircount filecount $filecount symlinks $symlinkcount] + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + lappend result filebytes [punk::lib::format_number $filebytes] + } + if {[punk::nav::fs::system::codethread_is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + #if ansi is off - punk::console::titleset will try 'local' api method - which can fail + catch {::punk::console::titleset [lrange $result 1 end]} + } + } + if {[string match //zipfs:/* $location]} { + set stripbase 0 + } else { + set stripbase 1 + } + set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo] + set chunklist [list] + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + if {[file normalize $VIRTUAL_CWD] ne [pwd]} { + lappend chunklist [list stderr "[punk::ansi::a+ red]PWD:[pwd] VIRTUAL_CWD:$VIRTUAL_CWD[punk::ansi::a]"] + } + lappend chunklist [list result $result] + if {$repl_runid != 0} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + } else { + punk::nav::fs::system::emit_chunklist $chunklist + } + #puts stdout "-->[ansistring VIEW $result]" + return $result + } else { + set atail [lassign $args a1] + if {[llength $args] == 1} { + set a1 [lindex $args 0] + switch -exact -- $a1 { + . - ./ { + tailcall punk::nav::fs::d/ + } + .. - ../ { + if {$VIRTUAL_CWD eq "//zipfs:/" && ![string match //zipfs:/* [pwd]]} { + #exit back to last nonzipfs path that was in use + set VIRTUAL_CWD [pwd] + tailcall punk::nav::fs::d/ + } + + #we need to use normjoin to allow navigation to //server instead of just to //server/share (//server browsing unimplemented - review) + # [file join //server ..] would become /server/.. - use normjoin to get //server + # file dirname //server/share would stay as //server/share + #set up1 [file dirname $VIRTUAL_CWD] + set up1 [punk::path::normjoin $VIRTUAL_CWD ..] + if {[string match //zipfs:/* $up1]} { + if {[Zipfs_path_within_zipfs_mounts $up1]} { + cd $up1 + set VIRTUAL_CWD $up1 + } else { + set VIRTUAL_CWD $up1 + } + } else { + cd $up1 + #set VIRTUAL_CWD [file normalize $a1] + } + tailcall punk::nav::fs::d/ + } + } + + if {[file pathtype $a1] ne "relative"} { + if { ![string match //zipfs:/* $a1]} { + if {[file type $a1] eq "directory"} { + cd $a1 + #set VIRTUAL_CWD $a1 + tailcall punk::nav::fs::d/ + } + } + } + + + if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} { + if {[file type $a1] eq "directory"} { + cd $a1 + #set VIRTUAL_CWD [file normalize $a1] + tailcall punk::nav::fs::d/ + } + } + + if {![regexp {[*?]} $a1]} { + #NON-Glob target + #review + if {[string match //zipfs:/* $a1]} { + if {[Zipfs_path_within_zipfs_mounts $a1]} { + commandstack::basecall cd $a1 + } + set VIRTUAL_CWD $a1 + set curdir $a1 + } else { + set target [punk::path::normjoin $VIRTUAL_CWD $a1] + if {[string match //zipfs:/* $VIRTUAL_CWD]} { + if {[Zipfs_path_within_zipfs_mounts $target]} { + commandstack::basecall cd $target + } + } + if {[file type $target] eq "directory"} { + set VIRTUAL_CWD $target + } + } + tailcall punk::nav::fs::d/ + } + set curdir $VIRTUAL_CWD + } else { + set curdir [pwd] + } + + + #globchar somewhere in path - treated as literals except in final segment (for now. todo - make more like ns/ which accepts full path globbing with double ** etc.) + + set searchspec [lindex $args 0] + + set result "" + set chunklist [list] + + #Only merge results if location matches previous (caller can deliberately intersperse bogus globs to force split if desired) + #TODO - remove duplicate file or dir items for overlapping patterns in same location!!! (at least for count, filebyte totals if not for display) + set last_location "" + set this_result [dict create] + foreach searchspec $args { + set path [path_to_absolute $searchspec $curdir $::tcl_platform(platform)] + set has_tailglob [expr {[regexp {[?*]} [file tail $path]]}] + #we have already done a 'cd' if only one unglobbed path was supplied - therefore any remaining non-glob tails must be tested for folderness vs fileness to see what they mean + #this may be slightly surprising if user tries to exactly match both a directory name and a file both as single objects; because the dir will be listed (auto /* applied to it) - but is consistent enough. + #lower level dirfiles or dirfiles_dict can be used to more precisely craft searches. ( d/ will treat dir the same as dir/*) + + set searchspec_relative [expr {[file pathtype $searchspec] eq "relative"}] + if {$has_tailglob} { + set location [file dirname $path] + set glob [file tail $path] + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase [file dirname $searchspec] + } + } else { + if {[string match //zipfs:/* $path]} { + set location $path + set glob * + set searchbase $path + } elseif {[file isdirectory $path]} { + set location $path + set glob * + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase $path + } + } else { + set location [file dirname $path] + set glob [file tail $path] ;#search for exact match file + if {$searchspec_relative} { + set searchbase [pwd] + } else { + set searchbase [file dirname $path] + } + } + } + set matchinfo [dirfiles_dict -searchbase $searchbase -tailglob $glob $location] + #puts stderr "=--->$matchinfo" + + + set location [file normalize [dict get $matchinfo location]] + if {[string match //xzipfs:/* $location] || $location ne $last_location} { + #REVIEW - zipfs test disabled with leading x + #emit previous result + if {[dict size $this_result]} { + dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] + lappend chunklist [list result $this_result] + if {$result ne ""} { + append result \n + } + append result $this_result + } + set this_result [dict create] + set dircount 0 + set filecount 0 + } + incr dircount [llength [dict get $matchinfo dirs]] + incr filecount [llength [dict get $matchinfo files]] + + #result for glob is count of matches - use dirfiles etc for script access to results + dict set this_result location $location + dict set this_result dircount $dircount + dict set this_result filecount $filecount + + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + dict incr this_result filebytes $filebytes + } else { + dict incr this_result filebytes 0 ;#ensure key exists! + } + dict lappend this_result pattern [dict get $matchinfo opts -glob] + + if {[string match //zipfs:/* $location]} { + set stripbase 0 + } else { + set stripbase 1 + } + set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo] + + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + + set last_location $location + } + #process final result + if {[dict size $this_result]} { + dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] + lappend chunklist [list result $this_result] + if {$result ne ""} { + append result \n + } + append result $this_result + } + if {[file normalize $VIRTUAL_CWD] ne [pwd]} { + lappend chunklist [list stderr "[punk::ansi::a+ red]PWD:[pwd] VIRTUAL_CWD:$VIRTUAL_CWD[punk::ansi::a]"] + } + + + if {[punk::nav::fs::system::codethread_is_running]} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + } + if {$repl_runid == 0} { + punk::nav::fs::system::emit_chunklist $chunklist + } + return $result + } + } + + proc dd/ {args} { + #set ::punk::last_run_display [list] + set repl_runid 0 + if {[info commands ::punk::get_repl_runid] ne ""} { + set repl_runid [punk::get_repl_runid] + } + if {![llength $args]} { + set path .. + } else { + set path ../[file join {*}$args] + } + set normpath [file normalize $path] + cd $normpath + set matchinfo [dirfiles_dict -searchbase $normpath $normpath] + set dircount [llength [dict get $matchinfo dirs]] + set filecount [llength [dict get $matchinfo files]] + set location [file normalize [dict get $matchinfo location]] + #result for glob is count of matches - use dirfiles etc for script access to results + set result [list location $location dircount $dircount filecount $filecount] + set filesizes [dict get $matchinfo filesizes] + if {[llength $filesizes]} { + set filesizes [lsearch -all -inline -not $filesizes na] + set filebytes [tcl::mathop::+ {*}$filesizes] + lappend result filebytes [punk::lib::format_number $filebytes] + } + + set out [dirfiles_dict_as_lines -stripbase 1 $matchinfo] + #return $out\n[pwd] + set chunklist [list] + lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"] + lappend chunklist [list result $result] + + if {[punk::nav::fs::system::codethread_is_running]} { + if {![tsv::llength repl runchunks-$repl_runid]} { + #set ::punk::last_run_display $chunklist + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + if {[llength [info commands ::punk::console::titleset]]} { + catch {::punk::console::titleset [lrange $result 1 end]} ;#strip location key + } + } + if {$repl_runid == 0} { + punk::nav::fs::system::emit_chunklist $chunklist + } + return $result + } + + proc d/new {args} { + if {![llength $args]} { + error "usage: d/new

\[ ...\]" + } + set a1 [lindex $args 0] + set curdir [pwd] + set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)] + set fullpath [file join $path1 {*}[lrange $args 1 end]] + + if {[file exists $fullpath]} { + error "Folder $fullpath already exists" + } + file mkdir $fullpath + d/ $fullpath + } + + #todo use unknown to allow d/~c:/etc ?? + proc d/~ {args} { + set home $::env(HOME) + set target [file join $home {*}$args] + if {![file isdirectory $target]} { + error "Folder $target not found" + } + d/ $target + } + + + #run a file + proc x/ {args} { + if {![llength $args]} { + set result [d/] + append result \n "x/ ?args?" + return $result + } + set curdir [pwd] + #todo - allow wish for those who want it.. but in punk we try to use tclsh or a kit and load Tk as a library + set scriptconfig [dict create\ + tcl [list exe tclsh extensions [list ".tcl" ".tm" ".tk" ".kit"]]\ + python [list exe python extensions [list ".py"]]\ + lua [list exe lua extensions [list ".lua"]]\ + perl [list exe perl extensions [list ".pl"]]\ + php [list exe php extensions [list ".php"]]\ + ] + set tcl_extensions [list ".tcl" ".tm" ".kit" ".tk"] ;#todo - load from config + set py_extensions [list ".py"] + set lua_extensions [list ".lua"] + set perl_extensions [list ".pl"] + + set script_extensions [list] + set extension_lookup [dict create] + tcl::dict::for {lang langinfo} $scriptconfig { + set extensions [dict get $langinfo extensions] + lappend script_extensions {*}$extensions + foreach e $extensions { + dict set extension_lookup $e $lang ;#provide reverse lookup + } + } + + #some executables (e.g tcl) can use arguments prior to the script + #use first entry on commandline for which a file exists *and has a script extension - or is executable* as the script to run + #we can't always just assume that first existant file on commandline is the one being run, as it might be config file + #e.g php -c php.ini -f script.php + set scriptlang "" + set scriptfile "" + foreach a $args { + set ext [file extension $a] + if {$ext in $script_extensions && [file exists $a]} { + set scriptlang [dict get $extension_lookup $ext] + set scriptfile $a + break + } + } + puts "scriptlang: $scriptlang scriptfile:$scriptfile" + + #todo - allow sh scripts with no extension ... look at shebang etc? + if {$scriptfile ne "" && $scriptlang ne ""} { + set path [path_to_absolute $scriptfile $curdir $::tcl_platform(platform)] + if {[file type $path] eq "file"} { + set ext [file extension $path] + set extlower [string tolower $ext] + if {$extlower in $tcl_extensions} { + set newargs [lrange $args 1 end] ;#todo - fix to allow script in position other than first + set ::argv0 $path + set ::argc [llength $newargs] + set ::argv $newargs + tailcall source $path + } elseif {$extlower in $py_extensions} { + set pycmd [auto_execok python] + tailcall {*}$pycmd {*}$args + } elseif {$extlower in $script_extensions} { + set exename [dict get $scriptconfig $scriptlang exe] + set cmd [auto_execok $exename] + tailcall {*}$cmd $args + } else { + set fd [open $path r] + set chunk [read $fd 4000]; close $fd + #consider any commented line near top of file containing 'tcl' as likely to be a tcl script of some sort and attempt to source it. + set toplines [split $chunk \n] + set tcl_indicator 0 + foreach ln $toplines { + set ln [string trim $ln] + if {[string match "#*tcl*" $ln]} { + set tcl_indicator 1 + break + } + } + if {$tcl_indicator} { + set newargs [lrange $args 1 end] ;#todo - fix to allow script in position other than first. + set ::argv0 $path + set ::argc [llength $newargs] + set ::argv $newargs + tailcall source $path + } + puts stderr "Cannot run [file extension $path] file directly ([file tail $path]) as tcl script. Ensure file has a known tcl extension ($tcl_extensions) or add a commented hint in the file such as #!/usr/bin/env tclsh" + return [pwd] + } + } + } else { + puts stderr "No script executable known for this" + } + + } + + + proc dirlist {{location ""}} { + set contents [dirfiles_dict $location] + return [dirfiles_dict_as_lines -stripbase 1 $contents] + } + + + #dirfiles dirfiles_dict always deliberately return absolute *unnormalized* path + #e.g when cwd is c:/repo/jn/punk dirfiles ../../ will return something like: + # c:/repo/jn/punk/../../blah + #dirfiles assumes we don't have glob chars in the filenames or paths - dirfiles_dict can be called directly with explicit -tailglob in the rare case that assumption doesn't hold + # dirfiles will test last segment (tail) of supplied searchspecs for fileness vs folderness (when no globchars present in tail) so that it can pass the appropriate flags downstream + proc dirfiles {args} { + set argspecs { + -stripbase -default 1 -type boolean + -formatsizes -default 1 -type boolean -help "Format file size numbers for clarity" + *values -min 0 -max -1 + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts values_dict + + set opt_stripbase [dict get $opts -stripbase] + set opt_formatsizes [dict get $opts -formatsizes] + + #todo - support multiple searchspecs - dirfiles_dict should merge results when same folder + set searchspec "" + dict for {_index val} $values_dict { + set searchspec $val + break + } + + set relativepath [expr {[file pathtype $searchspec] eq "relative"}] + set has_tailglobs [regexp {[?*]} [file tail $searchspec]] + + #dirfiles_dict would handle simple cases of globs within paths anyway - but we need to explicitly set tailglob here in all branches so that next level doesn't need to do file vs dir checks to determine user intent. + #(dir-listing vs file-info when no glob-chars present is inherently ambiguous so we test file vs dir to make an assumption - more explicit control via -tailglob can be done manually with dirfiles_dict) + if {$relativepath} { + set searchbase [pwd] + if {!$has_tailglobs} { + if {[file isdirectory [file join $searchbase $searchspec]]} { + set location [file join $searchbase $searchspec] + set tailglob * + } else { + set location [file dirname [file join $searchbase $searchspec]] + set tailglob [file tail $searchspec] ;#use exact match as a glob - will retrieve size,attributes etc. + } + } else { + #tailglobs exist - and we operate under assumption globchars aren't present in file/folder names - so no folderness/fileness check needed. + set location [file dirname [file join $searchbase $searchspec]] + set tailglob [file tail $searchspec] + } + } else { + #for absolute paths - searchbase AND location will change depending on globiness of tail and fileness vs folderness + if {!$has_tailglobs} { + if {[file isdirectory $searchspec]} { + set searchbase $searchspec + set location $searchspec + set tailglob * + } else { + set searchbase [file dirname $searchspec] + set location [file dirname $searchspec] + set tailglob [file tail $searchspec] ;#literal glob for single file - retrieves properties + } + } else { + set searchbase [file dirname $searchspec] + set location [file dirname $searchspec] + set tailglob [file tail $searchspec] + } + } + puts "--> -searchbase:$searchbase searchspec:$searchspec -tailglob:$tailglob location:$location" + set contents [dirfiles_dict -searchbase $searchbase -tailglob $tailglob $location] + return [dirfiles_dict_as_lines -stripbase $opt_stripbase -formatsizes $opt_formatsizes $contents] + } + + #todo - package as punk::nav::fs + #todo - in thread + #todo - streaming version + #glob patterns in path prior to final segment should already be resolved before using dirfiles_dict - as the underlying filesystem mechanisms can't do nested globbing themselves. + #dirfiles_dict will assume the path up to the final segment is literal even if globchars are included therein. + #final segment globs will be recognised only if -tailglob is passed as empty string + #if -tailglob not supplied and last segment has globchars - presume searchspec parendir is the container and last segment is globbing within that. + #if -tailglob not supplied and last segment has no globchars - presume searchspec is a container(directory) and use glob * + #caller should use parentdir as location and set tailglob to search-pattern or exact match if location is intended to match a file rather than a directory + #examples: + # somewhere/files = search is effectively somewhere/files/* (location somewhere/files glob is *) + # somewhere/files/* = (as above) + # -tailglob * somewhere/files = (as above) + # + # -tailglob "" somewhere/files = search somewhere folder for exactly 'files' (location somewhere glob is files) + # -tailglob files somewhere = (as above) + # + # somewhere/f* = search somewhere folder for f* (location somewhere glob is f*) + # -tailglob f* somewhere = (as above) + # + # This somewhat clumsy API is so that simple searches can be made in a default sensible manner without requiring extra -tailglob argument for the common cases - with lack of trailing glob segment indicating a directory listing + # - but we need to distinguish somewhere/files as a search of that folder vs somewhere/files as a search for exactly 'files' within somewhere, hence the -tailglob option to fine-tune. + # - this also in theory allows file/directory names to contain glob chars - although this is probably unlikely and/or unwise and not likely to be usable on all platforms. + # + #if caller supplies a tailglob as empty string - presume the caller hasn't set location to parentdir - and that last element is the search pattern. + # -searchbase is always passed through - and is only used to construct a location path if a relative searchspec was supplied + proc dirfiles_dict {args} { + set argspecs { + *opts -any 0 + -searchbase -default "" + -tailglob -default "\uFFFF" + #with_sizes & with_times must accept 0|1|f|d|l where f = files d = dirs l = links (punk::du) + -with_sizes -default "\uFFFF" -type string + -with_times -default "\uFFFF" -type string + *values -min 0 -max -1 -type string + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + + #puts stderr "searchspecs: $searchspecs [llength $searchspecs]" + #puts stdout "arglist: $opts" + + if {[llength $searchspecs] > 1} { + #review - spaced paths ? + error "dirfiles_dict: multiple listing not *yet* supported" + } + set searchspec [lindex $searchspecs 0] + # -- --- --- --- --- --- --- + set opt_searchbase [dict get $opts -searchbase] + set opt_tailglob [dict get $opts -tailglob] + set opt_with_sizes [dict get $opts -with_sizes] + set opt_with_times [dict get $opts -with_times] + # -- --- --- --- --- --- --- + + #we don't want to normalize.. + #for example if the user supplies ../ we want to see ../result + + set is_relativesearchspec [expr {[file pathtype $searchspec] eq "relative"}] + if {$opt_searchbase eq ""} { + set searchbase . + } else { + set searchbase $opt_searchbase + } + + + switch -- $opt_tailglob { + "" { + if {$searchspec eq ""} { + set location + } else { + if {$is_relativesarchspec} { + #set location [file dirname [file join $opt_searchbase $searchspec]] + set location [punk::path::normjoin $searchbase $searchspec ..] + } else { + set location [punk::path::normjoin $searchspec ..] + } + #here tail is treated as a search-pattern within location whether or not it contains glob chars "?" or "*" + set match_contents [file tail $searchspec] + } + } + "\uFFFF" { + set searchtail_has_globs [regexp {[*?]} [file tail $searchspec]] + if {$searchtail_has_globs} { + if {$is_relativesearchspec} { + #set location [file dirname [file join $searchbase $searchspec]] + #e.g subdir/* or sub/etc/x* + set location [punk::path::normjoin $searchbase $searchspec ..] + } else { + set location [punk::path::normjoin $searchspec ..] + } + set match_contents [file tail $searchspec] + } else { + #user didn't supply a glob within tail segment, nor did they specify a separate -tailglob - presume they want a directory listing + if {$searchspec eq ""} { + set location $searchbase + } else { + if {$is_relativesearchspec} { + #set location [file join $searchbase $searchspec] + set location [punk::path::normjoin $searchbase $searchspec] + } else { + #absolute path for search + set location $searchspec + } + } + set match_contents * + } + } + default { + #-tailglob supplied separately - ignore any globiness in tail segment of searchspec and treat literally + if {$searchspec eq ""} { + set location $searchbase + } else { + if {$is_relativesearchspec} { + #set location [file join $searchbase $searchspec] + set location [punk::path::normjoin $searchbase $searchspec] + } else { + set location $searchspec + } + } + set match_contents $opt_tailglob + } + } + puts stdout "searchbase: $searchbase searchspec:$searchspec" + + set in_vfs 0 + if {[llength [package provide vfs]]} { + foreach mount [vfs::filesystem info] { + if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} { + set in_vfs 1 + break + } + } + } + + if {$opt_with_sizes eq "\uFFFF"} { + #leave up to listing-provider defaults + set next_opt_with_sizes "" + } else { + set next_opt_with_sizes [list -with_sizes $opt_with_sizes] + } + if {$opt_with_times eq "\uFFFF"} { + #leave up to listing-provider defaults + set next_opt_with_times "" + } else { + set next_opt_with_times [list -with_times $opt_with_times] + } + if {$in_vfs} { + set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } else { + set in_zipfs 0 + if {[info commands ::tcl::zipfs::mount] ne ""} { + if {[string match //zipfs:/* $location]} { + set in_zipfs 1 + } + #dict for {zmount zpath} [zipfs mount] { + # if {[punk::mix::base::lib::path_a_atorbelow_b $location $zmount]} { + # set in_zipfs 1 + # break + # } + #} + } + if {$in_zipfs} { + #relative vs absolute? review - cwd valid for //zipfs:/ ?? + set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } else { + set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times] + } + } + + set dirs [dict get $listing dirs] + set files [dict get $listing files] + set filesizes [dict get $listing filesizes] + set vfsmounts [dict get $listing vfsmounts] + set flaggedhidden [dict get $listing flaggedhidden] + + + set nonportable [list] ;#illegal file/folder names for windows e.g trailing dot or trailing space - can still be read if //?/ AND shortname used + set underlayfiles [list] + set underlayfilesizes [list] + if {[llength $vfsmounts]} { + foreach vfsmount $vfsmounts { + if {[set fposn [lsearch $files $vfsmount]] >= 0} { + lappend underlayfiles [lindex $files $fposn] + set files [lreplace $files $fposn $fposn] + #for any change to files list must change filesizes too if list exists + if {[llength $filesizes]} { + lappend underlayfilesizes [lindex $filesizes $fposn] + set filesizes [lreplace $filesizes $fposn $fposn] + } + lappend dirs $vfsmount + } elseif {$vfsmount in $dirs} { + #either dirlisting mech was aware of vfs.. or mountpoint is overlaying an underlying folder + #for now - do nothing + #todo - review. way to query dirlisting mech to see if we are hiding a folder? + + } else { + #vfs mount but dirlisting mechanism didn't detect as file or folder + lappend dirs $vfsmount + } + } + } + + + #NOTE: -types {hidden d} * may return . & .. on unix platforms - but will not show them on windows. + #A mounted vfs exe (e.g sometclkit.exe) may be returned by -types {hidden d} on windows - but at the same time has "-hidden 0" in the result of file attr. + + #non-unix platforms may have attributes to indicate hidden status even if filename doesn't have leading dot. + #mac & windows have these + #windows doesn't consider dotfiles as hidden - mac does (?) + #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden + if {$::tcl_platform(platform) ne "windows"} { + lappend flaggedhidden {*}[lsearch -all -inline [concat $dirs $files] ".*"] + #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs + #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely + #set flaggedhidden [lsort -unique $flaggedhidden] + set flaggedhidden [punk::lib::lunique_unordered $flaggedhidden] + } + + set dirs [lsort $dirs] ;#todo - natsort + + + + #foreach d $dirs { + # if {[lindex [file system $d] 0] eq "tclvfs"} { + # lappend vfs $d [file system $d] + # } + #} + + #glob -types {hidden} will not always return the combination of glob -types {hidden f} && -types {hidden d} (on windows anyway) + + # -- --- + #can't lsort files without lsorting filesizes + #Note - the sort by index would convert an empty filesizes list to a list of empty strings - one for each entry in files + #We want to preserve the empty list if that's what the dirlisting mechanism returned (presumably because -with_sizes was 0 or explicitly excluded files) + if {[llength $filesizes] == 0} { + set sorted_files [lsort $files] + set sorted_filesizes [list] + } else { + set sortorder [lsort -indices $files] + set sorted_files [list] + set sorted_filesizes [list] + foreach i $sortorder { + lappend sorted_files [lindex $files $i] + lappend sorted_filesizes [lindex $filesizes $i] + } + } + + set files $sorted_files + set filesizes $sorted_filesizes + # -- --- + + + foreach nm [concat $dirs $files] { + if {[punk::winpath::illegalname_test $nm]} { + lappend nonportable $nm + } + } + set front_of_dict [dict create location $location searchbase $opt_searchbase] + set listing [dict merge $front_of_dict $listing] + + set updated [dict create dirs $dirs files $files filesizes $filesizes nonportable $nonportable flaggedhidden $flaggedhidden underlayfiles $underlayfiles underlayfilesizes $underlayfilesizes] + return [dict merge $listing $updated] + } + + #todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing? + proc dirfiles_dict_as_lines {args} { + package require overtype + + set argspecs { + -stripbase -default 0 -type boolean + -formatsizes -default 1 -type boolean + *values -min 1 -max -1 -type dict + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set list_of_dicts [dict values $vals] + + + + # -- --- --- --- --- --- --- --- --- --- --- --- + set opt_stripbase [dict get $opts -stripbase] + set opt_formatsizes [dict get $opts -formatsizes] + # -- --- --- --- --- --- --- --- --- --- --- --- + + #if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied + set common_base "" + set searchbases [list] + set searchbases_with_len [list] + if {$opt_stripbase} { + #todo - case-insensitive comparisons on platforms where that is appropriate (e.g windows) + # - note that the OS could be configured differently in this regard than the default (as could a filesystem such as ZFS), and that for example mounted SMB filesystems are likely to be configured to support the general windows client idea of case-preserving-but-case-insensitive. + # - we may be able to provide a reasonable default for windows vs other - but a proc option is likely needed to allow caller to override the default behaviour on a call by call basis, + # and a config option may be desirable for the user to override the platform default. + # The chosen defaults based on platform may be generally ok - but then not give the desired behaviour when accessing a particular filesystem/mount + if {$::tcl_platform(platform) eq "windows"} { + #case-preserving but case-insensitive matching is the default + foreach d $list_of_dicts { + set str [string tolower [string trim [dict get $d searchbase]]] + lappend searchbases $str + lappend searchbases_with_len [list $str [string length $str]] + } + } else { + #case sensitive + foreach d $list_of_dicts { + set str [string trim [dict get $d searchbase]] + lappend searchbases $str + lappend searchbases_with_len [list $str [string length $str]] + } + } + #if any of the searchbases is empty - there will be no common base - so leave common_base as empty string. + if {"" ni $searchbases} { + set shortest_to_longest [lsort -index 1 -integer $searchbases_with_len] + set prefix_test_list [tcl::prefix all $searchbases [lindex $shortest_to_longest 0 0]] + #if shortest doesn't match all searchbases - we have no common base + if {[llength $prefix_test_list] == [llength $searchbases]} { + set common_base [lindex $shortest_to_longest 0 0]; #we + } + } + } + + foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] { + set $fileset [list] + } + + #set contents [lindex $list_of_dicts 0] + foreach contents $list_of_dicts { + lappend dirs {*}[dict get $contents dirs] + lappend files {*}[dict get $contents files] + lappend links {*}[dict get $contents links] + lappend filesizes {*}[dict get $contents filesizes] + lappend underlayfiles {*}[dict get $contents underlayfiles] + lappend underlayfilesizes {*}[dict get $contents underlayfilesizes] + lappend flaggedhidden {*}[dict get $contents flaggedhidden] + lappend flaggedreadonly {*}[dict get $contents flaggedreadonly] + lappend flaggedsystem {*}[dict get $contents flaggedsystem] + lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective + lappend vfsmounts {*}[dict get $contents vfsmounts] + } + + if {$opt_stripbase && $common_base ne ""} { + set filetails [list] + set dirtails [list] + foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] { + set stripped [list] + foreach f [set $fileset] { + lappend stripped [strip_prefix_depth $f $common_base] + } + set $fileset $stripped + } + #Note: we need to remember to use common_base to rebuild the key when we need to query the dict-based elements: sizes & times - because we didn't strip those keys. + } + + # -- --- --- --- --- --- --- --- --- --- --- + #assign symlinks to the dirs or files collection (the punk::du system doesn't sort this out + #As at 2024-09 for windows symlinks - Tcl can't do file readlink on symlinks created with mklink /D name target (SYMLINKD) or mklink name target (SYMLINK) + #We can't read the target information - best we can do is classify it as a file or a dir + #we can't use 'file type' as that will report just 'link' - but file isfile and file isdirectory work and should work for links on all platforms - REVIEW + set file_symlinks [list] + set dir_symlinks [list] + set dir_shortcuts [list] ;#windows shell links (.lnk) that have a target that is a directory + foreach s $links { + if {[file isfile $s]} { + lappend file_symlinks $s + #will be appended in finfo_plus later + } elseif {[file isdirectory $s]} { + lappend dir_symlinks $s + lappend dirs $s + } else { + #dunno - warn for now + puts stderr "Warning - cannot determine link type for link $s" + } + } + #we now have the issue that our symlinks aren't sorted within the dir/file categorisation - they currently will have to appear at beginning or end - TODO + # -- --- --- --- --- --- --- --- --- --- --- + + + #todo - sort whilst maintaining order for metadata? + #we need to co-sort files only with filesizes (other info such as times is keyed on fname so cosorting not required) + + + #we can't sort on filesize after format_number (unless we were to enforce delim _ which we don't want to do) + if {$opt_formatsizes} { + set filesizes [punk::lib::format_number $filesizes] ;#accepts a list and will process each + } + #col2 with subcolumns + + #remove punk::pipedata dependency - allow use of punk::nav::fs without punk package + #set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + #widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max + set widest2a [tcl::mathfunc::max {*}[lmap v [list {*}$files {*}$file_symlinks ""] {string length $v}]] + + set c2a [string repeat " " [expr {$widest2a + 1}]] + #set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + set widest2b [tcl::mathfunc::max {*}[lmap v [list {*}$filesizes ""] {string length $v}]] + set c2b [string repeat " " [expr {$widest2b + 1}]] + set finfo [list] + foreach f $files s $filesizes { + #note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces + #hence we need to keep the filename as well, properly protected as a list element + lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"] + } + set flink_style [punk::ansi::a+ undercurly underline undt-green] ;#curly green underline with fallback to normal underline + set dlink_style [punk::ansi::a+ undercurly underline undt-green] + #We use an underline so the visual styling of a link can coexist with fg/bg colors applied for other attributes such as hidden + foreach flink $file_symlinks { + lappend finfo [list file $flink display "$flink_style[overtype::left $c2a $flink] [overtype::right $c2b 0]"] + } + + set fshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink] + set dshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink] + #examine windows .lnk shell link files (shortcuts) - these could be encountered on other platforms too - we should still be able to read them + #review - symlink to shortcut? hopefully will just work + #classify as file or directory - fallback to file if unknown/undeterminable + set finfo_plus [list] + foreach fdict $finfo { + set fname [dict get $fdict file] + if {[file extension $fname] eq ".lnk"} { + if {![catch {package require punk::winlnk}]} { + set shortcutinfo [punk::winlnk::file_get_info $fname] + set target_type "file" ;#default/fallback + if {[dict exists $shortcutinfo link_target]} { + set tgt [dict get $shortcutinfo link_target] + if {[file exists $tgt]} { + #file type could return 'link' - we will use ifile/isdirectory + if {[file isfile $tgt]} { + set target_type file + } elseif {[file isdirectory $tgt]} { + set target_type directory + } else { + set target_type file ;## ? + } + } else { + #todo - see if punk::winlnk has info about the type at the time of linking + #for now - treat as file + } + } + switch -- $target_type { + file { + set display [dict get $fdict display] + set display $fshortcut_style$display ;# + dict set fdict display $display + lappend finfo_plus $fdict + } + directory { + #target of link is a dir - for display/categorisation purposes we want to see it as a dir + #will be styled later based on membership of dir_shortcuts + lappend dirs $fname + lappend dir_shortcuts $fname + } + } + } + #if we don't have punk::winlnk to read the .lnk - it will get no special highlighting and just appear as an ordinary file even if it points to a dir + } else { + lappend finfo_plus $fdict + } + } + unset finfo + + + + #set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] + set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]] + + set displaylist [list] + set col1 [string repeat " " [expr {$widest1 + 2}]] + set RST [punk::ansi::a] + foreach d $dirs filerec $finfo_plus { + set d1 [punk::ansi::a+ cyan bold] + set d2 [punk::ansi::a+ defaultfg defaultbg normal] + #set f1 [punk::ansi::a+ white bold] + set f1 [punk::ansi::a+ white] + set f2 [punk::ansi::a+ defaultfg defaultbg normal] + set fdisp "" + if {[string length $d]} { + if {$d in $flaggedhidden} { + set d1 [punk::ansi::a+ cyan normal] + } + if {$d in $vfsmounts} { + if {$d in $flaggedhidden} { + #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW + #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows) + #mark it differently for now.. (todo bug report?) + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red Yellow bold] + } else { + set d1 [punk::ansi::a+ green Purple bold] + } + } else { + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red White bold] + } else { + set d1 [punk::ansi::a+ green bold] + } + } + } else { + if {$d in $nonportable} { + set d1 [punk::ansi::a+ red bold] + } + } + #dlink-style & dshortcut_style are for underlines - can be added with colours already set + if {$d in $dir_symlinks} { + append d1 $dlink_style + } elseif {$d in $dir_shortcuts} { + append d1 $dshortcut_style + } + } + if {[llength $filerec]} { + set fname [dict get $filerec file] + set fdisp [dict get $filerec display] + if {$fname in $flaggedhidden} { + set f1 [punk::ansi::a+ Purple] + } else { + if {$fname in $nonportable} { + set f1 [punk::ansi::a+ red bold] + } + } + } + lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST + } + + return [punk::lib::list_as_lines $displaylist] + } + + #pass in base and platform to head towards purity/testability. + #this function can probably never be pure in such a simple form - as it needs to read state from the os storage system configuration + #consider haskells approach of well-typed paths for cross-platform paths: https://hackage.haskell.org/package/path + #review: punk::winpath calls cygpath! + #review: file pathtype is platform dependant + proc path_to_absolute {path base platform} { + set ptype [file pathtype $path] + if {$ptype eq "absolute"} { + set path_absolute $path + } elseif {$ptype eq "volumerelative"} { + if {$platform eq "windows"} { + #unix looking paths like /c/users or /usr/local/etc are reported by tcl as volumerelative.. (as opposed to absolute on unix platforms) + if {[string index $path 0] eq "/"} { + #this conversion should be an option for the ./ command - not built in as a default way of handling volumerelative paths here + #It is more useful on windows to treat /usr/local as a wsl or mingw path - and may be reasonable for ./ - but is likely to surprise if put into utility functions. + #Todo - tidy up. + package require punk::unixywindows + set path_absolute [punk::unixywindows::towinpath $path] + #puts stderr "winpath: $path" + } else { + #todo handle volume-relative paths with volume specified c:etc c: + #note - tcl doesn't handle this properly anyway.. the win32 api should 'remember' the per-volume cwd + #not clear whether tcl can/will fix this - but it means these paths are dangerous. + #The cwd of the process can get out of sync with what tcl thinks is the working directory when you swap drives + #Arguably if ...? + + #set path_absolute $base/$path + set path_absolute $path + } + } else { + # unknown what paths are reported as this on other platforms.. treat as absolute for now + set path_absolute $path + } + } else { + set path_absolute $base/$path + } + if {$platform eq "windows"} { + if {[punk::winpath::illegalname_test $path_absolute]} { + set path_absolute [punk::winpath::illegalname_fix $path_absolute] ;#add dos-device-prefix protection if not already present + } + } + return $path_absolute + } + proc strip_prefix_depth {path prefix} { + set tail [lrange [file split $path] [llength [file split $prefix]] end] + if {[llength $tail]} { + return [file join {*}$tail] + } else { + return "" + } + } + + #REVIEW - at least one version of Tcl during development couldn't navigate using cd to intermediate paths between the zipfs root and the mountpoint. + #TODO - test if this can still occur. + proc Zipfs_path_within_zipfs_mounts {zipfspath} { + if {![string match //zipfs:/* $zipfspath]} {error "Zipfs_path_within_zipfs_mounts error. Supplied zipfspath $zipfspath must be a //zipfs:/* path"} + set is_within_mount 0 + dict for {zmount zpath} [zipfs mount] { + if {[punk::mix::base::lib::path_a_atorbelow_b $zipfspath $zmount]} { + set is_within_mount 1 + break + } + } + return $is_within_mount + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::nav::fs ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::nav::fs::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::nav::fs::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::nav::fs::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +tcl::namespace::eval punk::nav::fs::system { + #*** !doctools + #[subsection {Namespace punk::nav::fs::system}] + #[para] Internal functions that are not part of the API + + #ordinary emission of chunklist when no repl + proc emit_chunklist {chunklist} { + set result "" + foreach record $chunklist { + lassign $record type data + switch -- $type { + stdout { + puts stdout "$data" + } + stderr { + puts stderr $data + } + result {} + default { + puts stdout "$type $data" + } + } + } + return $result + } + + proc codethread_is_running {} { + if {[info commands ::punk::repl::codethread::is_running] ne ""} { + return [punk::repl::codethread::is_running] + } + return 0 + } + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::nav::fs [tcl::namespace::eval punk::nav::fs { + variable pkg punk::nav::fs + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm new file mode 100644 index 00000000..09b8a0be --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm @@ -0,0 +1,259 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.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) 2024 +# +# @@ Meta Begin +# Application punk::repl::codethread 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::repl::codethread 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::repl::codethread] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::repl::codethread +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::repl::codethread +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::config +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::repl::codethread::class { + #*** !doctools + #[subsection {Namespace punk::repl::codethread::class}] + #[para] class definitions + #if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::repl::codethread { + tcl::namespace::export * + variable replthread + variable replthread_cond + variable running 0 + + variable output_stdout "" + variable output_stderr "" + + #variable xyz + + #*** !doctools + #[subsection {Namespace punk::repl::codethread}] + #[para] Core API functions for punk::repl::codethread + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + proc is_running {} { + variable running + return $running + } + proc runscript {script} { + #puts stderr "->runscript" + variable replthread_cond + variable output_stdout "" + variable output_stderr "" + #expecting to be called from a thread::send in parent repl - ie in the toplevel interp so that the sub-interp "code" is available + #if a thread::send is done from the commandline in a codethread - Tcl will + if {"code" ni [interp children] || ![info exists replthread_cond]} { + #in case someone tries calling from codethread directly - don't do anything or change any state + #(direct caller could create an interp named code at the level "" -> "code" -"code" and add a replthread_cond value to avoid this check - but it probably won't do anything useful) + #if called directly - the context will be within the first 'code' interp. + #inappropriate caller could add superfluous entries to shellfilter stack if function errors out + #inappropriate caller could affect tsv vars (if their interp allows that anyway) + puts stderr "runscript is meant to be called from the parent repl thread via a thread::send to the codethread" + return + } + set outstack [list] + set errstack [list] + upvar ::punk::config::running running_config + if {[string length [dict get $running_config color_stdout_repl]] && [interp eval code punk::console::colour]} { + lappend outstack [interp eval code [list shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout_repl]]]] + } + lappend outstack [interp eval code [list shellfilter::stack::add stdout tee_to_var -settings {-varname ::punk::repl::codethread::output_stdout}]] + + if {[string length [dict get $running_config color_stderr_repl]] && [interp eval code punk::console::colour]} { + lappend errstack [interp eval code [list shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr_repl]]]] + # #lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour cyan]] + } + lappend errstack [interp eval code [list shellfilter::stack::add stderr tee_to_var -settings {-varname ::punk::repl::codethread::output_stderr}]] + + #an experiment + #set errhandle [shellfilter::stack::item_tophandle stderr] + #interp transfer "" $errhandle code + + set scope [interp eval code [list set ::punk::ns::ns_current]] + set status [catch { + interp eval code [list tcl::namespace::inscope $scope $script] + } result] + + + flush stdout + flush stderr + + #interp transfer code $errhandle "" + #flush $errhandle + set lastoutchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stdout]] end] + set lasterrchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stderr]] end] + #puts stderr "-->[ansistring VIEW -lf 1 $lastoutchar$lasterrchar]" + + set tid [thread::id] + tsv::set codethread_$tid info [list lastoutchar $lastoutchar lasterrchar $lasterrchar] + tsv::set codethread_$tid status $status + tsv::set codethread_$tid result $result + tsv::set codethread_$tid errorcode $::errorCode + + + #only remove from shellfilter::stack the items we added to stack in this function + foreach s [lreverse $outstack] { + interp eval code [list shellfilter::stack::remove stdout $s] + } + foreach s [lreverse $errstack] { + interp eval code [list shellfilter::stack::remove stderr $s] + } + thread::cond notify $replthread_cond + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::repl::codethread ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::repl::codethread::lib { + tcl::namespace::export * + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::repl::codethread::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::repl::codethread::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +tcl::namespace::eval punk::repl::codethread::system { + #*** !doctools + #[subsection {Namespace punk::repl::codethread::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::repl::codethread [tcl::namespace::eval punk::repl::codethread { + variable pkg punk::repl::codethread + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/unixywindows-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/unixywindows-0.1.0.tm new file mode 100644 index 00000000..1d0a3957 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/unixywindows-0.1.0.tm @@ -0,0 +1,237 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application punk::unixywindows 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + +#for illegalname_test +package require punk::winpath + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::unixywindows { + #'cached' name to make obvious it could be out of date - and to distinguish from unixyroot arg + variable cachedunixyroot "" + + + #----------------- + #e.g something like c:/Users/geek/scoop/apps/msys2/current c:/msys2 + proc get_unixyroot {} { + variable cachedunixyroot + if {![string length $cachedunixyroot]} { + if {![catch { + set result [exec cygpath -m /] ;# -m gives result with forward-slashes - which is ok for windows paths in a Tcl context. + set cachedunixyroot [punk::objclone $result] + file pathtype $cachedunixyroot; #this call causes the int-rep to be path + #set ::punk::last_run_display [list] ;#hack - review shouldn't really be necessary.. but because we call winpath from ./ - the repl looks for last_run_display + } errM]} { + + } else { + puts stderr "Warning: Failed to determine base for unix-like paths - using default of c:/msys2" + file pathtype [set cachedunixyroot [punk::objclone "c:/msys2"]] + } + } + #will have been shimmered from string to 'path' internal rep by 'file pathtype' call + + #let's return a different copy as it's so easy to lose path-rep + set copy [punk::objclone $cachedunixyroot] + return $copy + } + proc refresh_unixyroot {} { + variable cachedunixyroot + set result [exec cygpath -m /] ;# -m gives result with forward-slashes - which is ok for windows paths in a Tcl context. + set cachedunixyroot [punk::objclone $result] + file pathtype $cachedunixyroot; #this call causes the int-rep to be path + + set copy [punk::objclone $cachedunixyroot] + return $copy + } + proc set_unixyroot {windows_path} { + variable cachedunixyroot + file pathtype $windows_path + set cachedunixyroot [punk::objclone $windows_path] + #return the original - but probably int-rep will have shimmered to path even if started out as string + #- that's probably what we want. Caller can use as string or path without affecting cachedunixyroot + return $windows_path + } + + + proc windir {path} { + if {$path eq "~"} { + #as the tilde hasn't been normalized.. we can't assume we're running on the actual platform + return ~/.. + } + return [file dirname [towinpath $path]] + } + + #REVIEW high-coupling + proc cdwin {path} { + set path [towinpath $path] + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + ::punk::console::titleset $path + } + } + cd $path + } + proc cdwindir {path} { + set path [towinpath $path] + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + if {[llength [info commands ::punk::console::titleset]]} { + ::punk::console::titleset $path + } + } + cd [file dirname $path] + } + + #NOTE - this is an expensive operation - avoid where possible. + #review - is this intended to be useful/callable on non-windows platforms? + #it should in theory be useable from another platform that wants to create a path for use on windows. + #In this case - we shouldn't examine what volumes exist (assume A: .. Z: are valid) + #review zipfs:// other uri schemes? + proc towinpath {unixypath {unixyroot ""}} { + #NOTE: tcl file exists gives different answers on windows for paths like /c depending on cwd (presumably based on file pathtype of volumerelative) + #(Tcl is also somewhat broken as at 2023 as far as volume relative paths - process can get out of sync with tcl if cd to a vol relative path is used) + #This is add odds with attempting to navigate on a windows system which has cygwin, wsl etc... It also makes it difficult for functions intended to operate independent of CWD. + #e.g there is potential confusion when there is a c folder on c: drive (c:/c) + #I will attempt to provide a coherent operation for winpath ./ ../ etc , but it may disallow for example; change to /something or /x where these don't match a driveletter or /mnt + #whereas tcl may allow cd to /something if a something folder happens to exist on the current volume based on cwd. + #I think it's preferable to require an explicit driveletter /x or /mnt when using unix-like paths on windows - but practical considerations may prove me wrong.. + #It's possible that this function should also ignore the current set of driveletters - and operate completely independent of whether a path actually exists + #This makes it hard to use things like 'file normalize' - which also looks at things like current volume. + # + #Note for example the results of 'which' grep on windows can produce a path like /c/Users/somewhere/bin/grep + #which tcl's file normalize may change to C:/c/Users or X:/c/Users - based on current volumen. Given that C:/c might exist - this can be problematic in a couple of ways. + #The mixing of unix-like and windows commands on the same machine is a large part of the problem.. but this mix is now common + # + #convert /c/etc to C:/etc + set re_slash_x_slash {^/([[:alpha:]]){1}/.*} + set re_slash_else {^/([[:alpha:]]*)(.*)} + set volumes [file volumes] + #exclude things like //zipfs:/ ?? + set driveletters [list] + foreach v $volumes { + if {[regexp {^([[:alpha:]]){1}:/$} $v _ letter]} { + lappend driveletters $letter + } + } + #puts stderr "->$driveletters" + + set path [punk::objclone $unixypath] ;#take another copy that we can deliberatley shimmer to path and know is separate to the supplied argument + set supplied_pathtype [file pathtype $path] ;#we don't care about the pathtype so much as the act of making this call shimmers to a path internal-rep + + #copy of var that we can treat as a string without affecting path rep + #Note that some but not all read operations will lose path rep e.g 'string length $x' will lose any path-rep $x had, (but 'string equal $x something' won't) + #Todo - make int-rep tests to check stability of these behaviours across Tcl versions! + set strcopy_path [punk::objclone $path] + + set str_newpath "" + + set have_pathobj 0 + + if {[regexp $re_slash_x_slash $strcopy_path _ letter]} { + #upper case appears to be windows canonical form + set str_newpath [string toupper $letter]:/[string range $strcopy_path 3 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}/.*} [string tolower $strcopy_path] _ letter]} { + set str_newpath [string toupper $letter]:/[string range $strcopy_path 7 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}$} [string tolower $strcopy_path] _ letter]} { + set str_newpath [string toupper $letter]:/ + } elseif {[regexp $re_slash_else $strcopy_path _ firstpart remainder]} { + #could be for example /c or /something/users + if {[string length $firstpart] == 1} { + set letter $firstpart + set str_newpath [string toupper $letter]:/ + } else { + #according to regex we have a single leading slash + set str_tail [string range $strcopy_path 1 end] + if {$unixyroot eq ""} { + set unixyroot [get_unixyroot] + } else { + file pathtype $unixyroot; #side-effect generates int-rep of type path ) + } + set pathobj [file join $unixyroot $str_tail] + file pathtype $pathobj + set have_pathobj 1 + } + } + + if {!$have_pathobj} { + if {$str_newpath eq ""} { + #dunno - pass through + set pathobj $path + } else { + set pathobj [punk::objclone $str_newpath] + file pathtype $pathobj + } + } + + + + #puts stderr "=> $path" + #things like 'which' seem to return a path minus the .exe - so we'll just test the containing folder + # + #By now file normalize shouldn't do too many shannanigans related to cwd.. + #We want it to look at cwd for relative paths.. + #but we don't consider things like /c/Users to be relative even on windows where it would normally mean a volume-relative path e.g c:/c/Users if cwd happens to be somewhere on C: at the time. + #if {![file exists [file dirname $path]]} { + # set path [file normalize $path] + # #may still not exist.. that's ok. + #} + + + + #file normalize may change backslashes to forward slashes.. including things like the special \\?\ prefix which is intended to stop windows api from parsing a name + #2023 - this is ok as //?/ also seems to work.. but it is unclear if that is because Tcl is re-converting to backslashes + if {[punk::winpath::illegalname_test $pathobj]} { + set pathobj [punk::winpath::illegalname_fix $pathobj] + } + + return $pathobj + } + + #---------------------------------------------- + #leave the unixywindows related aliases available on all platforms + #interp alias {} cdwin {} punk::unixywindows::cdwin + #interp alias {} cdwindir {} punk::unixywindoes::cdwindir + #interp alias {} towinpath {} punk::unixywindows::towinpath + #interp alias {} windir {} punk::unixywindows::windir + #---------------------------------------------- + + + +} + + + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::unixywindows [namespace eval punk::unixywindows { + variable version + set version 0.1.0 +}] +return diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkapp-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkapp-0.1.tm new file mode 100644 index 00000000..ce46856b --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkapp-0.1.tm @@ -0,0 +1,239 @@ +#utilities for punk apps to call + +package provide punkapp [namespace eval punkapp { + variable version + set version 0.1 +}] + +namespace eval punkapp { + variable result + variable waiting "no" + proc hide_dot_window {} { + #alternative to wm withdraw . + #see https://wiki.tcl-lang.org/page/wm+withdraw + wm geometry . 1x1+0+0 + wm overrideredirect . 1 + wm transient . + } + proc is_toplevel {w} { + if {![llength [info commands winfo]]} { + return 0 + } + expr {[winfo toplevel $w] eq $w && ![catch {$w cget -menu}]} + } + proc get_toplevels {{w .}} { + if {![llength [info commands winfo]]} { + return [list] + } + set list {} + if {[is_toplevel $w]} { + lappend list $w + } + foreach w [winfo children $w] { + lappend list {*}[get_toplevels $w] + } + return $list + } + + proc make_toplevel_next {prefix} { + set top [get_toplevel_next $prefix] + return [toplevel $top] + } + #possible race condition if multiple calls made without actually creating the toplevel, or gap if highest existing closed in the meantime + #todo - reserve_toplevel_next ? keep list of toplevels considered 'allocated' even if never created or already destroyed? what usecase? + #can call wm withdraw to to reserve newly created toplevel. To stop re-use of existing names after destruction would require a list or at least a record of highest created for each prefix + proc get_toplevel_next {prefix} { + set base [string trim $prefix .] ;# .myapp -> myapp .myapp.somewindow -> myapp.somewindow . -> "" + + + + } + proc exit {{toplevel ""}} { + variable waiting + variable result + variable default_result + set toplevels [get_toplevels] + if {[string length $toplevel]} { + set wposn [lsearch $toplevels $toplevel] + if {$wposn > 0} { + destroy $toplevel + } + } else { + #review + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + puts stderr "punkapp::exit called without toplevel - showing console" + show_console + return 0 + } else { + puts stderr "punkapp::exit called without toplevel - exiting" + if {$waiting ne "no"} { + if {[info exists result(shell)]} { + set temp [set result(shell)] + unset result(shell) + set waiting $temp + } else { + set waiting "" + } + } else { + ::exit + } + } + } + + set controllable [get_user_controllable_toplevels] + if {![llength $controllable]} { + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + show_console + } else { + if {$waiting ne "no"} { + if {[info exists result(shell)]} { + set temp [set result(shell)] + unset result(shell) + set waiting $temp + } elseif {[info exists result($toplevel)]} { + set temp [set result($toplevel)] + unset result($toplevel) + set waiting $temp + } elseif {[info exists default_result]} { + set temp $default_result + unset default_result + set waiting $temp + } else { + set waiting "" + } + } else { + ::exit + } + } + } + } + proc close_window {toplevel} { + wm withdraw $toplevel + if {![llength [get_user_controllable_toplevels]]} { + punkapp::exit $toplevel + } + destroy $toplevel + } + proc wait {args} { + variable waiting + variable default_result + if {[dict exists $args -defaultresult]} { + set default_result [dict get $args -defaultresult] + } + foreach t [punkapp::get_toplevels] { + if {[wm protocol $t WM_DELETE_WINDOW] eq ""} { + wm protocol $t WM_DELETE_WINDOW [list punkapp::close_window $t] + } + } + if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { + puts stderr "repl eventloop seems to be running - punkapp::wait not required" + } else { + if {$waiting eq "no"} { + set waiting "waiting" + vwait ::punkapp::waiting + return $::punkapp::waiting + } + } + } + + #A window can be 'visible' according to this - but underneath other windows etc + #REVIEW - change name? + proc get_visible_toplevels {{w .}} { + if {![llength [info commands winfo]]} { + return [list] + } + set list [get_toplevels $w] + set mapped [lmap v $list {expr {[winfo ismapped $v] ? $v : {}}}] + set mapped [concat {*}$mapped] ;#ignore {} + set visible [list] + foreach m $mapped { + if {[wm overrideredirect $m] == 0 } { + lappend visible $m + } else { + if {[winfo height $m] >1 && [winfo width $m] > 1} { + #technically even a 1x1 is visible.. but in practice even a 10x10 is hardly likely to be noticeable when overrideredirect == 1 + #as a convention - 1x1 with no controls is used to make a window invisible so we'll treat anything larger as visible + lappend visible $m + } + } + } + return $visible + } + proc get_user_controllable_toplevels {{w .}} { + set visible [get_visible_toplevels $w] + set controllable [list] + foreach v $visible { + if {[wm overrideredirect $v] == 0} { + lappend controllable $v + } + } + #only return visible windows with overrideredirect == 0 because there exists some user control. + #todo - review.. consider checking if position is outside screen areas? Technically controllable.. but not easily + return $controllable + } + proc hide_console {args} { + set opts [dict create -force 0] + if {([llength $args] % 2) != 0} { + error "hide_console expects pairs of arguments. e.g -force 1" + } + #set known_opts [dict keys $defaults] + foreach {k v} $args { + switch -- $k { + -force { + dict set opts $k $v + } + default { + error "Unrecognised options '$k' known options: [dict keys $opts]" + } + } + } + set force [dict get $opts -force] + + if {!$force} { + if {![llength [get_user_controllable_toplevels]]} { + puts stderr "Cannot hide console while no user-controllable windows available" + return 0 + } + } + if {$::tcl_platform(platform) eq "windows"} { + #hide won't work for certain consoles cush as conemu,wezterm - and doesn't really make sense for tabbed windows anyway. + #It would be nice if we could tell the console window to hide just the relevant tab - or the whole window if only one tab present - but this is unlikely to be possible in any standard way. + #an ordinary cmd.exe or pwsh.exe or powershell.exe window can be hidden ok though. + #(but with wezterm - process is cmd.exe - but it has style popup and can't be hidden with a twapi::hide_window call) + package require twapi + set h [twapi::get_console_window] + set pid [twapi::get_window_process $h] + set pinfo [twapi::get_process_info $pid -name] + set pname [dict get $pinfo -name] + set wstyle [twapi::get_window_style $h] + #tclkitsh/tclsh? + if {($pname in [list cmd.exe pwsh.exe powershell.exe] || [string match punk*.exe $pname]) && "popup" ni $wstyle} { + twapi::hide_window $h + return 1 + } else { + puts stderr "punkapp::hide_console unable to hide this type of console window" + return 0 + } + } else { + #todo + puts stderr "punkapp::hide_console unimplemented on this platform (todo)" + return 0 + } + } + + proc show_console {} { + if {$::tcl_platform(platform) eq "windows"} { + package require twapi + if {![catch {set h [twapi::get_console_window]} errM]} { + twapi::show_window $h -activate -normal + } else { + #no console - assume launched from something like wish? + catch {console show} + } + } else { + #todo + puts stderr "punkapp::show_console unimplemented on this platform" + } + } + +} diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck/cli-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck/cli-0.1.0.tm new file mode 100644 index 00000000..609df5c3 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck/cli-0.1.0.tm @@ -0,0 +1,333 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application punkcheck::cli 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + +package require punk::mix::util + +namespace eval punkcheck::cli { + namespace ensemble create + #package require punk::overlay + #punk::overlay::import_commandset debug. ::punk:mix::commandset::debug + + #init proc required - used for lazy loading of commandsets + variable initialised 0 + proc _init {args} { + variable initialised + if {$initialised} { + return + } + puts stderr "punkcheck::cli::init $args" + + set initialised 1 + } + + proc help {args} { + set basehelp [punk::mix::base help {*}$args] + return $basehelp + } + + proc paths {{path {}}} { + if {$path eq {}} { set path [pwd] } + set search_from $path + set bottom_to_top [list] + while {[string length [set pcheck_file [punkcheck::cli::lib::find_nearest_file $search_from]]]} { + set pcheck_folder [file dirname $pcheck_file] + lappend bottom_to_top $pcheck_file + set search_from [file dirname $pcheck_folder] + } + return $bottom_to_top + } + #todo! - group by fileset + proc status {{path {}}} { + if {$path eq {}} { set path [pwd] } + set fullpath [file normalize $path] + + set ftype [file type $fullpath] + + + set files [list] + if {$ftype eq "file"} { + set container [file dirname $fullpath] + lappend files $fullpath + } else { + set container $fullpath + #vfs can mask mounted files - so we can't just use 'file type' or glob with -type f + ##set files [glob -nocomplain -dir $fullpath -type f *] + package require punk::nav::fs + set folderinfo [punk::nav::fs::dirfiles_dict $fullpath] + set files [concat [dict get $folderinfo files] [dict get $folderinfo underlayfiles]] + } + set punkcheck_files [paths $container] + set repodict [punk::repo::find_repo $container] + + if {![llength $punkcheck_files]} { + puts stderr "No .punkcheck files found at or above this folder" + } + + set table "" + set files_with_records [list] + foreach p $punkcheck_files { + set basedir [file dirname $p] + set recordlist [punkcheck::load_records_from_file $p] + set tgt_dict [punkcheck::recordlist::records_as_target_dict $recordlist] + + foreach f $files { + set relpath [punkcheck::lib::path_relative $basedir $f] + + if {[dict exists $tgt_dict $relpath]} { + set filerec [dict get $tgt_dict $relpath] + set records [punkcheck::dict_getwithdefault $filerec body [list]] + if {$ftype eq "file"} { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set pcheck \n + foreach irec $records { + append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } else { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set display_records [list] + set pcheck \n + set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] + if {[llength $latest_install_record]} { + lappend display_records $latest_install_record + } + if {$latest_install_record ne [lindex $records end]} { + lappend display_records [lindex $records end] + } + foreach irec $display_records { + append pcheck "[format %-14s [dict get $irec tag]] [punkcheck::dict_getwithdefault $irec -tsiso "no-timestamp"]" + set bodyrecords [punkcheck::dict_getwithdefault $irec body [list]] + set source_files [list] + set source_files_changed [list] + set source_folders [list] + set source_folders_changed [list] + foreach r $bodyrecords { + if {[dict get $r tag] eq "SOURCE"} { + set path [dict get $r -path] + set changed [dict get $r -changed] + switch -- [dict get $r -type] { + file { + lappend source_files $path + if {$changed} { + lappend source_files_changed $path + } + } + directory { + lappend source_folders $path + if {$changed} { + lappend source_folders_changed $path + } + } + } + } + } + if {[llength $source_files]} { + append pcheck " source files : [llength $source_files] (changed [llength $source_files_changed])" + } + if {[llength $source_folders]} { + append pcheck " source folders: [llength $source_folders] (changed [llength $source_folders_changed])" + } + append pcheck \n + + #append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } + append table "$f $pcheck" \n + } + } + } + return $table + } + proc status_by_file {{path {}}} { + if {$path eq {}} { set path [pwd] } + set fullpath [file normalize $path] + set ftype [file type $fullpath] + set files [list] + if {$ftype eq "file"} { + set container [file dirname $fullpath] + lappend files $fullpath + } else { + set container $fullpath + set files [glob -nocomplain -dir $fullpath -type f *] + } + set punkcheck_files [paths $container] + set repodict [punk::repo::find_repo $container] + + if {![llength $punkcheck_files]} { + puts stderr "No .punkcheck files found at or above this folder" + } + + set table "" + set files_with_records [list] + foreach p $punkcheck_files { + set basedir [file dirname $p] + set recordlist [punkcheck::load_records_from_file $p] + set tgt_dict [punkcheck::recordlist::records_as_target_dict $recordlist] + foreach f $files { + set relpath [punkcheck::lib::path_relative $basedir $f] + + if {[dict exists $tgt_dict $relpath]} { + set filerec [dict get $tgt_dict $relpath] + set records [punkcheck::dict_getwithdefault $filerec body [list]] + if {$ftype eq "file"} { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set pcheck \n + foreach irec $records { + append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } else { + if {![llength $records]} { + set pcheck "(has file record but no installation entries)" + } else { + set display_records [list] + set pcheck \n + set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] + lappend display_records $latest_install_record + if {$latest_install_record ne [lindex $records end]} { + lappend display_records [lindex $records end] + } + foreach irec $display_records { + append pcheck "[format %-14s [dict get $irec tag]] [punkcheck::dict_getwithdefault $irec -tsiso "no-timestamp"]" + set bodyrecords [punkcheck::dict_getwithdefault $irec body [list]] + set source_files [list] + set source_files_changed [list] + set source_folders [list] + set source_folders_changed [list] + foreach r $bodyrecords { + if {[dict get $r tag] eq "SOURCE"} { + set path [dict get $r -path] + set changed [dict get $r -changed] + switch -- [dict get $r -type] { + file { + lappend source_files $path + if {$changed} { + lappend source_files_changed $path + } + } + directory { + lappend source_folders $path + if {$changed} { + lappend source_folders_changed $path + } + } + } + } + } + if {[llength $source_files]} { + append pcheck " source files : [llength $source_files] (changed [llength $source_files_changed])" + } + if {[llength $source_folders]} { + append pcheck " source folders: [llength $source_folders] (changed [llength $source_folders_changed])" + } + append pcheck \n + + #append pcheck [punk::tdl::prettyprint [list $irec] 1] \n + #append pcheck " $irec" \n + } + } + } + append table "$f $pcheck" \n + } + } + } + return $table + } +} + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punkcheck::cli::lib { + namespace path ::punk::mix::util ;#askuser, do_in_path, foreach-file etc + + proc find_nearest_file {{path {}}} { + if {$path eq {}} { set path [pwd] } + set folder [lib::scanup $path lib::is_punkchecked_folder] + if {$folder eq ""} { + return "" + } else { + return [file join $folder .punkcheck] + } + } + + proc is_punkchecked_folder {{path {}}} { + if {$path eq {}} { set path [pwd] } + foreach control { + .punkcheck + } { + set control [file join $path $control] + if {[file isfile $control]} {return 1} + } + return 0 + } + + proc scanup {path cmd} { + if {$path eq {}} { set path [pwd] } + #based on kettle::path::scanup + if {[file pathtype $path] eq "relative"} { + set path [file normalize $path] + } + while {1} { + # Found the proper directory, per the predicate. + if {[{*}$cmd $path]} { return $path } + + # Not found, walk to parent + set new [file dirname $path] + + # Stop when reaching the root. + if {$new eq $path} { return {} } + if {$new eq {}} { return {} } + + # Ok, truly walk up. + set path $new + } + return {} + } + +} + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punkcheck::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command status + package require punk::mix::base + package require punk::overlay + punk::overlay::custom_from_base [namespace current] ::punk::mix::base +} + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punkcheck::cli [namespace eval punkcheck::cli { + variable version + set version 0.1.0 +}] +return + + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.1.9.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.1.9.tm new file mode 100644 index 00000000..b8f4dec0 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.1.9.tm @@ -0,0 +1,3078 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + + #'tag' is an identifier for the log source. + # each tag will use it's own thread to write to the configured log target + proc open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + package require shellthread + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #note new_worker + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + + +namespace eval shellfilter::ansi { + #maint warning - + #ansistrip from punk::ansi is better/more comprehensive + proc stripcodes {text} { + #obsolete? + #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). + dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. + #line endings can theoretically occur within an ansi escape sequence (review e.g title?) + set inputlist [split $text ""] + set outputlist [list] + + #self-contained 2 byte ansi escape sequences - review more? + set 2bytecodes_dict [dict create\ + "reset_terminal" "\033c"\ + "save_cursor_posn" "\u001b7"\ + "restore_cursor_posn" "\u001b8"\ + "cursor_up_one" "\u001bM"\ + ] + set 2bytecodes [dict values $2bytecodes_dict] + + set in_escapesequence 0 + #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls + set i 0 + foreach u $inputlist { + set v [lindex $inputlist $i+1] + set uv ${u}${v} + if {$in_escapesequence eq "2b"} { + #2nd byte - done. + set in_escapesequence 0 + } elseif {$in_escapesequence != 0} { + set escseq [dict get $escape_terminals $in_escapesequence] + if {$u in $escseq} { + set in_escapesequence 0 + } elseif {$uv in $escseq} { + set in_escapseequence 2b ;#flag next byte as last in sequence + } + } else { + #handle both 7-bit and 8-bit CSI and OSC + if {[regexp {^(?:\033\[|\u009b)} $uv]} { + set in_escapesequence CSI + } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { + set in_escapesequence OSC + } elseif {$uv in $2bytecodes} { + #self-contained e.g terminal reset - don't pass through. + set in_escapesequence 2b + } else { + lappend outputlist $u + } + } + incr i + } + return [join $outputlist ""] + } + +} +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method flush {ch} { + return "" + } + method write {ch bytes} { + set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + foreach v $o_datavars { + append $v $stringdata + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method write {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [encoding convertfrom $o_enc $bytes] + if 0 { + if {"utf-16le" in [encoding names]} { + set logdata [encoding convertfrom utf-16le $bytes] + } else { + set logdata [encoding convertto utf-8 $bytes] + #set logdata [encoding convertfrom unicode $bytes] + #set logdata $bytes + } + } + #set logdata $bytes + #set logdata [string map [list \r -r- \n -n-] $logdata] + #if {[string equal [string range $logdata end-1 end] "\r\n"]} { + # set logdata [string range $logdata 0 end-2] + #} + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $logdata + #return $bytes + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [dict get $tf -encoding] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_colour + variable o_do_colour + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method Trackcodes {chunk} { + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + append emit $o_do_colour$pt$o_do_normal + #append emit $pt + } else { + append emit $pt + } + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } else { + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + append emit $o_do_colour$trailing_pt$o_do_normal + } else { + append emit $trailing_pt + } + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string last \x1b $buf] == [llength $buf]-1} { + #only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit [string range $buf 0 end-1] + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[llength $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + } else { + set emit_anyway 1 + } + } else { + set possible_code_len [expr {[llength $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set emit "" + } else { + set emit_anyway 1 + } + } + } + } + if {$emit_anyway} { + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + set emit $o_do_colour$buf$o_do_normal + } else { + set emit $buf + } + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + set emit $o_do_colour$buf$o_do_normal + } else { + set emit $buf + } + #set emit $buf + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + return + } + method write {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set streaminfo [my Trackcodes $instring] + set emit [dict get $streaminfo emit] + if {[dict get $streaminfo stacksize] == 0} { + #no ansi on the stack - we can wrap + #review + set outstring "$o_do_colour$emit$o_do_normal" + } else { + set outstring $emit + } + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $outstring] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + #todo - implement as oo + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + set t [textblock::class::table new $tableprefix] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + set stack [lreplace $stack $idposn end] + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + set stack [lreplace $stack $bottom_pop_posn end] + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + #add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + ::shellfilter::log::open $tag {-syslog ""} + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create \ + -tag redirected_$chan -syslog "" -file ""\ + ] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list \ + \r cr\ + \n lf\ + {"} doublequote\ + {'} singlequote\ + "`" backquote\ + "^" caret\ + \t tab\ + " " sp\ + "\[" lsquare\ + "\]" rsquare\ + "(" lbracket\ + ")" rbracket\ + "\{" lbrace\ + "\}" rbrace\ + \\ backslash\ + / forwardslash\ + ] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list \ + "\"" [list "\"" "\"" "\""]\ + {^} [list "\"" "\"" "^"]\ + "'" [list "'" "'" "'"]\ + "\{" [list "\{" "\}" "\}"]\ + {[} [list {[} {]} {]}]\ + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create \ + -teehandle command \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -tclscript 0 \ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if $experiment { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list \ + -timeout \ + -outprefix \ + -errprefix \ + -debug \ + -copytempfile \ + -outbuffering \ + -errbuffering \ + -inbuffering \ + -readprocesstranslation \ + -outtranslation \ + -stdinhandler \ + -outchan \ + -errchan \ + -inchan \ + -teehandle\ + ] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {([llength $args] % 2) != 0} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -outbuffering none \ + -errbuffering none \ + -readprocesstranslation auto \ + -outtranslation lf \ + -inbuffering none \ + -timeout 900000\ + -outprefix ""\ + -errprefix ""\ + -debug 0\ + -copytempfile 0\ + -stdinhandler ""\ + ] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + chan configure $wrerr -blocking 0 + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem\ + {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + }\ + {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list \ + [chan configure $inchan -buffering] \ + [chan configure $outchan -buffering] \ + [chan configure $errchan -buffering] \ + ] + + set remember_in_out_err_translation [list \ + [chan configure $inchan -translation] \ + [chan configure $outchan -translation] \ + [chan configure $errchan -translation] \ + ] + + + + + + chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + after $timeout [string map [list %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }] + + + vwait $waitvar + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.1.9 +}]