代码是如何被被编译的?你知道吗?

最近需要写一个编辑扩展组件,主要功能类似于Excel的单元格编辑框,主要针对单元格输入内容的处理。要知道在Excel中,每个单元格除了可以输入文本内容(包括字符、数字、日期等)外,还有包括函数。 那么在输入函数时,如果聚焦到函数(比如IF VLOOKUP)或单元格(比如A1 A2)上,都会有对应的响应,那么我们输入的文本,是如何识别到其中的函数,常量,单元格的呢?并且在被聚焦后能够做出对应的响应?

基于这样的需求,经过分析后发现真正需要做的事情,其实是将输入的内容进行解析,然后根据解析的结果,进行相应的处理,最终生成一段HTML内容,而所有的点击响应都是对DOM元素的事件监听而已, 而这里面的关键则是如何将输入的内容,解析成对应的AST,后面生成HTML的过程也就简单了。

思考

现在有这样的一个Excel公式,调用了IF、VLOOKUP函数,引用了单元格与外部sheet区域,包括操作符号,常量等等元素:

IF(VLOOKUP(A1,"sheet1"!$A$1:$C$50,1,2) > 3, 4, 5)

接下来我们需要做的是将上面的文本转换成下面的存储结构,也就是AST(抽象语法树):

{
  type: 'Program',
  body: [{
 type: 'function',
 name: 'IF',
 params: [
        {
            type: 'expression,
            body: [
                {
                    type: 'function',
                    name: 'VLOOKUP',
                    params: [
                        {
                            type: 'cell',
                            value: 'A1',
                        }, 
                        {
                            type: 'range',
                            ref: 'sheet1'
                            value: '$A$1:$C$50',
                        },
                        {
                            type: 'number',
                            value: '1'
                        },
                        {
                            type: 'number',
                            value: '2'
                        }
                    ]
                },
                {
                    type: 'operator',
                    value: '>'
                },
                {
                    type: 'number',
                    value: '3'
                }
            ]
        },
        {
            type: 'number',
            value: '4',
        }, 
        {
            type: 'NumberLiteral',
            value: '5',
        }
    ]
  }]
}

基于结构化的树,可以很放标转换成类似下面的HTML形式,要知道对html的操作是非常方便的:


    IF
    (
    
        VLOOKUP
        (
        A1
        ,
        sheet1!
        "sheet"!$A$1:$C$50
        ,
        1
        ,
        2
        )
        >
        3
    
    4
    ,
    5
    )

最后为为上面的html内容添加样式,并为不同类型的元素绑定事件与处理逻辑,这样不仅可以对输入内容高亮,在点击函数、单元格时,还可以给出对应的提示信息或响应。

上面的内容仅仅是实现一个功能的思考与假设,实际的实现可能有所差异,但是实现的过程是类似的。

在我们使用的所有编程语言,比如Java、C、javascript等,我们都会编写文本格式的源代码,编译器或解释器会将源代码按照语言语法解析成对应的语法结构树,基于该结构一来可以实现语法检查、代码高亮等功能,同时针对特定代码块可以有很多其他操作。

实现

通过上面的分析,可以看到核心要实现的是将文本内容解析成语法树的过程。在完成这部分功能的过程中,看到一个基于javascript实现的类似功能的例子the-super-tiny-compiler.js,通过几百行的代码,把这这一过程清晰地表现出来。

其过程主要包括下面几个步骤:

  • tokenizer:词法分析的,以字符为单位逐个遍历文本,构建成一个token数组来描述文本内容,其中每一个token主要包含type,value
  • parser:语法分析,将上面的token数组转换树结构
  • transformer:遍历上面语法树各种类型的节点,针对不同的节点类型,执行不同的操作,生成最终带有语言特性标识的语法树
  • codeGenerator:基于语法树生成最终的代码
  • 下面是具体的实现过程:

    tokenizer

    function tokenizer(input) {
    
      let current = 0;
    
      let tokens = [];
    
      while (current < input.length) {
    
        let char = input[current];
    
        if (char === '(') {
    
          tokens.push({
            type: 'paren',
            value: '(',
          });
    
          current++;
    
          continue;
        }
    
        if (char === ')') {
          tokens.push({
            type: 'paren',
            value: ')',
          });
          current++;
          continue;
        }
    
        let WHITESPACE = /\s/;
        if (WHITESPACE.test(char)) {
          current++;
          continue;
        }
    
        let NUMBERS = /[0-9]/;
        if (NUMBERS.test(char)) {
    
          let value = '';
    
          while (NUMBERS.test(char)) {
            value += char;
            char = input[++current];
          }
    
          tokens.push({ type: 'number', value });
    
          continue;
        }
    
        if (char === '"') {
          let value = '';
    
          char = input[++current];
    
          while (char !== '"') {
            value += char;
            char = input[++current];
          }
    
          char = input[++current];
    
          tokens.push({ type: 'string', value });
    
          continue;
        }
    
        let LETTERS = /[a-z]/i;
        if (LETTERS.test(char)) {
          let value = '';
    
          while (LETTERS.test(char)) {
            value += char;
            char = input[++current];
          }
    
          tokens.push({ type: 'name', value });
    
          continue;
        }
    
        throw new TypeError('I dont know what this character is: ' + char);
      }
    
      return tokens;
    }

    parser

    function parser(tokens) {
    
      let current = 0;
    
      function walk() {
    
        let token = tokens[current];
    
        if (token.type === 'number') {
    
          current++;
    
          return {
            type: 'NumberLiteral',
            value: token.value,
          };
        }
    
        if (token.type === 'string') {
          current++;
    
          return {
            type: 'StringLiteral',
            value: token.value,
          };
        }
    
        if (
          token.type === 'paren' &&
          token.value === '('
        ) {
    
          token = tokens[++current];
    
          let node = {
            type: 'CallExpression',
            name: token.value,
            params: [],
          };
    
          token = tokens[++current];
    
    
          while (
            (token.type !== 'paren') ||
            (token.type === 'paren' && token.value !== ')')
          ) {
            node.params.push(walk());
            token = tokens[current];
          }
    
          current++;
    
          return node;
        }
    
        throw new TypeError(token.type);
      }
    
      let ast = {
        type: 'Program',
        body: [],
      };
    
      while (current < tokens.length) {
        ast.body.push(walk());
      }
    
      return ast;
    }

    traverser

    function traverser(ast, visitor) {
    
      function traverseArray(array, parent) {
        array.forEach(child => {
          traverseNode(child, parent);
        });
      }
    
      function traverseNode(node, parent) {
    
        let methods = visitor[node.type];
    
        if (methods && methods.enter) {
          methods.enter(node, parent);
        }
    
        switch (node.type) {
    
          case 'Program':
            traverseArray(node.body, node);
            break;
    
          case 'CallExpression':
            traverseArray(node.params, node);
            break;
    
          case 'NumberLiteral':
          case 'StringLiteral':
            break;
    
          default:
            throw new TypeError(node.type);
        }
    
        if (methods && methods.exit) {
          methods.exit(node, parent);
        }
      }
    
      traverseNode(ast, null);
    }

    transformer

    function transformer(ast) {
    
      let newAst = {
        type: 'Program',
        body: [],
      };
    
      ast._context = newAst.body;
    
      traverser(ast, {
    
        NumberLiteral: {
          enter(node, parent) {
            parent._context.push({
              type: 'NumberLiteral',
              value: node.value,
            });
          },
        },
    
        StringLiteral: {
          enter(node, parent) {
            parent._context.push({
              type: 'StringLiteral',
              value: node.value,
            });
          },
        },
    
        CallExpression: {
          enter(node, parent) {
    
            let expression = {
              type: 'CallExpression',
              callee: {
                type: 'Identifier',
                name: node.name,
              },
              arguments: [],
            };
    
            node._context = expression.arguments;
    
            if (parent.type !== 'CallExpression') {
    
              expression = {
                type: 'ExpressionStatement',
                expression: expression,
              };
            }
    
            parent._context.push(expression);
          },
        }
      });
    
      return newAst;
    }

    codeGenerator

    function codeGenerator(node) {
    
      switch (node.type) {
    
        case 'Program':
          return node.body.map(codeGenerator)
            .join('\n');
    
        case 'ExpressionStatement':
          return (
            codeGenerator(node.expression) +
          );
    
        case 'CallExpression':
          return (
            codeGenerator(node.callee) +
            '(' +
            node.arguments.map(codeGenerator)
              .join(', ') +
            ')'
          );
    
        case 'Identifier':
          return node.name;
    
        case 'NumberLiteral':
          return node.value;
    
        case 'StringLiteral':
          return '"' + node.value + '"';
    
        default:
          throw new TypeError(node.type);
      }
    }

    compiler

    function compiler(input) {
      let tokens = tokenizer(input);
      let ast    = parser(tokens);
      let newAst = transformer(ast);
      let output = codeGenerator(newAst);
    
      return output;
    }
    const input  = '(add 2 (subtract 4 2))';
    compiler(input);

    感兴趣的可以查看相关的源代码,上面的解释比代码还多,逐行都有非常详细的描述。虽然只是一段简单的示例,但是将一段代码的编译过程都清晰地展现出来,对理解编译过程非常有帮助。像Vue React这样的框架,其编译过程也是基于这个思路,通过一系列的处理流程,最终生成带有语言特性的内容。

    结束语

    本篇文章主要基于目前遇到的的一个需求,结合自己分析与思考来了解需求的本质,最后通过一个javascript的示例,帮助我们理解这个过程。