17

F# as a language is great for writing language interpreters or compilers, however, one thing keeps hitting us where we don't want it: the StackOverflowException.

It's well known that an SO-exception cannot be caught and cannot be recovered from. An obvious technique for preventing such an exception is by counting the depth of the stack as you go along. Overhead, yes, but doable and perhaps not necessary in every function.

With F#, this technique doesn't bring much benefit though. We make a lot of use of tail-call optimization techniques in the on-the-fly generated expressions of the interpreter. The problems we face with SO-exceptions are:

  • how can we inform the user of them, instead of crashing the whole current AppDomain?
  • if we go for counting the stack-depth, how do we know whether a function is TCO'ed or inlined so we don't have to count up?
  • if we go for another approach (like inspecting the stack itself at given depth-intervals), is there any (known) way to do this without seriously impeding performance?

Just increasing the stack-size is not going to help enough, we want to give the user a loggable error, preferably catchable by the calling application. For that we need to be able to hand-throw the exception, which makes it catchable. But how do we determine the right moment?

Update:
Hans Passant correctly suggests predictability here. However, the programmers using this DSL expect that (certain) calls get TCO'ed, hence they don't want a strong stack-limit. They know what they are doing. Still, their programs need to be able to die gracefully, at least to the extend that any calling application (i.e., a C# program using our libraries) is not harmed.

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
Abel
  • 56,041
  • 24
  • 146
  • 247
  • @HansPassant, thanks for jumping in. It has crossed our minds to solve it that way, but it feels so much like solving a small problem with a sledge hammer that we haven't even attempted it yet. Furthermore, our compiler should be callable as a library from within a host language and spawning a process each time a small snippet is compiled is really too much overhead. – Abel Nov 28 '13 at 19:41
  • Oh, you updated your comment. The problem here is the predictability. The DSL may contain many functions that will get TCO'ed, setting a hard limit (which we currently have) appears to be too rigid, because we cannot count towards that limit in any conceivable way. – Abel Nov 28 '13 at 19:43
  • @HansPassant: our program translates another language into compiled expressions and then runs that against variable input. To complex things further, the language definition contains an evaluate-function. Increasing stack-size only works to a certain level, I don't know why, but after a certain threshold it brings the program to snail-speed. For one, our program will be called from an IDE (not ours), bringing the IDE down because we have an SO would be disastrous. There used to be a way around with AppDomains, but I believe that is not possible any longer. – Abel Nov 28 '13 at 20:01
  • @HansPassant: just some things that I've come across: CER (constrained execution region) and `RuntimeHelpers.ProbeForSufficientStack`. Not sure it will solve our problems and not sure how I could use CER in my own application. – Abel Nov 28 '13 at 20:08
  • 1
    Why don't you just rewrite your interpreter so that it doesn't leak stack space? – J D Nov 30 '13 at 19:08
  • We are implementing an open standard, fairly complex, functional language that allows a lot of freedom of expressions. We cannot rewrite every conceivable expression that the result only consists of continuations etc. We already prevent a certain depth of recursion in the DSL, and maybe that's all we can do, but the way F# optimizes things, we shouldn't need to be too rigid about this. Also, from a security standpoint, we should not allow SO's to tear down everything, so we either need a foolproof way to ensure stackdepth, or catch it (I'm currently experimenting with hosting the CLR). – Abel Nov 30 '13 at 22:58
  • 1
    "We cannot rewrite every conceivable expression that the result only consists of continuations". I don't understand why that is not possible. Can you post some code or a concrete example to highlight why that is not possible? – J D Dec 01 '13 at 12:22
  • Consider implementing a stackless interpreter or compiler. CPS transform might help. – SK-logic Dec 07 '13 at 11:46

1 Answers1

5

I'm not familiar with F# but I did write an ECMAScript-262 v.5 interpreter in C# so I can relate to some of your issues. As you know, the StackOverFlowException can't be caught in .NET apps since v2.0. There is a fairly reliable workaround though and it's fast.

If you declare a variable on the stack, for instance an int, the address of that variable represents the top of the stack and lets you know how much space is left. So, if you record that variable at startup while the stack is basically empty, you can reference it each time you enter a new execution context.

Here are some exerpts from my interpreter that address this issue.

C#:

These are static variables declared in the main Interpreter class.

private static int TopOfStack;
private const int STACK_SIZE = 1000000;

This is the static constructor of the main Interpreter class.

static Interpreter() {
    InitializeGlobalEnvironment();

    //---------------------------------------------------
    // Get the address of a new variable allocated on the stack 
    // to represent the amount of memory available. Record 
    // the address.
    //---------------------------------------------------
    int stackVariable;
    TopOfStack = (int)&stackVariable;
}

This code gets called before an ECMAScript function is interpreted. If the address of a new stack allocated variable is less than short.Max, I throw the catchable exception. You need to leave some space for the call stack to unwind.

internal static ExecutionContext EnterFunctionContext(IValue thisArg, LIST args, FUNCTION function) {
...
    LexicalEnvironment localEnv = ECMA.NewDeclarativeEnvironment(function.Scope);

    ExecutionContext context = new ExecutionContext() {
        Strict = function.IsStrict,
        VariableEnvironment = localEnv,
        LexicalEnvironment = localEnv
    };

    int remainingStackSpace;

    if (STACK_SIZE - (TopOfStack - (int)&remainingStackSpace) < short.MaxValue) 
            throw new ECMARuntimeException("stack overflow", RuntimeErrorType.RangeError);


    CallStack.Push(context);
    LexicalEnvironment env = CurrentContext.VariableEnvironment;
...
}

When the following code is interpreted, the exception is thrown around iteration 1200.

Update: In release build it is around 4100 iterations.

ECMAScript:

RecursiveCall(0);

function RecursiveCall(counter){
    return RecursiveCall(++counter);
}

Output: RangeError: stack overflow

You could increase the stack size in the Thread using the Thread(ParameterizedThreadStart, Int32) constructor. I just didn't feel the need.

Good luck with your project. I hope this helps.

drankin2112
  • 4,715
  • 1
  • 15
  • 20