From 61df3a53f862d2285442c0ba7a7ae6283008abf8 Mon Sep 17 00:00:00 2001 From: Daniel Penas Varela Date: Mon, 1 Jun 2026 18:27:58 -0400 Subject: [PATCH 1/7] new integration with Matts corrected code no tests yet --- src/bin/eqmap_asic.rs | 6 ++- src/bin/eqmap_fpga.rs | 6 ++- src/lib.rs | 1 + src/netlist.rs | 27 ++++++++++- src/timing.rs | 109 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/timing.rs diff --git a/src/bin/eqmap_asic.rs b/src/bin/eqmap_asic.rs index c1d88f44..79489bdf 100644 --- a/src/bin/eqmap_asic.rs +++ b/src/bin/eqmap_asic.rs @@ -260,7 +260,11 @@ fn main() -> std::io::Result<()> { PartitionMethod::ArcSet => { mapper.insert_partitioned().map_err(std::io::Error::other)?; } - PartitionMethod::DelayPaths => todo!("Implement delay-based partitioning"), + PartitionMethod::DelayPaths => { + mapper + .insert_delay_paths(2) + .map_err(std::io::Error::other)?; + } } let mut mapping = mapper.mappings(); diff --git a/src/bin/eqmap_fpga.rs b/src/bin/eqmap_fpga.rs index 945a8e11..7e1409cf 100644 --- a/src/bin/eqmap_fpga.rs +++ b/src/bin/eqmap_fpga.rs @@ -290,7 +290,11 @@ fn main() -> std::io::Result<()> { PartitionMethod::ArcSet => { mapper.insert_partitioned().map_err(std::io::Error::other)?; } - PartitionMethod::DelayPaths => todo!("Implement delay-based partitioning"), + PartitionMethod::DelayPaths => { + mapper + .insert_delay_paths(2) + .map_err(std::io::Error::other)?; + } } let mut mapping = mapper.mappings(); diff --git a/src/lib.rs b/src/lib.rs index ceb63e2c..84c8e118 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ pub mod pass; pub mod rewrite; #[cfg(feature = "graph_dumps")] pub mod serialize; +pub mod timing; pub mod verilog; #[cfg(test)] diff --git a/src/netlist.rs b/src/netlist.rs index 78c85372..024ff9c1 100644 --- a/src/netlist.rs +++ b/src/netlist.rs @@ -7,10 +7,11 @@ use crate::asic::CellLang; use crate::driver::CircuitLang; use crate::lut::LutLang; +use crate::timing::{expand_n_nodes, get_critical_path}; use bitvec::field::BitField; use egg::{Id, RecExpr, Symbol}; use nl_compiler::FromId; -use safety_net::graph::MultiDiGraph; +use safety_net::graph::{CombDepthInfo, MultiDiGraph}; use safety_net::{ Analysis, DrivenNet, Error, Identifier, Instantiable, Logic, Net, Netlist, Parameter, format_id, iter::NetDFSIterator, @@ -347,6 +348,30 @@ impl<'a, L: CircuitLang, I: Instantiable + LogicFunc> LogicMapper<'a, L, I> { } } +impl<'a, L: CircuitLang> LogicMapper<'a, L, PrimitiveCell> +where + PrimitiveCell: LogicFunc, +{ + /// Map the critical path and a bounded amount of its fan-in cone. + pub fn insert_delay_paths(&mut self, expansion: usize) -> Result, String> { + let analysis = self + ._netlist + .get_analysis::>() + .map_err(|e| e.to_string())?; + let critical_path = + get_critical_path(&analysis).ok_or_else(|| "Critical path is empty".to_string())?; + let endpoint = critical_path.endpoint(); + let expanded_nodes = expand_n_nodes(critical_path, expansion); + let roots = vec![DrivenNet::from(&endpoint)]; + + self.insert_filtered( + roots, + move |d| expanded_nodes.contains(&d.clone().unwrap()), + |_| true, + ) + } +} + /// Create an instantiable cell out of the [CellType] #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrimitiveCell { diff --git a/src/timing.rs b/src/timing.rs new file mode 100644 index 00000000..6c478f6e --- /dev/null +++ b/src/timing.rs @@ -0,0 +1,109 @@ +/*! + + Timing analysis helpers for timing-aware optimization flows. + +*/ + +use crate::netlist::PrimitiveCell; +use safety_net::NetRef; +use safety_net::graph::CombDepthInfo; +use std::collections::HashSet; + +#[derive(Debug)] +/// A representative critical path ending at a timing endpoint. +pub struct DelayPath { + /// The path from endpoint backward through critical fan-in. + path: Vec>, +} + +impl DelayPath { + /// Returns the depth/length of the delay path. + pub fn depth(&self) -> usize { + self.path.len() + } + + /// The signal being driven by this path + pub fn endpoint(&self) -> NetRef { + self.path[0].clone() + } + + /// The nodes along the delay path as a slice + pub fn path(&self) -> &[NetRef] { + &self.path + } +} + +impl IntoIterator for DelayPath { + type Item = NetRef; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.path.into_iter() + } +} + +fn build_path_from_endpoint( + analysis: &CombDepthInfo<'_, PrimitiveCell>, + endpoint: NetRef, +) -> Option { + let mut path = Vec::new(); + let mut current = endpoint; + + while let Some(crit) = analysis.get_crit_input(¤t) { + path.push(current.clone()); + if let Some(c) = crit.get_driver() { + current = c.unwrap(); + } else { + return None; + } + } + + path.push(current); + Some(DelayPath { path }) +} + +/// Gets one of the top critical paths from the combinational-depth analysis. +pub fn get_critical_path(analysis: &CombDepthInfo<'_, PrimitiveCell>) -> Option { + analysis.get_max_depth()?; + let endpoint = analysis.get_critical_points().into_iter().next()?.clone(); + build_path_from_endpoint(analysis, endpoint) +} + +/// Gets up to `n` most critical paths. +pub fn get_critical_paths(analysis: &CombDepthInfo<'_, PrimitiveCell>, n: usize) -> Vec { + if analysis.get_max_depth().is_none() { + return Vec::new(); + } + + let mut vec = Vec::new(); + + for p in analysis.get_critical_points().into_iter().take(n) { + if let Some(path) = build_path_from_endpoint(analysis, p.clone()) { + vec.push(path); + } + } + + vec +} + +/// Expands a critical path backward through fan-in for `n` frontier steps. +pub fn expand_n_nodes(path: DelayPath, n: usize) -> HashSet> { + let mut frontier: Vec> = path.into_iter().collect(); + let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); + + for _ in 0..n { + let mut next_frontier = Vec::new(); + + for node in frontier { + for driver in node.drivers().flatten() { + if expanded_nodes.insert(driver.clone()) { + next_frontier.push(driver); + } + } + } + + frontier = next_frontier; + } + + expanded_nodes +} From 8a040ba775c914a71cacb0b5e40f0f8e8a5fb9fd Mon Sep 17 00:00:00 2001 From: Daniel Penas Varela Date: Tue, 2 Jun 2026 07:21:27 -0400 Subject: [PATCH 2/7] using generic I type and including insert_delay_paths in same impl block --- src/netlist.rs | 18 +++++++----------- src/timing.rs | 44 ++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/netlist.rs b/src/netlist.rs index 024ff9c1..2a9748d5 100644 --- a/src/netlist.rs +++ b/src/netlist.rs @@ -7,7 +7,7 @@ use crate::asic::CellLang; use crate::driver::CircuitLang; use crate::lut::LutLang; -use crate::timing::{expand_n_nodes, get_critical_path}; +use crate::timing::{expand_n_nodes, get_critical_paths}; use bitvec::field::BitField; use egg::{Id, RecExpr, Symbol}; use nl_compiler::FromId; @@ -130,8 +130,7 @@ where }) } } - -impl<'a, L: CircuitLang, I: Instantiable + LogicFunc> LogicMapper<'a, L, I> { +impl<'a, L: CircuitLang, I: Instantiable + LogicFunc + 'static> LogicMapper<'a, L, I> { /// Map `nets` to [CircuitLang] nodes. `nets` that do not pass `filter_netref` *and* `filter_inst` become leaves. fn insert_filtered( &mut self, @@ -346,20 +345,17 @@ impl<'a, L: CircuitLang, I: Instantiable + LogicFunc> LogicMapper<'a, L, I> { pub fn mappings(self) -> Vec> { self.mappings } -} -impl<'a, L: CircuitLang> LogicMapper<'a, L, PrimitiveCell> -where - PrimitiveCell: LogicFunc, -{ - /// Map the critical path and a bounded amount of its fan-in cone. + /// Maps a critical delay path plus a bounded amount of surrounding fan-in logic. pub fn insert_delay_paths(&mut self, expansion: usize) -> Result, String> { let analysis = self ._netlist .get_analysis::>() .map_err(|e| e.to_string())?; - let critical_path = - get_critical_path(&analysis).ok_or_else(|| "Critical path is empty".to_string())?; + let critical_path = get_critical_paths(&analysis, 1) + .into_iter() + .next() + .ok_or_else(|| "Critical path is empty".to_string())?; let endpoint = critical_path.endpoint(); let expanded_nodes = expand_n_nodes(critical_path, expansion); let roots = vec![DrivenNet::from(&endpoint)]; diff --git a/src/timing.rs b/src/timing.rs index 6c478f6e..d37a379e 100644 --- a/src/timing.rs +++ b/src/timing.rs @@ -4,48 +4,48 @@ */ -use crate::netlist::PrimitiveCell; +use safety_net::Instantiable; use safety_net::NetRef; use safety_net::graph::CombDepthInfo; use std::collections::HashSet; #[derive(Debug)] /// A representative critical path ending at a timing endpoint. -pub struct DelayPath { +pub struct DelayPath { /// The path from endpoint backward through critical fan-in. - path: Vec>, + path: Vec>, } -impl DelayPath { +impl DelayPath { /// Returns the depth/length of the delay path. pub fn depth(&self) -> usize { self.path.len() } /// The signal being driven by this path - pub fn endpoint(&self) -> NetRef { + pub fn endpoint(&self) -> NetRef { self.path[0].clone() } /// The nodes along the delay path as a slice - pub fn path(&self) -> &[NetRef] { + pub fn path(&self) -> &[NetRef] { &self.path } } -impl IntoIterator for DelayPath { - type Item = NetRef; - type IntoIter = std::vec::IntoIter>; +impl IntoIterator for DelayPath { + type Item = NetRef; + type IntoIter = std::vec::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.path.into_iter() } } -fn build_path_from_endpoint( - analysis: &CombDepthInfo<'_, PrimitiveCell>, - endpoint: NetRef, -) -> Option { +fn build_path_from_endpoint( + analysis: &CombDepthInfo<'_, I>, + endpoint: NetRef, +) -> Option> { let mut path = Vec::new(); let mut current = endpoint; @@ -62,15 +62,11 @@ fn build_path_from_endpoint( Some(DelayPath { path }) } -/// Gets one of the top critical paths from the combinational-depth analysis. -pub fn get_critical_path(analysis: &CombDepthInfo<'_, PrimitiveCell>) -> Option { - analysis.get_max_depth()?; - let endpoint = analysis.get_critical_points().into_iter().next()?.clone(); - build_path_from_endpoint(analysis, endpoint) -} - /// Gets up to `n` most critical paths. -pub fn get_critical_paths(analysis: &CombDepthInfo<'_, PrimitiveCell>, n: usize) -> Vec { +pub fn get_critical_paths( + analysis: &CombDepthInfo<'_, I>, + n: usize, +) -> Vec> { if analysis.get_max_depth().is_none() { return Vec::new(); } @@ -87,9 +83,9 @@ pub fn get_critical_paths(analysis: &CombDepthInfo<'_, PrimitiveCell>, n: usize) } /// Expands a critical path backward through fan-in for `n` frontier steps. -pub fn expand_n_nodes(path: DelayPath, n: usize) -> HashSet> { - let mut frontier: Vec> = path.into_iter().collect(); - let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); +pub fn expand_n_nodes(path: DelayPath, n: usize) -> HashSet> { + let mut frontier: Vec> = path.into_iter().collect(); + let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); for _ in 0..n { let mut next_frontier = Vec::new(); From 733535871f14545b26b8ead0f40e6437c9fba64d Mon Sep 17 00:00:00 2001 From: Daniel Penas Varela Date: Mon, 8 Jun 2026 14:15:30 -0400 Subject: [PATCH 3/7] new tests updated will only work with new safety net implementation --- tests/timing.rs | 291 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 tests/timing.rs diff --git a/tests/timing.rs b/tests/timing.rs new file mode 100644 index 00000000..1a0799e0 --- /dev/null +++ b/tests/timing.rs @@ -0,0 +1,291 @@ +use std::rc::Rc; + +use eqmap::driver::CircuitLang; +use eqmap::lut::LutLang; +use eqmap::netlist::{LogicMapper, PrimitiveCell}; +use eqmap::timing::{expand_n_nodes, get_critical_paths}; +use safety_net::graph::CombDepthInfo; +use safety_net::{DrivenNet, NetRef, Netlist}; +use safety_pass::CellType; + +fn and_gate() -> PrimitiveCell { + PrimitiveCell::new(CellType::AND, None) +} + +fn reg_cell() -> PrimitiveCell { + PrimitiveCell::new(CellType::FDRE, None) +} + +fn timing_analysis( + netlist: &Rc>, +) -> safety_net::graph::CombDepthInfo<'_, PrimitiveCell> { + netlist.get_analysis::>().unwrap() +} + +// Visual representation +// a ──┐ +// ├── [AND left] ──┐ +// b ──┘ │ +// ├── [AND root] ── y +// c ──┐ │ +// ├── [AND right] ─┘ +// d ──┘ +fn reconvergent_netlist() -> ( + Rc>, + NetRef, + NetRef, + NetRef, +) { + let netlist = Netlist::new("reconvergent".to_string()); + + let a = netlist.insert_input("a".into()); + let b = netlist.insert_input("b".into()); + let c = netlist.insert_input("c".into()); + let d = netlist.insert_input("d".into()); + + let left = netlist + .insert_gate(and_gate(), "left".into(), &[a, b]) + .unwrap(); + let right = netlist + .insert_gate(and_gate(), "right".into(), &[c, d]) + .unwrap(); + let root = netlist + .insert_gate( + and_gate(), + "root".into(), + &[left.get_output(0), right.get_output(0)], + ) + .unwrap(); + root.clone().expose_with_name("y".into()); + + (netlist, root, left, right) +} + +struct TwoOutputNetlist { + netlist: Rc>, + first_root: NetRef, + first_leaf: NetRef, + second_root: NetRef, + second_leaf: NetRef, +} + +fn two_output_netlist() -> TwoOutputNetlist { + let netlist = Netlist::new("two_output".to_string()); + + let a = netlist.insert_input("a".into()); + let b = netlist.insert_input("b".into()); + let c = netlist.insert_input("c".into()); + let d = netlist.insert_input("d".into()); + let e = netlist.insert_input("e".into()); + let f = netlist.insert_input("f".into()); + + let first_leaf = netlist + .insert_gate(and_gate(), "first_leaf".into(), &[a, b]) + .unwrap(); + let first_root = netlist + .insert_gate( + and_gate(), + "first_root".into(), + &[first_leaf.get_output(0), c], + ) + .unwrap(); + first_root.clone().expose_with_name("y0".into()); + + let second_leaf = netlist + .insert_gate(and_gate(), "second_leaf".into(), &[d, e]) + .unwrap(); + let second_root = netlist + .insert_gate( + and_gate(), + "second_root".into(), + &[second_leaf.get_output(0), f], + ) + .unwrap(); + second_root.clone().expose_with_name("y1".into()); + + TwoOutputNetlist { + netlist, + first_root, + first_leaf, + second_root, + second_leaf, + } +} + +fn single_chain_netlist() -> ( + Rc>, + NetRef, + NetRef, + NetRef, +) { + let netlist = Netlist::new("single_chain".to_string()); + + let a = netlist.insert_input("a".into()); + let b = netlist.insert_input("b".into()); + let c = netlist.insert_input("c".into()); + let d = netlist.insert_input("d".into()); + + let first = netlist + .insert_gate(and_gate(), "first".into(), &[a, b]) + .unwrap(); + let second = netlist + .insert_gate(and_gate(), "second".into(), &[first.get_output(0), c]) + .unwrap(); + let third = netlist + .insert_gate(and_gate(), "third".into(), &[second.get_output(0), d]) + .unwrap(); + third.clone().expose_with_name("y".into()); + + (netlist, first, second, third) +} + +#[test] +fn critical_path_uses_one_max_depth_branch() { + let (netlist, root, left, _right) = reconvergent_netlist(); + let analysis = timing_analysis(&netlist); + + let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + + assert_eq!(path.endpoint(), root); + assert_eq!(path.path(), &[root, left]); +} + +#[test] +fn expansion_adds_neighboring_fanin_nodes() { + let (netlist, _root, _left, right) = reconvergent_netlist(); + let analysis = timing_analysis(&netlist); + let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + + let unexpanded = expand_n_nodes(path, 0); + let expanded = expand_n_nodes( + get_critical_paths(&analysis, 1).into_iter().next().unwrap(), + 1, + ); + + assert!(!unexpanded.contains(&right)); + assert!(expanded.contains(&right)); +} + +#[test] +fn gets_multiple_critical_paths() { + let TwoOutputNetlist { + netlist, + first_root, + first_leaf, + second_root, + second_leaf, + } = two_output_netlist(); + let analysis = timing_analysis(&netlist); + + let paths = get_critical_paths(&analysis, 2); + + assert_eq!(paths.len(), 2); + assert!( + paths + .iter() + .any(|path| path.path() == [first_root.clone(), first_leaf.clone()]) + ); + assert!( + paths + .iter() + .any(|path| path.path() == [second_root.clone(), second_leaf.clone()]) + ); +} + +#[test] +fn requesting_zero_critical_paths_returns_empty_result() { + let (netlist, _root, _left, _right) = reconvergent_netlist(); + let analysis = timing_analysis(&netlist); + + let paths = get_critical_paths(&analysis, 0); + + assert!(paths.is_empty()); +} + +#[test] +fn critical_paths_use_timing_endpoints_not_internal_chain_nodes() { + let (netlist, first, second, third) = single_chain_netlist(); + let analysis = timing_analysis(&netlist); + + let paths = get_critical_paths(&analysis, 3); + let debug_paths = paths + .iter() + .map(|path| { + path.path() + .iter() + .map(|net| net.get_identifier().to_string()) + .collect::>() + }) + .collect::>(); + eprintln!("debug critical paths: {debug_paths:?}"); + + assert_eq!(paths.len(), 1); + assert_eq!(paths[0].endpoint(), third); + assert_eq!(paths[0].depth(), 3); + assert_eq!(paths[0].path(), &[third, second, first]); +} + +#[test] +fn critical_path_stops_at_register_boundary() { + let netlist = Netlist::new("registered".to_string()); + + let a = netlist.insert_input("a".into()); + let b = netlist.insert_input("b".into()); + let c = netlist.insert_input("c".into()); + let d = netlist.insert_input("d".into()); + let clk = netlist.insert_input("clk".into()); + let ce = netlist.insert_input("ce".into()); + let rst = netlist.insert_input("rst".into()); + + let before_a = netlist + .insert_gate(and_gate(), "before_a".into(), &[a, b]) + .unwrap(); + let before_b = netlist + .insert_gate(and_gate(), "before_b".into(), &[before_a.get_output(0), c]) + .unwrap(); + + let reg = netlist.insert_gate_disconnected(reg_cell(), "reg".into()); + reg.find_input(&"D".into()) + .unwrap() + .connect(before_b.get_output(0)); + reg.find_input(&"C".into()).unwrap().connect(clk); + reg.find_input(&"CE".into()).unwrap().connect(ce); + reg.find_input(&"R".into()).unwrap().connect(rst); + + let after = netlist + .insert_gate(and_gate(), "after".into(), &[reg.get_output(0), d]) + .unwrap(); + after.expose_with_name("y".into()); + + let analysis = timing_analysis(&netlist); + let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + + assert_eq!(path.path(), &[before_b, before_a]); + assert!(!path.path().contains(®)); +} + +#[test] +fn insert_delay_paths_maps_only_the_critical_region() { + let (netlist, root, left, right) = reconvergent_netlist(); + let mut mapper = netlist + .get_analysis::>() + .unwrap(); + + mapper.insert_delay_paths(0).unwrap(); + + let mappings = mapper.mappings(); + assert_eq!(mappings.len(), 1); + + let mapping = &mappings[0]; + let roots = mapping.root_nets().collect::>(); + let expr = mapping.get_expr(); + let vars = expr + .iter() + .filter_map(|node| node.get_var().map(|sym| sym.to_string())) + .collect::>(); + + assert_eq!(roots, vec![DrivenNet::from(&root)]); + assert!(vars.contains(&right.get_identifier().to_string())); + assert!(!vars.contains(&root.get_identifier().to_string())); + assert!(!vars.contains(&left.get_identifier().to_string())); +} From e24be570f6fb20d340d43426aa9b40506096cae8 Mon Sep 17 00:00:00 2001 From: Daniel Penas Varela Date: Wed, 10 Jun 2026 16:01:12 -0400 Subject: [PATCH 4/7] adding tests for timing and fixes --- src/bin/eqmap_asic.rs | 2 +- src/bin/eqmap_fpga.rs | 2 +- src/netlist.rs | 27 ++++++++++++++++++--------- src/timing.rs | 43 +++++++++++++++++++++---------------------- tests/timing.rs | 14 +++++++------- 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/bin/eqmap_asic.rs b/src/bin/eqmap_asic.rs index 79489bdf..9f252765 100644 --- a/src/bin/eqmap_asic.rs +++ b/src/bin/eqmap_asic.rs @@ -262,7 +262,7 @@ fn main() -> std::io::Result<()> { } PartitionMethod::DelayPaths => { mapper - .insert_delay_paths(2) + .insert_delay_paths(1, 2) .map_err(std::io::Error::other)?; } } diff --git a/src/bin/eqmap_fpga.rs b/src/bin/eqmap_fpga.rs index 7e1409cf..5050fe13 100644 --- a/src/bin/eqmap_fpga.rs +++ b/src/bin/eqmap_fpga.rs @@ -292,7 +292,7 @@ fn main() -> std::io::Result<()> { } PartitionMethod::DelayPaths => { mapper - .insert_delay_paths(2) + .insert_delay_paths(1, 2) .map_err(std::io::Error::other)?; } } diff --git a/src/netlist.rs b/src/netlist.rs index 2a9748d5..3bf84548 100644 --- a/src/netlist.rs +++ b/src/netlist.rs @@ -7,7 +7,7 @@ use crate::asic::CellLang; use crate::driver::CircuitLang; use crate::lut::LutLang; -use crate::timing::{expand_n_nodes, get_critical_paths}; +use crate::timing::get_critical_paths; use bitvec::field::BitField; use egg::{Id, RecExpr, Symbol}; use nl_compiler::FromId; @@ -347,18 +347,27 @@ impl<'a, L: CircuitLang, I: Instantiable + LogicFunc + 'static> LogicMapper<' } /// Maps a critical delay path plus a bounded amount of surrounding fan-in logic. - pub fn insert_delay_paths(&mut self, expansion: usize) -> Result, String> { + pub fn insert_delay_paths( + &mut self, + topk: usize, + branch_factor: usize, + ) -> Result, String> { let analysis = self ._netlist .get_analysis::>() .map_err(|e| e.to_string())?; - let critical_path = get_critical_paths(&analysis, 1) - .into_iter() - .next() - .ok_or_else(|| "Critical path is empty".to_string())?; - let endpoint = critical_path.endpoint(); - let expanded_nodes = expand_n_nodes(critical_path, expansion); - let roots = vec![DrivenNet::from(&endpoint)]; + let critical_paths = get_critical_paths(&analysis, topk); + if critical_paths.is_empty() { + return Err("Critical paths are empty".to_string()); + } + let mut expanded_nodes = HashSet::new(); + let mut roots = Vec::new(); + + for critical_path in &critical_paths { + let endpoint = critical_path.endpoint(); + expanded_nodes.extend(critical_path.expand_n_nodes(branch_factor)); + roots.push(DrivenNet::from(&endpoint)); + } self.insert_filtered( roots, diff --git a/src/timing.rs b/src/timing.rs index d37a379e..293b51d9 100644 --- a/src/timing.rs +++ b/src/timing.rs @@ -31,6 +31,27 @@ impl DelayPath { pub fn path(&self) -> &[NetRef] { &self.path } + /// Expands and collects the transitive fan-in along the critical path provided by a branch factor of n. + pub fn expand_n_nodes(&self, n: usize) -> HashSet> { + let mut frontier: Vec> = self.path().to_vec(); + let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); + + for _ in 0..n { + let mut next_frontier = Vec::new(); + + for node in frontier { + for driver in node.drivers().flatten() { + if expanded_nodes.insert(driver.clone()) { + next_frontier.push(driver); + } + } + } + + frontier = next_frontier; + } + + expanded_nodes + } } impl IntoIterator for DelayPath { @@ -81,25 +102,3 @@ pub fn get_critical_paths( vec } - -/// Expands a critical path backward through fan-in for `n` frontier steps. -pub fn expand_n_nodes(path: DelayPath, n: usize) -> HashSet> { - let mut frontier: Vec> = path.into_iter().collect(); - let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); - - for _ in 0..n { - let mut next_frontier = Vec::new(); - - for node in frontier { - for driver in node.drivers().flatten() { - if expanded_nodes.insert(driver.clone()) { - next_frontier.push(driver); - } - } - } - - frontier = next_frontier; - } - - expanded_nodes -} diff --git a/tests/timing.rs b/tests/timing.rs index 1a0799e0..16ff0ada 100644 --- a/tests/timing.rs +++ b/tests/timing.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use eqmap::driver::CircuitLang; use eqmap::lut::LutLang; use eqmap::netlist::{LogicMapper, PrimitiveCell}; -use eqmap::timing::{expand_n_nodes, get_critical_paths}; +use eqmap::timing::get_critical_paths; use safety_net::graph::CombDepthInfo; use safety_net::{DrivenNet, NetRef, Netlist}; use safety_pass::CellType; @@ -156,11 +156,8 @@ fn expansion_adds_neighboring_fanin_nodes() { let analysis = timing_analysis(&netlist); let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); - let unexpanded = expand_n_nodes(path, 0); - let expanded = expand_n_nodes( - get_critical_paths(&analysis, 1).into_iter().next().unwrap(), - 1, - ); + let unexpanded = path.expand_n_nodes(0); + let expanded = path.expand_n_nodes(1); assert!(!unexpanded.contains(&right)); assert!(expanded.contains(&right)); @@ -217,6 +214,9 @@ fn critical_paths_use_timing_endpoints_not_internal_chain_nodes() { .collect::>() }) .collect::>(); + // if you run cargo test --test timing -- --nocapture --test-threads=1 u can see that it is printing 3 critical paths + // when there should only be one. This is becuase it is identifying as critical ends all the ndoes in the critical path + // this is I believe an issue with CombDepthInfo in safety net in the compute method eprintln!("debug critical paths: {debug_paths:?}"); assert_eq!(paths.len(), 1); @@ -271,7 +271,7 @@ fn insert_delay_paths_maps_only_the_critical_region() { .get_analysis::>() .unwrap(); - mapper.insert_delay_paths(0).unwrap(); + mapper.insert_delay_paths(1, 0).unwrap(); let mappings = mapper.mappings(); assert_eq!(mappings.len(), 1); From a7a100f08b892c54dee5bd75feec18f7fc4887a7 Mon Sep 17 00:00:00 2001 From: Daniel Penas Varela Date: Thu, 11 Jun 2026 15:04:16 -0400 Subject: [PATCH 5/7] new changes to acommodate driven net --- src/netlist.rs | 8 ++------ src/timing.rs | 34 +++++++++++++++++-------------- tests/timing.rs | 53 ++++++++++++++++++++++++++++++------------------- 3 files changed, 54 insertions(+), 41 deletions(-) diff --git a/src/netlist.rs b/src/netlist.rs index 3bf84548..a734d250 100644 --- a/src/netlist.rs +++ b/src/netlist.rs @@ -366,14 +366,10 @@ impl<'a, L: CircuitLang, I: Instantiable + LogicFunc + 'static> LogicMapper<' for critical_path in &critical_paths { let endpoint = critical_path.endpoint(); expanded_nodes.extend(critical_path.expand_n_nodes(branch_factor)); - roots.push(DrivenNet::from(&endpoint)); + roots.push(endpoint); } - self.insert_filtered( - roots, - move |d| expanded_nodes.contains(&d.clone().unwrap()), - |_| true, - ) + self.insert_filtered(roots, move |d| expanded_nodes.contains(d), |_| true) } } diff --git a/src/timing.rs b/src/timing.rs index 293b51d9..e58fcfde 100644 --- a/src/timing.rs +++ b/src/timing.rs @@ -4,8 +4,8 @@ */ +use safety_net::DrivenNet; use safety_net::Instantiable; -use safety_net::NetRef; use safety_net::graph::CombDepthInfo; use std::collections::HashSet; @@ -13,7 +13,7 @@ use std::collections::HashSet; /// A representative critical path ending at a timing endpoint. pub struct DelayPath { /// The path from endpoint backward through critical fan-in. - path: Vec>, + path: Vec>, } impl DelayPath { @@ -23,25 +23,29 @@ impl DelayPath { } /// The signal being driven by this path - pub fn endpoint(&self) -> NetRef { + pub fn endpoint(&self) -> DrivenNet { self.path[0].clone() } /// The nodes along the delay path as a slice - pub fn path(&self) -> &[NetRef] { + pub fn path(&self) -> &[DrivenNet] { &self.path } /// Expands and collects the transitive fan-in along the critical path provided by a branch factor of n. - pub fn expand_n_nodes(&self, n: usize) -> HashSet> { - let mut frontier: Vec> = self.path().to_vec(); - let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); + pub fn expand_n_nodes(&self, n: usize) -> HashSet> { + let mut frontier: Vec> = self.path().to_vec(); + let mut expanded_nodes: HashSet> = frontier.iter().cloned().collect(); for _ in 0..n { let mut next_frontier = Vec::new(); - for node in frontier { - for driver in node.drivers().flatten() { - if expanded_nodes.insert(driver.clone()) { + for net in frontier { + let node = net.unwrap(); + + for input in node.inputs() { + if let Some(driver) = input.get_driver() + && expanded_nodes.insert(driver.clone()) + { next_frontier.push(driver); } } @@ -55,8 +59,8 @@ impl DelayPath { } impl IntoIterator for DelayPath { - type Item = NetRef; - type IntoIter = std::vec::IntoIter>; + type Item = DrivenNet; + type IntoIter = std::vec::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.path.into_iter() @@ -65,15 +69,15 @@ impl IntoIterator for DelayPath { fn build_path_from_endpoint( analysis: &CombDepthInfo<'_, I>, - endpoint: NetRef, + endpoint: DrivenNet, ) -> Option> { let mut path = Vec::new(); let mut current = endpoint; - while let Some(crit) = analysis.get_crit_input(¤t) { + while let Some(crit) = analysis.get_crit_input(¤t.clone().unwrap()) { path.push(current.clone()); if let Some(c) = crit.get_driver() { - current = c.unwrap(); + current = c; } else { return None; } diff --git a/tests/timing.rs b/tests/timing.rs index 16ff0ada..4743c47e 100644 --- a/tests/timing.rs +++ b/tests/timing.rs @@ -5,7 +5,7 @@ use eqmap::lut::LutLang; use eqmap::netlist::{LogicMapper, PrimitiveCell}; use eqmap::timing::get_critical_paths; use safety_net::graph::CombDepthInfo; -use safety_net::{DrivenNet, NetRef, Netlist}; +use safety_net::{DrivenNet, Netlist}; use safety_pass::CellType; fn and_gate() -> PrimitiveCell { @@ -32,9 +32,9 @@ fn timing_analysis( // d ──┘ fn reconvergent_netlist() -> ( Rc>, - NetRef, - NetRef, - NetRef, + DrivenNet, + DrivenNet, + DrivenNet, ) { let netlist = Netlist::new("reconvergent".to_string()); @@ -58,15 +58,20 @@ fn reconvergent_netlist() -> ( .unwrap(); root.clone().expose_with_name("y".into()); - (netlist, root, left, right) + ( + netlist, + root.get_output(0), + left.get_output(0), + right.get_output(0), + ) } struct TwoOutputNetlist { netlist: Rc>, - first_root: NetRef, - first_leaf: NetRef, - second_root: NetRef, - second_leaf: NetRef, + first_root: DrivenNet, + first_leaf: DrivenNet, + second_root: DrivenNet, + second_leaf: DrivenNet, } fn two_output_netlist() -> TwoOutputNetlist { @@ -105,18 +110,18 @@ fn two_output_netlist() -> TwoOutputNetlist { TwoOutputNetlist { netlist, - first_root, - first_leaf, - second_root, - second_leaf, + first_root: first_root.get_output(0), + first_leaf: first_leaf.get_output(0), + second_root: second_root.get_output(0), + second_leaf: second_leaf.get_output(0), } } fn single_chain_netlist() -> ( Rc>, - NetRef, - NetRef, - NetRef, + DrivenNet, + DrivenNet, + DrivenNet, ) { let netlist = Netlist::new("single_chain".to_string()); @@ -136,7 +141,12 @@ fn single_chain_netlist() -> ( .unwrap(); third.clone().expose_with_name("y".into()); - (netlist, first, second, third) + ( + netlist, + first.get_output(0), + second.get_output(0), + third.get_output(0), + ) } #[test] @@ -260,8 +270,11 @@ fn critical_path_stops_at_register_boundary() { let analysis = timing_analysis(&netlist); let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); - assert_eq!(path.path(), &[before_b, before_a]); - assert!(!path.path().contains(®)); + assert_eq!( + path.path(), + &[before_b.get_output(0), before_a.get_output(0)] + ); + assert!(!path.path().contains(®.get_output(0))); } #[test] @@ -284,7 +297,7 @@ fn insert_delay_paths_maps_only_the_critical_region() { .filter_map(|node| node.get_var().map(|sym| sym.to_string())) .collect::>(); - assert_eq!(roots, vec![DrivenNet::from(&root)]); + assert_eq!(roots, vec![root.clone()]); assert!(vars.contains(&right.get_identifier().to_string())); assert!(!vars.contains(&root.get_identifier().to_string())); assert!(!vars.contains(&left.get_identifier().to_string())); From 1c4a4fda0b0df9e8a6fe582d7c28d43d7eaa887e Mon Sep 17 00:00:00 2001 From: Daniel Penas Varela Date: Sun, 14 Jun 2026 16:29:47 -0400 Subject: [PATCH 6/7] new verilog test --- tests/timing.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/timing.rs b/tests/timing.rs index 4743c47e..25c23ed7 100644 --- a/tests/timing.rs +++ b/tests/timing.rs @@ -4,6 +4,8 @@ use eqmap::driver::CircuitLang; use eqmap::lut::LutLang; use eqmap::netlist::{LogicMapper, PrimitiveCell}; use eqmap::timing::get_critical_paths; +use eqmap::verilog::sv_parse_wrapper; +use nl_compiler::from_vast; use safety_net::graph::CombDepthInfo; use safety_net::{DrivenNet, Netlist}; use safety_pass::CellType; @@ -302,3 +304,100 @@ fn insert_delay_paths_maps_only_the_critical_region() { assert!(!vars.contains(&root.get_identifier().to_string())); assert!(!vars.contains(&left.get_identifier().to_string())); } + +// Visual representation +// a ──┐ +// ├─[c1]─┐ +// b ──┘ ├─[c2]─┐ +// c ─────────┘ ├─[c3]─┐ +// d ────────────────┘ ├─[c4]─┐ +// e ───────────────────────┘ │ +// ├─[root]─ y +// f ──┐ │ +// ├─[s1]─┐ │ +// g ──┘ ├─[s2]─┐ │ +// h ─────────┘ ├─[s3]────────┘ +// i ────────────────┘ +#[test] +fn delay_path_expansion_from_verilog_stops_at_requested_depth() { + let verilog = r#" +module timing_branch ( + a, b, c, d, e, f, g, h, i, y +); + input a; + input b; + input c; + input d; + input e; + input f; + input g; + input h; + input i; + output y; + wire a; + wire b; + wire c; + wire d; + wire e; + wire f; + wire g; + wire h; + wire i; + wire y; + wire c1_out; + wire c2_out; + wire c3_out; + wire c4_out; + wire s1_out; + wire s2_out; + wire s3_out; + + AND c1 (.A(a), .B(b), .Y(c1_out)); + AND c2 (.A(c1_out), .B(c), .Y(c2_out)); + AND c3 (.A(c2_out), .B(d), .Y(c3_out)); + AND c4 (.A(c3_out), .B(e), .Y(c4_out)); + + AND s1 (.A(f), .B(g), .Y(s1_out)); + AND s2 (.A(s1_out), .B(h), .Y(s2_out)); + AND s3 (.A(s2_out), .B(i), .Y(s3_out)); + + AND root (.A(c4_out), .B(s3_out), .Y(y)); +endmodule +"#; + + let ast = sv_parse_wrapper(verilog, None).unwrap(); + let netlist = from_vast::(&ast).unwrap(); + let analysis = timing_analysis(&netlist); + let critical_path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + let path_names = critical_path + .path() + .iter() + .map(|net| net.get_identifier().to_string()) + .collect::>(); + + assert_eq!(path_names, ["y", "c4_out", "c3_out", "c2_out", "c1_out"]); + + let mut mapper = netlist + .get_analysis::>() + .unwrap(); + mapper.insert_delay_paths(1, 2).unwrap(); + + let mappings = mapper.mappings(); + assert_eq!(mappings.len(), 1); + + let mapping = &mappings[0]; + let root_names = mapping + .root_nets() + .map(|net| net.get_identifier().to_string()) + .collect::>(); + let vars = mapping + .get_expr() + .iter() + .filter_map(|node| node.get_var().map(|symbol| symbol.to_string())) + .collect::>(); + + assert_eq!(root_names, ["y"]); + assert!(vars.contains(&"s1_out".to_string())); + assert!(!vars.contains(&"s2_out".to_string())); + assert!(!vars.contains(&"s3_out".to_string())); +} From 4d062ac1c90b65a068bf979daafda4891555d63f Mon Sep 17 00:00:00 2001 From: matth2k Date: Wed, 17 Jun 2026 19:23:19 -0400 Subject: [PATCH 7/7] Small refactor --- src/netlist.rs | 11 ++++++----- src/timing.rs | 22 ++++++---------------- tests/timing.rs | 35 ++++++----------------------------- 3 files changed, 18 insertions(+), 50 deletions(-) diff --git a/src/netlist.rs b/src/netlist.rs index a734d250..35e6fb3e 100644 --- a/src/netlist.rs +++ b/src/netlist.rs @@ -356,19 +356,20 @@ impl<'a, L: CircuitLang, I: Instantiable + LogicFunc + 'static> LogicMapper<' ._netlist .get_analysis::>() .map_err(|e| e.to_string())?; - let critical_paths = get_critical_paths(&analysis, topk); - if critical_paths.is_empty() { - return Err("Critical paths are empty".to_string()); - } + let mut expanded_nodes = HashSet::new(); let mut roots = Vec::new(); - for critical_path in &critical_paths { + for critical_path in get_critical_paths(&analysis).take(topk) { let endpoint = critical_path.endpoint(); expanded_nodes.extend(critical_path.expand_n_nodes(branch_factor)); roots.push(endpoint); } + if roots.is_empty() { + return Err("No critical endpoints found".to_string()); + } + self.insert_filtered(roots, move |d| expanded_nodes.contains(d), |_| true) } } diff --git a/src/timing.rs b/src/timing.rs index e58fcfde..e2cc02d4 100644 --- a/src/timing.rs +++ b/src/timing.rs @@ -87,22 +87,12 @@ fn build_path_from_endpoint( Some(DelayPath { path }) } -/// Gets up to `n` most critical paths. +/// Build the critical paths along each critical endpoint pub fn get_critical_paths( analysis: &CombDepthInfo<'_, I>, - n: usize, -) -> Vec> { - if analysis.get_max_depth().is_none() { - return Vec::new(); - } - - let mut vec = Vec::new(); - - for p in analysis.get_critical_points().into_iter().take(n) { - if let Some(path) = build_path_from_endpoint(analysis, p.clone()) { - vec.push(path); - } - } - - vec +) -> impl Iterator> { + analysis + .get_critical_points() + .into_iter() + .flat_map(|p| build_path_from_endpoint(analysis, p)) } diff --git a/tests/timing.rs b/tests/timing.rs index 25c23ed7..b7f91c8a 100644 --- a/tests/timing.rs +++ b/tests/timing.rs @@ -156,7 +156,7 @@ fn critical_path_uses_one_max_depth_branch() { let (netlist, root, left, _right) = reconvergent_netlist(); let analysis = timing_analysis(&netlist); - let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + let path = get_critical_paths(&analysis).next().unwrap(); assert_eq!(path.endpoint(), root); assert_eq!(path.path(), &[root, left]); @@ -166,7 +166,7 @@ fn critical_path_uses_one_max_depth_branch() { fn expansion_adds_neighboring_fanin_nodes() { let (netlist, _root, _left, right) = reconvergent_netlist(); let analysis = timing_analysis(&netlist); - let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + let path = get_critical_paths(&analysis).next().unwrap(); let unexpanded = path.expand_n_nodes(0); let expanded = path.expand_n_nodes(1); @@ -186,7 +186,7 @@ fn gets_multiple_critical_paths() { } = two_output_netlist(); let analysis = timing_analysis(&netlist); - let paths = get_critical_paths(&analysis, 2); + let paths = get_critical_paths(&analysis).take(2).collect::>(); assert_eq!(paths.len(), 2); assert!( @@ -201,35 +201,12 @@ fn gets_multiple_critical_paths() { ); } -#[test] -fn requesting_zero_critical_paths_returns_empty_result() { - let (netlist, _root, _left, _right) = reconvergent_netlist(); - let analysis = timing_analysis(&netlist); - - let paths = get_critical_paths(&analysis, 0); - - assert!(paths.is_empty()); -} - #[test] fn critical_paths_use_timing_endpoints_not_internal_chain_nodes() { let (netlist, first, second, third) = single_chain_netlist(); let analysis = timing_analysis(&netlist); - let paths = get_critical_paths(&analysis, 3); - let debug_paths = paths - .iter() - .map(|path| { - path.path() - .iter() - .map(|net| net.get_identifier().to_string()) - .collect::>() - }) - .collect::>(); - // if you run cargo test --test timing -- --nocapture --test-threads=1 u can see that it is printing 3 critical paths - // when there should only be one. This is becuase it is identifying as critical ends all the ndoes in the critical path - // this is I believe an issue with CombDepthInfo in safety net in the compute method - eprintln!("debug critical paths: {debug_paths:?}"); + let paths = get_critical_paths(&analysis).collect::>(); assert_eq!(paths.len(), 1); assert_eq!(paths[0].endpoint(), third); @@ -270,7 +247,7 @@ fn critical_path_stops_at_register_boundary() { after.expose_with_name("y".into()); let analysis = timing_analysis(&netlist); - let path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + let path = get_critical_paths(&analysis).next().unwrap(); assert_eq!( path.path(), @@ -368,7 +345,7 @@ endmodule let ast = sv_parse_wrapper(verilog, None).unwrap(); let netlist = from_vast::(&ast).unwrap(); let analysis = timing_analysis(&netlist); - let critical_path = get_critical_paths(&analysis, 1).into_iter().next().unwrap(); + let critical_path = get_critical_paths(&analysis).next().unwrap(); let path_names = critical_path .path() .iter()