跳轉到

6.10 專案工程學#

對象:veryl2chip 的 Rust 多 crate 結構、發佈與整合測試。 這一章講「怎麼把一堆 EDA 演算法組織成一個可以 發上 crates.io、可以在瀏覽器跑、敢讓人改的專案」。

6.10.1 Crate 地圖#

兩個 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)更是如此——它只是一個分派器:

v2c dispatches to veryl_sim::cli::main (sim, list) and veryl_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-sim and veryl2chip-eda (not veryl-*, which is the official Veryl project's prefix); [lib] name / [[bin]] name keep veryl_sim / veryl-sim etc., and dependents use veryl-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:

  1. 無參數:dry-run(cargo publish --dry-run、 檢查版本一致性);
  2. --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/dist is 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 為什麼這樣設計#

  1. 兩個 workspace 而非一個根 workspace:sim 與 eda 可以各自發佈、各自 CI;倉庫根保持乾淨(只有 build/test 腳本與文件)。
  2. v2c 統一分派:一個二進位、一份 help、 veryl-eda/tests/v2c.rs 把每個子命令都跑一遍—— 使用者只需要學一個命令。
  3. oracle 優先:寫演算法之前先想好「拿什麼證明它對」。 yosys/spike/KLayout 都是現成的權威,自己寫的第二實作 (netsim、ISS、cosim)則負責無外部依賴的快速回饋。
  4. 慢測試分層:cargo test(快、自包含)→ test.sh(中等)→ test.sh --full(形式驗證)。 開發迴圈不被最重的檢查拖住,但重檢查一定在。

6.10.10 本章重點#

  • lib+bin 分離讓演算法可被 wasm/測試依賴;bin(v2c) 只是分派器。
  • crates.io 命名:veryl2chip-* 套件 + rename 回 veryl-sim/veryl-eda 的 lib 名;wasm publish = false。
  • 每層都有 oracle:spike(ISA)、yosys(等價)、 KLayout(DRC)、iverilog、cosim.py、netsim、ISS。
  • 發佈:sim → eda、tag vX.Y;publish.sh dry-run 先行。
  • native/wasm 共用同一份演算法(平台分支只在殼層); web/dist 進 git,CI 部署 Pages。