TIP #90 Version 1.3: Enable [return -code] in Control Structure Procs

This is not necessarily the current version of this TIP.


TIP:90
Title:Enable [return -code] in Control Structure Procs
Version:$Revision: 1.3 $
Author:Don Porter <dgp at users dot sf dot net>
State:Draft
Type:Project
Tcl-Version:8.4
Vote:Pending
Created:Friday, 15 March 2002

Abstract

This TIP analyzes existing limitations on the coding of control structure commands as procs, and presents expanded forms of catch and return to remove those limitations.

Background

It is a distinguishing feature of Tcl that everything is a command, including control structure functionality that in many other languages are part of the language itself, such as if, for, and switch. The command interface of Tcl, including both a return code and a result, allows extensions to create their own control structure commands.

Control structure commands have the feature that one or more of their arguments is a script, often called a body, meant to be evaluated in the caller's context. The control structure command exists to control whether, when, in what context, or how many times that script is evaluated. When the body is evaluated, however, it is intended to behave as if it were interpreted directly in the place of the control structure command.

The built-in commands of Tcl provide the ability for scripts themselves to define new commands. Notably, the proc command makes this possible. In addition, other commands such as catch, return, uplevel, and upvar offer enough control and access to the caller's context that it is possible to create new control structure commands for Tcl, entirely at the script level.

Almost.

There is one limitation that separates control structure commands created by proc from those created in C by a direct call to Tcl_Create(Obj)Command. It is most easily seen in the following example that compares the built-in command while to the command control::do created by proc in the control package of tcllib.

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

The control structure command control::do fails to evaluate return -code error in such a way that it acts the same as if return -code error was evaluated directly within proc b.

Analysis

There are two deficiencies in Tcl's built-in commands that lead to this incapacity in control structure commands defined by proc.

First, catch is not able to capture the information. Consider:

   %  set code [catch {
          return -code error -errorinfo foo -errorcode bar baz
      } message]

After evaluation, code contains "2" (TCL_RETURN), message contains "baz", ::errorInfo contains "foo", and ::errorCode contains "bar". But there is nothing accessible at the script level that contains the value passed to the -code option. That code is stored internally in the Tcl_Interp structure as interp->returnCode, but scripts cannot access that value.

Second, even if the information were available, there is no built-in command in Tcl that can be evaluated within the body of a proc to make the proc itself act as if it were the command return -code. Stated another way, it is not possible to create a command with proc that behaves exactly the same as return -code. Because of that, it is also not possible to create a command with proc that behaves exactly the same as while, if, etc. - any command that evaluates any of its arguments as a script in the caller's context.

This is a curious, and likely unintentional, limitation. Tcl goes to great lengths to be sure I can create my own break replacement with proc.

 proc myBreak {} {return -code break}

It would be a welcome completion of Tcl's built-in commands to be able to create a replacement for every one of them using proc.

Alternatives

For now, I will present a few alternatives, without proposing any one of them in particular, to elicit comments. Later revisions of this Draft TIP will select and propose one of the alternatives on the basis of community discussion.

  1. Do nothing.

    We can maintain the status quo, and just accept that it is not possible to create fully functional control structure commands with proc. If you want to create such a command, it must be coded in C.

    Note that this limitation is one of the main reasons that TIP #89 proposes to add a try command directly to Tcl, rather than implementing it in a package. This limitation forces try to be coded in C, and then the lack of good distribution mechanisms for C-coded packages motivates inclusion in Tcl itself. If we choose to do nothing, we can expect more proposals to fill Tcl with more control structure commands.

  2. Extend catch

    Dealing with the deficiency in catch is not difficult, and may be desirable even if we choose not to correct the other deficiency. An optional additional argument to catch can be a variable name in which to store the interp->returnCode value that is currently not accessible. Syntax would be:

       catch script ?msgVarName? ?returnCodeVarName?
    

    Only when catch is returning 2 (TCL_RETURN), and when the returnCodeVarName argument is provided, then catch would store the value of interp->returnCode in the variable named $returnCodeVarName.

  3. Add a scalar-valued switch/argument to return -code return.

    This would be in addition to alternative 2.

    The built-in commands of Tcl enable proc to create a replacement for the return command with no arguments or only the result argument.

       proc myReturn {{message {}}} {return -code return $message}
    

    The myReturn example could be further extended to silently ignore any -errorinfo or -errorcode switches, since they have no relevance when not returning an error. It is the -code switch that myReturn cannot emulate.

    Since return -code return solves most of the problem, we can consider extending its syntax to solve the rest. Perhaps an additional -returncode switch:

       return -code return -returncode $returnCode ...
    

    Or perhaps an additional optional argument:

       return -code return ... ?$result? ?$returnCode?
    

    An additional field within the Tcl_Interp structure would need to be added for storage of the value of the returnCode argument, so that that value could be transferred into the interp->returnCode field by the caller.

    Advantage

    it's a simple change.

    Disadvantage

    it just pushes the incompleteness problem down one level.

    After this change, commands defined by proc would be able to behave exactly the same as the command return -code $code. However, then commands created by proc would not be able to behave exactly as the command return -code return -returncode $code. Said another way, this change will enable access to the returnCode field of the Tcl_Interp structure, but does so only by creating a new field in the Tcl_Interp structure that we do not have access to. So there would still be a fundamental incompatibility between commands created by proc and those coded in C. But perhaps that change is enough to reduce the incompatibility to the point that it is no longer important.

  4. Add a list-valued switch/argument to return -code return.

    The incompleteness remaining after the combination of alternatives 2 and 3 could be solved by allowing the value of returnCode to be a list in both the extended catch and the extended return -code return. For example, after evaluation of

        set code [catch {
            return -code return -returncodes $oldReturnCodes baz
        } message returnCodes]
    

    code contains "2" (TCL_RETURN), message contains "baz", and returnCodes contains [linsert $oldReturnCodes 0 2]. For other arguments to -code:

        set code [catch {
            return -code $value ... baz
        } message returnCodes]
    

    returnCodes contains [list $value]. The additional field of the Tcl_Interp structure would hold the list, and the caller would pop the first element of that list and place it in the returnCode field of the Tcl_Interp structure when handling a return -code return situation.

    I believe this could work, but I'll need to prototype it to see if and where it might lead to trouble.

    Also, it appears that the first N-1 elements of the returnCodes list might always be TCL_RETURN. In that case, a simple pair of the last return code and the number of levels deep would be a simpler way to describe the required information.

    Advantage

    appears to be a complete solution.

    Disadvantage

    still have to figure it out.

  5. Add a new return code, TCL_EVAL.

    The problem that one cannot cause a proc to behave as if it were the command return -code can be viewed as a special case of the general weakness that one cannot make a proc behave as if it were replaced by an arbitrary Tcl command. This general power could be granted via a new return code, TCL_EVAL.

    When detecting a TCL_EVAL return code from a command, the result of the command would be treated as a new command to be evaluated in the same context as the first. The result and return code of the new command would take the place of the original.

    With that capability, combined with alternative 2, one could easily take care of the problem posed in the analysis above:

        set code [catch {uplevel 1 $body} message returnCode]
        if {$code == 2} {
            return -code eval [list return -code $returnCode $message]
        }
    
    Advantage

    A complete solution that's easy to understand.

    Disadvantage

    Very powerful technique that might enable things we don't want to enable. In particular, without great care, and perhaps some special case handling that could be difficult to document, it appears this would open up a way for a safe interp to force code to be evaluated in a trusted interp.

Generalization

The particular problem and solutions presented here can be seen as the remedy to a general incapacity of return and catch. Most fundamentally, the purpose of return is to allow a proc to set its return code and its result. However, it has always been the case that one particular return code, TCL_ERROR, has required additional information. Thus, return has always recognized the -errorcode and -errorinfo options, and has stuffed the values into global variables ::errorCode and ::errorInfo so that those values can be accessible to a later catch.

The current motivation shows that another return code, TCL_RETURN, also needs to be able to pass additional information. One alternative has been to add yet another special purpose option, -returncodes, to return. Rather than invent another global variable, another argument has been proposed for catch as a cleaner way for it to collect the additional information.

Could it not be the case that other return codes might also benefit from the ability to pass additional information? This could be accomplished by having return take all option value pairs, and add them to a list, stored in a Tcl_Obj-valued field in the Tcl_Interp structure. We already permit new return codes to be defined. This would add a simple way for those custom return codes to pass along arbitrary additional information. The additional argument to catch would then be the name of a variable to be assigned the value of that entire list.

One complication is the need for a policy or convention for naming the options. Note that return forces all option value pairs to come before the result argument. Thus, there is no technical requirement for a leading "-" character in the option value. For consistency with what has gone before, though, such a requirement might not be a bad idea to enforce. Then, in order that the additional information associated with different return code would not conflict, a prefix convention could be adopted where each return code would define additional options with the prefix -$code. For the built-in return codes, both the numeric and word forms would be acceptable, so -errorinfo and -1-info would refer to the same option. The values stored in the list variable named by the last catch argument would all use the numeric prefixes.

Under that proposal, one could completely reproduce the return code, result, and additional information of a script, caught by catch like so:

 set code [catch {uplevel 1 $script} message args]
 eval [list return -code $code] $args [list $message]

Most control structure commands would add processing between the two commands above to handle whatever custom return codes they honor.

One big advantage to this approach is that the -errorcode and -errorinfo arguments are preserved through an arbitrary amount of nesting, so there's no need to keep saving and restoring the values of global variables ::errorCode and ::errorInfo at each level of processing.

For compatibility reasons, the options -errorcode and -errorinfo would have to continue to be handled through existing mechanisms at least through the Tcl 8.x releases. However, once the more general approach was established and included an appropriate public C interface, the use of special routines like Tcl_AddErrorInfo and its relatives could be deprecated. The errorCode and errorInfo' fields of the Tcl_Interp structure could also go away. In the very long run, the use of global variables ::errorCode and ::errorInfo'' could be phased out as well.

A related interface is the bgerror command that accepts only a single message argument, relying on the global variables ::errorCode and ::errorInfo to access the additional information. This is good enough for the TCL_ERROR return code. However, Tcl_BackgroundError is called for any non-TCL_OK return code, but is not passed what the return code is, so all non-TCL_OK codes get treated like errors. This should be cleaned up. Possibly, Tcl_BackgroundError should be called only for errors. Possibly, other non-TCL_OK codes should be more explicitly converted to TCL_ERROR along with a more explicit error message. Or, possibly, Tcl_BackgroundError and bgerror need extending to detect and handle different non-TCL_OK return codes differently. In the last case, the additional return information needs to be passed to bgerror.

Acknowledgment

Thanks to Joe English for pointing out the opportunities for generalization in this proposal.

See also

Documentation for tcllib's control package: http://tcllib.sf.net/doc/control.html

Copyright

This document has been placed in the public domain.


Powered by TclThis is not necessarily the current version of this TIP.

TIP AutoGenerator - written by Donal K. Fellows