(基于Java)编写编译器和解释器-第3章:扫描-第二部分(连载)
>>>续 第一部分
从这个小例子从可知如下的一些关键点:
- 扫描器扫描并跳过Token之间的空白符(比如空格)。当此操作结束时,当前字符肯定不是空白符。
- 非空字符判定下个要提取的Token类型,且此字符成为这个token的首字符。
- 扫描器不停地通过扫描和拷贝源字符创建Token,直到字符不能成为这个Token的一部分。
- 提取token将吞噬掉组成此token的所有源字符。因此,提取过后,当前字符是token尾字符后的下一个字符。
一个Pascal扫描器
上一章在frontend.pascal包中已完成了PascalScanner类的前期工作。现在扩展方法extractToken()并加一个新的方法skipWhilteSpace()。见清单3-8
清单3-8:PascalScanner类的extract()和skipWhiteSpace方法 详细参见本章源代码,这里不再显示。
设计笔记
语法图很好理解:顺着箭头指引的方向。分叉路径代表选择:字母可以是A或B或C等等。其它绕回路径表示连续:在单词token的首字母后,有连续的0或多个字母/数字。
圆框表示字面文本,比如字母A或数字0。(在第5章开头,你也会用圆框表示关键字如AND、OR以及AND的字面文本。)矩形框是另一图形的引用。例如,单词Token引用表示字母和数字的语法图。较正式的说法是,原型框表示末端符(没法再分),而矩形框表示非末端符(可以再次细分)
单词Token
PascalScanner类中的extractToken()方法(参见清单3-8)在当前字符是字母时创建一个新的Pascal 单词Token。
if (Character.isLetter(currentChar)) {
new PascalWordToken(source);
3: }
frontend.pascal.tokens包中的PascalWordToken类是PascalToken的子类。清单3-11 展示了它的extract()方法。详细参见本章源代码,这里不再显示。
后续在中间码,符号表章节会处理这个值)。
清单3-4 展示了PascalWordToken的一些实例。(这里省略,请运行源代码 java -classpath classes Pascal compile scannertest.txt 并留意从9到12行的输出)
源代码第11行包含了四个关键字BEGIN用以检验PascalWordToken的大小写敏感性。毫无疑问,begins的token类型一定是标识符(begins 不等于 begin)。
字符串token
图3-3 展示了Pascal 字符串token的语法图
Pascal字符串以单引号'开始,以单引号结束。单引号之间是组成字符串token值的0到多个连续字符。在字符序列中,两个相邻的单引号表示字符串中的一个单引号(此时的单引号是文本字符,不是表示Pascal字符串的起止字符)。
类PascalScanner在当前字符是单引号时,创建一个PascalToken对象。
new PascalStringToken(source);
}
清单3-12 展示了PascalToken子类PascalStringToken中的extract方法。详细参见本章源代码,这里不再显示。
我们认为起止的单引号不算字符串内容,只算字符串标识),extract将调用peekChar()去检测这个字符(单引号)是字符串结束符还是相邻两个单引号的前一个。如果是后一种情况,此方法将噬掉这对相邻单引号,并附加一个单引号在字符串内容上。如果文件意外终止,方法将token的类型设为ERROR并且把值置成PascalErrorCode枚举值UNEXPECTED_EOF。
清单3-4 展示了PascalStringToken的一些输出例子:(这里省略,请运行源代码 java -classpath classes Pascal compile scannertest.txt 并留意从13到21行的输出)
PascalStringToken很好的处理了空字符串(行016)和相邻单引号问题(行017)。它把每一个字符串中的换行符替换成为空格(行018,019,020)。
特殊符号Token。
如果当前字符是PascalTokenType.SPECIAL_SYMBOLS哈希表某一项的值,类PascalScanner(见清单3-8) 会创建一个PascalSpecialSymbolToken对象。
if (PascalTokenType.SPECIAL_SYMBOLS.containsKey(Character.toString(currentChar))){
new PascalSpecialSymbolToken(source);
3: }
清单3-13 展示了PascalToken子类PascalSpecialSymbolToken的extract方法,详细参见本章源代码,这里不再显示。此extract()方法尝试提取一个特殊符号token并吞噬当前token字符。Pascal特殊字符token由1或2个字符构成。如果成功提取,extract方法通过SPECIAL_SYMBOLS哈希表去设置token的类型为恰当的枚举值。然如果有错,此方法设置token的类型为ERROR并设其值为PascalErrorCode的枚举值INVALID_CHARACTER。
清单3-14 显示了PascalSpecialSymbolToken的一些例子。(这里省略,请运行源代码 java -classpath classes Pascal compile scannertest.txt 并留意从22行到25行的输出)
数字Token
图3-4 展示了Pascal数字Token的语法图。
一个无符号整数是一个数字序列。一个Pascal整数token是一个无符号整数。一个Pascal实数token以一个整数部分开始,接着是以下一种:
I:一个小数点,紧接着一个无符号整数(小数部分)。
II:一个E或e,之前可能有+或-,紧接着是一个无符号整数(指数部分)。
III:一个小数,紧接着一个指数部分。
清单3-15 展示了PascalToken子类PascalNumberToken中的extract方法。详细参见本章源代码,这里不再显示。
字串类型的域wholeDigits,fractionDigits和exponentDigits分别表示小数点之前的序列,小数点之后的序列和E或e之后的序列。如图3-4展示的语法图那样,域fractionDigits和exponentDigits可能为null。此方法(PascalNumberToken)方法初始设定token的类型为INTEGER,如有小数部分或指数部分,则改类型为REAL。它调用unsignedIntegerDigits方法在三部分(整数,小数和指数)提取数字,这可以保证此每个部分至少有一个数字。(如果有小数部分,指数部分,才能保证响应的部分至少有1个数字,但记住这两个部分是可选的)。
清单 3-16 展示了PascalNumberToken类的unsignedIntegerDigits方法。详细参见本章源代码,这里不再显示。
如果extractNumber方法在整数部分后遇到一个'.'字符,它不能马上假定这是一个小数点,因为它可能是 .. Token(1..10表示范围)的第一个字符。调用peekChar()方法前探一个字符就是用来判断这种情况。在数字的所有部分都被提取之后,它视类型为INTEGER或REAL调用computeIntegerValue或computeFloatValue 方法计算数字的值。见清单3-17和清单3-18
清单3-17:类PascalNumberToken中的computeIntegerValue方法 详细参见本章源代码,这里不再显示。
computeIntegerValue方法计算一串数字的整数值。它通过确定值折回(二进制的位数有限,如果超过表示的最大值,可能将当前的位数置为0,比如 1111 加上 一个1 就变成 0000,这个算折回,从头开始了)且小于前一个值来检查溢出。如果有溢出,方法设token的类型为ERROR且设置token的值为PascalErrorCode的枚举值RANGE_INTEGER。
清单3-17:类PascalNumberToken中的computeFloatValue方法 详细参见本章源代码,这里不再显示。
computerFloatValue()方法计算包含整数部分、小数部分、指数部分和指数符号的数字串的float值。它调用computerIntegerValue计算指数的整数值,如果指数符号为'-',取整数值的反数。如果有小数,方法会调整值即减去小数部分的长度。最后,如果调整后的指数值加上指数部分的值超过MAX_EXPONENT,则数字越界了(超过表示范围),方法设置token的类型为ERROR且设置token的值为PascalErrorCode的枚举值RANGE_REAL。(MAX_EXPONENT的值依赖于底层机器架构和附带的浮点数实现标准,最小指数不比是最大指数的反数,比如最大指数为64,则最小指数不一定是-64)
computerFloatValue()方法将整数和小数部分和在一起计算,并将值乘上调用以调整后的指数为参数的Math.pow方法得到的值,得到数字token的最终值。
这儿有一个关于extractNumber方法怎样计算数字token的例子。有token字串为31415.926e-4,extractNumber方法将以下值传递给computerFloatValue方法:
exponentSign: '-'
方法computerFloatValue调用computerIntegerValue计算exponentValue(参考代码中变量)的值,接着它调整两次,一次是因为exponentSign是'-'将值(exponentValue)取反,接着(还是exponentValue)减去fractionDigits字串的长度3。
exponentValue: 4 as computed by computeIntegerValue()
exponentValue: -4 after negation since exponentSign is '-'
exponentValue: -7 after subtracting fractionDigits.length()
最后,computerFloatValue方法计算wholeDigits + fractionDigits联合字串的值,得到31415926.0,接着乘以Math.pow(10, exponentValue),得到此数字token的最终值3.1415926。
清单3-5 显示了PascalNumberToken的一些例子。:(这里省略,请运行源代码 java -classpath classes Pascal compile scannertest.txt 并留意从26到31行的输出)。
设计笔记 |
扫描器是编译器器/解释器前端的一个重要组件,这章写了很多的代码。但你遵循策略设计模式实现PascalToken的子类,这样每个Token子类都知道怎么从源程序中提取token字串。每个子类的实现方法只有一个职责,都是高聚合。比如PascalNumberToken方法只负责提取数字token。高聚合类耦合少,易于维护。如果你决定改进扫描器的实数计算方式,减少舍入(类似于四舍五入的舍入)错误,你只需要修改PascalNumberToken类。假设你的设计决定是让PascalToken自己提取所有Pascal Token类型(而不是通过子类),那么这个类(PascalToken)的聚合性极低。很难在一种token的提取方式发生改变的时候不影响另一个。 |
以上是 (基于Java)编写编译器和解释器-第3章:扫描-第二部分(连载) 的全部内容, 来源链接: utcz.com/z/390723.html