(基于Java)编写编译器和解释器-第3A章:基于Antlr构造词法分析器(连载)

时间:2022-12-27 17:08:38

在上一章(第三章)中我们用纯手工的方式构造了一个Pascal的扫描器(也称词法分析器)。细心的读者会想到,大部分语言的词法构造过程都差不多,都有变量ID,字符串,整数,浮点数,关键字,特殊符号等(如比较符,赋值,索引括号)等等。事实上在编译技术发展到今天,手写词法分析器基本很少了,因为编程语言的词不同于自然语言,很容易通过机械的手段实现。有很多工具可以生成词法分析器,比如Flex,JavaCC等。不过在本书中,不管词法还是语法还是代码生成,我将使用Antlr(http://www.antlr.org)这个大名鼎鼎的工具来完成后续的代码自动化工作。关于Antlr的基础我就不介绍了,网上有很多的教程。这儿我推荐两个:《使用 Antlr 开发领域语言 - 开发一个完整的应用》,《The Definitive ANTLR Reference

==>> 本章中文版源代码下载:svn co http://wci.googlecode.com/svn/branches/ch3_antlr/ 源代码使用了UTF-8编码,下载到本地请修改!

Antlr的Lexer和Parser还有AST属于自动化产物,自成体系。编译器/解释器的非自动化部分都是通过"hook"手段灌进Antlr语法中。如果使用Antlr,将会抛弃原有框架但是复用原有的代码和逻辑,再者项目也从纯粹的Java项目变成了Maven项目。

 

1 创建一个Antlr文件Pascal.g,首先是抬头,我们使用了不同于PascalToken的另外一个Token类型PascalAntlrToken,因为对于Antlr来说,只能继承Antlr的CommonToken,而PascalToken继承自Token。

   1: grammar Pascal;
   2: options{
   3:   TokenLabelType=PascalAntlrToken;
   4:   output=AST;
   5: }
   6: tokens{
   7:   NUMBER_REAL;
   8: }

这里为简单使用了混合语法,即Lexer和Parser的语法放在一起。因为Antlr认为词法的前向过程(LL)和语法的前向过程基本一样,这两个只是单位不一样,一个是character,一个是token。输出为AST,现在暂且不用管它。调用maven命令之后,此语法文件会生成两个Java文件:PascalLexer和PascalParser。

 

2 因为Pascal大小写不敏感,必须让Antlr支持忽略大小写:

   1: fragment A:('a'|'A');
   2: fragment B:('b'|'B');
   3: fragment C:('c'|'C');
   4: ...
   5: ...
   6: fragment X:('x'|'X');
   7: fragment Y:('y'|'Y');
   8: fragment Z:('z'|'Z');

3 建立Pascal关键字列表:

   1: AND              : A N D    ;
   2: ARRAY            : A R R A Y    ;
   3: BEGIN            : B E G I N    ;
   4: CASE             : C A S E    ;
   5: CHAR         : C H A R    ;
   6: CHR         : C H R    ;
   7: CONST            : C O N S T    ;
   8: DIV              : D I V    ;
   9: DO               : D O        ;
  10: DOWNTO           : D O W N T O    ;
  11: ELSE             : E L S E    ;
  12: END              : E N  D    ;
  13: FILE             : F I L E    ;
  14: FOR              : F O R    ;
  15: FUNCTION         : F U N C T I O N;
  16: GOTO             : G O T O    ;
  17: IF               : I F        ;
  18: IN               : I N        ;
  19: INTEGER          : I N T E G E R;
  20: LABEL            : L A B E L    ;
  21: MOD              : M O D    ;
  22: NIL              : N I L    ;
  23: NOT              : N O T    ;
  24: OF               : O F        ;
  25: OR               : O R        ;
  26: PACKED           : P A C K E D    ;
  27: PROCEDURE        : P R O C E D U R E;
  28: PROGRAM          : P R O G R A M;
  29: REAL             : R E A L    ;
  30: RECORD           : R E C O R D    ;
  31: REPEAT           : R E P E A T    ;
  32: SET              : S E T    ;
  33: THEN             : T H E N    ;
  34: TO               : T O        ;
  35: TYPE             : T Y P E    ;
  36: UNTIL            : U N T I L    ;
  37: VAR              : V A R    ;
  38: WHILE            : W H I L E    ;
  39: WITH             : W I T H    ;
4 建立特殊符号列表:
   1: PLUS            : '+'   ;
   2: MINUS           : '-'   ;
   3: STAR            : '*'   ;
   4: SLASH           : '/'   ;
   5: ASSIGN          : ':='  ;
   6: COMMA           : ','   ;
   7: SEMI            : ';'   ;
   8: COLON           : ':'   ;
   9: EQUAL           : '='   ;
  10: NOT_EQUAL       : '<>'  ;
  11: LT              : '<'   ;
  12: LE              : '<='  ;
  13: GE              : '>='  ;
  14: GT              : '>'   ;
  15: LPAREN          : '('   ;
  16: RPAREN          : ')'   ;
  17: LBRACK          : '['   ;
  18: LBRACK2         : '(.'  ;
  19: RBRACK          : ']'   ;
  20: RBRACK2         : '.)'  ;
  21: POINTER         : '^'   ;
  22: AT              : '@'   ;
  23: DOT             : '.' ;
  24: DOTDOT          
  25:     :     '..' ;
  26: LCURLY          : '{' ;
  27: RCURLY          : '}' ;

5 上一章讲述了三种Token,单词Token,数字Token和字符串Token。对于单词Token来说,关键字已经在第三条列出,剩下的只是标识符即ID了。

标识符 Token:

ID  :    ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
表示由一个字母开头,后续是组成ID的字母和数字,还有下划线。
 
字符串Token:
STRING
: '\'' ('\'\'' | ~('\''))* '\'' ;
这里考虑到了两个连续单引号的问题。
 
数字Token:
   1: NUMBER
   2:     :    ('0'..'9')+ 
   3:         (    (    {(input.LA(2)!='.')&&(input.LA(2)!=')')}?        
   4:                 '.' {$type = NUMBER_REAL;}    
   5:                 ('0'..'9')+ (EXPONENT)?
   6:             )?
   7:         |    EXPONENT {$type= NUMBER_REAL;}    
   8:         )
   9:     ;
  10: fragment
  11: EXPONENT
  12:     :    ('e') ('+'|'-')? ('0'..'9')+
  13:     ;
这个数字Token词法稍微复杂了点,但与PascalNumberToken里的extract方法基本一样,同样有整数部分,小数点,小数部分和指数部分。在第4行和第7行红色部分我们修改了Token的Type为预定义的NUMBER_REAL。NUMBER默认表示整数(Integer)。碰到小数点和(或)指数就表示实数(Real)了
 
6 空白和注释的处理
空白和注释在词法分析里面是要被“处理”掉的,也就是这个只有阅读上的意义,基本对语法和语义没影响。
空白:
   1: WS      : ( ' '
   2:         |    '\t'
   3:         |    '\f'
   4:         |    (    '\r\n' 
   5:             |    '\r'   
   6:             |    '\n'   
   7:             )
   8:             { 
   9:             }
  10:         )
  11:         { $channel=HIDDEN; }
  12:     ;
注意11行有一个$channel=HIDDEN的Antlr Action,这就是要被Antlr忽略掉的意思。
 
注释:
   1: COMMENT
   2:         :  '{'
   3:            (
   4:             :   '\r' '\n'     
   5:             |    '\r'            
   6:             |    '\n'
   7:             |   ~('}' | '\n' | '\r')
   8:             )*
   9:            '}'
  10:         {$channel=HIDDEN;}
  11:     ;
在2行有一个标明注释的大括号,第9行有一个结束的反大括号。注释里面包含了各种操作系统的换行符,比如 \r(苹果MAC),\n(类Unix),和\r\n(Win系列),所以这种注释是可以跨行的。
 
上一章中的Token常量token比如字符串和数字,我个人认为token只表示字面上的意思,即这样写对不对,至于逻辑(比如数字大小)是否正确,那应该是运行时的行为,还有好多编译优化都会讲常量放入 常量池中,而不是每个都计算。这里虽然用了带value域的PascalAntlrToken,但我们不准备在词法阶段就将常量词的值计算出来,使用了一个占位方法ValueComputer.computerToken()来与前章意义统一。
 
7 错误恢复目前暂且不适用,到时候联合语法错误恢复一起。
 
8 运行源代码中的ShowToken类看输出效果,下面是从hello.pas文件中抽取的token。
--hello.pas----------------

1行:PROGRAM[0] ID[8] LPAREN[14] ID[15] RPAREN[21] SEMI[22]
5行:VAR[0]
6行:ID[4] COLON[6] INTEGER[8] SEMI[15]
8行:BEGIN[0]
9行:FOR[4] ID[8] ASSIGN[10] NUMBER[13] TO[15] NUMBER[18] DO[21] BEGIN[24]
10行:ID[8] LPAREN[15] STRING[16] RPAREN[31] SEMI[32]
11行:END[4] SEMI[7]
12行:END[0] DOT[11]
每个token标识了它的类型,括号里面的数字表示它在当前行中的其实位置。
 
9 在Pascal.g文件中有@lex::members的类容,它表示覆盖lexer中token的输出。从默认的CommonToken改成继承自CommonTOken的PascalAntlrToken。