>>>续 第一部分
从这个小例子从可知如下的一些关键点:
- 扫描器扫描并跳过Token之间的空白符(比如空格)。当此操作结束时,当前字符肯定不是空白符。
- 非空字符判定下个要提取的Token类型,且此字符成为这个token的首字符。
- 扫描器不停地通过扫描和拷贝源字符创建Token,直到字符不能成为这个Token的一部分。
- 提取token将吞噬掉组成此token的所有源字符。因此,提取过后,当前字符是token尾字符后的下一个字符。
一个Pascal扫描器
上一章在frontend.pascal包中已完成了PascalScanner类的前期工作。现在扩展方法extractToken()并加一个新的方法skipWhilteSpace()。见清单3-8
清单3-8:PascalScanner类的extract()和skipWhiteSpace方法 详细参见本章源代码,这里不再显示。
Pascal Tokens
在上一章中,你定义了语言无关的TokeType占位接口。现在定义一个枚举类型PascalTokenType,它的枚举值表示所有Pascal Tokens。清单3-9 展示了必要的TokenType实现。
清单3-9 :PascalTokeType枚举类型 详细参见本章源代码,这里不再显示。
静态集合RESERVED_WORDS包含Pascal关键字的文本串。其后在类PascalWordToken中使用这个集合判断一个单词是一个关键字还是一个标识符。因为每个关键字的枚举值在关键字后标识,值的文本即是关键字的字面串。比如枚举值BEGIN的值是字符串"BEGIN"。因为Pascal单词大小写不敏感,通过将它们(枚举值文本)变成小写去归化单词。
静态哈希表SPECIAL_SYMBOLS包含每一个Pascal特别符号项。项的键是特殊符号的文本字串,如枚举值的构造器一样,项的值是枚举值本身。比如,某项的键是":=",它的值就是COLON_EQUALS。类PascalScanner(见清单3-8)使用哈希表判断是否去创建和返回一个PascalSpecialSymbolToken对象。类PascalSpecialSymbolToken也会使用这个哈希表。
参考源代码中PascalToken类,它将是所有Pascal Token子类的基类,它扩展了语言无关的框架类Token。尽管当前它没有添加任何域或方法,它也给后续开发带来了方便。(也就是一个Mark类,TAG类)
语法图(syntax diagrams)
我们将开发frontend.pascal.tokens包中的剩余PascalToken子类,以便提取各种Pascal Token。但首先,你需要一个好的关于语言元素的语法规范,这有几种常见的方法,但Pascal相对简单的语法可以很好的使用语法图即语言语法规则的图形化表示。(最后一章以文本方式表示一种语言的语法,也就是EBNF范式)
图3-2 展示了三张图。第一张明确字母可以是大写A到Z和小写a-z中的任何一个字符。第二张图明确一位数字可以是0到9的任意字符。而第三张明确单词token是由单个字母加后续0到多个字母或数字构成。
设计笔记 |
语法图很好理解:顺着箭头指引的方向。分叉路径代表选择:字母可以是A或B或C等等。其它绕回路径表示连续:在单词token的首字母后,有连续的0或多个字母/数字。 圆框表示字面文本,比如字母A或数字0。(在第5章开头,你也会用圆框表示关键字如AND、OR以及AND的字面文本。)矩形框是另一图形的引用。例如,单词Token引用表示字母和数字的语法图。较正式的说法是,原型框表示末端符(没法再分),而矩形框表示非末端符(可以再次细分) |
单词Token
PascalScanner类中的extractToken()方法(参见清单3-8)在当前字符是字母时创建一个新的Pascal 单词Token。
1: else if (Character.isLetter(currentChar)) {
2: token = new PascalWordToken(source);
3: }
frontend.pascal.tokens包中的PascalWordToken类是PascalToken的子类。清单3-11 展示了它的extract()方法。详细参见本章源代码,这里不再显示。
清单3-4 展示了PascalWordToken的一些实例。(这里省略,请运行源代码 java -classpath classes Pascal compile scannertest.txt 并留意从9到12行的输出)
字符串token
图3-3 展示了Pascal 字符串token的语法图
else if (currentChar == '\'') {
token = new PascalStringToken(source);
}
特殊符号Token。
1: }else if (PascalTokenType.SPECIAL_SYMBOLS.containsKey(Character.toString(currentChar))){
2: token = new PascalSpecialSymbolToken(source);
3: }
数字Token
清单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方法:
wholeDigits: "31415"
fractionDigits: "926"
exponentDigits: "4"
exponentSign: '-'
exponentValue: 4 as computed by computeIntegerValue()
exponentValue: -4 after negation since exponentSign is '-'
exponentValue: -7 after subtracting fractionDigits.length()
设计笔记 |
扫描器是编译器器/解释器前端的一个重要组件,这章写了很多的代码。但你遵循策略设计模式实现PascalToken的子类,这样每个Token子类都知道怎么从源程序中提取token字串。每个子类的实现方法只有一个职责,都是高聚合。比如PascalNumberToken方法只负责提取数字token。高聚合类耦合少,易于维护。如果你决定改进扫描器的实数计算方式,减少舍入(类似于四舍五入的舍入)错误,你只需要修改PascalNumberToken类。假设你的设计决定是让PascalToken自己提取所有Pascal Token类型(而不是通过子类),那么这个类(PascalToken)的聚合性极低。很难在一种token的提取方式发生改变的时候不影响另一个。 |