AMG: I developed the following code so I could have multiple "instantiations" of a static-enabled proc, where each instantiation has its own independent copies of the static variables. All instantiations share the same bytecode-compiled proc body.
proc sproc {name args statics script} { upvar #0 Sproc-$name data if {[info exists data]} { error "sproc \"$name\" already exists" } set data(next) 0 set data(instances) [dict create] set data(lambda) [list $args\ "dict with [list [namespace current]::Sproc-$name](\[list\ \[namespace tail \[lindex \[info level 0\] 0\]\]\]) {$script}"] set data(statics) $statics proc $name {args} { set name [list [namespace tail [lindex [info level 0] 0]]] upvar #0 Sproc-$name data set others [info commands $name*] while {1} { set nameid $name$data(next) incr data(next) if {$nameid ni $others} { break } } dict set data(instances) $nameid "" set data($nameid) [dict replace $data(statics) {*}$args] proc $nameid {*}$data(lambda) trace add command $nameid delete [list apply {{name cmd args} { upvar #0 Sproc-$name data unset data([list [namespace tail $cmd]]) dict unset data(instances) [namespace tail $cmd] }} $name] return $nameid } trace add command $name delete [list apply {{name args} { upvar #0 Sproc-$name data foreach instance [dict keys $data(instances)] { catch {rename $instance ""} } unset data }} $name] return $name }
[sproc name args statics script] creates a sproc called name that accepts arguments args, has static variables statics, and has body script. It returns name.
[name ?var1 val1 var2 val2 ...?], where name is a previously-defined sproc, creates an instance of the sproc with optionally overridden or augmented static variables var1, var2, etc. assigned to have initial values val1, val2, etc., respectively. It returns the name of the instance.
[instance ?...?], where instance is a name returned by the name sproc invocation, calls the sproc instance. Arguments are passed as normal. The sproc script body is able to freely access and modify static variables, which persist between subsequent invocations of the same instance. Static variables are not shared between separate instances of the same sproc.
Deleting instance (a sproc instance) with rename instance "" removes it from the sproc's associated data structure.
Deleting name (a sproc) with rename name "" deletes its associated data structure and all its instances.
Each sproc has an associated data structure named Sproc-name, where name is the name of the sproc. It is an array containing the following variables:
% sproc counter {{increment 1}} {value 0} {incr value $increment} counter % set p [counter] counter0 % set q [counter value 10] counter1 % $p 1 % $p 5 6 % $q 0 10 % # Show internal data structure % parray Sproc-counter Sproc-counter(counter0) = value 6 Sproc-counter(counter1) = value 10 Sproc-counter(instances) = counter0 {} counter1 {} Sproc-counter(lambda) = {{increment 1}} {dict with ::::Sproc-counter([list [namespace tail [lindex [info level 0] 0]]]) {incr value $increment}} Sproc-counter(next) = 2 Sproc-counter(statics) = value 0 % # Demonstrate instance deletion % rename $p "" % parray Sproc-counter Sproc-counter(counter1) = value 10 Sproc-counter(instances) = counter1 {} Sproc-counter(lambda) = {{increment 1}} {dict with ::::Sproc-counter([list [namespace tail [lindex [info level 0] 0]]]) {incr value $increment}} Sproc-counter(next) = 2 Sproc-counter(statics) = value 0 % # Demonstrate sproc deletion % rename counter "" % parray Sproc-counter "Sproc-counter" isn't an array % $q invalid command name "counter1"
Static variables cannot be arrays. Use dicts instead. This makes it impossible to create traces or upvar aliases on individual elements of an array-like static variable.
Traces can be set on static variables, but they have to be set inside the sproc script body. In other words, they have to be recreated every time the sproc instance is invoked. They will not detect access to the static variables outside of the sproc script body, e.g. by directly modifying the internal data structure.
Renaming sprocs or sproc instances is not supported. The only allowed use of [rename] is to delete sprocs and sproc instances.
AMG: I wanted to avoid customizing each sproc instance's script body, so I needed some other way of communicating its unique ID into the (generic) script body. Since I want this code to be easy to use, this must be done automatically. I guess I could have used [interp alias] to curry an ID argument into the invocation, but I didn't want to confuse [info args]. Instead I took advantage of the proc name being a sort of hidden parameter, which is accessed using [lindex [info level 0] 0]. In the case of the sproc (the procedure that creates instances), this gives the name of the sproc. In the case of the instances themselves, this gives the instance name, and the sproc name was already compiled into the lambda and doesn't need to be looked up dynamically.
To make accessing the sproc or instance data easier, I used [upvar #0] to link a global variable to a local variable simply named "data".
I didn't want to generate any procs not intended to be called by the user of this code, so I instead used [apply] to do the [trace] scripts.
I did my best to facilitate bytecode sharing between all sprocs or between all instances of any given sproc. To do the former, I wrote generic code that dynamically figures out the sproc name using the trick described above. To do the latter, I generate one lambda to be used for all instances of a sproc, again dynamically figuring out the instance name. The name works somewhat like a "this" pointer in C++.
I was disappointed to discover that [info level 0] is not fooled by [interp alias], that it gives the real target proc name not the alias. If not for this, I would have simply made a generic instance proc and instantiated the sproc by creating aliases. But so it turned out, I had to make a separate proc for each instance. (This is better anyway because it enables deletion through rename instance "".) I wanted for the separate procs to share bytecode, so I wrote proc $nameid {*}$data(lambda).
To support deleting all instances when the sproc is deleted, I must maintain a list of instances. But I didn't want to write code to search-and-destroy through the instance list on instance deletion, so I used [dict unset] on a dict with empty string for the values. In this way I make a set using a dict.
AMG: Do all sproc instances really share the same bytecode-compiled script body? Somebody please confirm this for me.
AMG: How well does this code work with namespaces?
[Category ???] |
---|