Arrange for a kind of non-local exit called an escape. For C-ers, the escape expression is a generalization of break, continue, return and setjmp/longjmp. For Schemers, this is like a dynamic-extent call/cc, except that any finally clauses of intervening try-finally expressions are executed on the way out. It creates an ejector function, binds it to pattern, and evaluates the contained eExpr. If the ejector function is called during execution of the contained expression, the stack is unwound to the escape expression, and the escape expression as a whole succeeds, evaluating to the ejector function's argument. Otherwise, the escape expression evaluates to the outcome of the contained expression.
When the escape expression is evaluated, it will first generate a new primitive ejector function. The escape expression will then match the only reference to this ejector against the pattern. If this match fails or escapes, then the ejector is disabled and the same non-local exit happens as if the corresponding "def pattern := ejector" had exited non-locally instead. (By "ejector function", we mean that an ejector is an object whose major behaviors are defined by its "run" methods. An ejector has both zero-argument and one-argument "run" methods. When we refer to "calling the ejector function", we mean specifically calling it as a function, ie, invoking its one of its "run" methods.) If the match succeeds, then the contained expression is evaluated. If, during this evaluation, the run/0 or run/1 method of the ejector is called, then a non-local exit is initiated, which we call an escape. The other form of non-local exit in E is the failure, caused by throwing a problem. Note that try-finally expressions may intervene and substitute a different non-local exit (ie, a finally-clause which fails, or escapes using a different ejector function) In this case, the ejector remains valid and we continue processing as if we simply had an instance of the substituted non-local exit. The contained expression may then exit in one of three ways:
In all cases, the ejector is disabled when the escape expression exits. Once an ejector is disabled, it is forever disabled, and the run methods on a disabled ejector will simply throw an informative (XXX to be specified) exception. It should not be possible for an ejection with ejector x to happen unless the escape expression that created x is higher on the call stack. In particular, it should not be possible for the top-level expression of a turn to exit with an ejection. The escape expression is all in one scope box, so the variables defined in the pattern are visible in the contained expression, but no variables defined in the escape expression are visible it the succeeding scope. ExamplesThe Simple CaseThis is the basic example, showing that "escape" binds the variable "x" to an ejector function. The variable name "x" is visible from its defining occurence left-to-right until the end of the escape expression that defines it -- normal lexical scoping. Like all variables in E, "x" is a normal lexical variable. Like all values in E, the ejector is first-class. Therefore, "x" can be passed, returned, captured in closures, stored in data structures, etc. However, the ejector "x" is bound to is only enabled during the execution of this same escape expression -- meaning its special power is only of dynamic extent. Afterwards, it is disabled. What is an ejector's special power? To unwind the stack to cause early termination of the escape expression that created it, and to cause that escape expression to evaluate to the value provided as argument to the ejector. Since "x" initially holds the only reference to the ejector and there are no magic construct for obtaining an already-created ejector, this power follows normal capability discipline. ? pragma.syntax("0.8") ? escape x { > print("foo") > x(3) > print("bar") > 7 > } # value: 3 # stdout: foo Above we see that the first print happened, but not the second, since the call to the ejector stopped all further processing. The escape expression as a whole evaluated to 3, not 7, since that was the ejector's argument. ? escape x { > print("foo") > x() > print("bar") > 7 > } == null # value: true # stdout: foo "x()" is equivalent to "x(null)". The Special Keyword ShorthandsIn order to support the expectations from the C syntactic tradition, E provides the keywords "break", "continue", and "return" as syntactic shorthands for invoking functions bound to variables named "__break", "__continue", and "__return". These invocations can be written in a C-like syntax, but expand to the corresponding function call. For example, "return 3" as an expression expands to "__return(3)", and "return" expands to "__return()". The keywords are, of course, reserved. The variable names are not reserved, but "__break" and "__continue" are implicitly bound by escape expressions generated by expansion of the synyactic loop shorthands ("for" and "while"). We reserve the right to introduce an expansion that generates an "escape __return {.." as well. ? escape __return { > return 3 > } # value: 3 Invoking a Disabled EjectorThis demontrates the dynamic extent of ejectors. ? var x1 := null > escape x2 { > x1 := x2 > } > x1(3) # problem: Failed: Ejector must be enabled The try-catch Expression Only Catches FailureBoth failures and escapes are forms of non-local exit that causes the stack to unwind. Both failures and escapes must unwind try-catches, since these are also dynamic extent. In the Java implementation of E, they are even both implemented by throwing Java Throwables, and can therefore both be caught only by Java try-catches. But in the E language, they are quite distinct. While unwinding, matching catch clauses will catch failures, whereas catch clauses cannot catch escapes. ? escape x { > try { > x(3) > } catch problem { > print(`oops: $problem`) > 7 > } > } # value: 3
Implementations of E prior to 0.8.21d would confuse escapes for failures on occasion, and for the above code would print the "oops..." and return the 7. Try-Finally Catches AllUnlike the try-catch expression, which only catches failure, the try-finally expression catches all three kinds of completion: success, failure, and escape. ? escape x { > try { > x(3) > } finally { > print("foo") > } > } # stdout: foo # value: 3 Above, the finally clause printed "foo" during the stack unwinding caused by the escape. Since the above finally clause succeeded, the original escape then continued to unwind. Masking Previous Non-Local ExitsIf the finally clause itself does a non-local exit, this replaces the current exit as the cause for further stack unwinding. ? escape x { > try { > x(3) > } finally { > throw("foo") > } > } # problem: foo The throw("foo") causes the x(3) to be forgotten. Ejectors Aren't Use-OnceWhen a finally clause is executed while unwinding an escape, the escape isn't done yet, so the ejector is still enabled. The ejector isn't disabled by calling it, only by exit from its escape expression. If it gets reused while it's still alive, this is simply another case of a new non-local exit replacing the current one. ? escape x { > try { > x(3) > } finally { > x(4) > } > } # value: 4 The x(4) causes the x(3) to be forgotten. Match FailureA corner case that's particularly easy for an implementor to miss is, what happens if the pattern at the head of the escape expression fails to match? ? var x1 := null > def stealer(x2) :boolean { > x1 := x2 > false > } > try { > escape x3 ? stealer(x3) { 7 } > } catch p { > stderr.print(`oops: $p`) > 9 > } > x1(3) # stderr: oops: problem: such-that expression was false # problem: Failed: Ejector must be enabled Above, the pattern reads "bind to x3 such that stealer(x3) is true". We wrote stealer to squirrel away the ejector and then always cause the pattern to fail. This failure causes a problem to thrown, and the surrounding try-catcher catches this problem and prints it. However, we still have ahold of the ejector in the x1 variable, and the escape expression was mostly skipped. Clearly, even in this case, when we leave the escape expression the ejector must be disabled. The above code shows that it is. |
||||||||||||||||||||||||
Unless stated otherwise, all text on this page which is either unattributed or by Mark S. Miller is hereby placed in the public domain.
|