动手写一个简单的编译器:在JavaScript中使用Swift的尾闭包语法

首先跟你们说一下我为何会有这个想法吧,由于最近在空闲时间学习SwiftSwiftUI的时候会常用到这种叫作尾闭包的语法,就以为颇有趣。同时由于很早以前看过jamiebuildsthe-super-tiny-compiler,就想着能不能本身也实现一个相似的有趣好玩简单的编译器。因此就有了js-trailing-closure-toy-compiler这个项目,以及今天的这篇文章。javascript

对于不熟悉Swift的同窗来讲,我先来解释一下什么是尾闭包。简单来讲,就是若是一个函数的最后一个参数也是一个函数,那么咱们就可使用尾闭包的方式来传递最后一个函数。你们能够看下面的代码示例:html

// 例子中的 a 表示一个函数

// #1:简单版
// Swift 的方式
a(){
  // 尾闭包的内容
}
// JavaScript 的方式
a(() => {})

// #2:函数带参数
// Swift 的方式,这里先忽略参数的类型
a(1, 2, 3){
  // 尾闭包的内容
}
// JavaScript 的方式
a(1, 2, 3, () => {})

// #3:尾闭包带有参数
// Swift 的方式,这里先忽略参数的类型
a(1, 2, 3){ arg1, arg2 in
  // 尾闭包的内容
}
// JavaScript 的方式
a(1, 2, 3, (arg1, arg2) => {})

若是关于Swift的尾闭包还有什么疑问的话,你们能够看一下官方的文档Closures,里面解释的也很清楚。前端

我记得本身很早以前就看过the-super-tiny-compiler项目的源码,不过当时只是简单的看了一遍。就觉得本身掌握了里面的一些知识和原理。可是当我想实现我本身心中的这个想法的时候。却发现以前并无把这个项目里面实践的一些方法和技巧掌握好。因此我决定先好好的把这个项目的源码看懂,而后本身先实现一个跟原来的项目功能同样的样例以后才开始着手实现本身的小编译器。java

友情提示,接下来的文章内容比较长,建议收藏后再仔细阅读node

编译器的实现过程

the-super-tiny-compiler咱们能够了解到,对于通常的编译器来讲。主要有四个步骤去完成编译的过程,这四个步骤分别是:git

  • tokenizer:将咱们的代码文本字符串转换成一个个有意义的单元(也就是token。好比if"hello"123letconst等等。
  • parser:将上一步获取到的token转换成当前语言的抽象语法树,也就是AST( Abstract Syntax Tree )。为何要这么作呢?由于这样处理以后,咱们就知道代码中语句的前后关系和层级关系。也知道运行的顺序,以及上下文等等相关的信息了。
  • transformer:将上一步获取到的AST转换成目标语言的AST。为何要作这一步呢?对于相同功能的程序语句来讲,若是选择实现的语言不同,那它们的语法大几率也是不同的。这就致使了它们对应的抽象语法树也是不同的。因此咱们须要作一次转换,为了下一步生成目标语言的代码作好准备。
  • codeGenerator:这一步相对来讲比较简单,知道了目标语言的语法以后,咱们根据上一步骤生成的新的抽象语法树,能够方便快捷的生成咱们想要的目标语言的代码。

上面的步骤就是编译器的大概工做流程了,可是仅仅知道这些流程仍是不够的,还须要咱们亲自动手实践一下。若是看到这里你有兴趣的话,能够点击这里JavaScript Trailing Closure Toy Compiler先体验一下最后实现的效果。若是你对具体的实现过程感兴趣的话,能够继续下面的阅读,相信看过以后你会有很大的收获的,也许会想要本身也实现一个有趣的编译器呢。github

Tokenizer:将代码字符串转换为token

首先咱们须要明白为何要把字符串转换为一个个的token,由于若是不作转换,咱们就不知道这段程序要表示的是什么意思,由于token是理解一段程序的必要条件。swift

这就比如console.log("hello world!")这个语句来讲,咱们一眼就知道它是干吗的,可是咱们是怎么思考的呢?是否是首先是console咱们知道是console对象,而后是.咱们知道是获取对象的属性操做符,再而后是log方法,而后方法的调用须要(左括号做为开始,而后是hello world!字符串为参数,而后遇到了后面的)右括号表示结束。segmentfault

代码字符串转换为token

因此把字符串转换成token就是为了让咱们知道这段程序要表示的是什么意思。由于根据每个token的值,以及token所处的位置,咱们能够准确知道这个token表示的是什么,它有什么做用。设计模式

那对于咱们这个编译器来讲,第一步须要把咱们所须要的token作一个划分,那么根据上面的代码示例。咱们能够知道,咱们须要的token的类型有这么几种:

  • 数字:好比166等。
  • 字符串:好比"hello"等。
  • 标识符:好比a,在咱们这个编译器的环境下,通常表示函数名或者变量名。
  • 小括号(),在这里用来表示函数的调用。
  • 花括号{},在这里用来表示函数体。
  • 逗号,,用来分割参数。
  • 空白符 ,用来区分不一样的token

由于咱们这个编译器暂时只专一于咱们想要的尾闭包的实现,因此暂时只须要关注上面这些token的类型就能够了。

这一步其实比较简单,就是按照咱们的需求,循环读取token,代码部分以下所示:

// 将字符串解析为Tokens
const tokenizer = (input) => {
    // 简单的正则
    const numReg = /\d/;
    const idReg = /[a-z]/i;
    const spaceReg = /\s/;

    // Tokens 数组
    const tokens = [];

    // 判断 input 的长度
    const len = input.length;
    if (len > 0) {
        let cur = 0;
        while(cur < len) {
            let curChar = input[cur];

            // 判断是不是数字
            if (numReg.test(curChar)) {
                let num = '';
                while(numReg.test(curChar) && curChar) {
                    num += curChar;
                    curChar = input[++cur];
                }
                tokens.push({
                    type: 'NumericLiteral',
                    value: num
                });
                continue;
            }

            // 判断是不是标识符
            if (idReg.test(curChar)) {
                let idVal = '';
                while(idReg.test(curChar) && curChar) {
                    idVal += curChar;
                    curChar = input[++cur];
                }

                // 判断是不是 in 关键字
                if (idVal === 'in') {
                    tokens.push({
                        type: 'InKeyword',
                        value: idVal
                    });
                } else {
                    tokens.push({
                        type: 'Identifier',
                        value: idVal
                    });
                }
                continue;
            }

            // 判断是不是字符串
            if (curChar === '"') {
                let strVal = '';
                curChar = input[++cur];
                while(curChar !== '"') {
                    strVal += curChar;
                    curChar = input[++cur];
                }
                tokens.push({
                    type: 'StringLiteral',
                    value: strVal
                });
                // 须要处理字符串的最后一个双引号
                cur++;
                continue;
            }

            // 判断是不是左括号
            if (curChar === '(') {
                tokens.push({
                    type: 'ParenLeft',
                    value: '('
                });
                cur++;
                continue;
            }

            // 判断是不是右括号
            if (curChar === ')') {
                tokens.push({
                    type: 'ParenRight',
                    value: ')'
                });
                cur++;
                continue;
            }

            // 判断是不是左花括号
            if (curChar === '{') {
                tokens.push({
                    type: 'BraceLeft',
                    value: '{'
                });
                cur++;
                continue;
            }

            // 判断是不是右花括号
            if (curChar === '}') {
                tokens.push({
                    type: 'BraceRight',
                    value: '}'
                });
                cur++;
                continue;
            }

            // 判断是不是逗号
            if (curChar === ',') {
                tokens.push({
                    type: 'Comma',
                    value: ','
                });
                cur++;
                continue;
            }

            // 判断是不是空白符号
            if (spaceReg.test(curChar)) {
                cur++;
                continue;
            }

            throw new Error(`${curChar} is not a good character`);
        }
    }

    console.log(tokens, tokens.length);
    return tokens;
};

上面的代码虽然不是很复杂,可是有一些须要注意的点,若是不细心很容易出错或者进入一个死循环。下面是我以为一些容易出现问题的地方:

  • 外层使用了while循环,每次循环开始时会首先获取当前下标对应的字符。之因此没有使用for循环是由于这里关于当前字符的下标cur是由里面的判断来推动的,使用while更方便一些。
  • 若是读取到字符串,数字以及标识符的话,须要进行内循环进行连续读取,直到下一个字符不是当前想要的类型为止。由于若是读取的类型可能不止一个字符的话,就须要判断下一个字符是否符合当前的类型。若是不符合的话,就终止当前类型的读取,且须要跳出当前循环,进行下一轮的外循环。
  • 对于字符串来讲,在字符串的开头和结尾的"须要跳过,不计入字符串的值里面。遇到空白符须要跳过

这个过程技术难度不大,须要多一点耐心。实现完成以后,咱们能够测试一下:

tokenizer(`a(1){}`)

能够看到输出的结果以下:

(6) [{…}, {…}, {…}, {…}, {…}, {…}]
0: {type: "Identifier", value: "a"}
1: {type: "ParenLeft", value: "("}
2: {type: "NumericLiteral", value: "1"}
3: {type: "ParenRight", value: ")"}
4: {type: "BraceLeft", value: "{"}
5: {type: "BraceRight", value: "}"}

能够看到输出的结果是咱们想要的结果,到这里咱们已经成功了25%了。接下来就是把获得的token数组转换为AST抽象语法树。

Parser:将token数组转换为AST抽象语法树

接下来的步骤就是把token数组转换为AST(抽象语法树)了,进行了上一个步骤以后,咱们把代码字符串,转变为一个个有意义的token。当咱们获得了这些token以后,就能够根据每个token表示的意义进而推导出整个抽象语法树。

好比咱们遇到了{,咱们就知道在遇到下一个}为止,这中间的全部的token表示的是一个函数的函数体(暂时不考虑其它状况)。

下图所示的token示例:

token示例

表示的程序语句应该是:

a(1) {
  // block
};

那么它所对应的抽象语法树应该是这个样子的:

{
 "type": "Program",
 "body": [
   {
     "type": "CallExpression",
     "value": "a",
     "params": [
       {
         "type": "NumericLiteral",
         "value": "1",
         "parentType": "ARGUMENTS_PARENT_TYPE"
       }
     ],
     "hasTrailingBlock": true,
     "trailingBlockParams": [],
     "trailingBody": []
   }
 ]
}

咱们能够简单的看一下上面的抽象语法树,首先最外层的类型是Program,而后body里面的内容就表示咱们的代码内容。在这里咱们的body数组只有一个元素,表示的是CallExpression,也就是一个函数调用。

这个CallExpression的函数名字是a,而后函数第一个参数类型值是NumericLiteral,数值是1。这个参数的父节点类型是ARGUMENTS_PARENT_TYPE,下面还会对这个属性进行解释。而后这个CallExpressionhasTrailingBlock值为true,表示这是一个尾闭包函数调用。而后trailingBlockParams表示尾闭包没有参数,trailingBody表示尾闭包里面的内容为空。

上面只是一个简单的解释,详细的代码部分以下所示:

// 将 Tokens 转换为 AST
const parser = (tokens) => {
    const ast = {
        type: 'Program',
        body: []
    };

    let cur = 0;

    const walk = () => {
        let token = tokens[cur];

        // 是数字直接返回
        if (token.type === 'NumericLiteral') {
            cur++;
            return {
                type: 'NumericLiteral',
                value: token.value
            };
        }

        // 是字符串直接返回
        if (token.type === 'StringLiteral') {
            cur++;
            return {
                type: 'StringLiteral',
                value: token.value
            };
        }

        // 是逗号直接返回
        if (token.type === 'Comma') {
            cur++;
            return;
        }

        // 若是是标识符,在这里咱们只有函数的调用,因此须要判断函数有没有其它的参数
        if (token.type === 'Identifier') {
            const callExp = {
                type: 'CallExpression',
                value: token.value,
                params: [],
                hasTrailingBlock: false,
                trailingBlockParams: [],
                trailingBody: []
            };
            // 指定节点对应的父节点的类型,方便后面的判断
            const specifyParentNodeType = () => {
                // 过滤逗号
                callExp.params = callExp.params.filter(p => p);
                callExp.trailingBlockParams = callExp.trailingBlockParams.filter(p => p);
                callExp.trailingBody = callExp.trailingBody.filter(p => p);

                callExp.params.forEach((node) => {
                    node.parentType = ARGUMENTS_PARENT_TYPE;
                });
                callExp.trailingBlockParams.forEach((node) => {
                    node.parentType = ARGUMENTS_PARENT_TYPE;
                });
                callExp.trailingBody.forEach((node) => {
                    node.parentType = BLOCK_PARENT_TYPE;
                });
            };
            const handleBraceBlock = () => {
                callExp.hasTrailingBlock = true;
                // 收集闭包函数的参数
                token = tokens[++cur];
                const params = [];
                const blockBody = [];
                let isParamsCollected = false;
                while(token.type !== 'BraceRight') {
                    if (token.type === 'InKeyword') {
                        callExp.trailingBlockParams = params;
                        isParamsCollected = true;
                        token = tokens[++cur];
                    } else {
                        if (!isParamsCollected) {
                            params.push(walk());
                            token = tokens[cur];
                        } else {
                            // 处理花括号里面的数据
                            blockBody.push(walk());
                            token = tokens[cur];
                        }
                    }
                }
                // 若是 isParamsCollected 到这里仍是 false,说明花括号里面没有参数
                if (!isParamsCollected) {
                    // 若是没有参数 收集的就不是参数了
                    callExp.trailingBody = params;
                } else {
                    callExp.trailingBody = blockBody;
                }
                // 处理右边的花括号
                cur++;
            };
            // 判断后面紧接着的 token 是 `(` 仍是 `{`
            // 须要判断当前的 token 是函数调用仍是参数
            const next = tokens[cur + 1];
            if (next.type === 'ParenLeft' || next.type === 'BraceLeft') {
                token = tokens[++cur];
                if (token.type === 'ParenLeft') {
                    // 须要收集函数的参数
                    // 须要判断下一个 token 是不是 `)`
                    token = tokens[++cur];
                    while(token.type !== 'ParenRight') {
                        callExp.params.push(walk());
                        token = tokens[cur];
                    }
                    // 处理右边的圆括号
                    cur++;
                    // 获取 `)` 后面的 token
                    token = tokens[cur];
                    // 处理后面的尾部闭包;须要判断 token 是否存在 考虑`func()`
                    if (token && token.type === 'BraceLeft') {
                        handleBraceBlock();
                    }
                } else {
                    handleBraceBlock();
                }
                // 指定节点对应的父节点的类型
                specifyParentNodeType();
                return callExp;
            } else {
                cur++;
                return {
                    type: 'Identifier',
                    value: token.value
                };
            }
        }

        throw new Error(`this ${token} is not a good token`);
    };

    while (cur < tokens.length) {
        ast.body.push(walk());
    }

    console.log(ast);
    return ast;
};

为了方便你们理解,我把一些关键的地方都添加了一些注释。下面再次对上面的代码作一些简单的解释。

首先咱们须要对tokens数组进行遍历,咱们首先定义了抽象语法树的最外层的结构是:

const ast = {
  type: 'Program',
  body: []
};

这样定义是为了后续的节点对象可以按照必定的规则添加到咱们的抽象语法树上。

而后咱们定义了一个walk函数用来对tokens数组中的元素进行遍历。对于walk函数来讲,若是直接遇到数字字符串逗号的话都是直接返回的。当遇到的token是一个标识符的话,须要判断的状况比较多。

对于一个标识符来讲,在咱们这种情境下有两种处理:

  • 一种状况就是一个单独的标识符,这时候标识符后面紧跟的token既不是表示(的,也不是表示{
  • 另外一种状况表示的是一个函数的调用,对于函数的调用来讲,咱们须要考虑如下这几种状况

    • 一种状况是函数只有一个尾闭包,不含有其它的参数。好比a{}
    • 另外一种状况是函数的调用不含有尾闭包,能够含有参数也能够不带参数。好比a()或者a(1);
    • 最后一种就是函数的调用含有尾闭包。好比a{},a(){},a(1){}等等。对于有尾闭包的状况还须要考虑尾部闭包有没有参数,好比a(1){b, c in }

接下来主要对token是标识符类型的处理作一个简单的解释,若是判断token的类型是标识符的话,咱们会先定义一个CallExpression类型的对象callExp,这个对象就是用来表示咱们函数调用的语法树对象。这个对象有如下几个属性:

  • type:表示节点的类型
  • value:表示节点的名称,这里表示函数名
  • params:表示函数调用的参数
  • hasTrailingBlock:表示当前函数调用是否包含尾闭包
  • trailingBlockParams:表示尾闭包是否含有参数
  • trailingBody:尾闭包里面的内容

接下来判断标识符后面的token类型是什么,若是是函数的调用的话,当前token后面的token必须是(或者是{。若是不是的话,咱们直接返回这个标识符。

若是是函数的调用,咱们须要作两个事情,一个是收集函数调用的参数,一个是判断函数调用后面是否是含有尾闭包。对于函数参数的收集比较简单,首先判断当前token后面的token是否是表示的是(,若是是的话,开始收集参数,直到遇到下一个token的类型是)表示参数收集结束。还要注意的一点是,由于参数有多是一个函数,因此咱们须要在收集参数的时候再次调用walk函数,来帮助咱们递归的进行参数的处理

接下来就是判断函数调用后面是否含有尾闭包,对于尾闭包的判断有两种状况须要考虑:一种就是函数的调用含有有参数,在参数的后面含有尾闭包;另外一种是函数的调用没有参数,直接就是一个尾闭包。因此咱们须要对这两种状况都作一下处理

既然有两个地方都要进行是不是尾闭包的判断,咱们能够把这部分的逻辑抽离到handleBraceBlock函数中,这个函数就是帮助咱们来进行尾闭包的处理。接下来来解释一下尾闭包是如何进行处理的。

若是咱们判断下一个token{那么说明咱们须要进行尾闭包的处理了,咱们首先把callExp对象的hasTrailingBlock属性的值设置为true;而后须要判断尾闭包是否含有参数,而且须要处理尾闭包的内部内容。

如何收集尾闭包的参数呢?咱们须要判断在尾闭包里面是否含有in关键字,若是含有in关键字,那就说明尾闭包里面含有参数,若是没有就表示尾闭包里不含有参数,只须要处理尾闭包内部的内容便可。

又由于咱们刚开始不知道尾闭包中是否含有in关键字,因此咱们一开始收集的内容多是尾闭包里面的内容,也有多是参数;因此当在遇到}尾闭包的结束token以后,这期间若是没有in关键字,那说明咱们收集到的都是尾闭包的内容。

不管是收集尾闭包的参数仍是内容,咱们都须要使用walk函数来进行递归操做,由于参数和内容均可能不是基本的数值类型值(为了简化操做,咱们这里对尾闭包的参数也使用walk来进行递归操做)。

在返回callExp对象以前,咱们须要使用specifyParentNodeType帮助函数额外的作一下处理。第一个处理是去掉表示,token,另外一个操做就是须要给callExp对象的paramstrailingBlockParamstrailingBody属性中的节点指定一下父节点的类型,对于paramstrailingBlockParams来讲,它们的父节点类型都是ARGUMENTS_PARENT_TYPE类型的;对于trailingBody来讲,它的父节点类型是BLOCK_PARENT_TYPE类型的。这样的处理方便咱们进行下一步的操做。在进行下面步骤讲解的时候咱们会再次对其进行说明。

Transformer:将旧 AST 转换为目标语言的 AST

接下来就是把咱们获取到的原始AST转换为目标语言的AST,那么咱们为何要作这一步处理呢?这是由于一样的编码逻辑,在不一样的宿主语言的表现是不同的。因此咱们要把原始的AST转换成咱们目标语言的AST

那咱们怎么进行操做呢?原始的AST是一个树形的结构,咱们须要对这个树形的结构进行遍历;遍历须要使用深度优先的遍历,由于对于一个嵌套的结构来讲,只有将里面的内容肯定了以后,外面的内容才可以随之肯定。

这里对树形结构的遍历咱们会用到一种设计模式,那就是访问者模式。咱们须要一个访问者对象对咱们的树形对象进行深度优先的遍历,这个访问者对象有针对不一样类型节点的处理函数,当遇到一个节点的时候,咱们就会根据当前节点的类型,从访问者对象身上获取相应的处理函数对这个节点进行处理。

咱们首先看一下如何对原始的树形结构进行遍历,对于原来的树形结构来讲,每个节点要么是一个具体类型的对象,要么是一个数组。因此咱们要对这两种状况分别进行处理。咱们首先肯定如何进行树形结构的遍历,这部分的代码以下所示:

// 遍历节点
const traverser = (ast, visitor) => {
    const traverseNode = (node, parent) => {

        const method = visitor[node.type];
        if (method && method.enter) {
            method.enter(node, parent);
        }

        const t = node.type;
        switch (t) {
            case 'Program':
                traverseArr(node.body, node);
                break;
            case 'CallExpression':
                // 处理 ArrowFunctionExpression
                // TODO 考虑body 里面存在尾部闭包
                if (node.hasTrailingBlock) {
                    node.params.push({
                        type: 'ArrowFunctionExpression',
                        parentType: ARGUMENTS_PARENT_TYPE,
                        params: node.trailingBlockParams,
                        body: node.trailingBody
                    });
                    traverseArr(node.params, node);
                } else {
                    traverseArr(node.params, node);
                }
                break;
            case 'ArrowFunctionExpression':
                traverseArr(node.params, node);
                traverseArr(node.body, node);
                break;
            case 'Identifier':
            case 'NumericLiteral':
            case 'StringLiteral':
                break;
            default:
                throw new Error(`this type ${t} is not a good type`);
        }

        if (method && method.exit) {
            method.exit(node, parent);
        }
    };
    const traverseArr = (arr, parent) => {
        arr.forEach((node) => {
            traverseNode(node, parent);
        });
    };
    traverseNode(ast, null);
};

我来简单解释一下这个traverser函数,这个函数内部定义了两个函数,一个是traverseNode,一个是traverseArrtraverseArr函数的做用是,若是当前的节点是一个数组的话,咱们须要对数组里面的每个节点分别进行处理。

节点的主要处理逻辑都在traverseNode里面,咱们来看一下这个函数都作了哪些事情?首先根据节点的类型,从visitor对象上获取对应节点的处理方法。而后对接点类型进行判断,若是节点的类型是基本类型的话,就不作处理;若是节点的类型是ArrowFunctionExpression箭头函数的话,须要依次遍历这个节点的paramsbody属性。若是节点的类型是CallExpression的话,表示当前的节点是一个函数调用节点,那么咱们就须要判断这个函数调用是否包含尾闭包,若是包含尾闭包的话,那就说明咱们原来的函数调用须要额外添加一个参数,这个参数是一个箭头函数。因此会有下面这样一段代码进行判断:

// ...
if (node.hasTrailingBlock) {
    node.params.push({
        type: 'ArrowFunctionExpression',
        parentType: ARGUMENTS_PARENT_TYPE,
        params: node.trailingBlockParams,
        body: node.trailingBody
    });
    traverseArr(node.params, node);
} else {
    traverseArr(node.params, node);
}
// ...

而后就是对这个CallExpression节点的params属性进行遍历。当函数的调用包含尾闭包的时候,咱们往节点的params属性里添加了一个类型是ArrowFunctionExpression的对象,并且这个对象的parentType的值是ARGUMENTS_PARENT_TYPE,由于这样咱们就知道这个对象的父节点类型,方便咱们下面进行语法树转换时使用。

再接下来就是定义访问者对象上面不一样节点类型的处理方法了,具体的代码以下:

const transformer = (ast) => {
    const newAst = {
        type: 'Program',
        body: []
    };

    ast._container = newAst.body;

    const getNodeContainer = (node, parent) => {
        const parentType = node.parentType;
        if (parentType) {
            if (parentType === BLOCK_PARENT_TYPE) {
                return parent._bodyContainer;
            }
            if (parentType === ARGUMENTS_PARENT_TYPE) {
                return parent._argumentsContainer;
            }
        } else {
            return parent._container;
        }
    };

    traverser(ast, {
        NumericLiteral: {
            enter: (node, parent) => {
                getNodeContainer(node, parent).push({
                    type: 'NumericLiteral',
                    value: node.value
                });
            }
        },
        StringLiteral: {
            enter: (node, parent) => {
                getNodeContainer(node, parent).push({
                    type: 'StringLiteral',
                    value: node.value
                });
            }
        },
        Identifier: {
            enter: (node, parent) => {
                getNodeContainer(node, parent).push({
                    type: 'Identifier',
                    name: node.value
                });
            }
        },
        CallExpression: {
            enter: (node, parent) => {
                // TODO 优化一下
                const callExp = {
                    type: 'CallExpression',
                    callee: {
                        type: 'Identifier',
                        name: node.value
                    },
                    arguments: [],
                    blockBody: []
                };
                // 给参数添加 _container
                node._argumentsContainer = callExp.arguments;
                node._bodyContainer = callExp.blockBody;
                getNodeContainer(node, parent).push(callExp);
            }
        },
        ArrowFunctionExpression: {
            enter: (node, parent) => {
                // TODO 优化一下
                const arrowFunc = {
                    type: 'ArrowFunctionExpression',
                    arguments: [],
                    blockBody: []
                };
                // 给参数添加 _container
                node._argumentsContainer = arrowFunc.arguments;
                node._bodyContainer = arrowFunc.blockBody;
                getNodeContainer(node, parent).push(arrowFunc);
            }
        }
    });
    console.log(newAst);
    return newAst;
};

咱们首先定义了新的AST的外层属性,而后是ast._container = newAst.body,这个操做的做用是将旧的AST和新的AST最外层进行关联,由于咱们遍历的是旧的AST。这样咱们就能够经过_container属性指向新的AST。这样咱们向_container里面添加元素的时候,实际上就是在新的AST上添加对应的节点。这样处理对咱们来讲相对比较简单一点。

而后就是getNodeContainer函数,这个函数的做用就是获取当前节点的父节点的_container属性,若是当前节点的parentType属性不为空,那说明当前节点的父节点表示的多是函数调用的参数,也有多是尾闭包里面的内容。这时能够根据node.parentType的类型进行判断。若是当前节点的parentType属性为空,那就说明当前节点的父节点的_container属性就是父节点的_container属性。

接下来就是visitor对象上面不一样节点类型的处理方法了,对于基本类型仍是直接返回对应的节点就能够了。若是是CallExpressionArrowFunctionExpression类型的话,就须要一些额外的处理了。

首先对于ArrowFunctionExpression类型节点来讲,首先声明了一个arrowFunc对象,而后将对应节点的_argumentsContainer属性指向arrowFunc对象的arguments属性;将节点的_bodyContainer属性指向arrowFunc对象的blockBody属性。而后获取当前节点的父节点的_container属性,最后将arrowFunc添加到这个属性上。对于节点类型是CallExpression的节点的处理跟上面的相似,只不过定义的对象多了一个callee属性,代表函数调用的函数名称。

到此为止将旧的AST转换为新的AST就完成了。

CodeGenerator:遍历新的 AST 生成代码

这一步就比较简单了,根据节点的类型拼接对应类型的代码就能够了;详细的代码以下所示:

const codeGenerator = (node) => {
    const type = node.type;
    switch (type) {
        case 'Program':
            return node.body.map(codeGenerator).join(';\n');
        case 'Identifier':
            return node.name;
        case 'NumericLiteral':
            return node.value;
        case 'StringLiteral':
            return `"${node.value}"`;
        case 'CallExpression':
            return `${codeGenerator(node.callee)}(${node.arguments.map(codeGenerator).join(', ')})`;
        case 'ArrowFunctionExpression':
            return `(${node.arguments.map(codeGenerator).join(', ')}) => {${node.blockBody.map(codeGenerator).join(';')}}`;
        default:
            throw new Error(`this type ${type} is not a good type`);
    }
};

须要注意的可能就是对于CallExpressionArrowFunctionExpression节点的处理了,对于CallExpression须要添加函数的名称,而后接下来就是函数调用的参数了。对于ArrowFunctionExpression来讲,须要处理箭头函数的参数以及函数体的内容。相比上面的三个步骤来讲,这个步骤仍是相对比较简单的。

接下来就是将这四个步骤组合一下,这个简单的编译器就算完成了。具体的代码以下所示:

// 组装
const compiler = (input) => {
    const tokens = tokenizer(input);
    const ast = parser(tokens);
    const newAst = transformer(ast);
    return codeGenerator(newAst);
};

// 导出对应的模块
module.exports = {
    tokenizer,
    parser,
    transformer,
    codeGenerator,
    compiler
};

简单总结

若是你有耐心看完的话,你会发现完成一个简单的编译器其实也没有很复杂。咱们须要把这四个过程要作什么理清楚,而后注意一些特殊的地方须要作特殊的处理,还有一个就是须要一点耐心了

固然咱们这个版本的实现只是简单的完成了咱们想要的那部分功能,实际上真正的编译器要考虑的东西是很是多的。上面这个版本的代码有不少地方也不是很规范,当初实现的时候先考虑如何实现,细节和可维护性没有考虑太多。若是你有什么好的想法,或者发现了什么错误欢迎给这个小项目提Issues或者Pull Request,让这个小项目变得更好一点。也欢迎你们在文章下面留言,看看是否是能碰撞出什么新的思路与想法。

一些同窗可能会说,学习这种东西有什么做用?其实用途有不少,首先咱们如今前端的构建基本上离不开BabelJavaScript新特性的支持,而Babel的做用其实就是一个编译器的做用,把咱们语言新的特性转换成目前的浏览器能够支持的一些语法,让咱们能够方便的使用新的语法,也减轻了前端开发的一些负担

另外一方面你若是知道这些原理,你不只能够很容易看懂一些Babel的语法转换插件的源代码,你还能够本身亲自动手实现一个简单的语法转换器或者一些有意思的插件。这会让你的前端能力有一个大的提高。

时间过得好快,距离上次发布文章已通过去两个月了😂,这篇文章也是过完年后的第一篇文章,但愿之后还可以持续的输出一些高质量的文章。固然以前的设计模式大冒险系列还会持续更新,也欢迎你们继续保持关注。

今天的文章到这里就结束了,若是你对这篇文章有什么意见和建议,欢迎在文章下面留言,或者在这里提出来。也欢迎你们关注个人公众号关山不难越,若是你以为这篇文章写的不错,或者对你有所帮助,那就点赞分享一下吧~

参考:

相关文章
相关标签/搜索