问候,
本周的编译器文章全部涉及簿记; 无聊,我承认,但是
我们的令牌生成器和解析器需要它。 两周前,我展示了Tokenizer
班级代码。 它使用TokenTable,其中包含Tokenizer所需的数据。
TokenTable包含数据,但是该表如何初始化? 一世
可以对表中的所有数据进行硬编码,但我没有。 我希望能够
播放和更改该数据,而无需更改代码本身中的单个字母。
我还希望该表进行初始化。 属性文件是良好的外部
为此目的的资源。 属性文件只是键值对的列表
以等号“ =”分隔。
这是“ tokens.properties”文件:
space = ^\\s*
number = ^\\d+(\\.\\d*)?([eE][+-]?\\d+)?
word = ^[A-Za-z_]\\w*
symbol2 = ^(==|<=|>=|!=|^=|\\+=|-=|\\*=|/=|\\+\\+|--)
symbol1 = ^[:,=!<>+*/(){}^-]
char = ^\\S
我们已经在描述Tokenizer的文章部分中看到了该内容。
类。
资源资源我们需要一种从某处读取文件并产生一个
属性对象。 如果该文件之前已被读取过,我们不想读取它
一次,但是我们只是想一次又一次地产生相同的Properties对象。
为此,我们需要一个缓存:
private static Map<String, Properties> cache=
new HashMap<String, Properties>();
给定一个键的字符串(例如“令牌”),我们要缓存一个Properties对象
例如上面作为地图的value元素列出的那一项。
下面的方法在给定String键的情况下为我们找到一个Properties对象:
private static final fs= System.getProperty("file.separator");
protected static synchronized Properties getProperties(String name) {
Properties properties= cache.get(name);
if (properties != null) return properties;
try {
InputStream is= Resources.class.getResourceAsStream(
"resources"+fs+name+".properties");
properties= new Properties();
properties.load(is);
is.close();
cache.put(name, properties);
return properties;
}
catch (IOException ioe) {
return null;
}
}
给定名称(例如“令牌”)后,此方法要么查找已缓存的
从高速缓存中的Properties对象,或者它搜索名为
“资源/ <名称> .properties”。 在我们的示例中,它搜索名为
“资源/tokens.properties”。 如果一切正常,它将创建一个新的Properties对象,
在对象中加载文件的内容,将对象添加到缓存中,
最后返回Properties对象。 如果失败,则此方法返回
“空”值。
请注意,我们并未对文件分隔符(斜杠或反斜杠)进行硬编码。
在西方语言中),因为它可以是任何东西; 我们不知道的任何字符
提前,我们也不想知道它:让Java处理它。
下次需要属性文件时,它要么已经被缓存,要么将被缓存。
重新加载并放入缓存。 这个小场景要注意
每个不同的Properties对象最多只能加载一次。
这里发生了一件有趣的小事情:此方法加载属性
相对于定义此方法的类存储位置的对象。
假设类本身存储在“ / usr / jos / java / compiler”中。 然后
资源将在目录“ / usr / jos / java / compiler / resource”中搜索。
我们将此方法和缓存本身保留在名为“ Resources”的类中:
abstract class Resources {
protected Resources() { }
private static Map<String, Properties> cache=
new HashMap<String, Properties>();
protected static synchronized Properties getProperties(String name) {
...
}
}
我用一个受保护的默认构造函数使它成为一个抽象类
因为我想从中扩展表类。 请注意,
提供属性对象是同步的,以防万一有两个单独的线程
想要同时获取相同的Properties对象,即我们不想
同时装载两次或两次以上。
还要注意,该方法是静态方法,就像缓存对象本身一样。
我们不需要资源对象仅加载属性对象。 班级
是一个实用程序类; 它所做的只是提供功能。
令牌表让我们继续这些无聊的东西吧; 这是TokenTable类:
abstract class TokenTable extends Resources {
private TokenTable() { }
// different types of tokens:
static final int T_ENDT= -1; // end of stream reached
static final int T_CHAR= 0; // an ordinary character
static final int T_NUMB= 1; // a number
static final int T_TEXT= 2; // a recognized token
static final int T_NAME= 3; // an identifier
// regexps for different token types
static final Pattern spcePattern= Pattern.compile(pat("space"));
static final Pattern numbPattern= Pattern.compile(pat("number"));
static final Pattern wordPattern= Pattern.compile(pat("word"));
static final Pattern sym2Pattern= Pattern.compile(pat("symbol2"));
static final Pattern sym1Pattern= Pattern.compile(pat("symbol1"));
static final Pattern charPattern= Pattern.compile(pat("char"));
private static String pat(String pattern) {
return getProperties("tokens").getProperty(pattern);
}
}
我们从Resourses(抽象)类扩展而来,并创建了TokenTable类
也是一个抽象类。 此类的默认构造函数是private,所以没有一个
可以实例化此类中的对象。 不需要,即只有一张桌子
实例化的任何Tokenizer都需要它。
此类的第一部分定义了几个int常量,我们已经看到了
在本文的前面部分中介绍了Tokenizer类。
最后一部分初始化令牌生成器所需的模式。 我们利用
通过获取属性值在基类资源中的缓存工具
对于所有六个值,一次又一次地从同一Property对象开始。
再次资源解析器还需要了解一些或多或少的固定问题:
1)内置函数的名称;
2)“特殊”内置函数的名称;
3)一元运算符令牌;
4)二进制运算符令牌;
5)赋值运算符令牌;
6)所有操作员和职能的统一。
最重要的是,对于每个运算符或函数名称,无论是否为“特殊”,
类名与名称/令牌关联。 类名称由
代码生成器,但是为了简单起见,我们将该信息存储在ParserTable中。
运算符或函数的多样性是该运算符或函数所需的参数数量
运算符或函数,例如'sin'函数的奇数为1,
'*'运算符是2,依此类推。
所有属性文件都具有相同的结构:其中的每一行都看起来像这样:
<说明> = <arity> <名称> <class>
<description>是函数或运算符的单个单词唯一描述。
<arity>是一个数字,即* ahem *,即函数或运算符的arity。
<name>是函数或运算符本身以及<class>的名称或标记
是实现函数或运算符的类的完全限定名称。
例如,这是“ functions.properties”文件的内容的一部分:
sine = 1 sin compiler.instruction.SinInstruction
cosine = 1 cos compiler.instruction.CosInstruction
tangent = 1 tan compiler.instruction.TanInstruction
exponential = 1 exp compiler.instruction.ExpInstruction
logarithm = 1 log compiler.instruction.LogInstruction
abs = 1 abs compiler.instruction.AbsInstruction
minimum = 2 min compiler.instruction.MinInstruction
maximum = 2 max compiler.instruction.MaxInstruction
第一列是描述性的单词,它构成了
属性对象。 字符串值由三列组成:arity,
函数或运算符的名称或标记,最后一列是完整的
实现函数或运算符的类的限定名称。
Resources类为此使用了另一个表:GeneratorTable,它定义了
地图也:
protected static Map<String, String> classes= new HashMap<String, String>();
此Map存储<name或token>-<class name>形式的元组,并且已构建
由资源类即时生成。
接下来,我们向Resources类添加一个方法来处理Properties对象,例如
这个:
protected static Set<String> getResource(String name) {
Properties properties= getProperties(name);
Set<String> tokens;
if (properties == null) return null;
tokens= new HashSet<String>();
for(Map.Entry property: properties.entrySet()) {
String[] entries= ((String)property.getValue()).
trim().split("\\s+");
int marked= entries[1].indexOf('@');
ParserTable.arity.put(entries[1], Integer.valueOf(entries[0]));
if (marked < 0)
tokens.add(entries[1]);
else
tokens.add(entries[1].substring(0, marked));
GeneratorTable.classes.put(entries[1], entries[2]);
}
return tokens;
}
这是一个很大的方法。
让我们看看它的作用:它返回一组名称
或令牌; 集合由表第二栏中的所有名称组成
(往上看)。 它建立集合如下:
它读取每个值(“ =”字符后的字符串)并拆分
值放入一个字符串数组“ entries”。 entry [0]是Arity,entries [1]是
函数或运算符的名称,entries [2]是类名称。 我们检查
名称中包含一个“ @”符号,并将每个元素放在适当的位置
地图。 更新“类”图,更新并返回“令牌”集
在此方法的末尾。 “ @”符号从“令牌”的名称中删除
已设置,但已用作“类”映射的键的一部分。
请注意,此方法直接引用其子类的成员:“ arity”
地图。 它为.properties文件中的每一行更新该映射。 纯粹主义者会
争辩说我们正在打破Liskov换人原则(请参阅另一篇文章
进行解释),但此基类及其子类TokenTable
和ParserTable不是真正的类; 他们只是一堆静态方法
为方便起见,在抽象类中进行了分组。 我本可以丢弃那些方法
以及Maps属于一个大型实用程序类,但我没有。 我想分组
类中的功能需要一些明确说明。
我们仍然没有完成这个Resources类。 一个ParserTable需要存储
有关“特殊”功能和保留字的信息。 ParserTable的需求
一组字符串。 这是我们的Resources类中的另一个实用程序方法:
protected static Set<String> getSet(String name) {
Properties properties= getProperties(name);
Set<String> set= new HashSet<String>();
if (properties != null)
for (Object elem : properties.keySet())
set.add((String)elem);
return set;
}
此方法使用Properties对象并将该对象的键存储在Set中。
这是列出我们小语言保留字的文件; 我命名了
'reserved.properties':
function = a declaration or definition of a user function
listfunc = a declaration or definition of a list user function
给定此文件的上述方法的返回值是一个包含两个元素的Set
其中:“功能”和“ listfunc”,均为保留字。
解析器表最后,经过无聊的簿记之后,下面是ParserTable类:
abstract public class ParserTable extends Resources {
// total number of binary operator precedences
private static final int PREC= 6;
private ParserTable() { }
public static final int T_FUNC= TokenTable.T_NAME+1;
public static final int T_QUOT= TokenTable.T_NAME+2;
public static final int T_USER= TokenTable.T_NAME+3;
public static final int T_WORD= TokenTable.T_NAME+4;
public static final Map<String, Integer> arity=
new HashMap<String, Integer>();
public static final Set<String> funcs= getResource("functions");
public static final Set<String> quots= getResource("quotes");
public static final Set<String> rword= getSet("reserved");
public static final Set<String> lfncs= getSet("listfuncs");
// all unary, binary operators and assignments
static final Set<String> unaops= getResource("unaops");
static final Set<String> pstops= getResource("postops");
static final Set<String> asgns= getResource("assigns");
static final List<Set<String>> binops= new ArrayList<Set<String>>(PREC);
static {
for (int i= 0; i < PREC; i++)
binops.add(getResource("binops"+i));
}
}
与TokenTable相似(参见上文),此类仅是几个
集,地图和地图列表的集合。 此类的第一部分定义了几个
其余类使用方法时解析器需要的常量
在其超类(资源)中填充其“集合”和“地图”。
加载其他地图和集合时,会立即填充“ arity”地图。
映射列表代表二进制运算符; 此列表的第0个元素
存储具有最低优先级(':')的二进制运算符,依此类推。
有关所有二进制运算符及其说明的描述,请参见前一篇文章。
优先。
实现我们的Parser时,将讨论ParserTable的集合。
一小段时间:
-Set funcs存储内置普通函数名称的名称;
-Set quots存储特殊内置函数名称的名称;
-Set rword存储保留字;
-Set lfncs存储将列表取为的内置函数的名称
他们的参数。
发电机表还有一个要讨论的表:GeneratorTable。 该表存储
运算符或功能的名称,并将它们与它们的完整名称相关联
合格的班级名称。 此表的数据由参考资料选择
.properties文件中的类,并存储在此表的Map中。
此类中的第二个Map缓存指令:
private static Map<String, Instruction> instructions=
new HashMap<String, Instruction>();
给定指令名称,可以将真实指令与其关联。
这种缓存机制实现了Flyweight模式,例如,只有一个
无论正弦函数执行多少次,都需要“ sin”指令实例化
用于表达式中。 该Map照顾了该模式所需的机制。
GeneratorTable类为此实现了两个方法:
protected static Instruction getInstruction(String name) {
try {
return (Instruction)Class.forName(classes.get(name)).
newInstance();
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}
给定运算符的名称或名称,此方法实例化一个新指令
功能。 当失败(不应该失败)时,它将打印出完整的堆栈跟踪信息
我们*为什么*失败了。 .properties文件很可能包含错误的数据,这些数据
应该是固定的。
第二种方法负责缓存指令:
protected static Instruction cacheInstruction(String name) {
Instruction instruction= instructions.get(name);
if (instruction == null)
instructions.put(name, instruction= getInstruction(name));
return instruction;
}
如果该指令之前已缓存,则返回该指令,否则返回前一个方法
调用,在给定存储了Map的情况下为我们实例化一条新指令
所需指令的全限定类名。
两种方法都受到保护,因为外部世界甚至都不知道
类存在。 它仅由解析器和/或解释器使用。
结束语上述四个类没有什么有趣的。 资源
类是TokenTable,ParserTable和GeneratorTable类的基类
为了方便。
这四个类是简单的实用性类,不多也不少,不需要
完全没有任何实例。 基类Resources完成所有工作:它加载内容
从文件到Properties对象,并填充子类的映射。
这四个类的主要目的是我不想对每个类进行硬编码
单个名称或令牌,或代码其余部分中的任何名称。 我希望能够
更改简单的.properties文件并更改语言,而无需更改
很多Java代码。
这是编译器文章中无聊的章节; 它必须为
完整性原因; 这也是很重要的一章; 我展示了表格如何填充
自己由JVM加载时。 数据来自.properties文件,该文件
易于编辑。 这些.properties文件不是“用户功能”; 内容
这些.properties文件必须正确才能进行整个编译
系统正常运行。
我确实希望我不会对Compilers文章的这一部分过分担心,
但这就是生命:编译器的写作是20%的灵感和80%的汗水。 这个
无聊的文章部分照顾了大部分(即使不是全部)汗水。
这种沉迷将重新出现在本文的续篇中。 我确定我们会
下周见面,我们终于可以深入解析器了。
亲切的问候,
乔斯
From: https://bytes.com/topic/java/insights/658385-compilers-4-bookkeeping