欢迎回到本周提示的第二部分。 本文部分显示
你是 Tokenizer 类。 让我们分小部分做:
- public class Tokenizer {
- // default size of a tab character
- private static final int TAB= 8;
- // the current line and its backup
- private String line;
- private String wholeLine;
- // current column and tab size
- private int column;
- private int tab= TAB;
- // the reader used for line reading
- private LineNumberReader lnr;
- // last unprocessed token
- private Token token;
这些是 Tokenizer 携带的私有成员变量; 最
其中不需要任何解释。 我们使用 LineNumberReader 来保存
跟踪行号(原文如此)。 以下是构造函数:
-
Tokenizer() { } -
public void initialize(Reader r) { -
lnr= new LineNumberReader(r); -
line= null; -
token= null; -
}
它们也没什么好谈的:只有一个构造函数,它不是
上市; 这意味着只有同一包中的其他类才能创建一个
分词器。 解析器位于同一个包中; 解析器调用
在他们使用包实例化 Tokenizer 之后初始化方法
范围默认构造函数。 初始化方法将 Reader 包装在一个
线号阅读器。 wapper 阅读器用于实际读取来自
的行
输入并更新行号。
以下是解析器调用的两种方法:
-
public Token getToken() throws InterpreterException { -
if (token == null) token= read(); -
return token; -
} -
public void skip() { token= null; }
如果还没有读取令牌,我们读取一个新令牌,否则我们简单地返回
与之前读取的相同令牌。 “skip()”方法向 Tokenizer
发出信号
当前令牌已被处理,因此第一个方法将读取一个新的
下次调用时再次标记。
接下来是一些“getters”:
-
public String getLine() { return wholeLine; } -
public int getLineNumber() { return lnr.getLineNumber(); } -
public int getColumn() { return column; } -
public int getTab() { return tab; } -
public void setTab(int tab) { if (tab > 0) this.tab= tab; }
‘setTab()’ 方法进行了一些完整性检查,而 ‘getters’ 则简单
归还他们应该归还的东西。 让我们来看看这个的私人部分
发生更多有趣事情的班级。 当一个令牌从一个
需要更新“列”值的行。 ‘tab’ 变量包含
制表位大小的值,因此我们必须在更新时考虑它
“列”值:
-
private void addColumn(String str) { -
for (int i= 0, n= str.length(); i < n; i++) -
if (str.charAt(i) != ‘\t’) column++; -
else column= ((column+tab)/tab)*tab; -
}
当字符不是制表符时,我们只需增加“列”值,
否则我们确定下一个制表位值。
以下方法检查是否需要读取下一行:如果
当前行为空或仅包含我们需要读取下一行的空格。 如果
没有什么要读的了,我们将行的备份设置为“<eof>” 就在
case 有些东西想要显示刚刚读过的内容。 仅显示“空”
看起来很傻。 此方法再次重置“列”值,因为
将扫描新行以查找新令牌:
-
private String readLine() throws IOException { -
for (; line == null || line.trim().length() == 0; ) { -
column= 0; -
if ((wholeLine= line= lnr.readLine()) == null) { -
wholeLine= “<eof>”; -
return null; -
} -
} -
return line; -
}
以下方法是 Tokenizer 的核心,即它尝试匹配
针对“匹配器”的当前行。 匹配器是模式的对应物
对象:Pattern 编译正则表达式,而 Matcher 尝试匹配
该模式针对字符串。 字符串是我们当前的“行”。 如果匹配是
发现匹配的前缀从“行”中截断,“列”值为
更新; 最后返回匹配的令牌(如果有)。 这是方法:
-
private String read(Matcher m) { -
String str= null; -
if (m.find()) { -
str= line.substring(0, m.end()); -
line= line.substring(m.end()); -
addColumn(str); -
} -
return str; -
}
Tokenizer类的最后一个方法是最大的方法,但是有点
一种无聊的方法。 它所做的只是尝试使用不同的
正则表达式按本周技巧第一部分中解释的顺序排列。
这是方法:
-
private Token read() throws InterpreterException { -
String str; -
try { -
if (readLine() == null) -
return new Token(“eof”, TokenTable.T_ENDT); -
read(TokenTable.spcePattern.matcher(line)); -
if ((str= read(TokenTable.numbPattern.matcher(line))) != null) -
return new Token(Double.parseDouble(str)); -
if ((str= read(TokenTable.wordPattern.matcher(line))) != null) -
return new Token(str, TokenTable.T_NAME); -
if ((str= read(TokenTable.sym2Pattern.matcher(line))) != null) -
return new Token(str, TokenTable.T_TEXT); -
if ((str= read(TokenTable.sym1Pattern.matcher(line))) != null) -
return new Token(str, TokenTable.T_TEXT); -
return new Token(read(TokenTable.charPattern.matcher(line)), TokenTable.T_CHAR); -
} -
catch (IOException ioe) { -
throw new TokenizerException(ioe.getMessage(), ioe); -
} -
}
正则表达式的模式由 TokenTable 类提供
这个方法所做的只是尝试以固定的顺序匹配它们。 这就是全部
是w.r.t。 我们的小语言的词法分析:据此
模式匹配一个相应的标记被返回。 以前的方法采取
注意不要跳过和忘记未处理的令牌(它只是简单地返回
一次又一次,直到 Tokenizer 被通知它实际上已经被通知了
处理)。 其他方法(见上文)负责读取下一行
必要,并且在运行中更新列值。
您可能已经注意到,
的一部分中抛出了 TokenizerException
标记器代码。 TokenizerException 是一个小类,它扩展了
解释器异常类。 后一类更有趣,看起来
像这样:
-
public class InterpreterException extends Exception { -
private static final long serialVersionUID = 99986468888466836L; -
private String message; -
public InterpreterException(String message) { -
super(message); -
} -
public InterpreterException(String message, Throwable cause) { -
super(message, cause); -
} -
public InterpreterException(Tokenizer tz, String message) { -
super(message); -
process(tz); -
} -
public InterpreterException(Tokenizer tz, String message, Throwable cause) { -
super(message, cause); -
process(tz); -
} -
private void process(Tokenizer tz) { -
StringBuilder sb= new StringBuilder(); -
String nl= System.getProperty(“line.separator”); -
sb.append("["+tz.getLineNumber()+":"+tz.getColumn()+"] "+ -
super.getMessage()+nl); -
sb.append(tz.getLine()+nl); -
for (int i= 1, n= tz.getColumn(); i < n; i++) -
sb.append(’-’); -
sb.append(’^’); -
message= sb.toString(); -
} -
public String getMessage() { -
return (message != null)?message:super.getMessage(); -
} -
}
它看起来像大多数例外,即它有一条消息和一个可能的“原因”
对于异常(所谓的“根”异常)。 有一个有趣的
此 InterpreterException 中的方法:‘process’ 方法。 当一个 Tokenizer
在此对象的构造时传递它能够构造一个不错的
打印出来的错误信息; 错误信息看起来像他的:
-
[line:column] error mesage -
line that has a column with an error -
---------------------^
StringBuilder 用于连接三行的不同部分
信息; “nl”变量包含系统的“行尾”序列
此应用程序正在运行(“\r”和“\n”的任意组合都是可能的)。
并连接正确数量的“-”符号以构成“^”插入符号
出现在当前行下的正确位置。
Tokenizer 使用的 LineNumber 读取器从 0(零)开始计算行数,
人类喜欢从 1(一)开始计数,但对我们来说幸运的是,第一行 (0)
已完全读取,LineNumberReader 将返回下一行
当我们要求时为我们提供编号,因此无需调整值。
请注意,列号也从 0 开始,但该行上至少有一个标记有
已被读取,并且该列指向字符串中的位置
跟随令牌,因此这里也不需要调整。
当没有向此构造函数提供标记器时,仅提供消息
传递给超类将被返回,否则我们精心制作的
消息由 getMessage() 方法返回。
解析器在遇到
时会广泛使用此 InterpreterException
令牌流中的语法错误。他们将 Tokenizer 本身传递给 new
InterpreterException,因此只要有可能,就会出现格式良好的错误消息
当您打印出此 InterpreterException 的消息时显示。
结束语
我在本周的文章中展示并解释了相当多的代码。
尝试理解代码,并在/如果您不理解时毫不犹豫地回复
了解某事。编译器构建是一项艰巨的任务,包含
许多棘手的细节。本周展示了简单的 Token 类;长而
无聊的 Tokenizer 类和 InterpreterException 类。
在下一个技巧中,我将解释如何初始化表类。那是什么时候
结束了,我将提供一些实际代码作为附件,以便您
可以尝试一下。
本文的以下部分解释了解析器,复杂的部分
我们的编译器。解析器与简单的代码生成器密切协作。
生成器生成的代码(多么令人惊讶!)可以提供给最后一个
类:解释器类本身。生成的代码由指令组成;
它们有时很复杂,但大多数时候都是简单而连贯的片段
由我们的解释器激活的代码(Java 指令序列)。