编译器-6A:口译员

问候,

上周,我们完成了编译器的重要部分。 我们可以解析和

编译源文本。 源文本包含表达式和函数的列表

定义。 编译均成功,我们从以下位置获取了Code对象

我们的解析器或InterpreterException被抛出,指示导致

错误。

严格来说,这篇编译器文章已经完成:所有编译细节

本文前面的部分已经讨论过了。 令牌生成器

将字符的输入序列转换为令牌的输入序列。

解析器检查语法正确性,并指示生成器

相应地在Code对象中生成指令。

并不是严格地说:我想看到结果; 拥有代码没意思

包含指令序列的对象,我无能为力

用它。 现在,让我们跳过生成器,这是一个愚蠢的东西

解析器的正确说明,仅此而已。

让我们专注于能够解释指令的解释器

在Code对象中,以便我们可以看到结果。

口译员

我们的解释器将是一个简单的解释器,即它所做的就是执行一个

指令清单,由Code对象抽象出来(请参阅

上一篇文章的一部分)。

基本上它所做的就是:


public Stack execute(Code code) throws InterpreterException { 
    for (Instruction instruction : code) {
        preExecute(code, instruction);
        instruction.execute(this);
        preExecute(code, instruction);
    } 
    return stack;
} 
解释器所做的全部工作都是遍历Code对象(List <Instruction>)

并在指令上调用“执行”方法。 口译员没有

甚至知道*什么*被执行,它所知道的只是一条指令很简单

接口:


public interface Instruction extends Serializable { 
    public void execute(Interpreter interpreter) 
                throws InterpreterException; 
    public void initialize(String token, int arity);
} 
我们暂时忽略第二种方法(代码生成器使用了它)。 的

第一个方法声明承诺一条指令可以执行只要

我们向它传递一个Interpreter对象。 可能会抛出InterpreterException

发生任何错误。

这符合解释器的要求:它在每个解释器上都调用“ execute”方法

代码列表中的指令并将其本身作为参数传递。 注意两个

pre和postExecute方法。 我不知道该怎么办,但我确定

他们会派上用场。 我已经将它们都实现为两个空方法

解释器类:


protected void preExecute(Code code, Instruction instruction) 
                throws InterpreterException { } 
protected void postExecute(Code code, Instruction instruction) 
                throws InterpreterException { } 
它们可以用于Interpreter类的其他对象。

如果发现它们没有用,我只需将它们再次踢出去。

Interpreter类的“执行”方法返回“堆栈”。 尽你所能

已经注意到或将在短时间内注意到,我们的解析器基本上会生成

后缀代码:例如,当表达式“ 1 + 2”被编译时,前两个常量

为数字1和2生成指令,然后是一条指令

为“ +”运算符生成。 象征性地,生成的代码看起来像“ 1 2 +”

这是原始中缀“ 1 + 2”表达式的后缀形式。

后缀代码几乎是针对函数或操作数在其上的堆栈的乞讨

操作员被推并弹出。 后缀表达式'1 2 +'读为:

1)将1压入堆栈

2)将2推入堆栈

3)弹出两个操作数,将它们相加并将结果推回堆栈中。

但是,谁或什么将那些常量1和2推入堆栈呢? 口译员

对此,它本身过于简单愚蠢; 答案是常数

指令可以做到; 它将常量推入传递给它的堆栈上

由生成器创建的时间。 检查ExpressionParser并尝试

找出所有发生的地方。

我们的解释器是基于堆栈的机器。 Stack对象是

解释器对象本身:


protected Stack stack= new Stack(); 
public Stack stack() { return stack; } 
Stack对象本身如下所示:

package compiler; 
import java.util.ArrayList; 
import compiler.value.DblValue;
import compiler.value.Value; 
public class Stack extends ArrayList<Value> { 
    private static final long serialVersionUID = -2663291338225528177L; 
    public void add(double value) { add(new DblValue(value)); }
} 
堆栈是一个List <Value>,它被当作真实堆栈使用。 班级

实现了一些方便的方法,可以在堆栈上压入“ double”

将其包装在DblValue中之后。

下一段说明了价值的含义。

我们所有的说明和口译员都使用价值观。 我们没有很多

我们的小语言有不同的数据类型(还可以吗?),它可以处理双精度和

列表。 它们都是价值观。 这是一个封装了两者的小场景

类型; 作为示例,我也包含了一个未被使用的字符串值

目前。 我们从一个抽象类Value开始:


public abstract class Value implements Serializable { 
    public static final Value NUL= new DblValue(0.0);
    public static final Value ONE= new DblValue(1.0); 
这个抽象类为我们定义了一些常量(请阅读:说明):

零和一。 请注意,该类是可序列化的,因此我们可以将Value写入

流或从流中读取(如果需要)。

以下是抽象Value类中的一些方法:


public double getDbl() throws InterpreterException { 
    throw new InterpreterException("not a double: "+toString());
} 
public String getStr() throws InterpreterException { 
    throw new InterpreterException("not a string: "+toString());
} 
public List<Value> getLst() throws InterpreterException { 
    throw new InterpreterException("not a list: "+toString());
} 
这些是“ cast”方法,都在此方法中抛出InterpreterException

抽象基类。 DblValue类及其Compadres应该重写

适用的方法,以便调用者可以轻松找到所需的Java类型。

在适用的情况下,还将覆盖以下方法:


public boolean isDbl() { return false; }
public boolean isStr() { return false; }
public boolean isLst() { return false; } 
其他所有类型(目前只有double和list)都应该扩展

这个基类。 这是DblValue类:


public class DblValue extends Value { 
    private static final long serialVersionUID = -5067625407505210072L; 
    private double value; 
    public DblValue(double value) { this.value= value; } 
    public double getDbl() { return value; } 
    public boolean isDbl() { return true; } 
    public boolean equals(Object obj) { 
        if (obj == null || !(obj instanceof DblValue)) return false; 
        return value == ((DblValue)obj).value;
    } 
    public int hashCode() { return (int)value; } 
    public String toString() { return ""+value; }
} 
这个小类是一个不错的数据类:可以比较相等性和

它封装了原始的double类型。 正是我们想要的。

尽管我们不使用它; 看一下其他类型之一:StrValue类:


public class StrValue extends Value { 
    private static final long serialVersionUID = -5355082990666947635L; 
    private String value; 
    public StrValue(String value) { this.value= value; } 
    public String getStr() { return value; } 
    public boolean isStr() { return true; } 
    public boolean equals(Object obj) { 
        if (obj == null || !(obj instanceof StrValue)) return false; 
        return value == ((StrValue)obj).value;
    } 
    public int hashCode() { return value.hashCode(); }  
    public String toString() { return value; }
} 
它与DblClass几乎相同,但是它封装了String类。 的

LstValue类是列表的小语言概念。 这里是:


public class LstValue extends Value { 
    private static final long serialVersionUID = 8665468454551339099L; 
    private List<Value> value; 
    public LstValue(List<Value> value) { this.value= value; } 
    public List<Value> getLst() { return value; } 
    public boolean isLst() { return true; } 
    public boolean isSimple() { 
        for (Value elem : value)
            if (!elem.isDbl()) return false;
        return true;
    } 
    public int size() { return value.size(); } 
    public boolean equals(Object obj) { 
        if (obj == null || !(obj instanceof LstValue)) return false; 
        return value.equals(((LstValue)obj).value);
    } 
    public int hashCode() { return value.hashCode(); } 
    public String toString() { return value.toString(); }
} 
同样,该类与其兄弟姐妹几乎相同(请检查),但是

有一种额外的方法; “ isSimple”方法。 这是我们需要的

说明,它检查列表中的所有元素是否均为双精度。

观察其toString()方法委托给值成员变量

最了解如何生成封装的String表示形式

List <Value>对象。

所有三个类(DblValue,StrValue和LstValue)都覆盖equals()方法

和hashCode()方法:将它们粘贴在Maps,Sets中的最佳选择

以及需要对象具有适当方法的任何集合

价值语义平等。

所有这三个分类都是可序列化的,当我们想要

将其保存或加载到流可以代表的位置或从中加载。

这些小类还知道如何从自身产生可读的String

当我们需要阅读这些值的实际含义时,这非常方便。

间奏曲

我们的小语言基本上看起来像这样:


D1;D2; ... Dm;E1;E2;E3; ... En; 
D和E可以是任意顺序,甚至可以混合。 D是定义

和用户功能的声明(如果有),而E代表

可能使用这些函数或使用内置函数的表达式。

定义,声明和表达式将转换为以下内容的列表

解析器和代码生成器的指令。 分号什么也没做

实际上:它们充当解析器的语法糖,并指出何时

定义的表达已完成,可能在其后开始下一个表达。

正则表达式具有一个值,例如表达式1 + 2具有一个值3

保留在值堆栈上。 一堆正则表达式留下一堆

该堆栈上的值。 我们需要特殊的“功能”(注意引号)

可以直接操纵该堆栈(由口译员拥有),否则我们

程序完成时,可能最终会在堆栈上产生一堆值

我们不需要或想要的。 这样的特殊功能可能不会返回值。

它们等效于其他语言中的过程。

过程不能仅仅作为过程表达的一部分,因为它们不

结果(返回)值。 例如,存在“ clear()”过程或

特殊功能。 它清除所有值结果都保留在其中的堆栈。

表达式“ clear()+ 42”毫无道理。 看看

此非表达式的后缀表示形式:“ clear()42 +”。 这是什么

内部发生:

1)clear()清除解释器中的Value堆栈;

2)将42推送到值堆栈上;

3)'+'指令尝试从堆栈中弹出两个值。

步骤3)是一个错误:堆栈上没有要添加的两个操作数。 口译员

防止堆栈下溢。 但是这些程序或特殊功能在哪里可以

然后用? 正如我们刚刚看到的那样,它们不能成为表达式的一部分,但是它们

可以是表达式本身,后跟分号。 表达方式'

(再次用引号引起来)只是没有导致(返回)值,但没有

很重要,因为不需要将该值用作较大表达式的操作数。

稍后我们将看到,这些特殊功能可以在

强大的方式 一种使我们的编译器/解释器类玩起来有趣的方式

它也可以有教育目的。 仍然是“老歌”(excusez le mot)的人

记得那些惠普反向抛光符号计算器一定知道

我在说什么 我们的小语言可能是“正常”后缀的混合体

表达式和后缀显式堆栈操作。 选择一个你喜欢的。

变量绑定

我们的小语言也了解局部和全局变量。 在哪

这些变量存储了吗? 答案是:口译员知道如何处理

他们。 口译员管理一堆“范围”。 范围是一个

将变量的名称与其值关联。

总有一个可用范围。 即使在最外层。 看一下

这个例子:


x= 1;
x+= 41; 
第一个表达式将值1与名称“ x”关联。 第二

表达式查找'x'的当前值(即1),向其加41,然后

将名为“ x”的变量与值42重新关联。如果变量不是

但仍与值相关联,则假定值为0。

用户定义的函数将另一个作用域推送到作用域堆栈中。 有

看看这个:


function f(x) = x+= 41;
x= 54;
f(1); 
当函数f开始执行时,它首先将新的空作用域推入作用域

堆栈,并将其参数“ x”与值1关联,因为这就是

参数值是调用f(1)时的值。

接下来,对表达式“ x + = 41”进行求值,这意味着将更新变量“ x”

并获得值42。然后是哪个“ x”? 答案:在范围内找到变量'x

发现最接近范围堆栈的顶部。

在评估f(1)之前,作用域堆栈仅包含一个作用域:


{ x=54 } 
调用用户定义的函数时,将推送新作用域:

{ }
{ x=54 } 
并且参数“ x”与值1相关联:

{ x=1 }
{ x=54 } 
函数的主体从顶部开始查找与“ x”关联的值

堆栈 它找到值1并加上41,然后将“ x”与

该新值:


{ x=42 }
{ x=54 } 
并返回与函数表达式的返回值相同的值(42)。

再次弹出作用域,并将值42推送到

口译员

如您所见,“ x”的“全局”值的旧关联不变。

正如我们对其他编程语言所期望的那样:局部变量“隐藏”

具有相同名称的更多全局变量。

当我们搜索变量的关联时,我们可以在以下位置搜索它

范围堆栈上的顶级范围。 但是为什么要这样呢? 我们可以搜索全部

范围堆栈上的关联从顶部到底部,直到我们找到一个。 我们

找不到变量的关联,我们仅假设值是0。

看一下这个例子:


function f(x);
function g(x, y)= f(x);
function f= x+y;
x= 41;
y= 1;
f(x);
g(x, 13); 
棘手的函数是函数“ f”:它具有一个参数“ x”,但会添加其参数

值等于“ y”可能是什么。 从全局范围调用第一个f(x)

“ y”的值为1,因此结果将为41 + 1 = 42。 下一个函数“ g”被调用

其中有两个参数“ x”和“ y”。 在范围堆栈的下一个范围中

变量“ y”将与值13绑定(或关联)。函数“ f”

再次被呼叫; 它添加了'x'的值,该值是函数'g'的局部值,

再次具有值41,即与该值相关联的“ y”值

在函数“ g”的函数调用中为13,因此结果为54。

一遍又一遍地阅读,直到您了解“深度绑定”。

深层绑定(与浅层绑定相反)是传递和

范围界定机制在现代C / C ++ / Java之类的语言中有些丢失。

这些语言使用浅表绑定,也称为词法作用域:

如果在作用域堆栈的最顶层作用域中找不到变量,则为

被认为是错误。 深度绑定更有趣。

深度绑定用于Lisp的变体中,Lisp是一种功能编程语言,

比我们的小语言更强大,但是我们也可以做到,我们将

承诺在以后的文章中使用它。

这是在我们的解释器中实现以下代码堆栈所需的代码

我们的范围; 不多 我们再一次将List用于我们的范围栈,

作用域只是一个Map <String,Value),它与变量的名称相关联

(字符串),其值:


package compiler; 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map; 
import compiler.exception.InterpreterException;
import compiler.value.Value; 
public class Scope extends ArrayList<Map<String, Value>> { 
    private static final long serialVersionUID = 3520767277469443623L; 
    public Scope() { push(); } 
    private Map<String, Value> find (String name) { 
        for (int i= size(); --i >= 0;) {
            Map<String, Value> map= get(i);
            if (map.containsKey(name)) return map;
        } 
        return get(size()-1);
    } 
    public void push() { add(new HashMap<String, Value>()); } 
    public void pop() { remove(size()-1); } 
    public void put(String name, Value value) { 
        find(name).put(name, value);
    } 
    public Value get(String name) throws InterpreterException { 
        Value value= find(name).get(name); 
        return (value == null)?Value.NUL:value;
    }
} 
push方法将新创建的空作用域压入堆栈,而pop方法将压入堆栈

再次将其删除。 这不是很有趣。

如果需要一个变量值,则搜索整个关联堆栈;

如果找到了关联(通过私有查找方法),则关联为

返回,否则top关联由find方法返回。

如果关联Map不包含特定值,则返回0。

放置变量及其值的关联仅存储关联

在最上方的地图中。 如果我们将搜索范围限制在

堆栈,我们将实现浅层绑定; 当前的实现

深度绑定(在整个堆栈中搜索关联),这更有趣。

请注意,如果需要分配一个变量,并且在

关联的堆栈,使用顶部关联Map; 这意味着

如果当时未知的变量总是在全局范围内终止,

赋值仅作为表达式执行,或者如果

分配是在用户定义的函数内执行的。 换句话说:一个用户

定义的函数无法创建全局变量。

解释器本身封装了这样一个Scope对象,并允许我们访问它:


protected Scope scope= new Scope(); 
public Scope scope() { return scope; } 
加速记忆或浅层装订

如您在上一段中所见,深度绑定更有趣(而且功能更强大)。

也比浅层装订或词汇作用域更重要。 我们的小口译员支持

后者的绑定形式也略有加速。

假设用户定义的函数f(x)需要花费一些时间才能产生返回值

值。 还要假设函数调用f(x)总是产生相同的返回值

始终保持其参数x的相同值。 我们为什么要这样称呼

如果我们已经知道前一个函数的返回值,则再次调用该函数

叫f(x)?

如果函数“ f”产生相同的结果,我们只能“知道”该返回值

当然,相同参数值“ n”的返回值。 深度绑定使

这是不可能的,因为不属于参数列表的变量可能

改变我们的背后。

在上一段中,我们已经了解了深度绑定的行为。 如果发生什么情况

给定此绑定,我们记下了每个用户定义函数的每个返回值

机制? 我们“记住”函数调用“属于”的返回值

给定某些参数值和未绑定的变量值

或由功能本身关联。 这几乎与浅层装订相同

不同之处在于,即使第一个函数调用也会失败。

我们去吧:解释器管理一个简单的表来记忆功能

给定给定参数(列表)值的调用。 给出用户名

定义的函数,我们需要与返回值关联的参数值列表

值。 我们需要两个地图:顶层地图与用户名相关联

包含所有调用的Map的已定义函数。 每次调用都是一个列表

与该函数的返回值关联的参数值。

为此,我制作了一个“备注”课程。 这里是:


public class Memo extends HashMap<String, Map<List<Value>, Value>> { 
    private static final long serialVersionUID = 4800064570213212044L; 
    private boolean memo= false; 
    public void put(String name, List<Value> args, Value value) { 
        if (!memo) return; 
        Map<List<Value>, Value> values= get(name); 
        if (values == null) 
            put(name, values= new HashMap<List<Value>, Value>()); 
        values.put(new ArrayList<Value>(args), value);
    } 
    public Value get(String name, List<Value> args) { 
        if (!memo) return null; 
        Map<List<Value>, Value> values= get(name); 
        return (values != null)?values.get(args):null;
    } 
    public void setMemo(boolean memo) { this.memo= memo; } 
    public boolean isMemo() { return memo; }
} 
它有一个专用的布尔标志“ memo”,用于指示是否允许

“记住”函数调用及其与

返回值。

此类实现所需的方法:“ put”和“ get”方法。 'put'

方法添加关联,并且“ get”方法再次检索关联

(如果有)。 这个小课程是一个很好的例子,说明了如何使用

集合框架。

在本文的下一部分中见。

亲切的问候,

乔斯

From: https://bytes.com/topic/java/insights/739701-compilers-6a-interpreters

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值