
Proof of SQL 的 Yul 函数导入预处理器在 Solidity 内联汇编中实现可复用的 Yul 代码组织【免费下载链接】sxt-proof-of-sqlSpace and Time | Proof of SQL项目地址: https://gitcode.com/GitHub_Trending/sx/sxt-proof-of-sql导读本文围绕 solidity/preprocessor/README.md 展开系统讲解 Space and Time Proof of SQL 仓库中 SolidYul Preprocessoryul_preprocessor.py的设计动机、导入语法、处理流程与工程实践。该预处理器在assembly {}块内以// import注释声明 Yul 函数导入将散落在不同.presl文件中的内联汇编函数解析、去重并合并进最终产物是 Solidity 侧验证器代码得以模块化组织的关键工具。读完本文你将掌握该预处理器的三种导入语法、相对路径解析、循环依赖处理、缓存与自动格式化机制并能在自己的 Foundry 合约项目中复用它组织可复用的内联汇编代码。背景为什么 Solidity 项目需要一个 Yul 导入预处理器Solidity 的assembly {}Yul块中定义的函数作用域严格限制在所在块内无法跨文件、跨函数复用。在 Proof of SQL 的 Solidity 验证器实现中如 solidity/src/base/DataType.presl、solidity/src/proof_gadgets/FoldLogExpr.presl 等大量密码学与数据解析逻辑都以 Yul 函数形式内联实现如果没有导入机制只能把整个 Yul 代码复制到每个使用点或者把所有函数堆进单一巨型文件——两者都严重影响可维护性。该预处理器给出的方案是让开发者在assembly块内以注释形式书写导入声明由 Python 脚本在编译前完成文本级的函数注入生成可直接交给forge编译的.post.sol文件。Yul 导入注释会被解析、被替换为实际函数定义而 Solidity 编译器永远不会看到这些注释因此不会影响编译语义。整个工作流可以概括为*.presl / *.t.presl带导入注释的源码 ↓ python3 yul_preprocessor.py directory *.post.sol / *.t.post.sol导入已解析的产物 ↓ forge fmt / forge build / forge test特性总览文档列出的核心特性均可从 yul_preprocessor.py 源码中得到印证特性说明Yul 函数导入从其他.presl文件向 assembly 块导入 Yul 函数单行多函数导入一条语句同时导入多个函数Self 导入引用同一文件内其他 assembly 块中定义的函数相对路径支持支持../、./、普通子目录相对路径循环依赖文件间可以互相导入依赖环自动作为一个整体解析传递依赖解析导入一个函数时自动带入其递归调用的依赖闭包函数去重同一函数被重复导入时仅保留一份缓存同一文件在依赖树中被多次引用时复用处理结果自动格式化产物自动调用forge fmt保持风格一致安装与快速开始预处理器是零依赖的 Python 3 脚本仅使用标准库os、re、sys、subprocess、pathlib、typing无需pip installpython3 yul_preprocessor.py directory其中directory必须是一个目录脚本入口 main 会先校验dir_path.is_dir()非法参数会打印Error: directory is not a directory并退出码 1。可选依赖Foundryforge用于对输出文件执行forge fmt。若 PATH 中不存在forge脚本不会失败只会在 stderr 打印警告并跳过格式化见 format_with_forge 中对FileNotFoundError的处理。Foundry 的安装步骤可参考 solidity/README.md 中的Development Dependencies Installation一节。处理目录时脚本通过dir_path.rglob(*.presl)递归收集全部.presl文件.t.presl以.presl结尾天然包含在内逐一处理并生成对应产物preprocess_directory。全部文件处理完毕后若存在产物还会对整个目录执行一次forge fmt。跳过标记源码提供了一项文档未展开说明的细节——若文件前 10 行内出现// does-not-compile或// doesnotcompile空格被归一化后匹配标记该文件会被跳过不处理should_skip_file用于容纳有意不参与编译的用例。三种导入语法预处理器通过正则//\s*import\s([\w\s,])\sfrom\s([^\s])匹配导入声明import_pattern支持三种形式。1. 单函数导入assembly { // import add5 from utils.presl let result : add5(10) }2. 单行多函数导入assembly { // import add, multiply, divide from math.presl let sum : add(5, 10) let product : multiply(3, 7) }导入名以逗号分隔后逐个解析process_assembly_block 中func_names [name.strip() for name in func_names_str.split(,)]。仓库中的真实示例见 multiple_imports_per_line/main.presl// import _add, multiply from operations.presl。3. Self 导入同文件跨 assembly 块contract Example { function defineHelpers() external pure { assembly { function helper(x) - result { result : mul(x, 2) } } } function useHelpers() external pure { assembly { // import helper from self let doubled : helper(5) } } }self关键字使解析器扫描当前文件全部assembly 块提取所有函数后返回目标函数及其依赖resolve_import。测试用例 single_self_import.presl 验证了utilFunc会被注入第二个块并被正常调用。相对路径导入路径相对于当前文件所在目录解析// import compute_fold from ../base/MathUtil.presl // import err from ./errors/Errors.sol // import safe_add from lib/SafeMath.presl源码中的路径解析逻辑见 resolve_import_path以/开头视为相对于root_dir的绝对路径否则用(current_file.parent / import_path).resolve()求相对于当前文件的路径。注意导入目标不限于.presl文件——直接导入普通.sol文件同样被支持resolve_import中对非.presl后缀文件直接read_text见 yul_preprocessor.py这正对应仓库中// import err from Errors.sol这类写法。相对路径导入的测试见 relative_path_import/main_with_relative_import.presl// import double from lib/helper.presl。完整示例从源文件到产物源文件utils.presl// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Utils { function process() external pure returns (uint256) { assembly { function add5(x) - result { result : add(x, 5) } function multiply2(x) - result { result : mul(x, 2) } } } }目标文件main.presl// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Main { function compute() external pure returns (uint256) { assembly { // import add5, multiply2 from utils.presl let a : add5(10) // a 15 let b : multiply2(a) // b 30 } } }输出文件main.post.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Main { function compute() external pure returns (uint256) { assembly { function add5(x) - result { result : add(x, 5) } function multiply2(x) - result { result : mul(x, 2) } let a : add5(10) // a 15 let b : multiply2(a) // b 30 } } }从源码看产物并不是简单地把函数定义追加在调用点——process_assembly_block 会先逐行扫描块内容剥离所有// import行并解析对应函数把导入行替换为函数集合最后将全部导入函数置于块的开头return \n.join(func_lines) \n \n.join(result_lines)。这保证 Yul 语义正确函数必须先定义后调用。文件命名约定类别输入输出标准文件*.presl*.post.sol测试文件*.t.presl*.t.post.sol命名转换在 preprocess_directory 中通过pre_file.with_suffix(.post.sol)实现——因为.t.presl的with_suffix同样得到.t.post.sol两种后缀天然统一。.t.presl测试文件处理的专项测试见 test_yul_preprocessor.py。循环依赖把依赖环当作统一整体处理循环依赖被完全支持。当 A 导入 B、B 又导入 A 时预处理器不会陷入无限递归而是通过处理栈检测到环process_file中发现file_path in processing_stack见 yul_preprocessor.py收集环内所有文件cycle_files用collect_all_functions_in_cycle汇总环内全部文件定义的 Yul 函数并校验重名函数的签名一致性再调用collect_external_dependencies_for_cycle收集环外导入的外部函数将完整函数集合缓存到cycle_groups[frozenset(cycle_files)]环内每个 assembly 块最终都拥有相同的函数全集。文档中的示例circular_regular 目录与文档描述一致file_a.presl:assembly { // import funcB from file_b.presl function funcA() - result { result : 1 } }file_b.presl:assembly { // import funcA from file_a.presl function funcB() - result { result : 2 } }处理完成后两个文件的 assembly 块中都会同时出现funcA与funcB。测试 test_circular_minimal_allowed 断言result_a与result_b都包含funcA与funcB文件 C 从该环导入时也能拿到funcA、funcB、funcCtest_circular_with_external_import。嵌套循环依赖也是支持的Group C → Group BB0、B1 互依→ Group AA0、A1 互依→ 独立文件 utils。所有传递依赖都会被正确解析并逐层传播。对应测试 test_nested_circular_dependencies 验证了 C 的产物中不仅包含 B 环函数还包含 B 环外部依赖A 环函数以及 A 环自身的 utils 依赖。依赖解析只带真正被调用的函数这是文档Transitive dependency resolution的准确实现也是本文想重点澄清的细节导入并非把源文件 assembly 块里的全部函数一股脑搬过来而是只导入被请求的函数及其递归调用闭包。核心逻辑在 get_function_dependencies它以目标函数为起点做工作队列 BFS用 find_yul_function_calls正则\b(\w)\s*\(扫描函数体且只统计确实存在于函数表中的名字从而排除add、mul等 Yul 内建操作找出每个函数调用了哪些已定义函数逐层展开。若同一函数被多次调用只保留一份。unused_functions 测试目录精确刻画了这一行为library.presl定义了baz、foo内部调用bar、bar、unrelated四个函数main.presl只导入baz测试 test_unused_functions_excluded 断言产物中只出现baz与本地mainFuncfoo、bar、unrelated均不出现。同时同一文件多函数导入测试test_multiple_imports_per_line的注释也说明从文件导入时会获取其完整依赖定义属于预期行为而非冗余。错误处理文档明确了两类错误源码中对应抛出ValueError的位置缺失函数ValueError: Function nonExistent not found in utils.presl Available functions: add5, multiply2对应 resolve_import 中的查找失败分支self 导入的版本见 L747-L752环内查找失败见 L766-L773。测试 test_missing_function_error 用pytest.raises(ValueError, matchFunction nonExistentFunc not found)覆盖。函数签名不匹配ValueError: Function signature mismatch for add: Existing: function add(a, b) - result New: function add(x) - result当同一名字的函数以不同签名被重复导入时抛出见 process_assembly_block。此外循环依赖组内若出现同名不同签名的函数会抛出Function signature conflict in circular dependency groupcollect_all_functions_in_cycle。YulFunction的__eq__与__hash__均基于signature实现yul_preprocessor.py这意味着签名相同是去重与冲突判定的唯一标准。架构与处理流程核心组件从源码结构看yul_preprocessor.py实现由两个类构成YulFunctionL32-L66表示一个已解析的 Yul 函数字段包括name、signaturefunction name(...) - ...、body、full_text、pre_comments/post_comments保留函数前后的 Slither 豁免注释、source_file定义来源。YulPreprocessorL69 起主处理器负责文件解析、导入解析、函数提取与缓存。内部维护两个缓存字典processed_cache文件级缓存与cycle_groups循环依赖组函数集缓存。处理流程文档给出的流程与源码实现一一对应Input .presl file ↓ Find assembly blocks find_assembly_blocks花括号配对扫描 ↓ For each assembly block: - Parse import statements import_pattern 逐行匹配 - Resolve imported functions resolve_import 递归解析 - Process dependencies recursivelyprocess_file 递归处理依赖文件 - Deduplicate functions 按签名去重 - Insert functions at block start统一注入块首 ↓ Generate .post.sol file write_text 写出几个值得一提的源码级细节assembly 块定位find_assembly_blocksL90-L123用assembly\s*\{定位起点随后做花括号配对计数找到匹配的右括号因此能正确处理嵌套花括号。多行函数签名extract_yul_functionsL125-L260逐行扫描function关键字签名与函数体都可能跨行while i len(lines) and { not in lines[i]累加签名行再用花括号计数收拢函数体。测试 test_multiline_function_definition 覆盖了multiline_with_many_params(...) - result_a, result_b这类超长多返回值签名。Slither 注释保留提取函数时会回溯收集函数前的// slither-disable-start/// slither-disable-next-line注释但不误收属于上一个函数的slither-disable-end并前向搜索匹配的slither-disable-end注入时按coverage-start → pre-comments → 函数 → post-comments → coverage-stop的顺序拼接process_assembly_block。测试 test_slither_comments_preserved 甚至对五个标记的相对位置做了严格断言。覆盖率排除标记凡是从其他文件或同文件其他块导入的函数都会被打上function exclude_coverage_start_name() {}与function exclude_coverage_stop_name() {}空函数标记附带// solhint-disable-line no-empty-blocks用于在覆盖率统计中圈定导入代码范围本块内定义的函数则不加这些标记test_coverage_exclusion_for_external_imports 与 test_coverage_exclusion_circular_dependencies。缓存机制处理结果按文件缓存同一文件在依赖树中被多处引用时后续直接命中processed_cache避免重复解析process_file 的缓存查询与 L518-L519 的写入。循环依赖组的完整函数集同样缓存于cycle_groups。测试 test_caching 断言连续两次process_file结果相等且文件确实进入缓存。高级特性函数去重同一函数被重复导入时只保留一份按签名比较首个遇到者胜出assembly { // import add from math.presl // import add from math.presl // 被去重 let x : add(1, 2) }测试 test_function_deduplication 断言result.count(function square(x) - result) 1。多 assembly 块一个合约内多个函数各自携带 assembly 块时每个块独立处理各自的导入process_file 对块做逆序替换以维持文本位置正确。测试用例见 multiple_assembly_blocks/main_multi.presl断言 test_multiple_assembly_blocks 验证func1、func2都出现在产物中。Solidity import 语句的自动改写除 Yul 函数导入外预处理器还会把产物中 Solidity 层级的import语句从.presl改写为.post.solprocess_filere.sub(r(import\s(?:.*?\sfrom\s)?[\])([^\]*?)\.presl([\]), r\1\2.post.sol\3, content)该正则同时覆盖两种风格import ./SomeLib.presl;与import {Util} from ./Utils.presl;。测试 test_solidity_imports_converted_in_presl_output 断言产物中不残留任何.presl导入test_no_presl_references_in_post_sol_imports 更是以严格模式扫描多组输出确保.post.sol产物中不存在任何指向.presl的 import 引用。集成进真实工作流仓库中预处理器是 Solidity 验证器构建链的一环。以 solidity/scripts/pre_forge.sh 为例实际调用模式为# 删除历史产物 find . -type f -name *.post.sol -delete # 递归预处理当前目录全部 .presl python3 preprocessor/yul_preprocessor.py . # 随后执行 forge 命令 forge $在真实源码中导入注释随处可见例如 solidity/src/base/DataType.preslassembly { // import err from Errors.sol // import case_const from SwitchUtil.presl function read_binary(result_ptr) - result_ptr_out, entry { ... } }这里的err来自Errors.sol普通 Solidity 文件直接导入case_const来自SwitchUtil.presl需要先经预处理器展开——同一块内混合两种来源恰是预处理器设计目标的最佳写照。完整的*.t.presl验证器测试产物目标为src/verifier/Verifier.t.post.sol可参考 solidity/README.md 的 Build 部分与 solidity/scripts/lint-and-test.sh。测试与验证运行完整测试套件python3 -m pytest test_yul_preprocessor.py -vtest_yul_preprocessor.py 覆盖的场景与文档宣称的能力一一对应基础导入test_basic_import_preprocessing单行多函数导入test_multiple_imports_per_lineSelf 导入含多函数与外部依赖场景test_self_import、test_self_import_with_external_deps相对路径导入test_relative_path_import函数去重test_function_deduplication循环依赖检测与解析test_circular_minimal_allowed缺失函数错误test_missing_function_error复杂/多行函数签名test_complex_function_signature、test_multiline_function_definition多 assembly 块test_multiple_assembly_blocks缓存test_caching输出文件生成test_preprocess_file_output测试用例样本全部位于 solidity/preprocessor/test_files/ 目录每个子目录对应一种场景可作为扩展新特性时的参照模板。行为注意事项快速参考传递依赖导入某个函数时其递归调用的依赖闭包会自动随之导入保证产物可直接编译运行。导入语句必须单行import_pattern以行为单位匹配block_content.split(\n)后逐行search跨行书写的导入声明不会被识别。语法固定必须严格遵循// import names from path形式names可为逗号分隔的多个函数名path不得包含空白字符正则中[^\s]。仅提取 Yul 函数只处理assembly {}块内的function定义块外的 Solidity 代码不受影响。Solidity import 改写产物中的import *.presl/import {X} from *.presl会统一改写为.post.sol。覆盖率与静态分析跨块导入的函数会附带 coverage 排除标记与保留的 Slither 豁免注释无需手工维护。【免费下载链接】sxt-proof-of-sqlSpace and Time | Proof of SQL项目地址: https://gitcode.com/GitHub_Trending/sx/sxt-proof-of-sql创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考