词法分析(Lexical Analysis) 是编译的第一阶段。词法分析器的主要任务是读入源程序的输入字符、将他们组成词素,生成并输出一个词法单元序列,每个词法单元对应一个词素。这个词法单元序列被输出到语法分析器进行语法分析。
知识储备
词法单元:由一个词法单元名和一个可选的属性值组成。词法单元名是一个表示某种词法单位的抽象符号,比如一个特定的关键字,或者代表一个标识符的输入字符序列。词法单元名字是由语法分析器处理的输入符号。
模式:描述了一个词法单元的词素可能具有的形式。
词素: 源程序中的一个字符序列,它和某个词法单元的模式匹配,并被词法分析器识别为该词法单元的一个实例。
例如:count = count + increment;
可以表示成 <id, "count"> <=> <"id", "count"> <+> <id, "increment"> <;>
id 表示 identifier, 即标识符.
每一个 <> 部分都是一个词法单元。 标识符的模式用正则表示如下:
letter_ -> A | B | ... | Z | a | b | ... | z | _
digit -> 0 | 1 | ... | 9
id -> letter_(letter_|digit)*
因此count, increment都是标识符的实例,即词素.
类的结构如下:
类 Tag
类 Tag 定义了各个词法单元对应的常量:
public class Tag { public static final int NUM = 256, ID = 257, TRUE = 258, FALSE = 259, AND = 260, EQ = 261, GE = 263, LE = 267, OR = 271, REAL = 272, NE = 273; }
类 Token
类 Token 有一个 tag 字段, 用于做出语法分析决定。
类 Num
类 Num 表示的是整数常量, 例如 29, 1
当在输入流中出现一个数位序列时,词法分析器将向语法分析器传送一个词法单元,该词法单元包含终结符号 Num 及 根据这些数位计算得到的整数属性值。如果把词法单元写成用 <> 括起来的元组,那么 29, 1 分别被表示成 <num, 29>, <num, 1>.
Num 继承 Token, 增加了一个用于存放整数值的字段 value。
类 Real
与 Num 类似,用来表示实数。
类 Word
类 Word 继承 Token, 增加了一个字段 lexeme, 用于保存关键字和标识符的词素。
程序需要预读一个字符,所以使用一个变量peek来保存下一个输入字符, 同时规定: 要么保存了当前词法单元词素后的那个字符,要么保存空白符。
识别关键字和标识符
大多数程序设计语言使用 for、do、if 这样的固定字符串作为标点符号,或者用于表示某种构造。这些字符串成为关键字 (keyword)。
关键字通常也满足标识符的组成规则,因此我们需要某种机制来确定一个词素什么时候组成一个关键字,什么时候组成一个标识符。如果关键字作为保留字,也就是说,如果他们不能被用作标识符,这个问题相对容易解决。此时,只有一个字符串不是关键字时,它才能组成一个标识符。
解决方案:
用一个表来保存字符串
Hashtable<String, Word> words = new Hashtable<String, Word>();
在初始化时在字符串表中加入保留的字符串以及他们对应的词法单元。
reserve( new Word(Tag.TRUE, "true") );
reserve( new Word(Tag.FALSE, "false"));
当词法分析器读到一个可以组成标识符的字符串或词素时,它首先检查这个字符串表中是否有这个词素。如是,它就返回表中的词法单元,否则返回带有终结符号 id 的词法单元。
为了方便输出,为每一个类 重写 toString 方法
具体步骤:
首先跳过空白字符.
for (;; readch()) { if (peek == ' ' || peek == ' ') continue; else if (peek == ' ') line++; else break; }
接下来要处理数字。如果 peek 是数字,证明当前要处理的为数字常量,如果含有 小数点. 证明是实数,否则为整数.
if (Character.isDigit(peek)) { int v = 0; do { v = v * 10 + Character.digit(peek, 10); readch(); } while (Character.isDigit(peek)); if (peek != '.') return new Num(v); float x = v; float d = 10; for (;;) { readch(); if (!Character.isDigit(peek)) break; x += Character.digit(peek, 10) / d; d = d*10; } return new Real(x); }
处理标识符麻烦些,因为标识符的第一个字母不能是数字,所以如果第一个是字母就开始处理标识符。每次读取一个字符或数字(第一个不能是数字),组成一个尽量长的字符串。如果该字符串在表中存在,则直接返回表中对应的 Word,如果不存在,新建一个 Word 为标识符类型, 并放到表中。
if (Character.isLetter(peek)) { StringBuilder b = new StringBuilder(); do { b.append(peek); readch(); } while (Character.isLetterOrDigit(peek)); String s = b.toString(); Word w = words.get(s); if (w != null) return w; w = new Word(s, Tag.ID); words.put(w.lexeme, w); return w; }
效果图: