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 裡只有三種做法:
- 放在
package裡,然後import。SV 的 package 函式需要在import後才能呼叫,且automatic關鍵字、static變數、遞迴等都有細微規則。 - 用
`define巨集。但巨集是文字展開,無法做型別檢查,且在巨集內部引用巨集參數時有著名的語法陷阱。 - 複製貼上到每個模組。這是最常見的,也是最糟糕的——複製的版本會逐漸分歧。
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 控制它是否對外可見(見後面的可視性控制一節)。
實務上的重要建議:全域函式是「最後手段」,不是「預設做法」。理由是:
- 全域函式無法被參數化(不像 package 可以明確宣告「這個函式屬於這個套件」)。
- 全域命名空間是扁平的——名字必須在整個專案中唯一。
- 測試與文件較難組織。
建議的原則:如果一個函式只服務一個 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 有幾個問題:
- SV 的
typedef不能用於介面參數化別名的所有情境,而且在typedef介面時,modport型別的別名語法非常彆扭。 - SV 的
typedef與import的互動有已知的坑(前面提過的類別命名空間問題)。 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 模組,編譯在一起就會衝突。業界的常見解法是:
- 命名前綴約定(例如
ip_a_fifo、ip_b_fifo)——靠紀律。 - 編譯單元(compile unit)隔離——SV-2012 引入的
library與package隔離,但工具支援不一致。 - 編譯順序與隱式繼承——靠工具。
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;— 匯入的是套件名下的 packagesample_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,包含:
- 一組位寬常數;
- 一個位元索引函式(找出最高位 set bit 的索引);
- 一個位元反轉函式;
- 在多個模組中使用它。
解答一: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 可以丟棄掉只為了解析方便而存在的語法機制,因為它在輸出端已經不需要了。
本章小結#
本章涵蓋了從「單一模組」到「大型專案」所需的組織機制:
package:共用常數、型別、函式。存取有四種形式(限定名、import ::名稱、import ::{多個}、import ::*),列舉成員也可 import。- 全域函式:定義在專案層級,呼叫時被發射進呼叫端並加上前綴。解決了 SV 無法定義頂層函式的問題。泛型與
pub都可用。 - 泛型 package:
PackageA::<T>,讓常數集依參數變化。 alias:為泛型實例化取短名,alias package/alias module/alias interface。pub可視性:預設私有,pub明確宣告 API 面。veryl doc只產生pub項目,因此「文件 = API」自動成立。proto與for:把「實作關係」語法化,是泛型與 mixin 的基礎設施。- 跨專案命名空間:
import 套件名::package名;匯入,再以package名::成員存取。 - 專案前綴:
my_project_ModuleA,機械性地避免多專案命名衝突,與pub分工——pub決定「能不能用」,前綴決定「叫什麼名字」。
這三章的內容構成了 Veryl 語言的「骨架層」。接下來第 4 章要處理的是語言中最具特色、也最需要重新思考的部分:時序邏輯、always_ff / always_comb 的語意,以及 clock / reset 的抽象。在那裡,我們會看到 Veryl 如何把第 1.3 節所批判的「語意不可預測」徹底消滅。
下一章:第 4 章「時序邏輯與時鐘重置抽象」