章节目录

结构化 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:

1
t.0 = record

operand 仍是逻辑值

Operand 继续只有整数和临时值:

1
2
3
4
enum class OperandKind {
    integer,
    temp,
};

源码整数 40 在 IR 中仍打印为 40,不是 tagged word 321。record 的字段列表也保存逻辑 operand:

1
t.0 = record 40 2

到了汇编阶段,立即数 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
(record? (record))

得到:

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 汇编:分配可变大小对象,并在读取前检查

下一节