本篇内容主要由 the-super-tiny-compiler中的注释翻译而来,该项目实现了一款包含编译器核心组成的极简的编译器。但愿可以给想要初步了解编译过程的同窗提供到一些帮助。java
add
和 subtract
他们用对应的语言分别实如余下:内容 | 类lisp | 类C | |
---|---|---|---|
2 + 2 | (add 2 2) | add(2, 2) | |
4 - 2 | (subtract 4 2) | subtract(4,2) | |
2 + ( 4-2 ) | (add 2 (subtract 4 2)) | add(2, subtract(4,2)) |
大部分的编译器能够粗略的划分为3个阶段: 解析 Parsing,翻译 Transformation,代码生成Code Generationnode
解析过程一般被分为两个部分: 词法分析,语法分析python
由这些词构成的词组用来描述语法,他们能够是数字,文本,标点符号,运算符等等git
抽象语法树(简称AST)是一个嵌套很深的对象,它以一种既容易使用又能告诉咱们不少信息的方式表示代码。github
(add 2 (subtract 4 2))
tokens表示以下express
{ 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: ')' }, ]
{ 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并对其进行更改。它能够用同一种语言操纵AST,也能够将AST翻译成一种全新的语言。数组
让咱们看看如何转换AST。函数
你可能会注意到咱们的AST中有看起来很是类似的元素。这些对象具备类型属性。每一个节点都称为AST节点。这些节点定义了描述树的一个独立部分的属性。学习
咱们有一个数字节点 "NumberLiteral"ui
{ type: 'NumberLiteral', value: '2', }
或者一个调用表达式节点
{ type: 'CallExpression', name: 'subtract', params: [...nested nodes here...], }
转换AST时,咱们能够经过添加/删除/替换属性来操纵节点,能够添加新节点,删除节点,也能够不使用现有的AST直接基于它建立一个全新的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,咱们将:
若是咱们直接操做此AST,而不是建立单独的AST,则可能会在这里引入各类抽象。 可是仅访问树中的每一个节点就足以完成咱们要尝试的操做。
我之因此使用“访问”一词,是由于存在这种模式来表示对象结构元素上的操做。
这里的基本思想是,咱们将建立一个“访客”对象,该对象的方法将接受不一样的节点类型。
var visitor = { NumberLiteral() {}, CallExpression() {}, };
可是,也有可能在“退出”时调用相应的操做。 想象一下之前以列表形式的树结构:
Program
CallExpression
当咱们往下遍历时,咱们遍历尽全部分支时。咱们“退出”它。 所以,沿着树下来,咱们“进入[enter]”每一个节点,而后“退出[exit]”。
-> Program (enter) -> CallExpression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Call Expression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Number Literal (enter) <- Number Literal (exit) <- CallExpression (exit) <- CallExpression (exit) <- Program (exit)
为了支持进入和退出操做,咱们将vistitor定义调整以下
var visitor = { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, } };
请注意,并非说每一个编译器看起来都和这里描述的彻底同样。编译器根据目的不一样有不少种,可能须要好比下详细介绍的步骤更多的步骤。
如今您应该对编译器的主要外观有一个大体的整体了解。
通过上面的解释和介绍,如今能够开始编写本身的编译器了,那么开始代码走起。
咱们将获取咱们的代码串将其解析成token数组
(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
function tokenizer(input) { // current变量,用来标记当前读入代码的字符位置的游标 let current = 0; // tokens数组变量,用来存入解析的token词组 let tokens = []; // 开启一个while循环,将current设置为循环内部的增量 while(current < input.length){ // 获取当前游标对应的字符 let char = input[current]; // 检查当前字符是不是一个括号 if(char=== "("){ // 若是是括号,则新增一个`paren`括号类型的,值为作括号的词到tokens词组 tokens.push({ type: 'paren', value: '(', }); //而后游标向后前进一位 current ++; // 进入下一循环 continue; } // 检查是否右括号,如是则新增一个右括号词组,增长游标,继续下一次循环 if (char === ')') { tokens.push({ type: 'paren', value:')' }) current++; continue; } // 检查当前字符是否空格,若是是空格则直接跳过,游标后移 // (add 123 456) // ^^^ ^^^ number let WHITESPACE = /\s/; if (WHITESPACE.test(char)) { current++; continue; } //下一个将检测的类型是number数字.和以前不一样的是number类型可能由多个数字字符组成,咱们须要 // 获取整个连续的数字串做为一个number类型的词token let NUMBERS = /[0-9]/; if(NUMBERS.test(char)){ //新建一个value串用来设置数字字符串 let value=''; while(NUMBERS.test(char)){ value += char; char = input[++current]; } tokens.push({type:'number',value}); continue; } // 在将要实现的编译器中也支持被双引号括起来的字符串 // (concat "foo" "bar") // ^^^^ ^^^^ 支付串 if (char === '"') { let value = ''; char = input[++current]; while (char != '"') { value += char; char = input[++current] } // 游标跳过终结的引号 char = input[++current]; tokens.push({ type:'string', value }) continue; } // 最后一个类型的token是`name`类型.由一串字母构成。该类型用做本编译器 // 的lisp语法风格的函数名 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; }
function parser(tokens) { // 新建current变量做为游标 let current = 0; // 该方法中将用递归代替while循环,先定义一个walk方法 function walk() { //获取当前token let token = tokens[current]; // 从number类型的token开始,将不一样类型的token置入代码的不一样位置 if (token.type === 'number') { // 若是当前是number类型,游标向前 current++; // 返回一个number类型的AST 节点 return { type: 'NumberLiteral', value: token.value } } // 字符token返回一个字符类型的AST节点 if (token.type === 'string') { current++; return { type: 'StringLiteral', value: token.value } } // 下面检查是否调用表达式.先判断是不是一个括号类型,且是左括号token if ( token.type === 'paren' && token.value === '(' ) { // 跳过当前左括号游标,获取下一个token token = tokens[++current]; let node = { type:'CallExpression', name: token.value, params: [] } // 游标向前移一位跳过 name类型的token token = tokens[++current]; // 如今开始遍历 CallExpression的参数,直到遇到右括号 // 这里开始会存在递归,咱们经过递归解决嵌套节点问题。 // 为了解释这一点,让咱们采用咱们的Lisp代码。 您能够看到 // add的参数是一个数字和一个包含本身的参数的嵌套的CallExpression。 // [ // { 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: ')' }, <<< Closing parenthesis // { type: 'paren', value: ')' }, <<< Closing parenthesis // ] // 咱们经过递归调用walk方式,去向前遍历内嵌的`CallExpression`. // 这里咱们建立一个While循环遍历直到遇到左括号 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; }
/*** * =================================== * ⌒(❀>◞౪◟<❀)⌒ * THE TRAVERSER!!! * =================================== * 如今经过parser有了一颗AST抽象语法树,如今经过vistor访问 * 每个节点 * 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) { 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); } /** * ============================================================================ * ⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽ * THE TRANSFORMER!!! * ============================================================================ */ /** * 下一步Ast转化. 将已经构建好的Ast树经过visitor转化成一颗新的Ast抽象语法树 * * ---------------------------------------------------------------------------- * Original AST | Transformed AST * ---------------------------------------------------------------------------- * { | { * type: 'Program', | type: 'Program', * body: [{ | body: [{ * type: 'CallExpression', | type: 'ExpressionStatement', * name: 'add', | expression: { * params: [{ | type: 'CallExpression', * type: 'NumberLiteral', | callee: { * value: '2' | type: 'Identifier', * }, { | name: 'add' * type: 'CallExpression', | }, * name: 'subtract', | arguments: [{ * params: [{ | type: 'NumberLiteral', * type: 'NumberLiteral', | value: '2' * value: '4' | }, { * }, { | type: 'CallExpression', * type: 'NumberLiteral', | callee: { * value: '2' | type: 'Identifier', * }] | name: 'subtract' * }] | }, * }] | arguments: [{ * } | type: 'NumberLiteral', * | value: '4' * ---------------------------------- | }, { * | type: 'NumberLiteral', * | value: '2' * | }] * (sorry the other one is longer.) | } * | } * | }] * | } * ---------------------------------------------------------------------------- */ //该方法接收类lisp抽象语法树,转化为类c语言的ast树 function transformer(ast) { //建立新的ast节点 let newAst = { type: 'Program', body: [] } // 将新ast树的body做为原ast树的_context属性 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: [], }; // 接下来,咱们将在原CallExpression节点 //定义一个上下文,引用expression的参数,以便设置参数。 node._context = expression.arguments; // 检测父节点是否CallExpresssion,若是不是执行下列代码 if (parent.type !== 'CallExpression') { // 用`ExpressionStatement`节点包裹 `CallExpression` // 作这一步转换的缘由是调用表达式最终是一个语句 expression = { type: 'ExpressionStatement', expression: expression }; } parent._context.push(expression); } } }) return newAst; }
/** * 这里开始最后一步代码:代码生成 */ 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`编译函数,将上述方法按以下顺序结合便可 * 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; }
如上即用javscript完成了一个简单的编译器,若是你习惯用其余的语言如java,go,python等等,能够尝试改写一下。固然以上介绍分享的内容只包含了编译器的主要步骤,至关于一个编译器的hello world,可是经过代码实现有一个更直观的感觉。后续有须要实现一些可能与编译有关的功能能够起到必定的帮助。