结构化 IR:保留一组有序字段
本节阅读量:
解释器可以直接创建 C++ RecordValue,目标程序却不能依赖解释器进程中的 shared_ptr。编译路径需要把 record 语义降低成后端能够继续处理的四类动作:
1
2
3
4
|
record 用若干 Value 创建新对象
record? 判断 Value 是否为 record
size 读取 record 的字段数
get 读取指定字段
|
这一层仍不决定 tag、header、malloc 或错误出口标签。它只把源码树形求值顺序摊平成结构化操作。
先观察一份 IR
考虑:
1
|
(get (record 10 20 30) 2)
|
对应 IR 是:
1
2
3
|
t.0 = record 10 20 30
t.1 = get t.0 2
return t.1
|
record op 保存三个有序 operand,结果是新对象引用 t.0。get op 保存对象 operand 与 parser 已经确认的非负 index,结果是字段 Value。
现有示例可以直接检查:
1
|
./mini ir examples/record_get.lang
|
每种动作都有显式 OpKind
IR 延续全书的 kind + switch 约定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
enum class OpKind {
copy,
add,
sub1,
equal,
branch,
jump,
label,
function_ref,
call,
record,
record_predicate,
record_size,
field,
};
|
新增 kind 不只是为了 dump。栈槽分配和汇编 emitter 都必须在 switch 中明确处理这些操作;编译器不会根据字符串内容猜测 op 类型。
Op 为 record 增加 operand vector
前文的二元操作可以用 lhs、rhs;可变长度 record 需要一组 operand:
1
2
3
4
5
6
7
8
9
10
|
struct Op {
OpKind kind;
std::string dst;
Operand lhs;
Operand rhs;
std::string target;
std::string else_target;
std::size_t field_index = 0;
std::vector<Operand> fields;
};
|
不同 kind 使用的成员如下:
| kind |
有意义的数据 |
dump 形式 |
record |
dst、fields |
dst = record field... |
record_predicate |
dst、lhs |
dst = record? lhs |
record_size |
dst、lhs |
dst = size lhs |
field |
dst、lhs、field_index |
dst = get lhs index |
fields 是编译器内部的 std::vector<Operand>。它与 AST 中的 vector 作用不同:AST vector 拥有尚未求值的表达式子树,IR vector 保存已经 lowering 成简单 operand 的结果。
空 record 对应空 fields,无需特殊 op kind:
operand 仍是逻辑值
Operand 继续只有整数和临时值:
1
2
3
4
|
enum class OperandKind {
integer,
temp,
};
|
源码整数 40 在 IR 中仍打印为 40,不是 tagged word 321。record 的字段列表也保存逻辑 operand:
到了汇编阶段,立即数 operand 才编码成 8n + 1,临时值则已经从前一个 op 得到完整 tagged Value。
lowering 接收 ops 与环境
核心接口仍然明确传入当前操作列表和词法环境:
1
2
3
|
Operand lower_expr(const Expr& expr,
std::vector<Op>& ops,
Env& env);
|
同一个函数既能向主程序的 program_.ops 发射操作,也能向某个 IrFunction::ops 发射操作。record 不会引入第二套 AST 直达后端路径。
lowering record:先字段,后分配
ExprKind::record 分支依次 lower 每个字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
case ExprKind::record: {
const auto& record_expr = static_cast<const RecordExpr&>(expr);
std::vector<Operand> fields;
fields.reserve(record_expr.fields.size());
for (const auto& field : record_expr.fields) {
fields.push_back(lower_expr(*field, ops, env));
}
std::string dst = new_temp();
Op op{OpKind::record,
dst,
Operand::integer(0),
Operand::integer(0),
"",
""};
op.fields = std::move(fields);
ops.push_back(std::move(op));
return Operand::temp(dst);
}
|
循环同时保证语言规定的左到右顺序。考虑:
1
|
(record (+ 20 20) (sub1 3) (+ 1 2))
|
IR 顺序是:
1
2
3
4
5
|
t.0 = 20 + 20
t.1 = sub1 3
t.2 = 1 + 2
t.3 = record t.0 t.1 t.2
return t.3
|
创建对象的 op 最后出现。这意味着汇编 emitter 执行 malloc 时,所有字段 Value 已经准备好并保存在 operand 对应的位置。
对于 (record),循环执行零次,仍然发出:
1
2
|
t.0 = record
return t.0
|
不能把空 record 优化成一个共享常量,否则两次创建将失去不同身份。
lowering record?
predicate 先 lower 子表达式,再发出一元 op:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
case ExprKind::record_predicate: {
const auto& predicate_expr =
static_cast<const RecordPredicateExpr&>(expr);
Operand value = lower_expr(*predicate_expr.expr, ops, env);
std::string dst = new_temp();
ops.push_back({OpKind::record_predicate,
dst,
std::move(value),
Operand::integer(0),
"",
""});
return Operand::temp(dst);
}
|
IR 不把它提前折叠成 C++ bool。dst 将保存源语言整数 1 或 0,汇编 emitter 负责按 tagged integer 编码。
例如:
得到:
1
2
3
|
t.0 = record
t.1 = record? t.0
return t.1
|
lowering size
size 同样先得到对象 operand:
1
2
3
4
5
6
7
8
9
10
11
12
|
case ExprKind::record_size: {
const auto& size_expr = static_cast<const RecordSizeExpr&>(expr);
Operand record = lower_expr(*size_expr.record, ops, env);
std::string dst = new_temp();
ops.push_back({OpKind::record_size,
dst,
std::move(record),
Operand::integer(0),
"",
""});
return Operand::temp(dst);
}
|
IR 只说明“读取 record 长度”。是否为 heap object、header kind 是否为 record、怎样提取 count,都留给机器表示层。
lowering get
对象表达式需要 lowering,index 已是 AST 元数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
case ExprKind::field: {
const auto& get_expr = static_cast<const GetExpr&>(expr);
Operand record = lower_expr(*get_expr.record, ops, env);
std::string dst = new_temp();
ops.push_back({OpKind::field,
dst,
std::move(record),
Operand::integer(0),
"",
"",
get_expr.index});
return Operand::temp(dst);
}
|
field_index 可以大于 1,因为 record 长度不再固定。IR lowerer 不知道 lhs 在运行时会指向哪个对象,不能在这里完成一般的越界判断。
嵌套对象如何摊平
考虑:
1
|
(get (record 0 (record 40 2)) 1)
|
lowering 先处理内层字段:
1
2
3
4
|
t.0 = record 40 2
t.1 = record 0 t.0
t.2 = get t.1 1
return t.2
|
t.2 是内层 record 引用。若源码继续 (get ... 0),才会再出现一个 get t.2 0 并得到 40。
IR 不把嵌套对象折叠成一块连续数据。两个 record op 代表两次独立分配,这正是身份语义要求的结构。
dump 也通过 kind 分派
record op 的 dump 使用循环:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
case OpKind::record: {
std::string line = indent + op.dst + " = record";
for (const auto& field : op.fields) {
line += " " + field.dump();
}
return line + "\n";
}
case OpKind::record_predicate:
return indent + op.dst + " = record? " +
op.lhs.dump() + "\n";
case OpKind::record_size:
return indent + op.dst + " = size " +
op.lhs.dump() + "\n";
case OpKind::field:
return indent + op.dst + " = get " + op.lhs.dump() + " " +
std::to_string(op.field_index) + "\n";
|
这让 mini ir 展示的文字和真正交给汇编 emitter 的 Op 来自同一结构,而不是另造一份只用于演示的字符串 IR。
栈槽分配要认识所有产出值的 op
四种 record 相关操作都有 dst,因此栈槽分配器的 switch 必须包含:
1
2
3
4
5
6
|
case OpKind::record:
case OpKind::record_predicate:
case OpKind::record_size:
case OpKind::field:
assign_dst(slots, op.dst, offset);
break;
|
遗漏任何一种,汇编 emitter 在 store_result(op.dst) 时都会发现未知临时值。
字段 operand 本身不需要在这里新增槽:复杂字段早已由前面的 op 产生临时值,简单整数则作为立即数保存。
compile 必须消费同一份 IrProgram
CLI 编译路径保持:
1
2
3
|
write_file(output_path,
mini::compile_to_assembly(
mini::lower_to_ir(*expr), target));
|
接口是:
1
2
|
std::string compile_to_assembly(const IrProgram& program,
Target target);
|
因此两条观察路径一致:
1
2
|
mini ir AST -> IrProgram -> dump
mini compile AST -> IrProgram -> assembly
|
下一节只消费 IrProgram。它会根据 op.fields.size() 计算对象大小和 header,用循环写入 payload,并为 predicate、size 与 get 生成 heap tag、header kind 和边界逻辑。
6.4 解释器:用共享对象保存一组 Value
上一节
6.6 汇编:分配可变大小对象,并在读取前检查
下一节