编译器-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 并在指令上调用“执行”方法。 口译员没有
甚至知道*什么*被执行,它所知道的只是一条指令很简单
接口:
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 { private static final long serialVersionUID = -2663291338225528177L; public void add(double value) { add(new DblValue(value)); }
} 堆栈是一个List 实现了一些方便的方法,可以在堆栈上压入“ 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 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; public LstValue(List value) { this.value= value; } public List 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
所有三个类(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 (字符串),其值: 再次将其删除。 这不是很有趣。 如果需要一个变量值,则搜索整个关联堆栈; 如果找到了关联(通过私有查找方法),则关联为 返回,否则top关联由find方法返回。 如果关联Map不包含特定值,则返回0。 放置变量及其值的关联仅存储关联 在最上方的地图中。 如果我们将搜索范围限制在 堆栈,我们将实现浅层绑定; 当前的实现 深度绑定(在整个堆栈中搜索关联),这更有趣。 请注意,如果需要分配一个变量,并且在 关联的堆栈,使用顶部关联Map; 这意味着 如果当时未知的变量总是在全局范围内终止, 赋值仅作为表达式执行,或者如果 分配是在用户定义的函数内执行的。 换句话说:一个用户 定义的函数无法创建全局变量。 解释器本身封装了这样一个Scope对象,并允许我们访问它: 如您在上一段中所见,深度绑定更有趣(而且功能更强大)。 也比浅层装订或词汇作用域更重要。 我们的小口译员支持 后者的绑定形式也略有加速。 假设用户定义的函数f(x)需要花费一些时间才能产生返回值 值。 还要假设函数调用f(x)总是产生相同的返回值 始终保持其参数x的相同值。 我们为什么要这样称呼 如果我们已经知道前一个函数的返回值,则再次调用该函数 叫f(x)? 如果函数“ f”产生相同的结果,我们只能“知道”该返回值 当然,相同参数值“ n”的返回值。 深度绑定使 这是不可能的,因为不属于参数列表的变量可能 改变我们的背后。 在上一段中,我们已经了解了深度绑定的行为。 如果发生什么情况 给定此绑定,我们记下了每个用户定义函数的每个返回值 机制? 我们“记住”函数调用“属于”的返回值 给定某些参数值和未绑定的变量值 或由功能本身关联。 这几乎与浅层装订相同 不同之处在于,即使第一个函数调用也会失败。 我们去吧:解释器管理一个简单的表来记忆功能 给定给定参数(列表)值的调用。 给出用户名 定义的函数,我们需要与返回值关联的参数值列表 值。 我们需要两个地图:顶层地图与用户名相关联 包含所有调用的Map的已定义函数。 每次调用都是一个列表 与该函数的返回值关联的参数值。 为此,我制作了一个“备注”课程。 这里是: “记住”函数调用及其与 返回值。 此类实现所需的方法:“ put”和“ get”方法。 'put' 方法添加关联,并且“ get”方法再次检索关联 (如果有)。 这个小课程是一个很好的例子,说明了如何使用 集合框架。 在本文的下一部分中见。 亲切的问候, 乔斯 From: https://bytes.com/topic/java/insights/739701-compilers-6a-interpreters
push方法将新创建的空作用域压入堆栈,而pop方法将压入堆栈
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 private static final long serialVersionUID = 3520767277469443623L; public Scope() { push(); } private Map
加速记忆或浅层装订
protected Scope scope= new Scope();
public Scope scope() { return scope; }
它有一个专用的布尔标志“ memo”,用于指示是否允许
public class Memo extends HashMap, Value> values= get(name);
if (values == null) put(name, values= new HashMap, Value>());
values.put(new ArrayList, Value> values= get(name);
return (values != null)?values.get(args):null;} public void setMemo(boolean memo) { this.memo= memo; } public boolean isMemo() { return memo; }
}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
