Version 1 of returneval

Updated 2002-10-17 22:47:36

A problem in Tcl (which is addressed, in various ways, by TIP #90 [L1 ]) is that the -code option to return has certain limitations; in particular it hides the final return code specified by this option in a place where Tcl scripts cannot access it, and so if a [return -code] is caught before it has caused a procedure body to return then some information will be lost. One consequence of this is that the two procedures defined by

 proc a {} {while 1 {return -code error}}
 package require control
 proc b {} {control::do {return -code error} while 1}

display different behaviour. a returns with an error on the first iteration. b returns without an error on the first iteration. In some sense this difference is a deficiency in control::do, but there is in fact no way to implement it so that it handles this. The language does not support it!

Below are defined some commands that provide a workaround for this. The idea is to have a command [returneval] which not only causes the calling procedure to return but also evaluates a user-supplied command "in place of" the procedure that returned. This lets one define a procedure c through

 eproc c {} {control::do {returneval {error}} while 1}

that behaves just like a. In particular,

 catch {a}
 catch {c}

both return 1 whereas

 catch {b}

returns 0.

What one must keep in mind when considering these matters is how Tcl command return codes work in general, and the manner in which the return return code (2) works in particular. It is really quite simple, but as it is also easily mistaken for being magical, many people are confused by it. Most Tcl commands make a distinction only between two classes of return codes: the ok return code (0), and the other return codes. If they get an ok return code from some recursive invocation of the Tcl interpreter then they do what they usually do. If they get any other return code then they return themselves, passing the nonzero return code (and the corresponding return value / error message) back down to whatever called the interpreter that time. The main exception is of course the catch command, since it just returns the return code of the script it recursively invoked as its return value, whereas its return code is the normal 0.

This quick propagation of a nonzero return code is how errors are transferred back to the outermost caller. This is also how return works: the return code of this command is nonzero (2, to be precise) and the commands surrounding a return will therefore quickly pass this along. If it reaches a catch then it will be caught (and the return value of that catch will be 2), but it usually doesn't. What makes the return command different from the error command in normal usage is however that procedure bodies treat the corresponding return codes differently. An error return code (1) is passed along, but a return return code is intercepted and it will usually change to something different. A simple [return] or [return -code ok] will get the return code ok (0) when they meet a procedure body. A [return -code error] will get the return code error (1) when it meets a procedure body. A [return -code break] will get the return code break (3) when it meets a procedure body. And so on.

Something similar happens with the break and continue commands, although the return codes produced by these are intercepted not only by catch and procedure bodies, but also by the loop commands (for, foreach, while, ...). On the other hand, the uplevel command does not intercept any return codes.

The returneval command makes use of the nonstandard return code -1. The reason for choosing this can be found on the uplevel page.

 proc returneval {script} {return -code -1 $script}

However, this will just behave as an error if it isn't intercepted at some point. Therefore any procedure which you might want to returneval from must be defined using eproc rather than proc.

 proc eproc {name arglist body} {
     interp alias {} $name {} eproc_call "$name "
     proc "$name " $arglist $body
 }
 proc eproc_call {args} {
     set code [catch [list uplevel 1 $args] res]
     if {$code == -1} then {
         set code [catch [list uplevel 1 $res] res]
         return -code $code $res
     } elseif {$code == 1} then {
         global errorInfo errorCode
         return -code error -errorinfo $errorInfo -errorcode $errorCode $res
     } else {
         return -code $code $res
     }
 }

[Also explain why the above works]


DGP Very nice. This could possibly be a way to work around the limitations of the control package commands until a TIP 90 solution is in place.

Some nitpicking: it looks like eproc assumes it is called from the :: namespace. To make this robust, there should be an [uplevel 1 ::namespace current] to discover the namespace context of the caller, then be careful to create both the alias and the proc in that namespace.