From 899385967b3e16a00a80fdd9aeb9da6516dbdc78 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 09:40:14 +0100 Subject: [PATCH 01/14] Skeleton for visualize_cmd --- tooling/ssa_cli/src/cli/mod.rs | 15 ++++++++++++ tooling/ssa_cli/src/cli/transform_cmd.rs | 15 ++++-------- tooling/ssa_cli/src/cli/visualize_cmd.rs | 31 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 tooling/ssa_cli/src/cli/visualize_cmd.rs diff --git a/tooling/ssa_cli/src/cli/mod.rs b/tooling/ssa_cli/src/cli/mod.rs index 26d005225aa..ca1676d4076 100644 --- a/tooling/ssa_cli/src/cli/mod.rs +++ b/tooling/ssa_cli/src/cli/mod.rs @@ -5,12 +5,14 @@ use clap::{Args, Parser, Subcommand, command}; use color_eyre::eyre::{self, Context, bail}; use const_format::formatcp; use noir_artifact_cli::commands::parse_and_normalize_path; +use noir_artifact_cli::fs::artifact::write_to_file; use noirc_driver::CompileOptions; use noirc_errors::{println_to_stderr, println_to_stdout}; use noirc_evaluator::ssa::{SsaEvaluatorOptions, SsaPass, primary_passes, ssa_gen::Ssa}; mod interpret_cmd; mod transform_cmd; +mod visualize_cmd; const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION_STRING: &str = formatcp!("version = {}\n", PKG_VERSION,); @@ -49,6 +51,7 @@ enum SsaCommand { Check, Interpret(interpret_cmd::InterpretCommand), Transform(transform_cmd::TransformCommand), + Visualize(visualize_cmd::VisualizeCommand), } pub(crate) fn start_cli() -> eyre::Result<()> { @@ -70,6 +73,7 @@ pub(crate) fn start_cli() -> eyre::Result<()> { } SsaCommand::Interpret(cmd) => interpret_cmd::run(cmd, ssa()?)?, SsaCommand::Transform(cmd) => transform_cmd::run(cmd, ssa()?)?, + SsaCommand::Visualize(cmd) => visualize_cmd::run(cmd, ssa()?)?, } Ok(()) @@ -96,6 +100,17 @@ fn read_source(path: Option) -> eyre::Result { } } +/// Write the output to a file or stdout. +fn write_output(output: &str, path: Option) -> eyre::Result<()> { + if let Some(path) = path { + write_to_file(output.as_bytes(), &path) + .wrap_err_with(|| format!("failed to write output to {}", path.to_string_lossy()))?; + } else { + println_to_stdout!("{output}"); + } + Ok(()) +} + /// Parse the SSA. /// /// If parsing fails, print errors to `stderr` and return a failure. diff --git a/tooling/ssa_cli/src/cli/transform_cmd.rs b/tooling/ssa_cli/src/cli/transform_cmd.rs index 792c835e2b8..cd9250ea63b 100644 --- a/tooling/ssa_cli/src/cli/transform_cmd.rs +++ b/tooling/ssa_cli/src/cli/transform_cmd.rs @@ -2,11 +2,13 @@ use std::path::PathBuf; use clap::Args; use color_eyre::eyre::{self, Context, bail}; -use noir_artifact_cli::{commands::parse_and_normalize_path, fs::artifact::write_to_file}; +use noir_artifact_cli::commands::parse_and_normalize_path; use noirc_driver::CompileOptions; -use noirc_errors::{println_to_stderr, println_to_stdout}; +use noirc_errors::println_to_stderr; use noirc_evaluator::ssa::{SsaLogging, SsaPass, ssa_gen::Ssa}; +use crate::cli::write_output; + /// Parse the input SSA, run some SSA passes on it, then write the output SSA. #[derive(Debug, Clone, Args)] pub(super) struct TransformCommand { @@ -56,14 +58,7 @@ pub(super) fn run(args: TransformCommand, mut ssa: Ssa) -> eyre::Result<()> { // Print the final state so that that it can be piped back to the CLI. let output = format_ssa(&mut ssa, msg, true); - if let Some(path) = args.output_path { - write_to_file(output.as_bytes(), &path) - .wrap_err_with(|| format!("failed to write SSA to {}", path.to_string_lossy()))?; - } else { - println_to_stdout!("{output}"); - } - - Ok(()) + write_output(&output, args.output_path) } /// Run an SSA pass, and optionally print to `stderr`, distinct from `stdout` where the final result goes. diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs new file mode 100644 index 00000000000..cd0e9e88492 --- /dev/null +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use clap::Args; +use color_eyre::eyre; +use noir_artifact_cli::commands::parse_and_normalize_path; +use noirc_evaluator::ssa::ssa_gen::Ssa; + +use crate::cli::write_output; + +/// Parse the SSA and render the CFG for visualization with Mermaid. +#[derive(Debug, Clone, Args)] +pub(super) struct VisualizeCommand { + /// Path to write the output to. + /// + /// If empty, the output will be written to stdout. + #[clap(long, short, value_parser = parse_and_normalize_path)] + pub output_path: Option, + + /// Surround the output with syntax for direct embedding in a Markdown file. + /// + /// Useful for dumping the output into a file that can be previewed in VS Code for example. + #[clap(long, short = 'p')] + pub markdown: bool, +} + +pub(super) fn run(args: VisualizeCommand, ssa: Ssa) -> eyre::Result<()> { + // Print the final state so that that it can be piped back to the CLI. + let output: String = todo!(); + + write_output(&output, args.output_path) +} From 9222c02d4b446dc023a04f978fed5ce7de7f0346 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 09:48:02 +0100 Subject: [PATCH 02/14] Print the SSA in check --- tooling/ssa_cli/src/cli/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tooling/ssa_cli/src/cli/mod.rs b/tooling/ssa_cli/src/cli/mod.rs index ca1676d4076..d0c767129a9 100644 --- a/tooling/ssa_cli/src/cli/mod.rs +++ b/tooling/ssa_cli/src/cli/mod.rs @@ -48,6 +48,7 @@ enum SsaCommand { /// List the SSA passes we can apply. List, /// Parse and (optionally) validate the SSA. + /// Prints the normalized SSA, with canonic ID assignment. Check, Interpret(interpret_cmd::InterpretCommand), Transform(transform_cmd::TransformCommand), @@ -69,7 +70,8 @@ pub(crate) fn start_cli() -> eyre::Result<()> { } } SsaCommand::Check => { - let _ = ssa()?; + // Parsing normalizes the SSA, so we just need to print it. + println_to_stdout!("{}", ssa()?); } SsaCommand::Interpret(cmd) => interpret_cmd::run(cmd, ssa()?)?, SsaCommand::Transform(cmd) => transform_cmd::run(cmd, ssa()?)?, From 73cf70be52fd11045f0b05fbbe82677ba45a762b Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 11:46:39 +0100 Subject: [PATCH 03/14] Add FunctionView --- .../noirc_evaluator/src/ssa/ir/function.rs | 93 +++++++++++++------ tooling/ast_fuzzer/src/input/dictionary.rs | 2 +- tooling/ast_fuzzer/tests/parser.rs | 5 +- tooling/nargo_cli/src/cli/interpret_cmd.rs | 2 +- tooling/ssa_cli/src/cli/interpret_cmd.rs | 2 + 5 files changed, 73 insertions(+), 31 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/ir/function.rs b/compiler/noirc_evaluator/src/ssa/ir/function.rs index 6c96ce5225b..cbd1ceaa866 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/function.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/function.rs @@ -6,6 +6,9 @@ use iter_extended::vecmap; use noirc_frontend::monomorphization::ast::InlineType; use serde::{Deserialize, Serialize}; +use crate::ssa::ir::instruction::Instruction; +use crate::ssa::ir::post_order::PostOrder; + use super::basic_block::BasicBlockId; use super::dfg::{DataFlowGraph, GlobalsGraph}; use super::instruction::TerminatorInstruction; @@ -223,10 +226,69 @@ impl Function { .sum() } + pub fn view(&self) -> FunctionView { + FunctionView(self) + } +} + +impl Clone for Function { + fn clone(&self) -> Self { + Function::clone_with_id(self.id(), self) + } +} + +impl std::fmt::Display for RuntimeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RuntimeType::Acir(inline_type) => write!(f, "acir({inline_type})"), + RuntimeType::Brillig(inline_type) => write!(f, "brillig({inline_type})"), + } + } +} + +/// Provide public access to certain aspects of a `Function` without bloating its API. +pub struct FunctionView<'a>(&'a Function); + +impl<'a> FunctionView<'a> { + /// Iterate over every Value in this DFG in no particular order, including unused Values, + /// for testing purposes. + pub fn values_iter(&self) -> impl DoubleEndedIterator { + self.0.dfg.values_iter() + } + + /// Iterate over the blocks in the CFG in reverse-post-order. + pub fn blocks_iter(&self) -> impl ExactSizeIterator { + let post_order = PostOrder::with_function(self.0); + post_order.into_vec_reverse().into_iter() + } + + /// Iterate over the successors of a blocks. + pub fn block_successors_iter( + &self, + block_id: BasicBlockId, + ) -> impl ExactSizeIterator { + let block = &self.0.dfg[block_id]; + block.successors() + } + + /// Iterate over the functions called from a block. + pub fn block_callees_iter(&self, block_id: BasicBlockId) -> impl Iterator { + let block = &self.0.dfg[block_id]; + block.instructions().iter().map(|id| &self.0.dfg[*id]).filter_map(|instruction| { + let Instruction::Call { func, .. } = instruction else { + return None; + }; + let Value::Function(func) = self.0.dfg[*func] else { + return None; + }; + Some(func) + }) + } + /// Iterate over the numeric constants in the function. pub fn constants(&self) -> impl Iterator { - let local = self.dfg.values_iter(); - let global = self.dfg.globals.values_iter(); + let local = self.0.dfg.values_iter(); + let global = self.0.dfg.globals.values_iter(); local.chain(global).filter_map(|(_, value)| { if let Value::NumericConstant { constant, typ } = value { Some((constant, typ)) @@ -237,41 +299,20 @@ impl Function { } pub fn has_data_bus_return_data(&self) -> bool { - self.dfg.data_bus.return_data.is_some() + self.0.dfg.data_bus.return_data.is_some() } /// Return the types of the function parameters. pub fn parameter_types(&self) -> Vec { - vecmap(self.parameters(), |p| self.dfg.type_of_value(*p)) + vecmap(self.0.parameters(), |p| self.0.dfg.type_of_value(*p)) } /// Return the types of the returned values, if there are any. pub fn return_types(&self) -> Option> { - self.returns().map(|rs| vecmap(rs, |p| self.dfg.type_of_value(*p))) - } -} - -impl Clone for Function { - fn clone(&self) -> Self { - Function::clone_with_id(self.id(), self) + self.0.returns().map(|rs| vecmap(rs, |p| self.0.dfg.type_of_value(*p))) } } -impl std::fmt::Display for RuntimeType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RuntimeType::Acir(inline_type) => write!(f, "acir({inline_type})"), - RuntimeType::Brillig(inline_type) => write!(f, "brillig({inline_type})"), - } - } -} - -/// Iterate over every Value in this DFG in no particular order, including unused Values, -/// for testing purposes. -pub fn function_values_iter(func: &Function) -> impl DoubleEndedIterator { - func.dfg.values_iter() -} - /// FunctionId is a reference for a function /// /// This Id is how each function refers to other functions diff --git a/tooling/ast_fuzzer/src/input/dictionary.rs b/tooling/ast_fuzzer/src/input/dictionary.rs index b64788ed721..48cf7576c7f 100644 --- a/tooling/ast_fuzzer/src/input/dictionary.rs +++ b/tooling/ast_fuzzer/src/input/dictionary.rs @@ -7,7 +7,7 @@ use noirc_evaluator::ssa::ssa_gen::Ssa; pub(crate) fn build_dictionary_from_ssa(ssa: &Ssa) -> BTreeSet { let mut constants = BTreeSet::new(); for func in ssa.functions.values() { - for (constant, _) in func.constants() { + for (constant, _) in func.view().constants() { constants.insert(*constant); } } diff --git a/tooling/ast_fuzzer/tests/parser.rs b/tooling/ast_fuzzer/tests/parser.rs index 8d6022d322e..effa59195af 100644 --- a/tooling/ast_fuzzer/tests/parser.rs +++ b/tooling/ast_fuzzer/tests/parser.rs @@ -12,7 +12,6 @@ use noirc_evaluator::{ brillig::BrilligOptions, ssa::{ self, - ir::function::function_values_iter, opt::inlining::MAX_INSTRUCTIONS, primary_passes, ssa_gen::{self, Ssa}, @@ -95,8 +94,8 @@ fn arb_ssa_roundtrip() { continue; } let func2 = &ssa2.functions[&func_id]; - let values1 = function_values_iter(&func1).collect::>(); - let values2 = function_values_iter(func2).collect::>(); + let values1 = func1.view().values_iter().collect::>(); + let values2 = func2.view().values_iter().collect::>(); similar_asserts::assert_eq!(values1, values2); } diff --git a/tooling/nargo_cli/src/cli/interpret_cmd.rs b/tooling/nargo_cli/src/cli/interpret_cmd.rs index 7187b96e389..d8ff20b0f33 100644 --- a/tooling/nargo_cli/src/cli/interpret_cmd.rs +++ b/tooling/nargo_cli/src/cli/interpret_cmd.rs @@ -120,7 +120,7 @@ pub(crate) fn run(args: InterpretCommand, workspace: Workspace) -> Result<(), Cl // correctness, it's enough if we make sure the flattened values match. let ssa_return = ssa_return.map(|ssa_return| { let main_function = &ssa.functions[&ssa.main_id]; - if main_function.has_data_bus_return_data() { + if main_function.view().has_data_bus_return_data() { let values = flatten_databus_values(ssa_return); vec![Value::array(values, vec![Type::Numeric(NumericType::NativeField)])] } else { diff --git a/tooling/ssa_cli/src/cli/interpret_cmd.rs b/tooling/ssa_cli/src/cli/interpret_cmd.rs index 6672ffd482d..e17206915d9 100644 --- a/tooling/ssa_cli/src/cli/interpret_cmd.rs +++ b/tooling/ssa_cli/src/cli/interpret_cmd.rs @@ -103,6 +103,7 @@ fn abi_from_ssa(ssa: &Ssa) -> Abi { let visibility = AbiVisibility::Public; let parameters = main + .view() .parameter_types() .iter() .enumerate() @@ -114,6 +115,7 @@ fn abi_from_ssa(ssa: &Ssa) -> Abi { .collect(); let return_type = main + .view() .return_types() .filter(|ts| !ts.is_empty()) .map(|types| AbiReturnType { abi_type: abi_type_from_multi_ssa(&types), visibility }); From c21d9902f2a6fd9c65b53e8fc1ed0ce28b1d629b Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 11:46:53 +0100 Subject: [PATCH 04/14] Visualize the SSA --- tooling/ssa_cli/src/cli/visualize_cmd.rs | 52 ++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs index cd0e9e88492..5de4db488fd 100644 --- a/tooling/ssa_cli/src/cli/visualize_cmd.rs +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -1,7 +1,8 @@ +use std::fmt::Write; use std::path::PathBuf; use clap::Args; -use color_eyre::eyre; +use color_eyre::eyre::{self, Context}; use noir_artifact_cli::commands::parse_and_normalize_path; use noirc_evaluator::ssa::ssa_gen::Ssa; @@ -24,8 +25,51 @@ pub(super) struct VisualizeCommand { } pub(super) fn run(args: VisualizeCommand, ssa: Ssa) -> eyre::Result<()> { - // Print the final state so that that it can be piped back to the CLI. - let output: String = todo!(); - + let output = render_mermaid(ssa).wrap_err("failed to render SSA to Mermaid")?; write_output(&output, args.output_path) } + +/// Render the SSA as a Mermaid [flowchart](https://mermaid.js.org/syntax/flowchart.html). +fn render_mermaid(ssa: Ssa) -> eyre::Result { + let indent = " "; + let mut out = String::new(); + let mut write_out = |i: usize, s: String| writeln!(out, "{}{s}", indent.repeat(i)); + + write_out(0, "flowchart TB".into())?; + + // Defer rendering calls after all subgraphs have been defined, + // otherwise Mermaid doesn't know that the target is going to + // be a subgraph and treats it as a node. + let mut calls = Vec::new(); + + // Render each function as a subgraph, with internal jumps only. + for (func_id, func) in ssa.functions { + write_out(1, format!("subgraph {func_id}"))?; + let view = func.view(); + for block_id in view.blocks_iter() { + let successors = view.block_successors_iter(block_id); + + // Show exit blocks as double circle, normal blocks as circles. + let (bl, br) = if successors.len() == 0 { ("(((", ")))") } else { ("((", "))") }; + + // Use the function ID to identify the block uniquely across all subgraphs. + write_out(2, format!("{func_id}.{block_id}{bl}{block_id}{br}"))?; + + for successor_id in successors { + write_out(2, format!("{func_id}.{block_id} --> {func_id}.{successor_id}"))?; + } + + for callee_id in view.block_callees_iter(block_id) { + calls.push((func_id, block_id, callee_id)); + } + } + write_out(1, "end\n".into())?; + } + + // Render function calls. + for (func_id, block_id, callee_id) in calls { + write_out(1, format!("{func_id}.{block_id} -..-> {callee_id}"))?; + } + + Ok(out) +} From b35ca77845ec3f5cba6a3234ab7e037d580f1a51 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 12:04:28 +0100 Subject: [PATCH 05/14] Add --markdown syntax --- tooling/ssa_cli/src/cli/visualize_cmd.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs index 5de4db488fd..6d08bb302bf 100644 --- a/tooling/ssa_cli/src/cli/visualize_cmd.rs +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -17,7 +17,7 @@ pub(super) struct VisualizeCommand { #[clap(long, short, value_parser = parse_and_normalize_path)] pub output_path: Option, - /// Surround the output with syntax for direct embedding in a Markdown file. + /// Surround the output with brackets for direct embedding in a Markdown file. /// /// Useful for dumping the output into a file that can be previewed in VS Code for example. #[clap(long, short = 'p')] @@ -25,16 +25,20 @@ pub(super) struct VisualizeCommand { } pub(super) fn run(args: VisualizeCommand, ssa: Ssa) -> eyre::Result<()> { - let output = render_mermaid(ssa).wrap_err("failed to render SSA to Mermaid")?; + let output = render_mermaid(ssa, args.markdown).wrap_err("failed to render SSA to Mermaid")?; write_output(&output, args.output_path) } /// Render the SSA as a Mermaid [flowchart](https://mermaid.js.org/syntax/flowchart.html). -fn render_mermaid(ssa: Ssa) -> eyre::Result { +fn render_mermaid(ssa: Ssa, markdown: bool) -> eyre::Result { let indent = " "; let mut out = String::new(); let mut write_out = |i: usize, s: String| writeln!(out, "{}{s}", indent.repeat(i)); + if markdown { + write_out(0, "```mermaid".into())?; + } + write_out(0, "flowchart TB".into())?; // Defer rendering calls after all subgraphs have been defined, @@ -71,5 +75,9 @@ fn render_mermaid(ssa: Ssa) -> eyre::Result { write_out(1, format!("{func_id}.{block_id} -..-> {callee_id}"))?; } + if markdown { + write_out(0, "```".into())?; + } + Ok(out) } From 9ff761bb6faa47711fe55886b19fb43cd80737ec Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 12:06:02 +0100 Subject: [PATCH 06/14] Show the function name in the subgraph title --- tooling/ssa_cli/src/cli/visualize_cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs index 6d08bb302bf..17596ba8b9c 100644 --- a/tooling/ssa_cli/src/cli/visualize_cmd.rs +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -48,7 +48,7 @@ fn render_mermaid(ssa: Ssa, markdown: bool) -> eyre::Result { // Render each function as a subgraph, with internal jumps only. for (func_id, func) in ssa.functions { - write_out(1, format!("subgraph {func_id}"))?; + write_out(1, format!("subgraph {func_id} [{} {func_id}]", func.name()))?; let view = func.view(); for block_id in view.blocks_iter() { let successors = view.block_successors_iter(block_id); From 1783957cefd0a202b5162b714d55e0f6c5c989f0 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 12:57:53 +0100 Subject: [PATCH 07/14] Add --url-encode --- Cargo.lock | 2 ++ tooling/ssa_cli/Cargo.toml | 2 ++ tooling/ssa_cli/src/cli/visualize_cmd.rs | 43 ++++++++++++++++++------ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af1b69b118a..44443b84216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3568,6 +3568,7 @@ dependencies = [ name = "noir_ssa_cli" version = "1.0.0-beta.12" dependencies = [ + "base64", "clap", "color-eyre", "const_format", @@ -3578,6 +3579,7 @@ dependencies = [ "noirc_driver", "noirc_errors", "noirc_evaluator", + "serde_json", "tempfile", "thiserror 1.0.69", "toml", diff --git a/tooling/ssa_cli/Cargo.toml b/tooling/ssa_cli/Cargo.toml index 898fcd7896b..e21f01ce865 100644 --- a/tooling/ssa_cli/Cargo.toml +++ b/tooling/ssa_cli/Cargo.toml @@ -16,9 +16,11 @@ path = "src/main.rs" workspace = true [dependencies] +base64.workspace = true clap.workspace = true color-eyre.workspace = true const_format.workspace = true +serde_json.workspace = true thiserror.workspace = true toml.workspace = true tempfile.workspace = true diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs index 17596ba8b9c..c69be37e7a6 100644 --- a/tooling/ssa_cli/src/cli/visualize_cmd.rs +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -1,10 +1,12 @@ use std::fmt::Write; use std::path::PathBuf; +use base64::Engine; use clap::Args; use color_eyre::eyre::{self, Context}; use noir_artifact_cli::commands::parse_and_normalize_path; use noirc_evaluator::ssa::ssa_gen::Ssa; +use serde_json::json; use crate::cli::write_output; @@ -20,25 +22,32 @@ pub(super) struct VisualizeCommand { /// Surround the output with brackets for direct embedding in a Markdown file. /// /// Useful for dumping the output into a file that can be previewed in VS Code for example. - #[clap(long, short = 'p')] + #[clap(long, conflicts_with = "url_encode")] pub markdown: bool, + + /// Encode the data to be included in a URL as expected by http://mermaid.live/view + #[clap(long, conflicts_with = "markdown")] + pub url_encode: bool, } pub(super) fn run(args: VisualizeCommand, ssa: Ssa) -> eyre::Result<()> { - let output = render_mermaid(ssa, args.markdown).wrap_err("failed to render SSA to Mermaid")?; + let mut output = render_mermaid(ssa).wrap_err("failed to render SSA to Mermaid")?; + + if args.markdown { + output = format!("```mermaid\n{output}\n```") + } else if args.url_encode { + output = url_encode(output)?; + } + write_output(&output, args.output_path) } /// Render the SSA as a Mermaid [flowchart](https://mermaid.js.org/syntax/flowchart.html). -fn render_mermaid(ssa: Ssa, markdown: bool) -> eyre::Result { +fn render_mermaid(ssa: Ssa) -> eyre::Result { let indent = " "; let mut out = String::new(); let mut write_out = |i: usize, s: String| writeln!(out, "{}{s}", indent.repeat(i)); - if markdown { - write_out(0, "```mermaid".into())?; - } - write_out(0, "flowchart TB".into())?; // Defer rendering calls after all subgraphs have been defined, @@ -75,9 +84,21 @@ fn render_mermaid(ssa: Ssa, markdown: bool) -> eyre::Result { write_out(1, format!("{func_id}.{block_id} -..-> {callee_id}"))?; } - if markdown { - write_out(0, "```".into())?; - } - Ok(out) } + +/// Encode the Mermaid markup as it is expected by the [Mermaid Live Editor](https://mermaid.live) +/// +/// The returned URL fragment should be used as `https://mermaid.live/view#{fragment}` +fn url_encode(markup: String) -> eyre::Result { + // See https://github.com/mermaid-js/mermaid-live-editor/discussions/1291 + let json = json!({ + "code": markup, + "mermaid": {"theme": "default"} + }); + let json = serde_json::to_string(&json)?; + // We can use the `pako:` encoding for compression, or the `base64:` encoding for no-compression. + let encoded = base64::engine::general_purpose::STANDARD.encode(json); + let fragment = format!("base64:{encoded}"); + Ok(fragment) +} From 5501d52a2bd46e2f8e9b11df0cfedc88a655fd53 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 13:25:14 +0100 Subject: [PATCH 08/14] Add README.md --- tooling/ssa_cli/README.md | 100 +++++++++++++++++++++++ tooling/ssa_cli/src/cli/visualize_cmd.rs | 2 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tooling/ssa_cli/README.md diff --git a/tooling/ssa_cli/README.md b/tooling/ssa_cli/README.md new file mode 100644 index 00000000000..2884449d48d --- /dev/null +++ b/tooling/ssa_cli/README.md @@ -0,0 +1,100 @@ +# SSA CLI + +The SSA CLI facilitates experimenting with SSA snippets outside the context of a Noir program. For example we can print the Initial SSA (or any itermediate step) of a Noir program from `nargo [compile|execute|interpret]` using the `--show-ssa-pass` option, and then use the SSA CLI to further `transform`, `interpret` or `visualize` it. + + +## Example + +Take this simple Noir program: + +```rust +fn main(c: i32, h: i32) { + assert(c < h); + for t in -10..40 { + show(c, h, t); + } +} + +fn show(c: i32, h: i32, t: i32) { + if t < 0 { + println(f"{t}: freezing"); + } else if t < c { + println(f"{t}: cold"); + } else if t < h { + println(f"{t}: mild"); + } else { + println(f"{t}: hot"); + } +} +``` + +Print the initial SSA and save it to a file: +```bash +cargo run -q -p nargo_cli -- compile --silence-warnings --show-ssa-pass Initial \ + | tail -n +2 \ + > example.ssa +``` + +For quick checks we can also just copy-paste some SSA snippet and use e.g. `pbpaste` to pipe it to the SSA CLI. + +### Visualize + +We can render the Control Flow Graph of the program to a Markdown file which we can preview in an editor (e.g. using the VS Code [Mermaid Markdown extension](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)): + +```bash +cat example.ssa \ + | cargo run -q -p noir_ssa_cli -- visualize --markdown \ + > example.md +code example.md +``` + +Or we can go directly to the [Mermaid Live Editor](https:://mermaid.live): + +```bash +open https://mermaid.live/view#$(cargo run -q -p noir_ssa_cli -- visualize --source-path example.ssa --url-encode) +``` + +The result should look like [this](https://mermaid.live/view#pako:eNqNlN2ymjAUhV-FyZXOeJj8kQAXvWj7CL2qdDooUZyjwYkwp63ju3cbAZMoZ2S8CGt9e0nI3pzRuqkUytFm33ys69K00Y-vhY7gOnWrrSmPdbTB0fJQ7jQsft2s67XB8QrPZis8nwdi9Pb2xa6IbxCgSUiTkaZTBvMNNoMcNg-DKKj0QQyfRemq0OH-SLQ81c0HLNz9kWf7I-P-iL8_z6C-8fhoZHw0WPEpI_GNBGKSMCYZaTllpL6RQkwaxqQjnfmGBFqGtJyiM6CzkM5GWvgGB5qHNJ-iBdAipMVIM994bDYy9tQjHfTU0y6h0fJodrrdwyBQt1GobRTbKZ_VM6eeufXstXru1HO3nr9Wn_T1vzu9bvSpNTDSqgLdzUpeyxITWcLNEq9lyYks6WbJz7L6SY9je7i9dmvqm8bvmhw0dtf4E40MWj_L9pQHrR81e3KD1nerPY1BgwFGC7Q1uwrlrenUAh2UgU8p3KLzlS9QW6uDKlAOy6o07wUq9AVqjqX-2TSHocw03bZG-abcn-CuO1Zlq77vSniDdwReiDLfmk63KCfMRqD8jP6gnCU8ZiTNBPwol3yB_gKCs5iljHNKGMkwSdhlgf7Z_8RxiomgLOOYC3llLv8BmsiLzg). + +### Transform + +We can experiment with applying various SSA passes with the `transform` command, using the `--ssa-pass` option to specify which passes we want to run. The same pass can appear multiple times. + +```bash +cargo run -q -p noir_ssa_cli -- transform --source-path example.ssa --ssa-pass Unrolling +``` + +Use the `list` command to see the available options for `--ssa-pass`. + +### Interpret + +The SSA can be evaluated with some inputs using the `interpret` command. The inputs can come from a JSON or TOML file, or CLI options. Notably the variable names have to match the SSA, not the original Noir code. + +For example if we look at the `example.ssa` above, we see that the variables are called `v0` and `v1`: + +```console +$ head example.ssa +acir(inline) fn main f0 { + b0(v0: i32, v1: i32): + v3 = lt v0, v1 + constrain v3 == u1 1 + jmp b1(i32 -10) + b1(v2: i32): + v7 = lt v2, i32 40 + jmpif v7 then: b2, else: b3 + b2(): + call f1(v0, v1, v2) +``` + +We can run the SSA for example by passing values encoded as TOML using the `--input-toml` option: + +```console +$ cargo run -q -p noir_ssa_cli -- interpret --source-path example.ssa --input-toml "v0=10; v1=30" +-10: freezing +-9: freezing +... +39: hot +--- Interpreter result: +Ok() +--- +``` \ No newline at end of file diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs index c69be37e7a6..44d81284430 100644 --- a/tooling/ssa_cli/src/cli/visualize_cmd.rs +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -34,7 +34,7 @@ pub(super) fn run(args: VisualizeCommand, ssa: Ssa) -> eyre::Result<()> { let mut output = render_mermaid(ssa).wrap_err("failed to render SSA to Mermaid")?; if args.markdown { - output = format!("```mermaid\n{output}\n```") + output = format!("```mermaid\n{output}\n```"); } else if args.url_encode { output = url_encode(output)?; } From 7d102dfe53e88d9b6a43c5e5efb654d2a9d2050d Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 13:37:43 +0100 Subject: [PATCH 09/14] Fix link --- tooling/ssa_cli/src/cli/visualize_cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/ssa_cli/src/cli/visualize_cmd.rs b/tooling/ssa_cli/src/cli/visualize_cmd.rs index 44d81284430..0be83aab18d 100644 --- a/tooling/ssa_cli/src/cli/visualize_cmd.rs +++ b/tooling/ssa_cli/src/cli/visualize_cmd.rs @@ -25,7 +25,7 @@ pub(super) struct VisualizeCommand { #[clap(long, conflicts_with = "url_encode")] pub markdown: bool, - /// Encode the data to be included in a URL as expected by http://mermaid.live/view + /// Encode the data to be included in a URL as expected by #[clap(long, conflicts_with = "markdown")] pub url_encode: bool, } From f8a1f99244462735d89fbeb8d3e67e482d77006d Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 14:53:15 +0100 Subject: [PATCH 10/14] Update tooling/ssa_cli/README.md Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- tooling/ssa_cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/ssa_cli/README.md b/tooling/ssa_cli/README.md index 2884449d48d..57e41331661 100644 --- a/tooling/ssa_cli/README.md +++ b/tooling/ssa_cli/README.md @@ -1,6 +1,6 @@ # SSA CLI -The SSA CLI facilitates experimenting with SSA snippets outside the context of a Noir program. For example we can print the Initial SSA (or any itermediate step) of a Noir program from `nargo [compile|execute|interpret]` using the `--show-ssa-pass` option, and then use the SSA CLI to further `transform`, `interpret` or `visualize` it. +The SSA CLI facilitates experimenting with SSA snippets outside the context of a Noir program. For example we can print the Initial SSA (or any intermediate step) of a Noir program from `nargo [compile|execute|interpret]` using the `--show-ssa-pass` option, and then use the SSA CLI to further `transform`, `interpret` or `visualize` it. ## Example From 71d20f6a2b8e0f5206abaeb0be8d72e003789ad7 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 15:14:37 +0100 Subject: [PATCH 11/14] Add justfile shortcut --- justfile | 8 ++++++++ tooling/ssa_cli/README.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/justfile b/justfile index 212fdc8cf18..783d215e8ff 100644 --- a/justfile +++ b/justfile @@ -133,6 +133,14 @@ format-noir: cargo run -- --program-dir={{justfile_dir()}}/noir_stdlib fmt --check cd ./test_programs && NARGO="{{justfile_dir()}}/target/debug/nargo" ./format.sh check +# Visualize the CFG after a certain SSA pass and open the Mermaid Live editor. +[no-cd] +visualize-live PASS: + open https://mermaid.live/view#$( \ + cargo run -q -p nargo_cli -- compile --show-ssa-pass {{PASS}} \ + | grep -v After \ + | cargo run -q -p noir_ssa_cli -- visualize --url-encode) + # Javascript # Lints Javascript code diff --git a/tooling/ssa_cli/README.md b/tooling/ssa_cli/README.md index 2884449d48d..56229484d84 100644 --- a/tooling/ssa_cli/README.md +++ b/tooling/ssa_cli/README.md @@ -35,6 +35,8 @@ cargo run -q -p nargo_cli -- compile --silence-warnings --show-ssa-pass Initial > example.ssa ``` +The `tail` is just to get rid of the "After Initial SSA:" message. + For quick checks we can also just copy-paste some SSA snippet and use e.g. `pbpaste` to pipe it to the SSA CLI. ### Visualize @@ -56,6 +58,12 @@ open https://mermaid.live/view#$(cargo run -q -p noir_ssa_cli -- visualize --sou The result should look like [this](https://mermaid.live/view#pako:eNqNlN2ymjAUhV-FyZXOeJj8kQAXvWj7CL2qdDooUZyjwYkwp63ju3cbAZMoZ2S8CGt9e0nI3pzRuqkUytFm33ys69K00Y-vhY7gOnWrrSmPdbTB0fJQ7jQsft2s67XB8QrPZis8nwdi9Pb2xa6IbxCgSUiTkaZTBvMNNoMcNg-DKKj0QQyfRemq0OH-SLQ81c0HLNz9kWf7I-P-iL8_z6C-8fhoZHw0WPEpI_GNBGKSMCYZaTllpL6RQkwaxqQjnfmGBFqGtJyiM6CzkM5GWvgGB5qHNJ-iBdAipMVIM994bDYy9tQjHfTU0y6h0fJodrrdwyBQt1GobRTbKZ_VM6eeufXstXru1HO3nr9Wn_T1vzu9bvSpNTDSqgLdzUpeyxITWcLNEq9lyYks6WbJz7L6SY9je7i9dmvqm8bvmhw0dtf4E40MWj_L9pQHrR81e3KD1nerPY1BgwFGC7Q1uwrlrenUAh2UgU8p3KLzlS9QW6uDKlAOy6o07wUq9AVqjqX-2TSHocw03bZG-abcn-CuO1Zlq77vSniDdwReiDLfmk63KCfMRqD8jP6gnCU8ZiTNBPwol3yB_gKCs5iljHNKGMkwSdhlgf7Z_8RxiomgLOOYC3llLv8BmsiLzg). +The same can be achieved for a Noir project with the following utility command: + +```shell +just visualize-live Initial +``` + ### Transform We can experiment with applying various SSA passes with the `transform` command, using the `--ssa-pass` option to specify which passes we want to run. The same pass can appear multiple times. From 65f0704bfd4e3ba051cc6e064e0dfc4956e91f57 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 15:19:22 +0100 Subject: [PATCH 12/14] Rename to just visualize --- justfile | 3 ++- tooling/ssa_cli/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 783d215e8ff..22896f74ac5 100644 --- a/justfile +++ b/justfile @@ -134,8 +134,9 @@ format-noir: cd ./test_programs && NARGO="{{justfile_dir()}}/target/debug/nargo" ./format.sh check # Visualize the CFG after a certain SSA pass and open the Mermaid Live editor. +# This is mostly here for reference: it only works if the pass matches a single unique pass in the pipeline, and there are no errors. [no-cd] -visualize-live PASS: +visualize PASS: open https://mermaid.live/view#$( \ cargo run -q -p nargo_cli -- compile --show-ssa-pass {{PASS}} \ | grep -v After \ diff --git a/tooling/ssa_cli/README.md b/tooling/ssa_cli/README.md index f83c62b82cc..c262b6a7839 100644 --- a/tooling/ssa_cli/README.md +++ b/tooling/ssa_cli/README.md @@ -61,7 +61,7 @@ The result should look like [this](https://mermaid.live/view#pako:eNqNlN2ymjAUhV The same can be achieved for a Noir project with the following utility command: ```shell -just visualize-live Initial +just visualize Initial ``` ### Transform From 1637d27306d3fc8c496b72c4039f46e0c829d2fb Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 16 Sep 2025 16:11:53 +0100 Subject: [PATCH 13/14] Rename to just visualize-ssa-cfg --- justfile | 2 +- tooling/ssa_cli/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 22896f74ac5..724c23478f7 100644 --- a/justfile +++ b/justfile @@ -136,7 +136,7 @@ format-noir: # Visualize the CFG after a certain SSA pass and open the Mermaid Live editor. # This is mostly here for reference: it only works if the pass matches a single unique pass in the pipeline, and there are no errors. [no-cd] -visualize PASS: +visualize-ssa-cfg PASS: open https://mermaid.live/view#$( \ cargo run -q -p nargo_cli -- compile --show-ssa-pass {{PASS}} \ | grep -v After \ diff --git a/tooling/ssa_cli/README.md b/tooling/ssa_cli/README.md index c262b6a7839..1af96e3b60c 100644 --- a/tooling/ssa_cli/README.md +++ b/tooling/ssa_cli/README.md @@ -61,7 +61,7 @@ The result should look like [this](https://mermaid.live/view#pako:eNqNlN2ymjAUhV The same can be achieved for a Noir project with the following utility command: ```shell -just visualize Initial +just visualize-ssa-cfg Initial ``` ### Transform From ec94434c484a503855607bed0dcde158a8c2d28f Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 22 Sep 2025 10:37:22 +0100 Subject: [PATCH 14/14] Update tooling/ssa_cli/src/cli/mod.rs Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- tooling/ssa_cli/src/cli/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/ssa_cli/src/cli/mod.rs b/tooling/ssa_cli/src/cli/mod.rs index d0c767129a9..38ad36cbef6 100644 --- a/tooling/ssa_cli/src/cli/mod.rs +++ b/tooling/ssa_cli/src/cli/mod.rs @@ -48,7 +48,7 @@ enum SsaCommand { /// List the SSA passes we can apply. List, /// Parse and (optionally) validate the SSA. - /// Prints the normalized SSA, with canonic ID assignment. + /// Prints the normalized SSA, with canonical ID assignment. Check, Interpret(interpret_cmd::InterpretCommand), Transform(transform_cmd::TransformCommand),