要设计词法分析器,首先要考虑每一种类型的单词的定义,规定怎样的字符串才能构成一个单词。这里最重要的是不能有歧义。某个特定的字符串只能是某种特定类型的单词。举例来讲,要是字符串123h既能被解释为标识符,又能被解释为整型字面量,之后的处理就会相当麻烦。这种单词的定义方式是不可取的。 代码清单3.1 Token.java package stone; public abstract class Token { public static final Token EOF = new Token(-1){}; // end of file public static final String EOL = "\n"; // end of line private int lineNumber; protected Token(int line) { lineNumber = line; } public int getLineNumber() { return lineNumber; } public boolean isIdentifier() { return false; } public boolean isNumber() { return false; } public boolean isString() { return false; } public int getNumber() { throw new StoneException("not number token"); } public String getText() { return ""; } } 代码清单3.2 StoneException.java package stone; import stone.ast.ASTree; public class StoneException extends RuntimeException { public StoneException(String m) { super(m); } public StoneException(String m, ASTree t) { super(m + " " + t.location()); } } Stone语言支持三种类型的单词,即标识符、整型字面量及字符串字面量。 标识符(identifier)指的是变量名、函数名或类名等名称。此外,+或-等运算符及括号等标点符号也属于标识符。标点符号与保留字有时也会被归为另一种类型的单词,不过Stone语言在实现时没有对它们加以区分,都作为标识符处理。 A 保留字是什么? F 指的是那些无法用作变量名或类名的名称。Java语言中的class或public之类的就是保留字。 整型字面量(integer literal)指的是127或2014等字符序列。如果仅使用整型这样的名称,读者可能会把它与程序执行过程中赋值给变量的整数值混同,因此这里使用了整型字面量的名称,用于指代整数值的字符序列。 例如,Java语言支持0x1f这样的16进制数表示。这种4个字符组成的字符串也是整型字面量。用一个整数值来表示的话,即为31。 字符串字面量(string literal)是一串用于表示字符串的字符序列。与Java等语言一样,被双引号(")括起来的字符序列就是一个字符串字面量。双引号及其中的字符构成了一个字符串字面量,表示的是某一字符串类型的值,该值即为双引号内包含的字符序列。例如,字符串字面量"Java"表示的是字符串值Java。 双引号之间可以使用n、"与\这三种类型的转义字符。它们分别表示换行符、双引号和反斜杠。因此,尽管"xn"这一字符串字面量含有5个字符,但它表示的是一个由2个字符组成的字符串值,其中第一个字符是x,第二个是换行符。 C 如果能用one、two、three之类的字符串作为整型字面量来表示数字,会是一件挺有意思的事吧?表示的值当然就是整数1、2、3了。 本书在定义单词时使用了正则表达式。这样一来,就能够借助正则表达式库简单地实现词法分析器。简言之,正则表达式(regular expression)是一种用于字符串模式匹配的书写记号。 正则表达式中能使用一些特殊的记号(元字符)。在不同的正则表达式实现方式中,允许使用的元字符有所不同。表3.1列出的记号在大多数情况下都能使用。例如,.*.java指的是以.java结束的任意长度的字符串模式。.*.由两部分组成,.*表示由任意字符组成的任意长度的字符串模式,.表示与句点字符相匹配的字符串模式。(java|javax)..*则表示由java.或javax.起始的任意长度的字符串模式。 C 正则表达式内涵丰富,在此不多赘述,我们先继续介绍。 表3.1 正则表达式的元字符 .(句点) 与任意字符匹配 [0-9] 与0至9中的某个数字匹配 [^0-9] 与0至9这些数字之外的某一个字符匹配 pat* 模式pat至少重复出现0次 pat+ 模式pat至少重复出现1次 pat? 模式pat出现0次或1次 pat1|pat2 与模式pat1或模式pat2匹配 () 将括号内视为一个完整的模式 c 与单个字符c(元字符*或.等)匹配 接下来,我们借助正则表达式来定义Stone语言的单词。正则表达式的写法遵循Java正则表达式库java.util.regex的规定。 首先来定义整型字面量,它比较简单。 [0-9]+ 从0到9中取出一个或以上的数字,就能构成一个整型字面量。 然后定义标识符。 [A-Z_a-z][A-Z_a-z0-9]* 这个正则表达式表示至少需要一个字母、数字或下划线_,且首字符不能是数字,这种表示方式涵盖了常用的名称。根据该定义,对整型字面量和标识符的判断不存在二义性。 Stone语言的标识符包括各类符号,因此下面才是真正完整的正则表达式。各个模式之间需要通过|连接。即, [A-Z_a-z][A-Z_a-z0-9]*|==|<=|>=|&&||||p{Punct} 最后的p{Punct}表示与任意一个符号字符匹配。模式||将会匹配||。由于|是正则表达式的元字符,因此在使用时必须在前面添加来转义。==、>=、<=、&&与||由两个字符组成,Stone语言将它们视为一个整体,即含有2个字符的符号。除此之外的字符组合将被拆开处理,例如,+-将被视作+与-两个不同的符号。 最后需要定义的是字符串字面量。由于不得不处理各种转义字符,字符串字面量的定义稍微有些复杂。 "(\"|\\|\n|[^"])*" 首先,从整体上来看,这是一个"(pat)*"形式的模式,即双引号内是一个与pat重复出现至少0次的结果匹配的字符串。其中,模式pat与"、\、n或除"之外任意一个字符匹配。反斜杠具有特殊的含义,因此在正则表达式中需要通过\的方式转义,使整个模式变得复杂。 F 老师,这样的正则表达式就能完全对应所有的字符串字面量了吗? C 也许吧……应该都没问题吧……至少准备的一些测试用例,都通过了。