3.3 控制流到資料流:mux 樹、one-hot 合併、展開與內嵌#
硬體沒有「跳躍」。
if、case、迴圈、函式呼叫——所有程式控制流都必須 變成資料路徑上的選擇器。本節追蹤exec()/exec_chain()/exec_onehot()(elab.rs:1298-1455)如何完成這個翻譯。
3.3.1 if:兩個分支都執行,再用 mux 合併#
Stmt::If 先把每個分支條件歸約成單一位,然後交給 exec_chain:
Stmt::If { arms, els } => {
let mut conds = vec![];
for (c, body) in arms {
let v = self.eval(sc, env, c, 0)?;
conds.push((self.b.reduce_or(&v), body.as_slice()));
}
self.exec_chain(sc, &conds, els, env)?;
}
exec_chain(elab.rs:1379)是整個翻譯的心臟:
fn exec_chain(&mut self, sc: &Scope, conds: &[(Bit, &[Stmt])], els: &[Stmt], env: &mut Env) -> R<()> {
let Some(((c, body), rest)) = conds.split_first() else { return self.exec(sc, els, env) };
match *c {
Bit::C1 => return self.exec(sc, body, env), // 條件常真:直落
Bit::C0 => return self.exec_chain(sc, rest, els, env), // 條件常假:刪除
_ => {}
}
let mut et = env.clone();
self.exec(sc, body, &mut et)?; // then 分支在複本上
let mut ef = env.clone();
self.exec_chain(sc, rest, els, &mut ef)?; // else 鏈在另一個複本上
...
for k in keys {
...
let base = self.cur(sc, env, &k)?;
let t = t.cloned().unwrap_or_else(|| base.clone());
let f = f.cloned().unwrap_or(base.clone());
...
let m = self.b.muxv(*c, &f, &t); // 逐位 mux
env.insert(k, m);
}
}
語義可以用數學式寫成:
三個省閘的細節:
- 常數條件直落:
if_reset之外若條件被參數摺成C1/C0,直接執行 對應分支,不產生任何閘——generate if與參數化設計因此免費。 - 兩分支相同的值直接共用:
if t == f { ... continue; }。 - 分支沒碰的鍵(
env中不存在):以base填入,mux 兩端同值後被Builder::mux摺掉。
3.3.2 case:優先鏈 vs one-hot 樹#
case 有兩種合併策略,取決於分支條件是否互斥(elab.rs:1323-1354):
// distinct constant labels are mutually exclusive: parallel one-hot merge
let mut labels: Vec<u128> = vec![];
let mut exclusive = true;
for (items, body) in arms {
...
match it {
CaseItem::V(e) => {
let v = self.eval(sc, env, e, 0)?;
match const_of(&v) {
Some(k) if !labels.contains(&k) => labels.push(k),
_ => exclusive = false,
}
self.b.eq(&sv, &v)
}
CaseItem::R(lo, hi, incl) => { ... exclusive = false; ... } // 範圍標籤:不可證明互斥
}
...
}
if exclusive { self.exec_onehot(sc, &conds, def, env)?; }
else { self.exec_chain (sc, &conds, def, env)?; }
規則:所有標籤都是互異的編譯期常數 ⇒ 各分支的 hit 天然 one-hot, 可以平行合併;只要有一個非常數標籤或範圍標籤(lo..hi),就退回優先鏈。
優先鏈(exec_chain 複用)#
深度 \(O(n)\) ,對應 Verilog 的 casez/priority 語義,也是 switch(條件 可重疊)唯一的選擇。
one-hot 樹(exec_onehot,elab.rs:1412)#
互斥分支用平衡 OR 樹:
每個位元是「命中項 OR 在一起」;深度 \(O(\log n)\) 的 reduce_or 而不是串接 mux。exec_onehot 還做了兩個重要的合併:
// arms with the same value share one select; a signal most arms leave alone
// (a register held in other states) becomes a 2-way mux: changed ? new : old
let mut groups: Vec<(Vec<Bit>, &Vec<Bit>)> = vec![];
for (h, v) in &arms {
match groups.iter_mut().find(|g| g.1 == *v) { Some(g) => g.0.push(*h), None => groups.push((vec![*h], *v)) }
}
...
let hold = groups.iter().position(|g| *g.1 == base);
let sel: Vec<(Bit, &Vec<Bit>)> = groups.iter().enumerate()
.filter(|(i, _)| Some(*i) != hold)
.map(|(_, g)| (self.b.reduce_or(&g.0), g.1)).collect();
let changed = if hold.is_some() { Some(self.b.reduce_or(&sel.iter().map(|x| x.0).collect::<Vec<_>>())) } else { None };
...
let new = self.b.reduce_or(&terms);
out.push(match changed { Some(c) => self.b.mux(c, at(&base, i), new), None => new });
- 同值分支共用一個選擇訊號:FSM 裡多個 state 寫同一個輸出值時, 它們的
hit被 OR 成一個 enable。 - hold 最佳化:狀態機最常見的形態是「大多數 state 不動這個暫存器」。 把「不動」的分支挑出來後,整個 mux 樹塌縮成兩輸入:
這個型態後續還會被技術映射器認成 flip-flop 的 clock-enable(§3.4)。
flowchart TD
subgraph chain["exec_chain(優先)"]
c0[h0] --> m0{mux}
c1[h1] --> m1{mux}
def[default] --> m1 --> m0 --> out
end
subgraph onehot["exec_onehot(互斥)"]
h0a[h0·v0] --> r[balanced OR tree]
h1a[h1·v1] --> r
nd[~any·vdef] --> r --> out2
end3.3.3 for 迴圈:必須展開#
lower_stmt 的 ForStatement 分支(elab.rs:660):
Statement::ForStatement(x) => {
// loops are unrolled: the bounds must be constant
let from = self.const_of(sc, &toks(|k| k.expression(&f.range.expression)))?.v.sat();
let o = f.range.range_opt.as_ref().ok_or("for range needs `a..b`")?;
let to = self.const_of(sc, &toks(|k| k.expression(&o.expression)))?.v.sat();
...
for i in idx {
sc.consts.insert(var.clone(), Val::new(32, i));
out.extend(self.lower_block(sc, &f.statement_block)?);
}
}
要點:
- 邊界必須常數折疊成功,否則直接報錯——硬體無法「動態重複未知次數的 組合塊」而不需要狀態機,那是使用者的責任。
- 迴圈變數被塞進
sc.consts,因此迴圈體內它是編譯期常數:a[i]這類索引在展開後全部摺疊成位替換(§3.2.6)。 down(step -1)以idx.reverse()實作;step在語句層不支援 (generate for 支援,走veryl_sim::gen::for_indices)。
generate for 走的是同一套(pass2 的 GenerateForDeclaration): 每個索引 i 複製一個 scope、字首加上 label[i],再遞迴 items()—— 展開成 N 份子電路,訊號名帶著 [i]。
3.3.4 函式:內嵌執行 + return 的資料流化#
函式在 pass1 先 lowering 成 FnDef { params, ret, body }(無遞迴、只有 input 參數)。呼叫時 call()(elab.rs:1457)建立一個小型 env:
let mut env = Env::new();
for ((p, ty), v) in f.params.iter().zip(args) { env.insert(p.clone(), self.fit(&v, ty.w, ty.signed)); }
env.insert("$ret".into(), vec![Bit::C0; ret.w as usize]);
env.insert("$done".into(), vec![Bit::C0]);
...
self.exec(&fsc, &f.body, &mut env)?;
Ok(env.remove("$ret").unwrap())
return 不是跳躍,而是兩個 env 更新(Stmt::Return):
self.write(sc, env, "$ret", &Sel::All, &v)?;
env.insert("$done".into(), vec![Bit::C1]);
$done 之後任何賦值都被蓋掉(write() 尾端,elab.rs:1291):
// after a `return`, later assignments must not take effect
if let Some(d) = env.get("$done").map(|v| v[0]) {
if d != Bit::C0 && name != "$done" { nv = self.b.muxv(d, &nv, &cur); }
}
亦即
——early return 變成一層守門 mux。若函式根本沒有 early return,done 恆為 C0, 這層 mux 在 Builder 裡被摺成 0 閘。
為什麼不做真正的內嵌(函式體複製到呼叫點的 IR)? 因為符號執行本身 就是內嵌:
call()在呼叫者的 Builder 上直接執行函式體,共享 hash-consing 快取。同一函式被呼叫兩次,若引數相同,gate()快取會讓第二次呼叫 幾乎零成本。
3.3.5 一個完整例子#
always_comb {
case state {
IDLE: out = 0;
RUN: out = cnt;
default: out = 8'hFF;
}
}
state 是 enum(寬度 1 位,兩個成員)→ 三個 eq(sv, label) 比較, hit 皆為常數標籤 ⇒ 互斥 ⇒ exec_onehot ⇒
out = 0 分支與 default 的 FF 值不同、cnt 分支又不同 ⇒ 三組不會被 group 合併;但若 IDLE 與 default 都寫同一個值,就只剩兩組——閘數 直接減半。技術映射後(iCE40 LUT4),整個 case 往往只需 2 顆 LUT。
為什麼這樣設計#
- 優先鏈與 one-hot 樹並存:優先鏈語義保守(任何 case 都對),one-hot 樹只在可證明互斥時使用——正確性不用賭。
- hold / 同值合併發生在語義層而非閘層,因為符號執行手上有
base(「這個訊號原本的值」)這個資訊,AIG 管線要到最佳化才可能重新發現。 - 迴圈與函式都不需要 IR 層的機制:展開與內嵌是「執行 N 次」的自然結果。
本章重點#
exec_chain= if/else-if/else → 逐位 mux,條件常數時直落。exec_onehot= 互斥 case → 平衡 OR 樹;同值分支共用 enable、hold 分支 把 mux 樹塌縮成changed ? new : old。- case 互斥性由「標籤皆為互異常數」這個可判定條件決定,範圍標籤一律走 優先鏈。
- for/generate-for 以常數邊界展開,迴圈變數是編譯期常數。
- 函式內嵌 = 在呼叫者的 Builder 上執行函式體;
return經$ret/$done變成守門 mux,常見情況下摺疊為零。