I’ve been researching how the SuperCollider interpreter supports exceptions, and I’ve learned some subtle business involving the method return operator ^, which I thought I’d share.

Lexical Scoping

SuperCollider has two primary mechanisms for holding code, allowing users to define methods on classes and literal functions. Functions support lexical scoping, for instance, in the following code:


Foo {
    *bar {
        var first = 1;
        var second = 2;
        var func = { first = first + second; first; };
        ^func;
    }
}

(
var func = Foo.bar;
func.value.postln;  // prints 3
func.value.postln;  // prints 5
func.value.postln;  // prints 7
)

Not only are the local variables first and second accessible in the literal function func, but the interpreter also persists their state with func.

Method Returns

Notice the ^func command on the last line of the *bar class method in the previous example. The method return operator is necessary here because if a method doesn’t specify a return value, the interpreter supplies this as the return. Since this is a class method, omitting that ^func line means that calls to Foo.bar will return the class Foo as the value of this.

So why not add a ^first line to the function definition of func? Why the naked first; statement? Unlike methods, functions always return the last value evaluated. I could omit the first; statement because the value of the assignment first = first + second is first, but I’ve included it for clarity.

Some folks have called the caret, ^, the method return operator, to clarify its behavior. And, if you use it inside a function, it will return to the caller frame on function creation:


Fizz {
    *buzz {
        var func = { |val| ^val; };
        "beginning".postln;
        func.value("middle").postln;
        "end".postln;
    }
}

(
Fizz.buzz;  // prints "beginning", returns "middle"
)

What happened there is the ^val inside of func returned to the caller from *buzz directly.

Lexical Scoping Includes Method Return

For regular programming in SuperCollider, most folks have internalized the guidance that they should avoid the method return operator in functions because of reasons, and leave it at that. However, researching exception support in the class library taught me an interpreter edge case when we combine the lexical scoping and method return language features. Consider the following simplified exception-handling code:


Handler {
    classvar <handler;

    *wrap { |func|
        var value = Handler.prWrap(func);
        ^value;
    }

    *prWrap { |func|
        var value;
        handler = { |error| ^error; };
        value = f.value;
        ^value;
    }
}

(
var func = { Handler.handler.value(\handled); \unhandled; };
Handler.wrap(func);  // returns 'handled'
)

In this example, wrap calls prWrap, creates the handler function, saving wrap’s stack frame as the caller. When func invokes handler, the method return in handler returns to wrap regardless of the invocation context. In other words, the interpreter includes the caller frame in a function’s lexical scope.

It’s possible to construct functions that return to caller frames no longer in scope. Let’s move the handler creation to a oneTime setup function:


Handler {
    classvar <handler;

    // This is brittle code, don't re-use.
    *oneTime {
        handler = { |error| ^error; };
    }

    *wrap { |func|
        var value = Handler.prWrap(func);
        ^value;
    }

    *prWrap { |func|
        var value = func.value;
        ^value;
    }
}

(
var func = { Handler.handler.value(\handled); \unhandled; };
Handler.oneTime;
Handler.wrap(func);
)

This example works because the outmost frame is the caller frame for Handler.oneTime, and it still exists when func invokes handler. Note the interpreter updates the instruction pointer on a stack frame on every method invocation, so when we don’t repeat the call to Handler.wrap(func) infinitely.

If we add a layer of indirection to the handler creation:


Handler {
    classvar <handler;

    *oneTime {
        Handler.prOneTime;
    }

    *prOneTime {
        handler = { |error| ^error; };
    }

    *wrap { |func|
        var value = Handler.prWrap(func);
        ^value;
    }

    *prWrap { |func|
        var value = func.value;
        ^value;
    }
}

(
var func = { Handler.handler.value(\handled); \unhandled; };
Handler.oneTime;
Handler.wrap(func);
) // ERROR: 'Meta_Handler-prOneTime' Out of context return of value: handled

Now, the caller frame during handler setup is for oneTime, which is out of scope at invocation time, and the interpreter reports an error.

Hadron Emulation

I’ve gone back and forth a few times, but currently, Hadron compiles methods that respect the C ABI and use the C stack frame for both function and method invocation. I’m uncertain how to emulate this kind of return behavior from functions, perhaps with some setjmp/longjmp trickery. However, the problem with this approach is that it doesn’t allow instruction pointer updates in the caller frame, so I’m a bit stumped for the moment.

[Updated 2022-10-10]: Clarified code examples based on feedback from Nathan Ho.