logicaffeine_compile/codegen_sva/fanout.rs
1//! fanout — the pit-of-success primitive for antenna-safe wide selects.
2//!
3//! A wide mux/select driven by ONE bit is a met1-antenna hazard on an ASIC: that single select
4//! net must fan out across the FULL datapath (every mux bit it gates), and a long, high-fan-out
5//! wire is exactly the metal-1 area-ratio violation antenna checks flag. The GPU's
6//! `SequentialFma` datapath (`gpu_simt_rtl`) solved this for its per-cycle conditional-add partial
7//! product `sel ? data : 0` by SLICING it: emit `slice` `(* keep *)` copies of the select bit,
8//! each ANDing its OWN `width / slice`-bit group of the data wire, so NO net's fan-out exceeds
9//! `width / slice`. Every copy equals the same one bit, so the concatenation is bit-for-bit the
10//! original select — the value is unchanged; only the physical fan-out is bounded.
11//!
12//! That is a GENERAL principle, not a GPU quirk: **any wide mux/select driven by a single bit
13//! should route it through [`bounded_fanout_select`]** so its fan-out is structurally bounded by
14//! `width / slice` and a met1 antenna is impossible by construction, no diode insertion needed.
15//! The `slice` knob trades wires for fan-out: smaller `slice` = each copy gates FEWER bits = lower
16//! fan-out = less antenna, at the cost of MORE `(* keep *)` copies; larger `slice` = the reverse.
17
18/// Emit a fan-out-bounded image of the one-bit select `sel_expr ? data_wire : 0` over `width`
19/// bits, sliced into `slice` groups.
20///
21/// Returns `(decls, pp_expr)`:
22/// * `decls` — the `slice` `(* keep *)` select-bit copies, one per line, each named
23/// `{prefix}{s}{suffix}` and driven by `sel_expr`. The `(* keep *)` attribute stops synthesis
24/// (yosys) from merging the copies back into one high-fan-out net; iverilog ignores it, so
25/// simulation is unchanged.
26/// * `pp_expr` — the concatenation `{ {G{prefix_{slice-1}_suffix}} & data[hi:lo] , ... ,
27/// {G{prefix_0_suffix}} & data[hi:lo] }` where `G = width / slice`. Groups are concatenated
28/// high→low so group 0 lands at the LSB. No net in this expression drives more than `G` mux
29/// bits — the fan-out is bounded by `width / slice`, never the full datapath (the met1-antenna
30/// root cause).
31///
32/// `prefix`/`suffix` frame the select-copy family so copy `s` is named `{prefix}{s}{suffix}` (the
33/// `suffix` carries any per-instance tag, e.g. a lane suffix `_l3` — pass `""` when there is none).
34/// `data_wire` is the wire being gated (e.g. `"mc"`). Panics unless `slice > 0 && width % slice ==
35/// 0`, so the byte-groups tile `data_wire` exactly with no dropped or misaligned bit.
36pub fn bounded_fanout_select(
37 prefix: &str,
38 suffix: &str,
39 sel_expr: &str,
40 data_wire: &str,
41 width: usize,
42 slice: usize,
43) -> (String, String) {
44 assert!(slice > 0, "bounded_fanout_select slice must be positive");
45 assert_eq!(
46 width % slice,
47 0,
48 "bounded_fanout_select slice ({slice}) must divide width ({width}) evenly"
49 );
50 let group = width / slice;
51 let mut decls = String::new();
52 for s in 0..slice {
53 decls.push_str(&format!(
54 " (* keep *) wire {prefix}{s}{suffix} = {sel_expr};\n"
55 ));
56 }
57 let terms: Vec<String> = (0..slice)
58 .rev()
59 .map(|s| {
60 let hi = (s + 1) * group - 1;
61 let lo = s * group;
62 format!("{{{group}{{{prefix}{s}{suffix}}}}} & {data_wire}[{hi}:{lo}]")
63 })
64 .collect();
65 let pp_expr = format!(
66 "{{ {} }}",
67 terms.join(",\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20 ")
68 );
69 (decls, pp_expr)
70}
71
72#[cfg(test)]
73#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/fanout/tests.rs"]
74mod tests;