Foundry Chisel 内联汇编(Inline Assembly)表达式求值能力详解:从 REPL 输入到 Yul 结果展示的完整链路
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
导读
本文围绕 Foundry 中 Chisel 交互式 Solidity REPL 的一项核心增强——允许 Chisel 检视内联汇编块中的最终表达式(final expression)——展开。该能力对应仓库中的 changelog 记录 .changelog/chisel-inline-assembly.md,属于chisel: minor级别的功能改进。读完本文,你将理解 Chisel 如何把assembly { ... }块末尾的 Yul 表达式当作可求值的"结果"来展示、它与此前"仅能展示 Solidity 语句结果"的行为有何差异、底层源码如何实现这一逻辑,以及对应的测试用例验证方式,从而能够熟练使用 Chisel 调试汇编代码片段。
功能背景:Chisel 是什么
Chisel 是 Foundry 提供的交互式 Solidity 运行环境,用户可以在其中逐行输入 Solidity 语句、函数定义、!开头的特殊命令,并在每次输入后立即看到求值结果(例如Decimal: 2、Hex: 0x...等输出)。其核心运行机制是:每次输入都会被拼接到一个名为REPL的合约中,形成run()函数,然后编译、部署并在本地 EVM 上执行,最终通过源码映射(source map)定位run()函数最后一条语句对应的程序计数器(PC),读取执行结束后的栈与内存状态并格式化输出。
Chisel 的源码位于 crates/chisel/src,其中 executor.rs 负责输入检视(inspection)与执行,source.rs 负责 REPL 会话源码的生成与编译产物的分析。
变更内容:检视内联汇编块中的最终表达式
变更前的问题
在早期版本中,Chisel 对输入的常规检视路径是:把输入包装为bytes memory inspectoor = abi.encode(<input>);追加到源码中,通过 ABI 编码拿到结果。但对于内联汇编块(inline assembly block),例如:
assembly { add(3, 4) }这种写法在abi.encode(...)包装下无法直接编译,旧版 Chisel 无法把块内最后的 Yul 表达式作为结果展示,用户不得不手动改成assembly { let x := add(3, 4) }之类的形式再单独检视变量。
变更后的行为
本次变更后,Chisel 允许把assembly { ... }块中的最后一个表达式(final expression)作为检视结果。也就是说,下面这类输入会直接输出表达式的结果:
assembly { add(3, 4) }执行后 Chisel 会显示Decimal: 7。
这一行为在仓库测试用例中有明确验证,见 crates/chisel/tests/it/repl/mod.rs 中的inline_assembly_expression测试(对应 Issue #4963):
repl_test!(inline_assembly_expression, |repl| { repl.sendln("uint256 value = 1"); repl.sendln("assembly { value := 2 add(value, 0) } // trailing comment"); repl.expect("Decimal: 2"); repl.sendln("value"); repl.expect("Decimal: 2"); repl.sendln("assembly { let __chisel_yul_result := 3 add(__chisel_yul_result, 0) }"); repl.expect("Decimal: 3"); repl.sendln("uint256 __chisel_yul_result_1 = 0"); repl.sendln("assembly { add(3, 4) }"); repl.expect("Decimal: 7"); });从测试可以看到三种典型场景均被支持:
- 赋值表达式作为末尾表达式:
assembly { value := 2 add(value, 0) },把value更新为2并作为结果展示(即使后面有// trailing comment也不影响); let声明加末尾表达式:assembly { let __chisel_yul_result := 3 add(__chisel_yul_result, 0) },结果展示为3;- 纯表达式:
assembly { add(3, 4) },结果展示为7。
底层实现剖析
检视入口与 Yul 分支
Chisel 的检视入口是SessionSource::inspect(见 executor.rs)。其常规流程是先把输入包装成bytes memory inspectoor = abi.encode(<input>);尝试构建新的会话源码;如果这条常规路径失败(内联汇编块无法通过abi.encode包装),则会调用专门为 Yul 设计的yul_inspection函数,代码注释也明确说明:事件和元组等无法被inspectoor编码时会走其他分支,而内联汇编则单独处理。
yul_inspection的核心逻辑
yul_inspection(input, session_source)(见 executor.rs)通过 solar 解析器把用户输入解析为单条语句(parse_stmt),然后校验其类型是否为AstStmtKind::Assembly。其处理步骤可概括为:
- 定位末尾表达式:取汇编块
assembly.block.stmts的最后一条语句,要求它是yul::StmtKind::Expr(即一个表达式语句,而非let声明或其他语句类型); - 提取表达式源码片段:通过
sess.source_map().span_to_source(expr.span)把表达式的 span 还原为输入中的原始文本(expression); - 生成唯一结果变量名:从
__chisel_yul_result、__chisel_yul_result_1、__chisel_yul_result_2… 依次尝试,选出既不与用户输入冲突、也不与会话现有源码冲突的名字; - 构造"检视输入":把原汇编块中的末尾表达式原位替换为
{result_var} := {expression},并包装为:uint256 {result_var}; {assembly} bytes memory inspectoor = abi.encode({result_var});这样就把一个原本无法 ABI 编码的 Yul 表达式,改写为"先赋值给临时变量、再编码该变量"的形式,从而复用常规检视路径;
- 构造"重放输入":把末尾表达式替换为
pop({expression}),用于把该语句持久化写入会话源码时保持语义等价(表达式的值被丢弃,但副作用保留)。
结果展示链路
构造出的inspector_input会被clone_with_new_line加入会话源码并执行,随后通过abi.encode({result_var})得到编码结果,再由类型推断与格式化逻辑输出为Decimal: .../Hex: ...等形式。InspectResult(见 executor.rs)中的replay_input字段用于在展示结果的同时,把经过pop(...)改写后的语句持久化到会话中,保证后续输入状态连续。
配套能力:汇编块作为末尾语句的 PC 定位
除了"末尾表达式检视",Chisel 还处理了"内联汇编块作为run()函数末尾语句"时如何确定最终 PC 的问题,两者共同构成对汇编块完整的 REPL 支持。相关逻辑集中在 source.rs:
final_pc(source.rs):查找run()函数体最后一条语句,若它是HirStmtKind::AssemblyBlock或Err,则调用trailing_assembly_last_stmt_span获取具体的 Yul span,再通过源码映射(source map)把该 span 映射为部署字节码中的程序计数器,从而知道执行到哪一步停止;trailing_assembly_last_stmt_span(source.rs):当run()的最后一条语句是汇编块时,从块内倒序查找最后一个非let(即非VarDecl)的 Yul 语句并返回其 span。这是为了让 Chisel 在汇编块结束时选取一个有意义的"结束位置",而不是落在变量声明上;first_yul_return_span(source.rs):查找run()内任意汇编块顶层第一个return(...)调用的 span。当 Yul 的return位置比末尾语句更靠前时,以return的位置作为最终 PC(对应历史 Issue #4617 的场景:assembly { mstore(0x0, 0x1337) return(0x0, 0x20) }后再跟 Solidity 语句);repl_run_ast_body(source.rs):从 solar AST 中恢复REPL合约run()函数的函数体,为上述 Yul 级分析提供 AST 基础。代码注释明确说明其用途是"让内联汇编块可以按 Yul 语句粒度被检视"。
测试验证
仓库测试覆盖了内联汇编相关的多个场景,均可作为复现与验证依据,全部位于 crates/chisel/tests/it/repl/mod.rs:
| 测试名 | 场景 | 期望行为 |
|---|---|---|
inline_assembly_expression(Issue #4963) | 汇编块末尾表达式(赋值、let+表达式、纯表达式) | 展示表达式求值结果,如Decimal: 2、Decimal: 3、Decimal: 7 |
assembly_return(Issue #4617) | 汇编return之后继续执行 Solidity 语句 | !md内存转储正常,无报错 |
assembly_memory_dump(Issue #4938) | 多行汇编块配合内存/栈转储 | 状态保持正确,!md可输出内存区间 |
assembly_return_final | 汇编块为末尾语句且含return | 同时命中first_yul_return_span与trailing_assembly_last_stmt_span,!md正常 |
assembly_no_return_intermediate | 无return的汇编块作为中间语句 | 后续 Solidity 语句仍正确求值(x变为2) |
uninitialized_variables | 汇编中引用未初始化变量 | 正确展示Hex: 0x0、Data: 0xFFfF... |
这些测试印证了本文描述的"末尾表达式检视"与"末尾语句 PC 定位"两条实现路径,且都标注了对应的历史 Issue 编号,方便追溯设计动机。
使用示例
在 Chisel 中可以直接实践该能力:
$ chisel ➜ uint256 value = 1; ➜ assembly { value := 2 add(value, 0) } // 末尾是赋值表达式 Decimal: 2 ➜ value Decimal: 2 ➜ assembly { let x := 3 add(1, 1) } // let 声明 + 末尾表达式 Decimal: 3 ➜ assembly { add(3, 4) } // 纯 Yul 表达式 Decimal: 7要点小结:
- 只取汇编块的最后一条Yul 表达式作为结果;若最后一条是
let声明等非表达式语句,则无法走此检视路径; - 结果变量的命名冲突会被自动规避(
__chisel_yul_result、__chisel_yul_result_1…); - 展示结果的同时,原汇编语句会以
pop(...)形式持久化到会话,后续状态连续; - 汇编块中的
return(...)若出现在末尾语句之前,Chisel 会以return位置作为执行停止点,保证内存/栈转储结果可预期。
小结
本次chisel: minor变更让 Chisel 真正具备了"直接检视内联汇编末尾表达式"的能力,补齐了 REPL 调试汇编代码的最后一块拼图。从源码看,其实现分为两条互补的链路:yul_inspection负责把末尾表达式改写为可 ABI 编码的临时变量赋值以完成求值展示(executor.rs),final_pc/trailing_assembly_last_stmt_span/first_yul_return_span负责在汇编块收尾时确定有意义的执行停止点(source.rs),并由 crates/chisel/tests/it/repl/mod.rs 中的一组测试用例持续守护。对日常使用 Chisel 调试 Yul 代码的开发者而言,这意味着assembly { <expr> }可以直接获得求值反馈,无需再手工声明中间变量。
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考