跳轉到

3.3 package、別名與可視性控制#

從單一模組到大型專案#

第 3.1 節建立了模組的基本骨架,第 3.2 節建立了介面的封裝能力。但這兩者都還停留在「一個模組」的尺度。真正的硬體專案是:

  • 一個 SoC 有幾百個模組;
  • 幾十組人共用同一組位寬常數(例如 DATA_WIDTH = 64、NUM_LANES = 8);
  • 幾十組人共用同一組位元索引工具(例如「取得第 n 個 set bit 的索引」);
  • 有些模組是可重用的 IP,要提供給其他專案;
  • 有些模組是內部實作細節,不該出現在公開 API 裡。

這一章的三個機制正好對應這三個需求:

機制 解決的問題
package 共用常數、型別、函式
alias 簡化冗長的泛型實例化
pub / 可視性 管理「公開 API 面」
專案前綴 多專案整合的命名隔離

SystemVerilog 的 package 機制#

SV 的 package 是比 module 更一般的容器,可以包含 parameter、localparam、typedef、enum、struct、function、task、class 與 modport。

package my_pkg;
    parameter int DATA_WIDTH = 64;
    typedef enum logic [1:0] {IDLE, RUN, DONE} state_e;
    function automatic int count_bits(input logic [31:0] v);
        return $countones(v);
    endfunction
endpackage

使用時需要 import:

import my_pkg::*;

或選擇性匯入:

import my_pkg::DATA_WIDTH;

SV 的 package 機制有幾個實際的痛點:

(一)命名空間是「扁平」的。 import my_pkg::*; 會把套件裡的所有名稱倒進當前作用域,與本地名稱產生衝突。SV 的 import 支援 :: 限定存取(my_pkg::DATA_WIDTH),但一旦用了 import ... ::*,衝突就無法避免。

(二)編譯順序依賴。 SV 的編譯順序(package 必須在使用之前編譯)讓多專案整合變得脆弱。

(三)類別繼承的命名空間問題。 import 進來的類別,在定義新的子類別時會遇到「簡單名 vs 限定名」的經典問題(class B extends A 在 A 被 import 時可用,但 class B extends pkg::A 就不行——這是 SV 的著名限制)。

(四)typedef 的命名空間。 typedef 在 SV 中是編譯單元作用域的(package 外),這造成跨 package 的型別引用複雜。

Veryl 的 package 借鑑了 Rust 的模組系統,在這些點上做了更乾淨的設計。


Veryl 的 package#

定義#

package Pkg {
    const A : u32 = 1;
    const B : u32 = 2;

    type Word = logic<64>;

    function f(
        i_x : u32,
    ) -> u32 {
        return i_x + A;
    }
}

對照 SV:

package Pkg;
    localparam int A = 1;
    localparam int B = 2;
    typedef logic [63:0] Word;
    function automatic int f(input int i_x);
        return i_x + A;
    endfunction
endpackage

三種存取方式#

(一)完整限定名(永遠可用,且是最明確的寫法):

const X : u32 = Pkg::A;
var  y : Pkg::Word;
var  z : u32 = Pkg::f(3);

(二)匯入單一名稱:

import Pkg::A;

const X : u32 = A;

(三)匯入多個名稱:

import Pkg::{A, B};

const X : u32 = A;
const Y : u32 = B;

(四)匯入全部:

import Pkg::*;

const X : u32 = A;

列舉成員的 import#

這是 Veryl 的一個特色功能,源於 2.2 節介紹的「列舉變體各有命名空間」設計。

package Pkg {
    enum Color {
        Red,
        Green,
        Blue,
    }
}

匯入列舉成員本身:

import Pkg::Color::Red;

var c : Pkg::Color = Red;

匯入列舉的所有成員:

import Pkg::Color::*;

var c : Pkg::Color = Red;

為什麼這個功能有價值?

在 SV 裡,列舉成員是全域的,所以「匯入列舉成員」這件事不存在——它們本來就在作用域裡(這正是第 2.2 節批評的「全域污染」)。

在 Veryl 裡,列舉成員有自己的命名空間,所以若不匯入,就必須寫 Pkg::Color::Red——這有點冗長。import Pkg::Color::*; 提供了 SV 使用者熟悉的簡短寫法,同時保留了命名空間隔離的性質。

這是「兩全其美」的設計:想要短就用 import,想要明確就用限定名,而兩者都不會造成全域污染。

命名空間的巢狀#

package 之內還可以定義 package:

package Outer {
    const X : u32 = 1;

    package Inner {
        const Y : u32 = 2;
    }
}

存取:

const A : u32 = Outer::X;
const B : u32 = Outer::Inner::Y;

這在大型專案中非常實用:可以按功能分層組織常數,例如 Bus::Axi::ADDR_WIDTH、Bus::Apb::ADDR_WIDTH。


全域函式(Global Function)#

定義與發射#

Veryl 允許在專案層級(即 module / interface / package 之外)定義函式:

function clog2(
    i_x : u32,
) -> u32 {
    var r : u32 = 0;
    var v : u32 = i_x;

    while v > 1 {
        v = v >> 1;
        r = r + 1;
    }

    return r;
}

這個函式不屬於任何 package,因此可以在專案中的任何地方呼叫:

const DEPTH : u32 = clog2(1024);

「發射進呼叫端的命名空間」是關鍵機制。

當 clog2 在模組 A 與模組 B 中都被呼叫時,產生的 SystemVerilog 會是:

module A;
    ...
    localparam int DEPTH = A_clog2(1024);
    function automatic int A_clog2(input int i_x);
        ...
    endfunction
endmodule
module B;
    ...
    localparam int DEPTH = B_clog2(1024);
    function automatic int B_clog2(input int i_x);
        ...
    endfunction
endmodule

也就是說,函式的定義會被複製到每個使用它的模組裡,並加上該模組的前綴。

為什麼要這樣?

因為 SV 不允許在模組外部定義全域函式。SV 的頂層只能有模組、介面、package 與程式(program)。所以一個「共用的 utility 函式」在 SV 裡只有三種做法:

  1. 放在 package 裡,然後 import。SV 的 package 函式需要在 import 後才能呼叫,且 automatic 關鍵字、static 變數、遞迴等都有細微規則。
  2. 用 `define 巨集。但巨集是文字展開,無法做型別檢查,且在巨集內部引用巨集參數時有著名的語法陷阱。
  3. 複製貼上到每個模組。這是最常見的,也是最糟糕的——複製的版本會逐漸分歧。

Veryl 的全域函式機制同時解決了這三個問題:不需要 import、不需要巨集、不需要複製到原始碼裡(複製發生在編譯器產生的碼裡,而且保證每份都是最新的)。

這個設計的取捨是:產生的 SV 會有重複的函式定義。 對合成器來說這是無害的(函式會被內聯或作為靜態函式處理);對 lint 工具來說可能會有一些「重複定義」的雜訊,但因為有前綴所以不會真的衝突。

全域函式的泛型與 pub#

function max_generic::<T: u32> (
    i_a : T,
    i_b : T,
) -> T {
    if i_a > i_b {
        return i_a;
    } else {
        return i_b;
    }
}

pub 控制它是否對外可見(見後面的可視性控制一節)。

實務上的重要建議:全域函式是「最後手段」,不是「預設做法」。理由是:

  1. 全域函式無法被參數化(不像 package 可以明確宣告「這個函式屬於這個套件」)。
  2. 全域命名空間是扁平的——名字必須在整個專案中唯一。
  3. 測試與文件較難組織。

建議的原則:如果一個函式只服務一個 package,就放在 package 裡;如果它確實是「整個專案都要用的小工具」(例如 clog2、max、min、reverse_bits),才放在全域。


泛型 package#

定義#

package PackageA::<T: u32> {
    const X : u32 = T;
    const Y : u32 = T * 2;
}

注意泛型參數 T 可以用在 const 的值裡——這是泛型 package 最有用的地方:它是一種「依參數產生不同常數集」的機制。

實例化#

const V : u32 = PackageA::<1>::X;    // 等於 1
const W : u32 = PackageA::<4>::Y;    // 等於 8

與泛型模組的比較#

泛型模組 泛型 package
產生硬體 是 否
產生常數 是(透過參數) 是
典型用途 可參數化的元件 依參數變化的常數集

泛型 package 的實務用例:當一個常數是「某個位寬的衍生值」時,泛型 package 可以保證一致性:

package Widths::<W: u32> {
    const BYTE    : u32 = W / 8;
    const WORD    : u32 = W / 32;
    const ADDR    : u32 = $clog2(W);
}
const A : u32 = Widths::<64>::BYTE;    // 8
const B : u32 = Widths::<64>::ADDR;    // 6

別名 alias#

問題#

泛型實例化的完整名單可能非常冗長:

const X : u32 = PackageA::<1, 2, 3>::X;

如果這個泛型實例化在專案中到處使用(例如一個複雜的泛型介面 InterfaceA::<32, 8, 4>),每次都寫全名會很繁瑣,而且寫錯一個參數就會導致型別不匹配。

alias 的語法#

alias package PkgA123 = PkgA::<1, 2, 3>;

注意 alias package 這個雙關鍵字寫法:它表明被別名的是一個 package。

使用#

const X : u32 = PkgA123::X;

別名的其他形式#

alias 也可以用在模組與介面上:

alias module Fifo64 = Fifo::<64>;
alias interface DataBus64 = DataBus::<64>;

為什麼 alias 是必要的#

在 SV 裡對應的機制是 typedef:

typedef Fifo #(64) fifo64_t;

但 SV 的 typedef 有幾個問題:

  1. SV 的 typedef 不能用於介面參數化別名的所有情境,而且在 typedef 介面時,modport 型別的別名語法非常彆扭。
  2. SV 的 typedef 與 import 的互動有已知的坑(前面提過的類別命名空間問題)。
  3. typedef 別名在跨越編譯單元時,語意取決於編譯順序。

Veryl 的 alias 是一個宣告式、單一功能、方向明確**的機制:它只做一件事——「給一個型別(或 package)取一個本地別名」。

別名與 import 的組合#

alias package PkgA123 = PkgA::<1, 2, 3>;
import PkgA123::{X, Y};

const V : u32 = X;

這是常用組合:先取一個短別名,再從別名匯入特定名稱。


可視性控制 pub#

預設:專案內私有#

Veryl 的規則:

所有頂層項目(module、interface、package、function)預設都是 private(僅專案內可見)。只有標記 pub 的項目才對外可見。

pub module PublicModule {
}

module InternalModule {
}
pub interface PublicInterface {
}

interface InternalInterface {
}
pub package PublicPackage {
    pub const A : u32 = 1;

    const B : u32 = 2;    // 私有:只有 PublicPackage 內部與其使用者可見
}
pub function public_util(
    i_x : u32,
) -> u32 {
    return i_x;
}

與 SV 的對比#

SV 沒有語言層級的「可見性」概念。SV 的模組與介面一旦編譯,就是全域可見的(取決於編譯清單)。

這是 SV 在大型專案裡的一個實際問題:命名衝突。 兩個不同的 IP 都定義了 Fifo 模組,編譯在一起就會衝突。業界的常見解法是:

  1. 命名前綴約定(例如 ip_a_fifo、ip_b_fifo)——靠紀律。
  2. 編譯單元(compile unit)隔離——SV-2012 引入的 library 與 package 隔離,但工具支援不一致。
  3. 編譯順序與隱式繼承——靠工具。

Veryl 用語言層級的 pub 加上專案前綴(下一節)把這件事變成工具保證。

pub 的四個好處#

(一)公開 API 面是明確的、可查詢的。

「這個專案對外提供哪些模組?」這個問題,在 Veryl 裡可以用一行命令回答(grep ^pub),而在 SV 裡沒有直接答案——所有東西都是隱式公開的。

(二)文件產生自動對應 API。

veryl doc 只為 pub 項目產生文件。這意味著:

文件 = 公開 API。

這個等式自動成立,不需要任何人去維護「哪些東西該寫進文件」這份清單。專案的文件頁面就是 API 參考手冊。

(三)重構安全。

把一個模組從 pub 改成私有(或者刪除),是安全的——因為外部沒有任何東西依賴它(編譯器會保證)。反過來,把一個私有模組改成 pub 是新增 API。

(四)鼓勵「預設隱私」的設計紀律。

Rust 社群有個著名口號:「如果不會被用到,就讓它保持私有」。這個口號的實際力量來自語言層級的支援——編譯器會警告你未使用的私有項目。Veryl 的 pub 預設值是同一個原則。

pub 與巢狀項目的傳播#

一個重要的細節:pub 不會自動傳播到 package 內的項目。

pub package Pkg {
    pub const A : u32 = 1;
    const B : u32 = 2;
}

Pkg::A 對外可見,Pkg::B 只有在 Pkg 內部可見。

這個語意是正確的:pub package Pkg 表示「這個 package 本身是公開的」,但不代表「裡面所有的東西都是公開的」。這讓 package 可以提供一個精確的 API 面。


原型(proto)與 for 綁定#

proto 宣告#

proto module ProtoA {
    var x : logic;
}

proto 宣告一個原型模組:它定義了介面形狀,但沒有實作。

for 綁定#

module ModuleC for ProtoA {
    var x : logic;

    assign x = 1;
}

ModuleC for ProtoA 的意思是:「ModuleC 實作 ProtoA 定義的介面」。

這個機制的用途是「介面實作關係的語法化」。

為什麼需要它#

在 SV 裡,「這個模組實作了那個介面」這個資訊只能存在於:

  • 命名慣例;
  • 文件;
  • 或者用 interface 埠的 modport 檢查(但這只檢查埠方向,不檢查「模組是否遵循了某個協定」)。

Veryl 的 proto + for 把「實作關係」變成語言層級的宣告。這帶來幾個好處:

(一)產生碼可以攜帶這個資訊。 在產生的 SV 中,實作關係可以透過屬性或註解表達,供外部工具(形式驗證、lint、文件工具)使用。

(二)泛型與 mixin 的基礎。 當一個模組要「實作某個泛型介面」時,需要一個方式來宣告「這個模組的形狀由這個原型決定」。for 綁定正是這個機制。

(三)文件工具知道繼承關係。 veryl doc 可以生成「ModuleC 實作了 ProtoA」的資訊。

一個簡單例子#

proto module ProtoA {
    var a : logic;
    var b : logic;
}

module ModuleB for ProtoA {
    var a : logic;
    var b : logic;

    always_comb {
        a = b;
    }
}

ModuleB 的埠形狀由 ProtoA 決定。當 ModuleB 被實例化時,它的埠必須符合 ProtoA 的定義。

這個機制在實務上還在發展中,它的成熟度不如 package 與 pub。但在架構上,它是「介面實作關係語法化」這個方向的第一步,而這個方向在大型專案裡有明確的價值。


跨專案命名空間#

專案相依#

Veryl.toml 宣告相依:

[project]
name = "my_project"
version = "0.1.0"

[dependencies]
veryl_sample = { git = "https://github.com/veryl-lang/veryl_sample", tag = "v0.1.0" }

匯入#

import veryl_sample::sample_pkg;

const PARAM_A : u32 = sample_pkg::PARAM_A;

注意匯入與存取的寫法不同:

  • import veryl_sample::sample_pkg; — 匯入的是套件名下的 package sample_pkg;
  • sample_pkg::PARAM_A — 存取的是該 package 的成員。

這與 Rust 的 use 語法一致,語意也清晰:

import 引入的是一個名稱(這裡是 sample_pkg),引入之後就可以直接寫 sample_pkg::PARAM_A——注意 sample_pkg 這個名字是本地的,不帶套件前綴。

對照 SV:

import veryl_sample::sample_pkg::*;

localparam int PARAM_A = sample_pkg::PARAM_A;

SV 的 import ... ::* 把所有內容倒進當前作用域;Veryl 的 import 只引入 package 這個名字,內容仍透過 :: 存取。這就是第 3.3 節開頭提到的「SV 的扁平命名空間」問題的解法。

跨專案的可見性#

跨專案可見的項目必須是 pub:

// 在 veryl_sample 專案中
pub package sample_pkg {
    pub const PARAM_A : u32 = 1;
}
// 在 my_project 專案中
import veryl_sample::sample_pkg;
const X : u32 = sample_pkg::PARAM_A;    // OK
const Y : u32 = sample_pkg::PRIVATE;    // 錯誤:PRIVATE 不是 pub

這意味著 pub 是一個跨專案的契約邊界。 這是套件註冊表生態能夠健康運作的技術前提:套件作者用 pub 明確宣告「這是我的 API」,使用者只依賴那些宣告。


專案前綴機制#

生成的 SystemVerilog 對照#

// Veryl 原始碼(my_project 專案)
pub module ModuleA {
    var r_data : logic<8>;

    assign r_data = 8'h00;
}
// 產生的 SystemVerilog
module my_project_ModuleA (
);
    logic [7:0] r_data;

    assign r_data = 8'h00;
endmodule

模組名被加上了專案前綴 my_project_。

為什麼需要這個機制#

考慮一個 SoC 整合情境:五個不同的開源 IP 專案,每個都有一個 Fifo 模組。如果直接放在同一個編譯清單裡:

module Fifo (...);   // 來自專案 A
module Fifo (...);   // 來自專案 B — 衝突!

SV 的解法是命名約定(靠人)或編譯單元隔離(工具相關)。

Veryl 的專案前綴把這件事變成工具保證。 而且因為是機械性的規則(加上前綴),工程師不需要思考「我要不要加前綴」——規則是固定的。

前綴的傳播規則#

前綴會套用到所有產生的頂層名稱:

Veryl 產生
module ModuleA module my_project_ModuleA
interface IfaceA interface my_project_IfaceA
package PkgA package my_project_PkgA
全域函式 f(用於模組 ModuleB) ModuleB_f

全域函式的前綴是模組名,不是專案名——這是合理的,因為函式是複製到使用它的模組裡的。

與 pub 的關係#

pub 與專案前綴是兩個不同層級的機制,各有分工:

機制 作用範圍 解決的問題
pub 可見性(誰能參照) API 面管理
專案前綴 命名(生成時的實際名字) 多專案命名衝突

一個 pub module ModuleA 產生 my_project_ModuleA,這個名稱對外部專案是可見的(因為 pub),但它的實際名字帶有前綴,因此不會與其他專案的 ModuleA 衝突。

兩個機制是互補的:pub 決定「能不能用」,前綴決定「用什麼名字」。

對除錯的影響#

專案前綴會出現在:

  • 波形視窗的階層路徑(top.my_project_ModuleA.r_data);
  • 錯誤訊息中;
  • 產生的 SV 檔名。

這是好事,因為它讓工程師在波形中一眼看出「這段邏輯來自哪個專案」。


實作練習:共用的 common_pkg#

題目#

建立一個共用的 common_pkg,包含:

  1. 一組位寬常數;
  2. 一個位元索引函式(找出最高位 set bit 的索引);
  3. 一個位元反轉函式;
  4. 在多個模組中使用它。

解答一:package 定義#

pub package common_pkg {
    // ---- 位寬常數 ----
    pub const DATA_WIDTH : u32 = 64;
    pub const ADDR_WIDTH : u32 = 32;
    pub const BYTE_WIDTH : u32 = 8;

    // 衍生常數
    pub const DATA_BYTES : u32 = DATA_WIDTH / BYTE_WIDTH;
    pub const DATA_LANES : u32 = DATA_WIDTH / BYTE_WIDTH;

    // ---- 位元工具函式 ----

    /// 取得最高位 set bit 的索引(0-based),若為 0 則回傳 0
    pub fn msb_index(
        i_value : u64,
    ) -> u32 {
        var r : u32 = 0;
        var v : u64 = i_value;

        while v > 1 {
            v = v >> 1;
            r = r + 1;
        }

        return r;
    }

    /// 位元反轉
    pub fn reverse_bits(
        i_value : u64,
    ) -> u64 {
        var r : u64 = 0;
        var v : u64 = i_value;

        for i in 0..64 {
            if v[0] {
                r = r | (64'd1 << i);
            }
            v = v >> 1;
        }

        return r;
    }

    /// 計算 2 的冪次(向上取整到下一個 2 的冪)
    pub fn ceil_log2(
        i_value : u32,
    ) -> u32 {
        var r : u32 = 0;
        var v : u32 = i_value - 1;

        while v > 0 {
            v = v >> 1;
            r = r + 1;
        }

        return r;
    }
}

注意幾個設計決定:

(一)所有項目都是 pub。 因為這個 package 是要給多個模組(甚至多個專案)共用的。

(二)衍生常數由基本常數算出。 DATA_BYTES 定義為 DATA_WIDTH / BYTE_WIDTH,而不是寫死 8。這樣當 DATA_WIDTH 改變時,衍生值自動一致。這是 package 存在的核心價值之一:讓「相關的常數」保持關聯。

(三)函式參數與回傳型別都是明確的。 msb_index 接受 u64 回傳 u32——這裡有個型別不一致(輸入 64-bit,輸出 32-bit),但它是刻意的且合理的。

解答二:在多個模組中使用#

模組 A:

module Datapath #(
    parameter WIDTH : u32 = common_pkg::DATA_WIDTH,
) (
    i_clk   : clock,
    i_rst_n : reset,
    i_valid : input  logic,
    i_data  : input  logic<WIDTH>,
    o_data  : output logic<WIDTH>,
) {
    import common_pkg::{msb_index, reverse_bits};

    var r_mask : logic<WIDTH>

    always_ff {
        if !i_rst_n {
            r_mask = 0;
        } else if i_valid {
            r_mask = reverse_bits(i_data);
        }
    }

    assign o_data = r_mask;
}

模組 B:

module PriorityArbiter #(
    parameter N : u32 = 4,
) (
    i_clk   : clock,
    i_rst_n : reset,
    i_req   : input  logic<N>,
    o_grant : output logic<N>,
) {
    import common_pkg::msb_index;

    var r_grant : logic<N>

    always_comb {
        r_grant = 0;
        for i in 0..N {
            if i_req[i] {
                r_grant = {N{1'b0}};
                r_grant[i] = 1'b1;
            }
        }
    }

    assign o_grant = r_grant;
}

模組 C(在專案內使用,並示範 alias):

alias package common = common_pkg;

module Control #(
    parameter WIDTH : u32 = common::DATA_WIDTH,
) (
    i_clk   : clock,
    i_rst_n : reset,
    o_index : output logic<8>,
) {
    var r_value : u64

    always_ff {
        if !i_rst_n {
            r_value = 0;
        } else {
            r_value = r_value + 1;
        }
    }

    always_comb {
        o_index = common::msb_index(r_value);
    }
}

解答三:專案設定與產生的 SV#

Veryl.toml:

[project]
name = "my_design"
version = "0.1.0"

產生的 SystemVerilog(common_pkg):

package my_design_common_pkg;
    localparam int DATA_WIDTH = 64;
    localparam int ADDR_WIDTH = 32;
    localparam int BYTE_WIDTH = 8;
    localparam int DATA_BYTES = DATA_WIDTH / BYTE_WIDTH;
    localparam int DATA_LANES = DATA_WIDTH / BYTE_WIDTH;

    function automatic int my_design_common_pkg_msb_index(input logic [63:0] i_value);
        int r;
        logic [63:0] v;
        r = 0;
        v = i_value;
        while (v > 1) begin
            v = v >> 1;
            r = r + 1;
        end
        return r;
    endfunction
    ...
endpackage

產生的 SystemVerilog(Datapath):

module my_design_Datapath #(
    parameter WIDTH = 64
) (
    input  logic         clk,
    input  logic         rst_n,
    input  logic         valid,
    input  logic [WIDTH-1:0] data,
    output logic [WIDTH-1:0] o_data
);
    logic [WIDTH-1:0] mask;

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            mask <= '0;
        end
        else if (valid) begin
            mask <= my_design_common_pkg_reverse_bits(data);
        end
    end

    assign o_data = mask;
endmodule

值得注意的對應關係:

概念 Veryl 產生的 SV 說明
package common_pkg my_design_common_pkg 專案前綴
函式 common_pkg::reverse_bits my_design_common_pkg_reverse_bits package 前綴 + 函式名
import import common_pkg::{...} 產生完整限定名 匯入只是本地的短名
pub const pub const DATA_WIDTH localparam int DATA_WIDTH 可見性轉為 SV 的存取修飾
pub package pub package common_pkg package my_design_common_pkg

「import 在 SV 中消失」這件事很值得體會。 Veryl 的 import 是一個編譯期的名稱解析指示,它不需要在輸出中留下痕跡(因為輸出的名稱已經是完全限定的)。SV 的 import 則是語義的一部分(它影響名字的解析),必須保留。

這正是「語義等價」的另一個面向:Veryl 可以丟棄掉只為了解析方便而存在的語法機制,因為它在輸出端已經不需要了。


本章小結#

本章涵蓋了從「單一模組」到「大型專案」所需的組織機制:

  1. package:共用常數、型別、函式。存取有四種形式(限定名、import ::名稱、import ::{多個}、import ::*),列舉成員也可 import。
  2. 全域函式:定義在專案層級,呼叫時被發射進呼叫端並加上前綴。解決了 SV 無法定義頂層函式的問題。泛型與 pub 都可用。
  3. 泛型 package:PackageA::<T>,讓常數集依參數變化。
  4. alias:為泛型實例化取短名,alias package / alias module / alias interface。
  5. pub 可視性:預設私有,pub 明確宣告 API 面。veryl doc 只產生 pub 項目,因此「文件 = API」自動成立。
  6. proto 與 for:把「實作關係」語法化,是泛型與 mixin 的基礎設施。
  7. 跨專案命名空間:import 套件名::package名; 匯入,再以 package名::成員 存取。
  8. 專案前綴:my_project_ModuleA,機械性地避免多專案命名衝突,與 pub 分工——pub 決定「能不能用」,前綴決定「叫什麼名字」。

這三章的內容構成了 Veryl 語言的「骨架層」。接下來第 4 章要處理的是語言中最具特色、也最需要重新思考的部分:時序邏輯、always_ff / always_comb 的語意,以及 clock / reset 的抽象。在那裡,我們會看到 Veryl 如何把第 1.3 節所批判的「語意不可預測」徹底消滅。


下一章:第 4 章「時序邏輯與時鐘重置抽象」