Skip to content
kingpoemPublic

About

2026 毕昇杯编译实现赛riscv赛道比赛作品

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

LycorisRecompile

南京邮电大学

这是一个 2026 年全国大学生计算机系统能力大赛 编译系统设计赛道 毕昇杯, 由 Rust 编写的 SysY2022 RISC-V 64 编译器

这是什么

LycorisRecompile 接收自组委会的祖传神必 C 语言子集 SysY2022,输出 RV64GC 汇编,目标机器是 BOOM v3 乱序双发射软核,整个项目大约4.7w行,4.3w+ Rust,2500+ ISLE,以及测试脚本,目前官方 199 个测试用例在 QEMU PASS

该神必编译器动工的理由,并不是为了去拿奖,而是为了一碟醋才包的这盘饺子,其初始目的想亲手实现一遍 E-Graph 和 ISLE 的醋, 来源于 Cranelift 基于项重写的声明式指令选择,以及基于等价图的中端优化,比赛和编译器本身只是顺带产出的饺子

Cranelift 的 ISLE 工具链整个搬进了仓库 crates/isle,islec 编译 .isle 规则生成 Rust 代码,其中 ISLE 的介绍博客、语言参考和官方 README 均放在 doc/markdown/ISLE/

总体结构

该神必编译器流水线如下:

SysY 源码 -> Lexer -> Parser -> SSA IR -> [可选的 SSA 解释器] -> ISLE 指令选择 -> 寄存器分配 -> RV64 汇编

每阶段 Pass 用命令行参数 --stage 指定,中间 100% Vibe Coding 而成的神必解释器,直接解释执行 SSA IR,并用于和 QEMU 输出对拍。可以说,几万行石山代码,这个神必解释器占了很大一部分,端到端 prompt 让各路 ai 们一层又一层往里面加补丁,越来越臃肿

SSA IR 块参数与内存令牌

中端则是花费两个星期的反复重构的,诡异和莫名其妙的 SSA IR。

第一个参考自 Cranelift,没有显式 Phi 指令用块参数。基本块像函数一样声明形参,前驱跳转时像调用一样传实参,块参数不需要像 Phi 节点需要知道任何前驱信息,数据流直观且避开了 Phi 指令变换的边界情况,其中 SSA 的构造算法同样参考自 Cranelift,使用 Braun 等人 2013 年的算法,按需插入块参数,为了避免深递归,实现改写成了显式状态机。

第二个是内存令牌,指针和内存状态被拆成两个东西 (ptr, token),每条Read都要消费一个令牌来证明自己读的是哪个版本的内存,每条Write消费旧 token,产生新 token,内存依赖显式成为 SSA 图普通可见的数据依赖边,这受启发自 LLVM Memory SSA,但在当时我写这玩意时,匮乏的编译原理知识没有让我意识到,世界上早就已经有这样的设计了,我又重复发明了轮子

%ptrA, %memA_v0 = array.alloc i32[10]
%memA_v1 = array.put %ptrA, [0], 42, (%memA_v0)   ; 消费 v0,产生 v1
%val     = array.get %ptrA, [1], (%memA_v1)       ; 消费 v1

优化器不再需要做完整的别名分析,不同内存对象各有各的令牌链。函数调用也同样遵循该原则,指针参数必须携带令牌,调用返回新令牌,这依靠 IR 多返回值来表达。

ISLE 指令选择

;; 乘以2的幂常数 -> 左移 强度削减
(rule 2 (lower (has_type (ty_i64 ty) (imul x (pow2_shift k))))
    (let (
        (dst XReg (xreg_new))
        (x_reg XReg (x_operand x))
        (_ Unit (emit (MInst.Slli dst x_reg (imm6_from_i64 k)))))
    (InstOutput.OutputReg dst)))

Inst Color & Sink

同样灾难的是SSA IR的设计缺陷,把比较和分支拆开了:icmp 产生 Bool 值并由 brif 消费,拆成了两条指令,IR 因此简单,但却忘记了 RISC-V 本来就有 blt 这样的融合比较分支

因此,为了给这个缺陷擦屁股,想到 Cranelift 里有一套并不完整的 instruction coloring 与 sinking 机制来处理"两条 IR 指令融合到一条机器指令"的问题,其主要用于 x86 的内存操作数合并。于是我被迫把这套机制完整实现了一遍,用于缝合被解耦的 if 和 cond

具体来说,降级前先按副作用点给指令染色;ISLE 的提取器可以"看穿"一个值,直达定义它的指令,只要定义和使用同色,所谓中间没有副作用边界,且这个值只被用这一次或复算成本可以接受,就允许把定义指令 sink 使用点,融合成一条机器指令,原指令不再单独发射

如下 isle 规则是我写过的最优雅和精妙的代码之一,可惜无人在意😭:

;; 高级封装: Value=>Inst
;; 判断定义Value的Inst能否融合, 如果可以就返回该Inst
(decl peek_def_inst (Inst) Value)
; (extractor (peek_def_inst pinst) 
;     (def_inst (can_sink_inst pinst)))
(extern extractor peek_def_inst peek_def_inst)

;; 判断是否可以融合
;; ⚠️后端实现比较复杂: 需要同时判断:
;;  - InstColor: 判断是否跨越副作用指令
;;  - InputSourceInst: 判断指令的使用次数 & 复制成本
; (decl can_sink_inst (Inst) Inst)
; (extern extractor can_sink_inst can_sink_inst)

;; 融合指令 -- 仅仅只是通知后端进行融合, 不做判断
(decl sink_inst (Inst) Unit)
(extern constructor sink_inst sink_inst)

;; "看穿"一个Value并尝试提取比较运算
(decl peek_icmp_eq (Value Value Inst) Value)
(extractor (peek_icmp_eq x y pinst) (peek_def_inst pinst @ (and 
        (eq x y) 
        (arg1_has_type (and (ty_fit_xreg) (ty_fit_signed)) _))))

(decl peek_icmp_lti (Value Value Inst) Value)
(extractor (peek_icmp_lti x y pinst) 
    (peek_def_inst pinst @ (and 
        (lt x y) 
        (arg1_has_type (and (ty_fit_xreg) (ty_fit_signed)) _))))

; 匹配 eq
(rule 2 (lower_branch (brif (peek_icmp_eq x y pinst) _ _) (two_targets then else))
    (let (
        (x_reg XReg (x_operand x))
        (y_reg XReg (x_operand y))
        (_ Unit (emit (MInst.Beq x_reg y_reg then)))
        (_ Unit (emit (MInst.J else))))
    (sink_inst pinst))
)


; 匹配 lt-signed
(rule 2 (lower_branch (brif (peek_icmp_lti x y pinst) _ _) (two_targets then else))
    (let (
        (x_reg XReg (x_operand x))
        (y_reg XReg (x_operand y))
        (_ Unit (emit (MInst.Blt x_reg y_reg then)))
        (_ Unit (emit (MInst.J else))))
    (sink_inst pinst))
)

具体来说效果如下:

slt   t0, a0, a1
bnez  t0, .L_then ->  blt   a0, a1, .L_then
j     .L_else         j     .L_else

寄存器分配

寄存器分配拆成两个阶段:先用 MIN 算法把每个程序点的寄存器压力降到 k 以下(RISC-V k=25),溢出决策基于操作系统同款的 next-use 距离,并对函数调用点做了额外处理;然后把降压后的程序交给 Appel 与 George 的迭代寄存器合并做 IRC 图着色

理论上,该算法是完美的,降到 k 以下之后干涉图必然可着色,第二阶段不该再失败

时隔一年重新回来看了一眼,Fable 5 给我加了一大堆补丁,让实现变得丑陋起来,但至少能 AC 所有测例了

E-Graph

E-Graph 的设计对标 Cranelift 的 aegraph,IR 里已经加入了表达等价值的 Union 和 Alias 节点,等价类的数据结构,值到等价类的映射,IR 建图,饱和循环的驱动骨架,等价图的 Graphviz 可视化,都写完了;六百多行的 ISLE 绑定层已经把 SSA 的全部指令模式暴露给了规则语言,islec 的代码生成管道也接通了。这些东西都在仓库里,都能编译。

构建与运行

需要 Rust 工具链(edition 2024)、CMake 3.25+、支持 C++23 的宿主编译器、 riscv64-linux-gnu-gcc 和 qemu-riscv64。Cargo 会自动构建配套的 SMTVC/2 backend;superopt 默认开启,并在 backend 缺失或协议握手失败时立即退出。仅在明确 排障时使用 --no-superopt。

# 构建
RUSTFLAGS="-A warnings" cargo build --release

# 编译单个文件到汇编
cargo run --release -- input.sy -S -o output.s

# 单文件完整验证(编译 -> 交叉链接 -> QEMU 执行 -> 对拍)
./scripts/run/03-simple-run.sh run qemu tests_self/99-simple-add.sy

# 跑全部测试
./scripts/run/03-simple-run.sh qemu

官方测试数据包含数百MB的神必测试输入/输出 case,因此全部以 submodule 形式存在,想找的自己去找 tests_self/ 有多个用例可用,修改 .isle 规则后,需要手动运行 ./scripts/run/04-isle-gen.sh 重新生成 selection.rs

项目结构

compiler/src/
├── lexer/        手写词法分析
├── parser/       手写递归下降
├── ssa/          SSA IR、Braun 构造、优化 Pass、E-Graph
├── interpreter/  SSA 解释器(差分测试用)
├── asm/          ISLE 指令选择、MIN 溢出、IRC 着色、栈帧
└── utils/        RefMap 等基础设施
crates/isle/      Cranelift ISLE 工具链(vendored)
doc/markdown/    设计文档与 ISLE 中文翻译
tests_self/       自写测试用例

论文与致谢

这个编译器参考了下面这些工作:

crates/ ISLE 工具链来自 wasmtime Apache-2.0 WITH LLVM-exception

许可

主仓库代码以 MIT 许可发布

About

2026 毕昇杯编译实现赛riscv赛道比赛作品

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages