diff --git a/.github/actions/download-noir-ssa/action.yml b/.github/actions/download-noir-ssa/action.yml new file mode 100644 index 00000000000..f4358108ffa --- /dev/null +++ b/.github/actions/download-noir-ssa/action.yml @@ -0,0 +1,18 @@ +name: Download noir-ssa +description: Downloads the noir-ssa binary from an artifact and adds it to the path + +runs: + using: composite + steps: + - name: Download noir-ssa binary + uses: actions/download-artifact@v4 + with: + name: noir-ssa + path: ./noir-ssa + + - name: Set noir-ssa on PATH + shell: bash + run: | + noir_binary="${{ github.workspace }}/noir-ssa/noir-ssa" + chmod +x $noir_binary + echo "$(dirname $noir_binary)" >> $GITHUB_PATH diff --git a/.github/workflows/test-js-packages.yml b/.github/workflows/test-js-packages.yml index 63a452bf226..774aa4f651d 100644 --- a/.github/workflows/test-js-packages.yml +++ b/.github/workflows/test-js-packages.yml @@ -99,7 +99,7 @@ jobs: cache-on-failure: true save-if: ${{ github.event_name != 'merge_group' }} - - name: Build noir-execute + - name: Build artifact run: cargo build --package noir_artifact_cli --release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -111,6 +111,37 @@ jobs: path: ./target/release/noir-execute retention-days: 3 + build-noir-ssa: + runs-on: ubuntu-22.04 + timeout-minutes: 30 + permissions: + contents: read + + steps: + - name: Checkout Noir repo + uses: actions/checkout@v5 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@1.85.0 + + - uses: Swatinem/rust-cache@v2 + with: + key: x86_64-unknown-linux-gnu + cache-on-failure: true + save-if: ${{ github.event_name != 'merge_group' }} + + - name: Build artifact + run: cargo build --package noir_ssa_cli --release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: noir-ssa + path: ./target/release/noir-ssa + retention-days: 3 + build-noirc-abi: runs-on: ubuntu-22.04 timeout-minutes: 30 @@ -533,7 +564,7 @@ jobs: test-examples: name: Example scripts runs-on: ubuntu-24.04 - needs: [build-nargo, build-noir-execute, build-acvm-js, build-noirc-abi] + needs: [build-nargo, build-noir-execute, build-noir-ssa, build-acvm-js, build-noirc-abi] timeout-minutes: 30 permissions: contents: read @@ -567,6 +598,9 @@ jobs: - name: Download noir-execute binary uses: ./.github/actions/download-noir-execute + - name: Download noir-ssa binary + uses: ./.github/actions/download-noir-ssa + - name: Download acvm_js package artifact uses: actions/download-artifact@v4 with: diff --git a/Cargo.lock b/Cargo.lock index 766802845e8..c0a0aecf941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,6 +3563,26 @@ dependencies = [ "prost", ] +[[package]] +name = "noir_ssa_cli" +version = "1.0.0-beta.12" +dependencies = [ + "clap", + "color-eyre", + "const_format", + "iter-extended", + "noir_artifact_cli", + "noir_ast_fuzzer", + "noirc_abi", + "noirc_driver", + "noirc_errors", + "noirc_evaluator", + "tempfile", + "thiserror 1.0.69", + "toml", + "tracing-subscriber", +] + [[package]] name = "noir_ssa_executor" version = "1.0.0-beta.12" diff --git a/Cargo.toml b/Cargo.toml index 75fdd1697fd..8aaee0555bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ # Utility crates "utils/iter-extended", + "tooling/ssa_cli", "tooling/ssa_fuzzer", "tooling/ssa_fuzzer/fuzzer", "tooling/ssa_verification", @@ -52,6 +53,7 @@ default-members = [ "tooling/nargo_cli", "tooling/acvm_cli", "tooling/artifact_cli", + "tooling/ssa_cli", "tooling/profiler", "tooling/inspector", ] diff --git a/compiler/noirc_errors/src/lib.rs b/compiler/noirc_errors/src/lib.rs index a775bbf2e8b..c867ca129e2 100644 --- a/compiler/noirc_errors/src/lib.rs +++ b/compiler/noirc_errors/src/lib.rs @@ -9,11 +9,8 @@ pub use position::{Located, Location, Position, Span, Spanned}; pub use reporter::{CustomDiagnostic, DiagnosticKind}; use std::io::Write; -/// Print the input to stdout, and exit gracefully if `SIGPIPE` is received. -/// Rust ignores `SIGPIPE` by default, converting pipe errors into `ErrorKind::BrokenPipe` -pub fn print_to_stdout(args: std::fmt::Arguments) { - let mut stdout = std::io::stdout(); - if let Err(e) = stdout.write_fmt(args) { +pub fn print_args_or_exit(args: std::fmt::Arguments, mut out: W) { + if let Err(e) = out.write_fmt(args) { if e.kind() == std::io::ErrorKind::BrokenPipe { // Gracefully exit on broken pipe std::process::exit(0); @@ -23,6 +20,18 @@ pub fn print_to_stdout(args: std::fmt::Arguments) { } } +/// Print the input to stdout, and exit gracefully if `SIGPIPE` is received. +/// Rust ignores `SIGPIPE` by default, converting pipe errors into `ErrorKind::BrokenPipe` +pub fn print_to_stdout(args: std::fmt::Arguments) { + print_args_or_exit(args, std::io::stdout()); +} + +/// Print the input to stderr, and exit gracefully if `SIGPIPE` is received. +/// Rust ignores `SIGPIPE` by default, converting pipe errors into `ErrorKind::BrokenPipe` +pub fn print_to_stderr(args: std::fmt::Arguments) { + print_args_or_exit(args, std::io::stderr()); +} + /// Macro to print formatted output to stdout #[macro_export] macro_rules! print_to_stdout { @@ -31,9 +40,26 @@ macro_rules! print_to_stdout { }; } +/// Macro to print formatted output to stdout #[macro_export] macro_rules! println_to_stdout { ($($arg:tt)*) => { noirc_errors::print_to_stdout(format_args!("{}\n", format!($($arg)*))) }; } + +/// Macro to print formatted output to stderr +#[macro_export] +macro_rules! print_to_stderr { + ($($arg:tt)*) => { + noirc_errors::print_to_stderr(format_args!($($arg)*)) + }; +} + +/// Macro to print formatted output to stderr +#[macro_export] +macro_rules! println_to_stderr { + ($($arg:tt)*) => { + noirc_errors::print_to_stderr(format_args!("{}\n", format!($($arg)*))) + }; +} diff --git a/compiler/noirc_evaluator/src/ssa/builder.rs b/compiler/noirc_evaluator/src/ssa/builder.rs index ed562d514c9..90082916331 100644 --- a/compiler/noirc_evaluator/src/ssa/builder.rs +++ b/compiler/noirc_evaluator/src/ssa/builder.rs @@ -190,16 +190,7 @@ impl<'local> SsaBuilder<'local> { self.ssa.normalize_ids(); } - let print_ssa_pass = match &self.ssa_logging { - SsaLogging::None => false, - SsaLogging::All => true, - SsaLogging::Contains(strings) => strings.iter().any(|string| { - let string = string.to_lowercase(); - let string = string.strip_prefix("after ").unwrap_or(&string); - let string = string.strip_suffix(':').unwrap_or(string); - msg.to_lowercase().contains(string) - }), - }; + let print_ssa_pass = self.ssa_logging.matches(msg); if print_ssa_pass { println_to_stdout!("After {msg}:\n{}", self.ssa.print_with(self.files)); diff --git a/compiler/noirc_evaluator/src/ssa/ir/function.rs b/compiler/noirc_evaluator/src/ssa/ir/function.rs index 22d757a91f2..4cf20d3c66e 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/function.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/function.rs @@ -239,6 +239,16 @@ impl Function { pub fn has_data_bus_return_data(&self) -> bool { self.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)) + } + + /// 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 { diff --git a/compiler/noirc_evaluator/src/ssa/mod.rs b/compiler/noirc_evaluator/src/ssa/mod.rs index 9d1c3215be6..53e9f8e73e3 100644 --- a/compiler/noirc_evaluator/src/ssa/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/mod.rs @@ -59,6 +59,22 @@ pub enum SsaLogging { Contains(Vec), } +impl SsaLogging { + /// Check if an SSA pass should be printed. + pub fn matches(&self, msg: &str) -> bool { + match self { + SsaLogging::None => false, + SsaLogging::All => true, + SsaLogging::Contains(strings) => strings.iter().any(|string| { + let string = string.to_lowercase(); + let string = string.strip_prefix("after ").unwrap_or(&string); + let string = string.strip_suffix(':').unwrap_or(string); + msg.to_lowercase().contains(string) + }), + } + } +} + #[derive(Debug, Clone)] pub struct SsaEvaluatorOptions { /// Emit debug information for the intermediate SSA IR diff --git a/examples/ssa_cli/test.sh b/examples/ssa_cli/test.sh new file mode 100755 index 00000000000..72c6c28ef34 --- /dev/null +++ b/examples/ssa_cli/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -eu + +cd $(dirname $0) + +# This file is used for Noir CI and is not required. + +./transform_and_interpret_ssa.sh diff --git a/examples/ssa_cli/test.ssa b/examples/ssa_cli/test.ssa new file mode 100644 index 00000000000..655796dcdce --- /dev/null +++ b/examples/ssa_cli/test.ssa @@ -0,0 +1,22 @@ +acir(inline) fn main f0 { + b0(v0: [[u8; 2]; 2], v1: u1, v2: u32): + jmpif v1 then: b1, else: b2 + b1(): + v3 = array_get v0, index v2 -> [u8; 2] + v4 = array_get v3, index v2 -> u8 + call f1(v4) + jmp b2() + b2(): + return +} +acir(inline) fn println f1 { + b0(v0: u8): + call f2(u1 1, v0) + return +} +brillig(inline) fn print_unconstrained f2 { + b0(v0: u1, v1: u8): + v20 = make_array b"{\"kind\":\"unsignedinteger\",\"width\":8}" + call print(v0, v1, v20, u1 0) + return +} diff --git a/examples/ssa_cli/test.toml b/examples/ssa_cli/test.toml new file mode 100644 index 00000000000..c525ba7c502 --- /dev/null +++ b/examples/ssa_cli/test.toml @@ -0,0 +1,3 @@ +v0 = [[1, 2], [3, 4]] +v1 = true +v2 = 0 diff --git a/examples/ssa_cli/transform_and_interpret_ssa.sh b/examples/ssa_cli/transform_and_interpret_ssa.sh new file mode 100755 index 00000000000..c24feb28f5e --- /dev/null +++ b/examples/ssa_cli/transform_and_interpret_ssa.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -eu + +cd $(dirname $0) + +# Sanity check that we can parse an SSA, pipe it through some transformations +# and interpret it with values from a TOML file. +cat ./test.ssa \ + | noir-ssa transform --ssa-pass "step 1" \ + | noir-ssa transform --ssa-pass "Dead Instruction Elimination" \ + | noir-ssa interpret --input-path ./test.toml \ No newline at end of file diff --git a/justfile b/justfile index 258690022eb..212fdc8cf18 100644 --- a/justfile +++ b/justfile @@ -4,7 +4,7 @@ ci := if env("CI", "") == "true" { "1" } else { "0" -} +} use-cross := env("JUST_USE_CROSS", "") # target information @@ -113,7 +113,7 @@ fuzz-nightly: install-rust-tools # Checks if there are any pending insta.rs snapshots and errors if any exist. check-pending-snapshots: #!/usr/bin/env bash - snapshots=$(find . -name *.snap.new) + snapshots=$(find . -name *.snap.new) if [[ -n "$snapshots" ]]; then \ echo "Found pending snapshots:" echo "" diff --git a/tooling/artifact_cli/src/bin/execute.rs b/tooling/artifact_cli/src/bin/execute.rs index b4de9552bfd..6b779781852 100644 --- a/tooling/artifact_cli/src/bin/execute.rs +++ b/tooling/artifact_cli/src/bin/execute.rs @@ -40,7 +40,7 @@ fn main() { .init(); if let Err(e) = start_cli() { - eprintln!("{e:?}"); + eprintln!("{e:#}"); std::process::exit(1); } } diff --git a/tooling/artifact_cli/src/commands/mod.rs b/tooling/artifact_cli/src/commands/mod.rs index 9049b3695b7..45aba6ee993 100644 --- a/tooling/artifact_cli/src/commands/mod.rs +++ b/tooling/artifact_cli/src/commands/mod.rs @@ -8,7 +8,7 @@ pub mod execute_cmd; /// Parses a path and turns it into an absolute one by joining to the current directory, /// then normalizes it. -fn parse_and_normalize_path(path: &str) -> eyre::Result { +pub fn parse_and_normalize_path(path: &str) -> eyre::Result { use fm::NormalizePath; let mut path: PathBuf = path.parse().map_err(|e| eyre!("failed to parse path: {e}"))?; if !path.is_absolute() { diff --git a/tooling/nargo_cli/src/cli/mod.rs b/tooling/nargo_cli/src/cli/mod.rs index b49881a6dac..a567c907fd4 100644 --- a/tooling/nargo_cli/src/cli/mod.rs +++ b/tooling/nargo_cli/src/cli/mod.rs @@ -5,6 +5,7 @@ use nargo_toml::{ ManifestError, NargoToml, PackageConfig, PackageMetadata, PackageSelection, get_package_manifest, resolve_workspace_from_fixed_toml, resolve_workspace_from_toml, }; +use noir_artifact_cli::commands::parse_and_normalize_path; use noirc_driver::{CrateName, NOIR_ARTIFACT_VERSION_STRING}; use std::{ collections::BTreeMap, @@ -60,11 +61,11 @@ struct NargoCli { #[derive(Args, Clone, Debug)] pub struct NargoConfig { // REMINDER: Also change this flag in the LSP test lens if renamed - #[arg(long, hide = true, global = true, default_value = "./", value_parser = parse_path)] + #[arg(long, hide = true, global = true, default_value = "./", value_parser = parse_and_normalize_path)] program_dir: PathBuf, /// Override the default target directory. - #[arg(long, hide = true, global = true, value_parser = parse_path)] + #[arg(long, hide = true, global = true, value_parser = parse_and_normalize_path)] target_dir: Option, } @@ -297,16 +298,6 @@ fn lock_workspace( Ok(locks) } -/// Parses a path and turns it into an absolute one by joining to the current directory. -fn parse_path(path: &str) -> Result { - use fm::NormalizePath; - let mut path: PathBuf = path.parse().map_err(|e| format!("failed to parse path: {e}"))?; - if !path.is_absolute() { - path = std::env::current_dir().unwrap().join(path).normalize(); - } - Ok(path) -} - #[cfg(test)] mod tests { use super::NargoCli; diff --git a/tooling/ssa_cli/Cargo.toml b/tooling/ssa_cli/Cargo.toml new file mode 100644 index 00000000000..898fcd7896b --- /dev/null +++ b/tooling/ssa_cli/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "noir_ssa_cli" +description = "CLI tool to work with SSA independently of any Noir program" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "noir-ssa" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +clap.workspace = true +color-eyre.workspace = true +const_format.workspace = true +thiserror.workspace = true +toml.workspace = true +tempfile.workspace = true +tracing-subscriber.workspace = true + +# Noir repo dependencies +noir_artifact_cli.workspace = true +noir_ast_fuzzer.workspace = true +noirc_abi.workspace = true +noirc_driver.workspace = true +noirc_evaluator.workspace = true +noirc_errors.workspace = true +iter-extended.workspace = true diff --git a/tooling/ssa_cli/src/cli/interpret_cmd.rs b/tooling/ssa_cli/src/cli/interpret_cmd.rs new file mode 100644 index 00000000000..6672ffd482d --- /dev/null +++ b/tooling/ssa_cli/src/cli/interpret_cmd.rs @@ -0,0 +1,189 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use clap::Args; +use color_eyre::eyre::{self, Context, bail}; +use iter_extended::vecmap; +use noir_artifact_cli::{commands::parse_and_normalize_path, fs::artifact::write_to_file}; +use noirc_abi::{ + Abi, AbiParameter, AbiReturnType, AbiType, AbiVisibility, InputMap, Sign, + input_parser::InputValue, +}; +use noirc_errors::println_to_stdout; +use noirc_evaluator::ssa::{ + interpreter::InterpreterOptions, + ir::types::{NumericType, Type}, + ssa_gen::Ssa, +}; +use tempfile::NamedTempFile; + +const TOML_LINE_SEP: char = ';'; + +/// Parse the input SSA and it arguments, run the SSA interpreter, +/// then write the return values to stdout. +#[derive(Debug, Clone, Args)] +pub(super) struct InterpretCommand { + /// Path to the input arguments to the SSA interpreter. + /// + /// Expected to be in TOML format or JSON, similar to `Prover.toml`. + /// + /// If empty, we assume the SSA has no arguments. + #[clap(long, short, value_parser = parse_and_normalize_path, conflicts_with = "input_json", conflicts_with = "input_toml")] + pub input_path: Option, + + /// Verbatim inputs in JSON format. + #[clap(long, conflicts_with = "input_path", conflicts_with = "input_toml")] + pub input_json: Option, + + /// Verbatim inputs in TOML format. + /// + /// Use ';' to separate what would normally be multiple lines. + #[clap(long, conflicts_with = "input_path", conflicts_with = "input_json")] + pub input_toml: Option, + + /// Turn on tracing in the SSA interpreter. + #[clap(long, default_value_t = false)] + pub trace: bool, +} + +pub(super) fn run(args: InterpretCommand, ssa: Ssa) -> eyre::Result<()> { + // Construct an ABI, which we can then use to parse input values. + let abi = abi_from_ssa(&ssa); + + let options = InterpreterOptions { trace: args.trace, ..Default::default() }; + + let (input_map, return_value) = read_inputs_and_return(&abi, &args)?; + let ssa_args = noir_ast_fuzzer::input_values_to_ssa(&abi, &input_map); + + let ssa_return = + if let (Some(return_type), Some(return_value)) = (&abi.return_type, return_value) { + Some(noir_ast_fuzzer::input_value_to_ssa(&return_type.abi_type, &return_value)) + } else { + None + }; + + let result = ssa.interpret_with_options(ssa_args, options, std::io::stdout()); + + // Mimicking the way `nargo interpret` presents its results. + match &result { + Ok(value) => { + let value_as_string = vecmap(value, ToString::to_string).join(", "); + println_to_stdout!("--- Interpreter result:\nOk({value_as_string})\n---"); + } + Err(err) => { + println_to_stdout!("--- Interpreter result:\nErr({err})\n---"); + } + } + + if let Some(return_value) = ssa_return { + let return_value_as_string = vecmap(&return_value, ToString::to_string).join(", "); + let Ok(result) = result else { + bail!( + "Interpreter produced an unexpected error.\nExpected result: {return_value_as_string}" + ); + }; + if return_value != result { + let result_as_string = vecmap(&result, ToString::to_string).join(", "); + bail!( + "Interpreter produced an unexpected result.\nExpected result: {return_value_as_string}\nActual result: {result_as_string}" + ) + } + } + + Ok(()) +} + +/// Derive an ABI description from the SSA parameters. +fn abi_from_ssa(ssa: &Ssa) -> Abi { + let main = &ssa.functions[&ssa.main_id]; + + // We ignore visibility and treat everything as public, because visibility + // is only available in the Program with the monomorphized AST from which + // we normally generate the SSA. The SSA itself doesn't carry information + // about the databus, for example. + let visibility = AbiVisibility::Public; + + let parameters = main + .parameter_types() + .iter() + .enumerate() + .map(|(i, typ)| AbiParameter { + name: format!("v{i}"), + typ: abi_type_from_ssa(typ), + visibility, + }) + .collect(); + + let return_type = main + .return_types() + .filter(|ts| !ts.is_empty()) + .map(|types| AbiReturnType { abi_type: abi_type_from_multi_ssa(&types), visibility }); + + Abi { parameters, return_type, error_types: Default::default() } +} + +/// Create an ABI type from multiple SSA types, for example when multiple values are returned, or appear in arrays. +fn abi_type_from_multi_ssa(types: &[Type]) -> AbiType { + match types.len() { + 0 => unreachable!("cannot construct ABI type from 0 types"), + 1 => abi_type_from_ssa(&types[0]), + _ => AbiType::Tuple { fields: vecmap(types, abi_type_from_ssa) }, + } +} + +/// Create an ABI type from a single SSA type. +fn abi_type_from_ssa(typ: &Type) -> AbiType { + match typ { + Type::Numeric(numeric_type) => match numeric_type { + NumericType::NativeField => AbiType::Field, + NumericType::Unsigned { bit_size: 1 } => AbiType::Boolean, + NumericType::Unsigned { bit_size } => { + AbiType::Integer { sign: Sign::Unsigned, width: *bit_size } + } + NumericType::Signed { bit_size } => { + AbiType::Integer { sign: Sign::Signed, width: *bit_size } + } + }, + Type::Array(items, length) => { + AbiType::Array { length: *length, typ: Box::new(abi_type_from_multi_ssa(items)) } + } + Type::Reference(_) => unreachable!("refs do not appear in SSA ABI"), + Type::Function => unreachable!("functions do not appear in SSA ABI"), + Type::Slice(_) => unreachable!("slices do not appear in SSA ABI"), + } +} + +fn write_to_temp_file(content: &str, extension: &str) -> eyre::Result { + let tmp = NamedTempFile::with_suffix(format!("ssa.input.{extension}"))?; + + write_to_file(content.as_bytes(), tmp.path()).wrap_err_with(|| { + format!("failed to write {extension} to temp file at {}", tmp.path().to_string_lossy()) + })?; + + Ok(tmp) +} + +fn read_inputs_and_return( + abi: &Abi, + args: &InterpretCommand, +) -> eyre::Result<(InputMap, Option)> { + let (input_path, _guard) = if let Some(ref json) = args.input_json { + let tmp = write_to_temp_file(json, "json")?; + (Some(tmp.path().to_path_buf()), Some(tmp)) + } else if let Some(ref toml) = args.input_toml { + // Split along the line separator and rejoin into a file. + let lines = toml.split(TOML_LINE_SEP).map(|s| s.trim_start()).collect::>(); + let toml = lines.join("\n"); + let tmp = write_to_temp_file(&toml, "toml")?; + (Some(tmp.path().to_path_buf()), Some(tmp)) + } else { + (args.input_path.clone(), None) + }; + + let (input_map, return_value) = match input_path { + Some(path) => noir_artifact_cli::fs::inputs::read_inputs_from_file(&path, abi) + .wrap_err_with(|| format!("failed to read inputs from {}", path.to_string_lossy()))?, + None => (BTreeMap::default(), None), + }; + + Ok((input_map, return_value)) +} diff --git a/tooling/ssa_cli/src/cli/mod.rs b/tooling/ssa_cli/src/cli/mod.rs new file mode 100644 index 00000000000..26d005225aa --- /dev/null +++ b/tooling/ssa_cli/src/cli/mod.rs @@ -0,0 +1,126 @@ +use std::io::{IsTerminal, Read}; +use std::path::PathBuf; + +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 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; + +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +static VERSION_STRING: &str = formatcp!("version = {}\n", PKG_VERSION,); + +#[derive(Parser, Debug)] +#[command(name="noir-ssa", author, version=VERSION_STRING, about, long_about = None)] +struct SsaCli { + #[command(subcommand)] + command: SsaCommand, + + #[command(flatten)] + args: SsaArgs, +} + +/// Common SSA command parameters. +#[derive(Args, Clone, Debug)] +struct SsaArgs { + /// Path to the source SSA. + /// + /// If empty, the SSA will be read from stdin. + #[clap(long, short, global = true, value_parser = parse_and_normalize_path)] + source_path: Option, + + /// Turn off validation of the source SSA. + /// + /// This can be used to test how invalid input behaves. + #[clap(long, global = true, default_value_t = false)] + no_validate: bool, +} + +#[derive(Subcommand, Clone, Debug)] +enum SsaCommand { + /// List the SSA passes we can apply. + List, + /// Parse and (optionally) validate the SSA. + Check, + Interpret(interpret_cmd::InterpretCommand), + Transform(transform_cmd::TransformCommand), +} + +pub(crate) fn start_cli() -> eyre::Result<()> { + let SsaCli { command, args } = SsaCli::parse(); + + let ssa = || read_source(args.source_path).and_then(|src| parse_ssa(&src, !args.no_validate)); + + match command { + SsaCommand::List => { + // This command doesn't actually use the common parameters, but we could potentially + // read the source, and figure out which passes we can apply to it based on its state. + let options = CompileOptions::default().as_ssa_options(Default::default()); + for (msg, _) in ssa_passes(&options) { + println_to_stdout!("{msg}"); + } + } + SsaCommand::Check => { + let _ = ssa()?; + } + SsaCommand::Interpret(cmd) => interpret_cmd::run(cmd, ssa()?)?, + SsaCommand::Transform(cmd) => transform_cmd::run(cmd, ssa()?)?, + } + + Ok(()) +} + +/// Read the SSA from a file or stdin. +fn read_source(path: Option) -> eyre::Result { + if let Some(path) = path { + std::fs::read_to_string(&path) + .wrap_err_with(|| format!("failed to read the SSA from {}", path.to_string_lossy())) + } else { + let mut src = String::new(); + let stdin = std::io::stdin(); + + if stdin.is_terminal() { + // If we are in terminal mode, we can type in the SSA, but that's unlikely + // what we wanted to achieve, and I'm not sure how to even signal EOF. + bail!("The CLI is in terminal mode. Expected to read the SSA from a pipe.") + } + + let mut handle = stdin.lock(); + handle.read_to_string(&mut src)?; + Ok(src) + } +} + +/// Parse the SSA. +/// +/// If parsing fails, print errors to `stderr` and return a failure. +/// +/// If validation is enabled, any semantic error causes a panic. +fn parse_ssa(src: &str, validate: bool) -> eyre::Result { + let result = if validate { Ssa::from_str(src) } else { Ssa::from_str_no_validation(src) }; + match result { + Ok(ssa) => Ok(ssa), + Err(source_with_errors) => { + println_to_stderr!("{source_with_errors:?}"); + bail!("Failed to parse the SSA.") + } + } +} + +/// List of the SSA passes in the primary pipeline, enriched with their "step" +/// count so we can use unambiguous naming in filtering. +fn ssa_passes(options: &SsaEvaluatorOptions) -> Vec<(String, SsaPass<'_>)> { + primary_passes(options) + .into_iter() + .enumerate() + .map(|(i, pass)| { + let msg = format!("{} (step {})", pass.msg(), i + 1); + (msg, pass) + }) + .collect() +} diff --git a/tooling/ssa_cli/src/cli/transform_cmd.rs b/tooling/ssa_cli/src/cli/transform_cmd.rs new file mode 100644 index 00000000000..792c835e2b8 --- /dev/null +++ b/tooling/ssa_cli/src/cli/transform_cmd.rs @@ -0,0 +1,93 @@ +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 noirc_driver::CompileOptions; +use noirc_errors::{println_to_stderr, println_to_stdout}; +use noirc_evaluator::ssa::{SsaLogging, SsaPass, ssa_gen::Ssa}; + +/// Parse the input SSA, run some SSA passes on it, then write the output SSA. +#[derive(Debug, Clone, Args)] +pub(super) struct TransformCommand { + /// Path to write the output SSA to. + /// + /// If empty, the SSA will be written to stdout. + #[clap(long, short, value_parser = parse_and_normalize_path)] + pub output_path: Option, + + /// Name of the SSA pass(es) to apply. + /// + /// The names are used to look up the first matching pass in the default pipeline, + /// and apply them in the order of appearance (potentially multiple times). + /// + /// If no pass is specified, it applies all passes in the default pipeline. + #[clap(long, short = 'p')] + pub ssa_pass: Vec, + + #[clap(flatten)] + pub(super) compile_options: CompileOptions, +} + +pub(super) fn run(args: TransformCommand, mut ssa: Ssa) -> eyre::Result<()> { + let options = args.compile_options.as_ssa_options(PathBuf::default()); + let passes = super::ssa_passes(&options); + + let mut msg = "Initial"; + + if args.ssa_pass.is_empty() { + for pass in &passes { + (ssa, msg) = run_pass(ssa, pass, &options.ssa_logging)?; + } + } else { + for name in args.ssa_pass { + let Some(pass) = + passes.iter().find(|(msg, _)| msg.to_lowercase().contains(&name.to_lowercase())) + else { + bail!( + "cannot find SSA pass (use the `list` command to see available passes): '{}'", + name + ); + }; + (ssa, msg) = run_pass(ssa, pass, &options.ssa_logging)?; + } + } + + // 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(()) +} + +/// Run an SSA pass, and optionally print to `stderr`, distinct from `stdout` where the final result goes. +fn run_pass<'a>( + ssa: Ssa, + (msg, pass): &'a (String, SsaPass<'_>), + ssa_logging: &SsaLogging, +) -> eyre::Result<(Ssa, &'a String)> { + let mut ssa = pass.run(ssa).wrap_err_with(|| format!("failed to run pass '{msg}'"))?; + + if ssa_logging.matches(msg) { + println_to_stderr!("{}", format_ssa(&mut ssa, msg, false)); + } + + Ok((ssa, msg)) +} + +/// Render the SSA to a string. +fn format_ssa(ssa: &mut Ssa, msg: &str, parsable: bool) -> String { + // Differentiate between log output and the final one by whether the "After" is commented out. + let prefix = if parsable { "// " } else { "" }; + + // Make sure variable IDs are consistent. + ssa.normalize_ids(); + + format!("{prefix}After {msg}:\n{ssa}") +} diff --git a/tooling/ssa_cli/src/main.rs b/tooling/ssa_cli/src/main.rs new file mode 100644 index 00000000000..a1609729e37 --- /dev/null +++ b/tooling/ssa_cli/src/main.rs @@ -0,0 +1,19 @@ +#![forbid(unsafe_code)] + +use noirc_errors::println_to_stderr; +use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; +mod cli; + +fn main() { + tracing_subscriber::fmt() + .with_span_events(FmtSpan::ACTIVE) + .with_writer(std::io::stderr) + .with_ansi(true) + .with_env_filter(EnvFilter::from_env("NOIR_LOG")) + .init(); + + if let Err(e) = cli::start_cli() { + println_to_stderr!("{e:#}"); + std::process::exit(1); + } +}