Search code examples
javascriptscopeeval

Context-preserving eval


We're building a small REPL that evaluates (with eval) javascript expressions as they are being entered by the user. Since the whole thing is event-driven, evaluation must take place in a separate function, but the context (that is, all declared variables and functions) must be preserved between the calls. I came up with the following solution:

function* _EVAL(s) {
    while (1) {
        try {
            s = yield eval(s)
        } catch(err) {
            s = yield err
        }
    }
}

let _eval = _EVAL()
_eval.next()

function evaluate(expr) {
    let result = _eval.next(expr).value
    if (result instanceof Error)
        console.log(expr, 'ERROR:', result.message)
    else
        console.log(expr, '===>', result)
}

evaluate('var ten = 10')
evaluate('function cube(x) { return x ** 3 }')
evaluate('ten + cube(3)')
evaluate('console.log("SIDE EFFECT")')
evaluate('let twenty = 20')
evaluate('twenty + 40') // PROBLEM

As you can see it works fine with function-scoped variables (var and function), but fails on block scoped ones (let).

How can I write a context-preserving eval wrapper that would also preserve block-scoped variables?

The code runs in a browser, DOM and Workers are fully available.

It should be mentioned that the desired function must handle side effects properly, that is, each line of code, or, at least, each side effect, should be performed exactly once.

Links:

JavaScript: do all evaluations in one vm | https://vane.life/2016/04/03/eval-locally-with-persistent-context/


Solution

  • The article you linked contains a crazy approach that actally works: during each eval() call, we create a new closure inside that eval scope and export it so that to we can use it evaluate the next statement.

    var __EVAL = s => eval(`void (__EVAL = ${__EVAL.toString()}); ${s}`);
    
    function evaluate(expr) {
        try {
            const result = __EVAL(expr);
            console.log(expr, '===>', result)
        } catch(err) {
            console.log(expr, 'ERROR:', err.message)
        }
    }
    
    evaluate('var ten = 10')
    evaluate('function cube(x) { return x ** 3 }')
    evaluate('ten + cube(3)')
    evaluate('console.log("SIDE EFFECT")')
    evaluate('let twenty = 20')
    evaluate('twenty + 40') // NO PROBLEM :D