问候,
上周,我们完成了编译器的重要部分。 我们可以解析和
编译源文本。 源文本包含表达式和函数的列表
定义。 编译均成功,我们从以下位置获取了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