【摘要】 javascript实现递归下降语法解析

示例代码托管在:http://www.github.com/dashnowords/blogs

B站地址:【编译原理】

Stanford公开课:【Stanford大学公开课官网】

课程里涉及到的内容讲的还是很清楚的,但个别地方有点脱节,建议课下自己配合经典著作《Compilers-priciples, Techniques and Tools》(也就是大名鼎鼎的龙书)作为补充阅读。

一. Parse阶段

词法分析阶段的任务是将字符串转为Token组,而Parse阶段的目标是将Token变为Parse Tree,本篇只是这部分内容最基础的一部分。

CFG

CFGcontext free grammer,定义一种CFG语法规则需要声明如下特征:

  • 一组终止符号集,也称为“词法单元”

  • 一组非终止符号集,也称为“语法变量”

  • 一个开始符号集

  • 若干产生式规则(产生式则就是指在当前CFG的语法下,产生符号->左右两侧可以互相替代)

CFG的基本转换流程如下:

从隶属于开始集S开始,尝试将字符串中的非终止符X替换为终止集的形式(X->Y1Y2...Yn),重复这个步骤直到字符串序列中不再有非终止符。这个过程被称为Derivation(派生),它是一系列变换过程的序列,可以转换为树的形式,树的根节点即为起始集合S中的成员,转换后的每个终止集以子节点的形式挂载在根节点下,这棵生成的树就被称为Parse Tree,可以看出最后的结果实际上就是Parse Tree的叶节点遍历结果。

当需要转换的非终结字符有多个时,需要按照一定的顺序来逐个推导,派生过程可以按照left-mostright-most进行,但有时会得到不同的合法的转换树,通常会通过修改转换集语法或设定优先级来解决。

Recursive Descent(递归下降遍历)

Recursive Descent是一种遍历parse tree的策略,是一种典型的递归回溯算法,从树的根节点开始,逐个尝试当前父节点上记录的非终止字符能够支持的产生规则,并判断其子节点是否符合这样的形式,直到子节点符合某个特定的产生式规则,然后再继续递归进行深度遍历,如果在某个非终止节点上尝试完所有的产生式规则都无法继续向下进行使得子树的叶节点都符合终止符号集,则需要通过回溯到上一节点并尝试父节点的下一个产生式规则,使得循环程序可以继续向后进行。课程里用了很多的数学符号定义和伪代码来描述递归遍历的过程,如果觉得太抽象不好理解可以暂时略过。需要注意左递归文法会使得递归下降遍历进入死循环,在文法设计时应该避免,龙书中也提供了一种通用的拆分方法来解决这个问题。

二. 递归下降遍历

【声明】由于课程中并没有看到从tokensparse tree的全貌,只能先逐步消化基础知识。下文的过程只是笔者自己的理解(尤其是逐行分析的形式,因为尚未涉及任何结构性语法,所以通用性还有待考量),仅供参考,也欢迎交流指正。但对于直观理解递归下降法而言是足够的。

2.1 预备知识

本节中使用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】进行了解。

2.2 多行语句的处理思路

我们把上面的目标解析代码当做是一段Javascript代码,自顶向下分析时,根节点的类型是Program,它可以由多个Statement节点(语句节点)构成,所以本例中进行简化后以semicolon(分号)作为词素批量处理的分界点,每次将两个分号之间的部分读入缓冲区进行分析,由于上例中均为单行语句,所以理解起来比较简单。

在更为复杂的情况中,代码中包含条件语句,循环语句等一些结构化的关键词时可能会存在跨行的语句,此时可以在递归下降之前先对缓冲区的词素队列进行基本的结构分析,如果发现匹配的结构化模式,就从tokens序列中将下一行(或多行)也读入缓冲区,直到缓冲区中的所有tokens放在一起符合了某些特定的结构,再开始进行递归下降。

2.3 简易的文法定义

为方便理解,本例中均使用关键词缩写来表示可能的语法规则集,如果你对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,这里就是一种左递归,如果不进行任何处理,在代码运行时就会陷入死循环然后爆栈,这也就是前文强调的需要在语法产生式设计时消除左递归的场景。这里并不是说spiderMonkeyparserAPI是错的,因为消除左递归的语法改造只是一种等价形式的转换,是为了防止产生式产生无限递推(或者说程序实现时进入无限递归的死循环)而做的一种形式处理,改造的过程可能只是引入了某个中间集合来消除这种场景的影响,对于最终的语法表意并不会产生影响。

下文示例代码中并没有进行严谨的"左递归消除",而是简单地使用了一个E_集合,与原本的E进行一些微小的差异区分,从而避免了死循环。

2.4 文法产生式的代码转换

下面将上一小节的语法规则进行代码翻译(只包含部分产生式的推导,本例中的完整代码可以从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) {//本例中不涉及,直接返回falsereturn false;
}//判断是否为IfStatement I -> if ( ExpressionStatement ) { Statement }
function I(tokens) {//本例中不涉及,直接返回falsereturn 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) {//本例中不涉及,直接返回falsereturn 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));
}//....其他代码形式雷同,不再赘述

2.5 逐行解析

解析时默认每次遇到一个分号时表示一个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;
}

2.6 查看计算过程

单步执行查看计算过程可以帮助我们更好地理解递归下降法的执行过程:

在demo所在目录下打开命令行,输入:node --inspect-brk recursive-descent.js,然后在chrome中打开chrome://inspect,单步执行就很容易看出代码在执行过程中如何实现递归和回溯:

三.小结

单纯地递归下降法最终的结果只找出了不满足任何语法规则的语句,或是最终所有语句都符合语法规则时给出提示,但并没有得到一个树结构的对象,也没有向下一个环节提供输出,如何在编译过程中与后续环节进行连接还有待探索。

demo.rar

md原文.rar

作者:大史不说话

Stanford公开课《编译原理》学习笔记(2)递归下降法相关推荐

  1. [编译原理学习笔记2-2] 程序语言的语法描述

    [编译原理学习笔记2-2] 程序语言的语法描述 文章目录 [编译原理学习笔记2-2] 程序语言的语法描述 [2.3.1] 上下文无关文法 [2.3.2] 语法分析树与二义性 [2.3.3] 形式语言鸟 ...

  2. 编译原理学习笔记20——符号表

    编译原理学习笔记20--符号表 20.1 符号表的组织与操作 20.2 符号表的内容 20.3 利用符号表分析名字的作用域 20.1 符号表的组织与操作 符号表 符号表的作用与组织 符号表的整理和查找 ...

  3. 编译原理学习笔记2——高级程序设计语言概述

    编译原理学习笔记2--高级程序设计语言概述 2.1常用的高级程序设计语言 2.2程序设计语言的定义 2.2.1语法 2.2.1语法 2.2.3程序语言的基本功能和层次机构 2.2.4程序语言成分的逻辑 ...

  4. 编译原理学习笔记 5.1 翻译文法和语法制导翻译

    前言 参考课上PPT内容. 该学习笔记目前仅打算个人使用. 后续会进一步整理,包括添加笔记内容,标明参考资料. 更新中... 跳过目录 目录 导言 一.翻译文法和语法制导翻译 输入文法 翻译文法 活动 ...

  5. 编译原理学习笔记 3.3 正则文法的状态图

    前言 参考东南大学廖力老师的编译原理教程和课上PPT内容. 该学习笔记目前仅打算个人使用. 后续会进一步整理,包括添加笔记内容,标明参考资料. 更新中... 跳过目录 目录 状态图的画法(根据文法画出 ...

  6. 编译原理学习笔记一(待续)

    这几天忙着学英语,同时在学习编译原理,对这门课很感兴趣,已经制作了词法分析器,同时还在补充这个分析器的功能,也准备着手开始写语法分析器,看到最后能不能连在一起,我想如果能够将整套编译器的流程跑下来真的 ...

  7. Open SAP 上 SAP Fiori Elements 公开课第一单元学习笔记

    Open SAP 课程地址 这门公开课的教学大纲: 第一单元:Painting the big picture 本课程将使用 SAP Fiori Elements 开发一系列的应用,如下图所示: Th ...

  8. 编译原理学习笔记 6.2 符号表的组织与内容

    前言 参考课上PPT内容. 该学习笔记目前仅打算个人使用. 后续会进一步整理,包括添加笔记内容,标明参考资料. 更新中... 跳过目录 目录 一.符号表的结构与内容 "名字"域 & ...

  9. 吴恩达机器学习公开课第一周学习笔记

    Octave是一种编程语言,旨在解决线性和非线性的数值计算问题.Octave为GNU项目下的开源软件,早期版本为命令行交互方式,4.0.0版本发布基于QT编写的GUI交互界面.Octave语法与Mat ...

  10. 程序设计语言编译原理_编译原理学习笔记(二):高级程序设计语言

    高级程序设计语言 一.语言概述 1.1 语法 v.s. 语义 程序本质上是一定字符集上的字符串 语法:一组规则,用它可以形成和产生一个合式(well-formed)的程序 定义了程序的形式结构 定义语 ...

最新文章

  1. CSS之布局(盒子模型--内边距)
  2. Use MVS Dsbame convensions. windows下ftp.exe客户端上传错误
  3. shell笔记之sed编辑器的基础用法(上)
  4. PCL点云数据 滤波降噪
  5. Android按钮持续按下执行,Android 按钮长按下去重复执行某个动作,放开后停止执行动作...
  6. 华科计算机考研2022年分数线,2022年华中科技大学软件工程考研分数线、参考书、上岸前辈初复试经验...
  7. 怎么修改谷歌浏览器文件提交按钮样式_使用css自定义input file浏览按钮样式
  8. EF创建上下文对象HttpContext和CallContext
  9. python3.12答案_编程常见问题
  10. 为什么定义!doctype html表格高度变高,!DOCTYPE html声明下div高度100%的问题解决方法...
  11. 190729知识笔记
  12. 对类HelloWorld程序中添加一个MessageBox弹窗
  13. jsoup教程_2 http-client 讲解
  14. please verify the preference field with the prompt:Tomcat JDK name
  15. Delphi Sysem.JSON 链式写法(转全能中间件)
  16. jsp点击按钮弹出输入框_【问答3】需要点击虚拟键盘上发送(搜索)按钮的写法...
  17. element ui 表格拆分表格_python拆分表格数据
  18. 极域电子教室破解还原卡
  19. 2.3 Visio画虚线后插入word或PPT变为实线
  20. java 通过 ip地址 找到 打印机_有没有办法使用java套接字程序找到打印机状态?...

热门文章

  1. SLAM Cartographer(17)分支定界闭环检测
  2. pb string 接收dll按值返回_JavaScript 是如何工作的:JavaScript 的共享传递和按值传递...
  3. sql like 多个条件_都9012年啦,不懂得这些SQL语句优化,你是要吃大亏的
  4. L2-DAY 2-程序完善夜
  5. AssertJ断言系列一
  6. django的form常用字段和参数
  7. [Java][内存模型]
  8. e课表项目第二次冲刺周期第四天
  9. composer安装laravel-u-editor及其使用
  10. 解决 spring mvc 3.0 结合 hibernate3.2 使用tx:annotation-driven声明式事务无法提交的问题(转载)...