目录html
示例代码托管在:http://www.github.com/dashnowords/blogs前端
博客园地址:《大史住在大前端》原创博文目录node
华为云社区地址:【你要的前端打怪升级指南】git
B站地址:【编译原理】github
Stanford公开课:【Stanford大学公开课官网】算法
课程里涉及到的内容讲的仍是很清楚的,但个别地方有点脱节,建议课下本身配合经典著做《Compilers-priciples, Techniques and Tools》(也就是大名鼎鼎的龙书)做为补充阅读。数组
词法分析阶段的任务是将字符串转为Token
组,而Parse
阶段的目标是将Token
变为Parse Tree
,本篇只是这部份内容最基础的一部分。ide
CFG
即context free grammer
,定义一种CFG
语法规则须要声明以下特征:函数
CFG
的语法下,产生符号->
左右两侧能够互相替代)CFG
的基本转换流程以下:命令行
从隶属于开始集S
开始,尝试将字符串中的非终止符X
替换为终止集的形式(X->Y1Y2...Yn
),重复这个步骤直到字符串序列中再也不有非终止符。这个过程被称为Derivation
(派生),它是一系列变换过程的序列,能够转换为树的形式,树的根节点即为起始集合S
中的成员,转换后的每一个终止集以子节点的形式挂载在根节点下,这棵生成的树就被称为Parse Tree,能够看出最后的结果实际上就是Parse Tree
的叶节点遍历结果。
当须要转换的非终结字符有多个时,须要按照必定的顺序来逐个推导,派生过程能够按照left-most
或right-most
进行,但有时会获得不一样的合法的转换树,一般会经过修改转换集语法或设定优先级来解决。
Recursive Descent
是一种遍历parse tree
的策略,是一种典型的递归回溯算法,从树的根节点开始,逐个尝试当前父节点上记录的非终止字符可以支持的产生规则,并判断其子节点是否符合这样的形式,直到子节点符合某个特定的产生式规则,而后再继续递归进行深度遍历,若是在某个非终止节点上尝试完全部的产生式规则都没法继续向下进行使得子树的叶节点都符合终止符号集,则须要经过回溯到上一节点并尝试父节点的下一个产生式规则,使得循环程序能够继续向后进行。课程里用了不少的数学符号定义和伪代码来描述递归遍历的过程,若是以为太抽象很差理解能够暂时略过。须要注意左递归文法会使得递归降低遍历进入死循环,在文法设计时应该避免,龙书中也提供了一种通用的拆分方法来解决这个问题。
【声明】因为课程中并无看到从
tokens
到parse tree
的全貌,只能先逐步消化基础知识。下文的过程只是笔者本身的理解(尤为是逐行分析的形式,由于还没有涉及任何结构性语法,因此通用性还有待考量),仅供参考,也欢迎交流指正。但对于直观理解递归降低法而言是足够的。
本节中使用JavaScript
来实现递归降低遍历,目标代码还是上一篇博文中的示例代码:
var b3 = 2; a = 1 + ( b3 + 4); return a;
通过上一节的分词器后能够获得下面的词素序列:
[ 'keywords', 'var' ], [ 'id', 'b3' ], [ 'assign', '=' ], [ 'num', '2' ], [ 'semicolon', ';' ], [ 'id', 'a' ], [ 'assign', '=' ], [ 'num', '1' ], [ 'plus', '+' ], [ 'lparen', '(' ], [ 'id', 'b3' ], [ 'plus', '+' ], [ 'num', '4' ], [ 'rparen', ')' ], [ 'semicolon', ';' ], [ 'keywords', 'return' ], [ 'id', 'a' ], [ 'semicolon', ';' ]
语法分析是基于语法规则的,所谓语法规则,一般是指一系列CFG表示的产生式,大多数开发者并不具有设计一套语法规则的能力,此处直接借鉴Mozilla
中的Javascript
引擎SpiderMonkey
中的文法定义来进行基本产生式,因为Javascript
语言中涉及的文法很是多,本节只筛选出与目标解析式相关的一部分简化的语法规则(图中标记为蓝色的部分):
完整的语法规则能够查看【SpiderMonkey_ParserAPI】进行了解。
咱们把上面的目标解析代码当作是一段Javascript
代码,自顶向下分析时,根节点的类型是Program
,它能够由多个Statement
节点(语句节点)构成,因此本例中进行简化后以semicolon
(分号)做为词素批量处理的分界点,每次将两个分号之间的部分读入缓冲区进行分析,因为上例中均为单行语句,因此理解起来比较简单。
在更为复杂的状况中,代码中包含条件语句
,循环语句
等一些结构化的关键词时可能会存在跨行的语句,此时能够在递归降低以前先对缓冲区的词素队列进行基本的结构分析,若是发现匹配的结构化模式,就从tokens
序列中将下一行(或多行)也读入缓冲区,直到缓冲区中的全部tokens
放在一块儿符合了某些特定的结构,再开始进行递归降低。
为方便理解,本例中均使用关键词缩写来表示可能的语法规则集,若是你对Javascript
语言有必定了解,它们是很是容易理解的
/** * 文法定义-生产规则 * Program -> Statement * P -> S * * 语句 -> 块状语句 | if语句 | return语句 | 声明 | 表达式 |...... * Statement -> BlockStatement | IfStatement | ReturnStatement | Declaration | Expression |...... * S -> B | I | R | D | E * * B -> { Statement } * * I -> if ( ExpressionStatement ) { Statement } * * R -> return Expression | null * * 声明 -> 函数声明 | 变量声明 * Declaration -> FunctionDeclaration | VariableDeclaration * D -> F | V * * F -> function ID ( SequenceExpression ) { ... } * * V -> 'var | let | const' ID [= Expression | Null] ? * * 表达式 -> 赋值表达式 | 序列表达式 | 一元运算表达式 | 二元运算表达式 |...... * Expression -> AssignmentExpression | SequenceExpression | UnaryExpression | BinaryExpression | BracketExpression...... * E -> A | Seq | U | BI | BRA |... * * A -> E = E //赋值表达式 * * Seq -> ID,ID,ID//相似形式 * * //一元表达式 * U -> "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" E * * //二元表达式 * BI -> E "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in" | "instanceof" | ".." E * * //括号表达式 * BRA -> ( E ) * * N -> null */
须要额外注意的是表达式Expression
到赋值表达式AssignmentExpression
的产生式,E
的判断规则里须要判断A
,而A
的逻辑里又再次调用了E
,这里就是一种左递归,若是不进行任何处理,在代码运行时就会陷入死循环而后爆栈,这也就是前文强调的须要在语法产生式设计时消除左递归的场景。这里并非说spiderMonkey
的parserAPI
是错的,由于消除左递归的语法改造只是一种等价形式的转换,是为了防止产生式产生无限递推(或者说程序实现时进入无限递归的死循环)而作的一种形式处理,改造的过程可能只是引入了某个中间集合来消除这种场景的影响,对于最终的语法表意并不会产生影响。
下文示例代码中并无进行严谨的"左递归消除",而是简单地使用了一个E_
集合,与本来的E
进行一些微小的差别区分,从而避免了死循环。
下面将上一小节的语法规则进行代码翻译(只包含部分产生式的推导,本例中的完整代码能够从demo或代码仓中获取):
//判断是否为Statement function S(tokens) { //把结尾的分号所有去除 while(tokens[tokens.length - 1][0] === TT.semicolon){ tokens.pop(); } return B(tokens) || I(tokens) || R(tokens) || D(tokens) || E(tokens); } //判断是否为BlockStatement B -> { Statement } (本例中并不涉及本方法,故暂不考虑末尾分号和文法递归的状况) function B(tokens) { //本例中不涉及,直接返回false return false; } //判断是否为IfStatement I -> if ( ExpressionStatement ) { Statement } function I(tokens) { //本例中不涉及,直接返回false return false; } //判断是否为ReturnStatement R -> return Expression | null function R(tokens) { return isReturn(tokens[0]) && (E(tokens.slice(1)) || N(tokens.slice(1)[0])); } //判断是否为声明语句 Declaration -> FunctionDeclaration | VariableDeclaration function D(tokens) { return F(tokens) || V(tokens); } //判断是否为函数声明 F -> function ID ( SequenceExpression ) { ... } function F(tokens) { //本例中不涉及,直接返回false return false; } //判断是否为变量声明 V -> 'var | let | const' ID [= Expression | Null] ? function V(tokens) { //判断为1.单纯的声明 仍是 2.带有初始值的声明 if (tokens.length === 2) { return isVariableDeclarationKeywords(tokens[0]) && tokens[1][0] === TT.id; } return isVariableDeclarationKeywords(tokens[0]) && (A(tokens.slice(1))) || N(tokens.slice(1)); } //....其余代码形式雷同,再也不赘述
解析时默认每次遇到一个分号时表示一个statement
的结束,前文已经说起过对于多行语句的处理思路。实现时只须要将tokens
序列一点点读进buffer数组并从顶层的S
方法启动分析,便可完成自顶向下的推理过程。
/**parser */ function parse(tokens) { let buffer = nextStatement(tokens); let flag = true; while (buffer && flag){ if (!S(buffer)) { console.log('检测到不符合语法的tokens序列'); flag = false; } buffer = nextStatement(tokens); } //若是没有出错则提示正确 flag && console.log('检测结束,被检测tokens序列是合法的代码段'); } //将下一个Statement所有读入缓冲区 function nextStatement(tokens) { let result = []; let token; while(tokens.length) { token = tokens.shift(); result.push(token); //若是不是换行符则 if (token[0] === CRLF) { break; } } return result.length ? result : null; }
单步执行查看计算过程能够帮助咱们更好地理解递归降低法的执行过程:
在demo所在目录下打开命令行,输入:node --inspect-brk recursive-descent.js
,而后单步执行就很容易看出代码在执行过程当中如何实现递归和回溯:
单纯地递归降低法最终的结果只找出了不知足任何语法规则的语句,或是最终全部语句都符合语法规则时给出提示,但并无获得一个树结构的对象,也没有向下一个环节提供输出,如何在编译过程当中与后续环节进行链接还有待探索。