
Cranelift 中 ISLE 的集成机制指令选择 DSL 的构建、代码生成与约束设计【免费下载链接】wasmtimeA lightweight WebAssembly runtime that is fast, secure, and standards-compliant项目地址: https://gitcode.com/gh_mirrors/wa/wasmtimeISLEInstruction Selection/Lowering Expressions是 Bytecode Alliance 为 Cranelift 设计的一门领域专用语言DSL用于以声明式重写规则的方式编写指令选择instruction selection与指令降级lowering逻辑。本文以 Cranelift 官方文档 isle-integration.md 为主线结合当前仓库源码系统讲解 ISLE 如何接入 Cranelift 的构建系统、生成代码如何与后端粘合、降级规则必须遵守的纯函数与 SSA 约束、副作用隔离策略以及隐式类型转换的设计约定。读完本文你将理解 Cranelift 各 ISA 后端x64、aarch64 等中inst.isle、lower.isle等文件的角色并掌握通过环境变量调试 ISLE 生成代码的实操方法。ISLE 是什么ISLE 是一门用于编写指令选择与重写规则的领域专用语言。ISLE 的源文本会被编译成 Rust 代码由 cranelift/isle/isle/src 中的编译器实现完成这一翻译源码目录下可以看到完整的编译器流水线模块包括lexer.rs词法分析、parser.rs语法分析、sema.rs语义分析、codegen.rsRust 代码生成、compile.rs编译入口以及overlap.rs规则重叠检测、trie_again.rs决策树构建等。从 cranelift/isle/README.md 可以看出 ISLE 的设计定位它基于项重写term-rewriting范式输入——某种编译器 IR——在概念上是一棵项树而规则集把这棵树改写为另一棵项树。生成的指令选择器在单次遍历中高效工作把全部规则合并进一棵决策树在共享公共子结构的同时尊重每条规则上的用户可配置优先级。此外ISLE 语言被刻意设计为规则既能编译成高效的编译器后端又能用于对编译器做形式化推理——当前仓库中的编译器实现了前者后者是未来工作。关于 ISLE 语言本身的语法与语义可阅读 cranelift/isle/docs/language-reference.md即原文档中../isle/docs/language-reference.md的仓库根路径形式。ISLE 如何接入构建系统构建脚本与编译流水线ISLE 的构建集成位于 cranelift/codegen/build.rs。其核心机制是ISLE 编译器cranelift-isle被作为cranelift-codegen的构建依赖build-dependency引入构建脚本用它把 ISLE 源码编译成生成的 Rust 代码。也就是说ISLE 编译器充当了一个额外的编译步骤ISLE 源码会像任何 Rust 源码一样被重新构建——编辑 ISLE 时无需任何额外操作。从build.rs的实现可以看到整个流程读取OUT_DIRCargo 提供的生成文件输出目录与TARGET目标三元组根据all-arch/all-native-arch等 feature 及目标平台确定需要包含的 ISA 列表调用cranelift-codegen-meta的meta::generate生成 ISLE 声明文件调用build_isle遍历meta::isle::get_isle_compilations返回的每个编译单元通过isle::compile::from_files把 ISLE 源文件编译为 Rust 代码并写入输出文件通过cargo:rerun-if-changed声明输入文件依赖保证 ISLE 源变化后自动重编。值得注意的是构建顺序的约束build.rs注释明确指出 ISLE 编译必须发生在cranelift-codegen-meta之后因为它要消费后者生成的文件。此外build.rs还做了两件细节工作通过make_isle_source_path_relative把文件路径相对化以保持构建可复现deterministic build对include!()引入的生成代码剥离全局#![allow(...)]pragma原因是 Rust 编译器不允许在include!的文件里放置全局 pragma。用 ISLE_SOURCE_DIR 查看生成代码默认情况下生成的代码放在 Cargo 管理的target/路径下。如果想直接查看源码可以用环境变量ISLE_SOURCE_DIR指定另一个目录$ ISLE_SOURCE_DIRpwd/isle-sources cargo check -p cranelift-codegen执行后ISLE 源码会被放置在./isle-sources目录中可以随意检查也可以在其中设置断点进行调试。在 build.rs 中可以看到该变量的处理逻辑它会把相对路径基于 crate 根目录做规范化canonicalize绝对路径则直接覆盖crate_dir最终目录不存在时会被创建同时cargo:rerun-if-env-changedISLE_SOURCE_DIR保证了切换目录后必然触发重建。错误信息与 isle-errors feature如果在 ISLE 编译期间出现错误例如类型不匹配默认只会看到包含文件名、行号和单行错误描述的基础报错。如需更详细的输出可以启用isle-errorsfeature$ cargo check -p cranelift-codegen --features isle-errors这会输出带源码上下文的 pretty-printed 错误。在 build.rs 中可以看到非isle-errors构建下报错时构建脚本会原样提示你运行上面这条命令获取更详细的错误报告。其他可用的构建期环境变量从 build.rs 中还可以发现几个对调试和调优有用的构建期开关SKIP_ISLE1跳过 ISLE 编译步骤第 84 行判断适合只想快速检查 meta 生成产物时的场景CRANELIFT_VERBOSE设置后会在构建时打印支持的 ISA 列表、构建耗时以及生成文件所在目录CARGO_FEATURE_TRACE_LOG启用时ISLE 编译器会在生成代码中插入log::{debug,trace}!调用帮助调试规则匹配过程ISLE_SPLIT_MATCH_THRESHOLD配合ISLE_SPLIT_MATCHfeature控制迭代器项中 match 分支的拆分阈值。build.rs中 debug 构建总是强制拆分阈值 4以保证栈帧大小合理release 构建则默认不拆可通过该变量调整。相关文件地图原文档给出了 ISLE 在仓库中的完整文件布局以下均转换为仓库根目录相对路径cranelift/isleISLE 编译器的源码。内部包含isle/编译器库本体与islec/命令行前端两个子 crate编译器库源码见 cranelift/isle/isle/src。cranelift/codegen/src/prelude.isleISLE 的公共定义与声明会被纳入每一次 ISLE 编译。target/.../out/clif_lower.isle自动生成的、面向指令降级instruction lowering的 CLIF 声明与辅助函数由 cranelift/codegen/build.rs 生成并构建进每个后端。target/.../out/clif_opt.isle自动生成的、面向中端优化mid-end optimizations的 CLIF 声明与辅助函数由同一构建脚本生成并构建进中端优化器。cranelift/codegen/src/machinst/isle.rs把 ISLE 生成代码粘合进目标架构后端的公共 Rust 代码包含 ISLE 中声明的 ISA 无关externhelper 的实现。cranelift/codegen/src/isa/arch/inst.isleISA 特定的 ISLE helper例如每个 ISA 指令的构造函数、获取特定寄存器的 helper 等负责弥合原始、非 SSA 的 ISA与降级规则所见的纯 SSA 视图之间的差距。以 x64 为例即 cranelift/codegen/src/isa/x64/inst.isle。cranelift/codegen/src/isa/arch/lower.isle某 ISA 的指令选择降级规则。这些规则必须是纯的、SSA 的重写规则以便将来可被形式化验证。cranelift/codegen/src/isa/arch/lower/isle.rs把该 ISA 的 ISLE 生成 Rust 代码集成进后端其余部分的 Rust 胶水代码包含 ISLE 中声明的 ISA 特定externhelper 的实现。x64 的实现位于 cranelift/codegen/src/isa/x64/lower/isle.rs。此外负责自动生成 ISLEextern声明与 CLIF 辅助代码的cranelift-codegen-metacrate其逻辑定义在 cranelift/codegen/meta/src/gen_inst.rs它会在target/输出目录生成若干 ISLE 文件这些文件随后作为 prologue 的一部分被 ISLE 编译器读取。把 ISLE 生成代码粘合进 Cranelift每个 ISA 特定的 ISLE 生成文件都泛化于一个Contexttrait该 trait 为 ISLE 中定义的每一个externhelper 提供一个 trait 方法。每个这样的 trait 都有一个具体实现定义在cranelift/codegen/src/isa/arch/lower/isle.rs中。ISLE 生成代码与系统其余部分粘合的一般方式就是这些 trait 实现。此外isle.rs中通常还会定义一个lower函数它封装了创建 ISLEContext并调用生成代码的完整流程作为后端调用指令选择器的统一入口。以 x64 的加载下沉load sinking为例extern extractor sinkable_load与extern constructor sink_load的 Rust 侧实现都在 cranelift/codegen/src/isa/x64/lower/isle.rs 中与 cranelift/codegen/src/isa/x64/inst.isle 中的 ISLE 声明一一对应详见下文Extractor 不允许副作用一节。降级规则必须保持纯函数、坚持 SSA定义在cranelift/codegen/src/isa/arch/lower.isle中的降级规则本身必须始终是从 CLIF 指令到目标 ISA 的MachInst的纯映射。原文档明确列出降级规则自身不应处理或涉及的事项被修改的寄存器同时读和写违反 SSA寄存器的隐式使用维护每个 CLIF value 或虚拟寄存器的使用计数use counts。这些事务应该由inst.isle、lower/isle.rs中的 Rust 代码或其他 Rust 代码的组合来处理。当一条指令修改一个寄存器既读又写时应当为它构建一个 SSA 视图并通过从寄存器中拆出一条 move 指令来使其合法化。原文档以 x86 的add为例做了清晰演示x86 的add指令读且写其第一个操作数add a, b a a b于是规则呈现一个三操作数的 SSA 外观其中定义一个、读取另外两个且不修改它们add a, b, c a b c然后作为这个外观facade的实现细节按需发射 move 指令add a, b, c mov a, b; add b, c对于像 x86 这样广泛使用修改寄存器、指令多为双操作数形式的 ISA这个机制通过该 ISA 的MachInst上的方法实现而对于 aarch64 这类更RISC、修改寄存器很少出现的 ISA则在inst.isle层处理少数必要情形。无论哪种方式关键都是让降级规则保持纯。这些 move 最终通常会被寄存器分配器的 move coalescing 清理掉。类似地隐式操作特定寄存器、或要求某些操作数必须位于特定寄存器的指令也用同样的方式处理降级规则使用忽略这些约束的纯范式让指令显式携带隐式操作数而在降级规则之下的层面inst.isle或 Rust 胶水代码确保约束得到满足。Extractor 不允许副作用以加载下沉为例Extractor出现在rule左侧的匹配器绝不能有副作用。原因在于求值一条规则的 extractor 时我们尚未承诺执行该规则的右侧right-hand side。如果 extractor 执行了副作用就可能出现远距离作用action-at-a-distance式的深层困惑 bug——从未完整匹配的规则悄悄改变了状态。任何时候你忍不住想在 extractor 里执行副作用正确的做法是把执行该副作用所需的东西打包成数据再写一个独立的 constructor接收这个数据包并执行其描述的副作用。constructor 只能在规则的右侧被调用而右侧只在确定采用这条规则之后才求值从而避免了上面提到的远距离作用 bug。原文档给出的经典案例是 x86 的加载下沉load sinkingCLIF 中的 load 有副作用——它可能 trap。因此即使加载的值从未被使用我们仍会发射实现该 load 的代码。但在 x86 上根据加载值的使用方式可以把 load 下沉进另一个操作的操作数里。如果把 load 下沉进比如add就需要告诉降级上下文不再降级这条 CLIFload因为它已经被当作add降级的一部分处理掉了。标记指令为已降级是一种副作用我们可能会忍不住在匹配可下沉 load 的 extractor 中执行它但不能这么做虽然 load 本身可下沉但最终可能因某种原因不采用这条下沉规则此时仍需要降级这条 CLIF load。因此设计上让sinkable_loadextractor 创建一个SinkableLoad类型把关于这个 load 的一切信息以及如何告诉降级上下文它已被下沉、无需再降级打包进去——但此刻并不真正告知降级上下文;; inst.isle ;; A load that can be sunk into another operation. (type SinkableLoad extern (enum)) ;; Extract a SinkableLoad from a value if the value is defined by a compatible ;; load. (decl sinkable_load (SinkableLoad) Value) (extern extractor sinkable_load sinkable_load)然后把它与一个sink_loadconstructor 配对sink_load接收SinkableLoad执行告诉降级上下文不再降级该 load的副作用并返回带下沉 load 的 x86 操作数;; inst.isle ;; Sink a SinkableLoad into a RegMemImm.Mem. ;; ;; This is a side-effectful operation that notifies the context that the ;; instruction that produced the SinkableImm has been sunk into another ;; instruction, and no longer needs to be lowered. (decl sink_load (SinkableLoad) RegMemImm) (extern constructor sink_load sink_load)最后在创建操作数直接从内存加载的指令的降级规则中组合使用sinkable_load与sink_load;; lower.isle (rule (lower (has_type (fits_in_64 ty) (iadd x (sinkable_load y)))) (value_reg (add ty (put_in_reg x) (sink_load y))))在 cranelift/codegen/src/isa/x64/inst.isle 中可以看到这套声明的实际形态第 1368 行用(model SinkableLoad (type (bv)))建模、第 1369 行声明(type SinkableLoad extern (enum))随后有sinkable_load/sinkable_load_exact两个 extractor后者支持所有位宽的 load以及sink_load下沉到SyntheticAmode、sink_load_to_gpr_mem_imm、sink_load_to_xmm_mem等构造函数仓库中还进一步扩展了按位宽区分的SinkableLoad8/16/32/64/128类型族。它们的 Rust 侧extern实现位于 cranelift/codegen/src/isa/x64/lower/isle.rs。原文档还提示结合下文ISLE 代码应该充分利用类型一节理解这一模式。ISLE 代码应该充分利用类型ISLE 是强类型语言应当充分利用类型系统来在可能的情况下预防整类 bug——多使用新类型newtypes。原文档给出的典型例子是with_flags一族 helper用它把产生标志位flags的指令与消费标志位的指令配对确保任何错误指令都不会被插入到标志位使用指令之间、破坏其标志位。with_flags、ProducesFlags、ConsumesFlags的声明定义在 cranelift/codegen/src/prelude.isle 中。其实际使用遍布各后端的inst.isle例如 cranelift/codegen/src/isa/aarch64/inst.isle 中的alu_rrr_with_flags_paired、add_with_flags_paired、sub_with_flags_paired等以ProducesFlags为返回类型的声明以及规则体里(with_flags (sub_with_flags_paired ...))、(with_flags_reg ...)、(with_flags (cmp_imm ...))这类配对写法。类型在此承担了编译期约束一条ProducesFlags的指令如果不通过with_flags*家族被消费就无法通过类型检查从而在结构上杜绝了标志位被意外破坏的 bug 类别。隐式类型转换ISLE 支持隐式类型转换implicit type conversionsCranelift 会在可能的地方加以利用来简化降级规则。例如Value表示 CLIF 中的 SSA 值ValueRegs表示降级后代码中持有该值的寄存器一个或多个。在引入隐式类型转换之前代码中充斥着形如(value_regs r1 r2)或(value_reg r)的表达式。既然已经定义了value_reg这一 term可以定义如下的转换(convert Reg ValueRegs value_reg)ISLE 编译器会在类型暗示必要之处自动插入该转换。转换器定义得当的情况下可以写出这样简洁的规则(rule (lower (has_type (fits_in_64 ty) (iadd x y))) (add ty x y))隐式类型转换与副作用隐式转换虽然方便但必须小心管理它们可能引入的隐形副作用。最关键的事实是隐式转换可能发生多次。例如如果定义了(convert Value Reg put_value_in_reg)并发射一条(add a a)指令比如将a翻倍其中a是Value那么put_value_in_reg会被调用两次。如果这是一个每次调用都执行某种唯一动作例如分配一个全新寄存器的外部 constructor那这是不可取的。因此定义隐式类型转换时要遵循一条约定转换必须是幂等的idempotent即重复调用必须返回相同的结果。put_value_in_reg恰好已经具备这一性质所以它可以安全地用作隐式转换器。需要注意这个条件不等于说转换器不能有副作用——转换器带副作用完全没问题甚至能带来显著的便利多调用一次带来的效率损失可以容忍将来如有必要还可以再优化。即便如此有时显式表达依然更清晰。例如下沉 load 是某些降级模式中显式且重要的细节虽然可以定义从SinkableLoad到Reg的隐式转换但目前保留(sink_load ...)这种显式形式可能更利于代码的可读性。小结ISLE 与 Cranelift 的集成可以概括为三条主线构建期cranelift-codegen的 build.rs 把cranelift-isle编译器作为构建依赖把prelude.isle、meta 自动生成的clif_lower.isle/clif_opt.isle与各后端的inst.isle/lower.isle编译为 Rust 代码并通过ISLE_SOURCE_DIR、isle-errorsfeature 等提供调试通道运行期生成代码通过 machinst/isle.rs 与各后端的lower/isle.rs中的Contexttrait 实现粘合进整个后端语言设计上降级规则保持纯函数与 SSA 视图、extractor 杜绝副作用、类型系统with_flags家族、隐式转换的幂等约定被用来在编译期拦下整类错误。理解了这三条主线再去看任意一个 ISA 的inst.isle与lower.isle就能快速定位哪些是声明、哪些是规则、哪些是副作用边界。【免费下载链接】wasmtimeA lightweight WebAssembly runtime that is fast, secure, and standards-compliant项目地址: https://gitcode.com/gh_mirrors/wa/wasmtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考