Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursion limit handling differences between Execute and Invoke as well as weird relationship when chaining them #1580

Closed
Zmarfan opened this issue Jul 6, 2023 · 1 comment · Fixed by #1714

Comments

@Zmarfan
Copy link
Contributor

Zmarfan commented Jul 6, 2023

Version: 3.0.0-beta

Description

I was working on catching recursion exceptions in case my users input endless recursion code and I think the engine is behaving a little odd. It could be that my understanding of how it should work is wrong but I was expecting invoking a function would reset the recursion limit per call and not carry over. This wasn't really the case. I I call function foo using invoke and it reaches the limit of recursion it will stop as expected. If I call foo again however it will throw out immediately as it recurses and not after limit is reached. This does not happen with Execute. I also noticed that you get an even weirder interaction if you call Execute after Invoke. Then you throw immediately. I also noticed that invoke apparently takes one more recursion to reach limit than Execute.

I believe my example code illustrate everything best. I've made a bunch of different combinations of scenarios mixing Execute and Invoke when it comes to recursion error handling.

Code example

Sorry if the code is a little bit messy but I hope this will show more clearly what I mean! <3

private static void TestRecursion() {
    void ExecuteAction(Engine engine) => engine.Execute("recursion()");
    void InvokeAction(Engine engine) => engine.Invoke("recursion");

    List<int> test1 = RunLoop(CreateEngine(), ExecuteAction); // [6, 6, 6, 6, 6]
    
    List<int> test2 = RunLoop(CreateEngine(), InvokeAction); // [7, 1, 1, 1, 1]

    Engine e1 = CreateEngine();
    List<int> test3_1 = RunLoop(e1, ExecuteAction); // list = [6, 6, 6, 6, 6]
    List<int> test3_2 = RunLoop(e1, InvokeAction);  // list = [7, 1, 1, 1, 1]
    
    Engine e2 = CreateEngine();
    List<int> test4_1 = RunLoop(e2, InvokeAction);  // list = [7, 1, 1, 1, 1]
    List<int> test4_2 = RunLoop(e2, ExecuteAction); // list = [0, 6, 6, 6, 6]
    
    Engine e3 = CreateEngine();
    List<int> test5_1 = RunLoop(e3, InvokeAction);  // list = [7, 1, 1, 1, 1]
    List<int> test5_2 = RunLoop(e3, ExecuteAction); // list = [0, 6, 6, 6, 6]
    List<int> test5_3 = RunLoop(e3, InvokeAction);  // list = [7, 1, 1, 1, 1]
    
    Engine e4 = CreateEngine();
    List<int> test6_1 = RunLoop(e4, InvokeAction);  // list = [7, 1, 1, 1, 1]
    List<int> test6_2 = RunLoop(e4, InvokeAction);  // list = [1, 1, 1, 1, 1]
}

private static Engine CreateEngine() {
    Engine engine = new(options => options.LimitRecursion(5));
    return engine.Execute(@"
        var num = 0;
        function recursion() {
            num++;
            recursion(num);
        }
    ");
}

private static List<int> RunLoop(Engine engine, Action<Engine> engineAction) {
    List<int> results = new();
    for (int i = 0; i < 5; i++) {
        try {
            engine.SetValue("num", 0);
            engineAction.Invoke(engine);
        }
        catch (RecursionDepthOverflowException) {
            results.Add((int)engine.GetValue("num").AsNumber());
        }
    }

    return results;
}

Expected behavior

I'm not entirely sure what the underlying intention is regarding invoke and execute in relation to recursion but my own expectation was that I would be able to invoke the same function multiple times and the recursion limit would be reset for each call. My expectation could be wrong of course :3 For my own purposes I can just use Execute in favor of Invoke to call my function and have the recursion be reset for each call but I'm curious as to why they would differ!

Result

Execute:      [6, 6, 6, 6, 6]

Invoke:       [7, 1, 1, 1, 1]

Execute:      [6, 6, 6, 6, 6]
Then Invoke:  [7, 1, 1, 1, 1]

Invoke:       [7, 1, 1, 1, 1]
Then Execute: [0, 6, 6, 6, 6]

Invoke:       [7, 1, 1, 1, 1]
Then Execute: [0, 6, 6, 6, 6]
Then Invoke:  [7, 1, 1, 1, 1]

Invoke:       [7, 1, 1, 1, 1]
Then Invoke:  [1, 1, 1, 1, 1]
@lahma
Copy link
Collaborator

lahma commented Jan 1, 2024

I'm see quite constant behavior of always having [6, 6, 6, 6, 6] for Execute and [7, 7, 7, 7, 7] for Invoke, there's a stack frame difference because direct invoke is skipping one layer. With #1714 test suite passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants