7.2 build、fmt、lint、doc 與 CI 整合#
四個命令與一個前提#
Veryl 的日常開發迴路由四個命令構成:
| 命令 | 職責 | 對應的軟體概念 |
|---|---|---|
veryl build | 產生 SystemVerilog 與 filelist | 編譯 |
veryl fmt | 格式化原始碼 | gofmt / rustfmt |
veryl check | lint 檢查 | clippy / eslint |
veryl doc | 產生文件 | rustdoc / javadoc |
這四個命令共享同一個前提,也是本章反覆出現的主題:它們都作用在語意明確的語法模型上。2.2 節已經論證過:語法簡化不是生產力工具的裝飾性前提,而是生產力工具得以正確運作的前提——一個語意模糊的語言,格式化器會改壞程式碼,lint 會誤報,文件產生器會漏項。本章讀者應該帶著這個視角檢視每一個命令。
veryl build 的完整行為#
產出物#
對專案執行 veryl build,產出包括:
- 每個原始檔對應的
.sv與.sv.map:.sv是生成的 SystemVerilog;.sv.map是 source map,把生成的每一行對應回 Veryl 原始碼的檔案與行號(2.2 節已詳述其在除錯中的決定性作用)。 - filelist(
.f檔案):列出所有生成 SV 檔案的路徑,供下游工具直接引用:
verilator --lint-only -f Veryl.f
filelist 看似小事,實際上它消除的是硬體流程中一類持續性的摩擦:Makefile 裡手動維護的檔案清單,隨專案成長必然漂移,而「忘了把新模組加進清單」造成的「在我機器上編譯得過」問題,是硬體團隊的日常。 3. Veryl.lock 與 dependencies/:若專案有相依套件,build 會解析相依、鎖定版本、下載到 dependencies/(完整機制見 7.3 節)。 4. veryl clean:清除所有產出物,回到乾淨的原始碼狀態。
生成的 SV 與原始碼並排#
build 的一個重要行為是:生成的 .sv 與原始碼並排存放(預設在同一專案樹下)。這使得「對照閱讀」成為零成本的動作——打開 Top.sv 與 Top.veryl 並排放,逐行對應。
這個設計直接服務於全書的核心命題:可預測性。生成的 SV 是可讀的(2.2 節),而「可讀」要發揮作用,前提是工程師看得到它。把它藏在 build 目錄的深處,可讀性就只是理論值。
Veryl.toml:一份設定,決定生成的硬體#
專案設定的全貌#
[project]
name = "my_soc"
version = "0.3.1"
[build]
source = "src"
target = { type = "source" }
clock_type = "posedge"
reset_type = "reset_async_low"
filelist_type = "absolute"
[lint]
unused_variable = "enable"
unassign_variable = "error"
[dependencies]
veryl_sample = { git = "https://github.com/veryl-lang/sample", version = "0.1.0" }
四個區段的職責:
| 區段 | 職責 | 影響 |
|---|---|---|
[project] | 專案名與版本 | 生成 SV 的模組名前綴(3 章)、相依解析(7.3 節) |
[build] | 產生 SV 的方式 | 硬體本身:時脈極性、重置風格(4.2 節) |
[dependencies] | 相依套件 | 7.3 節 |
[lint] | lint 規則的等級 | 7.2 節下文 |
同一份原始碼,不同設定,不同的硬體#
4.2 節已經展示過這個機制的語法層效果,這裡從專案管理的角度再看一次。同一份 Veryl 原始碼:
設定 A(ASIC 風格):
[build]
clock_type = "posedge"
reset_type = "reset_async_low"
設定 B(FPGA 風格):
[build]
clock_type = "posedge"
reset_type = "reset_sync_high"
生成的 SV 分別是:
// 設定 A:非同步低電位重置
always_ff @(posedge i_clk or negedge i_rst) begin
if (!i_rst) begin
// 設定 B:同步高電位重置
always_ff @(posedge i_clk) begin
if (i_rst) begin
兩份 SV 描述的是不同的觸發器單元、不同的重置網路、不同的時序簽核要求——而原始碼一行未改。這就是 4.2 節的結論在工具鏈層的落實:極性與同步性是系統層級的決定,集中在 Veryl.toml 一處,可版本控制、可稽核、可溝通。
由此產生一條工程紀律:Veryl.toml 必須納入版本控制。它不是「設定」,它是原始碼的一部分——它與 .veryl 檔案共同構成了完整的硬體描述。
veryl fmt:語意不變的格式化#
為什麼「語意不變」在硬體語言是攸關性質#
任何語言的格式化器都宣稱「不改變語意」,但在硬體語言裡,這個保證的失敗後果比軟體嚴重得多,原因回到 4.1 節的語意分類:
- 硬體語意由賦值所在的區塊(
always_ff/always_comb)與賦值語境共同決定; - 格式化器若是重排了區塊結構、合併了語句、或改變了表達式的斷行位置到某種會影響解析的程度,就會改變生成的硬體;
- 更糟的是:改變之後的程式碼仍然編譯通過、仍然模擬通過——差異只在矽上或特定時序條件下暴露。
因此 Veryl 的 formatter 遵循一條保守策略:保留結構、規範縮排與空白、不重排、不壓行。它不做任何「聰明」的重構——因為在硬體語言裡,聰明的重構是安全攸關的風險。
CLI 用法與 CI 整合#
veryl fmt # 就地格式化整個專案
veryl fmt --check # 只檢查不修改:有未格式化的檔案則非零退出
--check 模式是 CI 的關鍵:格式不一致會讓 CI 失敗,迫使問題在 PR 階段解決。這把「格式爭論」從 code review 中整個移除——review 的注意力是團隊最稀缺的資源,不該花在縮排上。
格式化屬性#
#[fmt(compact)]
enum State {
Idle, Run, Error,
}
#[fmt(skip)]
var r_lookup : logic<8> = {
// 人工對齊的查找表:格式化器不碰這裡
}
#[fmt(compact)]:壓縮該項目的排版(適合短的列舉、簡單宣告);#[fmt(skip)]:完全跳過——格式化器必須有「不碰這裡」的權利(2.2 節論證過:沒有 skip 的格式化器會被工程師棄用)。
#[align(number, identifier)] 則處理硬體原始碼的特有需求——大型位元串接表的對齊:
#[align(number, identifier)]
always_comb {
o_decode = '0;
case i_opcode {
8'h01: o_decode = OPCODE_LOAD;
8'h02: o_decode = OPCODE_STORE;
8'h33: o_decode = OPCODE_ALU;
default: o_decode = OPCODE_ILLEGAL;
}
}
十六進位數值的寬度對齊、欄位名的對齊,讓解碼表一眼可比對——這種人工排版價值是軟體語言沒有的,因為軟體沒有「位欄位對位欄位」的視覺閱讀需求。
veryl check:lint 清單#
完整清單與觸發情境#
| lint | 觸發情境 | 硬體後果 |
|---|---|---|
unused_variable | 宣告了但從未讀取 | 死碼;通常是遺漏的邏輯 |
unassign_variable | 宣告了但從未賦值 | 模擬中是 x,硬體中是隨機值 |
missing_reset_statement | always_ff 中被賦值的變數沒有重置分支 | 上電狀態不可預測 |
missing_port | 實例化時有埠未連接 | 懸空輸入(隨機)或懸空輸出 |
initial_assign | 不該有初始賦值的地方出現初始賦值 | 模擬與合成不一致的來源 |
multiple_assign | 同一變數被多處賦值 | 多驅動衝突 |
逐一說明最有硬體特徵的幾項:
missing_reset_statement:時序邏輯沒有重置分支,意味著該 FF 的上電值取決於製程隨機性。有些 FF(資料路徑中間級)刻意不重置以省面積與時序壓力——這是合法的設計選擇,所以此規則是警告級,並可用 _ 前綴或 #[allow(...)] 抑制。但抑制必須是顯式的:無重置的 FF 應該是「決定過不重置」,不是「忘了寫」。
initial_assign:這是與「可綜合性」關係最深的一項。initial 區塊在模擬中執行、在 ASIC 合成中被忽略(部分 FPGA 工具支援)——同一段程式碼在模擬與真實硬體上行為不同。這正是第 0 章說的「語意不確定」的直接來源:你的模擬通過可能依賴於一個根本不會存在的初始化。Veryl 把它列為檢查項,是在維護「模擬通過 = 硬體正確」這條推理鏈的完整性。
multiple_assign:SystemVerilog 的 logic 允許多個 assign,行為是後者覆蓋前者(或衝突,依工具而定)——靜默的錯誤。Veryl 的語意模型直接把多重賦值列為檢查項。
抑制機制#
// 方式一:_ 前綴——把「刻意不用」編碼進識別字
var _debug_probe : logic<8>
// 方式二:屬性——區塊層級的抑制
#[allow(unused_variable)]
module Top {
var unused_signal : logic<8>
}
兩種抑制的分工(2.2 節已述):_ 前綴是識別字層級的、隨著訊號走;#[allow(...)] 是模組層級的、適合整段刻意豁免的區域。兩者都是程式碼內可搜尋的——與 6.2 節 unsafe (cdc) 同一個哲學:豁免必須顯式、可審查。
veryl doc:文件與公開 API 綁定#
基本用法#
/// 文件註解(2.2 節已展示語法)經 veryl doc 產生靜態文件網站,支援三種內嵌格式:
- Markdown:一般說明;
- WaveDrom:時序圖——硬體文件最重要的圖表類型。「這個介面的握手時序長什麼樣」用文字永遠說不清楚,用 WaveDrom 一目了然;
- Mermaid:方塊圖與流程圖。
/// 非同步 FIFO
///
/// 資料經雙埠 RAM 傳遞 指標以格雷碼跨域同步
///
/// ```wavedrom
/// { signal: [
/// { name: "wr_en", wave: "01.0.." },
/// { name: "full", wave: "0..10." },
/// { name: "rd_en", wave: "0...10" },
/// { name: "empty", wave: "10...0" },
/// ]}
/// ```
module AsyncFifo #(param WIDTH: u32 = 8) (
i_clk_w : input 'w clock,
i_wr_en : input 'w logic,
// ...
) {
}
只產生 public 項目#
veryl doc 只為標記 pub 的項目產生文件。這個限制是工程紀律的機制化:
文件 = 公開 API。 內部實作細節不進文件,公開介面必須有文件。
它同時是雙向的壓力:想讓元件出現在文件裡,就必須 pub;pub 了就必須寫文件。3.3 節的 pub 可視性控制與本節的文件產生,是同一個設計的兩半——可視性決定文件範圍,文件要求可視性有理由。
CI/CD 整合#
完整的 GitHub Actions 範例#
name: rtl-ci
on: [push, pull_request]
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 安裝 verylup 並鎖定工具鏈版本
run: |
curl -LO https://github.com/veryl-lang/verylup/releases/latest/download/verylup-latest-linux-x86_64.tar.gz
tar xzf verylup-latest-linux-x86_64.tar.gz
sudo mv verylup /usr/local/bin/
verylup setup
verylup install 0.13.0
- name: lint 檢查
run: veryl +0.13.0 check
- name: 格式檢查
run: veryl +0.13.0 fmt --check
- name: 建置
run: veryl +0.13.0 build
- name: 測試(原生模擬器)
run: veryl +0.13.0 test
- name: 產生文件
run: veryl +0.13.0 doc
- name: Verilator lint 生成的 SV
run: |
sudo apt-get install -y verilator
verilator --lint-only -f Veryl.f
品質閘門的設計#
這條 pipeline 是一組閘門,依成本遞增排列:
| 閘門 | 秒級成本 | 攔截什麼 |
|---|---|---|
check(lint) | 秒 | 意圖不完整:未賦值、缺重置、多驅動 |
fmt --check | 秒 | 格式漂移(review 噪音) |
build | 秒 | 語法與域檢查錯誤(含 CDC 編譯錯誤,6.2 節) |
test | 分鐘 | 功能錯誤(第 8 章) |
doc | 秒 | 缺文件的公開 API |
| Verilator lint | 分鐘 | 對生成 SV 的獨立第二意見 |
排列原則:便宜的閘門在前。lint 一秒鐘就能擋掉的錯誤,不應該消耗模擬時間;模擬才能發現的問題,不應該等到下游綜合。這是軟體 CI 的標準智慧,硬體同樣適用——而且硬體的「下游成本」更陡(綜合一小時、簽核一天、投片一百萬美元),閘門前移的收益更大。
硬體 CI 與軟體 CI 的差異#
| 面向 | 軟體 CI | 硬體 CI |
|---|---|---|
| 編譯 | 秒級 | veryl build 秒級,但下游綜合是分鐘到小時級 |
| 測試 | 單元測試秒級 | 功能模擬分鐘級,全晶片模擬小時級 |
| 「部署」 | 部署可回滾 | 投片不可回滾 |
| 失敗訊號 | 測試失敗即失敗 | 模擬通過仍可能有 CDC/時序/物理問題 |
最後一列是本書第 6 章與本章的交會:硬體 CI 的綠燈比軟體 CI 的綠燈弱。因此硬體 pipeline 除了功能測試,必須加入結構性檢查——Veryl 把 CDC 檢查放進了編譯(6.2 節),把 lint 放進了 check,這正是為了讓「結構性問題」在秒級閘門就被攔截,而不是累積到小時級或矽上。
與第 8 章的分工#
最後釐清本章與下一章的邊界:
- 本章講「檢查」:靜態的、針對程式碼本身的、不需要執行設計的——lint、格式、域檢查、文件完整性;
- 第 8 章講「驗證」:動態的、針對設計行為的、需要執行設計的——測試框架、模擬器、斷言、隨機測試。
兩者的分界線是「是否執行設計」。檢查便宜、必過、放進每個 commit 的 CI;驗證昂貴、可選深度、按風險分配資源。一個成熟的硬體專案,兩層都要有,而且都要進 CI——只是進入的階段與失敗的容忍度不同。
實作練習#
練習:為一個小專案搭建完整品質閘門#
- 建立含三個模組的專案(計數器、非同步 FIFO、頂層橋接),刻意留下一處
unused_variable、一處缺重置分支; - 執行
veryl check,確認兩條 lint 都被觸發;分別用_前綴與#[allow(...)]修復,體會兩種抑制的粒度差異; - 執行
veryl fmt --check,製造一處格式不一致(手動加空格),確認非零退出; - 把練習中的 CI YAML 放進專案,提交一個故意失敗的 PR(lint 失敗),觀察閘門攔截;
- 為非同步 FIFO 加上 WaveDrom 時序圖註解,
veryl doc之後在瀏覽器中檢視。
本節小結#
veryl build產生.sv+.sv.map+ filelist +Veryl.lock/dependencies/;生成的 SV 與原始碼並排,服務可預測性。Veryl.toml的[build]區段決定硬體本身(時脈極性、重置風格)——同一份原始碼、不同設定、不同 SV;它必須納入版本控制。veryl fmt的核心性質是語意不變——對硬體語言這是安全攸關的保證;#[fmt(skip)]/#[fmt(compact)]/#[align(...)]處理硬體特有的排版需求。- lint 清單檢查的是「硬體意圖的完整性」:
missing_reset_statement、initial_assign(模擬與合成不一致的來源)、multiple_assign等都有直接的硬體後果;抑制必須顯式(_前綴或#[allow(...)])。 veryl doc只產生pub項目的文件——文件與公開 API 綁定;WaveDrom 時序圖是硬體文件的高價值格式。- CI 是一組依成本遞增的閘門:lint → fmt → build → test → doc → 下游工具;硬體 CI 的綠燈比軟體弱,結構性檢查必須前移。
- 本章是「檢查」(靜態),第 8 章是「驗證」(動態)——分界線是是否執行設計。
下一節(7.3)處理專案規模化的最後一塊:相依套件從哪裡來、版本如何鎖定、以及硬體特有的版本治理問題。
下一節:第 7.3 節「相依性管理、套件註冊表與版本治理」