前言¶
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