博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
代码来构建一个简单的compiler
阅读量:6885 次
发布时间:2019-06-27

本文共 11824 字,大约阅读时间需要 39 分钟。

关于可以参考这篇文章。或者微信搜索订阅号'北宸南蓁'或者搜索wDxKn89。

Parsing

我们来构建一个tokenizer用于进行lexical analysis(词法分析)

设计思路

我们向tokenizer中传递需要转换的字符类型的code,并且将code通过一些分解规则,拆分为tokens数组。

(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]复制代码

tokenizer代码实现

function tokenizer(input) {  //该'current'变量用于追踪代码走到哪里,可以类比SQL中的游标(https://blog.csdn.net/DreamLLOver/article/details/51523887)  let current = 0;  //用于存放tokens的数组  let tokens = [];  //通过判断'current'与input.length的长度来控制循环次数和对tokens的处理次数  while (current < input.length) {    //取出指定游标下需要处理的code.    let char = input[current];    //首先我们向要校验是否是'(',之后会被'CallExpression'使用,但是我们现在只关心这个字符是什么。       if (char === '(') {         //如果满足条件,我们向token数组push一个type值为'parn',同时value为'('的token对象。      tokens.push({        type: 'paren',        value: '(',      });      // 更新游标的值。      current++;      // 继续处理剩余的code      continue;    }    //道理同上    if (char === ')') {      tokens.push({        type: 'paren',        value: ')',      });      current++;      continue;    }       //由于在代码中会存在空格/tab等制造的空格,同时我们在构建token的时候,是不必要关注空格的,因为空格本身对代码运行没有任何影响。    let WHITESPACE = /\s/;    if (WHITESPACE.test(char)) {      current++;      continue;    }    //由于我们实现的是基于两个数字参数的转换,对于一些非数字会有其他匹配规则或者类似'12sdsd4'是不满足运行规则的,所以需要挑选出满足条件的数字token    let NUMBERS = /[0-9]/;    if (NUMBERS.test(char)) {      //用于存放数字字符      let value = '';      //然后会不停的去loop code 序列      while (NUMBERS.test(char)) {        value += char;        char = input[++current];      }      // 将'number'token存放到tokens中      tokens.push({ type: 'number', value });      continue;    }       //同时为了满足compiler的多样性,我们还支持String的转换,只要满足字符串被double quotes 包裹    if (char === '"') {            let value = '';      // 跳过引号      char = input[++current];      //纪录String        value += char;        char = input[++current];      }      // 跳过引号      char = input[++current];      // 添加StringToken      tokens.push({ type: 'string', value });      continue;    }        //最后我们需要处理function name,用于标识操作动作    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;    }    //最后如果code没有满足条件说明,该code是一个'脏code'    throw new TypeError('I dont know what this character is: ' + char);  }//最后返回处理过的tokens  return tokens;}复制代码

Parser的代码实现

我们已经有了tokenizer(已经将raw code格式化为tokens array),既然烹饪材料已经有了,就需要对'材料'进行进一步加工,从而转换为AST。

[{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }复制代码

我们定义一个接收tokens数组的'parser'函数

function parser(tokens) {  //与处理raw code 的方式一样,需要一个"游标"来跟踪代码的运行轨迹  let current = 0;  //在处理raw code生成tokens是用的while对需要处理的char来根据不同的处理规格进行token的生成,现在我们处理token的时候,完全可以构建一个函数,这样能够使得代码更加的清晰  function walk() {   //获取tokens数组对应current下标的token对象    let token = tokens[current];    //我们根据用于标识token类型的type来进行token的分类    if (token.type === 'number') {      //更新游标      current++;      //构建AST node结点,type是需要事先按照一定的规则进行赋值,value是token的value      return {        type: 'NumberLiteral',        value: token.value,      };    }    // 处理type为string的token,用于生成满足对应type的AST node    if (token.type === 'string') {      current++;      return {        type: 'StringLiteral',        value: token.value,      };    }   //根据案例code,我们是将LISP的函数转换为C语言的,根据LISP函数的特点,'('代表函数的开始,所以我们需要处理type为paren(括号)同时值为'('的token,用于构建一个代表函数的AST node。    if (      token.type === 'paren' &&      token.value === '('    ) {     //这一步需要额外的注意,我们通过处理tokens用于构建对应的AST,我们只关心函数名是什么,参数是什么,而不关心'('或者')'这些不具备函数特性的东西,它们只是标识一个函数的函数开始和结束      token = tokens[++current];      //构建一个type为CallExpression Base node ,我们将raw code的函数名作为该node的name的值。同时params用于存放函数内部的参数或者是子函数的nodes.      let node = {        type: 'CallExpression',        name: token.value,        params: [],      };      // 刚才如果是处理'(',在处理完之后,current还是指向函数名,所以需要将游标更新到最新      token = tokens[++current];     //由于raw code的形式是变化不定的,所以如果只是根据tokens.length来进行while处理是远远不够,也不正确的,因为可能在LISP的一个函数中可能内嵌n多个子函数。          //   (add 2 (subtract 4 2))      //       // 下面的代码中存在多个')',这种情况在parser的过程中是不可预知的。      //   [      //     { type: 'paren',  value: '('        },      //     { type: 'name',   value: 'add'      },      //     { type: 'number', value: '2'        },      //     { type: 'paren',  value: '('        },      //     { type: 'name',   value: 'subtract' },      //     { type: 'number', value: '4'        },      //     { type: 'number', value: '2'        },      //     { type: 'paren',  value: ')'        }, <<< 内嵌)的情况      //     { type: 'paren',  value: ')'        }, <<< )最外层的)      //   ]      //所以,我们是不能通过简单的循环来对tokens进行AST化,但是我们可以利用walk函数,进行递归调用,来让walk来处理内嵌CallExpression的情况,只要控制好停止条件就可以。      //通过while来控制递归是否终止      while (        (token.type !== 'paren') ||        (token.type === 'paren' && token.value !== ')')      ) {        //调用walk(),将返回的node push到用于存放AST树形结构的node.params中。        node.params.push(walk());        token = tokens[current];      }       //更新游标,跳出tokens的数据范围      current++;      // 返回处理之后的node结点。      return node;    }    // 容错处理.    throw new TypeError(token.type);  }   //构建type为Program的root node。  let ast = {    type: 'Program',    body: [],  };    //调用walk()来处理tokens,然后将处理之后的结果存放到AST root node的body属性中。  while (current < tokens.length) {    ast.body.push(walk());  }  // 返回根据tokens处理过的AST对象。  return ast;}复制代码

Transformation

通过Parsing对raw code的处理,生成对应的AST。但是我们需求是对AST进行处理来生成目标AST。但是现在有一个问题,如何才能遍历这些树形结构,总不能用while来处理,同时也不能保证node信息的被按访问顺序的纪录。

所以我们需要能利用visitor(针对树形结构的游标)来构建一个访问AST同时按照访问顺序纪录一些node信息的"遍历器"。

traverser的代码实现

大致思路,traverse接收ast,还有不同类型的visitor。

traverse(ast, {      Program: {        enter(node, parent) {          // ...        },        exit(node, parent) {          // ...        },      },       CallExpression: {        enter(node, parent) {          // ...        },        exit(node, parent) {          // ...        },      },       NumberLiteral: {        enter(node, parent) {          // ...        },        exit(node, parent) {          // ...        },      },    });复制代码

真实代码

function traverser(ast, visitor) {  //该函数用于处理node.body/node.params,处理顶层的树结构,将child剥离出来,然后进行traverseNode处理  function traverseArray(array, parent) {    array.forEach(child => {      traverseNode(child, parent);    });  }  //接收需要被处理的'node'结点,同时将该'node'的直接parent的结点传入  function traverseNode(node, parent) {    //根据被遍历的node结点的type,从visitor中获取到对应type的处理对象    let methods = visitor[node.type];    //如果该处理对象存在同时有enter方法(也就是visitor匹配了node type了),    //然后将node,node 的直接parent结点传入,进行下一步处理    if (methods && methods.enter) {      methods.enter(node, parent);    }        //通过node type来进行不同的操作处理    switch (node.type) {      //从AST的顶层入口,顶层的type为`Program`,同时树形的关联关系和逻辑都被存放在body属性中,所以我们需要对body进行traverseArray处理,由于traverseArray()内部调用traverseNode(),所以会对body内部所有的child进行递归traverseNode()处理。      case 'Program':        traverseArray(node.body, node);        break;      //对AST中的CallExpression进行处理      case 'CallExpression':        traverseArray(node.params, node);        break;              //由于`NumberLiteral` 和`StringLiteral`没有任何child node,所以不需要进行traverseArray()处理,直接跳过      case 'NumberLiteral':      case 'StringLiteral':        break;      //容错处理      default:        throw new TypeError(node.type);    }    //由于遍历AST是采用depth-first,所以需要在处理完所有child的时候,进行推出操作    if (methods && methods.exit) {      methods.exit(node, parent);    }  }  //我们通过启动traverseNode()来触发遍历操作,由于顶层的AST是不存在parent node,所以直接传入null  traverseNode(ast, null);}复制代码

transformer

通过构建了traverser(),我们现在有能力可以对AST进行有目的的遍历,同时还可以保证他们直接存在的原有关联不被破坏。接下来,我们就需要利用traverser()来对AST进行有目的的改造(生成新的AST)。

原始的AST

{      type: 'Program',      body: [{        type: 'CallExpression',        name: 'add',        params: [{          type: 'NumberLiteral',          value: '2'        }, {          type: 'CallExpression',          name: 'subtract',          params: [{            type: 'NumberLiteral',            value: '4'          }, {            type: 'NumberLiteral',            value: '2'          }]        }]      }]    }复制代码

转换后的AST

{    type: 'Program',        body: [{            type: 'ExpressionStatement',            expression: {                type: 'CallExpression',                callee: {                    type: 'Identifier',                    name: 'add'                },                arguments: [{                    type: 'NumberLiteral',                    value: '2'                }, {                    type: 'CallExpression',                    callee: {                        type: 'Identifier',                        name: 'subtract'                    },                    arguments: [{                        type: 'NumberLiteral',                        value: '4'                    }, {                        type: 'NumberLiteral',                        value: '2'                    }]                }             }        }]}复制代码

transformer的代码实现(接收lisp ast)

function transformer(ast) {  //首先先构建一个 newAst和lisp ast拥有相同的program node  let newAst = {    type: 'Program',    body: [],  };  //我们采用直接在old ast中设置一个context(或者说在每一级的parent中设置一个用于接收处理过的AST),context 是从old ast转换为new ast的引用  ast._context = newAst.body;    //调用traverser对ast在特定的visior下针对满足条件的node 结点进行处理。  traverser(ast, {    // 处理type为 `NumberLiteral`的node,    NumberLiteral:       enter(node, parent) {              //重新构建了一个type为'NumberLiteral'并push到用于纪录树的关联关系的context中。        parent._context.push({          type: 'NumberLiteral',          value: node.value,        });      },    },    // 构建处理 `StringLiteral`    StringLiteral: {      enter(node, parent) {        parent._context.push({          type: 'StringLiteral',          value: node.value,        });      },    },    // 处理 `CallExpression`.    CallExpression: {      enter(node, parent)               //构建一个新增了内置Identifier的node 结点        let expression = {          type: 'CallExpression',          callee: {            type: 'Identifier',            name: node.name,          },          arguments: [],        };                /在原始的expression node中新增一个context,用于存放函数的参数nodes        node._context = expression.arguments;               //判断parent node的type是否是'CallExpression'(可能会存在如下的lisp (add substricl(2,3),但是是不满足情况的,由于在ast生成的阶段只是根据type来构建,没有进行语法的校验)        if (parent.type !== 'CallExpression') {          //我们将`CallExpression`node 包装在ExpressionStatement中。(CallExpression在JS中保留声明)          expression = {            type: 'ExpressionStatement',            expression: expression,          };        }                //将处理之后的node更新到parent的context中        parent._context.push(expression);      },    }  });  //返回处理之后的ast  return newAst;}复制代码

Code Generator

我们采用递归调用code generator将new ast中的node字符化。 codeGenerator的代码实现

function codeGenerator(node) { // 根据type来区分不同的输出处理 switch (node.type) {   //如果遇到`Program` node,将body的数组中的item通过再次调用codeGenerator来进行类输出,(可以认为是递归调用)   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函数。他们直接的交互顺序和方式如下:

  1. input(数据源) => tokenizer => tokens
  2. tokens => parser => ast
  3. ast => transformer => newAst
  4. newAst => generator => output(目标)
function compiler(input) {  let tokens = tokenizer(input);  let ast    = parser(tokens);  let newAst = transformer(ast);  let output = codeGenerator(newAst);    return output;}复制代码

我们也可以将这些方法进行导出

module.exports = {  tokenizer,  parser,  traverser,  transformer,  codeGenerator,  compiler,};复制代码
你可能感兴趣的文章
Hark的数据结构与算法练习之若领图排序ProxymapSort
查看>>
绕过IE10直接安装VS2013
查看>>
ASP.NET SignalR 高可用设计
查看>>
《用户体验要素》澄清了 UI 原型设计中看不见确感受得到的那一层
查看>>
HttpClient使用具体解释
查看>>
导师制
查看>>
js面向对象
查看>>
EasyUI学习之menu and button(菜单和按钮)
查看>>
微信开放平台 公众号第三方平台开发 教程一 平台介绍
查看>>
javascript有关this的那些事(某渣提出的问题)
查看>>
Java构造和解析Json数据的两种方法详解二——org.json
查看>>
using log4net on my project within a self-hosted WCF application z
查看>>
[唐诗]江亭夜月送别二首(其二)-王勃
查看>>
【转】android 4.3 BLE onCharacteristicWrite没有回调
查看>>
tar命令的详解
查看>>
1.3 函数调用反汇编解析以及调用惯例案例分析
查看>>
谈谈JAVA工程狮面试中经常遇到的面试题目------什么是MVC设计模式
查看>>
【转】Android Service完全解析,关于服务你所需知道的一切(下) ---- 不错
查看>>
[STL][C++]LIST
查看>>
SQL2005:使用varchar(max)代替text
查看>>