乐趣区

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

首先跟大家说一下我为什么会有这个想法吧,因为最近在闲暇工夫学习 SwiftSwiftUI的时候会常常应用到这种叫做 尾闭包 的语法,就感觉很乏味。同时因为很早之前看过 jamiebuildsthe-super-tiny-compiler,就想着能不能自己也实现一个相似的乏味好玩简略的编译器。所以就有了 js-trailing-closure-toy-compiler 这个我的项目,以及明天的这篇文章。

对于不相熟 Swift 的同学来说,我先来解释一下什么是 尾闭包。简略来说,就是如果一个函数的最初一个参数也是一个函数,那么咱们就能够应用尾闭包的形式来传递最初一个函数。大家能够看上面的代码示例:

// 例子中的 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 我的项目的源码,不过过后只是简略的看了一遍。就认为本人把握了外面的一些常识和原理。然而当我想实现我本人心中的这个想法的时候。却发现之前并没有把这个我的项目外面实际的一些办法和技巧把握好。所以我决定先好好的把这个我的项目的源码看懂,而后本人先实现一个跟原来的我的项目性能一样的样例之后才开始着手实现本人的小编译器。

情谊提醒,接下来的文章内容比拟长,倡议珍藏后再仔细阅读

编译器的实现过程

the-super-tiny-compiler 咱们能够理解到,对于个别的编译器来说。次要有四个步骤去实现编译的过程,这四个步骤别离是:

  • tokenizer:将咱们的代码文本字符串转换成一个个有意义的单元(也就是 token。比方if"hello"123letconst 等等。
  • parser:将上一步获取到的 token 转换成以后语言的形象语法树,也就是AST(Abstract Syntax Tree)。为什么要这么做呢?因为这样解决之后,咱们就晓得代码中语句的先后关系和层级关系。也晓得运行的程序,以及上下文等等相干的信息了。
  • transformer:将上一步获取到的 AST 转换成目标语言的AST。为什么要做这一步呢?对于雷同性能的程序语句来说,如果抉择实现的语言不一样,那它们的语法大概率也是不一样的。这就导致了它们对应的形象语法树也是不一样的。所以咱们须要做一次转换,为了下一步生成目标语言的代码做好筹备。
  • codeGenerator:这一步相对来说比较简单,晓得了目标语言的语法之后,咱们依据上一步骤生成的新的形象语法树,能够方便快捷的生成咱们想要的目标语言的代码。

下面的步骤就是编译器的大略工作流程了,然而仅仅晓得这些流程还是不够的,还须要咱们亲自动手实际一下。如果看到这里你有趣味的话,能够点击这里 JavaScript Trailing Closure Toy Compiler 先体验一下最初实现的成果。如果你对具体的实现过程感兴趣的话,能够持续上面的浏览,置信看过之后你会有很大的播种的,兴许会想要本人也实现一个乏味的编译器呢。

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

首先咱们须要明确为什么要把字符串转换为一个个的 token,因为如果不做转换,咱们就不晓得这段程序要示意的是什么意思,因为token 是了解一段程序的必要条件。

这就好比 console.log("hello world!") 这个语句来说,咱们一眼就晓得它是干嘛的,然而咱们是怎么思考的呢?是不是首先是 console 咱们晓得是 console 对象,而后是 . 咱们晓得是获取对象的属性操作符,再而后是 log 办法,而后办法的调用须要 ( 左括号作为开始,而后是 hello world! 字符串为参数,而后遇到了前面的 ) 右括号示意完结。

所以把字符串转换成 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 示例:

示意的程序语句应该是:

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,让这个小我的项目变得更好一点。也欢送大家在文章上面留言,看看是不是能碰撞出什么新的思路与想法。

一些同学可能会说,学习这种货色有什么作用?其实用处有很多,首先咱们当初前端的构建基本上离不开 Babel 对 JavaScript 新个性的反对,而 Babel 的作用其实就是一个编译器的作用,把咱们语言新的个性转换成目前的浏览器能够反对的一些语法,让咱们能够不便的应用新的语法,也加重了前端开发的一些累赘

另一方面你如果晓得这些原理,你不仅能够很容易看懂一些 Babel 的语法转换插件的源代码,你还能够本人亲自动手实现一个简略的语法转换器或者一些有意思的插件。这会让你的前端能力有一个大的晋升。

工夫过得好快,间隔上次公布文章曾经过来两个月了😂,这篇文章也是过完年后的第一篇文章,心愿当前还可能继续的输入一些高质量的文章。当然之前的 设计模式大冒险系列 还会继续更新,也欢送大家持续放弃关注。

明天的文章到这里就完结了,如果你对这篇文章有什么意见和倡议,欢送在文章上面留言,或者在这里提出来。也欢送大家关注我的公众号 关山不难越,如果你感觉这篇文章写的不错,或者对你有所帮忙,那就点赞分享一下吧~

参考:

  • the-super-tiny-compiler
退出移动版