nus-cs1101s-sicp-js-merge-6
NUS CS1101S:SICP JavaScript 描述(七)
译者:飞龙
-
c.使用这个来表达一个允许推导关系的规则
list(pair("great", $rel), $x, $y)
其中
$rel是以"grandson"结尾的列表。 -
d.检查你的规则在查询
related(list("great", "grandson"), $g, $ggs)和related($relationship, "Adam", "Irad")上的表现。
4.4.4 实现查询系统
4.4.2 节描述了查询系统的工作原理。现在我们通过提供系统的完整实现来填写细节。
4.4.4.1 驱动循环
查询系统的驱动循环反复读取输入表达式。如果表达式是要添加到数据库中的规则或断言,那么信息就会被添加。否则,假定表达式是一个查询。驱动程序将此查询传递给evaluate_query,并与一个由单个空帧组成的初始帧流一起传递。求值的结果是通过满足在数据库中找到的变量值来生成的帧流。这些帧用于形成一个新的流,其中包含原始查询的副本,其中变量被帧流提供的值实例化,最终的流被显示:
const input_prompt = "Query input:";
const output_prompt = "Query results:";
function query_driver_loop() {
const input = user_read(input_prompt) + ";";
if (is_null(input)) {
display("evaluator terminated");
} else {
const expression = parse(input);
const query = convert_to_query_syntax(expression);
if (is_assertion(query)) {
add_rule_or_assertion(assertion_body(query));
display("Assertion added to data base.");
} else {
display(output_prompt);
display_stream(
stream_map(
frame =>
unparse(instantiate_expression(expression, frame)),
evaluate_query(query, singleton_stream(null))));
}
return query_driver_loop();
}
}
在这里,与本章中的其他求值器一样,我们使用parse将作为字符串给出的查询语言的组件转换为 JavaScript 语法表示。(我们在输入表达式字符串后附加了一个分号,因为parse期望一个语句。)然后我们进一步将语法表示转换为适合查询系统的概念级别,使用convert_to_query_syntax,它在 4.4.4.7 节中声明,以及谓词is_assertion和选择器assertion_body。函数add_rule_or_assertion在 4.4.4.5 节中声明。查询求值产生的帧用于实例化语法表示,结果被解析成字符串进行显示。函数instantiate_expression和unparse在 4.4.4.7 节中声明。
4.4.4.2 求值器
evaluate_query函数由query_driver_loop调用,是查询系统的基本求值器。它以查询和帧流作为输入,并返回扩展帧的流。它通过使用get和put进行数据导向分派来识别句法形式,就像我们在第 2 章中实现通用操作一样。任何未被识别为句法形式的查询都被假定为简单查询,由simple_query处理。
function evaluate_query(query, frame_stream) {
const qfun = get(type(query), "evaluate_query");
return is_undefined(qfun)
? simple_query(query, frame_stream)
: qfun(contents(query), frame_stream);
}
函数type和contents在 4.4.4.7 节中定义,实现了句法形式的抽象语法。
简单查询
simple_query函数处理简单查询。它以简单查询(模式)和帧流作为参数,并返回通过扩展每个帧的所有数据库匹配项形成的流。
function simple_query(query_pattern, frame_stream) {
return stream_flatmap(
frame =>
stream_append_delayed(
find_assertions(query_pattern, frame),
() => apply_rules(query_pattern, frame)),
frame_stream);
}
对于输入流中的每个框架,我们使用find_assertions(第 4.4.4.3 节)来匹配数据库中所有断言与模式,产生一个扩展框架的流,并使用apply_rules(第 4.4.4.4 节)来应用所有可能的规则,产生另一个扩展框架的流。这两个流被合并(使用stream_append_delayed,第 4.4.4.6 节)以生成给定模式可以满足的所有方式的流,与原始框架一致(参见练习 4.68)。输入框架的流使用stream_flatmap(第 4.4.4.6 节)组合,形成一个大的流,列出原始输入流中任何框架可以扩展以与给定模式匹配的所有方式。
复合查询
我们处理and查询,如图 4.6 所示,使用conjoin函数,它以连接词和框架流作为输入,并返回扩展框架的流。首先,conjoin处理框架流以找到满足连接词中第一个查询的所有可能框架扩展的流。然后,使用这个新的框架流,它递归地将conjoin应用于其余的查询。
function conjoin(conjuncts, frame_stream) {
return is_empty_conjunction(conjuncts)
? frame_stream
: conjoin(rest_conjuncts(conjuncts),
evaluate_query(first_conjunct(conjuncts),
frame_stream));
}
陈述
put("and", "evaluate_query", conjoin);
设置evaluate_query以在遇到and时分派到conjoin。
我们类似地处理or查询,如图 4.7 所示。or的各个分离词的输出流分别计算,并使用第 4.4.4.6 节中的interleave_delayed函数合并。(参见练习 4.68 和 4.69。)
function disjoin(disjuncts, frame_stream) {
return is_empty_disjunction(disjuncts)
? null
: interleave_delayed(
evaluate_query(first_disjunct(disjuncts), frame_stream),
() => disjoin(rest_disjuncts(disjuncts), frame_stream));
}
put("or", "evaluate_query", disjoin);
在第 4.4.4.7 节中给出了表示连接词和分离词的谓词和选择器。
过滤器
not语法形式由第 4.4.2 节中概述的方法处理。我们尝试扩展输入流中的每个框架以满足被否定的查询,并且只有在不能扩展时才将给定框架包含在输出流中。
function negate(exps, frame_stream) {
return stream_flatmap(
frame =>
is_null(evaluate_query(negated_query(exps),
singleton_stream(frame)))
? singleton_stream(frame)
: null, frame_stream);
}
put("not", "evaluate_query", negate);
javascript_predicate语法形式类似于not的过滤器。流中的每个框架用于实例化谓词中的变量,实例化的谓词被求值,谓词求值为false的框架被过滤出输入流。使用evaluate(第 4.1 节)从the_global_environment求值实例化的谓词,因此可以处理任何 JavaScript 表达式,只要在求值之前实例化所有模式变量。
function javascript_predicate(exps, frame_stream) {
return stream_flatmap(
frame =>
evaluate(instantiate_expression(
javascript_predicate_expression(exps),
frame),
the_global_environment)
? singleton_stream(frame)
: null,
frame_stream);
}
put("javascript_predicate", "evaluate_query", javascript_predicate);
always_true语法形式提供了一个始终满足的查询。它忽略其内容(通常为空),并简单地通过输入流中的所有框架。rule_body选择器(第 4.4.4.7 节)使用always_true为没有定义体的规则提供体(即,始终满足的规则)。
function always_true(ignore, frame_stream) {
return frame_stream;
}
put("always_true", "evaluate_query", always_true);
定义not和javascript_predicate的选择器在第 4.4.4.7 节中给出。
4.4.4.3 通过模式匹配查找断言
函数find_assertions由simple_query(第 4.4.4.2 节)调用,以模式和框架作为输入。它返回一个框架流,每个框架都通过给定模式的数据库匹配扩展给定框架。它使用fetch_assertions(第 4.4.4.5 节)获取数据库中所有断言的流,应该检查这些断言是否与模式和框架匹配。这里使用fetch_assertions的原因是我们通常可以应用简单的测试来消除数据库中的许多条目,使其不再是成功匹配的候选项。如果我们消除了fetch_assertions并简单地检查数据库中所有断言的流,系统仍然可以工作,但计算效率会降低,因为我们需要对匹配器进行更多的调用。
function find_assertions(pattern, frame) {
return stream_flatmap(
datum => check_an_assertion(datum, pattern, frame),
fetch_assertions(pattern, frame));
}
函数check_an_assertion以数据对象(断言)、模式和框架作为参数,并返回一个包含扩展框架的单元素流,或者如果匹配失败则返回null。
function check_an_assertion(assertion, query_pat, query_frame) {
const match_result = pattern_match(query_pat, assertion,
query_frame);
return match_result === "failed"
? null
: singleton_stream(match_result);
}
基本模式匹配器返回字符串failed或给定框架的扩展。匹配器的基本思想是逐个元素地检查模式与数据,累积模式变量的绑定。如果模式和数据对象相同,则匹配成功,我们返回迄今为止累积的绑定框架。否则,如果模式是一个变量(由 4.4.4.7 节中声明的is_variable函数检查),我们通过将变量绑定到数据来扩展当前框架,只要这与框架中已有的绑定一致。如果模式和数据都是对,我们(递归地)将模式的头与数据的头进行匹配以产生一个框架;然后在这个框架中,我们将模式的尾与数据的尾进行匹配。如果这些情况都不适用,则匹配失败,我们返回字符串failed。
function pattern_match(pattern, data, frame) {
return frame === "failed"
? "failed"
: equal(pattern, data)
? frame
: is_variable(pattern)
? extend_if_consistent(pattern, data, frame)
: is_pair(pattern) && is_pair(data)
? pattern_match(tail(pattern),
tail(data),
pattern_match(head(pattern),
head(data),
frame))
: "failed";
}
这是一个通过添加新绑定来扩展框架的函数,如果这与框架中已有的绑定一致的话:
function extend_if_consistent(variable, data, frame) {
const binding = binding_in_frame(variable, frame);
return is_undefined(binding)
? extend(variable, data, frame)
: pattern_match(binding_value(binding), data, frame);
}
如果框架中没有变量的绑定,我们只需将变量的绑定添加到数据中。否则,我们在框架中将数据与框架中变量的值进行匹配。如果存储的值只包含常量,那么它必须在模式匹配期间由extend_if_consistent存储,匹配只是简单地测试存储的值和新值是否相同。如果是,则返回未修改的框架;如果不是,则返回失败指示。然而,存储的值可能包含模式变量,如果它是在统一期间存储的(见 4.4.4.4 节)。存储的模式与新数据的递归匹配将为这个模式中的变量添加或检查绑定。例如,假设我们有一个框架,其中$x绑定到list("f", $y),而$y未绑定,我们希望通过将$x绑定到list("f", "b")来扩充这个框架。我们查找$x并发现它绑定到list("f", $y)。这导致我们在同一个框架中将list("f", $y)与建议的新值list("f", "b")进行匹配。最终,这个匹配通过添加$y绑定到"b"来扩展框架。变量$x仍然绑定到list("f", $y)。我们从不修改存储的绑定,也不会为给定变量存储多个绑定。
extend_if_consistent使用的函数来操作绑定在 4.4.4.8 节中定义。
4.4.4.4 规则和统一
函数apply_rules是find_assertions(4.4.4.3 节)的规则类比。它以模式和框架作为输入,并通过应用来自数据库的规则形成一个扩展框架流。函数stream_flatmap将apply_a_rule映射到可能适用的规则流(由fetch_rules选择,4.4.4.5 节),并组合结果框架的流。
function apply_rules(pattern, frame) {
return stream_flatmap(rule => apply_a_rule(rule, pattern, frame),
fetch_rules(pattern, frame));
}
函数apply_a_rule使用 4.4.2 节中概述的方法应用规则。它首先通过将规则结论与给定框架中的模式统一来扩充其参数框架。如果成功,它就在这个新框架中求值规则主体。
然而,在发生这些情况之前,程序会将规则中的所有变量重命名为唯一的新名称。这样做的原因是为了防止不同规则应用的变量相互混淆。例如,如果两个规则都使用名为$x的变量,那么每个规则在应用时可能都会向框架中添加一个$x的绑定。这两个$x互不相关,我们不应该被误导以为这两个绑定必须一致。我们可以设计一个更聪明的环境结构来代替重命名变量;然而,我们选择的重命名方法是最直接的,即使不是最有效的(见练习 4.76)。这里是apply_a_rule函数:
function apply_a_rule(rule, query_pattern, query_frame) {
const clean_rule = rename_variables_in(rule);
const unify_result = unify_match(query_pattern,
conclusion(clean_rule),
query_frame);
return unify_result === "failed"
? null
: evaluate_query(rule_body(clean_rule),
singleton_stream(unify_result));
}
提取规则的部分的选择器rule_body和conclusion在 4.4.4.7 节中定义。
我们通过将唯一标识符(如数字)与每个规则应用关联,并将此标识符与原始变量名结合起来来生成唯一的变量名。例如,如果规则应用标识符为 7,我们可能会将规则中的每个$x更改为$x_7,将规则中的每个$y更改为$y_7。(函数make_new_variable和new_rule_application_id包含在第 4.4.4.7 节的语法函数中。)
function rename_variables_in(rule) {
const rule_application_id = new_rule_application_id();
function tree_walk(exp) {
return is_variable(exp)
? make_new_variable(exp, rule_application_id)
: is_pair(exp)
? pair(tree_walk(head(exp)),
tree_walk(tail(exp)))
: exp;
}
return tree_walk(rule);
}
统一算法被实现为一个函数,它以两个模式和一个框架作为输入,并返回扩展的框架或字符串"failed"。统一器类似于模式匹配器,只是它是对称的 - 变量允许在匹配的两侧。函数unify_match基本上与pattern_match相同,只是下面有一个额外的子句(标记为***),用于处理右侧对象为变量的情况。
function unify_match(p1, p2, frame) {
return frame === "failed"
? "failed"
: equal(p1, p2)
? frame
: is_variable(p1)
? extend_if_possible(p1, p2, frame)
: is_variable(p2) // *
? extend_if_possible(p2, p1, frame) // *
: is_pair(p1) && is_pair(p2)
? unify_match(tail(p1),
tail(p2),
unify_match(head(p1),
head(p2),
frame))
: "failed";
}
在统一中,就像单向模式匹配一样,我们只希望接受框架的提议扩展,只有当它与现有绑定一致时才会这样。在统一中使用的函数extend_if_possible与模式匹配中使用的函数extend_if_consistent相同,只是在程序中有两个特殊检查,标记为***。在第一种情况下,如果我们尝试匹配的变量未绑定,但我们尝试匹配的值本身是(不同的)变量,则有必要检查该值是否已绑定,并且如果是,则匹配其值。如果匹配的双方都未绑定,我们可以将其中一个绑定到另一个。
第二个检查处理尝试将变量绑定到包含该变量的模式的情况。只要在两个模式中重复一个变量,这种情况就会发生。例如,考虑在两个模式list($x, $x)和list($y, 包含 $y 的表达式)中统一,在其中$x和$y都未绑定的框架中。首先,将$x与$y匹配,将$x绑定到$y。接下来,将相同的$x与包含$y的给定表达式匹配。由于$x已绑定到$y,这导致将$y与表达式匹配。如果我们认为统一器是在找到一组使模式相同的模式变量的值,那么这些模式暗示了查找一个$y,使得$y等于包含$y的表达式。我们拒绝这样的绑定;这些情况由谓词depends_on识别。另一方面,我们不希望拒绝将变量绑定到自身的尝试。例如,考虑统一list($x, $x)和list($y, $y)。将$x绑定到$y的第二次尝试将$y($x的存储值)与$y($x的新值)匹配。这由unify_match的equal子句处理。
function extend_if_possible(variable, value, frame) {
const binding = binding_in_frame(variable, frame);
if (! is_undefined(binding)) {
return unify_match(binding_value(binding),
value, frame);
} else if (is_variable(value)) { // *
const binding = binding_in_frame(value, frame);
return ! is_undefined(binding)
? unify_match(variable,
binding_value(binding),
frame)
: extend(variable, value, frame);
} else if (depends_on(value, variable, frame)) { // *
return "failed";
} else {
return extend(variable, value, frame);
}
}
函数depends_on是一个谓词,用于测试提议作为模式变量值的表达式是否依赖于该变量。这必须相对于当前框架来完成,因为表达式可能包含已经依赖于我们测试变量的值的变量的出现。depends_on的结构是一个简单的递归树遍历,我们在必要时替换变量的值。
function depends_on(expression, variable, frame) {
function tree_walk(e) {
if (is_variable(e)) {
if (equal(variable, e)) {
return true;
} else {
const b = binding_in_frame(e, frame);
return is_undefined(b)
? false
: tree_walk(binding_value(b));
}
} else {
return is_pair(e)
? tree_walk(head(e)) || tree_walk(tail(e))
: false;
}
}
return tree_walk(expression);
}
4.4.4.5 维护数据库
设计逻辑编程语言的一个重要问题是安排事物,以便在检查给定模式时尽可能少地检查不相关的数据库条目。为此,我们将断言表示为一个列表,其头部是表示断言信息类型的字符串。我们将断言存储在单独的流中,每种信息类型一个流,在一个由信息类型索引的表中。要获取可能匹配模式的断言,我们返回(以便使用匹配器进行测试)所有具有相同头部(相同信息类型)的存储断言。更聪明的方法也可以利用帧中的信息。我们避免构建用于索引程序的标准;相反,我们调用体现我们标准的谓词和选择器。
function fetch_assertions(pattern, frame) {
return get_indexed_assertions(pattern);
}
function get_indexed_assertions(pattern) {
return get_stream(index_key_of(pattern), "assertion-stream");
}
get_stream函数在表中查找流,并在没有存储内容时返回一个空的流。
function get_stream(key1, key2) {
const s = get(key1, key2);
return is_undefined(s) ? null : s;
}
规则也是类似地存储,使用规则结论的头部。一个模式可以匹配具有相同头部的规则结论的规则。因此,当获取可能匹配模式的规则时,我们获取所有结论具有与模式相同头部的规则。
function fetch_rules(pattern, frame) {
return get_indexed_rules(pattern);
}
function get_indexed_rules(pattern) {
return get_stream(index_key_of(pattern), "rule-stream");
}
add_rule_or_assertion函数由query_driver_loop用于向数据库添加断言和规则。每个项目都存储在索引中。
function add_rule_or_assertion(assertion) {
return is_rule(assertion)
? add_rule(assertion)
: add_assertion(assertion);
}
function add_assertion(assertion) {
store_assertion_in_index(assertion);
return "ok";
}
function add_rule(rule) {
store_rule_in_index(rule);
return "ok";
}
要实际存储断言或规则,我们将其存储在适当的流中。
function store_assertion_in_index(assertion) {
const key = index_key_of(assertion);
const current_assertion_stream =
get_stream(key, "assertion-stream");
put(key, "assertion-stream",
pair(assertion, () => current_assertion_stream));
}
function store_rule_in_index(rule) {
const pattern = conclusion(rule);
const key = index_key_of(pattern);
const current_rule_stream =
get_stream(key, "rule-stream");
put(key, "rule-stream",
pair(rule, () => current_rule_stream));
}
模式(断言或规则结论)存储在表中的键是它以字符串开头的字符串。
function index_key_of(pattern) { return head(pattern); }
4.4.4.6 流操作
查询系统使用了一些流操作,这些操作在第 3 章中没有介绍。stream_append_delayed和interleave_delayed函数与stream_append和interleave(第 3.5.3 节)类似,只是它们接受了一个延迟参数(就像第 3.5.4 节中的integral函数)。这在某些情况下会推迟循环(参见练习 4.68)。
function stream_append_delayed(s1, delayed_s2) {
return is_null(s1)
? delayed_s2()
: pair(head(s1),
() => stream_append_delayed(stream_tail(s1),
delayed_s2));
}
function interleave_delayed(s1, delayed_s2) {
return is_null(s1)
? delayed_s2()
: pair(head(s1),
() => interleave_delayed(delayed_s2(),
() => stream_tail(s1)));
}
stream_flatmap函数在查询求值器中用于在帧流上映射函数并组合结果帧流,它是第 2.2.3 节中为普通列表引入的flatmap函数的流模拟。然而,与普通的flatmap不同,我们使用交错过程累积流,而不是简单地追加它们(参见练习 4.69 和 4.70)。
function stream_flatmap(fun, s) {
return flatten_stream(stream_map(fun, s));
}
function flatten_stream(stream) {
return is_null(stream)
? null
: interleave_delayed(
head(stream),
() => flatten_stream(stream_tail(stream)));
}
求值器还使用以下简单函数生成由单个元素组成的流:
function singleton_stream(x) {
return pair(x, () => null);
}
4.4.4.7 查询语法函数和实例化
我们在第 4.4.4.1 节中看到,驱动循环首先将输入字符串转换为 JavaScript 语法表示。输入被设计成看起来像 JavaScript 表达式,以便我们可以使用第 4.1.2 节中的parse函数,并且还支持javascript_predicate中的 JavaScript 表示。例如,
parse('job($x, list("computer", "wizard"));');
产生
list("application",
list("name", "job"),
list(list("name", "$x"),
list("application",
list("name", "list"),
list(list("literal", "computer"),
list("literal", "wizard")))))
标签"application"表示,从语法上讲,查询将被视为 JavaScript 中的函数应用。函数unparse将语法转换回字符串:
unparse(parse('job($x, list("computer", "wizard"));'));
'job($x, list("computer", "wizard"))'
在查询处理器中,我们假设了断言、规则和查询的查询语言特定表示。函数convert_to_query_syntax将语法表示转换为该表示。使用相同的示例,
convert_to_query_syntax(parse('job($x, list("computer", "wizard"));'));
产生
list("job", list("name", "$x"), list("computer", "wizard"))
查询系统函数,如第 4.4.4.5 节中的add_rule_or_assertion和第 4.4.4.2 节中的evaluate_query,使用下面声明的选择器和谓词,如type、contents、is_rule和first_conjunct,对查询语言特定表示进行操作。图 4.8 描述了查询系统使用的三个抽象屏障以及转换函数parse、unparse和convert_to_query_syntax如何连接它们。

图 4.8 查询系统中的语法抽象。
处理模式变量
在查询处理过程中,谓词is_variable用于查询语言特定表示,并在实例化过程中用于 JavaScript 语法表示,以识别以美元符号开头的名称。我们假设有一个char_at函数,它返回一个字符串,该字符串仅包含给定位置的给定字符串的字符。⁷⁵
function is_variable(exp) {
return is_name(exp) && char_at(symbol_of_name(exp), 0) === "$";
}
唯一变量是通过规则应用(在 4.4.4.4 节)中的以下函数构建的。规则应用的唯一标识符是一个数字,每次应用规则时都会递增。⁷⁶
let rule_counter = 0;
function new_rule_application_id() {
rule_counter = rule_counter + 1;
return rule_counter;
}
function make_new_variable(variable, rule_application_id) {
return make_name(symbol_of_name(variable) + "_" +
stringify(rule_application_id));
}
函数convert_to_query_syntax
函数convert_to_query_syntax通过简化断言、规则和查询,将 JavaScript 语法表示递归转换为查询语言特定表示,使得应用程序的函数表达式中的名称的符号成为标记,除非该符号是"pair"或"list",则构建一个(未标记的)JavaScript 对或列表。这意味着convert_to_query_syntax在转换过程中解释构造函数pair和list的应用,并且处理函数(如 4.4.4.3 节的pattern_match和 4.4.4.4 节的unify_match)可以直接操作预期的对和列表,而不是在解析器生成的语法表示上进行操作。javascript_predicate的(单元素)“参数”列表保持未处理,如下所述。变量保持不变,文字简化为其包含的原始值。
function convert_to_query_syntax(exp) {
if (is_application(exp)) {
const function_symbol = symbol_of_name(function_expression(exp));
if (function_symbol === "javascript_predicate") {
return pair(function_symbol, arg_expressions(exp));
} else {
const processed_args = map(convert_to_query_syntax,
arg_expressions(exp));
return function_symbol === "pair"
? pair(head(processed_args), head(tail(processed_args)))
: function_symbol === "list"
? processed_args
: pair(function_symbol, processed_args);
}
} else if (is_variable(exp)) {
return exp;
} else { // exp is literal
return literal_value(exp);
}
}
对此处理的一个例外是javascript_predicate。由于其谓词表达式的实例化 JavaScript 语法表示被传递给 4.1.1 节的evaluate,来自parse的原始语法表示需要保持在表达式的查询语言特定表示中。在 4.4.1 节的这个例子中
and(salary($person, $amount), javascript_predicate($amount > 50000))
convert_to_query_syntax生成一个数据结构,其中 JavaScript 语法表示嵌入在查询语言特定表示中:
list("and",
list("salary", list("name", "$person"), list("name", "$amount")),
list("javascript_predicate",
list("binary_operator_combination",
">",
list("name", "$amount"),
list("literal", 50000))))
为了求值已处理查询的javascript_predicate子表达式,4.4.4.2 节的函数javascript_predicate调用嵌入的 JavaScript 语法表示的$amount > 50000的instantiate_expression(下文)来替换变量list("name", "$amount")为一个文字,例如list("literal", 70000),表示$amount绑定的原始值,这里是 70000。JavaScript 求值器可以求值实例化的谓词,现在表示为70000 > 50000。
表达式的实例化
4.4.4.2 节的函数javascript_predicate和 4.4.4.1 节的驱动循环在表达式上调用instantiate_expression,以获得一个副本,其中表达式中的任何变量都被给定帧中的值替换。输入和结果表达式使用 JavaScript 语法表示,因此从实例化变量中得到的任何值都需要从其在绑定中的形式转换为 JavaScript 语法表示。
function instantiate_expression(expression, frame) {
return is_variable(expression)
? convert(instantiate_term(expression, frame))
: is_pair(expression)
? pair(instantiate_expression(head(expression), frame),
instantiate_expression(tail(expression), frame))
: expression;
}
函数instantiate_term以变量、对或原始值作为第一个参数,并以帧作为第二个参数,并递归地将第一个参数中的变量替换为帧中的值,直到达到原始值或未绑定变量。当过程遇到对时,将构造一个新的对,其部分是原始部分的实例化版本。例如,如果在帧f中将$x绑定到对[$y, 5],并且$y又绑定到 3,那么将instantiate_term应用于list("name", "$x")和f的结果是对[3, 5]。
function instantiate_term(term, frame) {
if (is_variable(term)) {
const binding = binding_in_frame(term, frame);
return is_undefined(binding)
? term // leave unbound variable as is
: instantiate_term(binding_value(binding), frame);
} else if (is_pair(term)) {
return pair(instantiate_term(head(term), frame),
instantiate_term(tail(term), frame));
} else { // term is a primitive value
return term;
}
}
函数convert构造了一个 JavaScript 语法表示,用于instantiate_term返回的变量、对或原始值。原始中的一对变成了 JavaScript 的对构造函数的应用,原始值变成了文字。
function convert(term) {
return is_variable(term)
? term
: is_pair(term)
? make_application(make_name("pair"),
list(convert(head(term)),
convert(tail(term))))
: // term is a primitive value
make_literal(term);
}
为了说明这三个函数,考虑查询时发生的情况
job($x, list("computer", "wizard"))
其 JavaScript 语法表示在第 4.4.4.7 节开头给出,由驱动循环处理。假设结果流的帧g将变量$x绑定到["Bitdiddle", $y],变量$y绑定到["Ben", null]。
instantiate_term(list("name", "$x"), g)
返回列表
list("Bitdiddle", "Ben")
将convert转换为
list("application",
list("name", "pair"),
list(list("literal", "Bitdiddle"),
list("application",
list("name", "pair"),
list(list("literal", "Ben"),
list("literal", null)))))
应用于查询的 JavaScript 语法表示和帧g的instantiate_expression的结果是:
list("application",
list("name", "job"),
list(list("application",
list("name", "pair"),
list(list("literal", "Bitdiddle"),
list("application",
list("name", "pair"),
list(list("literal", "Ben"),
list("literal", null))))),
list("application",
list("name", "list"),
list(list("literal", "computer"),
list("literal", "wizard")))))
驱动循环取消解析此表示并将其显示为:
'job(list("Bitdiddle", "Ben"), list("computer", "wizard"))'
函数unparse
函数unparse通过应用第 4.1.2 节的语法规则将给定的 JavaScript 语法表示转换为字符串。我们仅描述了unparse用于第 4.4.1 节示例中出现的那些表达式的类型,将语句和其余类型的表达式留给练习 4.2。通过stringifying其值来转换文字,将名称转换为其符号。应用通过取消解析函数表达式格式化,我们可以假定这里是一个名称,后面跟着用括号括起来的逗号分隔的参数表达式字符串。二元运算符组合使用中缀表示法格式化。
function unparse(exp) {
return is_literal(exp)
? stringify(literal_value(exp))
: is_name(exp)
? symbol_of_name(exp)
: is_list_construction(exp)
? unparse(make_application(make_name("list"),
element_expressions(exp)))
: is_application(exp) && is_name(function_expression(exp))
? symbol_of_name(function_expression(exp)) +
"(" +
comma_separated(map(unparse, arg_expressions(exp))) +
")"
: is_binary_operator_combination(exp)
? "(" + unparse(first_operand(exp)) +
" " + operator_symbol(exp) +
" " + unparse(second_operand(exp)) +
")"
〈unparsing other kinds of JavaScript components〉
: error(exp, "unknown syntax – unparse");
}
function comma_separated(strings) {
return accumulate((s, acc) => s + (acc === "" ? "" : ", " + acc),
"",
strings);
}
函数unparse可以在没有子句的情况下正常工作
: is_list_construction(exp)
? unparse(make_application(make_name("list"),
element_expressions(exp)))
但在模式变量由列表实例化的情况下,输出字符串将是不必要冗长的。在上面的例子中,处理查询
job($x, list("computer", "wizard"))
产生将$x绑定到["Bitdiddle", ["Ben", null]]的帧,unparse产生
'job(list("Bitdiddle", "Ben"), list("computer", "wizard"))'
但是,如果没有子句,它将产生
'job(pair("Bitdiddle", pair("Ben", null)), list("computer", "wizard"))'
明确构造了构成第一个列表的两个对。为了实现第 4.4.1 节中使用的更简洁的格式,我们插入了一个子句来检查表达式是否构造了一个列表,如果是的话,我们将其格式化为list的单个应用,该应用是我们从表达式中提取的元素表达式的列表。列表构造是文字null或pair的应用,其第二个参数本身是列表构造。
function is_list_construction(exp) {
return (is_literal(exp) && is_null(literal_value(exp))) ||
(is_application(exp) && is_name(function_expression(exp)) &&
symbol_of_name(function_expression(exp)) === "pair" &&
is_list_construction(head(tail(arg_expressions(exp)))));
}
从给定的列表构造中提取元素表达式相当于收集pair的应用的第一个参数,直到达到文字null。
function element_expressions(list_constr) {
return is_literal(list_constr)
? null // list_constr is literal null
: // list_constr is application of pair
pair(head(arg_expressions(list_constr)),
element_expressions(
head(tail(arg_expressions(list_constr)))));
}
查询语言特定表示的谓词和选择器
函数type和contents,由evaluate_query(第 4.4.4.2 节)使用,指定了查询语言特定表示的句法形式由其头部的字符串标识。它们与第 2.4.2 节中的type_tag和contents函数相同,只是错误消息不同。
function type(exp) {
return is_pair(exp)
? head(exp)
: error(exp, "unknown expression type");
}
function contents(exp) {
return is_pair(exp)
? tail(exp)
: error(exp, "unknown expression contents");
}
以下函数由query_driver_loop(第 4.4.4.1 节)使用,指定规则和断言通过assert命令添加到数据库中,函数convert_to_query_syntax将其转换为形式的一对["assert", rule-or-assertion]:
function is_assertion(exp) {
return type(exp) === "assert";
}
function assertion_body(exp) { return head(contents(exp)); }
以下是and、or、not和javascript_predicate语法形式(第 4.4.4.2 节)的谓词和选择器的声明:
function is_empty_conjunction(exps) { return is_null(exps); }
function first_conjunct(exps) { return head(exps); }
function rest_conjuncts(exps) { return tail(exps); }
function is_empty_disjunction(exps) { return is_null(exps); }
function first_disjunct(exps) { return head(exps); }
function rest_disjuncts(exps) { return tail(exps); }
function negated_query(exps) { return head(exps); }
function javascript_predicate_expression(exps) { return head(exps); }
以下三个函数定义了查询语言特定规则的表示:
function is_rule(assertion) {
return is_tagged_list(assertion, "rule");
}
function conclusion(rule) { return head(tail(rule)); }
function rule_body(rule) {
return is_null(tail(tail(rule)))
? list("always_true")
: head(tail(tail(rule)));
}
4.4.4.8 帧和绑定
帧被表示为绑定的列表,绑定是变量-值对:
function make_binding(variable, value) {
return pair(variable, value);
}
function binding_variable(binding) {
return head(binding);
}
function binding_value(binding) {
return tail(binding);
}
function binding_in_frame(variable, frame) {
return assoc(variable, frame);
}
function extend(variable, value, frame) {
return pair(make_binding(variable, value), frame);
}
练习 4.68
路易斯·里森纳想知道为什么simple_query和disjoin函数(第 4.4.4.2 节)是使用延迟表达式实现的,而不是定义如下:
function simple_query(query_pattern, frame_stream) {
return stream_flatmap(
frame =>
stream_append(find_assertions(query_pattern, frame),
apply_rules(query_pattern, frame)),
frame_stream);
}
function disjoin(disjuncts, frame_stream) {
return is_empty_disjunction(disjuncts)
? null
: interleave(
evaluate_query(first_disjunct(disjuncts), frame_stream),
disjoin(rest_disjuncts(disjuncts), frame_stream));
}
您能举例说明这些更简单的定义会导致不良行为的查询吗?
练习 4.69
为什么disjoin和stream_flatmap交错流而不是简单地附加它们?给出说明交错效果更好的例子。 (提示:为什么我们在 3.5.3 节中使用interleave?)
练习 4.70
flatten_stream为什么在其主体中使用延迟表达式?以下定义它会有什么问题:
function flatten_stream(stream) {
return is_null(stream)
? null
: interleave(head(stream),
flatten_stream(stream_tail(stream)));
}
练习 4.71
Alyssa P. Hacker 建议在negate,javascript_predicate和find_assertions中使用stream_flatmap的简化版本。她观察到在这些情况下映射到帧流中的函数总是产生空流或单例流,因此在组合这些流时不需要交错。
-
a. 填写 Alyssa 程序中的缺失表达式。
function simple_stream_flatmap(fun, s) { return simple_flatten(stream_map(fun, s)); } function simple_flatten(stream) { return stream_map(〈??〉, stream_filter(〈??〉, stream)); } -
b. 如果我们以这种方式改变它,查询系统的行为会改变吗?
练习 4.72
为查询语言实现一个名为unique的语法形式。unique的应用应该成功,如果数据库中满足指定查询的项目恰好有一个。例如,
unique(job($x, list("computer", "wizard")))
应该打印一个项目流
unique(job(list("Bitdiddle", "Ben"), list("computer", "wizard")))
由于 Ben 是唯一的计算机巫师,而
unique(job($x, list("computer", "programmer")))
应该打印空流,因为有不止一个计算机程序员。此外,
and(job($x, $j), unique(job($anyone, $j)))
应列出所有只由一个人填写的工作以及填写它们的人。
实现unique有两个部分。第一部分是编写处理这种语法形式的函数,第二部分是使evaluate_query分派到该函数。第二部分是微不足道的,因为evaluate_query以数据导向的方式进行分派。如果您的函数被称为uniquely_asserted,您只需要
put("unique", "evaluate_query", uniquely_asserted);
和evaluate_query将为每个type(头)为字符串"unique"的查询分派到此函数。
真正的问题是编写函数uniquely_asserted。这应该将unique查询的contents(尾部)作为输入,以及一系列帧的流。对于流中的每个帧,它应该使用evaluate_query来查找满足给定查询的所有扩展帧的流。任何流中不恰好有一个项目的流都应该被消除。剩下的流应该被传回来累积成一个大流,这是unique查询的结果。这类似于not语法形式的实现。
通过形成一个列出监督恰好一个人的所有人的查询来测试您的实现。
练习 4.73
我们将and的实现作为查询的系列组合(图 4.6)是优雅的,但效率低下,因为在处理and的第二个查询时,我们必须为第一个查询产生的每个帧扫描数据库。如果数据库有N个元素,并且典型查询产生的输出帧数量与N成比例(比如N / k),那么为了处理第一个查询产生的每个帧,需要N²/k次模式匹配器调用。另一种方法是分别处理and的两个子句,然后寻找所有兼容的输出帧对。如果每个查询产生N / k个输出帧,那么这意味着我们必须执行N²/k²个兼容性检查——比我们当前方法所需的匹配次数少k倍。
设计一个使用这种策略的and的实现。您必须实现一个函数,该函数以两个帧作为输入,检查帧中的绑定是否兼容,如果是,则生成一个合并两组绑定的帧。此操作类似于统一。
练习 4.74
在第 4.4.3 节中,我们看到not和javascript_predicate可能会导致查询语言在应用到变量未绑定的帧时给出“错误”的答案。想出一种修复这个缺点的方法。一个想法是通过向帧附加一个“承诺”来进行“延迟”过滤,只有当足够的变量被绑定时才能实现过滤操作。我们可以等到执行所有其他操作之后再执行过滤。然而,出于效率的考虑,我们希望尽快进行过滤,以减少生成的中间帧的数量。
练习 4.75
将查询语言重新设计为一个非确定性程序,以使用第 4.3 节的求值器来实现,而不是作为一个流程过程。在这种方法中,每个查询将产生一个单一的答案(而不是所有答案的流),用户可以输入retry来查看更多答案。你会发现,我们在本节构建的许多机制都被非确定性搜索和回溯所包含。然而,你可能也会发现,你的新查询语言在行为上有微妙的差异。你能找到一些例子来说明这种差异吗?
练习 4.76
当我们在第 4.1 节实现 JavaScript 求值器时,我们看到了如何使用局部环境来避免函数参数之间的名称冲突。例如,在求值中
function square(x) {
return x * x;
}
function sum_of_squares(x, y) {
return square(x) + square(y);
}
sum_of_squares(3, 4);
在square和sum_of_squares中的x之间没有混淆,因为我们在一个特别构造的环境中求值每个函数的主体,该环境包含局部名称的绑定。在查询系统中,我们使用了一种不同的策略来避免在应用规则时出现名称冲突。每次应用规则时,我们都会使用新的名称对变量进行重命名,这些名称保证是唯一的。对于 JavaScript 求值器的类似策略将是放弃局部环境,而是在应用函数时每次重命名函数的主体中的变量。
为查询语言实现一个使用环境而不是重命名的规则应用方法。看看你是否可以在你的环境结构上构建,以创建处理大型系统的查询语言构造,比如块结构函数的规则类比。你能把这些与在特定上下文中进行推理(例如,“如果我假设P是真的,那么我就能推断出A和B。”)作为解决问题的方法联系起来吗?(这个问题是开放式的。)
五、使用寄存器机进行计算
译者:飞龙
我的目标是表明天堂机器不是一种神圣的生命体,而是一种钟表(相信钟表有灵魂属性的人将制造者的荣耀归功于作品),因为几乎所有多种运动都是由一种最简单和物质力量引起的,就像钟表的所有运动都是由单一重力引起的。
——约翰内斯·开普勒(致赫尔瓦特·冯·霍恩堡的信,1605 年)
我们开始这本书是通过研究过程,并通过用 JavaScript 编写的函数来描述过程。为了解释这些函数的含义,我们使用了一系列的求值模型:第 1 章的替换模型,第 3 章的环境模型,以及第 4 章的元循环求值器。我们对元循环求值器的研究,特别是消除了 JavaScript 类似语言如何解释的许多神秘。但是,即使元循环求值器也留下了一些重要的问题没有解答,因为它未能阐明 JavaScript 系统中的控制机制。例如,求值器没有解释子表达式的求值如何返回一个值给使用这个值的表达式。此外,求值器也没有解释一些递归函数如何生成迭代过程(即使用恒定空间进行求值),而其他递归函数将生成递归过程。本章将解决这两个问题。
我们将描述过程,以传统计算机的逐步操作为基础。这样的计算机,或者寄存器机,顺序执行操作指令,这些指令操作固定一组称为寄存器的存储元素的内容。典型的寄存器机指令将原始操作应用于一些寄存器的内容,并将结果分配给另一个寄存器。我们对寄存器机执行的过程的描述看起来非常像传统计算机的“机器语言”程序。但是,我们不会专注于任何特定计算机的机器语言,而是会检查几个 JavaScript 函数,并设计一个特定的寄存器机来执行每个函数。因此,我们将从硬件架构师的角度而不是机器语言计算机程序员的角度来处理我们的任务。在设计寄存器机时,我们将开发用于实现重要编程构造(如递归)的机制。我们还将提供一种描述寄存器机设计的语言。在第 5.2 节中,我们将实现一个使用这些描述来模拟我们设计的机器的 JavaScript 程序。
我们的寄存器机的大多数原始操作都非常简单。例如,一个操作可能会将从两个寄存器中获取的数字相加,产生一个结果存储到第三个寄存器中。这样的操作可以通过简单描述的硬件来执行。然而,为了处理列表结构,我们还将使用head、tail和pair的内存操作,这需要一个复杂的存储分配机制。在第 5.3 节中,我们将研究它们的实现,以更基本的操作为基础。
在第 5.4 节中,当我们积累了将简单函数表述为寄存器机的经验后,我们将设计一台机器,执行第 4.1 节中元循环求值器描述的算法。这将填补我们对 JavaScript 程序如何解释的理解中的空白,通过为求值器中的控制机制提供一个明确的模型。在第 5.5 节中,我们将研究一个简单的编译器,将 JavaScript 程序转换为可以直接使用求值器寄存器机的寄存器和操作执行的指令序列。
5.1 设计寄存器机
设计一个寄存器机器,我们必须设计它的数据路径(寄存器和操作)和控制器,以便对这些操作进行排序。为了说明一个简单寄存器机器的设计,让我们来看一下欧几里得算法,它用于计算两个整数的最大公约数(GCD)。正如我们在 1.2.5 节中看到的,欧几里得算法可以通过迭代过程来执行,如下函数所示:
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}
执行此算法的机器必须跟踪两个数字a和b,因此让我们假设这些数字存储在两个具有这些名称的寄存器中。所需的基本操作是测试寄存器b的内容是否为零,并计算寄存器a的内容除以寄存器b的余数。余数操作是一个复杂的过程,但暂时假设我们有一个计算余数的原始设备。在 GCD 算法的每个周期中,寄存器a的内容必须被寄存器b的内容替换,b的内容必须被a的旧内容除以b的旧内容的余数替换。如果这些替换可以同时进行将会很方便,但在我们的寄存器机器模型中,我们假设每次只能为一个寄存器分配一个新值。为了完成这些替换,我们的机器将使用第三个“临时”寄存器,我们称之为t。(首先余数将被放置在t中,然后b的内容将被放置在a中,最后存储在t中的余数将被放置在b中。)
我们可以通过使用图 5.1 中显示的数据路径图来说明此机器所需的寄存器和操作。在此图中,寄存器(a、b和t)由矩形表示。将值分配给寄存器的每种方法都由一个带有按钮的箭头表示——绘制为——在箭头头部后面,从数据源指向寄存器。按下按钮时,允许数据源的值“流”入指定的寄存器。每个按钮旁边的标签是我们将用来引用按钮的名称。这些名称是任意的,可以选择具有助记值(例如,a<-b表示按下将寄存器b的内容分配给寄存器a的按钮)。寄存器的数据源可以是另一个寄存器(如a<-b分配中),操作结果(如t<-r分配中),或者是一个常数(一个无法更改的内置值,在数据路径图中由包含常数的三角形表示)。

图 5.1 GCD 机器的数据路径。
计算常数和寄存器内容的值的操作在数据路径图中由一个梯形表示,其中包含操作的名称。例如,图 5.1 中标记为rem的框表示一个计算所附寄存器a和b的内容的余数的操作。箭头(没有按钮)从输入寄存器和常数指向框,箭头将操作的输出值连接到寄存器。测试由包含测试名称的圆圈表示。例如,我们的 GCD 机器有一个测试操作,用于测试寄存器b的内容是否为零。测试也有从其输入寄存器和常数的箭头,但它没有输出箭头;它的值由控制器而不是数据路径使用。总的来说,数据路径图显示了机器所需的寄存器和操作,以及它们之间的连接方式。如果我们将箭头视为电线,按钮视为开关,数据路径图就非常像可以由电子元件构建的机器的接线图。
为了使数据路径实际计算 GCD,必须按正确的顺序按下按钮。我们将根据控制器图表描述这个顺序,如图 5.2 所示。控制器图表的元素指示应如何操作数据路径组件。控制器图表中的矩形框标识要按下的数据路径按钮,并且箭头描述从一步到下一步的顺序。图表中的菱形代表一个决定。根据菱形中指定的数据路径测试的值,将遵循两个顺序箭头中的一个。我们可以根据物理类比来解释控制器:将图表视为一个迷宫,弹珠在其中滚动。当弹珠滚入一个框中时,它会按照框的名称按下数据路径按钮。当弹珠滚入决策节点(例如b = 0的测试)时,它会根据指定测试的结果离开节点。

图 5.2 GCD 机器的控制器。
将数据路径和控制器结合起来,完全描述了一个计算 GCD 的机器。我们将控制器(滚动的弹珠)放在标有start的地方,然后在寄存器a和b中放入数字。当控制器到达done时,我们将在寄存器a中找到 GCD 的值。
练习 5.1
设计一个寄存器机器,使用以下函数指定的迭代算法计算阶乘。为这台机器绘制数据路径和控制器图表。
function factorial(n) {
function iter(product, counter) {
return counter > n
? product
: iter(counter * product,
counter + 1);
}
return iter(1, 1);
}
5.1.1 描述寄存器机器的语言
数据路径和控制器图表足以表示诸如 GCD 之类的简单机器,但对于描述 JavaScript 解释器之类的大型机器来说,它们是笨重的。为了能够处理复杂的机器,我们将创建一种以文本形式呈现数据路径和控制器图表中提供的所有信息的语言。我们将从直接反映图表的符号开始。
我们通过描述寄存器和操作来定义机器的数据路径。为了描述一个寄存器,我们给它一个名称,并指定控制分配给它的按钮。我们给每个按钮一个名称,并指定进入由按钮控制的寄存器的数据的来源(来源可以是寄存器、常量或操作)。为了描述一个操作,我们给它一个名称,并指定它的输入(寄存器或常量)。
我们将机器的控制器定义为一系列指令,以及标识序列中入口点的标签。指令可以是以下之一:
-
按下数据路径按钮的名称,以将值分配给寄存器。(这对应于控制器图表中的一个框。)
-
一个
test指令,执行指定的测试。 -
一个条件分支(
branch指令)到控制器标签指示的位置,基于先前测试的结果。(测试和分支一起对应于控制器图表中的菱形。)如果测试为假,则控制器应继续执行序列中的下一条指令。否则,控制器应继续执行标签后的指令。 -
一个无条件分支(
go_to指令),命名控制器标签,以便继续执行。
机器从控制器指令序列的开头开始,并在执行到达序列末尾时停止。除非分支改变了控制流,否则指令将按照它们列出的顺序执行。
图 5.3 显示了以这种方式描述的 GCD 机器。这个例子只是暗示了这些描述的一般性,因为 GCD 机器是一个非常简单的情况:每个寄存器只有一个按钮,并且每个按钮和测试在控制器中只使用一次。

图 5.3 GCD 机器的规范。
不幸的是,阅读这样的描述是困难的。为了理解控制器指令,我们必须不断地参考按钮名称和操作名称的定义,为了理解按钮的功能,我们可能必须参考操作名称的定义。因此,我们将转换我们的符号,将数据路径和控制器描述的信息合并在一起,以便我们可以一起看到。
为了获得这种描述形式,我们将用它们的行为定义替换任意按钮和操作名称。也就是说,我们将说(在控制器中)“按下分配给寄存器t的按钮”并分别说(在数据路径中)“按钮t<-r将rem操作的值分配给寄存器t”和“rem操作的输入是寄存器a和b的内容”,我们将说(在控制器中)“按下将rem操作的值分配给寄存器t的按钮”。类似地,我们将说(在控制器中)“执行=测试”并分别说(在数据路径中)“=测试作用于寄存器b的内容和常量 0”,我们将说“对寄存器b的内容和常量 0 执行=测试”。我们将省略数据路径描述,只保留控制器序列。因此,GCD 机器的描述如下:
controller(
list(
"test_b",
test(list(op("="), reg("b"), constant(0))),
branch(label("gcd_done")),
assign("t", list(op("rem"), reg("a"), reg("b"))),
assign("a", reg("b")),
assign("b", reg("t")),
go_to(label("test_b")),
"gcd_done"))
这种描述形式比图 5.3 中所示的形式更容易阅读,但它也有缺点:
-
对于大型机器来说,它更冗长,因为每当在控制器指令序列中提到元素时,就会重复数据路径元素的完整描述。(这在 GCD 示例中不是问题,因为每个操作和按钮只使用一次。)此外,重复数据路径描述会使机器的实际数据路径结构变得模糊;对于大型机器来说,有多少寄存器、操作和按钮以及它们如何相互连接并不明显。
-
因为机器定义中的控制器指令看起来像 JavaScript 表达式,很容易忘记它们不是任意的 JavaScript 表达式。它们只能表示合法的机器操作。例如,操作只能直接作用于常量和寄存器的内容,而不能作用于其他操作的结果。
尽管存在这些缺点,但在本章中我们将使用这种寄存器机器语言,因为我们更关心理解控制器,而不是理解数据路径中的元素和连接。然而,我们应该记住,数据路径设计在设计真实机器时至关重要。
练习 5.2
使用寄存器机器语言描述练习 5.1 中的迭代阶乘机器。
行动
让我们修改 GCD 机器,以便我们可以输入我们想要的最大公约数的数字并打印答案。我们不会讨论如何制作一个可以读取和打印的机器,而是假设(就像我们在 JavaScript 中使用prompt和display时一样)它们作为原始操作是可用的。
prompt操作类似于我们一直在使用的操作,因为它产生一个可以存储在寄存器中的值。但prompt不从任何寄存器中获取输入;它的值取决于我们设计的机器之外发生的事情。
我们将允许我们机器的操作具有这样的行为,并且将绘制和标注prompt的使用,就像我们对任何计算值的其他操作一样。
另一方面,display操作在根本上与我们一直在使用的操作不同:它不会产生要存储在寄存器中的输出值。 尽管它有一个效果,但这个效果不是在我们设计的机器的一部分上。 我们将这种操作称为动作。 我们将在数据路径图中表示动作,就像我们表示计算值的操作一样 - 作为一个包含动作名称的梯形。 箭头从任何输入(寄存器或常数)指向动作框。 我们还将一个按钮与动作关联起来。 按下按钮会使动作发生。 为了使控制器按下动作按钮,我们使用一种称为perform的新类型指令。 因此,打印寄存器a的内容的动作在控制器序列中表示为指令
perform(list(op("display"), reg("a")))
图 5.4 显示了新 GCD 机器的数据路径和控制器。 与其在打印答案后停止,我们让它重新开始,以便它反复读取一对数字,计算它们的 GCD,并打印结果。 这种结构类似于我们在第 4 章解释器中使用的驱动循环。

图 5.4 一个读取输入并打印结果的 GCD 机器。
5.1.2 机器设计中的抽象
我们经常定义一个机器包括实际上非常复杂的“原始”操作。 例如,在第 5.4 节和 5.5 节中,我们将把 JavaScript 的环境操作视为原始操作。 这种抽象是有价值的,因为它使我们能够忽略机器的某些部分的细节,以便我们可以集中精力处理设计的其他方面。 然而,我们将大量复杂性隐藏起来,并不意味着机器设计是不切实际的。 我们总是可以用更简单的原始操作来替换复杂的“原始”。
考虑 GCD 机器。 该机器具有一个指令,计算寄存器a和b的内容的余数,并将结果赋给寄存器t。 如果我们想要构建 GCD 机器而不使用原始的余数运算,我们必须指定如何通过更简单的操作(如减法)来计算余数。 实际上,我们可以编写一个 JavaScript 函数以这种方式找到余数:
function remainder(n, d) {
return n < d
? n
: remainder(n - d, d);
}
因此,我们可以用减法和比较测试来替换 GCD 机器数据路径中的余数运算。 图 5.5 显示了详细机器的数据路径和控制器。指令
assign("t", list(op("rem"), reg("a"), reg("b")))
在 GCD 控制器定义中被替换为包含循环的一系列指令,如图 5.6 所示。

图 5.5 详细 GCD 机器的数据路径和控制器。

图 5.6 GCD 机器的控制器指令序列,如图 5.5 所示。
练习 5.3
设计一个使用牛顿法计算平方根的机器,如第 1.1.7 节中描述的,并在第 1.1.8 节中用以下代码实现:
function sqrt(x) {
function is_good_enough(guess) {
return math_abs(square(guess) - x) < 0.001;
}
function improve(guess) {
return average(guess, x / guess);
}
function sqrt_iter(guess) {
return is_good_enough(guess)
? guess
: sqrt_iter(improve(guess));
}
return sqrt_iter(1);
}
首先假设is_good_enough和improve操作作为原始操作可用。 然后展示如何通过算术操作来扩展这些操作。 通过绘制数据路径图和用寄存器机器语言编写控制器定义,描述每个sqrt机器设计的版本。
5.1.3 子程序
设计执行计算的机器时,我们通常希望安排组件被不同部分的计算共享,而不是复制组件。考虑一个包括两个 GCD 计算的机器——一个是寻找寄存器a和b中内容的 GCD,另一个是寻找寄存器c和d中内容的 GCD。我们可能首先假设有一个原始的gcd操作,然后用更原始的操作来扩展两个gcd的实例。图 5.7 仅显示了结果机器数据路径的 GCD 部分,而没有显示它们如何连接到机器的其余部分。该图还显示了机器控制器序列的相应部分。

图 5.7 具有两个 GCD 计算的机器的数据路径和控制器序列的部分。
这台机器有两个余数运算框和两个用于测试相等性的框。如果复制的组件很复杂,比如余数框,这将不是一种经济的建造机器的方式。我们可以通过使用相同的组件来避免复制数据路径组件进行两个 GCD 计算,只要这样做不会影响较大机器的其余计算。如果寄存器a和b中的值在控制器到达gcd_2时不再需要(或者这些值可以移动到其他寄存器以供安全保管),我们可以更改机器,使其在计算第二个 GCD 时使用寄存器a和b,而不是寄存器c和d。如果这样做,我们将获得图 5.8 所示的控制器序列。

图 5.8 使用相同的数据路径组件进行两个不同的 GCD 计算的机器的控制器序列的部分。
我们已经删除了重复的数据路径组件(使数据路径再次如图 5.1 所示),但是控制器现在有两个仅在它们的入口点标签上不同的 GCD 序列。最好用单个序列的分支替换这两个序列——一个gcd子程序——在该子程序的末尾我们再次分支到主指令序列中的正确位置。我们可以通过以下方式实现这一点:在分支到gcd之前,我们将一个区分值(如 0 或 1)放入特殊寄存器continue。在gcd子程序结束时,根据continue寄存器的值,我们返回到after_gcd_1或after_gcd_2。图 5.9 显示了结果控制器序列的相关部分,其中仅包括gcd指令的单个副本。

图 5.9 使用continue寄存器避免图 5.8 中重复的控制器序列。
这是处理小问题的合理方法,但如果控制器序列中有许多 GCD 计算的实例,这将是很笨拙的。为了决定在gcd子程序之后继续执行的位置,我们需要在数据路径中进行测试,并在控制器中为所有使用gcd的地方添加分支指令。实现子程序的更强大方法是使continue寄存器保存控制器序列中执行完成后应继续执行的入口点的标签。实现这种策略需要寄存器机器的数据路径和控制器之间的一种新连接:必须有一种方法将标签分配给寄存器,以便可以从寄存器中获取此值,并用于在指定的入口点继续执行。
为了反映这种能力,我们将扩展寄存器机器语言的assign指令,允许将寄存器分配为控制器序列中标签的值(作为一种特殊类型的常量)。我们还将扩展go_to指令,允许执行继续在寄存器的内容描述的入口点处继续,而不仅仅是在常量标签描述的入口点处。使用这些新构造,我们可以通过分支到continue寄存器中存储的位置来终止gcd子程序。这导致了图 5.10 中显示的控制器序列。

图 5.10 将标签分配给continue寄存器简化并概括了图 5.9 中显示的策略。
具有多个子例程的机器可以使用多个继续寄存器(例如gcd_continue,factorial_continue),或者我们可以让所有子例程共享一个continue寄存器。共享更经济,但是如果我们有一个子例程(sub1)调用另一个子例程(sub2),我们必须小心。除非sub1在设置continue以调用sub2之前将continue的内容保存在其他寄存器中,否则sub1完成时将不知道要去哪里。下一节中开发的处理递归的机制也提供了解决嵌套子例程调用问题的更好解决方案。
5.1.4 使用堆栈实现递归
到目前为止,我们所展示的思想可以通过指定具有与过程的每个状态变量对应的寄存器的寄存器机器来实现任何迭代过程。该机器重复执行控制器循环,改变寄存器的内容,直到满足某个终止条件。在控制器序列的每一点上,机器的状态(表示迭代过程的状态)完全由寄存器的内容(状态变量的值)确定。
然而,实现递归过程需要额外的机制。考虑以下用于计算阶乘的递归方法,我们在 1.2.1 节中首次研究了这个方法:
function factorial(n) {
return n === 1
? 1
: n * factorial(n - 1);
}
从函数中我们可以看到,计算n!需要计算(n – 1)!. 我们的 GCD 机器,模拟了函数
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}
同样需要计算另一个 GCD。但是gcd函数和factorial之间有一个重要的区别,gcd函数将原始计算减少为新的 GCD 计算,而factorial需要计算另一个阶乘作为子问题。在 GCD 中,新 GCD 计算的答案是原始问题的答案。要计算下一个 GCD,我们只需将新参数放入 GCD 机器的输入寄存器中,并通过执行相同的控制器序列重用机器的数据路径。当机器完成解决最终的 GCD 问题时,它已经完成了整个计算。
在阶乘(或任何递归过程)的情况下,新阶乘子问题的答案不是原始问题的答案。必须将(n – 1)!的值乘以n才能得到最终答案。如果我们试图模仿 GCD 设计,并通过减少n寄存器并重新运行阶乘机器来解决阶乘子问题,我们将不再拥有旧值n以便将结果相乘。因此,我们需要第二个阶乘机器来处理
子问题。这第二个阶乘计算本身有一个阶乘子问题,需要第三个阶乘机器,依此类推。由于每个阶乘机器中包含另一个阶乘机器,因此总机器包含无限数量的类似机器的嵌套,因此无法从固定的有限数量的部分构建。
然而,如果我们能够安排在机器的每个嵌套实例中使用相同的组件,我们就可以将阶乘过程实现为一个寄存器机。具体来说,计算n!的机器应该使用相同的组件来处理计算(n – 1)!的子问题,以及(n – 2)!的子问题,依此类推。这是合理的,因为阶乘过程规定需要无限数量的相同机器的副本来执行计算,但在任何给定时间只有一个副本需要处于活动状态。当机器遇到递归子问题时,它可以暂停主问题的工作,重复使用相同的物理部件来处理子问题,然后继续暂停的计算。
在子问题中,寄存器的内容将与主问题中的内容不同。(在这种情况下,n寄存器被递减。)为了能够继续暂停的计算,机器必须保存任何在解决子问题后将需要的寄存器的内容,以便在解决子问题后恢复这些内容以继续暂停的计算。在阶乘的情况下,我们将保存n的旧值,在完成对递减的n寄存器的阶乘计算后将其恢复。
由于嵌套递归调用的深度没有先验限制,我们可能需要保存任意数量的寄存器值。这些值必须以它们被保存的相反顺序进行恢复,因为在递归的嵌套中,最后进入的子问题是第一个完成的。这决定了使用“栈”或“后进先出”数据结构来保存寄存器值。我们可以通过添加两种指令来扩展寄存器机器语言以包括一个栈:使用save指令将值放入栈中,并使用restore指令从栈中恢复值。在一系列值被保存到栈上后,一系列restore将以相反的顺序检索这些值。
借助栈的帮助,我们可以为每个阶乘子问题重复使用阶乘机器的数据路径的单个副本。在重用操作数据路径的控制器序列方面存在类似的设计问题。为了重新执行阶乘计算,控制器不能简单地回到开始,因为在解决(n – 1)!子问题后,机器仍然必须将结果乘以n。控制器必须暂停计算n!,解决(n – 1)!子问题,然后继续计算n!。阶乘计算的这种观点表明了在 5.1.3 节中描述的子程序机制的使用,其中控制器使用continue寄存器来转移到解决子问题的序列的部分,然后继续在主问题上离开的地方。因此,我们可以制作一个返回到存储在continue寄存器中的入口点的阶乘子程序。在每个子程序调用周围,我们保存和恢复continue,就像我们对n寄存器做的那样,因为阶乘计算的每个“级别”将使用相同的continue寄存器。也就是说,阶乘子程序在调用自身解决子问题时必须在continue中放入一个新值,但为了返回到调用它解决子问题的地方,它将需要旧值。
图 5.11 显示了实现递归factorial函数的机器的数据路径和控制器。该机器有一个堆栈和三个寄存器,称为n,val和continue。为了简化数据路径图,我们没有命名寄存器分配按钮,只有堆栈操作按钮(sc和sn用于保存寄存器,rc和rn用于恢复寄存器)。要操作这台机器,我们将要计算阶乘的数放入寄存器n中并启动机器。当机器到达fact_done时,计算完成,答案将在val寄存器中找到。在控制器序列中,每次递归调用之前都会保存n和continue,并在调用返回时恢复。从调用返回是通过跳转到continue中存储的位置来实现的。机器启动时会初始化continue寄存器,以便最后的返回将到达fact_done。val寄存器保存了阶乘计算的结果,不会在递归调用之前保存,因为在子程序返回后,旧的val内容是没有用的。只有新值,也就是子计算产生的值,是需要的。

图 5.11 递归阶乘机器。
尽管原则上阶乘计算需要一个无限的机器,但是图 5.11 中的机器实际上是有限的,除了堆栈,堆栈可能是无限的。然而,任何特定的堆栈物理实现都将是有限大小的,这将限制机器可以处理的递归调用的深度。阶乘的这种实现说明了将递归算法实现为普通寄存器机器加上堆栈的一般策略。当遇到递归子问题时,我们在堆栈上保存当前值将在解决子问题后需要的寄存器,解决递归子问题,然后恢复保存的寄存器并继续在主问题上执行。continue寄存器必须始终保存。是否需要保存其他寄存器取决于特定的机器,因为并非所有递归计算都需要在解决子问题时修改的寄存器的原始值(参见练习 5.4)。
双重递归
让我们来看一个更复杂的递归过程,即我们在 1.2.2 节中介绍的树递归计算斐波那契数:
function fib(n) {
return n === 0
? 0
: n === 1
? 1
: fib(n - 1) + fib(n - 2);
}
就像阶乘一样,我们可以使用寄存器机器来实现递归斐波那契计算,其中有寄存器n,val和continue。这台机器比阶乘的机器更复杂,因为在控制器序列中有两个地方需要进行递归调用——一次是计算Fib(n – 1),一次是计算Fib(n – 2)。为了为每个调用设置准备,我们保存将来需要的寄存器的值,将n寄存器设置为需要递归计算的数(n – 1或n – 2),并将continue分配给主序列中的入口点以便返回(分别是afterfib_n_1或afterfib_n_2)。然后我们进入fib_loop。当我们从递归调用返回时,答案在val中。图 5.12 显示了这台机器的控制器序列。

图 5.12 计算斐波那契数的机器的控制器。
练习 5.4
指定实现以下每个函数的寄存器机器。对于每台机器,编写一个控制器指令序列,并绘制一个显示数据路径的图。
-
a. 递归指数运算:
function expt(b, n) { return n === 0 ? 1 : b * expt(b, n - 1); } -
b. 迭代指数运算:
function expt(b, n) { function expt_iter(counter, product) { return counter === 0 ? product : expt_iter(counter - 1, b * product); } return expt_iter(n, 1); }
练习 5.5
手动模拟阶乘和斐波那契机器,使用一些复杂的输入(需要执行至少一个递归调用)。显示执行中每个重要点的堆栈内容。
练习 5.6
Ben Bitdiddle 观察到斐波那契机器的控制器序列有额外的save和额外的restore,可以删除以使机器更快。这些指令在哪里?
5.1.5 指令摘要
我们的寄存器机器语言中的控制器指令具有以下形式之一,其中每个input[i]是reg(register-name)或constant(constant-value)。
这些指令是在 5.1.1 节中引入的:
assign(register-name, reg(register-name))
assign(register-name, constant(constant-value))
assign(register-name, list(op(operation-name), input[1], ..., input[n]))
perform(list(op(operation-name), input[1], ..., input[n]))
test(list(op(operation-name), input[1], ..., input[n]))
branch(label(label-name))
go_to(label(label-name))
使用寄存器保存标签是在 5.1.3 节中引入的:
assign(register-name, label(label-name))
go_to(reg(register-name))
使用堆栈的指令是在 5.1.4 节中引入的:
save(register-name)
restore(register-name)
到目前为止,我们看到的唯一类型的constant-value是一个数字,但稍后我们还将使用字符串和列表。例如,constant("abc")是字符串"abc",constant(null)是空列表,constant(list("a", "b", "c"))是列表list("a", "b", "c")。
5.2 寄存器机器模拟器
为了更好地理解寄存器机器的设计,我们必须测试我们设计的机器,以查看它们是否按预期运行。测试设计的一种方法是手动模拟控制器的操作,就像练习 5.5 中那样。但是,除了最简单的机器外,这种方法非常乏味。在本节中,我们构建了一个模拟器,用于模拟寄存器机器语言描述的机器。该模拟器是一个 JavaScript 程序,具有四个接口函数。第一个使用寄存器机器的描述来构建机器的模型(一个数据结构,其部分对应于要模拟的机器的部分),另外三个允许我们通过操作模型来模拟机器:
-
make_machine(register-names, operations, controller)构建并返回具有给定寄存器、操作和控制器的机器模型。
-
set_register_contents(machine-model, register-name, value)在给定机器中的模拟寄存器中存储一个值。
-
get_register_contents(machine-model, register-name)返回给定机器中模拟寄存器的内容。
-
start(machine-model)模拟给定机器的执行,从控制器序列的开头开始,直到到达序列的末尾。
作为这些函数如何使用的示例,我们可以定义gcd_machine为 5.1.1 节中 GCD 机器的模型,如下所示:
const gcd_machine =
make_machine(
list("a", "b", "t"),
list(list("rem", (a, b) => a % b),
list("=", (a, b) => a === b)),
list(
"test_b",
test(list(op("="), reg("b"), constant(0))),
branch(label("gcd_done")),
assign("t", list(op("rem"), reg("a"), reg("b"))),
assign("a", reg("b")),
assign("b", reg("t")),
go_to(label("test_b")),
"gcd_done"));
make_machine的第一个参数是一个寄存器名称列表。下一个参数是一个表(包含两个元素列表的列表),将每个操作名称与实现该操作的 JavaScript 函数配对(即,给定相同的输入值产生相同的输出值)。最后一个参数指定控制器,格式为标签和机器指令的列表,就像 5.1 节中的格式。
要使用这台机器计算 GCD,我们设置输入寄存器,启动机器,并在模拟终止时检查结果:
set_register_contents(gcd_machine, "a", 206);
"done"
set_register_contents(gcd_machine, "b", 40);
"done"
start(gcd_machine);
"done"
get_register_contents(gcd_machine, "a");
`2`
这个计算将比用 JavaScript 编写的gcd函数运行得慢得多,因为我们将模拟低级机器指令,比如assign,通过更复杂的操作。
练习 5.7
使用模拟器测试您在练习 5.4 中设计的机器。
5.2.1 机器模型
make_machine生成的机器模型表示为使用消息传递技术在第 3 章中开发的本地状态的函数。为了构建这个模型,make_machine首先调用函数make_new_machine来构造所有寄存器机器共有的部分。由make_new_machine构建的基本机器模型本质上是一种包含一些寄存器和堆栈的容器,以及一个执行机制,逐个处理控制器指令。
然后函数make_machine扩展了这个基本模型(通过向其发送消息)以包括所定义的特定机器的寄存器、操作和控制器。首先,它为新机器中提供的每个寄存器名称分配一个寄存器,并在机器中安装指定的操作。然后,它使用一个汇编器(在第 5.2.2 节中描述)将控制器列表转换为新机器的指令,并将其安装为机器的指令序列。函数make_machine返回修改后的机器模型作为其值。
function make_machine(register_names, ops, controller) {
const machine = make_new_machine();
for_each(register_name =>
machine("allocate_register")(register_name),
register_names);
machine("install_operations")(ops);
machine("install_instruction_sequence")
(assemble(controller, machine));
return machine;
}
寄存器
我们将寄存器表示为具有局部状态的函数,就像第 3 章中一样。函数make_register创建一个可以访问或更改值的寄存器:
function make_register(name) {
let contents = "unassigned";
function dispatch(message) {
return message === "get"
? contents
: message === "set"
? value => { contents = value; }
: error(message, "unknown request – make_register");
}
return dispatch;
}
以下函数用于访问寄存器:
function get_contents(register) {
return register("get");
}
function set_contents(register, value) {
return register("set")(value);
}
栈
我们也可以将栈表示为具有局部状态的函数。函数make_stack创建一个栈,其局部状态包括栈上项目的列表。栈接受请求将项目push到栈上,pop弹出栈顶项目并返回它,以及initialize将栈初始化为空。
function make_stack() {
let stack = null;
function push(x) {
stack = pair(x, stack);
return "done";
}
function pop() {
if (is_null(stack)) {
error("empty stack – pop");
} else {
const top = head(stack);
stack = tail(stack);
return top;
}
}
function initialize() {
stack = null;
return "done";
}
function dispatch(message) {
return message === "push"
? push
: message === "pop"
? pop()
: message === "initialize"
? initialize()
: error(message, "unknown request – stack");
}
return dispatch;
}
以下函数用于访问栈:
function pop(stack) {
return stack("pop");
}
function push(stack, value) {
return stack("push")(value);
}
基本机器
make_new_machine函数,如图 5.13 所示,构造了一个对象,其局部状态包括一个栈、一个最初为空的指令序列、一个最初包含一个初始化栈操作的操作列表,以及一个寄存器表,最初包含两个寄存器,名为flag和pc(代表“程序计数器”)。内部函数allocate_register添加新条目到寄存器表中,内部函数lookup_register在表中查找寄存器。

图 5.13 make_new_machine函数实现了基本的机器模型。
flag寄存器用于控制模拟机器中的分支。我们的test指令将flag的内容设置为测试的结果(真或假)。我们的branch指令通过检查flag的内容来决定是否进行分支。
pc寄存器确定指令在机器运行时的顺序。这种顺序由内部函数execute实现。在模拟模型中,每条机器指令都是一个数据结构,其中包括一个没有参数的函数,称为指令执行函数,调用这个函数模拟执行指令。随着模拟的运行,pc指向指令序列中下一条要执行的指令的位置。函数execute获取该指令,通过调用指令执行函数来执行它,并重复这个循环,直到没有更多的指令需要执行(即,直到pc指向指令序列的末尾)。
作为其操作的一部分,每个指令执行函数修改pc以指示下一个要执行的指令。branch和go_to指令将pc更改为指向新的目的地。所有其他指令只是推进pc,使其指向序列中的下一条指令。请注意,每次调用execute都会再次调用execute,但这不会产生无限循环,因为运行指令执行函数会改变pc的内容。
函数make_new_machine返回一个分发函数,实现对内部状态的消息传递访问。请注意,启动机器是通过将pc设置为指令序列的开头并调用execute来完成的。
为了方便起见,我们提供了一个机器的start操作的替代接口,以及用于设置和检查寄存器内容的函数,如第 5.2 节开头所述:
function start(machine) {
return machine("start");
}
function get_register_contents(machine, register_name) {
return get_contents(get_register(machine, register_name));
}
function set_register_contents(machine, register_name, value) {
set_contents(get_register(machine, register_name), value);
return "done";
}
这些函数(以及第 5.2.2 和 5.2.3 节中的许多函数)使用以下内容来查找给定机器中具有给定名称的寄存器:
function get_register(machine, reg_name) {
return machine("get_register")(reg_name);
}
5.2.2 汇编器
汇编器将机器的控制器指令序列转换为相应的机器指令列表,每个指令都有其执行函数。总的来说,汇编器很像我们在第 4 章中学习的求值器——它有一个输入语言(在本例中是寄存器机器语言),我们必须对语言中的每种组件执行适当的操作。
为每条指令生成一个执行函数的技术正是我们在 4.1.7 节中用来通过将分析与运行时执行分离来加速求值器的技术。正如我们在第 4 章中看到的,可以在不知道名称的实际值的情况下执行对 JavaScript 表达式的有用分析。在这里,类似地,可以在不知道机器寄存器的实际内容的情况下执行对寄存器机器语言表达式的有用分析。例如,我们可以用指向寄存器对象的指针来替换对寄存器的引用,并且可以用指向标签在指令序列中指定位置的指针来替换对标签的引用。
在生成指令执行函数之前,汇编器必须知道所有标签的引用,因此它首先通过扫描控制器序列来将标签与指令分离。在扫描控制器时,它同时构造了指令列表和一个将每个标签与指向该列表中的指针关联起来的表。然后汇编器通过为每条指令插入执行函数来增强指令列表。
assemble函数是汇编器的主要入口。它接受控制器序列和机器模型作为参数,并返回要存储在模型中的指令序列。assemble函数调用extract_labels来从提供的控制器构建初始指令列表和标签表。extract_labels的第二个参数是一个函数,用于处理这些结果:该函数使用update_insts生成指令执行函数并将其插入指令列表,然后返回修改后的列表。
function assemble(controller, machine) {
return extract_labels(controller,
(insts, labels) => {
update_insts(insts, labels, machine);
return insts;
});
}
extract_labels函数接受一个名为controller的列表和一个名为receive的函数作为参数。函数receive将被调用并传入两个值:(1)一个名为insts的指令数据结构列表,其中包含来自controller的指令;和(2)一个名为labels的表,它将controller中的每个标签与其指定的insts列表中的位置关联起来。
function extract_labels(controller, receive) {
return is_null(controller)
? receive(null, null)
: extract_labels(
tail(controller),
(insts, labels) => {
const next_element = head(controller);
return is_string(next_element)
? receive(insts,
pair(make_label_entry(next_element,
insts),
labels))
: receive(pair(make_inst(next_element),
insts),
labels);
});
}
extract_labels函数通过顺序扫描controller的元素并累积insts和labels来工作。如果一个元素是字符串(因此是标签),则将适当的条目添加到labels表中。否则,该元素将被累积到insts列表中。
update_insts函数修改了指令列表,该列表最初只包含控制器指令,以包括相应的执行函数:
function update_insts(insts, labels, machine) {
const pc = get_register(machine, "pc");
const flag = get_register(machine, "flag");
const stack = machine("stack");
const ops = machine("operations");
return for_each(inst => set_inst_execution_fun(
inst,
make_execution_function(
inst_controller_instruction(inst),
labels, machine, pc,
flag, stack, ops)),
insts);
}
机器指令数据结构简单地将控制器指令与相应的执行函数配对。当extract_labels构造指令时,执行函数尚不可用,而是稍后由update_insts插入。
function make_inst(inst_controller_instruction) {
return pair(inst_controller_instruction, null);
}
function inst_controller_instruction(inst) {
return head(inst);
}
function inst_execution_fun(inst) {
return tail(inst);
}
function set_inst_execution_fun(inst, fun) {
set_tail(inst, fun);
}
我们的模拟器不使用控制器指令,但保留它以便进行调试(参见练习 5.15)。
标签表的元素是成对出现的:
function make_label_entry(label_name, insts) {
return pair(label_name, insts);
}
表中的条目将使用查找。
function lookup_label(labels, label_name) {
const val = assoc(label_name, labels);
return is_undefined(val)
? error(label_name, "undefined label – assemble")
: tail(val);
}
练习 5.8
以下寄存器机器代码是模棱两可的,因为标签here被定义了多次:
"start",
go_to(label("here")),
"here",
assign("a", constant(3)),
go_to(label("there")),
"here",
assign("a", constant(4)),
go_to(label("there")),
"there",
按照目前的模拟器,当控制到达there时,寄存器a的内容将是什么?修改extract_labels函数,使得汇编器在使用相同的标签名称指示两个不同位置时会发出错误信号。
5.2.3 指令及其执行函数
汇编器调用make_execution_function为控制器指令生成执行函数。就像第 4.1.7 节的求值器中的analyze函数一样,这根据指令类型分发以生成适当的执行函数。这些执行函数的细节决定了寄存器机器语言中各个指令的含义。
function make_execution_function(inst, labels, machine,
pc, flag, stack, ops) {
const inst_type = type(inst);
return inst_type === "assign"
? make_assign_ef(inst, machine, labels, ops, pc)
: inst_type === "test"
? make_test_ef(inst, machine, labels, ops, flag, pc)
: inst_type === "branch"
? make_branch_ef(inst, machine, labels, flag, pc)
: inst_type === "go_to"
? make_go_to_ef(inst, machine, labels, pc)
: inst_type === "save"
? make_save_ef(inst, machine, stack, pc)
: inst_type === "restore"
? make_restore_ef(inst, machine, stack, pc)
: inst_type === "perform"
? make_perform_ef(inst, machine, labels, ops, pc)
: error(inst, "unknown instruction type – assemble");
}
controller序列的元素由make_machine接收并传递给assemble,它们是字符串(用于标签)和带有标签的列表(用于指令)。指令中的标签是一个字符串,用于标识指令类型,比如"go_to",列表的其余元素包含参数,比如go_to的目的地。make_execution_function中的分发使用
function type(instruction) { return head(instruction); }
当求值作为make_machine的第三个参数的list表达式时,带有标签的列表被构造。list的每个参数都是一个字符串(求值为其自身)或者是一个带有指令标签列表构造函数的调用。例如,assign("b", reg("t"))调用构造函数assign,参数为"b"和调用构造函数reg的结果,参数为"t"。构造函数及其参数确定了寄存器机器语言中各个指令的语法。指令构造函数和选择器如下所示,以及使用选择器的执行函数生成器。
指令assign
make_assign_ef函数为assign指令生成执行函数:
function make_assign_ef(inst, machine, labels, operations, pc) {
const target = get_register(machine, assign_reg_name(inst));
const value_exp = assign_value_exp(inst);
const value_fun =
is_operation_exp(value_exp)
? make_operation_exp_ef(value_exp, machine, labels, operations)
: make_primitive_exp_ef(value_exp, machine, labels);
return () => {
set_contents(target, value_fun());
advance_pc(pc);
};
}
assign函数构造assign指令。选择器assign_reg_ name和assign_value_exp从assign指令中提取寄存器名称和值表达式。
function assign(register_name, source) {
return list("assign", register_name, source);
}
function assign_reg_name(assign_instruction) {
return head(tail(assign_instruction));
}
function assign_value_exp(assign_instruction) {
return head(tail(tail(assign_instruction)));
}
make_assign_ef函数使用get_register查找寄存器名称以生成目标寄存器对象。如果值是操作的结果,则将值表达式传递给make_ operation_exp_ef,否则将其传递给make_primitive_exp_ef。这些函数(如下所示)分析值表达式并为该值生成执行函数。这是一个没有参数的函数,称为value_fun,在模拟期间将被求值以产生要分配给寄存器的实际值。请注意,查找寄存器名称和分析值表达式的工作只在汇编时执行一次,而不是每次模拟指令时执行。这种工作的节省是我们使用执行函数的原因,并直接对应于我们在第 4.1.7 节的求值器中将程序分析与执行分开获得的工作节省。
make_assign_ef返回的结果是assign指令的执行函数。当这个函数被调用(由机器模型的execute函数调用),它将目标寄存器的内容设置为执行value_fun得到的结果。然后通过运行函数将pc前进到下一条指令
function advance_pc(pc) {
set_contents(pc, tail(get_contents(pc)));
}
advance_pc函数是除branch和go_to之外的所有指令的正常终止。
指令test,branch和go_to
make_test_ef函数以类似的方式处理test指令。它提取指定要测试的条件的表达式,并为其生成执行函数。在模拟时,调用条件的函数,将结果赋给flag寄存器,并将pc前进:
function make_test_ef(inst, machine, labels, operations, flag, pc) {
const condition = test_condition(inst);
if (is_operation_exp(condition)) {
const condition_fun = make_operation_exp_ef(
condition, machine,
labels, operations);
return () => {
set_contents(flag, condition_fun());
advance_pc(pc);
};
} else {
error(inst, "bad test instruction – assemble");
}
}
test函数构造test指令。选择器test_condition从测试中提取条件。
function test(condition) { return list("test", condition); }
function test_condition(test_instruction) {
return head(tail(test_instruction));
}
branch指令的执行函数检查flag寄存器的内容,然后将pc的内容设置为分支目的地(如果分支被执行),或者只是推进pc(如果分支未被执行)。请注意,branch指令中指定的目的地必须是一个标签,make_branch_ef函数强制执行此条件。还要注意,标签是在汇编时查找的,而不是每次模拟branch指令时查找。
function make_branch_ef(inst, machine, labels, flag, pc) {
const dest = branch_dest(inst);
if (is_label_exp(dest)) {
const insts = lookup_label(labels, label_exp_label(dest));
return () => {
if (get_contents(flag)) {
set_contents(pc, insts);
} else {
advance_pc(pc);
}
};
} else {
error(inst, "bad branch instruction – assemble");
}
}
branch函数构造branch指令。选择器branch_dest从分支中提取目的地。
function branch(label) { return list("branch", label); }
function branch_dest(branch_instruction) {
return head(tail(branch_instruction));
}
go_to指令类似于分支,不同之处在于目的地可以指定为标签或寄存器,并且没有条件需要检查——pc总是设置为新的目的地。
function make_go_to_ef(inst, machine, labels, pc) {
const dest = go_to_dest(inst);
if (is_label_exp(dest)) {
const insts = lookup_label(labels, label_exp_label(dest));
return () => set_contents(pc, insts);
} else if (is_register_exp(dest)) {
const reg = get_register(machine, register_exp_reg(dest));
return () => set_contents(pc, get_contents(reg));
} else {
error(inst, "bad go_to instruction – assemble");
}
}
go_to函数构造go_to指令。选择器go_to_dest从go_to指令中提取目的地。
function go_to(label) { return list("go_to", label); }
function go_to_dest(go_to_instruction) {
return head(tail(go_to_instruction));
}
其他指令
堆栈指令save和restore只是使用指定寄存器的堆栈并推进pc:
function make_save_ef(inst, machine, stack, pc) {
const reg = get_register(machine, stack_inst_reg_name(inst));
return () => {
push(stack, get_contents(reg));
advance_pc(pc);
};
}
function make_restore_ef(inst, machine, stack, pc) {
const reg = get_register(machine, stack_inst_reg_name(inst));
return () => {
set_contents(reg, pop(stack));
advance_pc(pc);
};
}
save和restore函数构造save和restore指令。选择器stack_inst_reg_name从这些指令中提取寄存器名称。
function save(reg) { return list("save", reg); }
function restore(reg) { return list("restore", reg); }
function stack_inst_reg_name(stack_instruction) {
return head(tail(stack_instruction));
}
由make_perform_ef处理的最终指令类型生成要执行的动作的执行函数。在模拟时,执行动作函数并推进pc。
function make_perform_ef(inst, machine, labels, operations, pc) {
const action = perform_action(inst);
if (is_operation_exp(action)) {
const action_fun = make_operation_exp_ef(action, machine,
labels, operations);
return () => {
action_fun();
advance_pc(pc);
};
} else {
error(inst, "bad perform instruction – assemble");
}
}
perform函数构造perform指令。选择器perform_action从perform指令中提取动作。
function perform(action) { return list("perform", action); }
function perform_action(perform_instruction) {
return head(tail(perform_instruction));
}
子表达式的执行函数
可能需要对reg、label或constant表达式的值进行赋值给寄存器(如上面的make_assign_ef)或输入到操作中(如下面的make_operation_exp_ef)。以下函数生成执行函数,以在模拟期间为这些表达式生成值:
function make_primitive_exp_ef(exp, machine, labels) {
if (is_constant_exp(exp)) {
const c = constant_exp_value(exp);
return () => c;
} else if (is_label_exp(exp)) {
const insts = lookup_label(labels, label_exp_label(exp));
return () => insts;
} else if (is_register_exp(exp)) {
const r = get_register(machine, register_exp_reg(exp));
return () => get_contents(r);
} else {
error(exp, "unknown expression type – assemble");
}
}
reg、label和constant表达式的语法由以下构造函数确定,以及相应的谓词和选择器。
function reg(name) { return list("reg", name); }
function is_register_exp(exp) { return is_tagged_list(exp, "reg"); }
function register_exp_reg(exp) { return head(tail(exp)); }
function constant(value) { return list("constant", value); }
function is_constant_exp(exp) {
return is_tagged_list(exp, "constant");
}
function constant_exp_value(exp) { return head(tail(exp)); }
function label(name) { return list("label", name); }
function is_label_exp(exp) { return is_tagged_list(exp, "label"); }
function label_exp_label(exp) { return head(tail(exp)); }
assign、perform和test指令可能包括对机器操作(由op表达式指定)对一些操作数(由reg和constant表达式指定)的应用。以下函数为“操作表达式”(包含指令中的操作和操作数表达式的列表)生成执行函数:
function make_operation_exp_ef(exp, machine, labels, operations) {
const op = lookup_prim(operation_exp_op(exp), operations);
const afuns = map(e => make_primitive_exp_ef(e, machine, labels),
operation_exp_operands(exp));
return () => apply_in_underlying_javascript(
op, map(f => f(), afuns));
}
操作表达式的语法由
function op(name) { return list("op", name); }
function is_operation_exp(exp) {
return is_pair(exp) && is_tagged_list(head(exp), "op");
}
function operation_exp_op(op_exp) { return head(tail(head(op_exp))); }
function operation_exp_operands(op_exp) { return tail(op_exp); }
注意,操作表达式的处理非常类似于求值器中analyze_application函数对函数应用的处理,我们为每个操作数生成一个执行函数。在模拟时,我们调用操作数函数并将模拟操作的 JavaScript 函数应用于生成的值。我们使用apply_in_underlying_javascript函数,就像在 4.1.4 节中的apply_primitive_function中所做的那样。这是为了将op应用于第一个map生成的参数列表afuns的所有元素,就好像它们是op的单独参数一样。如果没有这样做,op将被限制为一元函数。
通过在机器的操作表中查找操作名称来找到模拟函数:
function lookup_prim(symbol, operations) {
const val = assoc(symbol, operations);
return is_undefined(val)
? error(symbol, "unknown operation – assemble")
: head(tail(val));
}
练习 5.9
上面对机器操作的处理允许它们对标签以及寄存器的内容和常量进行操作。修改表达式处理函数以强制执行操作只能与寄存器和常量一起使用的条件。
练习 5.10
当我们在 5.1.4 节中介绍save和restore时,我们没有指定如果尝试恢复不是最后一个保存的寄存器会发生什么,例如在以下序列中
save(y);
save(x);
restore(y);
对于restore的含义有几种合理的可能性:
-
a.
restore(y)将最后一个保存在堆栈上的值放入y中,无论该值来自哪个寄存器。这是我们模拟器的行为方式。展示如何利用这种行为来消除 5.1.4 节(图 5.12)中 Fibonacci 机器的一条指令。 -
b.
restore(y)将最后一个保存在堆栈上的值放入y中,但前提是该值是从y保存的;否则,它会发出错误信号。修改模拟器以使其行为如此。您将不得不更改save以将寄存器名称与值一起放入堆栈。 -
c.
restore(y)将最后一个保存在y中的值放入y中,而不管在y之后保存的其他寄存器是什么。修改模拟器以使其行为如此。您将不得不为每个寄存器关联一个单独的堆栈。您应该使initialize_stack操作初始化所有寄存器堆栈。
练习 5.11
模拟器可用于帮助确定实现具有给定控制器的机器所需的数据路径。扩展汇编程序以在机器模型中存储以下信息:
-
所有指令的列表,去除重复项,按指令类型(
assign,go_to等)排序; -
一个(无重复)的寄存器列表,用于保存入口点(这些是由
go_to指令引用的寄存器); -
一个(无重复)的寄存器列表,这些寄存器被“保存”或“恢复”;
-
对于每个寄存器,列出(无重复)分配给它的源(例如,图 5.11 中的阶乘机器中
val寄存器的源是constant(1)和list(op("*"), reg("n"), reg("val")))。
扩展与机器的消息传递接口,以提供对这些新信息的访问。为了测试您的分析器,定义来自图 5.12 的 Fibonacci 机器,并检查您构建的列表。
练习 5.12
修改模拟器,使其使用控制器序列来确定机器具有哪些寄存器,而不是要求在make_machine的参数中预分配寄存器的列表。不要在make_machine中预分配寄存器,而是在汇编指令装配时首次看到它们时逐个分配它们。
5.2.4 监控机器性能
模拟不仅用于验证提议的机器设计的正确性,还用于测量机器的性能。例如,我们可以在模拟程序中安装一个“计量器”,用于测量计算中使用的堆栈操作次数。为此,我们修改我们的模拟堆栈以跟踪寄存器保存在堆栈上的次数和堆栈达到的最大深度,并在堆栈的接口中添加一个打印统计信息的消息,如下所示。我们还在基本机器模型中添加一个操作来打印堆栈统计信息,通过在make_new_machine中初始化the_ops来实现
list(list("initialize_stack",
() => stack("initialize")),
list("print_stack_statistics",
() => stack("print_statistics")));
这是make_stack的新版本:
function make_stack() {
let stack = null;
let number_pushes = 0;
let max_depth = 0;
let current_depth = 0;
function push(x) {
stack = pair(x, stack);
number_pushes = number_pushes + 1;
current_depth = current_depth + 1;
max_depth = math_max(current_depth, max_depth);
return "done";
}
function pop() {
if (is_null(stack)) {
error("empty stack – pop");
} else {
const top = head(stack);
stack = tail(stack);
current_depth = current_depth - 1;
return top;
}
}
function initialize() {
stack = null;
number_pushes = 0;
max_depth = 0;
current_depth = 0;
return "done";
}
function print_statistics() {
display("total pushes = " + stringify(number_pushes));
display("maximum depth = " + stringify(max_depth));
}
function dispatch(message) {
return message === "push"
? push
: message === "pop"
? pop()
: message === "initialize"
? initialize()
: message === "print_statistics"
? print_statistics()
: error(message, "unknown request – stack");
}
return dispatch;
}
练习 5.14 到 5.18 描述了可以添加到寄存器机模拟器的其他有用的监控和调试功能。
练习 5.13
测量计算n!所需的推送次数和最大堆栈深度,对于各个小值的n,使用图 5.11 中显示的阶乘机器。从您的数据中确定关于n的总推送操作次数和计算n!所需的最大堆栈深度的公式。请注意,这两者都是n的线性函数,因此由两个常数确定。为了打印统计信息,您将需要增加阶乘机器的指令来初始化堆栈并打印统计信息。您可能还希望修改机器,使其重复读取n的值,计算阶乘,并打印结果(就像我们在图 5.4 中对 GCD 机器所做的那样),这样您就不必反复调用get_register_contents,set_register_contents和start。
练习 5.14
将指令计数添加到寄存器机器模拟中。也就是说,让机器模型跟踪执行的指令数量。扩展机器模型的接口,接受一个新的消息,打印指令计数的值并将计数重置为零。
练习 5.15
增强模拟器以提供指令跟踪。也就是说,在执行每条指令之前,模拟器应该打印该指令。使机器模型接受trace_on和trace_off消息以打开和关闭跟踪。
练习 5.16
扩展练习 5.15 的指令跟踪,以便在打印指令之前,模拟器打印出控制器序列中紧接着该指令的任何标签。要小心以不干扰指令计数(练习 5.14)的方式进行此操作。您将需要使模拟器保留必要的标签信息。
练习 5.17
修改 5.2.1 节的make_register函数,以便可以跟踪寄存器。寄存器应该接受打开和关闭跟踪的消息。当寄存器被跟踪时,将值分配给寄存器应该打印寄存器的名称,寄存器的旧内容以及正在分配的新内容。扩展机器模型的接口,允许您为指定的机器寄存器打开和关闭跟踪。
练习 5.18
Alyssa P. Hacker 希望模拟器中有一个断点功能,以帮助她调试她的机器设计。您已被聘请为她安装此功能。她希望能够指定控制器序列中的一个位置,模拟器将在那里停止,并允许她检查机器的状态。您要实现一个函数
set_breakpoint(machine, label, n)
在给定标签后的第n条指令之前设置一个断点。例如,
`
set_breakpoint(gcd_machine, "test_b", 4)
在gcd_machine中的寄存器a分配之前设置断点。当模拟器到达断点时,它应该打印标签和断点的偏移量,并停止执行指令。然后 Alyssa 可以使用get_register_contents和set_register_contents来操纵模拟机的状态。然后她应该能够通过说
proceed_machine(machine)
她还应该能够通过以下方式删除特定的断点
cancel_breakpoint(machine, label, n)
或通过以下方式删除所有断点
cancel_all_breakpoints(machine)
5.3 存储分配和垃圾收集
在第 5.4 节中,我们将展示如何将 JavaScript 求值器实现为寄存器机器。为了简化讨论,我们将假设我们的寄存器机器可以配备列表结构内存,其中用于操作列表结构数据的基本操作是原始的。假设这样的内存存在是一个有用的抽象,当一个解释器专注于控制机制时,但这并不反映当代计算机的实际原始数据操作的真实视图。为了更全面地了解系统如何有效支持列表结构内存,我们必须调查如何表示列表结构,以使其与传统计算机内存兼容。
在实现列表结构时有两个考虑因素。第一个纯粹是一个表示问题:如何仅使用典型计算机内存的存储和寻址能力来表示对,使用“盒子和指针”结构。第二个问题涉及内存管理随着计算的进行。JavaScript 系统的操作至关重要的依赖于不断创建新的数据对象的能力。这些包括由 JavaScript 函数明确创建的对象,以及由解释器本身创建的结构,例如环境和参数列表。尽管在具有无限量快速可寻址内存的计算机上不断创建新的数据对象不会造成问题,但计算机内存只有有限的大小(更可惜)。因此,JavaScript 提供了自动存储分配设施,以支持无限内存的幻觉。当不再需要数据对象时,分配给它的内存会自动回收并用于构造新的数据对象。提供这种自动存储分配的各种技术。我们将在本节中讨论的方法称为垃圾收集。
5.3.1 记忆作为向量
传统计算机内存可以被认为是一个包含信息的小隔间数组。每个小隔间都有一个唯一的名称,称为其地址或位置。典型的内存系统提供两种原始操作:一种是获取存储在指定位置的数据,另一种是将新数据分配给指定位置。内存地址可以递增以支持对一些小隔间的顺序访问。更一般地,许多重要的数据操作要求将内存地址视为数据,可以存储在内存位置中,并在机器寄存器中进行操作。列表结构的表示是这种地址算术的一个应用。
为了模拟计算机内存,我们使用一种称为向量的新数据结构。抽象地说,向量是一个复合数据对象,其各个元素可以通过整数索引来访问,而访问的时间与索引无关。为了描述内存操作,我们使用两个用于操作向量的函数。
-
vector_ref(vector, n)返回向量的第n个元素。 -
vector_set(vector, n, value)将向量的第n个元素设置为指定的值。
例如,如果v是一个向量,那么vector_ref(v, 5)会得到向量v中的第五个条目,vector_set(v, 5, 7)会将向量v的第五个条目的值更改为 7。对于计算机内存,这种访问可以通过地址算术来实现,将指定向量在内存中的基地址与指定向量特定元素的索引相结合。
表示数据
我们可以使用向量来实现列表结构内存所需的基本对结构。让我们想象计算机内存被分成两个向量:the_heads和the_tails。我们将表示列表结构如下:对于一对的指针是两个向量中的索引。一对的head是指定索引的the_heads中的条目,一对的tail是指定索引的the_tails中的条目。我们还需要一种表示除对之外的对象(如数字和字符串)的方法,以及一种区分一种数据类型与另一种的方法。有许多方法可以实现这一点,但它们都可以归结为使用类型化指针,即将“指针”的概念扩展到包括有关数据类型的信息。数据类型使系统能够区分指向一对的指针(它由“对”数据类型和内存向量中的索引组成)和指向其他类型数据的指针(它由其他数据类型和用于表示该类型数据的任何内容组成)。如果它们的指针相同,两个数据对象被认为是相同的(===)。图 5.14 说明了使用这种方法表示list(list(1, 2), 3, 4),其盒式图也显示在图中。我们使用字母前缀来表示数据类型信息。因此,指向索引 5 的对的指针表示为p5,空列表由指针e0表示,指向数字 4 的指针表示为n4。在盒式图中,我们在每对的左下方指示了指定head和tail存储位置的向量索引。the_heads和the_tails中的空白位置可能包含其他列表结构的部分(这里不感兴趣)。

图 5.14 列表list(list(1, 2), 3, 4)的盒式图和内存向量表示。
例如,指向数字的指针,比如n4,可能由一个指示数字数据的类型和数字 4 的实际表示组成。为了处理无法在单个指针分配的固定空间中表示的太大的数字,我们可以使用一个不同的bignum数据类型,其中指针指定一个列表,其中存储了数字的各个部分。
一个字符串可以被表示为一个类型化的指针,该指针指定了形成字符串打印表示的字符序列。当解析器遇到字符串文字时,它构造这样一个序列,字符串连接运算符+和诸如stringify之类的字符串生成原始函数也构造这样一个序列。由于我们希望两个字符串实例被===识别为“相同”的字符串,并且我们希望===是指针相等的简单测试,我们必须确保如果系统两次看到相同的字符串,它将使用相同的指针(指向相同的字符序列)来表示这两个实例。为了实现这一点,系统维护一个称为字符串池的表,其中包含它曾经遇到的所有字符串。当系统即将构造一个字符串时,它会检查字符串池,看看它以前是否见过相同的字符串。如果没有,它会构造一个新的字符串(指向新的字符序列的类型化指针)并将这个指针输入字符串池。如果系统以前见过这个字符串,它会返回字符串池中存储的字符串指针。这个用唯一指针替换字符串的过程称为字符串国际化。
实现原始列表操作
根据上述表示方案,我们可以用一个或多个原始向量操作替换寄存器机器的每个“原始”列表操作。我们将使用两个寄存器the_heads和the_tails来标识内存向量,并假设vector_ref和vector_set可用作原始操作。我们还假设指针的数值操作(例如递增指针,使用对指针索引向量,或者将两个数字相加)仅使用类型指针的索引部分。
例如,我们可以使一个寄存器机器支持以下指令
assign(reg[1], list(op("head"), reg(reg[2])))
assign(reg[1], list(op("tail"), reg(reg[2])))
如果我们分别实现这些,作为
assign(reg[1], list(op("vector_ref"), reg("the_heads"), reg(reg[2])))
assign(reg[1], list(op("vector_ref"), reg("the_tails"), reg(reg[2])))
指令
perform(list(op("set_head"), reg(reg[1]), reg(reg[2])))
perform(list(op("set_tail"), reg(reg[1]), reg(reg[2])))
被实现为
perform(list(op("vector_set"), reg("the_heads"), reg(reg[1]), reg(reg[2])))
perform(list(op("vector_set"), reg("the_tails"), reg(reg[1]), reg(reg[2])))
pair操作通过分配一个未使用的索引并将pair的参数存储在该索引向量位置的the_heads和the_tails中来执行。我们假设有一个特殊的寄存器free,它始终保存一个包含下一个可用索引的对指针,并且我们可以递增该指针的索引部分以找到下一个空闲位置。¹² 例如,指令
assign(reg[1], list(op("pair"), reg(reg[2]), reg(reg[3])))
被实现为以下向量操作的序列:¹³
perform(list(op("vector_set"),
reg("the_heads"), reg("free"), reg(reg[2]))),
perform(list(op("vector_set"),
reg("the_tails"), reg("free"), reg(reg[3]))),
assign(reg[1], reg("free")),
assign("free", list(op("+"), reg("free"), constant(1)))
===操作
list(op("==="), reg(reg[1]), reg(reg[2]))
简单地测试寄存器中所有字段的相等性,而诸如is_pair、is_null、is_string和is_number之类的谓词只需要检查类型字段。
实现堆栈
尽管我们的寄存器机器使用堆栈,但在这里我们不需要做任何特殊处理,因为堆栈可以用列表来建模。堆栈可以是由特殊寄存器the_stack指向的保存值的列表。因此,save(reg)可以被实现为
assign("the_stack", list(op("pair"), reg(reg), reg("the_stack")))
同样,restore(reg)可以被实现为
assign(reg, list(op("head"), reg("the_stack")))
assign("the_stack", list(op("tail"), reg("the_stack")))
和perform(list(op("initialize_stack")))可以被实现为
assign("the_stack", constant(null))
这些操作可以进一步扩展为上述的向量操作。然而,在传统的计算机体系结构中,通常有利于将堆栈分配为单独的向量。然后,通过增加或减少对该向量的索引来推送和弹出堆栈。
练习 5.19
绘制盒子和指针表示以及由以下列表结构产生的内存向量表示(如图 5.14 中所示)。
const x = pair(1, 2);
const y = list(x, x);
初始情况下,free指针为p1。free的最终值是多少?哪些指针代表了x和y的值?
练习 5.20
为以下函数实现寄存器机器。假设列表结构内存操作可用作机器原语。
-
a. 递归
count_leaves:function count_leaves(tree) { return is_null(tree) ? 0 : ! is_pair(tree) ? 1 : count_leaves(head(tree)) + count_leaves(tail(tree)); } -
b. 递归
count_leaves并带有显式计数器:function count_leaves(tree) { function count_iter(tree, n) { return is_null(tree) ? n : ! is_pair(tree) ? n + 1 : count_iter(tail(tree), count_iter(head(tree), n)); } return count_iter(tree, 0); }
练习 5.21
3.3.1 节的练习 3.12 提出了一个append函数,它将两个列表连接起来形成一个新列表,以及一个append_mutator函数,它将两个列表拼接在一起。设计一个寄存器机器来实现这些函数。假设列表结构内存操作可用作原始操作。
5.3.2 维持内存无限的幻觉
5.3.1 节中概述的表示方法解决了实现列表结构的问题,前提是我们有无限的内存。在真实的计算机中,我们最终会耗尽用于构造新对的空间。¹⁴ 然而,在典型计算中生成的大多数对仅用于保存中间结果。在访问这些结果之后,这些对将不再需要——它们是垃圾。例如,计算
accumulate((x, y) => x + y,
0,
filter(is_odd, enumerate_interval(0, n)))
构造两个列表:枚举和过滤枚举的结果。当累积完成时,这些列表将不再需要,并且分配的内存可以被回收。如果我们可以安排定期收集所有的垃圾,并且如果这样做以大约与我们构造新对的速度相同的速度回收内存,我们将保留内存无限的幻觉。
为了回收对,我们必须有一种方法来确定哪些分配的对不再需要(即它们的内容不再能影响计算的未来)。我们将研究用于实现这一点的方法称为垃圾收集。垃圾收集基于这样的观察:在基于列表结构的内存的解释中的任何时刻,只有可以通过从当前机器寄存器中的指针开始的一系列head和tail操作到达的对象才能影响计算的未来。任何不可访问的内存单元都可以被回收。 ¹⁵
执行垃圾收集有许多方法。我们将在这里研究的方法称为停止-复制。基本思想是将内存分为两半:“工作内存”和“空闲内存”。当pair构造成对时,它们被分配在工作内存中。当工作内存满时,我们通过定位工作内存中所有有用的对并将其复制到空闲内存中来执行垃圾收集。(通过跟踪所有head和tail指针来定位有用的对,从机器寄存器开始。)由于我们不复制垃圾,所以可能会有额外的空闲内存,我们可以用来分配新的对。此外,工作内存中的任何内容都不再需要,因为其中的所有有用对都已经被复制。因此,如果我们交换工作内存和空闲内存的角色,我们可以继续处理;新的对将在新的工作内存中(原来的空闲内存)分配。当这个满了,我们可以将有用的对复制到新的空闲内存中(原来的工作内存)。
实现停止-复制垃圾收集器
现在我们使用寄存器机器语言更详细地描述停止-复制算法。我们假设有一个名为root的寄存器,其中包含一个指向最终指向所有可访问数据的结构的指针。这可以通过在开始垃圾收集之前将所有机器寄存器的内容存储在由root指向的预分配列表中来安排。我们还假设,除了当前的工作内存之外,还有可用的空闲内存,我们可以将有用的数据复制到其中。当前的工作内存由基地址在名为the_heads和the_tails的寄存器中的向量组成,而空闲内存在名为new_heads和new_tails的寄存器中。
当我们耗尽当前工作内存中的空闲单元时,也就是说,当pair操作尝试将free指针增加到内存向量的末尾之外时,垃圾收集就会被触发。当垃圾收集过程完成时,root指针将指向新的内存,从root可访问的所有对象都将被移动到新的内存中,free指针将指示新内存中可以分配新对的下一个位置。此外,工作内存和新内存的角色将被交换-新的对将在新的内存中构造,从free指示的位置开始,而(之前的)工作内存将作为下一次垃圾收集的新内存可用。图 5.15 显示了垃圾收集前后内存的布局。

图 5.15 垃圾收集过程中内存的重新配置。
垃圾收集过程的状态由维护两个指针来控制:free和scan。它们被初始化为指向新内存的开始。算法从将root指向的一对重新定位到新内存的开始。复制这对,调整root指针指向新位置,并递增free指针。此外,标记这对的旧位置以显示其内容已被移动。标记的方法如下:在“头”位置,我们放置一个特殊标记,表示这是一个已经移动的对象(这样的对象传统上被称为破碎的心)。在“尾”位置,我们放置一个转发地址,指向对象已被移动的位置。
搬迁根节点后,垃圾收集器进入基本循环。算法的每一步中,“扫描”指针(最初指向已搬迁的根节点)指向一个已移动到新内存中的一对,但其“头”和“尾”指针仍指向旧内存中的对象。这些对象都被重新定位,然后“扫描”指针递增。要重新定位一个对象(例如,我们正在扫描的一对中由“头”指针指示的对象),我们检查对象是否已经被移动(由对象的“头”位置上的破碎心标记表示)。如果对象尚未被移动,我们将其复制到free指示的位置,更新free,在对象的旧位置设置一个破碎的心,并更新指向对象的指针(在这个例子中,我们正在扫描的一对的“头”指针)指向新位置。如果对象已经被移动,其转发地址(在破碎心的“尾”位置找到)将替换正在扫描的一对中的指针。最终,所有可访问的对象都将被移动和扫描,此时“扫描”指针将超过free指针,进程将终止。
我们可以将停止-复制算法指定为寄存器机器的一系列指令。重新定位对象的基本步骤是通过一个名为relocate_old_result_in_new的子例程来完成的。这个子例程从一个名为old的寄存器中获取其参数,即要重新定位的对象的指针。它重新定位指定的对象(在此过程中递增free),将指向重新定位对象的指针放入一个名为new的寄存器中,并通过跳转到存储在寄存器relocate_continue中的入口点返回。要开始垃圾收集,我们调用这个子例程来重新定位root指针,然后初始化free和scan。当root的重新定位完成后,我们将新指针安装为新的root,并进入垃圾收集器的主循环。
"begin_garbage_collection",
assign("free", constant(0)),
assign("scan", constant(0)),
assign("old", reg("root")),
assign("relocate_continue", label("reassign_root")),
go_to(label("relocate_old_result_in_new")),
"reassign_root",
assign("root", reg("new")),
go_to(label("gc_loop")),
在垃圾收集器的主循环中,我们必须确定是否还有更多的对象需要扫描。我们通过测试scan指针是否与free指针重合来做到这一点。如果指针相等,则所有可访问的对象都已经被重新定位,我们将跳转到gc_flip,清理一切,以便我们可以继续中断的计算。如果仍有要扫描的一对,我们调用重新定位子例程来重新定位下一对的“头”(将“头”指针放入old中)。设置relocate_continue寄存器,以便子例程将返回更新“头”指针。
"gc_loop",
test(list(op("==="), reg("scan"), reg("free"))),
branch(label("gc_flip")),
assign("old", list(op("vector_ref"), reg("new_heads"), reg("scan"))),
assign("relocate_continue", label("update_head")),
go_to(label("relocate_old_result_in_new")),
在update_head中,我们修改正在扫描的一对的“头”指针,然后继续重新定位“尾”。当重新定位和更新“尾”完成后,我们完成了扫描该对,因此我们继续进行主循环。
"update_head",
perform(list(op("vector_set"),
reg("new_heads"), reg("scan"), reg("new"))),
assign("old", list(op("vector_ref"),
reg("new_tails"), reg("scan"))),
assign("relocate_continue", label("update_tail")),
go_to(label("relocate_old_result_in_new")),
"update_tail",
perform(list(op("vector_set"),
reg("new_tails"), reg("scan"), reg("new"))),
assign("scan", list(op("+"), reg("scan"), constant(1))),
go_to(label("gc_loop")),
子例程relocate_old_result_in_new的重定位对象如下:如果要重定位的对象(由old指向)不是一对,那么我们将返回指向对象的相同指针(在new中不变)。(例如,我们可能正在扫描一个head为数字 4 的对。如果我们按照第 5.3.1 节中描述的方式将head表示为n4,那么我们希望“重定位”的head指针仍然是n4。)否则,我们必须执行重定位。如果要重定位的对的head位置包含一个破碎心标记,那么该对实际上已经被移动,因此我们从破碎心的tail位置检索转发地址,并将其返回到new中。如果old中的指针指向尚未移动的对,则我们将该对移动到新内存中的第一个空闲单元(由free指向),并通过在旧位置存储破碎心标记和转发地址来设置破碎心。子例程relocate_old_result_in_new使用寄存器oldht来保存由old指向的对象的head或tail。¹⁹
"relocate_old_result_in_new",
test(list(op("is_pointer_to_pair"), reg("old"))),
branch(label("pair")),
assign("new", reg("old")),
go_to(reg("relocate_continue")),
"pair",
assign("oldht", list(op("vector_ref"),
reg("the_heads"), reg("old"))),
test(list(op("is_broken_heart"), reg("oldht"))),
branch(label("already_moved")),
assign("new", reg("free")), // new location for pair
// Update free pointer
assign("free", list(op("+"), reg("free"), constant(1))),
// Copy the head and tail to new memory
perform(list(op("vector_set"),
reg("new_heads"), reg("new"),
reg("oldht"))),
assign("oldht", list(op("vector_ref"),
reg("the_tails"), reg("old"))),
perform(list(op("vector_set"),
reg("new_tails"), reg("new"),
reg("oldht"))),
// Construct the broken heart
perform(list(op("vector_set"),
reg("the_heads"), reg("old"),
constant("broken_heart"))),
perform(list(op("vector_set"),
reg("the_tails"), reg("old"),
reg("new"))),
go_to(reg("relocate_continue")),
"already_moved",
assign("new", list(op("vector_ref"),
reg("the_tails"), reg("old"))),
go_to(reg("relocate_continue")),
在垃圾收集过程的最后,我们通过交换指针来交换旧内存和新内存的角色:交换the_heads和new_heads,以及the_tails和new_tails。然后,我们将准备好在内存耗尽时执行另一次垃圾收集。
"gc_flip",
assign("temp", reg("the_tails")),
assign("the_tails", reg("new_tails")),
assign("new_tails", reg("temp")),
assign("temp", reg("the_heads")),
assign("the_heads", reg("new_heads")),
assign("new_heads", reg("temp"))
5.4 显式控制求值器
在第 5.1 节中,我们看到如何将简单的 JavaScript 程序转换为寄存器机器的描述。我们现在将对更复杂的程序执行此转换,即第 4.1.1–4.1.4 节中的元循环求值器,该程序展示了如何用evaluate和apply函数描述 JavaScript 解释器的行为。本节中开发的显式控制求值器展示了在求值过程中使用的基础函数调用和参数传递机制如何可以用寄存器和堆栈上的操作来描述。此外,显式控制求值器可以作为 JavaScript 解释器的实现,用一种非常类似于传统计算机的本机机器语言编写。求值器可以由第 5.2 节的寄存器机器模拟器执行。或者,它可以用作构建 JavaScript 求值器的机器语言实现的起点,甚至是用于求值 JavaScript 程序的专用机器的起点。图 5.16 展示了这样一个硬件实现:一个硅片作为 Scheme 的求值器,该语言在本书的原版中代替了 JavaScript。芯片设计者从与本节描述的求值器类似的寄存器机器的数据路径和控制器规格开始,并使用设计自动化程序构建集成电路布局。²⁰

图 5.16 Scheme 求值器的硅片实现。
寄存器和操作
在设计显式控制求值器时,我们必须指定在我们的寄存器机器中使用的操作。我们描述了元循环求值器,使用诸如is_literal和make_function之类的函数来描述抽象语法。在实现寄存器机器时,我们可以将这些函数扩展为基本的列表结构内存操作的序列,并在我们的寄存器机器上实现这些操作。然而,这将使我们的求值器非常冗长,使基本结构被细节所遮盖。为了阐明表述,我们将在寄存器机器的原始操作中包括第 4.1.2 节中给出的语法函数和第 4.1.3 和 4.1.4 节中给出的表示环境和其他运行时数据的函数。为了完全指定一个可以在低级机器语言中编程或在硬件中实现的求值器,我们将用更基本的操作替换这些操作,使用我们在第 5.3 节中描述的列表结构实现。
我们的 JavaScript 求值器寄存器机器包括一个堆栈和七个寄存器:comp、env、val、continue、fun、argl和unev。comp寄存器用于保存要求值的组件,env包含要执行求值的环境。在求值结束时,val包含在指定环境中求值组件获得的值。continue寄存器用于实现递归,如第 5.1.4 节中所述。(求值器需要递归调用自身,因为求值一个组件需要求值其子组件。)fun、argl和unev寄存器用于求值函数应用。
我们不会提供数据路径图来显示求值器的寄存器和操作如何连接,也不会提供完整的机器操作列表。这些都隐含在求值器的控制器中,将会详细介绍。
5.4.1 调度程序和基本求值
求值器中的中心元素是从eval_dispatch开始的指令序列。这对应于第 4.1.1 节中描述的元循环求值器的evaluate函数。当控制器从eval_dispatch开始时,它在由env指定的环境中求值由comp指定的组件。求值完成后,控制器将转到存储在continue中的入口点,而val寄存器将保存组件的值。与元循环的evaluate一样,eval_dispatch的结构是对待求值组件的语法类型的情况分析。
"eval_dispatch",
test(list(op("is_literal"), reg("comp"))),
branch(label("ev_literal")),
test(list(op("is_name"), reg("comp"))),
branch(label("ev_name")),
test(list(op("is_application"), reg("comp"))),
branch(label("ev_application")),
test(list(op("is_operator_combination"), reg("comp"))),
branch(label("ev_operator_combination")),
test(list(op("is_conditional"), reg("comp"))),
branch(label("ev_conditional")),
test(list(op("is_lambda_expression"), reg("comp"))),
branch(label("ev_lambda")),
test(list(op("is_sequence"), reg("comp"))),
branch(label("ev_sequence")),
test(list(op("is_block"), reg("comp"))),
branch(label("ev_block")),
test(list(op("is_return_statement"), reg("comp"))),
branch(label("ev_return")),
test(list(op("is_function_declaration"), reg("comp"))),
branch(label("ev_function_declaration")),
test(list(op("is_declaration"), reg("comp"))),
branch(label("ev_declaration")),
test(list(op("is_assignment"), reg("comp"))),
branch(label("ev_assignment")),
go_to(label("unknown_component_type")),
求值简单表达式
数字和字符串、名称和 lambda 表达式没有要求值的子表达式。对于这些情况,求值器只需将正确的值放入val寄存器中,并在continue指定的入口点继续执行。简单表达式的求值由以下控制器代码执行:
"ev_literal",
assign("val", list(op("literal_value"), reg("comp"))),
go_to(reg("continue")),
"ev_name",
assign("val", list(op("symbol_of_name"), reg("comp"), reg("env"))),
assign("val", list(op("lookup_symbol_value"),
reg("val"), reg("env"))),
go_to(reg("continue")),
"ev_lambda",
assign("unev", list(op("lambda_parameter_symbols"), reg("comp"))),
assign("comp", list(op("lambda_body"), reg("comp"))),
assign("val", list(op("make_function"),
reg("unev"), reg("comp"), reg("env"))),
go_to(reg("continue")),
观察ev_lambda如何使用unev和comp寄存器来保存 lambda 表达式的参数和主体,以便它们可以与env中的环境一起传递给make_function操作。
条件句
与元循环求值器一样,语法形式通过选择性地求值组件的片段来处理。对于条件句,我们必须求值谓词,并根据谓词的值决定是求值结果还是替代方案。
在求值谓词之前,我们保存条件本身,即在comp中,以便稍后提取结果或替代项。为了求值谓词表达式,我们将其移动到comp寄存器中并转到eval_dispatch。env寄存器中的环境已经是正确的环境,用于求值谓词。但是,我们保存env,因为稍后我们将需要它来求值结果或替代项。我们设置continue,以便在求值完谓词后在ev_conditional_decide处恢复求值。然而,首先我们保存continue的旧值,因为稍后我们需要它来返回到等待条件值的语句的求值。
"ev_conditional",
save("comp"), // save conditional for later
save("env"),
save("continue"),
assign("continue", label("ev_conditional_decide")),
assign("comp", list(op("conditional_predicate"), reg("comp"))),
go_to(label("eval_dispatch")), // evaluate the predicate
在求值谓词后,在ev_conditional_decide处恢复时,我们测试它是真还是假,并根据结果在转到eval_dispatch之前将结果或替代项放在comp中。请注意,这里恢复env和continue设置了eval_dispatch具有正确的环境,并在正确的位置继续接收条件的值。
"ev_conditional_decide",
restore("continue"),
restore("env"),
restore("comp"),
test(list(op("is_falsy"), reg("val"))),
branch(label("ev_conditional_alternative")),
"ev_conditional_consequent",
assign("comp", list(op("conditional_consequent"), reg("comp"))),
go_to(label("eval_dispatch")),
"ev_conditional_alternative",
assign("comp", list(op("conditional_alternative"), reg("comp"))),
go_to(label("eval_dispatch")),
序列求值
显式控制求值器中从ev_sequence开始处理语句序列的部分类似于元循环求值器的eval_ sequence函数。
ev_sequence_next和ev_sequence_continue处的条目形成一个循环,依次求值序列中的每个语句。未求值语句的列表保存在unev中。在ev_sequence处,我们将要求值的语句序列放在unev中。如果序列为空,我们将val设置为undefined,并通过ev_sequence_empty跳转到continue。否则,我们开始序列求值循环,首先在堆栈上保存continue的值,因为continue寄存器将用于循环中的局部控制流,原始值在语句序列之后继续时是需要的。在求值每个语句之前,我们检查序列中是否有其他语句需要求值。如果有,我们保存未求值语句的其余部分(保存在unev中)和必须求值这些语句的环境(保存在env中),并调用eval_dispatch来求值已放置在comp中的语句。在此求值后,这两个保存的寄存器在ev_sequence_continue处被恢复。
该序列中的最终语句在入口点ev_sequence_last_statement处以不同的方式处理。由于在此之后没有更多的语句需要求值,因此在转到eval_dispatch之前,我们不需要保存unev或env。整个序列的值是最后一个语句的值,因此在求值最后一个语句之后,除了继续保存在ev_sequence处保存的入口点外,没有其他事情要做。我们不是设置continue以安排eval_dispatch返回到这里,然后从堆栈中恢复continue并在该入口点继续,而是在转到eval_dispatch之前从堆栈中恢复continue,以便eval_dispatch在求值语句后继续在该入口点继续。
"ev_sequence",
assign("unev", list(op("sequence_statements"), reg("comp"))),
test(list(op("is_empty_sequence"), reg("unev"))),
branch(label("ev_sequence_empty")),
save("continue"),
"ev_sequence_next",
assign("comp", list(op("first_statement"), reg("unev"))),
test(list(op("is_last_statement"), reg("unev"))),
branch(label("ev_sequence_last_statement")),
save("unev"),
save("env"),
assign("continue", label("ev_sequence_continue")),
go_to(label("eval_dispatch")),
"ev_sequence_continue",
restore("env"),
restore("unev"),
assign("unev", list(op("rest_statements"), reg("unev"))),
go_to(label("ev_sequence_next")),
"ev_sequence_last_statement",
restore("continue"),
go_to(label("eval_dispatch")),
"ev_sequence_empty",
assign("val", constant(undefined)),
go_to(reg("continue")),

浙公网安备 33010602011771号