Conditions in TOM are modeled after Common Lisp conditions (see Common Lisp the Language, 2nd edition ), with some simplifications. Conditions are not like exceptions in languages like C++ or Java, the most important difference being that the stack is not unwound while the condition is being handled.
Creation and issuing of conditions is functionality of the TOM runtime library. Handling conditions is part of the TOM language. We'll start the discussion of these two intertwined subjects with catching things that are thrown.
TOM provides exactly one way to perform a non-local goto: throwing something at an object, by invoking the throw method that is defined by the All instance, as in:
[my_object throw void]; |
Here, void is not the void type, but the single possible value of that type, which bears the same name. Thus basically, we throw nothing special at the object we know as my_object.
Of course, throwing something is not really interesting if you can not catch it. To setup a catch for a value thrown at an object, use a catch expression:
catch (my_object) [foo do_difficult_with bar]; |
catch is followed by the tag expression in parentheses, followed by a body expression. While the body of the catch is evaluated, anything thrown at the tag will be caught by this catch. In the example, we're catching values for the object we know as my_object.
The tag expression must be an object. The tag can be nil, but that is not very useful, since you will not be able to throw anything at nil.
A catch is an expression and like every expression, it has a type and it produces a value after evaluation. The value of the catch expression is either the value of the body expression or the value thrown at the tag object. For example, the following is an elaborate way to assign to the boolean a whether a1 is larger than a2.
boolean a = catch (self) ({ if (a1 > a2) [self throw TRUE]; FALSE; }); |
When executing, if a1 > a2, the value TRUE will be thrown at the catch: execution of the body expression is terminated, the stack is unwound up to the stack frame of the catch (think longjmp if you like), and the value TRUE is returned as the value of the catch expression. In the other case, if a1 is not larger than a2, the body of the if is not executed, and the result of the catch is the value of the last expression, FALSE.
Incidentally, note the parentheses around the body compound of catch: the body of a catch must be an expression, and a compound can only be an expression as an element of a tuple. (The tuple has only one element in this case.)
It is an error if the type of value thrown at an object does not match the type that the catch expects. The one exception to this rule is when the value void is thrown, in which case the value returned by the catch is the default value of the type to be returned (0 for numbers, nil for objects, etc).
A condition is an object, an instance of the Condition class. A condition is normally created by invoking the following method of the Condition class:
instance (id) for All object class ConditionClass condition_class message String message; |
The three arguments provide the values of the three instance variables with the same name, of the Condition object that will be created and returned. The object is the object to which the condition applies, for example the File object for a condition applying to some failed operation on that file. The message is meant to provide a description of the condition, to be read by a human and not to be interpreted by a program. An example of a useful message is the string returned by the C library function perror.
The condition_class is an instance of the ConditionClass class. Each condition-class object has a name, and through their super-condition-class instance variable, the condition class objects describe a single inheritance hierarchy of condition classes.
The Conditions class contains static class variables for the predefined condition classes. For example, one of them is the nil-receiver; it is a runtime-condition, which in turn is a serious-condition, which is a condition. The condition is the root of the condition-class hierarchy; it is the supercondition-class of all other condition classes.
As an example, when a message is sent to nil, the following condition is created:
Condition c = [Condition for nil class nil-receiver message "nil receiver"]; |
The object to which the condition applies is of course nil, since the fact that it is nil was the reason for creating the condition.
Conditions can be issued in two different ways: raised or signaled. When a condition is raised, as in
Condition c = ...; [c raise]; |
execution of the program is guaranteed to not return from the raise method, exiting the program if that is the only way to achieve the goal. A condition is signaled by invoking its signal method:
Any signal; |
An invocation of the signal method may or may not return, depending on the behavior of the installed condition handlers: if one of them performs a non-local goto, the signal invocation will not return.
To start with an example, the following is a main method that I used frequently to observe unhandled condition signals (until the library option :rt-signals was provided that does the same):
int (retcode) main Array arguments { ConditionClass cc = condition; bind ((cc, { [[[stdio err] print ("unhandled condition: ", condition)] nl]; condition; })) retcode = [self real_main arguments]; } |
The bind sets up a condition handler, which will be in place while the body of the bind is active, in this case during the invocation of real_main. When a condition is raised or signaled, all active handlers are considered in reverse order from their creation.
Each handler in a bind is a two-expression tuple; multiple handlers are separated by semicolons. The type of a handler is
(ConditionClass, All) |
The first element is a ConditionClass, indicating to which kind of conditions the handler applies. In the example, the condition class is condition, but a variable with a different name is used to denote it. The reason for this is the fact that within a handler, condition is the name of the condition that is being passed. In the example, a handler is installed that matches any condition with condition class condition, or any subcondition-class thereof.
The second element of a handler is an expression that will be executed to handle a condition. The condition is available in the implicit argument to the handler:
Condition condition; |
A condition handler can do one of three things:
Let the condition pass: in this case the handler decides that the condition is not interesting after all, and that the condition must continue to search for a handler that is willing to handle it. Searching continues with outstanding handlers further on the stack.
A handler shows its desinterest by returning the condition object, as is done in the above example.
Handle the condition: the handler returns any object, just not the condition being handled. What the value that is returned means depends on the kind of condition. In this case, signaling the condition will finish and the invocation of the signal of the condition will return with the value returned by the handler. See below for an example of this usage.
Perform a non-local goto, by throwing some value at some catch tag.
The above description is valid for a condition that is signaled. If a condition is raised, cases 1 and 2 are not discerned (and handled like case 1): if a condition is raised, some handler somewhere along the line must perform a non-local goto, or the program will exit when it has run out of possible handlers. A handler can ask a condition whether it is being signaled or raised by invoking its raised method, which returns a boolean.
The unwind expression guarantees that some protection code is executed, either when the body expression has been evaluated, or when the stack frame is cleared because of a non-local goto. In the following example, the unwind ensures that the file is closed no matter what happens:
File file = [File open ...]; unwind ([file close]) ({ /* Do something with the FILE. */ ...; }); |
The value of the unwind expression is the value of the body.
This section presents an example in which conditions are used to give the user control over what happens when a file can not be opened. This setup is only possible because the stack is not unwound while a condition is handled, i.e., because conditions can not only be raised but also signaled.
Suppose a (fictuous) ReadOnlyFile object is opened and created using this method:
ReadOnlyFile (the_file) open String filename { the_file = [[self alloc] initWithFilename filename]; if (![the_file open]) return nil; } |
Thus, opening a file first creates a new object for the given filename, and then lets the file open itself. The implementation of the open method could look like this; hopefully the methods being invoked have descriptive enough names for their missing implementation to not be a problem:
boolean (success) open { for (;;) { /* Try to open, and return TRUE upon success. */ [self attempt_to_open]; if ([self is_open]) return TRUE; /* See if we can get an alternative filename. */ Condition c = [Condition for self class file-open-problem message [self strerror]]; String alternative_name = [c signal]; if (!alternative_name) { /* The condition was not handled. */ return FALSE; } /* Make the ALTERNATIVE_NAME our new name. */ [self set_name alternative_name]; } } |
This method will try to open the file, getting alternative names as long as they are supplied, and finally return TRUE upon success, or FALSE upon failure.
We could use the mechanism provided by the ReadOnlyFile class in a program to offer the user the possibility of specifying an alternative filename, as shown in the following code:
/* Setup a handler for FILE-OPEN-PROBLEM conditions. */ bind ((file-open-problem, { /* Retrieve the file object to which the condition applies. */ ReadOnlyFile file = [condition object]; /* Describe the problem to the user and prompt for input. */ [[[stdio err] print ("trouble opening: ", [file name])] nl]; [[[stdio err] print ("reason: ", [condition message])] nl]; [[[stdio err] print "alternative (RET for original)? "] nl]; /* Read a line of input. */ String input = [[stdio err] readLine]; /* If the input is an empty line, return NIL, indicating that we did handle this condition, but that an alternative filename was not entered. Otherwise, return the filename entered by the user. As long as we do not return the CONDITION object, we will have handled this condition. */ [input length] > 0 ? input : nil; })) ({ file = [ReadOnlyFile open "/foo/bar"]; }); |
Below are two example runs; the program attempts to open the file and prints the value that is returned before exiting:
$ ./rofile foo trouble opening: foo reason: No such file or directory alternative (RET for original)? bar trouble opening: bar reason: No such file or directory alternative (RET for original)? *nil* $ ./rofile foo trouble opening: foo reason: No such file or directory alternative (RET for original)? rofile #<ReadOnlyFile 00201700 name=rofile> $ |