基于编译原理的计算器设计与实现样本.doc
《基于编译原理的计算器设计与实现样本.doc》由会员分享,可在线阅读,更多相关《基于编译原理的计算器设计与实现样本.doc(26页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、资料内容仅供您学习参考,如有不当之处,请联系改正或者删除。基于编译原理的计算器设计与实现 首先看一下这个计算器的功能: CALC set a = 1; b = 2CALC set c = 3CALC calc (10 + pow(b, c) * sqrt(4) - 135.0CALC exit如上所示, 这个计算器的功能非常简单: 1. 用set命令设置上下文中的变量。 2. 用calc命令计算一个表示式的值。 3. 用exit命令退出计算器。 我们把编译的重点放在calc命令后面的计算表示式的解析, 其它的部分我们能够简单处理( 如set命令能够这样简单处理: 先按分号分隔得到多个赋值处理,
2、 再按等号分隔就能够在上下文中设置变量了, 并不需要复杂的编译过程) 。如上的演示例子中, 我们使用编译技术处理的部分是(10 + pow(b, c) * sqrt(4) - 1, 其它部分我们只使用简单的文本处理。麻雀虽小, 但五脏俱全, 这个计算器包含编译技术中最必要的部分。虽然这次我们只是实现了一个计算器, 但所使用的技术足以实现一个简单的脚本语言的解释器了。这个计算器分为如下几个部分: 词法分析: 把表示式的文本, 解析成词法元素列表(tokenList)。 语法分析: 把tokenList解析成语法树(syntaxTree)。 语义分析: 把语法树转成汇编语言的代码(asm) 汇编器
3、: 把汇编代码翻译为机器码( 字节码) 。 虚拟机: 执行字节码。 一般的编译步聚中不包含”汇编器”和”虚拟机”, 而这里之因此包含这两个部分是因为: 一般编译器会直接由中间代码生成机器码, 而不是生成汇编代码, 而这里我之因此要生成汇编代码的原因是”调试的时候汇编的可读性很好”, 如果直接生成目标代码, 则会非常难用肉眼来阅读。 自己实现虚拟机的原因是: 现有的机器( 包括物理机和虚拟机以及模拟器) 的指令虽然也很丰富, 但似乎都没有直接计算”乘方”或”开方”的指令, 自已实现虚拟机能够任意设计计算指令, 这样能够降低整个程序的复杂度。 因汇编器与虚拟机并不是编译原理的部分, 因此下文中并不
4、会描述其实现细节, 但因为计算器代码编译后的目标代码就是汇编代码, 因此需要把汇编指令做一下说明( 以下把这个汇编语言简称为ASM) 。ASM指令介绍指令助记符 操作数 指命说明 storenumber把number放入栈顶add从栈顶取出两个数相加, 并把结果压回栈中sub从栈顶取出一个数做为被减数, 再取一个做为减数, 相减之后的结果入栈mul从栈顶取出两个数相乘, 并把结果入栈div从栈顶取出一个数做为除法的分子, 再取出一个做为除法的分母, 相除的结果入栈pow从栈顶取出一个数做为底, 再取出一个做为幂, 计算结果入栈sqrt从栈顶取出一个数, 把这个数开平方后的结果入栈print在控
5、制台打印栈顶的数字这个虚拟机是基于栈来设计的, 所有的计算指令的操作数都从栈中取, store命令向栈顶添加数据。print指令用于打印当前栈顶的数据, 在我们编译的汇编代码要做到: 正确计算出结果, 且计算完成之后的结果要刚好在栈顶, 这样最后调用一个print指令即能够控制台看到计算结果。ASM举例: 例1, 如果我们要计算1-2*3, 则我们写出的汇编代码如下( 行号是为下文解释代码方便而放上去的, 不是代码的一部分) : 点击(此处)折叠或打开 store 3store 2mulstore 1sub print 对这段代码的说明如下: 前两行向栈顶添加两个数字, 先压入3再压入2, 这
6、样栈顶的数字是2, 第二个数字是3。 第三行mul会从栈顶弹出两个数字( 2和3) 计算乘法, 并把结果( 6) 再压入栈中, 此时栈中只有一个数字6。 第四行向栈顶压入一个数字1, 此时栈顶为1, 第二个数字是6。 第五行sub指令从栈顶取出两个数字, 第一个数字1做为被减数, 第二个数字6做为减数, 即计算1-6, 并把结果压入栈中, 此时栈中只有一个数字-5。 最后一行print指令不对栈做写操作, 只读取栈顶的数字, 并打印出来。 在这里, 我们用到两个运算, mul和sub, 这两个运算都是二元运算, 因我在设计指令的时候, 先取出来的数字是第一个操作数, 因此先压入的应该是第二个操
7、作数, 这也是为什么代码中先压入的是3, 之后是2, 最后才是1。 例2, 如果我们要计算(10 + pow(2, 3) * sqrt(4) - 1, 则我们写出的汇编代码如下( 行号是为下文解释代码方便而放上去的, 不是代码的一部分) : 点击(此处)折叠或打开 store 1store 4sqrtstore 3store 2powstore 10addmulsubprint 对这段代码的说明如下: 这段代码稍有点复杂, 但有前一段代码的经验, 我们能够看到, 所有的操作数的先后顺序是从右向左store的, 因此store指令的顺序是固定下来的, 剩下的关键是操作指令应该放在哪里。 操作指令
8、也是有一个规律的, 即: 当前栈顶的数据刚刚好满足某运算时, 则操作指令就放在哪里, 如: store 1的时候没有任何操作的操作数都在栈中。 store 4的时候, 刚刚好sqrt的操作数都在栈中, 则此时加入sqrt指令。 store 3 store 2时, 刚刚好能够计算pow。 store 10之后, 就能够计算加法了, 因此此时加入add指令。 add计算完成之后, 再加上之前已计算完的sqrt指令, 则此时乘法的所有操作数都在栈中了, 则此时加入mul指令。 最后减操作也能够计算了, 则加上sub指令。 所有计算完成之后打印出结果。 在这个例子中, 我所说的”规律”其实就是”后缀表
9、示式”。我们平常写的算术表示式是”中缀”的, 即符号在操作数中间, 如1 + 2, 的 + 在1和2中间, 转为后缀形式即为1 2 +这里因为我对于参数顺序的设计是与”正常”顺序相反的, 因此1 + 2对于这个汇编器来说, 其后缀形式就应该为2 1 +大家是能够按照这个规律, 相对简单的实现这个计算器只要做好词法分析就能够按照后缀表示式的规律直接由tokenList生成汇编代码了但我们的目的是用这个计算器的例子来讲编译, 因此还是按步就班来讲。词法分析词法分析的目的是把一段文本分解成词法元素列表, 例如(10 + pow(2, 3) * sqrt(4) - 1, 做词法分析之后会分解成如下几个
10、词法元素: ( 10+pow(2,3)*sqrt(4)-1这里只是做了一次文本处理在处理之前, 我们手里有的东西就是一串字符组成的字符串, 在处理之后, 我们按照一定的”规则”分解为多个单词。算法是多种多样的, 有创造力的程序员会想出各种办法来处理这个单词分解的问题。在编译原理中, 普遍的方式是用如下一个状态转换图来实现的在图中, ”椭圆形”的是状态, 状态与状态之间有一条有方向的线, 这个线代表从一个状态到另一个状态的路径, 在这条线上面有一个花括号代表从前一个状态到达 后一个状态的输入( 为方便表示, 0-9表示从0到9十个数字, a-z表示从a到z二十六个字母等) , 如从START状态
11、开始, 输入一个数字1, 就会到达 INT状态。图中蓝色的状态是终结状态, 如果从START状态经过若干输入后到达终结状态, 则这些输入的字符可合并为词法的合法单词这里需要额外说明一点: 在对输入进行匹配状态时采用贪婪方式, 即: 尽量输入更多的字符。在识别到一个合法的词法单元之后, 状态回到START继续识别下一个元素, 直到没有新的元素为止。这个状态转换图在编译原理中有一个专有名词称呼它”确定有限状态自动机”, 英文简称DFA。这里”确定”的意思是每一个状态经过一个输入字符能够确定只有 一条路径到达另一个状态, ”有限”的意思是, 状态的个数是有限的。对于一个复杂语言的状态数量是这个状态机
12、的几个数量级的大小。但我们现在的计算器只需要 这几个状态就够了。一般的DFA会由工具读取正则方法描述而生成的, 而不是直接手工构造, 但对我们现在设计的计算器来说, 其DFA非常小, 手工构造是很方便的, 因此就不用工具了。另外, 如果使用工具的话, 我这篇文章也不会使用现有的工具, 而是自己实现一个工具。下面举个例子: 我们有一个表示式12.3 + abc, 下面我来描述一下DFA的运行过程: 定义一个变量s, 来表示当前状态机所在的状态( 初始时s = START) 。 输入第一个字符1, 此时START状态可接受这个输入1, 到达INT状态, 则变量s赋值为INT状态, 此时可看到INT
13、状态的颜色是蓝色表示当前可识别为一个合法的词法元素, 但因为我们的规则是贪婪匹配, 因此我们还要看看是否还能够匹配更多的字符。 输入第二个字符2, 此时INT状态也能够接受这人输入, 并到达INT状态( 转了一个圈又回到原来的状态) , 此时变量s赋值为INT状态。 再输入第三个字符 . , 此时INT状态可接受 . 到达NUMBER状态, 此时变量s赋值为NUMBER状态。 此时我们再向前看一个字符 + , 此时的状态NUMBER无法接受这个字符了, 同时我们可在看到NUMBER状态的颜色是蓝色的, 表示当前状态可识别一个合法的词法元素, 即: 从 START开始到当前我们一共经历的输入为1
14、2.3, 则这个单词做为第一个合法的词法元素。 第一个词法元素识别成功之后, 变量s要回到START状态, 并继续输入 + , 此时从START状态可到达ADD状态, 且ADD状态并不允许接受任何输入, 同时ADD状态的颜色也是蓝色的, 则我们又识别出第二个词法元素 + 。 在识别到第二个词法元素之后, 变更s要回到START状态, 并继续输入a, 此时START状态可由a指向的路径到达ID状态, 此时s赋值为新的状态ID。 现在的情况与识别NUMBER的情况类似, 当前的状态也是终结状态, 但按贪婪匹配的原则还要继续看看是否能够匹配到新的输入。 后面的b和c都在ID一个状态里转圈, 我就不在
15、赘述了, 这样我们就识别到了最后一个词法元素abc了。 用如上过程的方法能够识别任何正确的算术表示式, 但还有几点需要特别说明: 如何识别错误: 当前我见过的语言规范都在描述如何正确的编译一个语言, 但没有一个规范有描述如何报错的, 因此我们当前能做的是只要按正常的走法走不通了, 那就是错了, 就要报错, 并尽可能提供详细的错误信息。 对空白的识别: 我在DFA中并没有画识别空白的部分, 原因是对于计算器程序来说, 空白完全没有用处, 因此我只是在代码中直接忽略这个输入, 以免状态机无法 识别空白, 同时在识别到词法元素之后, 去掉单词前后的空白。对于某些语言来说, 空白是有意义的, 需要做为
16、词法元素识别出来, 不能忽略掉。 对于词法元素的表示: 一般我们会用一个类型Token来表示一个词法元素, Token中有两个属性, 一个用于表示这个Token的类型, 另一个用于表示内 容, 只有数字与标识符才需要使用Token类型的内容属性, 其它的类型因为同一类型只有一种表示的可能, 因此就不需要再把内容保存下来了( 如ADD的内容 一定是+) 。 关于标识符: DFA中的ID状态用于识别标识符, 这里的标识符包括自定义的变量, 也包括函数名。在DFA的设计过程中, 我们能够选择把普通标识符与保留字 做为不同的状态, 也能够用使用同一个状态。我们现在的设计就使用了一个ID状态表示所有的标
17、识符, 而在识别到一个ID之后, 我们在看是否是一个保留字, 这 样在返回Token对象时设置不同的类型。 对于INT和NUMBER: 这个计算器的所有计算都使用的是double类型的数字, 所在虽然我们的词法能够识别到INT, 但我们定义Token的类型 时, 就只定义一个NUMBER类型, INT或NUMBER状态确定的单词都返回NUMBER这个类型的Token对象。 前面的逻辑中有一个贪婪批配的原则, 这个原则在某些语言的词法中会有一些特例不适用, 例如在C+和JAVA中都有一个运算符”, 这个运算符代表右移, 但在C+11 标准中能够写出这样的代码std:vectorstd:vecto
18、r, 在JDK5及以上版本也能够这样写 ListList, 这里如果按贪婪批配就错了。因此必须在词法分析中加入上下文的判断才能决定是按两 个来识别还是按一个来识别上下文的判断是语法分析的部分了, 但对于复杂的词法结构在没有一个统一的词法解析算法能够处理的情 况下就得借助更高级的方法了。 现在剩下的就是写代码了。 如果我把代码贴在这里就太长了, 不太合适。因此下面我只描述一下描述DFA的思路: 思路一: 直接静态代码来描述, 类似这样的方式把状态机的每个路径都描述出来: IF S = START AND c = 1 THEN S = INT . ELSIF ., 这样就能够输入运行了。 思路二:
19、 表格驱动式的, 例如列出下面的表格即可知道哪个状态经过哪个输入之后到达哪个新的状态了下表的左标题是当前的状态, 上标题是在当前状态上的输入, 表格中的内容是经过路径到达的状态。 思路三: 结合前两个思路先写一个代码生成工具, 工具读取”思路二”中表格中的内容, 并生成”思路一”中的静态代码。0-9 . _a-zA-Z + - * / ( ) , START INT POINT ID ADD SUB MUL DIV LBT RBT COMMA INT INT NUMBER POINT NUMBER NUMBER NUMBER ID ID ID ADD SUB MUL DIV LBT RBT C
20、OMMA 下面举一下DFA返回的Token对象的类型: NUM, FUN, VAR, ADD, SUB, MUL, DIV, LBT, RBT, COMMA这里前三个与DFA的状态名不同: NUM代表INT状态和NUMBER状态两个状态识别出的词法元素。 FUN和VAR都是ID状态识别出的元素, 如果这个ID是一个函数名, 则Token为FUN类型, 否则即为VAR类型。 其它类型与DFA的状态是一一对应的。 最后说一下DFA的接口: 假设DFA有一个方法叫做parse, 则这个方法的参数只有一个字符串用于传入表示式即可。如果我们写的DFA是用于解析长文本的( 而不但仅是解析这个简短的算术表示
21、式) , 则能够考虑参数为输入流。 parse方法的返回能够考虑返回一个Token的游标, 游标供语法分析程序调用; 也能够考虑解析完所有的词法元素返回一个Token的列表。因为当前比较通用的语法分析算法只需要向前看一个Token对象, 因此游标就足够了。 因为语法分析程序有可能会把已读出来的Token放回去, 下次再用, 因此游 标可考虑加一个putBack方法也能够考虑不实现这个方法, 而由语法分析程序自己缓存当前用不到的词法元素如果DFA返回的是list就简单一 些, 语法分析器能够前后前后移动offset即可。 DFA返回list的方式实现虽然简单一点, 但对于要解析的数据非常大的情况
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 基于 编译 原理 计算器 设计 实现 样本
限制150内