跳轉到

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,產出包括:

  1. 每個原始檔對應的 .sv 與 .sv.map:.sv 是生成的 SystemVerilog;.sv.map 是 source map,把生成的每一行對應回 Veryl 原始碼的檔案與行號(2.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——只是進入的階段與失敗的容忍度不同。


實作練習#

練習:為一個小專案搭建完整品質閘門#

  1. 建立含三個模組的專案(計數器、非同步 FIFO、頂層橋接),刻意留下一處 unused_variable、一處缺重置分支;
  2. 執行 veryl check,確認兩條 lint 都被觸發;分別用 _ 前綴與 #[allow(...)] 修復,體會兩種抑制的粒度差異;
  3. 執行 veryl fmt --check,製造一處格式不一致(手動加空格),確認非零退出;
  4. 把練習中的 CI YAML 放進專案,提交一個故意失敗的 PR(lint 失敗),觀察閘門攔截;
  5. 為非同步 FIFO 加上 WaveDrom 時序圖註解,veryl doc 之後在瀏覽器中檢視。

本節小結#

  1. veryl build 產生 .sv + .sv.map + filelist + Veryl.lock/dependencies/;生成的 SV 與原始碼並排,服務可預測性。
  2. Veryl.toml 的 [build] 區段決定硬體本身(時脈極性、重置風格)——同一份原始碼、不同設定、不同 SV;它必須納入版本控制。
  3. veryl fmt 的核心性質是語意不變——對硬體語言這是安全攸關的保證;#[fmt(skip)] / #[fmt(compact)] / #[align(...)] 處理硬體特有的排版需求。
  4. lint 清單檢查的是「硬體意圖的完整性」:missing_reset_statement、initial_assign(模擬與合成不一致的來源)、multiple_assign 等都有直接的硬體後果;抑制必須顯式(_ 前綴或 #[allow(...)])。
  5. veryl doc 只產生 pub 項目的文件——文件與公開 API 綁定;WaveDrom 時序圖是硬體文件的高價值格式。
  6. CI 是一組依成本遞增的閘門:lint → fmt → build → test → doc → 下游工具;硬體 CI 的綠燈比軟體弱,結構性檢查必須前移。
  7. 本章是「檢查」(靜態),第 8 章是「驗證」(動態)——分界線是是否執行設計。

下一節(7.3)處理專案規模化的最後一塊:相依套件從哪裡來、版本如何鎖定、以及硬體特有的版本治理問題。


下一節:第 7.3 節「相依性管理、套件註冊表與版本治理」