前言¶
MLIR/LLVM 的 AOT 后端(toyc --emit=exe 之类)和 JIT 路径(toyc --emit=run)本该给出同样的运行结果。后端改动之后,差异往往只出现在某一类输入上,而且可能是 stdout 的第 3 行、退出码,或者某个写入文件的内容。手工对拍要两边各起一个进程、抄输出、对日志,慢且容易漏。
dsh-aotjit-compare 就是为此做的 DSH 插件:同一个用例分别交给 AOT 可执行文件和 JIT 启动命令跑一遍,逐字段比较 stdout / exit code / stderr / 文件副作用,直接给出「哪一行、哪一侧、期望什么、实际什么」。下面介绍它的能力、安装方式和用法。
这是什么¶
插件 id aotjit-compare,工具名 aotjit_compare,版本 0.1.0,维护者 Rcyc-1025,分类属于工作流,许可证 MIT。
它做一件事:跑两侧、比对、定位、留日志。对拍发现差异是正常返回,不是错误抛出——工具把差异结构化给你,让你自己决定下一步改哪侧。
核心功能¶
1、同一组用例分别跑 AOT 可执行文件与 JIT 启动命令,支持逐用例覆盖参数、stdin、env、cwd。
2、逐字段比较,默认比 stdout 和 exit,stderr 默认只采集不比较,文件副作用默认关闭。
3、差异定位到字段、行号、期望值、实际值;diffs[].expected 永远是 AOT 侧,actual 是 JIT 侧。
4、归一化策略可配:CRLF、ANSI 颜色、末尾换行、路径前缀替换。
5、文件副作用支持 hash / content / tree 三种模式,并提供隔离目录,让两侧副作用不互相污染。
6、每次调用写日志,含两侧 argv、退出码、耗时、截断与编码情况、每处差异;日志失败只进 warnings[],不影响结论。
7、非法配置不阻止加载,退回默认值并记入 warnings[];调用参数优先于配置,环境变量可做进程级覆盖。
安装与启用¶
插件目录需要被 desktop profile 引用,README 给了两种方式。第一种用 DSH CLI 把本地目录装进 profile:
D:\exe\DSH\resources\runtime\cli\bin\dsh.cmd plugin --profile desktop add D:\workplace\aotjit-compare
第二种手工在 <profile>/package.json 里登记,profile 目录通常为 %DSH_HOME%\data\profiles\desktop:
{
"dependencies": { "dsh-aotjit-compare": "file:D:/workplace/aotjit-compare" },
"dsh": { "profile": { "bundles": ["...", "dsh-aotjit-compare"] } }
}
cordis.patch.yml 会以 insert 方式注册本插件,name: 'dsh-aotjit-compare'。包声明 dsh: { bundle: { patch: "./cordis.patch.yml" } },Node 环境要求 ^22.19.0 || >=24.0.0,DSH 版本要求 >=0.2.1-rc.5 <0.3.0-0。
典型用法¶
最小调用只需要 jitSpec 与 cases。下面这个例子让 AOT 可执行文件和 JIT 命令跑同一个用例:
aotjit_compare({
aotExe: "build/s01/add.exe",
jitSpec: { cmd: "build/toyc.exe", args: ["examples/add.mul.mlir", "--emit=run"] },
cases: [{ name: "add.mul" }]
})
两侧输出都是 7\n、退出码都是 9 时,返回 allMatch: true。
argv 的拼装约定要记清楚,这是最容易配错的地方:
AOT: [aotExe, ...aotArgs, ...case.args]
JIT: [cmd, ...(case.jitArgs ?? jitSpec.args), ...case.args]
case.jitArgs 是覆盖 jitSpec.args,不是追加。这样 10 个用例可以共用同一个 JIT 入口、只换输入文件。case.args 追加到两侧,用于同一份 IR 的运行时开关(如 --seed=1)。argv 直传,不经过 shell,所以不需要引号转义,也不能写管道或重定向。jitSpec.cmd 若指向 .cmd / .bat 会被拒绝,返回 JIT_CMD_SHELL_WRAPPER。
输出与判定¶
返回结构里最容易混淆的是 ok 与 allMatch:ok 表示执行完成,不代表一致;allMatch 才是全部用例一致。差异被发现时 ok 仍为 true。
{
"ok": true,
"allMatch": false,
"firstMismatchCase": "case-07",
"firstMismatchField": "stdout",
"total": 10, "matched": 9, "mismatched": 1,
"rows": [
{
"index": 6, "case": "case-07", "match": false, "firstMismatch": "stdout",
"diffs": [
{ "field": "stdout", "kind": "text", "aotLine": 1, "jitLine": 1,
"expected": "7", "actual": "8", "expectedLength": 2, "actualLength": 2 }
],
"aot": { "cmd": ["D:/p/build/parity/case-07.exe"], "cwd": "D:/p", "exitCode": 0,
"exitCodeHex": "0x0", "signal": null, "timedOut": false, "durationMs": 12,
"stdout": "7\n", "stderr": "", "stdoutTruncated": false, "stderrTruncated": false,
"spawnError": null },
"jit": { "...": "同上" }
}
],
"logPath": "D:/p/.aotjit-logs/20261106-103000-a1b2-case07.log",
"durationMs": 183, "warnings": [], "hint": null
}
字段优先级是 spawnError > timedOut > exit > stdout > stderr > sideEffects。几条边界行为值得注意:任一侧输出被截断时不比较截断前缀,直接 firstMismatch: "truncated",避免拿残缺输出得出假结论;输出不是合法 UTF-8 时改比字节 SHA-256;Windows 负退出码保留原始数值,另给 exitCodeHex(如 0xc0000005)。
配置¶
配置写在 cordis.patch.yml 或宿主 config 里,README 列出的键与默认值如下:
| 键 | 默认 | 说明 |
|---|---|---|
timeoutMs |
120000 |
单个进程超时;超时后先 SIGTERM,graceMs 后 SIGKILL |
maxOutputBytes |
1048576 |
每条 stdout / stderr 流保留的字节上限 |
maxCases |
100 |
单次调用允许的用例数上限 |
maxSideEffectBytes |
16777216 |
副作用快照允许的总字节数 |
logDir |
.aotjit-logs |
日志目录(相对会话 cwd) |
compare |
{stdout:true, exit:true, stderr:"off", sideEffects:false} |
默认比较项 |
normalize |
{crlf:true, ansi:false, trailingNewline:"exact", replacePaths:[]} |
默认归一化 |
saveArtifacts |
false |
是否把每侧完整 stdout/stderr 另存到 artifacts/ |
归一化规则:crlf 把 CRLF 与孤立 CR 统一成 LF;ansi 置 true 才剥颜色序列;trailingNewline 为 "exact" 保留末尾换行、为 "ignore" 去掉末尾所有换行;replacePaths 把指定前缀替换成 <path>,stderr 里构建目录差异靠它抹平。compare.stderr 除了 "off" 还有 "normalized"(按 normalize 比)与 "strict"(只统一 CRLF)。
环境变量 AOTJIT_TIMEOUT_MS / AOTJIT_MAX_OUTPUT_BYTES 可做进程级覆盖,调用参数优先级最高。
文件副作用¶
开启 sideEffects 后,插件会对指定路径做快照与比对:
compare: {
sideEffects: { paths: ["out/result.txt"], mode: "content", isolate: true, seed: ["input.bin"] }
}
paths 必填,相对运行目录,禁止绝对路径与 .. 逃逸,否则返回 SIDE_EFFECT_ESCAPE。mode 三种:hash 比 SHA-256、content 比内容并给首个不同行、tree 递归目录树。
isolate: true 时 AOT 与 JIT 各跑在 <workDir>/.aotjit/<runId>/<caseIndex>-aot|jit,seed 里的文件或目录从 workDir 复制进两侧,确保 AOT 写的文件 JIT 看不到。一次调用结束后(成功、失败或中途 break)整棵 <workDir>/.aotjit/<runId>/ 会被清掉,清理失败只进 warnings[]。seed 的字节数按展开后统计,AOT/JIT 两侧合计受 maxSideEffectBytes 约束:超出返回 SIDE_EFFECT_TOO_LARGE,复制失败返回 SIDE_EFFECT_COPY_FAILED。
错误码¶
插件给每类失败定义了 code,并带中文 hint 说明下一步该改什么:
| code | 含义 |
|---|---|
AOT_EXE_NOT_FOUND |
aotExe 缺失或指向的文件不存在 |
JIT_CMD_NOT_FOUND |
jitSpec.cmd 既不是存在的路径,也不在宿主 PATH 中 |
JIT_CMD_SHELL_WRAPPER |
cmd 是 .cmd/.bat;本插件不走 shell |
NO_CASES / TOO_MANY_CASES |
cases 为空 / 超过 maxCases |
INVALID_CASE |
字段类型错误(name/args/stdin/jitArgs/env/cwd/aotExe) |
WORKDIR_NOT_FOUND |
workDir 或 jitSpec.cwd 不存在 |
SPAWN_FAILED |
所有行都启动失败(如 EPERM) |
TIMEOUT |
超时(同时 timedOut: true,ok 仍为 true) |
OUTPUT_TRUNCATED |
输出被截断(对应 firstMismatch: "truncated") |
SIDE_EFFECT_ESCAPE / SIDE_EFFECT_TOO_LARGE / SIDE_EFFECT_COPY_FAILED |
副作用路径越界 / 超限 / seed 复制失败 |
验证¶
仓库自带四类校验命令:
npm test # 85 个单测(node --test,无真实进程)
npm run smoke # 假 subprocess 走通「加载 → 调用 → 日志 → 渲染」
npm run integration # 8 条真机用例(真实 exe + cmd.exe,需要能 spawn 的宿主环境)
npm run integration:parity # 10 用例夹具 + 注入错误(需要 toyc 与 clang++)
npm run integration 用 node:child_process 冒充宿主 subprocess,覆盖单用例全一致、stdout 差异定位、超时、截断、文件副作用与隔离、JIT 命令缺失、.cmd 包装器拒绝。DSH 的 workspace-write 沙箱里 Node 自己 spawn 会 EPERM,所以这一步要在宿主侧运行;插件本身在宿主里走 ctx.subprocess,不受影响。
npm run integration:parity 用仓库自带的 test/fixtures/parity/case-01..10.mlir,现场用 toyc --emit=exe 生成 AOT 可执行文件、用 --emit=run 跑 JIT 侧,先断言 10/10 全一致;随后只改 case-05 的打印值(退出码不变)当作注入错误,断言 firstMismatchCase:"case-05"、firstMismatchField:"stdout"、100 vs 101,其余 9 行仍 match:true。工具链默认读 mlir-lang 的 build/toyc.exe 与 LLVM 23.1.4 的 clang++.exe,可用 AOTJIT_TOYC / AOTJIT_CLANG 覆盖;缺工具时打印 SKIP 并以 0 退出。
这里有个容易踩的坑:注入必须只改 stdout。字段优先级是 exit 早于 stdout,若同时改掉退出码,正确结果就是 firstMismatchField:"exit"。
适用场景与注意¶
适合场景:AOT 后端与 JIT 路径需要长期保持行为一致,后端改动后需要对同一组用例做回归对拍的场景。它的价值在于省掉手工起双进程、抄输出、逐行比对的工作,并把差异结构化输出,便于直接定位。
几点注意:
1、插件以当前 dsh 进程的权限运行,会 spawn 指定的可执行文件并读取指定路径的输出,安装前应检查源码与许可证。
2、本插件不走 shell,argv 直传。需要管道或重定向时,应自己写包装脚本,但 .cmd / .bat 会被拒绝。
3、workDir 必须存在,相对路径与默认 cwd 都按它解析。
4、日志与 artifacts/ 写在会话 cwd 下,注意不要把它们纳入版本控制或当成构建输入。
5、副作用隔离目录在调用结束后统一清理,不要在用例里依赖它持久存在。
结尾¶
这个插件定位很窄:不做模糊匹配,不做多场景编排,只把「跑两侧 + 比对 + 定位 + 留日志」做成一次调用,字段优先级和边界行为都写在 README 里。如果你的工作涉及 MLIR/LLVM 的 AOT 与 JIT 一致性回归,可以先从 npm run integration:parity 那条链路验证工具链,再用真实用例跑一次。
目录页:https://www.skillhub.cn/plugins/Rcyc-1025/dsh-aotjit-compare
GitHub:https://github.com/Rcyc-1025/dsh-aotjit-compare