BST

BST Community Official Blog
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

>>>续 第一部分

从这个小例子从可知如下的一些关键点:

  • 扫描器扫描并跳过Token之间的空白符(比如空格)。当此操作结束时,当前字符肯定不是空白符。
  • 非空字符判定下个要提取的Token类型,且此字符成为这个token的首字符。
  • 扫描器不停地通过扫描和拷贝源字符创建Token,直到字符不能成为这个Token的一部分。
  • 提取token将吞噬掉组成此token的所有源字符。因此,提取过后,当前字符是token尾字符后的下一个字符。

一个Pascal扫描器

上一章在frontend.pascal包中已完成了PascalScanner类的前期工作。现在扩展方法extractToken()并加一个新的方法skipWhilteSpace()。见清单3-8

清单3-8:PascalScanner类的extract()和skipWhiteSpace方法 详细参见本章源代码,这里不再显示。

extractToken()方法与前面例子描述的基本一样。首先,它扫描并跳过所有空白符,那么当前字符(不是空白符)是下一个token的首字符,此token类型通过当前字符判断。这个方法创建和返回任一PascalWordToken,PascalNumberToken,PascalStringToken或者PascalSpecialSymbolToken对象。因此在extractToken()方法读取每一Token的首字符之后,新创建的Token对象读取和拷贝此Token的剩余字符。
extractToken()方法在源文件结束时同样创建和返回一个EofToken对象,或者一个PascalErrorToken对象假如碰到一个不能成为任何Pascal Token首字符的无效字符。
skipWhiteSpace方法跳过Token间的所有空格,这由Java的Character.isWhiteSpace来判断。它同样忽略Pascal注释内容,因此每个注释相当于一个空格(好怪)。

Pascal Tokens

在上一章中,你定义了语言无关的TokeType占位接口。现在定义一个枚举类型PascalTokenType,它的枚举值表示所有Pascal Tokens。清单3-9 展示了必要的TokenType实现。

清单3-9 :PascalTokeType枚举类型 详细参见本章源代码,这里不再显示。

在清单3-1 中,类PascalParserTD的parse()方法中使用了枚举值ERROR。在清单3-2中,主类Pascal的内部类ParserMessageListener在处理TOKEN消息时使用了枚举值STRING。

静态集合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到多个字母或数字构成。

image

设计笔记

语法图很好理解:顺着箭头指引的方向。分叉路径代表选择:字母可以是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()方法。详细参见本章源代码,这里不再显示。

extract()实现了如图3-2语法图所示的Pascal单词Token语法规则。在提取第首字母后,它吞噬任意紧随其后的字母和数字以便构造单词token的文本串。当这结束后,currentChar的值将不是字母或数字,因此这个字符将是token的后一位字符。然后extract()方法判断单词是否一个关键字。如果单词文本串在TokenTypes.RESERVED_WORDS集合(见清单3-9) 中出现,它必定是一个关键字。因为Pascal关键字不区分大小写,测试集合所属关系通过小写完成(都以小写来比较),token的类型是相应的PascalTokenType枚举值。如果token类型是标识符,现在暂且将其值置为null(后续在中间码,符号表章节会处理这个值)。

清单3-4 展示了PascalWordToken的一些实例。(这里省略,请运行源代码 java -classpath classes Pascal compile scannertest.txt 并留意从9到12行的输出

源代码第11行包含了四个关键字BEGIN用以检验PascalWordToken的大小写敏感性。毫无疑问,begins的token类型一定是标识符(begins 不等于 begin)。

字符串token

图3-3 展示了Pascal 字符串token的语法图

imagePascal字符串以单引号'开始,以单引号结束。单引号之间是组成字符串token值的0到多个连续字符。在字符序列中,两个相邻的单引号表示字符串中的一个单引号(此时的单引号是文本字符,不是表示Pascal字符串的起止字符)。
类PascalScanner在当前字符是单引号时,创建一个PascalToken对象。
else if (currentChar == '\'') {
token = new PascalStringToken(source);
}
 
 
 
 
清单3-12 展示了PascalToken子类PascalStringToken中的extract方法。详细参见本章源代码,这里不再显示。
方法extract()吞噬字符串字符。它替换所有空白符(比如行结束符,再比如\t等) 为单个空格。它必须检测意外终止的情况(如果有开始的单引号,没有结束的单引号,字符串token将会把起始单引号之后的所有字符都认为是字符串的一部分,直到文件结束)。在字符串中如果碰到一个单引号(我们认为起止的单引号不算字符串内容,只算字符串标识),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对象。
   1: }else if (PascalTokenType.SPECIAL_SYMBOLS.containsKey(Character.toString(currentChar))){
   2:     token = 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的语法图。
image
一个无符号整数是一个数字序列。一个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方法:

wholeDigits:       "31415"
fractionDigits: "926"
exponentDigits: "4"
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的提取方式发生改变的时候不影响另一个。