问候,
欢迎回到本周的第二部分。 本文部分显示
您是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;
这些是令牌生成器携带的私有成员变量;
最
他们不需要任何解释。 我们使用LineNumberReader
跟踪行号(原文如此)。 这是构造函数:
Tokenizer() { }
public void initialize(Reader r) {
lnr= new LineNumberReader(r);
line= null;
token= null;
}
他们都没什么可谈论的:只有一个构造函数,而没有
上市; 这意味着只有同一包中的其他类才能创建一个
分词器。 解析器位于同一包中; 解析器将
在使用该程序包实例化了Tokenizer之后,使用initialize方法
作用域默认构造函数。 initialize方法将Reader包裹在一个
LineNumberReader。 wapper阅读器用于实际读取
输入并更新行号。
这是解析器调用的两种方法:
public Token getToken() throws InterpreterException {
if (token == null) token= read();
return token;
}
public void skip() { token= null; }
如果没有已经读取的令牌,我们将读取一个新的令牌,否则我们将简单地返回
与之前读取的令牌相同。 'skip()'方法向令牌生成器发出信号
当前令牌已被处理,因此第一种方法将读取新令牌
在下一次调用时再次令牌。
接下来是一些“获取器”:
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;
}
当字符不是制表符时,我们只需增加“列”值,
否则,我们将确定下一个Tabstop值。
以下方法检查是否需要读取下一行:
当前行为null或仅包含空格,我们需要阅读下一行。 如果
没什么要阅读的了,我们只是将行的备份设置为“ <eof>”
案例想显示刚刚阅读的内容。 只显示“ null”
看起来很傻。 此方法再次重置“列”值,因为
将在新行中扫描新令牌:
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的核心,即它尝试匹配
当前行与“匹配器”相对。 匹配器是模式的对应物
对象:匹配器尝试匹配时,模式会编译正则表达式
针对字符串的模式。 字符串是我们当前的“行”。 如果匹配
发现匹配的前缀从'line'中被切掉,并且'column'的值是
更新; 最后返回匹配的令牌(如果有)。 方法如下:
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类提供
而所有此方法所做的就是尝试以固定顺序匹配它们。 这就是全部
对我们的小语言进行词法分析是:
模式具有匹配项,则返回相应的令牌。 以前的方法
注意不要跳过和忘记未处理的令牌(只需将其返回)
一次又一次地直到令牌生成器被通知它实际上已经被
处理)。 其他方法(请参见上文)会在以下情况下读取下一行:
必要并随时更新列值。
您可能已经注意到TokenizerException抛出了
分词器代码。 TokenizerException是一个小类,它扩展了
InterpreterException类。 后一类更有趣,看起来
像这样:
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'方法。 当分词器
在此对象的构造时传递,它能够构造一个不错的对象
打印出错误消息; 错误消息看起来像他:
[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时会广泛使用
令牌流中的语法错误。 他们将令牌生成器本身传递给新
InterpreterException,因此在任何可能的情况下,格式正确的错误消息都是
在打印出此InterpreterException消息时显示。
结束语我在本周的文章中展示并解释了很多代码。
尝试理解代码,如果/如果您不这样做,请立即回复
了解一些。 编译器的构建是一项艰巨的任务,其中包含
许多棘手的细节。 本周展示了简单的令牌类; 长和
无聊的Tokenizer类和InterpreterException类。
在下一个技巧中,我将解释如何初始化表类。 那是什么时候
一遍又一遍,我将提供一些实际的代码作为附件,以便您
可以玩和尝试所有这一切。
本文的以下部分介绍了解析器,复杂的部分
我们的编译器。 解析器与简单的代码生成器紧密协作。
生成器生成代码(多么令人惊讶!),可以将其馈送到最后
class:解释器类本身。 生成的代码由指令组成;
它们有时很复杂,但大多数时候都是简单而连贯的作品
Intepreter激活的代码(Java指令序列)。
下周见
亲切的问候,
乔斯
From: https://bytes.com/topic/java/insights/739821-compilers-2b-tokenizers