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 }