6.10 專案工程學#
對象:veryl2chip 的 Rust 多 crate 結構、發佈與整合測試。 這一章講「怎麼把一堆 EDA 演算法組織成一個可以 發上 crates.io、可以在瀏覽器跑、敢讓人改的專案」。
6.10.1 Crate 地圖#
flowchart TB
subgraph 儲存庫根(無建置系統)
VS["veryl-sim/<br/>模擬器(lib+bin)"]
VE["veryl-eda/<br/>綜合→P&R→ASIC(lib+bin)"]
VW["veryl-wasm/<br/>cdylib(publish=false)"]
WEB["web/<br/>npm(esbuild+TS)"]
end
VS -->|"路徑依賴"| VE
VE --> VW
VW --> WEB
TB["examples/(獨立 Veryl 專案)"] --> VE
TU["tutorial/(獨立 Veryl 專案)"] --> VE兩個 Rust workspace 根(veryl-sim/、veryl-eda/), 倉庫根沒有 Cargo workspace——每個 crate 可以 獨立建置、獨立發佈。veryl-wasm 是 veryl-eda 的 cdylib 消費者;web/ 只認 wasm 與 TS。
6.10.2 lib + bin 分離#
每個主要 crate 都是 lib + bin:
# veryl-sim/Cargo.toml 的形狀
[lib] name = "veryl_sim"
[[bin]] name = "veryl-sim"
bin 是薄殼(cli.rs 的幾十行 wrapper),真正邏輯在 lib。 v2c(veryl-eda 的 bin)更是如此——它只是一個分派器:
v2cdispatches toveryl_sim::cli::main(sim, list) andveryl_eda::cli::main(synth, fpga, asic, gatesim, cc, pdk)
為什麼:lib 才能被別的 crate 依賴(wasm、另一個 bin、 測試),bin 不能。分離讓 veryl-wasm 能 use veryl_eda::... 而不拖進 CLI/arg 解析。
跨 crate 共用:路徑依賴#
veryl-eda 對 veryl-sim 用路徑依賴(AGENTS: "path dependency")——同倉庫內最簡單的共用方式, 不必發佈版本就能用最新碼。發佈時(見 6.10.5) 才把路徑依賴換成 crates.io 版本。
6.10.3 crates.io 命名的對策#
問題:veryl-* 前綴是官方 Veryl 專案的。所以:
the packages are
veryl2chip-simandveryl2chip-eda(notveryl-*, which is the official Veryl project's prefix);[lib] name/[[bin]] namekeepveryl_sim/veryl-simetc., and dependents useveryl-sim = { package = "veryl2chip-sim", ... }
也就是說套件名與 lib 名脫鉤:
| crates.io 套件名 | lib/bin 名 | 程式碼中 |
|---|---|---|
veryl2chip-sim | veryl_sim / veryl-sim | veryl_sim::... |
veryl2chip-eda | veryl_eda / veryl-eda | veryl_eda::... |
依賴方用 { package = "veryl2chip-sim" } 改指向, 程式碼一行不用改。這是 Cargo 的 rename 功能最乾淨的用法。 veryl-wasm 設 publish = false(它是 web 的內部構件, 不該出現在 crates.io)。
6.10.4 整合測試哲學:每一層都有 oracle#
本專案的測試不只是「單元測試通過」,而是 每個演算法階段都有一個獨立實作的參照:
| 層 | 被測物 | Oracle | 測試 |
|---|---|---|---|
| RTL 語義 | veryl-sim | veryl 的 $assert 測試台、iverilog | ./test.sh |
| 綜合 | elab/optimize | veryl check(官方語法真值) | 官方 CLI |
| 邏輯 | netlist | 隨機模擬 + ≤16 輸入窮舉證明 | optimize 內建 |
| FPGA 對映 | ice40 map | yosys 形式等價(veryl build 為金) | test.sh(kind ice40) |
| ECP5 對映 | ecp5 map | yosys(去 init 屬性) | test.sh(kind ecp5) |
| P&R 正確性 | own P&R | fpga/cosim.py:icebox_vlog 路由結果 vs 網表、隨機輸入 | pnr_test.sh |
| ASIC 對映 | asic map | yosys 等價 + netsim 情境 | cargo test |
| 版圖 | layout/drc | KLayout(tools/gds_check.py,含單 net 內凹缺) | GDS_PY |
| C 編譯器 | cc.rs | spike(官方 ISA sim) | iss_rv32ima_matches_spike |
| CPU 微架構 | RvCore RTL | ISS(逐週期) | threads_on_rvsoc |
| 板引擎 | board Engine | netsim | board_engine_matches_netsim |
| 位元流 | bitstream decode | nextpnr 對照(M13 測試) | cargo test bitstream |
設計原則可以濃縮成一句:
演算法寫得再自信,也要有一個不是自己的東西能證明它。
test.sh 的分層也反映了這個哲學:
./test.sh # veryl-sim、cargo test(veryl-eda)、tutorial/check.sh、npm test
./test.sh --full # + veryl-eda/test.sh(yosys 形式驗證,需要 oss-cad-suite)
形式驗證(yosys)與 cosim(Python)不在 CI—— 它們依賴外部工具鏈(oss-cad-suite、KLayout、 riscv 工具鏈),CI 只跑自包含的部分 (.github/workflows/static.yml)。
6.10.5 發佈流程#
./publish.sh:
- 無參數:dry-run(
cargo publish --dry-run、 檢查版本一致性); --yes:先 sim 後 eda(eda 依賴 sim 的版本號)、 然後打 tag。
tag 樣式(AGENTS):1.1.0 -> v1.1——兩段式, 只保留 major.minor。新版本要把兩個 crate 的版本 與 eda 對 sim 的依賴版本一起 bump。wasm 不發佈, web/dist 是提交的構件(「rebuild after changing Rust or TS」)。
6.10.6 wasm 與 native 共用碼#
同一份 veryl-eda 同時編成 native bin 與 wasm cdylib。 cfg(target_arch) 只用在必要處:
// 形狀示意
#[cfg(not(target_arch = "wasm32"))]
fn main() { cli::main(); }
控制性約束:
- 核心演算法(elab、netlist、pnr、board、rv)零平台分支; wasm 端只多一層 JSON API(6.8 節);
- wasm 端不能有 stdout——所有輸出走 JSON 回傳值; panic 走
panic_message(6.8 節); - 平台相依的輸入(chipdb 檔案)由呼叫者餵進來 (native 讀檔、wasm 收 base64),核心只認記憶體裡的字串。
如此一來「npm test 對 CLI 的 GDS 位元組逐一比對」 才成立:native 與 wasm 是同一份演算法的兩個殼。
6.10.7 文件的三語言紀律#
README 三份:README.md(英)、README.tw.md(繁,權威)、 README.cn.md(簡,生成):
Edit the Traditional and English texts; regenerate the Simplified one with
tools/tocn.py(OpenCC + mainland EDA terms).
與 i18n(6.8.7)同一哲學:兩份手寫、第三份生成, 工具保證一致性。課程文件(tutorial)另有 「教程文字與 src/ 必須一致」的規定。
6.10.8 版本化構件:web/dist 進 git#
大多數專案把構件放 .gitignore,這裡相反:
web/distis committed (open-and-use); rebuild it after changing Rust or TS.
理由是使用場景:教師/學生 clone 下來雙擊 index.html 就要能用,不能要求先裝 Rust 工具鏈。代價(大 diff、 構件漂移)用 CI 的「push to main 就重建並部署 Pages」 抵銷——倉庫裡的 dist 永遠等於 main 的構建結果。
6.10.9 build.sh 與 test.sh 的形狀#
兩個根腳本是「專案的 README 之外的 README」:
./build.sh # whole project: veryl-sim, veryl-eda (release),
# web/dist (wasm + TS); --no-web
./test.sh # veryl-sim, cargo test (veryl-eda),
# tutorial/check.sh, npm test; --full adds veryl-eda/test.sh
build.sh 的順序有意義:先兩個 Rust crate(release—— 模擬性能優先,永遠 --release),再 web(wasm 是 veryl-eda 的重編譯 + TS bundle)。--no-web 讓純 Rust 開發跳過最慢的一步。
test.sh 的四個測試區各自守一道門:
| 測試區 | 守的東西 |
|---|---|
| veryl-sim 自測 | 模擬器語義(counter 範例跑通) |
cargo test(veryl-eda) | 綜合→網表→情境模擬、board、rv |
tutorial/check.sh | 課程專案整條流程(教程爛 = 失敗) |
npm test | wasm + TS 前端(含 GDS 位元組比對 CLI) |
tutorial/check.sh 值得特別說:它把教學內容當 一等測試對象。課程的每個範例都要過完整的 模擬 + 綜合 + (有板子的話)虛擬板流程—— 「教程裡的程式碼不能編譯」在這個專案裡是 CI 失敗。
6.10.10 v2c:一個二進位、一個心智模型#
v2c(veryl-eda/src/bin/v2c.rs)存在的理由是 使用者體驗的收斂:
| v2c 子命令 | 轉發到 |
|---|---|
sim、list | veryl_sim::cli::main |
synth、fpga、asic、gatesim、cc、pdk | veryl_eda::cli::main |
cli::V2C 持有子命令,所以參數錯誤會指向 v2c help <子命令> 而不是裸的 clap 錯誤。 veryl-eda/tests/v2c.rs 把每個子命令都實際跑一遍—— 分派層是所有使用者的入口,它壞掉 = 一切都壞。
舊的 veryl-sim / veryl-eda binary 仍在(薄殼), 做為 crates.io 使用者的慣例入口。
6.10.11 錯誤訊息是資料,不是字串#
跨 crate 邊界的錯誤一律結構化:
pub struct Diag { pub file: String, pub line: usize, pub msg: String }
- C 編譯器回
Vec<Diag>(按檔案/行排序去重); - wasm 把它變
errors: [{file, line, msg}], IDE 直接在編輯器畫底線; - CLI 把它印成
file:line: msg。
同一個結構三個消費者,各取所需——這比 「Rust 端格式化好字串、前端再想辦法 parse 回來」 便宜得多。V2C 層的錯誤(找不到檔案、top 不存在) 也是 JSON 的 {ok:false, error} 一路傳到頁面。
6.10.12 文件即測試#
AGENTS.md 本身是這個專案工程學的一部分: 它把每一個「曾經絆過人」的決定寫成一句可執行的規則 (「Keep the A-extension address compare on rs1_data」、 「never read below sp」、tag 樣式、pcf 的專案檔化)。 新貢獻者(或下一個 AI 會話)讀它就能避開 所有已踩過的坑——這是把 review 意見沉澱成規格, 而不是讓它們留在 PR 的歷史裡。
同樣的哲學在測試裡:tests.rs 的每個測試函數名 就是一條規格(board_engine_matches_netsim、 iss_rv32ima_matches_spike、threads_on_rvsoc)—— 讀測試清單就能讀出「這個專案擔心什麼」。
6.10.13 為什麼這樣設計#
- 兩個 workspace 而非一個根 workspace:sim 與 eda 可以各自發佈、各自 CI;倉庫根保持乾淨(只有 build/test 腳本與文件)。
- v2c 統一分派:一個二進位、一份 help、
veryl-eda/tests/v2c.rs把每個子命令都跑一遍—— 使用者只需要學一個命令。 - oracle 優先:寫演算法之前先想好「拿什麼證明它對」。 yosys/spike/KLayout 都是現成的權威,自己寫的第二實作 (netsim、ISS、cosim)則負責無外部依賴的快速回饋。
- 慢測試分層:
cargo test(快、自包含)→test.sh(中等)→test.sh --full(形式驗證)。 開發迴圈不被最重的檢查拖住,但重檢查一定在。
6.10.10 本章重點#
- lib+bin 分離讓演算法可被 wasm/測試依賴;bin(v2c) 只是分派器。
- crates.io 命名:
veryl2chip-*套件 + rename 回veryl-sim/veryl-eda的 lib 名;wasmpublish = false。 - 每層都有 oracle:spike(ISA)、yosys(等價)、 KLayout(DRC)、iverilog、cosim.py、netsim、ISS。
- 發佈:sim → eda、tag
vX.Y;publish.shdry-run 先行。 - native/wasm 共用同一份演算法(平台分支只在殼層); web/dist 進 git,CI 部署 Pages。