编译器-4:簿记

问候,

本周的编译器文章全部涉及簿记; 无聊,我承认,但是

我们的令牌生成器和解析器需要它。 两周前,我展示了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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值