Skip to main content

logicaffeine_compile/codegen_sva/
netlist.rs

1//! Verilog to and-inverter graphs, through yosys: the bridge that puts any RTL the compiler prints in front of the
2//! certified equivalence engine ([`logicaffeine_proof::sweep`], [`logicaffeine_proof::seq_equiv`]).
3//!
4//! [`synthesize`] elaborates one top module with its parameters set ([`script`]), flattens it, maps memories to
5//! flip-flops, and writes two views: the word-level netlist as yosys JSON, read here for its clock domains, and the
6//! bit-level circuit as ASCII AIGER, read by [`Aig::parse`]. Nothing in the script rewrites logic: no `opt_expr`, no
7//! `abc`; every multiplier is lowered by one fixed map (`CANONICAL_MUL`), so a product is the same circuit in every
8//! design that computes it; cells computing one thing from the same signals are merged first (`opt_merge`), once
9//! every undefined bit is a cell of its own, so a design computing a product many times lowers it once. An undefined
10//! bit (`'x`, an unassigned branch) becomes a free input (`setundef -anyseq`), so a proof holds
11//! for every value it could take and never for a value an optimizer chose; [`synthesize_reading`] with
12//! [`Undefined::Zero`] reads every one as 0 instead (`setundef -zero`), the source as a circuit that fixed them does; asynchronous resets become synchronous
13//! (`async2sync`), the cycle-level reading every flip-flop then has on the one global clock the AIGER latches tick
14//! on. That reading is exact only for a design with a single clock and no level-sensitive latches, so a design with
15//! several clocks or any latch is refused by name ([`NetlistError::Clocks`], [`NetlistError::LevelLatches`]) rather
16//! than modeled wrongly; check such a design one clock domain at a time.
17//!
18//! yosys is `LOGOS_YOSYS` when set, else `yosys` on `PATH`. Each run works in its own directory under the system
19//! temporary directory and removes it.
20
21use std::borrow::Cow;
22use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
23use std::path::{Path, PathBuf};
24use std::process::Command;
25use std::sync::atomic::{AtomicU64, Ordering};
26
27use logicaffeine_proof::aig::{Aig, AigerError};
28
29/// A synthesized module: its bit-level circuit, its word-level yosys JSON, and its clock domains.
30#[derive(Clone, Debug)]
31pub struct Netlist {
32    pub aig: Aig,
33    pub json: String,
34    pub domains: Domains,
35}
36
37/// The sequential elements of a flattened module.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct Domains {
40    pub flops: usize,
41    /// Distinct clock nets the flip-flops sample on.
42    pub clocks: usize,
43    /// Level-sensitive latches.
44    pub latches: usize,
45}
46
47/// Why a module did not become an and-inverter graph.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum NetlistError {
50    /// yosys could not be started.
51    Unavailable(String),
52    /// yosys refused the design; the end of its log.
53    Yosys { log: String },
54    Io(String),
55    /// The JSON netlist is not a yosys netlist.
56    Json(String),
57    /// Flip-flops on this many clocks: one global clock cannot model them.
58    Clocks(usize),
59    /// This many level-sensitive latches: a flip-flop cannot model them.
60    LevelLatches(usize),
61    Aiger(AigerError),
62    /// The RTLIL netlist is not one yosys writes.
63    Rtlil(String),
64}
65
66impl std::fmt::Display for NetlistError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            NetlistError::Unavailable(why) => write!(f, "yosys could not run: {why}"),
70            NetlistError::Yosys { log } => write!(f, "yosys refused the design:\n{log}"),
71            NetlistError::Io(why) => write!(f, "{why}"),
72            NetlistError::Json(why) => write!(f, "the yosys JSON netlist: {why}"),
73            NetlistError::Clocks(n) => write!(f, "flip-flops on {n} clocks; check one clock domain at a time"),
74            NetlistError::LevelLatches(n) => write!(f, "{n} level-sensitive latches"),
75            NetlistError::Aiger(e) => write!(f, "{e}"),
76            NetlistError::Rtlil(why) => write!(f, "the yosys RTLIL netlist: {why}"),
77        }
78    }
79}
80
81impl std::error::Error for NetlistError {}
82
83/// The map every `$mul` is lowered by before the rest of the design: operands extended to the product's width by their
84/// own signedness, then summed row by row in a fixed ripple order. yosys's own lowering (`alumacc`, `maccmap`) orders a
85/// product's partial sums by its internal hashing, so one product in two designs becomes two different adder trees
86/// and proving them one is the hard problem of multiplier equivalence; lowered here, a product is the same circuit
87/// wherever its widths and signedness are, and two spellings of it are one structure.
88pub(crate) const CANONICAL_MUL: &str = r#"(* techmap_celltype = "$mul" *)
89module _logos_canonical_mul (A, B, Y);
90  parameter A_SIGNED = 0;
91  parameter B_SIGNED = 0;
92  parameter A_WIDTH = 1;
93  parameter B_WIDTH = 1;
94  parameter Y_WIDTH = 1;
95  input [A_WIDTH-1:0] A;
96  input [B_WIDTH-1:0] B;
97  output [Y_WIDTH-1:0] Y;
98  wire [Y_WIDTH-1:0] a, b;
99  generate
100    if (A_SIGNED) begin : a_signed
101      assign a = $signed(A);
102    end else begin : a_unsigned
103      assign a = A;
104    end
105    if (B_SIGNED) begin : b_signed
106      assign b = $signed(B);
107    end else begin : b_unsigned
108      assign b = B;
109    end
110  endgenerate
111  wire [Y_WIDTH*(Y_WIDTH+1)-1:0] acc;
112  assign acc[Y_WIDTH-1:0] = {Y_WIDTH{1'b0}};
113  genvar i;
114  generate
115    for (i = 0; i < Y_WIDTH; i = i + 1) begin : row
116      assign acc[(i+1)*Y_WIDTH +: Y_WIDTH] = acc[i*Y_WIDTH +: Y_WIDTH] + ({Y_WIDTH{a[i]}} & (b << i));
117    end
118  endgenerate
119  assign Y = acc[Y_WIDTH*Y_WIDTH +: Y_WIDTH];
120endmodule
121"#;
122
123/// What an undefined bit of the source is in the circuit.
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub enum Undefined {
126    /// A free input: a proof holds for every value it could take.
127    Free,
128    /// 0, as a circuit that fixed every undefined bit at 0 computes.
129    Zero,
130}
131
132/// The yosys script that elaborates `top` with `params` and writes `design.json` and `design.aag`, every undefined
133/// bit a free input. The design is read deferred: a module is elaborated only as the hierarchy instantiates it, with
134/// its instance's parameters, never at its defaults on the way in.
135pub fn script(top: &str, params: &[(&str, &str)]) -> String {
136    script_reading(top, params, Undefined::Free)
137}
138
139/// [`script`], each undefined bit read as `undefined` says.
140pub fn script_reading(top: &str, params: &[(&str, &str)], undefined: Undefined) -> String {
141    script_cut(top, params, undefined, &[])
142}
143
144/// [`script_reading`] with every instance of the `units` modules cut out: each module read from `units.v` as its ports
145/// alone (`read_verilog -lib`, its body never elaborated; a module no unit is named is an error), and once the design
146/// is flat and written, each instance turned
147/// into ports of the top (`expose -evert`) — its outputs inputs named `<instance>.<port>[bit]`, its inputs outputs of
148/// the same names.
149pub fn script_cut(top: &str, params: &[(&str, &str)], undefined: Undefined, units: &[&str]) -> String {
150    let mut hierarchy = format!("hierarchy -check -top {top}");
151    for (name, value) in params {
152        hierarchy.push_str(&format!(" -chparam {name} {value}"));
153    }
154    let setundef = match undefined {
155        Undefined::Free => "setundef -anyseq",
156        Undefined::Zero => "setundef -zero",
157    };
158    let mut steps: Vec<String> = vec!["verilog_defaults -add -defer".into(), "read_verilog -sv design.v".into(), "verilog_defaults -clear".into()];
159    if !units.is_empty() {
160        steps.push("read_verilog -sv -lib units.v".into());
161    }
162    for u in units {
163        steps.push(format!("select -assert-any ={u}"));
164    }
165    let cut = (!units.is_empty()).then(|| format!("expose -evert {}", units.iter().map(|u| format!("t:{u}")).collect::<Vec<_>>().join(" ")));
166    steps.extend(
167        [
168        &hierarchy,
169        "proc",
170        "flatten",
171        "opt_clean",
172        "memory_map",
173        "opt_clean",
174        "write_json design.json",
175        ]
176        .map(str::to_string),
177    );
178    if !units.is_empty() {
179        steps.push("write_rtlil design.il".into());
180    }
181    steps.extend(cut);
182    steps.extend(
183        [
184        setundef,
185        "async2sync",
186        "opt_merge",
187        "techmap -map logos_mul.v",
188        "techmap",
189        // A part-select at a moving offset is a shift whose bits past its operand are undefined only once lowered.
190        setundef,
191        "opt_clean",
192        "dffunmap",
193        "aigmap",
194        "opt_clean",
195        "write_aiger -ascii -symbols design.aag",
196        ]
197        .map(str::to_string),
198    );
199    steps.join("\n") + "\n"
200}
201
202const FLOPS: &[&str] = &[
203    "$dff", "$dffe", "$adff", "$adffe", "$sdff", "$sdffe", "$sdffce", "$dffsr", "$dffsre", "$aldff", "$aldffe",
204];
205const LATCHES: &[&str] = &["$dlatch", "$adlatch", "$dlatchsr"];
206
207/// The flip-flops, their distinct clock nets and the level latches of a yosys JSON netlist. A `$ff` samples on the
208/// implicit global clock, counted as one clock of its own.
209pub(crate) fn domains(json: &str) -> Result<Domains, NetlistError> {
210    let v: serde_json::Value = serde_json::from_str(json).map_err(|e| NetlistError::Json(e.to_string()))?;
211    let modules = v
212        .get("modules")
213        .and_then(|m| m.as_object())
214        .ok_or_else(|| NetlistError::Json("no `modules` object".into()))?;
215    let mut clocks = BTreeSet::new();
216    let (mut flops, mut latches) = (0, 0);
217    for module in modules.values() {
218        let Some(cells) = module.get("cells").and_then(|c| c.as_object()) else { continue };
219        for cell in cells.values() {
220            let ty = cell.get("type").and_then(|t| t.as_str()).unwrap_or("");
221            if ty == "$ff" {
222                flops += 1;
223                clocks.insert("$global".to_string());
224            } else if FLOPS.contains(&ty) {
225                flops += 1;
226                if let Some(clk) = cell.get("connections").and_then(|c| c.get("CLK")) {
227                    clocks.insert(clk.to_string());
228                }
229            } else if LATCHES.contains(&ty) {
230                latches += 1;
231            }
232        }
233    }
234    Ok(Domains { flops, clocks: clocks.len(), latches })
235}
236
237/// What drives a net that clocks registers.
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub enum ClockDriver {
240    /// An input port of the top: a clock a network brings in.
241    Port,
242    /// A register's output: a divided clock.
243    Register,
244    /// Logic, or a constant: a gated or selected clock.
245    Logic,
246}
247
248/// A net that clocks registers or memory ports: its name, what drives it, and the register bits it clocks.
249#[derive(Clone, Debug, PartialEq, Eq)]
250pub struct ClockSource {
251    pub net: String,
252    pub driver: ClockDriver,
253    pub registers: usize,
254}
255
256/// Every net that clocks a register or a memory port of `top` with its parameters set, flattened: what drives it and
257/// the register bits it clocks, the nets no port drives first, then by the bits they clock. A clock a register or
258/// logic makes inside a design reaches no global network unless the fitter gives it one, and a persona's region
259/// cannot; it rides general routing, its skew paid for in the delay the router adds for hold, so a design is refused
260/// such a clock before it is fitted.
261pub fn clock_sources(verilog: &str, top: &str, params: &[(&str, &str)]) -> Result<Vec<ClockSource>, NetlistError> {
262    let mut hierarchy = format!("hierarchy -check -top {top}");
263    for (name, value) in params {
264        hierarchy.push_str(&format!(" -chparam {name} {value}"));
265    }
266    let script = ["read_verilog -sv design.v", &hierarchy, "proc", "flatten", "opt_clean", "write_json clocks.json"].join("\n") + "\n";
267    let run = yosys_run(&reachable_modules(verilog, &[top]), &script, &["clocks.json"])?;
268    let json = match (&run.files[0], run.ok) {
269        (Some(bytes), true) => String::from_utf8_lossy(bytes).into_owned(),
270        _ => return Err(run.refused()),
271    };
272    let v: serde_json::Value = serde_json::from_str(&json).map_err(|e| NetlistError::Json(e.to_string()))?;
273    let module = v.get("modules").and_then(|m| m.get(top)).ok_or_else(|| NetlistError::Json(format!("no module {top}")))?;
274    let bits = |x: &serde_json::Value| -> Vec<serde_json::Value> { x.as_array().cloned().unwrap_or_default() };
275    let mut name: BTreeMap<String, (bool, usize, String)> = BTreeMap::new();
276    if let Some(nets) = module.get("netnames").and_then(|n| n.as_object()) {
277        for (n, net) in nets {
278            let hidden = net.get("hide_name").and_then(|h| h.as_u64()) == Some(1) || n.starts_with('$');
279            let all = bits(&net["bits"]);
280            for (i, b) in all.iter().enumerate() {
281                let shown = if all.len() == 1 { n.clone() } else { format!("{n}[{i}]") };
282                let rank = (hidden, shown.len(), shown);
283                let key = b.to_string();
284                if name.get(&key).map_or(true, |have| rank < *have) {
285                    name.insert(key, rank);
286                }
287            }
288        }
289    }
290    let mut ports = BTreeSet::new();
291    if let Some(p) = module.get("ports").and_then(|p| p.as_object()) {
292        for port in p.values().filter(|port| port["direction"] == "input") {
293            ports.extend(bits(&port["bits"]).iter().map(|b| b.to_string()));
294        }
295    }
296    let cells: Vec<&serde_json::Value> = module.get("cells").and_then(|c| c.as_object()).map(|c| c.values().collect()).unwrap_or_default();
297    let mut driver: BTreeMap<String, ClockDriver> = BTreeMap::new();
298    for cell in &cells {
299        let ty = cell["type"].as_str().unwrap_or("");
300        let made = if FLOPS.contains(&ty) || LATCHES.contains(&ty) || ty == "$ff" { ClockDriver::Register } else { ClockDriver::Logic };
301        if let (Some(dirs), Some(conns)) = (cell.get("port_directions").and_then(|d| d.as_object()), cell.get("connections").and_then(|c| c.as_object())) {
302            for (pin, dir) in dirs {
303                if dir == "output" {
304                    for b in conns.get(pin).map(bits).unwrap_or_default() {
305                        driver.insert(b.to_string(), made);
306                    }
307                }
308            }
309        }
310    }
311    let mut clocked: BTreeMap<String, usize> = BTreeMap::new();
312    for cell in &cells {
313        let ty = cell["type"].as_str().unwrap_or("");
314        let pins: &[&str] = if FLOPS.contains(&ty) {
315            &["CLK"]
316        } else if ty.starts_with("$mem") {
317            &["CLK", "RD_CLK", "WR_CLK"]
318        } else {
319            continue;
320        };
321        let width = if FLOPS.contains(&ty) { cell["parameters"].get("WIDTH").map(parameter).unwrap_or(1) as usize } else { 0 };
322        for pin in pins {
323            for b in cell["connections"].get(*pin).map(bits).unwrap_or_default() {
324                if b.is_string() {
325                    continue;
326                }
327                *clocked.entry(b.to_string()).or_insert(0) += width;
328            }
329        }
330    }
331    let mut out: Vec<ClockSource> = clocked
332        .into_iter()
333        .map(|(bit, registers)| {
334            let source = if ports.contains(&bit) { ClockDriver::Port } else { driver.get(&bit).copied().unwrap_or(ClockDriver::Logic) };
335            let net = name.get(&bit).map(|(_, _, n)| n.clone()).unwrap_or_else(|| format!("net{bit}"));
336            ClockSource { net, driver: source, registers }
337        })
338        .collect();
339    out.sort_by(|a, b| (a.driver == ClockDriver::Port).cmp(&(b.driver == ClockDriver::Port)).then(b.registers.cmp(&a.registers)).then_with(|| a.net.cmp(&b.net)));
340    Ok(out)
341}
342
343/// A yosys parameter: a binary string or a number.
344fn parameter(v: &serde_json::Value) -> u64 {
345    match v {
346        serde_json::Value::String(s) => u64::from_str_radix(s.trim_start_matches('0').trim(), 2).unwrap_or(0),
347        serde_json::Value::Number(n) => n.as_u64().unwrap_or(0),
348        _ => 0,
349    }
350}
351
352/// Whether one global clock models the design exactly.
353pub(crate) fn admit(d: Domains) -> Result<(), NetlistError> {
354    if d.latches > 0 {
355        return Err(NetlistError::LevelLatches(d.latches));
356    }
357    if d.clocks > 1 {
358        return Err(NetlistError::Clocks(d.clocks));
359    }
360    Ok(())
361}
362
363fn yosys() -> String {
364    std::env::var("LOGOS_YOSYS").ok().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| "yosys".to_string())
365}
366
367fn scratch() -> Result<PathBuf, NetlistError> {
368    static RUN: AtomicU64 = AtomicU64::new(0);
369    let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos());
370    let dir = std::env::temp_dir().join(format!(
371        "logos-netlist-{}-{nanos}-{}",
372        std::process::id(),
373        RUN.fetch_add(1, Ordering::Relaxed)
374    ));
375    std::fs::create_dir_all(&dir).map_err(|e| NetlistError::Io(format!("{}: {e}", dir.display())))?;
376    Ok(dir)
377}
378
379/// One yosys run: whether it succeeded, the end of its log, and the files it was asked for, each `None` when it
380/// did not write it.
381pub(crate) struct Run {
382    pub ok: bool,
383    pub log: String,
384    pub files: Vec<Option<Vec<u8>>>,
385}
386
387impl Run {
388    /// The refusal a failed run reports.
389    pub(crate) fn refused(&self) -> NetlistError {
390        NetlistError::Yosys { log: self.log.clone() }
391    }
392}
393
394/// Run `script` on `verilog` (read as `design.v`) in a scratch directory of its own, removed after; then read back
395/// each of `files`.
396pub(crate) fn yosys_run(verilog: &str, script: &str, files: &[&str]) -> Result<Run, NetlistError> {
397    yosys_run_with(&[("design.v", verilog)], script, files)
398}
399
400/// [`yosys_run`] with the input files `inputs` (each a name and its text) written beside the script.
401pub(crate) fn yosys_run_with(inputs: &[(&str, &str)], script: &str, files: &[&str]) -> Result<Run, NetlistError> {
402    let dir = scratch()?;
403    let result = yosys_in(&dir, inputs, script, files);
404    let _ = std::fs::remove_dir_all(&dir);
405    result
406}
407
408fn yosys_in(dir: &Path, inputs: &[(&str, &str)], script: &str, files: &[&str]) -> Result<Run, NetlistError> {
409    let io = |what: &str, e: std::io::Error| NetlistError::Io(format!("{what}: {e}"));
410    for (name, text) in inputs {
411        std::fs::write(dir.join(name), text).map_err(|e| io(name, e))?;
412    }
413    std::fs::write(dir.join("design.ys"), script).map_err(|e| io("design.ys", e))?;
414    std::fs::write(dir.join("logos_mul.v"), CANONICAL_MUL).map_err(|e| io("logos_mul.v", e))?;
415    let bin = yosys();
416    let out = Command::new(&bin)
417        .args(["-q", "-s", "design.ys"])
418        .current_dir(dir)
419        .output()
420        .map_err(|e| NetlistError::Unavailable(format!("{bin}: {e}")))?;
421    let mut log = String::from_utf8_lossy(&out.stdout).into_owned();
422    log.push_str(&String::from_utf8_lossy(&out.stderr));
423    let tail = log.char_indices().rev().nth(4000).map_or(0, |(i, _)| i);
424    Ok(Run {
425        ok: out.status.success(),
426        log: log[tail..].to_string(),
427        files: files.iter().map(|f| std::fs::read(dir.join(f)).ok()).collect(),
428    })
429}
430
431/// The modules of a Verilog text, in order: each one's name and its text from its `module` line to its `endmodule`. None
432/// for a text [`pieces`] cannot part.
433pub(crate) fn modules(verilog: &str) -> Option<Vec<(String, &str)>> {
434    Some(pieces(verilog)?.into_iter().filter_map(|(name, text)| Some((name?, text))).collect())
435}
436
437/// A Verilog text in pieces, in order: each module (its name, its text from its `module` line to its `endmodule`) and
438/// each line outside a module (no name). None for a text whose modules this cannot tell apart: a module line with no
439/// name, a module opened inside another or left open, a name defined twice.
440fn pieces(verilog: &str) -> Option<Vec<(Option<String>, &str)>> {
441    fn starts(t: &str) -> Option<&str> {
442        t.strip_prefix("module ").or_else(|| t.strip_prefix("module\t")).map(str::trim_start)
443    }
444    let mut pieces: Vec<(Option<String>, &str)> = Vec::new();
445    let mut open: Option<(String, usize)> = None;
446    let mut at = 0;
447    for line in verilog.split_inclusive('\n') {
448        let start = at;
449        at += line.len();
450        let t = line.trim_start();
451        match open.take() {
452            None => match starts(t) {
453                Some(rest) => {
454                    let name: String = rest.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '$').collect();
455                    if name.is_empty() {
456                        return None;
457                    }
458                    if t.trim_end().ends_with("endmodule") {
459                        pieces.push((Some(name), line));
460                    } else {
461                        open = Some((name, start));
462                    }
463                }
464                None => pieces.push((None, line)),
465            },
466            Some((name, from)) => {
467                if starts(t).is_some() {
468                    return None;
469                }
470                if t.starts_with("endmodule") {
471                    pieces.push((Some(name), &verilog[from..at]));
472                } else {
473                    open = Some((name, from));
474                }
475            }
476        }
477    }
478    let mut seen = HashSet::new();
479    (open.is_none() && pieces.iter().filter_map(|(n, _)| n.as_deref()).all(|n| seen.insert(n))).then_some(pieces)
480}
481
482/// `verilog` parted into the modules `names` names and the rest (every other module and every line outside a module),
483/// each in the text's order: the units a cut reads as their ports alone, apart from the design it elaborates. None for a
484/// text [`pieces`] cannot part.
485pub(crate) fn split_modules(verilog: &str, names: &[&str]) -> Option<(String, String)> {
486    let (mut rest, mut named) = (String::new(), String::new());
487    for (name, text) in pieces(verilog)? {
488        if name.as_deref().is_some_and(|n| names.contains(&n)) {
489            named.push_str(text);
490        } else {
491            rest.push_str(text);
492        }
493    }
494    Some((rest, named))
495}
496
497/// Two instances of a module and the bits between them.
498#[derive(Clone, Debug, PartialEq, Eq)]
499pub struct Link {
500    pub a: String,
501    pub b: String,
502    pub bits: u64,
503}
504
505/// A module's instances as a floorplan's wires: every instance, each pair of them and the bits between them, and each
506/// instance and the bits between it and the module's own ports, in name order, no wire of no bits.
507#[derive(Clone, Debug, PartialEq, Eq)]
508pub struct Graph {
509    pub instances: Vec<String>,
510    pub links: Vec<Link>,
511    pub ports: Vec<(String, u64)>,
512}
513
514/// One end of a wire: an instance, or the module's own ports (its inputs the nets it drives, its outputs those it reads).
515struct End {
516    name: String,
517    ins: BTreeSet<u64>,
518    outs: BTreeSet<u64>,
519    nets: BTreeSet<u64>,
520}
521
522/// The module's own logic as a graph of its nets: each glue cell's inputs and outputs, and the cells reading and driving
523/// each net, indexed by the net's number.
524struct Glue {
525    cells: Vec<(Vec<u64>, Vec<u64>)>,
526    readers: Vec<Vec<usize>>,
527    drivers: Vec<Vec<usize>>,
528}
529
530impl Glue {
531    /// The nets reached from `from` through one glue cell or more: forward, a cell's every output from any of its inputs;
532    /// backward, its every input from any of its outputs.
533    fn reach(&self, from: &BTreeSet<u64>, forward: bool) -> Vec<bool> {
534        let via = if forward { &self.readers } else { &self.drivers };
535        let mut seen = vec![false; self.cells.len()];
536        let mut reached = vec![false; via.len()];
537        let mut stack: Vec<usize> = from.iter().flat_map(|&n| via[n as usize].iter().copied()).collect();
538        while let Some(c) = stack.pop() {
539            if std::mem::replace(&mut seen[c], true) {
540                continue;
541            }
542            let (ins, outs) = &self.cells[c];
543            for &n in if forward { outs } else { ins } {
544                if !std::mem::replace(&mut reached[n as usize], true) {
545                    stack.extend(via[n as usize].iter().copied());
546                }
547            }
548        }
549        reached
550    }
551}
552
553/// The instances of `module` in `verilog` as a floorplan's wires: each pair of its instances and the bits between them,
554/// and each instance and the bits between it and the module's own ports. The modules `through` names are dissolved into
555/// it (`flatten`), so their instances are the module's, named by path (`fast|unit`; a generate block's
556/// `lane_port[0].port`); every other module is read by its ports alone (`read_verilog -lib`), so nothing under an instance
557/// is elaborated. The bits between two ends are the nets one drives and the other reads, and what each drives into the
558/// other through the module's own logic (its registers, multiplexers and the rest, one cell or more): the fewer of the
559/// nets leaving the one and the nets arriving at the other, so a bus through a register stage counts its width and a
560/// select steering a bus one bit. A net two ends only read joins neither to the other: a broadcast is a star from its
561/// driver, not a wire between every two of its readers. A glue cell's clock pins carry no bits: the clock network is no
562/// wire a floorplan shortens.
563pub fn module_graph(verilog: &str, module: &str, through: &[&str]) -> Result<Graph, NetlistError> {
564    let unparted = || NetlistError::Json("the Verilog's modules do not part".into());
565    let defined: HashSet<String> = pieces(verilog).ok_or_else(unparted)?.into_iter().filter_map(|(n, _)| n).collect();
566    let mut own_names = vec![module];
567    own_names.extend_from_slice(through);
568    if let Some(absent) = own_names.iter().find(|n| !defined.contains(**n)) {
569        return Err(NetlistError::Json(format!("no module {absent} in the Verilog")));
570    }
571    let (rest, own) = split_modules(verilog, &own_names).ok_or_else(unparted)?;
572    let script =
573        format!("read_verilog -sv -lib rest.v\nread_verilog -sv own.v\nhierarchy -top {module}\nproc\nflatten\nmemory_collect\nwrite_json graph.json\n");
574    let run = yosys_run_with(&[("rest.v", &rest), ("own.v", &own)], &script, &["graph.json"])?;
575    if !run.ok {
576        return Err(run.refused());
577    }
578    let json = run.files[0].as_ref().ok_or_else(|| NetlistError::Io("yosys wrote no graph.json".into()))?;
579    let v: serde_json::Value = serde_json::from_slice(json).map_err(|e| NetlistError::Json(e.to_string()))?;
580    let top = &v["modules"][module];
581    let cells = top["cells"].as_object().ok_or_else(|| NetlistError::Json(format!("{module} has no cells")))?;
582    let bits = |b: &serde_json::Value| -> Vec<u64> { b.as_array().into_iter().flatten().filter_map(|x| x.as_u64()).collect() };
583    let mut ends: Vec<End> = Vec::new();
584    let mut glue: Vec<(Vec<u64>, Vec<u64>)> = Vec::new();
585    for (id, cell) in cells {
586        let kind = cell["type"].as_str().unwrap_or_default();
587        let (mut ins, mut outs) = (BTreeSet::new(), BTreeSet::new());
588        for (port, connected) in cell["connections"].as_object().into_iter().flatten() {
589            let direction = cell["port_directions"][port.as_str()]
590                .as_str()
591                .ok_or_else(|| NetlistError::Json(format!("cell {id}'s port {port} has no direction")))?;
592            let clock = kind.starts_with('$') && matches!(port.as_str(), "CLK" | "RD_CLK" | "WR_CLK");
593            if matches!(direction, "input" | "inout") && !clock {
594                ins.extend(bits(connected));
595            }
596            if matches!(direction, "output" | "inout") {
597                outs.extend(bits(connected));
598            }
599        }
600        if kind.starts_with('$') {
601            glue.push((ins.into_iter().collect(), outs.into_iter().collect()));
602        } else {
603            let name = match cell["attributes"]["hdlname"].as_str() {
604                Some(path) => path.split(' ').collect::<Vec<_>>().join("|"),
605                None => json_name(id).to_string(),
606            };
607            let nets = ins.union(&outs).copied().collect();
608            ends.push(End { name, ins, outs, nets });
609        }
610    }
611    ends.sort_by(|a, b| a.name.cmp(&b.name));
612    let (mut ins, mut outs) = (BTreeSet::new(), BTreeSet::new());
613    for port in top["ports"].as_object().into_iter().flatten().map(|(_, p)| p) {
614        match port["direction"].as_str() {
615            Some("input") => outs.extend(bits(&port["bits"])),
616            Some("output") => ins.extend(bits(&port["bits"])),
617            _ => {
618                ins.extend(bits(&port["bits"]));
619                outs.extend(bits(&port["bits"]));
620            }
621        }
622    }
623    let nets = ins.union(&outs).copied().collect();
624    let ports = End { name: String::new(), ins, outs, nets };
625    let span = ends
626        .iter()
627        .chain([&ports])
628        .flat_map(|e| e.nets.iter())
629        .chain(glue.iter().flat_map(|(i, o)| i.iter().chain(o)))
630        .max()
631        .map_or(0, |&n| n as usize + 1);
632    let (mut readers, mut drivers) = (vec![Vec::new(); span], vec![Vec::new(); span]);
633    for (c, (i, o)) in glue.iter().enumerate() {
634        i.iter().for_each(|&n| readers[n as usize].push(c));
635        o.iter().for_each(|&n| drivers[n as usize].push(c));
636    }
637    let glue = Glue { cells: glue, readers, drivers };
638    let reach: Vec<(Vec<bool>, Vec<bool>)> =
639        ends.iter().chain([&ports]).map(|e| (glue.reach(&e.outs, true), glue.reach(&e.ins, false))).collect();
640    // From end `a` into end `b` through the module's logic: the fewer of a's nets leaving it that reach b and b's nets
641    // arriving that a reaches, a net the two share counted once, as shared.
642    let flow = |(a, ea): (usize, &End), (b, eb): (usize, &End)| -> u64 {
643        let leaving = ea.outs.iter().filter(|&&n| reach[b].1[n as usize] && !eb.nets.contains(&n)).count();
644        let arriving = eb.ins.iter().filter(|&&n| reach[a].0[n as usize] && !ea.nets.contains(&n)).count();
645        leaving.min(arriving) as u64
646    };
647    let between = |a: usize, ea: &End, b: usize, eb: &End| -> u64 {
648        let direct = ea.nets.intersection(&eb.nets).filter(|n| ea.outs.contains(n) || eb.outs.contains(n)).count();
649        direct as u64 + flow((a, ea), (b, eb)) + flow((b, eb), (a, ea))
650    };
651    let mut links = Vec::new();
652    for (i, a) in ends.iter().enumerate() {
653        for (j, b) in ends.iter().enumerate().skip(i + 1) {
654            let bits = between(i, a, j, b);
655            if bits > 0 {
656                links.push(Link { a: a.name.clone(), b: b.name.clone(), bits });
657            }
658        }
659    }
660    let p = ends.len();
661    let ports = ends.iter().enumerate().map(|(i, e)| (e.name.clone(), between(i, e, p, &ports))).filter(|(_, b)| *b > 0).collect();
662    Ok(Graph { instances: ends.iter().map(|e| e.name.clone()).collect(), links, ports })
663}
664
665/// The modules of `verilog` that `roots` reach, each through the module names its text mentions, and every line outside
666/// a module, in the text's order: what yosys elaborates of a library for a top, handed without the rest (yosys reads,
667/// and so elaborates, every module it is given). A module named only in a comment on the way is kept, which costs time
668/// and never changes the design. A root no module is named for, a module the text leaves open or names twice leaves the
669/// text whole, for yosys to judge.
670pub(crate) fn reachable_modules<'a>(verilog: &'a str, roots: &[&str]) -> Cow<'a, str> {
671    let Some(pieces) = pieces(verilog) else { return Cow::Borrowed(verilog) };
672    let index: HashMap<&str, usize> = pieces.iter().enumerate().filter_map(|(i, (name, _))| name.as_deref().map(|n| (n, i))).collect();
673    let Some(mut stack) = roots.iter().map(|r| index.get(r).copied()).collect::<Option<Vec<usize>>>() else {
674        return Cow::Borrowed(verilog);
675    };
676    let mut kept: HashSet<usize> = HashSet::new();
677    while let Some(i) = stack.pop() {
678        if !kept.insert(i) {
679            continue;
680        }
681        let text = pieces[i].1.as_bytes();
682        let mut k = 0;
683        while k < text.len() {
684            if text[k].is_ascii_alphabetic() || text[k] == b'_' {
685                let from = k;
686                while k < text.len() && (text[k].is_ascii_alphanumeric() || text[k] == b'_' || text[k] == b'$') {
687                    k += 1;
688                }
689                if let Some(&j) = std::str::from_utf8(&text[from..k]).ok().and_then(|w| index.get(w)) {
690                    stack.push(j);
691                }
692            } else {
693                k += 1;
694            }
695        }
696    }
697    if kept.len() == index.len() {
698        return Cow::Borrowed(verilog);
699    }
700    Cow::Owned(pieces.iter().enumerate().filter(|(i, (name, _))| name.is_none() || kept.contains(i)).map(|(_, (_, text))| *text).collect())
701}
702
703/// Synthesize `top` of `verilog` with `params` set into an and-inverter graph, every undefined bit a free input.
704pub fn synthesize(verilog: &str, top: &str, params: &[(&str, &str)]) -> Result<Netlist, NetlistError> {
705    synthesize_reading(verilog, top, params, Undefined::Free)
706}
707
708/// [`synthesize`], each undefined bit read as `undefined` says.
709pub fn synthesize_reading(verilog: &str, top: &str, params: &[(&str, &str)], undefined: Undefined) -> Result<Netlist, NetlistError> {
710    let run = yosys_run(&reachable_modules(verilog, &[top]), &script_reading(top, params, undefined), &["design.json", "design.aag"])?;
711    let json = run.files[0].as_ref().map(|b| String::from_utf8_lossy(b).into_owned());
712    let domains = match &json {
713        Some(j) => {
714            let d = domains(j)?;
715            admit(d)?;
716            Some(d)
717        }
718        None => None,
719    };
720    if !run.ok {
721        return Err(run.refused());
722    }
723    let (Some(json), Some(domains)) = (json, domains) else {
724        return Err(NetlistError::Io("yosys wrote no design.json".into()));
725    };
726    let aag = run.files[1].as_ref().ok_or_else(|| NetlistError::Io("yosys wrote no design.aag".into()))?;
727    let aig = Aig::parse(aag).map_err(NetlistError::Aiger)?;
728    Ok(Netlist { aig, json, domains })
729}
730
731/// An instance of a unit cut out of a design ([`synthesize_cut`]): its flattened name, its module, the parameters it
732/// gives its module (by name, each value in one spelling, `rtlil_value`; every one it leaves is the module's
733/// default), and its input and output ports with their widths, in the order the netlist lists them.
734#[derive(Clone, Debug, PartialEq, Eq)]
735pub struct UnitInstance {
736    pub name: String,
737    pub module: String,
738    pub parameters: Vec<(String, String)>,
739    pub operands: Vec<(String, usize)>,
740    pub results: Vec<(String, usize)>,
741}
742
743impl UnitInstance {
744    /// The function the instance computes: its module, then the parameters it gives, `<module> #(K=32'sd1, W=8'd3)`.
745    /// Two instances of one function elaborate to one circuit; an instance giving a value equal to its module's
746    /// default is another function's name, since the netlists yosys writes keep no default's sign.
747    pub fn function(&self) -> String {
748        if self.parameters.is_empty() {
749            return self.module.clone();
750        }
751        let given: Vec<String> = self.parameters.iter().map(|(k, v)| format!("{k}={v}")).collect();
752        format!("{} #({})", self.module, given.join(", "))
753    }
754}
755
756/// A design with its units cut out: its netlist and the instances, by name.
757#[derive(Clone, Debug)]
758pub struct CutNetlist {
759    pub netlist: Netlist,
760    pub units: Vec<UnitInstance>,
761}
762
763impl CutNetlist {
764    /// The units for [`logicaffeine_proof::units`]: each instance's operand and result bits by the names the cut gave
765    /// them, its operands in port order bit by bit with the ports `ignored` left out (a pipelined unit's clock is no
766    /// operand), its function ([`UnitInstance::function`]), and its latency as `latency` gives it by function. A port's
767    /// bits are every bit the cut exposes for it, past those the instance connects when its module declares the port
768    /// wider at its own parameters' defaults.
769    pub fn units(&self, latency: &dyn Fn(&str) -> usize, ignored: &[&str]) -> Vec<logicaffeine_proof::units::Unit> {
770        let aig = &self.netlist.aig;
771        let bits = |inst: &str, ports: &[(String, usize)], exposed: &dyn Fn(&str) -> bool| -> Vec<String> {
772            let mut ports: Vec<&(String, usize)> = ports.iter().filter(|(p, _)| !ignored.contains(&p.as_str())).collect();
773            ports.sort_by(|a, b| a.0.cmp(&b.0));
774            ports
775                .iter()
776                .flat_map(|(p, width)| {
777                    let base = format!("{inst}.{p}");
778                    if *width == 1 && !exposed(&format!("{base}[0]")) {
779                        return vec![base];
780                    }
781                    let mut n = *width;
782                    while exposed(&format!("{base}[{n}]")) {
783                        n += 1;
784                    }
785                    (0..n).map(|i| format!("{base}[{i}]")).collect()
786                })
787                .collect()
788        };
789        self.units
790            .iter()
791            .map(|u| logicaffeine_proof::units::Unit {
792                name: u.name.clone(),
793                function: u.function(),
794                latency: latency(&u.function()),
795                operands: bits(&u.name, &u.operands, &|n| aig.output_index(n).is_some()),
796                results: bits(&u.name, &u.results, &|n| aig.input_index(n).is_some()),
797            })
798            .collect()
799    }
800}
801
802/// `top` with every instance of the `units` modules cut out ([`script_cut`]), each undefined bit read as `undefined`
803/// says: a composition whose units are proven apart, to be proven over its units as functions it knows nothing of.
804pub fn synthesize_cut(verilog: &str, top: &str, params: &[(&str, &str)], undefined: Undefined, units: &[&str]) -> Result<CutNetlist, NetlistError> {
805    let roots: Vec<&str> = std::iter::once(top).chain(units.iter().copied()).collect();
806    let reached = reachable_modules(verilog, &roots);
807    let (design, unit_text) = split_modules(&reached, units).unwrap_or_else(|| (reached.to_string(), String::new()));
808    let run = yosys_run_with(&[("design.v", &design), ("units.v", &unit_text)], &script_cut(top, params, undefined, units), &["design.json", "design.aag", "design.il"])?;
809    if !run.ok {
810        return Err(run.refused());
811    }
812    let json = run.files[0].as_ref().map(|b| String::from_utf8_lossy(b).into_owned()).ok_or_else(|| NetlistError::Io("yosys wrote no design.json".into()))?;
813    let domains = domains(&json)?;
814    admit(domains)?;
815    let aag = run.files[1].as_ref().ok_or_else(|| NetlistError::Io("yosys wrote no design.aag".into()))?;
816    let aig = Aig::parse(aag).map_err(NetlistError::Aiger)?;
817    let mut instances = unit_instances(&json, units)?;
818    if !instances.is_empty() {
819        let il = run.files[2].as_ref().map(|b| String::from_utf8_lossy(b).into_owned()).ok_or_else(|| NetlistError::Io("yosys wrote no design.il".into()))?;
820        let mut given = given_parameters(&il, units)?;
821        for u in &mut instances {
822            u.parameters = given.remove(&u.name).ok_or_else(|| NetlistError::Rtlil(format!("no cell {} of the top module", u.name)))?;
823        }
824    }
825    Ok(CutNetlist { netlist: Netlist { aig, json, domains }, units: instances })
826}
827
828/// A name as the JSON netlist writes it: a public RTLIL name (`\x`) without its backslash, a private one (`$x`) as is.
829fn json_name(id: &str) -> &str {
830    id.strip_prefix('\\').unwrap_or(id)
831}
832
833/// A parameter's value as an RTLIL netlist writes it, `signed` or `real` as its flags say, in one spelling for one
834/// value: bits as `<width>'d<value>` (`'sd` when signed, a negative signed value as `-<width>'sd<magnitude>`), bits
835/// past 128 or with any not 0 or 1 as `<width>'b<bits>` (`'sb` when signed), a string as written, quoted, a real
836/// `real "<text>"`. A bare integer is a 32-bit constant, as yosys reads one.
837pub(crate) fn rtlil_value(signed: bool, real: bool, text: &str) -> Result<String, NetlistError> {
838    let bad = || NetlistError::Rtlil(format!("no constant: {text}"));
839    if text.starts_with('"') {
840        if text.len() < 2 || !text.ends_with('"') {
841            return Err(bad());
842        }
843        return Ok(if real { format!("real {text}") } else { text.to_string() });
844    }
845    if real {
846        return Err(bad());
847    }
848    let bits: String = match text.split_once('\'') {
849        Some((width, bits)) => {
850            let width: usize = width.parse().map_err(|_| bad())?;
851            if bits.len() != width || !bits.chars().all(|c| "01xzm-".contains(c)) {
852                return Err(bad());
853            }
854            bits.to_string()
855        }
856        None => format!("{:032b}", text.parse::<i32>().map_err(|_| bad())? as u32),
857    };
858    let (width, s) = (bits.len(), if signed { "s" } else { "" });
859    if width == 0 || width > 128 || !bits.chars().all(|c| c == '0' || c == '1') {
860        return Ok(format!("{width}'{s}b{bits}"));
861    }
862    let value = u128::from_str_radix(&bits, 2).map_err(|_| bad())?;
863    if signed && bits.starts_with('1') {
864        let mask = if width == 128 { u128::MAX } else { (1u128 << width) - 1 };
865        return Ok(format!("-{width}'sd{}", (!value).wrapping_add(1) & mask));
866    }
867    Ok(format!("{width}'{s}d{value}"))
868}
869
870/// The parameters each instance of a `units` module in the top of an RTLIL netlist gives, by instance (named as the
871/// JSON netlist names it), each list in name order and each value in one spelling ([`rtlil_value`]). The RTLIL keeps
872/// a given value's sign, which the JSON netlist drops.
873pub(crate) fn given_parameters(il: &str, units: &[&str]) -> Result<HashMap<String, Vec<(String, String)>>, NetlistError> {
874    let mut found: HashMap<String, Vec<(String, String)>> = HashMap::new();
875    let (mut marked, mut in_top, mut seen_top) = (false, false, false);
876    let mut cell: Option<(String, bool, Vec<(String, String)>)> = None;
877    for line in il.lines() {
878        let t = line.trim_start();
879        if !in_top {
880            if t.starts_with("attribute \\top ") {
881                marked = true;
882            } else if t.starts_with("module ") {
883                in_top = marked;
884                seen_top |= marked;
885                marked = false;
886            } else if !t.starts_with("attribute ") {
887                marked = false;
888            }
889            continue;
890        }
891        if line == "end" {
892            if cell.is_some() {
893                return Err(NetlistError::Rtlil("the top module ends inside a cell".into()));
894            }
895            in_top = false;
896            continue;
897        }
898        if let Some(rest) = t.strip_prefix("cell ") {
899            if cell.is_some() {
900                return Err(NetlistError::Rtlil(format!("a cell inside a cell: {t}")));
901            }
902            let mut words = rest.split(' ');
903            let (Some(kind), Some(name), None) = (words.next(), words.next(), words.next()) else {
904                return Err(NetlistError::Rtlil(format!("a cell line: {t}")));
905            };
906            cell = Some((json_name(name).to_string(), units.contains(&json_name(kind)), Vec::new()));
907            continue;
908        }
909        let Some((name, unit, params)) = cell.as_mut() else { continue };
910        if t == "end" {
911            if *unit {
912                params.sort();
913                found.insert(std::mem::take(name), std::mem::take(params));
914            }
915            cell = None;
916        } else if let (true, Some(rest)) = (*unit, t.strip_prefix("parameter ")) {
917            let (mut signed, mut real, mut rest) = (false, false, rest);
918            loop {
919                if let Some(r) = rest.strip_prefix("signed ") {
920                    (signed, rest) = (true, r);
921                } else if let Some(r) = rest.strip_prefix("real ") {
922                    (real, rest) = (true, r);
923                } else {
924                    break;
925                }
926            }
927            let Some((id, value)) = rest.split_once(' ') else {
928                return Err(NetlistError::Rtlil(format!("a parameter with no value: {t}")));
929            };
930            params.push((json_name(id).to_string(), rtlil_value(signed, real, value)?));
931        }
932    }
933    if !seen_top {
934        return Err(NetlistError::Rtlil("no top module".into()));
935    }
936    Ok(found)
937}
938
939/// The instances of the `units` modules in a flattened yosys JSON netlist, by name.
940fn unit_instances(json: &str, units: &[&str]) -> Result<Vec<UnitInstance>, NetlistError> {
941    let v: serde_json::Value = serde_json::from_str(json).map_err(|e| NetlistError::Json(e.to_string()))?;
942    let modules = v.get("modules").and_then(|m| m.as_object()).ok_or_else(|| NetlistError::Json("no `modules` object".into()))?;
943    let top = modules
944        .values()
945        .find(|m| m.pointer("/attributes/top").is_some())
946        .ok_or_else(|| NetlistError::Json("no top module".into()))?;
947    let mut found = Vec::new();
948    if let Some(cells) = top.get("cells").and_then(|c| c.as_object()) {
949        for (name, cell) in cells {
950            let Some(kind) = cell.get("type").and_then(|t| t.as_str()) else { continue };
951            if !units.contains(&kind) {
952                continue;
953            }
954            let (mut operands, mut results) = (Vec::new(), Vec::new());
955            let directions = cell.get("port_directions").and_then(|d| d.as_object());
956            for (port, bits) in cell.get("connections").and_then(|c| c.as_object()).into_iter().flatten() {
957                let width = bits.as_array().map_or(0, Vec::len);
958                match directions.and_then(|d| d.get(port)).and_then(|d| d.as_str()) {
959                    Some("output") => results.push((port.clone(), width)),
960                    _ => operands.push((port.clone(), width)),
961                }
962            }
963            found.push(UnitInstance { name: name.clone(), module: kind.to_string(), parameters: Vec::new(), operands, results });
964        }
965    }
966    found.sort_by(|a, b| a.name.cmp(&b.name));
967    Ok(found)
968}
969
970#[cfg(test)]
971#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/netlist/tests.rs"]
972mod tests;