Search code examples
tclcoroutineupvar

Why does this upvar refer to the global context? Or can upvar be used in a coroutine?


The following strained example, in attempt to match my real situation, completes successfully but not the way I'd like it to; nor do I understand why or can find an answer in one of my books.

I can see that proc func was called from the global level and, in turn, initially called proc cofunc; and, perhaps, that sets what upvar will use. But proc finalize calls the coroutine again and passes the variable name nbr to cofunc--which calls add. In this respect add "seems" to be two levels from finalize.

The upvar 2 $varName n in proc add is the variable name nbr and takes its value from the global variable of that name. I'd like to get it from proc finalize (so 15, not 85) which seems to be two levels away from proc add but apparently is not even in the list of caller contexts.

I assume [yield] is the reason; but would you please explain why and, more importantly, if the value in proc finalize was not a number but a "large" value that shouldn't be passed around, is it possible to upvar or uplevel from a coroutine?

Thank you for considering my question.

proc cofunc {callback vals} {
  set var [yield]
  $callback $vals $var
}
proc func {callback vals} {
  set nbr 22
  coroutine coro cofunc $callback $vals
}
proc add {vals varName} {
  upvar 2 $varName n
  chan puts stdout "varName: $varName; n: $n"
  # => varName: nbr; n: 85
  chan puts stdout [expr {[lindex $vals 0]+[lindex $vals 1]+$n}]; 
  # => 115
}
proc finalize {nbr} {
  coro nbr
}

set nbr 85
func add {10 20}
after 500 finalize 15
after 1000 set forever 1
vwait forever

Solution

  • The upvar (like the uplevel command) is defined in terms of the stack. The number argument says how many steps up the stack to take (or you can prefix the number with # to count down from the other end; that's a bit less common); it defaults to 1, the caller of the current procedure. There are two sorts of stack frame, one for procedures (and related entities, such as methods and lambda expressions) and another for direct namespaces (the global scope, namespace eval, and a few other things). All upvar does is go to the indicated stack frame, look up the named variable there, and make a local variable that is a link to it. By "link", I mean that the local variable is really a tagged C pointer to the implementation of the other variable. Once a linked variable is set up, access is very fast, as the slow part of access is resolving names to implementations, and local variables are usually compiled into indexed accesses.

    The global and namespace upvar commands do something very similar, but with different lookup strategies. (Also, global assumes that the global and local names should match.) That's also part of what the variable command does.


    In your code sample, the upvar 2 is to get past the cofunc procedure. I think it would be clearer if it was upvar #0, or if cofunc used tailcall $callback so a simple upvar could be used. And I'm pretty sure that the nbr accessed in add is not the same one as in func, as the latter's stack frame will be gone by then.