跳转至

实验三:中间表示生成

1. 作业概述

本次实验的目标是扩展 TeaLang 编译器的中间表示(IR)生成阶段,使编译器能够为新增的语法特性产生正确的 IR。我们将在已有的 AST → IR 生成流程基础上,在类型转换(conversions.rs)、类型推断(type_infer.rs)和函数体 IR 生成(function_gen.rs)中添加对浮点类型、for 循环、多维数组或方法调用的支持。

1.1 分数构成

部分 分值 说明
三选一 60 分 for 循环、impl 块与方法、多维数组任选其一完整实现 IR 生成
必做 30 分 浮点类型 (f32) + 类型转换 (as) 的 IR 生成
Bonus 10 分 开放任务:自行实现新语法特性的 IR 生成,或对 teac 做出有意义的改进(被合并进主分支)

1.2 特性与测试用例

特性 对应测试用例 涉及改动
浮点类型 (f32) float_basic, float_arith, float_cmp, float_cast, float_func types.rs, value.rs, stmt.rs, function.rs, conversions.rs, type_infer.rs, function_gen.rs
类型转换 (as) float_cast, float_arith, float_func type_infer.rs, function_gen.rs
For-In 循环 for_basic, for_continue, for_mixed, for_nested, for_range type_infer.rs, function_gen.rs
多维数组 array_2d_basic, array_2d_init, array_2d_matmul, array_3d, array_attention conversions.rs, type_infer.rs, function_gen.rs
Impl 块与方法 struct_method_basic, struct_method_calls, struct_method_loop, struct_method_namespace, struct_method_nested module_gen.rs, type_infer.rs, function_gen.rs

共 20 个测试用例。每个对应 tests/<name>/<name>.tea 源程序和 tests/<name>/<name>.out 期望输出。我们需要让 teac --emit ir <file> 对所选的特性涉及的所有测试文件都能成功输出 IR,且编译运行后的输出与 .out 文件一致。

注意:浮点(f32)和类型转换(as)是必做的,因此所有同学的测试用例都包含 float_* 系列。三选一的特性再额外增加 5 个测试。

1.3 交付物

修改以下文件(允许新增文件):

  • src/ir/types.rs — IR 类型定义(新增 Dtype::F32
  • src/ir/value.rs — 操作数定义(新增 FloatConst
  • src/ir/stmt.rs — IR 语句定义(新增 FBiOpFCmpSIToFPFPToSI
  • src/ir/function.rs — 函数生成器的 emit 辅助方法
  • src/ir.rsLlvmIdent 标识符格式化
  • src/ir/gen/conversions.rs — AST 类型 → IR 类型的转换
  • src/ir/gen/type_infer.rs — 局部变量前向类型推断
  • src/ir/gen/function_gen.rs — 函数体 IR 生成
  • src/ir/gen/module_gen.rs — 模块级 IR 生成(impl 块相关)

1.4 运行测试

# 运行全部 teac 主线端到端测试(不包括新增特性的测试)
cargo test

# 运行浮点测试(必做)—— 默认同时检查 AST 解析和 IR 生成
cargo test --features float

# 运行 for 循环测试(三选一)
cargo test --features for-loop

# 运行多维数组测试(三选一)
cargo test --features multi-dim-array

# 运行 impl 方法测试(三选一)
cargo test --features struct-method

# 运行某一个具体的测试
cargo test --features float float_cast

# 只运行 AST 解析检查(跳过 IR 生成,用于实验二阶段)
cargo test --features float,ast-only

# 查看编译器对某个文件的 IR 输出
cargo run -- tests/float_basic/float_basic.tea --emit ir

1.5 测试原理

每个测试用例依次执行两阶段检查:

阶段 1:AST 解析test_ast_parse

  1. teac --emit ast <file> 退出码为 0
  2. stderr 为空
  3. AST 输出非空且包含预期的标识符

阶段 2:IR 执行test_ir

  1. teac --emit ir <file> 生成 LLVM IR(.ll 文件)
  2. clang 将生成的 .ll 与运行时(tests/std/std.c)编译链接为可执行文件
  3. 运行可执行文件,捕获 stdout 和退出码
  4. .out 逐行比对(与端到端测试相同的正确性标准)

添加 --features ast-only 可以跳过 IR 执行检查,仅验证 AST 解析(用于实验一阶段,IR 尚未实现时)。

2. LLVM IR 简介

2.1 LLVM 与中间表示

LLVM 是一套模块化的编译器基础设施,其核心设计理念是将编译过程分为前端(源语言 → IR)和后端(IR → 目标机器码)两个独立阶段,中间通过一种统一的中间表示(Intermediate Representation, IR)衔接。

源代码  ──前端──▶  LLVM IR  ──后端──▶  机器码
(.tea)            (.ll)              (aarch64/x86)

LLVM IR 是一种强类型低级别的虚拟指令集,具有以下特征:

  • 三地址码(three-address code):每条指令最多一个目标寄存器和两个操作数,形如 %r2 = add i32 %r0, %r1
  • 静态单赋值(SSA)形式:每个虚拟寄存器(%r0, %r1, ...)只被赋值一次
  • 显式类型标注:每个值都携带类型信息,如 i32floatptr
  • 平台无关:同一份 .ll 文件可以被 LLVM 后端编译到 x86、AArch64、RISC-V 等多种目标架构

本实验的目标是让 teac 编译器输出合法的 LLVM IR(.ll 文件)。验证时,测试框架调用 clang 将生成的 .ll 与 C 运行时(提供 putint 等 I/O 函数的 std.c)一起编译链接为原生可执行文件,然后运行并比对输出。

2.2 模块结构

一个 .ll 文件对应一个 LLVM 模块(Module),模块由以下顶层实体组成:

; 1. 结构体类型定义
%Counter = type { i32 }

; 2. 全局变量
@g = dso_local global i32 0, align 4

; 3. 外部函数声明(类似 C 的 extern)
declare void @putint(i32)

; 4. 函数定义
define dso_local i32 @main() {
    ; 函数体 ...
}
  • 类型定义% 开头(如 %Counter),定义命名结构体
  • 全局变量@ 开头(如 @g),在整个模块可见
  • 函数声明declare)引入外部符号,不含函数体
  • 函数定义define)包含完整的函数体

2.3 函数与基本块

一个函数由若干基本块(Basic Block)组成。每个基本块是一段顺序执行的指令序列,以标签(label)开头、以终结指令(terminator)结尾:

define i32 @abs(i32 %r0) {
abs:                                    ; 入口基本块,标签与函数名相同
    %r1 = icmp slt i32 %r0, 0          ; 比较 %r0 < 0
    br i1 %r1, label %bb1, label %bb2  ; 终结指令:条件跳转
bb1:                                    ; 基本块 bb1
    %r2 = sub i32 0, %r0               ; 取相反数
    br label %bb3                       ; 终结指令:无条件跳转
bb2:                                    ; 基本块 bb2
    br label %bb3
bb3:                                    ; 基本块 bb3
    %r3 = phi i32 [ %r2, %bb1 ], [ %r0, %bb2 ]
    ret i32 %r3                         ; 终结指令:返回
}

基本块内部没有跳转——执行从第一条指令直线到达终结指令。控制流只发生在基本块之间。

LLVM IR 的终结指令有三种:

终结指令 语法 含义
ret ret i32 %r3ret void 函数返回
br(条件) br i1 %cond, label %T, label %F 根据条件跳转到两个目标之一
br(无条件) br label %target 无条件跳转

2.4 类型系统

LLVM IR 是强类型的——每条指令的操作数和结果都有明确的类型。teac 涉及的类型包括:

LLVM IR 类型 含义 示例
void 无返回值 ret void
i1 1 位整数(布尔) icmp 的结果类型
i32 32 位有符号整数 add i32 %r0, 1
float 32 位 IEEE 754 浮点数 fadd float %r0, %r1
ptr 不透明指针(LLVM 15+) load i32, ptr %r0
[N x T] 包含 N 个 T 类型元素的数组 [4 x i32][3 x [4 x i32]]
%Name 命名结构体 %Counter(引用 type 定义)

注意:LLVM 15 以后采用不透明指针(opaque pointer),所有指针统一为 ptr 类型,不再区分 i32float 等。teac 生成的 IR 使用这种新式指针。

浮点常量在 LLVM IR 中以 IEEE 754 双精度十六进制格式表示。例如 1.0 表示为 0x3FF00000000000003.14 表示为 0x40091EB860000000。即使操作类型是 float(单精度),常量仍以双精度十六进制写入。

注意精度:LLVM 不会自动截断双精度常量到单精度。如果为 float 类型写入了一个在单精度下无法精确表示的双精度十六进制值(如 3.14 的原始双精度编码 0x40091EB851EB851F),LLVM 会直接报错拒绝,因为将该位模式截断到 f32 会丢失精度。正确的做法是先将 f64 值转换为 f32 再扩展回 f64,确保发出的十六进制常量在单精度下是精确的。例如 3.14_f64 as f32 得到 3.1400001049041748,其双精度编码正是 0x40091EB860000000

2.5 指令集

LLVM IR 的指令可以分为以下几类。下面列出 teac 需要生成的指令:

算术运算

整数和浮点使用不同的指令助记符

%r2 = add  i32   %r0, %r1     ; 整数加法
%r2 = sub  i32   %r0, %r1     ; 整数减法
%r2 = mul  i32   %r0, %r1     ; 整数乘法
%r2 = sdiv i32   %r0, %r1     ; 有符号整数除法

%r2 = fadd float %r0, %r1     ; 浮点加法
%r2 = fsub float %r0, %r1     ; 浮点减法
%r2 = fmul float %r0, %r1     ; 浮点乘法
%r2 = fdiv float %r0, %r1     ; 浮点除法

比较

整数使用 icmp,浮点使用 fcmp,结果类型均为 i1

%r2 = icmp eq  i32   %r0, %r1   ; 整数相等
%r2 = icmp slt i32   %r0, %r1   ; 有符号小于

%r2 = fcmp oeq float %r0, %r1   ; 浮点有序相等(ordered equal)
%r2 = fcmp ogt float %r0, %r1   ; 浮点有序大于

浮点比较的 o 前缀表示 *ordered。如果任一操作数是 NaN,结果为 false。这是最常用的浮点比较语义。

内存操作

%r0 = alloca i32, align 4              ; 在栈上分配一个 i32 的空间,返回 ptr
store i32 42, ptr %r0, align 4         ; 将值 42 写入 %r0 指向的地址
%r1 = load i32, ptr %r0, align 4       ; 从 %r0 指向的地址读取一个 i32

getelementptr(GEP)是 LLVM IR 中最复杂的指令,用于计算复合类型(数组、结构体)内元素的地址,不访问内存

; 计算数组 arr[2] 的地址:从 [10 x i32]* 得到 i32*
%r1 = getelementptr [10 x i32], ptr %r0, i32 0, i32 2

; 计算结构体第 0 个字段的地址
%r1 = getelementptr %Counter, ptr %r0, i32 0, i32 0

GEP 的第一个索引 i32 0 是对指针本身的"解引用"(因为 %r0 的语义是"指向 [10 x i32] 的指针"),第二个索引才是数组/结构体内部的下标。

类型转换

%r1 = sitofp i32   %r0 to float   ; 有符号整数 → 浮点
%r1 = fptosi float %r0 to i32     ; 浮点 → 有符号整数(截断)

其他

call void @putint(i32 %r0)              ; 无返回值的函数调用
%r1 = call i32 @compute(i32 %r0)        ; 有返回值的函数调用
%r3 = phi i32 [ %r1, %bb1 ], [ %r2, %bb2 ]  ; φ 节点(见下节)

2.6 SSA 与 φ 节点

LLVM IR 采用 静态单赋值(Static Single Assignment, SSA)形式:每个虚拟寄存器(%r0, %r1, ...)在整个函数中只被定义一次。这个约束使得数据流分析和优化变得简单,但也带来一个问题——当同一个变量在不同控制流路径上被赋予不同的值时,如何在合流点确定使用哪个值?

答案是 φ(phi)节点。φ 节点放在基本块的开头,根据控制流的来源选择对应的值:

; if (x > 0) result = 1; else result = 0;
main:
    %r0 = icmp sgt i32 %x, 0
    br i1 %r0, label %bb1, label %bb2
bb1:
    br label %bb3
bb2:
    br label %bb3
bb3:
    %result = phi i32 [ 1, %bb1 ], [ 0, %bb2 ]

phi i32 [ 1, %bb1 ], [ 0, %bb2 ] 的含义:如果控制流从 %bb1 到达,取值 1;如果从 %bb2 到达,取值 0

φ 节点在循环中尤其重要。以一个求和循环为例:

; sum = 0; i = 0; while (i < 10) { sum += i; i++; }
entry:
    br label %loop
loop:
    %i   = phi i32 [ 0, %entry ], [ %i_next,   %loop ]
    %sum = phi i32 [ 0, %entry ], [ %sum_next,  %loop ]
    %sum_next = add i32 %sum, %i
    %i_next   = add i32 %i, 1
    %cond = icmp slt i32 %i_next, 10
    br i1 %cond, label %loop, label %exit
exit:
    ret i32 %sum_next

循环变量 %i 和累加器 %sum 都通过 φ 节点在循环头部合并——第一次进入时取初始值(来自 %entry),后续迭代取上一轮的更新值(来自 %loop 自身)。

2.7 内存模型:alloca / load / store

尽管 LLVM IR 是 SSA 形式,但源语言中的可变局部变量天然违反"只赋值一次"的约束。LLVM 用 alloca / load / store 三件套解决这个问题:

  1. alloca:在栈上分配空间,返回一个指针
  2. store:将值写入指针指向的位置(可执行多次——修改的是内存,不是寄存器)
  3. load:从内存中读取值到一个新的 SSA 寄存器

例如,源代码 let mut x: i32 = 1; x = x + 1; 的 IR:

%r0 = alloca i32, align 4         ; 为 x 分配栈空间
store i32 1, ptr %r0, align 4     ; x = 1
%r1 = load i32, ptr %r0, align 4  ; 读取 x 的当前值
%r2 = add i32 %r1, 1              ; x + 1
store i32 %r2, ptr %r0, align 4   ; x = x + 1(写回同一地址)

每次修改变量都生成一对 load + store,而不是直接修改寄存器。这样 %r0(指针)只被赋值一次,满足 SSA 约束。LLVM 后端的 mem2reg 优化 pass 会将这种模式提升为直接使用寄存器和 φ 节点,消除不必要的内存访问。

teac 的 IR 生成采用了混合策略:简单的临时值(如表达式求值的中间结果)直接使用 SSA 寄存器;而需要取地址或被多次赋值的变量使用 alloca/load/store 模式。循环中被修改的变量在 mem2reg 优化后会转换为 φ 节点形式。

2.8 完整示例

以下是一个完整的 .ll 文件,展示了上述所有概念的综合运用:

declare void @putint(i32)

define dso_local i32 @main() {
main:
    %r0 = alloca i32, align 4
    store i32 10, ptr %r0, align 4
    %r1 = load i32, ptr %r0, align 4
    %r2 = icmp sgt i32 %r1, 5
    br i1 %r2, label %bb1, label %bb2
bb1:
    call void @putint(i32 %r1)
    br label %bb3
bb2:
    call void @putint(i32 0)
    br label %bb3
bb3:
    ret i32 0
}

这个程序:分配一个局部变量并赋值 10,比较是否大于 5,根据结果打印不同的值,最后返回 0。可以直接用 clang test.ll -o test && ./test 编译运行(前提是提供 putint 的实现)。

2.9 teac 的 IR 类型(Dtype)

Dtype 是 teac 中对 LLVM IR 类型的建模,定义在 src/ir/types.rs

enum Dtype {
    Void,                              // void
    I1,                                // i1
    I32,                               // i32
    F32,                               // float
    Struct { type_name: String },      // %Name
    Pointer { pointee: Box<Dtype> },   // ptr
    Array { element: Box<Dtype>, length: Option<usize> },  // [N x T]
}

Dtype 打印为 LLVM IR 风格的文本:I32i32F32floatArray { I32, 4 }[4 x i32]Pointerptr

2.10 teac 的操作数(Operand)

Operand 表示一条 IR 指令的操作数,定义在 src/ir/value.rs

enum Operand {
    Const(IntConst),         // 整数常量,如 42
    FloatConst(FloatConst),  // 浮点常量,如 0x40091EB860000000
    Local(Local),            // SSA 局部值,如 %r3
    Global(GlobalRef),       // 全局变量引用,如 @c
}

FloatConst 内部以 f64 存储值,打印时输出 IEEE 754 双精度十六进制表示(0x{:016X}),这是 LLVM IR 的标准浮点常量格式(参见 2.4 节)。当 dtypeF32 时,Display 实现必须先将 f64 值转换为 f32 再扩展回 f64 来取 bits,确保输出的十六进制常量在单精度下精确可表示(详见 3.5 节"FloatConst 的精度陷阱")。

2.11 teac 的语句(Stmt)

每条 IR 指令是一个 Stmt,其核心是 StmtInner 枚举。每个变体对应 2.5 节介绍的一条 LLVM IR 指令:

enum StmtInner {
    Call(..),      // call void @putint(i32 1)
    Load(..),      // %r3 = load float, ptr %r0, align 4
    Phi(..),       // %r33 = phi i32 [ 0, %main ], [ %r8, %bb3 ]
    BiOp(..),      // %r6 = add i32 %r34, %r33
    FBiOp(..),     // %r5 = fdiv float %r4, 0x4000000000000000
    Alloca(..),    // %r0 = alloca float, align 4
    Cmp(..),       // %r3 = icmp slt i32 %r33, 10
    FCmp(..),      // %r5 = fcmp ogt float %r3, %r4
    CJump(..),     // br i1 %r3, label %bb2, label %bb4
    Label(..),     // bb1:
    Store(..),     // store float 0x40091EB860000000, ptr %r0, align 4
    Jump(..),      // br label %bb1
    Gep(..),       // %r7 = getelementptr [3 x [4 x i32]], ptr %r0, i32 0, i32 %r37
    Return(..),    // ret i32 0
    SIToFP(..),    // %r2 = sitofp i32 42 to float
    FPToSI(..),    // %r8 = fptosi float %r7 to i32
}

2.12 从 AST 到 IR 的流水线

module_gen.rs 中的 IrGenerator::generate() 分三遍扫描驱动整个降低过程:

Pass 职责 关键代码
Pass 1 处理 use 语句,注册外部符号 handle_use_stmt
Pass 2 注册全局变量、函数签名、结构体定义 handle_fn_def
Pass 3 对每个函数做类型推断 + IR 生成 type_infer::infer_functionFunctionGenerator::generate

Pass 3 是本实验的核心。对每个函数定义,编译器先调用 type_infer::infer_function 做前向局部类型推断,得到 HashMap<String, Dtype>(每个局部变量的具体类型),然后传入 FunctionGenerator 逐条语句生成 IR。

3. 浮点类型(f32)与类型转换(as)[必做]

3.1 新增 IR 构件

本次实验中,浮点支持涉及以下 IR 层面的新构件:

构件 定义位置 说明
Dtype::F32 types.rs 浮点类型,打印为 float
FloatConst value.rs 浮点常量操作数,存储 f64
FloatBinOp stmt.rs 浮点二元运算符:FAdd, FSub, FMul, FDiv
FCmpPredicate stmt.rs 浮点比较谓词:OEq, ONe, OGt, OGe, OLt, OLe
FBiOpStmt stmt.rs 浮点二元运算指令
FCmpStmt stmt.rs 浮点比较指令
SIToFPStmt stmt.rs i32float 转换指令
FPToSIStmt stmt.rs floati32 转换指令

这些类型和语句结构需要在 IR 层定义(types.rsvalue.rsstmt.rs),并在 function.rs 中提供 emit 辅助方法,最终在 IR 生成阶段(function_gen.rs 等)正确地产生这些指令。可以参考已有的整数类型对应物(如 BiOpStmtFBiOpStmtCmpStmtFCmpStmt)的定义和 Display 实现来编写浮点版本。

3.2 涉及的改动

conversions.rs:AST 类型到 IR 类型的转换

需要将 ast::BuiltIn::Float 正确映射到 Dtype::F32。框架中 From<&ast::TypeSpecifier> for DtypeBuiltIn 分支将所有内建类型一律映射为 I32

ast::TypeSpecifierInner::BuiltIn(_) => Self::I32,

需要拆分成两个匹配臂,分别处理 IntFloat。改动仅一行,但这是整个浮点支持的基础——没有这个映射,所有 f32 类型的声明和定义在 IR 层都会被错误地视为 i32

type_infer.rs:前向类型推断

需要增加以下几项处理:

  1. type_of_expr_unit:为 FloatNum 字面量返回 Dtype::F32;为 As(inner, ty) 表达式验证操作数类型后返回 Dtype::from(ty)(即目标类型决定结果类型)。
  2. type_of_arith_expr:算术表达式 left op right 中,如果任一操作数为 F32,则结果类型为 F32(而非原来一律返回 I32)。这与 IR 生成阶段根据操作数类型选择 FBiOp 还是 BiOp 的逻辑对应。
  3. check_bool_unit:比较表达式中需要检查两侧操作数的类型(调用 type_of_expr_unit),但不需要强制左右类型一致。类型推断器只验证标识符已定义,具体的 icmp vs fcmp 选择留给 IR 生成器。

function_gen.rs:函数体 IR 生成

这是改动量最大的文件,需要处理:

  1. 浮点字面量:在 handle_expr_unit 中,将 ExprUnitInner::FloatNum(v) 转为 Operand::FloatConst(FloatConst { dtype: Dtype::F32, val: *v })
  2. 浮点运算:在 handle_arith_biop_expr 中,若任一操作数为 f32,则将另一操作数隐式转换为 f32sitofp),然后生成 FBiOpfadd/fsub/fmul/fdiv)而非 BiOpadd/sub/mul/sdiv)。
  3. 浮点比较:在 handle_com_op_expr 中,若任一操作数为 f32,生成 FCmpfcmp oeq/one/...)而非 Cmpicmp eq/ne/...),同样需要对整数侧操作数做隐式转换。
  4. 类型转换as 表达式生成 SIToFPi32 → f32)或 FPToSIf32 → i32)指令。若源类型和目标类型相同,则直接返回原操作数。
  5. 隐式类型强转:在赋值(handle_assignment_stmt)、变量定义(handle_var_def)和返回语句(handle_return_stmt)处,当值的类型与目标类型不匹配时(如 f32 变量赋值一个 i32 表达式),需要插入隐式的 sitofp / fptosi
  6. 函数调用返回值F32 也是合法的函数返回类型。handle_fn_call(表达式位置的函数调用)、隐式 return 等处需要增加对 Dtype::F32 的处理。

3.3 示例

源程序:

let x: f32 = 3.14;
let y: f32 = x + 1.0;
let n: i32 = y as i32;

生成的 IR:

%r0 = alloca float, align 4
store float 0x40091EB860000000, ptr %r0, align 4
%r1 = load float, ptr %r0, align 4
%r2 = fadd float %r1, 0x3FF0000000000000
%r3 = alloca float, align 4
store float %r2, ptr %r3, align 4
%r4 = load float, ptr %r3, align 4
%r5 = fptosi float %r4 to i32

关键点:

  • 浮点常量 3.14 转为 IEEE 754 双精度十六进制 0x40091EB860000000
  • 加法使用 fadd 而非 add
  • as i32 转为 fptosi float %r4 to i32

再看反向转换(i32f32):

let x: i32 = 42;
let f: f32 = x as f32;
%r2 = sitofp i32 42 to float
%r3 = alloca float, align 4
store float %r2, ptr %r3, align 4

3.4 测试

cargo test --features float

每个测试生成 IR 后编译运行,与 .out 文件比对输出。

3.5 实现提示

基础设施层:Dtype::F32 的注册

src/ir/types.rs 中,Dtype 枚举需要新增 F32 变体,其 Display 输出为 float(这是 LLVM IR 中单精度浮点的类型名)。同时,FunctionType::try_from(从 ast::FnDecl 构建函数签名时验证返回类型)的白名单需要从 Void | I32 扩展为 Void | I32 | F32,否则返回 f32 的函数会被拒绝。

基础设施层:emit 辅助函数

src/ir/function.rs 中的 FunctionGenerator 已有 emit_biopemit_cmp 等方法。需要新增对应的浮点版本:

  • emit_fbiop — 发出 fadd/fsub/fmul/fdiv
  • emit_fcmp — 发出 fcmp
  • emit_sitofp — 发出 sitofp(i32 → float)
  • emit_fptosi — 发出 fptosi(float → i32)

这些方法只是简单的 self.irs.push(Stmt::as_xxx(...)) 封装,但将 IR 构造细节隔离在一处。

隐式类型强转辅助函数

function_gen.rs 中,多个场景需要在 i32f32 之间进行隐式转换。建议抽取三个辅助函数来避免重复代码:

/// 将 i32/i1 操作数提升为 f32(已经是 f32 则不变)
fn coerce_to_f32(&mut self, operand: Operand) -> Operand { ... }

/// 将 f32 操作数截断为 i32(已经是 i32 则不变)
fn coerce_to_i32(&mut self, operand: Operand) -> Operand { ... }

/// 根据目标类型选择 coerce_to_f32 或 coerce_to_i32(类型已匹配则不变)
fn coerce_to(&mut self, operand: Operand, target: &Dtype) -> Operand { ... }

这些函数在以下位置被使用:

  • handle_assignment_stmt:目标指针的 pointee 类型与右值类型不匹配时
  • handle_var_def:定义 let x: f32 = <i32 expr>
  • handle_return_stmt:函数返回类型为 f32 但 return 值为 i32
  • handle_arith_biop_expr:混合运算 f32 + i32 时提升整数侧
  • handle_com_op_expr:混合比较 f32 > i32 时提升整数侧

FloatConst 的精度陷阱

FloatConstDisplay 实现(src/ir/value.rs)需要特别注意:当 dtypeF32 时,不能直接以 self.val.to_bits() 输出十六进制。LLVM 要求 float 类型的十六进制常量必须在单精度下精确可表示,否则会拒绝编译。正确的做法是先将 f64 值截断为 f32 再扩展回 f64

match &self.dtype {
    Dtype::F32 => {
        let as_f32 = self.val as f32;
        let widened = as_f32 as f64;
        write!(f, "0x{:016X}", widened.to_bits())
    }
    _ => write!(f, "0x{:016X}", self.val.to_bits()),
}

这确保了 3.14 输出为 0x40091EB860000000(f32 精确值)而非 0x40091EB851EB851F(f64 精确值但 f32 不精确)。

LlvmIdentstd:: 前缀处理

use std; 语句会将 std.teah 中声明的函数以 std::putintstd::getch 等带前缀的名称注册到函数表中。但 C 运行时(tests/std/std.c)导出的符号是不带前缀的 putintgetch

src/ir.rs 中的 LlvmIdent(用于格式化 IR 中的函数名和全局变量名)需要在输出时去除 std:: 前缀,使发出的 IR 符号与 C 运行时匹配。例如 std::putint 应输出为 @putint 而非 @"std::putint"

4. For-In 循环 [三选一]

4.1 Lowering 策略

for i in start..end { body } 需要被降低为一个包含四个基本块的循环结构:

  1. 前序块:计算 startend 的值,跳转到测试块
  2. 测试块(bb_test):用 φ 节点合并循环变量的初始值和每次迭代后的更新值,比较 i < end,条件跳转到循环体或出口
  3. 循环体(bb_body):执行循环体语句
  4. 递增块(bb_incr)i = i + 1,跳回测试块
  5. 出口块(bb_exit):循环结束后继续执行

控制流图:

graph TD
    A[前序块] -->|br| B[bb_test]
    B -->|i < end| C[bb_body]
    B -->|i >= end| E[bb_exit]
    C --> D[bb_incr]
    D -->|br| B

break 跳转到 bb_exitcontinue 跳转到 bb_incr(先递增再回到测试,这与 while 的 continue 语义不同)。

4.2 φ 节点与 SSA

for 循环中,循环变量 i 和循环体内修改的变量都需要通过 φ 节点在测试块入口处合并来自前序块和递增块的值。例如对于 for i in 0..10 { sum = sum + i; },测试块开头需要两个 φ 节点:

bb1:
    %r33 = phi i32 [ 0, %main ], [ %r8, %bb3 ]   ; i: 来自前序的0 或 递增后的值
    %r34 = phi i32 [ 0, %main ], [ %r6, %bb3 ]   ; sum: 来自前序的0 或 循环体更新后的值
    %r3 = icmp slt i32 %r33, 10
    br i1 %r3, label %bb2, label %bb4

这与 while 循环的降低策略本质相同,teac 的 IR 是 SSA 形式,循环中被修改的变量必须经过 φ 节点合流。

4.3 示例

源程序:

let sum: i32 = 0;
for i in 0..10 {
    sum = sum + i;
}
std::putint(sum);

生成的 IR(经 mem2reg 优化后):

main:
    br label %bb1
bb1:
    %r33 = phi i32 [ 0, %main ], [ %r8, %bb3 ]
    %r34 = phi i32 [ 0, %main ], [ %r6, %bb3 ]
    %r3 = icmp slt i32 %r33, 10
    br i1 %r3, label %bb2, label %bb4
bb2:
    %r6 = add i32 %r34, %r33
    br label %bb3
bb3:
    %r8 = add i32 %r33, 1
    br label %bb1
bb4:
    call void @putint(i32 %r34)

关键点:

  • bb1 是测试块,%r33 是循环变量 i 的 φ 节点
  • bb2 是循环体,执行 sum = sum + i
  • bb3 是递增块,i = i + 1
  • bb4 是出口块

4.4 类型推断

for i in start..end 中,循环变量 i 的类型由 startend 表达式决定(在 teac 中固定为 i32)。类型推断器需要在处理 for 语句时:

  1. iResolved(I32) 注册到类型环境中
  2. 推断循环体内的变量类型
  3. 与 while 循环类似,将循环体的环境合并回循环前的环境

4.5 测试

cargo test --features for-loop

每个测试生成 IR 后编译运行,与 .out 文件比对输出。

4.6 实现提示

参考:while 循环的现有实现

框架中已有完整的 while 循环实现,for 循环可以直接对照着写。

类型推断type_infer.rs 中的 process_while):

fn process_while(&mut self, stmt: &ast::WhileStmt) -> Result<(), Error> {
    self.check_bool_unit(&stmt.bool_unit)?;

    let mut body_ctx = self.fork(self.env.clone());
    body_ctx.process_stmts(&stmt.stmts)?;
    let body_env = body_ctx.env;

    self.merge_env_single(&body_env)?;
    Ok(())
}

流程:验证条件表达式 → fork 环境 → 处理循环体 → 合并回外层环境。

IR 生成function_gen.rs 中的 handle_while_stmt):

pub fn handle_while_stmt(&mut self, stmt: &ast::WhileStmt) -> Result<(), Error> {
    let test_label = self.alloc_basic_block();
    let true_label = self.alloc_basic_block();
    let false_label = self.alloc_basic_block();

    self.emit_jump(test_label.clone());               // 前序块 → test

    self.emit_label(test_label.clone());               // test 块
    self.handle_bool_unit(&stmt.bool_unit, true_label.clone(), false_label.clone())?;

    self.emit_label(true_label);                       // body 块
    self.enter_scope();
    for s in &stmt.stmts {
        self.handle_block(s, Some(&test_label), Some(&false_label))?;
    }
    self.exit_scope();
    self.emit_jump(test_label);                        // 回边 → test

    self.emit_label(false_label);                      // exit 块
    Ok(())
}

while 是三个基本块(test / body / exit),continue → test,break → exit。

type_infer.rsprocess_for

for 循环的类型推断在 while 的基础上增加两点:

  1. 循环变量的绑定for i in start..end 中,i 的类型固定为 I32。在处理循环体之前,需要把 iResolved(I32) 注入到 forked 的 环境中(而不是当前环境——循环变量的作用域仅限于循环体内部)。
  2. 循环变量的作用域:在把循环体的环境合并回外层环境之前,需要从循环体环境中 移除 循环变量绑定,防止 i 泄漏到外层作用域。

提示:先在 pre-loop 环境中验证 startend 表达式(确保其中引用的变量已定义),然后 fork 环境、绑定循环变量、处理循环体、移除循环变量、合并环境。

function_gen.rshandle_for_stmt

for 循环在 while 的三块结构(test / body / exit)基础上增加一个 incr 块,形成四块结构。同样使用 alloca/load/store 策略,依赖 mem2reg 提升为 SSA 形式。实现步骤:

  1. 分配四个基本块:test、body、incr、exit。
  2. 前序块:计算 startend,为循环变量和上界各分配一个栈槽(alloca),将初始值 store 到对应栈槽。用 enter_scope() 开启新作用域,将循环变量注册到局部变量表中。
  3. test 块load 循环变量和上界,icmp slt 比较,条件跳转到 body 或 exit。
  4. body 块:逐条处理循环体语句。continue 的目标设为 incr_label(先递增再回到测试),break 的目标设为 exit_label。body 末尾无条件跳转到 incr。
  5. incr 块load 循环变量的当前值,add i32 %, 1store 回栈槽,跳转到 test。
  6. exit 块exit_scope() 退出作用域。

具体示例可参考 4.3 节。

与 while 的关键区别:

while for
基本块数 3(test / body / exit) 4(test / body / incr / exit)
循环变量 无(由用户在循环外手动管理) 有(i),需要 alloca 栈槽 + enter_scope/exit_scope
continue 目标 test(直接重新判断条件) incr(先递增 i 再回到 test)
条件判断 handle_bool_unit(通用布尔表达式) 手动 icmp slti < end
回边 body → test incr → test

5. 多维数组 [三选一]

5.1 嵌套的 Dtype

多维数组在 IR 层是嵌套的 Dtype::Array。例如 [[i32; 4]; 3] 对应:

Dtype::Array {
    element: Box::new(Dtype::Array {
        element: Box::new(Dtype::I32),
        length: Some(4),
    }),
    length: Some(3),
}

打印为 [3 x [4 x i32]]。这与 LLVM IR 的多维数组表示一致。

5.2 多级 GEP

访问多维数组元素需要多级 getelementptr(GEP)指令链。每一级 GEP 将指针从外层数组"索引进"一层,得到内层数组的指针,再用下一级 GEP 继续索引。

例如 mat[i][j]mat: [[i32; 4]; 3])的降低:

; 第一级 GEP: 从 [3 x [4 x i32]]* 索引到 [4 x i32]*
%r7 = getelementptr [3 x [4 x i32]], ptr %r0, i32 0, i32 %r_i

; 第二级 GEP: 从 [4 x i32]* 索引到 i32*
%r9 = getelementptr [4 x i32], ptr %r7, i32 0, i32 %r_j

每级 GEP 的第一个索引是 0("解引用"指针本身),第二个索引是实际的数组下标。

5.3 为什么现有代码已经"几乎"支持多维数组

teac 的一维数组 GEP 生成逻辑已经处理了 identifier[expr] 形式。对于多维数组 mat[i][j],AST 将其解析为一条后缀链:先是 mat[i](产生一个内层数组的指针),然后 [j](对内层数组再索引一次)。

关键在于:每次 [expr] 后缀的处理都产生一个 GEP,输入指针的类型从外层数组变为内层数组。如果 compose_var_decl_dtype 能正确将 [[i32; 4]; 3] 转换为嵌套的 Dtype::Array,且 function_gen.rs 中的 GEP 生成逻辑能正确处理 Array { Array { ... } } 类型的元素类型提取,那么多维数组就能自然地通过递归后缀处理来支持。

因此本特性的核心工作量在于:

  1. conversions.rs:确保嵌套的 ast::TypeSpecifierInner::Array 能递归转换为嵌套的 Dtype::Array
  2. type_infer.rs:在推断包含多维数组的表达式时,正确剥离 Array 的外层
  3. function_gen.rs:确保 GEP 生成在遇到 Array { element: Array { ... } } 时能正确提取内层元素类型

5.4 示例

源程序:

let mat: [[i32; 4]; 3];
mat[1][2] = 42;
let v: i32 = mat[1][2];

生成的 IR:

%r0 = alloca [3 x [4 x i32]], align 4

; 写入 mat[1][2] = 42
%r1 = getelementptr [3 x [4 x i32]], ptr %r0, i32 0, i32 1
%r2 = getelementptr [4 x i32], ptr %r1, i32 0, i32 2
store i32 42, ptr %r2, align 4

; 读取 mat[1][2]
%r3 = getelementptr [3 x [4 x i32]], ptr %r0, i32 0, i32 1
%r4 = getelementptr [4 x i32], ptr %r3, i32 0, i32 2
%r5 = load i32, ptr %r4, align 4

5.5 测试

cargo test --features multi-dim-array

每个测试生成 IR 后编译运行,与 .out 文件比对输出。

6. Impl 块与方法 [三选一]

6.1 名称修饰(Name Mangling)

TeaLang 中方法通过 impl 块定义,在 IR 层面以名称修饰的方式与自由函数共存。修饰规则是 TypeName::method_name。例如:

struct Counter { value: i32 }
impl Counter {
    fn get(&self) -> i32 { return self.value; }
    fn add(&mut self, delta: i32) -> i32 { ... }
}

在 IR 中产生两个函数定义:

define dso_local i32 @"Counter::get"(ptr %r0) { ... }
define dso_local i32 @"Counter::add"(ptr %r0, i32 %r1) { ... }

6.2 self 参数的处理

&self&mut self 在 IR 层面统一表示为 ptr 参数(指向 %Counter 结构体的指针)。在函数签名中,self 作为第一个参数,类型是 Dtype::ptr_to(Dtype::Struct { type_name: "Counter" })

在函数体内,self.value 被降低为一条 getelementptr 指令:

%r1 = getelementptr %Counter, ptr %r0, i32 0, i32 0
%r2 = load i32, ptr %r1, align 4

self 参数(%r0)不经过 alloca+store 间接层——它直接注册到局部变量表中,避免额外的 load/store。

6.3 模块级处理(Pass 2)

module_gen.rs 的 Pass 2 需要处理 ImplDef 节点:

  1. 遍历 impl_def.methods,对每个方法:
  2. TypeName::method_name 作为修饰后的名称
  3. 构造 FunctionType,将 self 参数替换为 ptr
  4. 注册到 RegistryModule::function_list

Pass 3 中,方法的 IR 生成与自由函数完全相同,只是名称使用修饰后的版本。

6.4 方法调用的解析

调用方有三种语法形式,最终都降低为相同的 IR:

c[0].get()                  // 点语法
c[0].add_twice(7)           // 带参数的点语法
Counter::get(c[0])          // 显式命名空间语法

三种形式都产生:

%r0 = getelementptr ..., ptr @c, i32 0, i32 0   ; 获取 c[0] 的指针
%r1 = call i32 @"Counter::get"(ptr %r0)

function_gen.rs 中,方法调用的 IR 生成需要:

  1. 确定接收者的指针:计算 self 对应的地址操作数(可能经过数组索引和成员访问)
  2. 拼接修饰名称:从接收者的类型(Dtype::Struct { type_name } 或其指针)提取类型名,拼接为 TypeName::method
  3. 生成 Call 指令:将接收者指针作为第一个参数传入

对于 Self::method(self, ...) 形式(在 impl 块内部调用同类型方法),需要将 Self:: 替换为当前 impl 的类型名。

6.5 示例

源程序:

struct Counter { value: i32 }
impl Counter {
    fn get(&self) -> i32 { return self.value; }
    fn add(&mut self, delta: i32) -> i32 {
        self.value = self.value + delta;
        return self.value;
    }
}

let c: [Counter; 1];
fn main() -> i32 {
    c[0].value = 10;
    std::putint(c[0].get());
    return c[0].get();
}

生成的 IR(节选):

%Counter = type { i32 }

@c = dso_local global [1 x %Counter] zeroinitializer, align 4

define dso_local i32 @"Counter::get"(ptr %r0) {
"Counter::get":
    %r1 = getelementptr %Counter, ptr %r0, i32 0, i32 0
    %r2 = load i32, ptr %r1, align 4
    ret i32 %r2
}

define dso_local i32 @"Counter::add"(ptr %r0, i32 %r1) {
"Counter::add":
    %r3 = getelementptr %Counter, ptr %r0, i32 0, i32 0
    %r4 = getelementptr %Counter, ptr %r0, i32 0, i32 0
    %r5 = load i32, ptr %r4, align 4
    %r7 = add i32 %r5, %r1
    store i32 %r7, ptr %r3, align 4
    %r8 = getelementptr %Counter, ptr %r0, i32 0, i32 0
    %r9 = load i32, ptr %r8, align 4
    ret i32 %r9
}

define dso_local i32 @main() {
main:
    %r0 = getelementptr [1 x %Counter], ptr @c, i32 0, i32 0
    %r1 = getelementptr %Counter, ptr %r0, i32 0, i32 0
    store i32 10, ptr %r1, align 4
    %r2 = getelementptr [1 x %Counter], ptr @c, i32 0, i32 0
    %r3 = call i32 @"Counter::get"(ptr %r2)
    call void @putint(i32 %r3)
    %r8 = getelementptr [1 x %Counter], ptr @c, i32 0, i32 0
    %r9 = call i32 @"Counter::get"(ptr %r8)
    ret i32 %r9
}

6.6 测试

cargo test --features struct-method

每个测试生成 IR 后编译运行,与 .out 文件比对输出。