编译器 - 2B:标记器

问候,

欢迎回到本周提示的第二部分。 本文部分显示
你是 Tokenizer 类。 让我们分小部分做:

Expand|Select|Wrap|Line Numbers
  1. public class Tokenizer {
  2. // default size of a tab character
  3. private static final int TAB= 8;
  4. // the current line and its backup
  5. private String line;
  6. private String wholeLine;
  7. // current column and tab size
  8. private int column;
  9. private int tab= TAB;
  10. // the reader used for line reading
  11. private LineNumberReader lnr;
  12. // last unprocessed token
  13. private Token token;

这些是 Tokenizer 携带的私有成员变量; 最

其中不需要任何解释。 我们使用 LineNumberReader 来保存

跟踪行号(原文如此)。 以下是构造函数:

Expand|Select|Wrap|Line Numbers

  1. Tokenizer() { }

  2. public void initialize(Reader r) {

  3. lnr= new LineNumberReader(r);

  4. line= null;

  5. token= null;

  6. }

它们也没什么好谈的:只有一个构造函数,它不是

上市; 这意味着只有同一包中的其他类才能创建一个

分词器。 解析器位于同一个包中; 解析器调用

在他们使用包实例化 Tokenizer 之后初始化方法

范围默认构造函数。 初始化方法将 Reader 包装在一个

线号阅读器。 wapper 阅读器用于实际读取来自
的行
输入并更新行号。


以下是解析器调用的两种方法:

Expand|Select|Wrap|Line Numbers

  1. public Token getToken() throws InterpreterException {

  2. if (token == null) token= read();

  3. return token;

  4. }

  5. public void skip() { token= null; }

如果还没有读取令牌,我们读取一个新令牌,否则我们简单地返回

与之前读取的相同令牌。 “skip()”方法向 Tokenizer
发出信号
当前令牌已被处理,因此第一个方法将读取一个新的

下次调用时再次标记。


接下来是一些“getters”:

Expand|Select|Wrap|Line Numbers

  1. public String getLine() { return wholeLine; }

  2. public int getLineNumber() { return lnr.getLineNumber(); }

  3. public int getColumn() { return column; }

  4. public int getTab() { return tab; }

  5. public void setTab(int tab) { if (tab > 0) this.tab= tab; }

‘setTab()’ 方法进行了一些完整性检查,而 ‘getters’ 则简单

归还他们应该归还的东西。 让我们来看看这个的私人部分

发生更多有趣事情的班级。 当一个令牌从一个

需要更新“列”值的行。 ‘tab’ 变量包含

制表位大小的值,因此我们必须在更新时考虑它

“列”值:

Expand|Select|Wrap|Line Numbers

  1. private void addColumn(String str) {

  2. for (int i= 0, n= str.length(); i < n; i++)

  3. if (str.charAt(i) != ‘\t’) column++;

  4. else column= ((column+tab)/tab)*tab;

  5. }

当字符不是制表符时,我们只需增加“列”值,

否则我们确定下一个制表位值。


以下方法检查是否需要读取下一行:如果

当前行为空或仅包含我们需要读取下一行的空格。 如果

没有什么要读的了,我们将行的备份设置为“<eof>” 就在

case 有些东西想要显示刚刚读过的内容。 仅显示“空”

看起来很傻。 此方法再次重置“列”值,因为

将扫描新行以查找新令牌:

Expand|Select|Wrap|Line Numbers

  1. private String readLine() throws IOException {

  2. for (; line == null || line.trim().length() == 0; ) {

  3. column= 0;

  4. if ((wholeLine= line= lnr.readLine()) == null) {

  5. wholeLine= “<eof>”;

  6. return null;

  7. }

  8. }

  9. return line;

  10. }

以下方法是 Tokenizer 的核心,即它尝试匹配

针对“匹配器”的当前行。 匹配器是模式的对应物

对象:Pattern 编译正则表达式,而 Matcher 尝试匹配

该模式针对字符串。 字符串是我们当前的“行”。 如果匹配是

发现匹配的前缀从“行”中截断,“列”值为

更新; 最后返回匹配的令牌(如果有)。 这是方法:

Expand|Select|Wrap|Line Numbers

  1. private String read(Matcher m) {

  2. String str= null;

  3. if (m.find()) {

  4. str= line.substring(0, m.end());

  5. line= line.substring(m.end());

  6. addColumn(str);

  7. }

  8. return str;

  9. }

Tokenizer类的最后一个方法是最大的方法,但是有点

一种无聊的方法。 它所做的只是尝试使用不同的

正则表达式按本周技巧第一部分中解释的顺序排列。

这是方法:

Expand|Select|Wrap|Line Numbers

  1. private Token read() throws InterpreterException {

  2. String str;

  3. try {

  4. if (readLine() == null)

  5. return new Token(“eof”, TokenTable.T_ENDT);

  6. read(TokenTable.spcePattern.matcher(line));

  7. if ((str= read(TokenTable.numbPattern.matcher(line))) != null)

  8. return new Token(Double.parseDouble(str));

  9. if ((str= read(TokenTable.wordPattern.matcher(line))) != null)

  10. return new Token(str, TokenTable.T_NAME);

  11. if ((str= read(TokenTable.sym2Pattern.matcher(line))) != null)

  12. return new Token(str, TokenTable.T_TEXT);

  13. if ((str= read(TokenTable.sym1Pattern.matcher(line))) != null)

  14. return new Token(str, TokenTable.T_TEXT);

  15. return new Token(read(TokenTable.charPattern.matcher(line)), TokenTable.T_CHAR);

  16. }

  17. catch (IOException ioe) {

  18. throw new TokenizerException(ioe.getMessage(), ioe);

  19. }

  20. }

正则表达式的模式由 TokenTable 类提供

这个方法所做的只是尝试以固定的顺序匹配它们。 这就是全部

是w.r.t。 我们的小语言的词法分析:据此

模式匹配一个相应的标记被返回。 以前的方法采取

注意不要跳过和忘记未处理的令牌(它只是简单地返回

一次又一次,直到 Tokenizer 被通知它实际上已经被通知了

处理)。 其他方法(见上文)负责读取下一行

必要,并且在运行中更新列值。


您可能已经注意到,
的一部分中抛出了 TokenizerException
标记器代码。 TokenizerException 是一个小类,它扩展了

解释器异常类。 后一类更有趣,看起来

像这样:

Expand|Select|Wrap|Line Numbers

  1. public class InterpreterException extends Exception {

  2. private static final long serialVersionUID = 99986468888466836L;

  3. private String message;

  4. public InterpreterException(String message) {

  5. super(message);

  6. }

  7. public InterpreterException(String message, Throwable cause) {

  8. super(message, cause);

  9. }

  10. public InterpreterException(Tokenizer tz, String message) {

  11. super(message);

  12. process(tz);

  13. }

  14. public InterpreterException(Tokenizer tz, String message, Throwable cause) {

  15. super(message, cause);

  16. process(tz);

  17. }

  18. private void process(Tokenizer tz) {

  19. StringBuilder sb= new StringBuilder();

  20. String nl= System.getProperty(“line.separator”);

  21. sb.append("["+tz.getLineNumber()+":"+tz.getColumn()+"] "+

  22. super.getMessage()+nl);

  23. sb.append(tz.getLine()+nl);

  24. for (int i= 1, n= tz.getColumn(); i < n; i++)

  25. sb.append(’-’);

  26. sb.append(’^’);

  27. message= sb.toString();

  28. }

  29. public String getMessage() {

  30. return (message != null)?message:super.getMessage();

  31. }

  32. }

它看起来像大多数例外,即它有一条消息和一个可能的“原因”

对于异常(所谓的“根”异常)。 有一个有趣的

此 InterpreterException 中的方法:‘process’ 方法。 当一个 Tokenizer

在此对象的构造时传递它能够构造一个不错的

打印出来的错误信息; 错误信息看起来像他的:

Expand|Select|Wrap|Line Numbers

  1. [line:column] error mesage

  2. line that has a column with an error

  3. ---------------------^

StringBuilder 用于连接三行的不同部分

信息; “nl”变量包含系统的“行尾”序列

此应用程序正在运行(“\r”和“\n”的任意组合都是可能的)。

并连接正确数量的“-”符号以构成“^”插入符号

出现在当前行下的正确位置。


Tokenizer 使用的 LineNumber 读取器从 0(零)开始计算行数,

人类喜欢从 1(一)开始计数,但对我们来说幸运的是,第一行 (0)

已完全读取,LineNumberReader 将返回下一行

当我们要求时为我们提供编号,因此无需调整值。

请注意,列号也从 0 开始,但该行上至少有一个标记有

已被读取,并且该列指向字符串中的位置

跟随令牌,因此这里也不需要调整。


当没有向此构造函数提供标记器时,仅提供消息

传递给超类将被返回,否则我们精心制作的

消息由 getMessage() 方法返回。


解析器在遇到
时会广泛使用此 InterpreterException
令牌流中的语法错误。他们将 Tokenizer 本身传递给 new

InterpreterException,因此只要有可能,就会出现格式良好的错误消息

当您打印出此 InterpreterException 的消息时显示。

结束语


我在本周的文章中展示并解释了相当多的代码。

尝试理解代码,并在/如果您不理解时毫不犹豫地回复

了解某事。编译器构建是一项艰巨的任务,包含

许多棘手的细节。本周展示了简单的 Token 类;长而

无聊的 Tokenizer 类和 InterpreterException 类。


在下一个技巧中,我将解释如何初始化表类。那是什么时候

结束了,我将提供一些实际代码作为附件,以便您

可以尝试一下。


本文的以下部分解释了解析器,复杂的部分

我们的编译器。解析器与简单的代码生成器密切协作。

生成器生成的代码(多么令人惊讶!)可以提供给最后一个

类:解释器类本身。生成的代码由指令组成;

它们有时很复杂,但大多数时候都是简单而连贯的片段

由我们的解释器激活的代码(Java 指令序列)。