Hey, beanskin fans. Happy May Day so in the past, have a good holiday to play it. Today, breeze Muzhu brings you an article “How to implement a JS interpreter with GO”.

Author: Breeze Muzhu

background

Some time ago, in the process of developing the version and releasing the system, in order to pursue the flexibility of the system, we allowed users to generate JSON configuration by writing JS. The customized needs of the business can be realized by JS code, so that the personalized needs of the business can be supported as far as possible without adjusting the underlying system. Since the distribution system is developed with Golang, we need a GO version of the JS interpreter (no need to consider gc, JIT, inline-cache and other complex content, just a simple implementation of the interpreter, can parse and execute JS), which can safely run JS code in Golang applications.

Implementation approach

There are many implementations of js interpreters, such as tinyjs(the c++ version), tinyjs.py(the py version), and several bootstrappers such as eval5. The idea behind these interpreters is as follows:

The transformation step is optional. The main job of this step is to convert nodes in the syntax tree to nodes executable by the target language. For jS-in-JS implementations such as EVAL5, this step is not required. But in the case of js-in-x(x could be go, c++, py), you need to add transformation steps. On the lexical analysis, grammar analysis of the two implementation data is more, here will not repeat, familiar with JS students can refer to acorn, Babel-Parser, ESpree and other implementation, here focuses on the process of conversion and traversal execution.

Go and JS data exchange

Before conversion and execution, the problem of go and JS data exchange needs to be solved first. Js < — > GO bidirectional scenario needs to be considered.

  1. The GO code accesses js variables

In the process of ast syntax tree transformation, the JS code is converted into expression node during the transformation of the corresponding AST node. Basic values are boxed into Value types. Golang accesses JS variables and actually accesses the expression node generated after the transformation of the corresponding AST node of the variable. For example, define the following in js:

var a = 2;
function print(name) {
  console.log('hello ' + name)
}
Copy the code

The variable definition is converted as follows:

The function definition is converted as follows:

Golang processes the variable definition before execution, and then generates a key (variable name or function name) on the corresponding scope object into a binding for expression:

Go does not access the JS variable directly, but the expression corresponding to the JS variable.

  1. Js code accesses the GO variable

Assume that go pre-registers variable X and function twoPlus:

vm := New()
vm.Set("x", 10)
vm.Set("twoPlus", func(call FunctionCall) Value {
right, _ := call.Argument(0).ToInteger()
result, _ := vm.ToValue(2 + right)
return result
})
Copy the code

Js access golang variable x, function twoPlus:

var a = x + 2;
var b = twoPlus(a)
console.log('twoPlus(a): ' + b)
Copy the code

The golang side registers the variable to the property of the current scope: golang side registers the variable to the property of the current scope:

When executing the ast transformed node, it is found that the corresponding value of identifier X needs to be obtained, and the corresponding value of X will be obtained from the map corresponding to the property. The same is true of functions.

conversion

  • The Statement transformation is executed from the body node of the AST tree

    For example, in the figure above, 1+1 corresponds to ExpressionStatement on the AST tree, and parseStatement will be called accordingly:

    ParseExpression:

    The expression inside the expression statement is a BinaryExpression, which is eventually converted to the following structure:

    _nodeBinaryExpression {
      operator:   token.PLUS,
      comparison: false, 
      left: &_nodeLiteral{
        value: Value{
          kind:  valueNumber,
          value: 1, 
        }, 
      },    
      right: &_nodeLiteral{
        value: Value{
          kind:  valueNumber, 
          value: 1,
        },
      },
    }
    Copy the code
  • Handle variable declaration cases

    Handles variable declarations and JSVariable ascensionAfter traversing the AST tree, the definitions of variables and functions in the tree are stored in the varList and functionList arrays.

Traverse, execute to

1 + 1, for example:

When iterating through the ast to execute the corresponding node, note that js has different scope (global scope, function scope). When the above code is executed, the code is executed in the global scope by default:

EnterGlobalScope and leaveScope are implemented as follows:

On entering globalScope, the scope of the current Runtime will be stored on the _scope.outer field. Defer’s corresponding anonymous function will be executed after the function is finished, and when it is finished, the scope temporarily stored on scope.outers will be restored. The Scope objects of globalScope and functionScope are isolated; in non-strict mode, functionScope is a deep-copied object from globalScope.

Next, handle variable definitions, such as var a = 1 or function f(){}, where variables and functions are stored in the variable.object.property field of the current scope object, which is essentially a giant map. When stored, the key is the variable name and the value is the wrapper type of the corresponding js value. After completing the definition of variables and functions, traverse the body node under program and perform the corresponding operations according to the node type:

Then execute expression in the ExpressionStatement, which is of type BinaryExpression:

BinaryExpression evaluates lvalues and rvalues, starting with Node. left:

When cmp_evaluate_nodeExpression is entered, the expression type is nodeLiteral, and node.value is returned. Perform the following calculation logic based on the node. Operator (Unpack the Value type first, obtain the basic Value and convert it to float64 to perform the calculation, and the calculation result is returned as the packaging type of Value) :

So now we’re done with 1 plus 1.

conclusion

With the help of the existing JS code parsing library can be relatively easy to achieve a JS interpreter, the idea of implementation is relatively clear, but the support for the new syntax specification is still relatively poor, and the syntax can be further expanded. In addition to the implementation of JS-in-Go introduced in this paper, jS-in-JS also has some interesting gameplay. For example, with the implementation of JS-in-JS, the hot update of JS code can be realized in the condition that the JS engine shields eval and new Function.

More exciting content, customized gift books, high-salary job promotion, wechat search attention to “doupi fan”