在语言解释器中防止StackOverflow

时间:2013-11-28 19:19:01

标签: .net compiler-construction f# stack-overflow tail-recursion

F#作为一种语言非常适合编写语言解释器或编译器,但是,有一件事情让我们不知所措:StackOverflowException。

众所周知,无法捕获SO异常,无法从中恢复。防止这种异常的一种显而易见的技术是在你进行时计算堆栈的深度。开销,是的,但可行,也许并非每项功能都必要。

使用F#,这项技术并没有带来太多好处。我们在解释器的动态生成表达式中大量使用尾调用优化技术。我们在SO异常中遇到的问题是:

  • 我们如何通知用户,而不是崩溃整个当前的AppDomain?
  • 如果我们计算堆栈深度,我们怎么知道函数是TCO还是内联,所以我们不必计算?
  • 如果我们采用另一种方法(比如以给定的深度间隔检查堆栈本身),是否有任何(已知的)方法可以在不严重妨碍性能的情况下执行此操作?

只是增加堆栈大小不会有足够的帮助,我们希望给用户一个可记录的错误,最好是由调用应用程序捕获。为此,我们需要能够手动抛出异常,这使它可以捕获。但我们如何确定合适的时机?

更新
Hans Passant在这里正确地提出了可预测性。但是,使用此DSL的程序员期望(某些)调用获得TCO,因此他们不希望有强大的堆栈限制。他们知道自己在做什么。尽管如此,他们的程序需要能够优雅地死掉,至少在任何调用应用程序(即使用我们的库的C#程序)的范围内。 没有伤害。

1 个答案:

答案 0 :(得分:5)

我不熟悉F#,但我确实在C#中编写了一个ECMAScript-262 v.5解释器,所以我可以解决你的一些问题。如您所知,自v2.0起,无法在.NET应用程序中捕获StackOverFlowException。虽然有一个相当可靠的解决方法,而且速度很快。

如果在堆栈上声明一个变量,例如int,则该变量的地址代表堆栈的顶部,并让您知道剩余多少空间。因此,如果您在启动时记录该变量,而堆栈基本上是空的,则每次输入新的执行上下文时都可以引用它。

以下是我的口译员解决这个问题的一些例子。

C#:

这些是在主Interpreter类中声明的静态变量。

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

这是主要的Interpreter类的静态构造函数。

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;
}

在解释ECMAScript函数之前调用此代码。如果新堆栈分配变量的地址小于short.Max,则抛出可捕获异常。您需要为调用堆栈留出一些空间来放松。

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;
...
}

解释以下代码时,会在迭代1200周围抛出异常。

更新:在发布版本中,它大约有4100次迭代。

的ECMAScript:

RecursiveCall(0);

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

输出:RangeError: stack overflow

您可以使用Thread(ParameterizedThreadStart, Int32)构造函数增加Thread中的堆栈大小。我只是觉得不需要。

祝你的项目好运。我希望这会有所帮助。