From 7dd6e65a5f1e628fa44715c3f0e7d41fa6eeb8a0 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 22 Jan 2021 21:13:11 +0000 Subject: [PATCH] Add halo2::dev::circuit_dot_graph behind dev-graph feature flag This method renders circuits as dot graphs, to help circuit developers understand their structure. --- Cargo.toml | 7 ++- src/dev.rs | 7 +++ src/dev/graph.rs | 158 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/dev/graph.rs diff --git a/Cargo.toml b/Cargo.toml index 31fa9c7..a9a083a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,8 @@ readme = "README.md" publish = false [package.metadata.docs.rs] -rustdoc-args = [ "--html-in-header", "katex-header.html" ] +all-features = true +rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "katex-header.html"] [dev-dependencies] criterion = "0.3" @@ -47,6 +48,10 @@ blake2b_simd = "0.5" lazy_static = "1.4.0" static_assertions = "1.1.0" +# Developer tooling dependencies +tabbycat = { version = "0.1", features = ["attributes"], optional = true } + [features] +dev-graph = ["tabbycat"] gadget-traces = ["backtrace"] sanity-checks = [] diff --git a/src/dev.rs b/src/dev.rs index 30105bc..88421bb 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -11,6 +11,13 @@ use crate::{ poly::Rotation, }; +#[cfg(feature = "dev-graph")] +mod graph; + +#[cfg(feature = "dev-graph")] +#[cfg_attr(docsrs, doc(cfg(feature = "dev-graph")))] +pub use graph::circuit_dot_graph; + /// The reasons why a particular circuit is not satisfied. #[derive(Debug, PartialEq)] pub enum VerifyFailure { diff --git a/src/dev/graph.rs b/src/dev/graph.rs new file mode 100644 index 0000000..954522b --- /dev/null +++ b/src/dev/graph.rs @@ -0,0 +1,158 @@ +use ff::Field; +use tabbycat::{AttrList, Edge, GraphBuilder, GraphType, Identity, StmtList}; + +use crate::plonk::{Advice, Assignment, Circuit, Column, ConstraintSystem, Error, Fixed}; + +/// Builds a dot graph string representing the given circuit. +/// +/// The graph is built from calls to [`Layouter::namespace`] both within the circuit, and +/// inside the gadgets and chips that it uses. +/// +/// [`Layouter::namespace`]: crate::circuit::Layouter#method.namespace +pub fn circuit_dot_graph>( + circuit: &ConcreteCircuit, +) -> String { + // Collect the graph details. + let mut cs = ConstraintSystem::default(); + let config = ConcreteCircuit::configure(&mut cs); + let mut graph = Graph::default(); + circuit.synthesize(&mut graph, config).unwrap(); + + // Construct the node labels. We need to store these, because tabbycat operates on + // string references, and we need those references to live long enough. + let node_labels: Vec<_> = graph + .nodes + .into_iter() + .map(|(name, gadget_name)| { + if let Some(gadget_name) = gadget_name { + format!("[{}] {}", gadget_name, name) + } else { + name + } + }) + .collect(); + + // Construct the dot graph statements. + let mut stmts = StmtList::new(); + for (id, label) in node_labels.iter().enumerate() { + stmts = stmts.add_node( + id.into(), + None, + Some(AttrList::new().add_pair(tabbycat::attributes::label(label))), + ); + } + for (parent, child) in graph.edges { + stmts = + stmts.add_edge(Edge::head_node(parent.into(), None).arrow_to_node(child.into(), None)) + } + + // Build the graph! + GraphBuilder::default() + .graph_type(GraphType::DiGraph) + .strict(false) + .id(Identity::id("circuit").unwrap()) + .stmts(stmts) + .build() + .unwrap() + .to_string() +} + +#[derive(Default)] +struct Graph { + /// Graph nodes in the namespace, structured as `(name, gadget_name)`. + nodes: Vec<(String, Option)>, + + /// Directed edges in the graph, as pairs of indices into `nodes`. + edges: Vec<(usize, usize)>, + + /// The current namespace, as indices into `nodes`. + current_namespace: Vec, +} + +impl Assignment for Graph { + fn enter_region(&mut self, _: N) + where + NR: Into, + N: FnOnce() -> NR, + { + // Do nothing; we don't care about regions in this context. + } + + fn exit_region(&mut self) { + // Do nothing; we don't care about regions in this context. + } + + fn assign_advice( + &mut self, + _: A, + _: Column, + _: usize, + _: V, + ) -> Result<(), Error> + where + V: FnOnce() -> Result, + A: FnOnce() -> AR, + AR: Into, + { + // Do nothing; we don't care about cells in this context. + Ok(()) + } + + fn assign_fixed( + &mut self, + _: A, + _: Column, + _: usize, + _: V, + ) -> Result<(), Error> + where + V: FnOnce() -> Result, + A: FnOnce() -> AR, + AR: Into, + { + // Do nothing; we don't care about cells in this context. + Ok(()) + } + + fn copy( + &mut self, + _: usize, + _: usize, + _: usize, + _: usize, + _: usize, + ) -> Result<(), crate::plonk::Error> { + // Do nothing; we don't care about permutations in this context. + Ok(()) + } + + fn push_namespace(&mut self, name_fn: N) + where + NR: Into, + N: FnOnce() -> NR, + { + // Store the new node. + let new_node = self.nodes.len(); + self.nodes.push((name_fn().into(), None)); + + // Create an edge from the parent, if any. + if let Some(parent) = self.current_namespace.last() { + self.edges.push((*parent, new_node)); + } + + // Push the new namespace. + self.current_namespace.push(new_node); + } + + fn pop_namespace(&mut self, gadget_name: Option) { + // Store the gadget name that was extracted, if any. + let node = self + .current_namespace + .last() + .expect("pop_namespace should never be called on the root"); + self.nodes[*node].1 = gadget_name; + + // Pop the namespace. + self.current_namespace.pop(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2accaaa..15fc3da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ //! # halo2 +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow( clippy::op_ref, clippy::assign_op_pattern,