diff --git a/.gitignore b/.gitignore index 10988465b29..a79bac63db7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ pkg/ *.tr *.pk *.vk -**/Verifier.toml \ No newline at end of file +**/Verifier.toml +*.trace \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1fa7a1df7bb..d80964b28a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,8 +17,7 @@ dependencies = [ [[package]] name = "acir" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744c21590f6da95914f1d83f247dba49d4f287beb725bfbcbc54eaf823484b1a" +source = "git+https://github.com/noir-lang/acvm?branch=mv/traces2#57ad1efdba4b8bcd887bb6857e73b3d64642b021" dependencies = [ "acir_field 0.6.0", "flate2", @@ -46,8 +45,7 @@ dependencies = [ [[package]] name = "acir_field" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081a09b71facabddd411bfb8459c0f4d9350c39707680da6c04debb3db9de503" +source = "git+https://github.com/noir-lang/acvm?branch=mv/traces2#57ad1efdba4b8bcd887bb6857e73b3d64642b021" dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.1", @@ -79,8 +77,7 @@ dependencies = [ [[package]] name = "acvm" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73876b7d5d23d0826ea4c9150b4a62af0b20a6937a79472ee0ceda30868aa207" +source = "git+https://github.com/noir-lang/acvm?branch=mv/traces2#57ad1efdba4b8bcd887bb6857e73b3d64642b021" dependencies = [ "acir 0.6.0", "acvm_stdlib 0.6.0", @@ -106,8 +103,7 @@ dependencies = [ [[package]] name = "acvm_stdlib" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2259c21b06db652e41bddb1deb22da8d303746a0e8bf877983c7d83f71943b8" +source = "git+https://github.com/noir-lang/acvm?branch=mv/traces2#57ad1efdba4b8bcd887bb6857e73b3d64642b021" dependencies = [ "acir 0.6.0", ] @@ -510,7 +506,7 @@ dependencies = [ [[package]] name = "barretenberg_static_lib" version = "0.1.0" -source = "git+https://github.com/noir-lang/aztec_backend?rev=2cb523d2ab95249157b22e198d9dcd6841c3eed8#2cb523d2ab95249157b22e198d9dcd6841c3eed8" +source = "git+https://github.com/noir-lang/aztec_backend?branch=mv/trace#2176e3df1fb1aef0b9568d64a973861a320613b5" dependencies = [ "barretenberg_wrapper", "common", @@ -519,7 +515,7 @@ dependencies = [ [[package]] name = "barretenberg_wasm" version = "0.1.0" -source = "git+https://github.com/noir-lang/aztec_backend?rev=2cb523d2ab95249157b22e198d9dcd6841c3eed8#2cb523d2ab95249157b22e198d9dcd6841c3eed8" +source = "git+https://github.com/noir-lang/aztec_backend?branch=mv/trace#2176e3df1fb1aef0b9568d64a973861a320613b5" dependencies = [ "common", "wasmer", @@ -853,7 +849,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/noir-lang/aztec_backend?rev=2cb523d2ab95249157b22e198d9dcd6841c3eed8#2cb523d2ab95249157b22e198d9dcd6841c3eed8" +source = "git+https://github.com/noir-lang/aztec_backend?branch=mv/trace#2176e3df1fb1aef0b9568d64a973861a320613b5" dependencies = [ "acvm 0.6.0", "blake2", @@ -3012,9 +3008,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.155" +version = "1.0.156" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" +checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" dependencies = [ "serde_derive", ] @@ -3030,9 +3026,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.155" +version = "1.0.156" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630" +checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 56931b3e685..db8c79bb59c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ edition = "2021" rust-version = "1.66" [workspace.dependencies] -acvm = "0.6.0" +acvm = { features = ["bn254"], git = "https://github.com/noir-lang/acvm", branch = "mv/traces2" } arena = { path = "crates/arena" } fm = { path = "crates/fm" } iter-extended = { path = "crates/iter-extended" } diff --git a/crates/nargo/Cargo.toml b/crates/nargo/Cargo.toml index 3cdaf585cf4..621e39b192a 100644 --- a/crates/nargo/Cargo.toml +++ b/crates/nargo/Cargo.toml @@ -35,8 +35,8 @@ color-eyre = "0.6.2" # Backends -aztec_backend = { optional = true, package = "barretenberg_static_lib", git = "https://github.com/noir-lang/aztec_backend", rev = "2cb523d2ab95249157b22e198d9dcd6841c3eed8" } -aztec_wasm_backend = { optional = true, package = "barretenberg_wasm", git = "https://github.com/noir-lang/aztec_backend", rev = "2cb523d2ab95249157b22e198d9dcd6841c3eed8" } +aztec_backend = { optional = true, package = "barretenberg_static_lib", git = "https://github.com/noir-lang/aztec_backend", branch = "mv/trace" } +aztec_wasm_backend = { optional = true, package = "barretenberg_wasm", git = "https://github.com/noir-lang/aztec_backend", branch = "mv/trace" } marlin_arkworks_backend = { optional = true, git = "https://github.com/noir-lang/marlin_arkworks_backend", rev = "144378edad821bfaa52bf2cacca8ecc87514a4fc" } [features] diff --git a/crates/nargo/src/cli/execute_cmd.rs b/crates/nargo/src/cli/execute_cmd.rs index de797d9973d..bd14a7aef14 100644 --- a/crates/nargo/src/cli/execute_cmd.rs +++ b/crates/nargo/src/cli/execute_cmd.rs @@ -1,11 +1,13 @@ use std::path::Path; +use acvm::acir::circuit::directives::SolvedLog; use acvm::PartialWitnessGenerator; use clap::Args; use noirc_abi::input_parser::{Format, InputValue}; use noirc_abi::{InputMap, WitnessMap}; use noirc_driver::{CompileOptions, CompiledProgram}; +use super::fs::logs::handle_logs; use super::fs::{inputs::read_inputs_from_file, witness::save_witness_to_dir}; use super::NargoConfig; use crate::{ @@ -53,7 +55,9 @@ fn execute_with_path( let (inputs_map, _) = read_inputs_from_file(program_dir, PROVER_INPUT_FILE, Format::Toml, &compiled_program.abi)?; - let solved_witness = execute_program(&compiled_program, &inputs_map)?; + let (solved_witness, logs) = execute_program(&compiled_program, &inputs_map)?; + + handle_logs(logs, compile_options.debug_file.clone(), program_dir)?; let public_abi = compiled_program.abi.public_abi(); let (_, return_value) = public_abi.decode(&solved_witness)?; @@ -64,11 +68,13 @@ fn execute_with_path( pub(crate) fn execute_program( compiled_program: &CompiledProgram, inputs_map: &InputMap, -) -> Result { +) -> Result<(WitnessMap, Vec), CliError> { let mut solved_witness = compiled_program.abi.encode(inputs_map, None)?; + let mut logs = Vec::new(); + let backend = crate::backends::ConcreteBackend; - backend.solve(&mut solved_witness, compiled_program.circuit.opcodes.clone())?; + backend.solve(&mut solved_witness, compiled_program.circuit.opcodes.clone(), &mut logs)?; - Ok(solved_witness) + Ok((solved_witness, logs)) } diff --git a/crates/nargo/src/cli/fs/logs.rs b/crates/nargo/src/cli/fs/logs.rs new file mode 100644 index 00000000000..6f7894e4240 --- /dev/null +++ b/crates/nargo/src/cli/fs/logs.rs @@ -0,0 +1,67 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use super::{create_named_dir, write_to_file}; +use crate::CliError; +use acvm::acir::circuit::directives::{SolvedLog, SolvedLogOutputInfo}; +use iter_extended::vecmap; +use noirc_abi::format_field_string; + +pub(crate) fn handle_logs>( + logs: Vec, + debug_file_name: Option, + path: P, +) -> Result<(), CliError> { + let mut traces: HashMap = HashMap::new(); + for log in logs { + let output_string = match log.output_info { + SolvedLogOutputInfo::FinalizedOutput(output_string) => output_string.clone(), + SolvedLogOutputInfo::WitnessValues(field_elements) => { + if field_elements.len() == 1 { + let element = &field_elements[0]; + format_field_string(*element) + } else { + // If multiple field elements are fetched for a solved log, + // it assumed that an array is meant to be printed to standard output + // + // Collect all field element values corresponding to the given witness indices (whose values were solved during PWG) + // and convert them to hex strings. + let elements_as_hex = vecmap(field_elements, format_field_string); + let comma_separated_elements = elements_as_hex.join(", "); + + "[".to_owned() + &comma_separated_elements + "]" + } + } + }; + if let Some(trace_label) = log.trace_label { + // We can have multiples traces with the same label. + // Below we group traces into a singular list containing all traces with a specific label + if let Some(val) = traces.get_mut(&trace_label) { + // If there are multiples traces with the same label we want to maintain the order of the first insertion. + // Thus, we alter the value in the traces map rather than inserting a new value + *val = val.clone() + ", " + &output_string; + } else { + traces.insert(trace_label, output_string); + }; + } else { + println!("{output_string}") + } + } + + match debug_file_name { + Some(file_name) if !traces.is_empty() => { + let mut debug_dir = PathBuf::from(path.as_ref()); + debug_dir.push("debug"); + let mut trace_path = create_named_dir(debug_dir.as_ref(), "debug"); + trace_path.push(file_name); + trace_path.set_extension("trace"); + + write_to_file(&serde_json::to_vec(&traces).unwrap(), &trace_path); + } + _ => (), + } + + Ok(()) +} diff --git a/crates/nargo/src/cli/fs/mod.rs b/crates/nargo/src/cli/fs/mod.rs index b1c859745b6..c83b31288dc 100644 --- a/crates/nargo/src/cli/fs/mod.rs +++ b/crates/nargo/src/cli/fs/mod.rs @@ -8,6 +8,7 @@ use crate::errors::CliError; pub(super) mod inputs; pub(super) mod keys; +pub(super) mod logs; pub(super) mod program; pub(super) mod proof; pub(super) mod witness; diff --git a/crates/nargo/src/cli/mod.rs b/crates/nargo/src/cli/mod.rs index 85489539a66..ddfdaac85a1 100644 --- a/crates/nargo/src/cli/mod.rs +++ b/crates/nargo/src/cli/mod.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use color_eyre::eyre; -mod fs; +pub mod fs; mod check_cmd; mod codegen_verifier_cmd; @@ -83,7 +83,8 @@ pub fn prove_and_verify(proof_name: &str, prg_dir: &Path, show_ssa: bool) -> boo use tempdir::TempDir; let tmp_dir = TempDir::new("p_and_v_tests").unwrap(); - let compile_options = CompileOptions { show_ssa, allow_warnings: false, show_output: false }; + let compile_options = + CompileOptions { show_ssa, allow_warnings: false, show_output: false, debug_file: None }; match prove_cmd::prove_with_path( Some(proof_name.to_owned()), diff --git a/crates/nargo/src/cli/prove_cmd.rs b/crates/nargo/src/cli/prove_cmd.rs index 02bad3a301d..6e7d928236a 100644 --- a/crates/nargo/src/cli/prove_cmd.rs +++ b/crates/nargo/src/cli/prove_cmd.rs @@ -5,6 +5,7 @@ use clap::Args; use noirc_abi::input_parser::Format; use noirc_driver::CompileOptions; +use super::fs::logs::handle_logs; use super::fs::{ inputs::{read_inputs_from_file, write_inputs_to_file}, keys::fetch_pk_and_vk, @@ -94,7 +95,9 @@ pub(crate) fn prove_with_path>( &compiled_program.abi, )?; - let solved_witness = execute_program(&compiled_program, &inputs_map)?; + let (solved_witness, logs) = execute_program(&compiled_program, &inputs_map)?; + + handle_logs(logs, compile_options.debug_file.clone(), &program_dir)?; // Write public inputs into Verifier.toml let public_abi = compiled_program.abi.clone().public_abi(); diff --git a/crates/nargo/src/cli/test_cmd.rs b/crates/nargo/src/cli/test_cmd.rs index abc8cde9dc2..0d8203a811e 100644 --- a/crates/nargo/src/cli/test_cmd.rs +++ b/crates/nargo/src/cli/test_cmd.rs @@ -8,7 +8,7 @@ use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use crate::{errors::CliError, resolver::Resolver}; -use super::{add_std_lib, NargoConfig}; +use super::{add_std_lib, fs::logs::handle_logs, NargoConfig}; /// Run the tests for this program #[derive(Debug, Clone, Args)] @@ -50,7 +50,7 @@ fn run_tests( writeln!(writer, "Testing {test_name}...").expect("Failed to write to stdout"); writer.flush().ok(); - match run_test(test_name, test_function, &driver, compile_options) { + match run_test(test_name, test_function, &driver, compile_options, program_dir) { Ok(_) => { writer.set_color(ColorSpec::new().set_fg(Some(Color::Green))).ok(); writeln!(writer, "ok").ok(); @@ -78,6 +78,7 @@ fn run_test( main: FuncId, driver: &Driver, config: &CompileOptions, + program_dir: &Path, ) -> Result<(), CliError> { let backend = crate::backends::ConcreteBackend; @@ -86,10 +87,15 @@ fn run_test( .map_err(|_| CliError::Generic(format!("Test '{test_name}' failed to compile")))?; let mut solved_witness = BTreeMap::new(); + let mut logs = Vec::new(); + let test_debug_file_name = + config.debug_file.as_ref().map(|debug_file| format!("{debug_file}-{test_name}")); // Run the backend to ensure the PWG evaluates functions like std::hash::pedersen, // otherwise constraints involving these expressions will not error. - if let Err(error) = backend.solve(&mut solved_witness, program.circuit.opcodes) { + if let Err(error) = backend.solve(&mut solved_witness, program.circuit.opcodes, &mut logs) { + handle_logs(logs, test_debug_file_name, program_dir)?; + let writer = StandardStream::stderr(ColorChoice::Always); let mut writer = writer.lock(); writer.set_color(ColorSpec::new().set_fg(Some(Color::Red))).ok(); @@ -97,5 +103,9 @@ fn run_test( writer.reset().ok(); return Err(error.into()); } + if config.show_output { + handle_logs(logs, test_debug_file_name, program_dir)?; + } + Ok(()) } diff --git a/crates/nargo/tests/test_data/strings/src/main.nr b/crates/nargo/tests/test_data/strings/src/main.nr index ca0d1691f86..c5985bc9181 100644 --- a/crates/nargo/tests/test_data/strings/src/main.nr +++ b/crates/nargo/tests/test_data/strings/src/main.nr @@ -10,6 +10,9 @@ fn main(message : pub str<11>, y : Field, hex_as_string : str<4>, hex_as_field : std::println(10); std::println(z); // x * 5 in println not yet supported + + std::trace("zz", z); + std::println(x); let array = [1, 2, 3, 5, 8]; @@ -21,7 +24,11 @@ fn main(message : pub str<11>, y : Field, hex_as_string : str<4>, hex_as_field : let hash = std::hash::pedersen([x]); std::println(hash); - + + std::trace("hello hash", hash); + std::trace("hello hash", hex_as_field); + std::trace("hello hash", "goodbye hash"); + constrain hex_as_string == "0x41"; // constrain hex_as_string != 0x41; This will fail with a type mismatch between str[4] and Field constrain hex_as_field == 0x41; @@ -45,8 +52,12 @@ fn test_prints_array() { std::println(array); + std::trace("yoyo", array); + let hash = std::hash::pedersen(array); std::println(hash); + + std::trace("yoyo", hash); } struct Test { diff --git a/crates/noirc_abi/src/lib.rs b/crates/noirc_abi/src/lib.rs index 1a5293b160c..1822afc5df1 100644 --- a/crates/noirc_abi/src/lib.rs +++ b/crates/noirc_abi/src/lib.rs @@ -396,6 +396,18 @@ pub fn decode_string_value(field_elements: &[FieldElement]) -> String { final_string.to_owned() } +/// This trims any leading zeroes. +/// A singular '0' will be prepended as well if the trimmed string has an odd length. +/// A hex string's length needs to be even to decode into bytes, as two digits correspond to +/// one byte. +pub fn format_field_string(field: FieldElement) -> String { + let mut trimmed_field = field.to_hex().trim_start_matches('0').to_owned(); + if trimmed_field.len() % 2 != 0 { + trimmed_field = "0".to_owned() + &trimmed_field + }; + "0x".to_owned() + &trimmed_field +} + #[cfg(test)] mod test { use std::collections::BTreeMap; diff --git a/crates/noirc_driver/src/lib.rs b/crates/noirc_driver/src/lib.rs index e49e769e3af..00c995c16d0 100644 --- a/crates/noirc_driver/src/lib.rs +++ b/crates/noirc_driver/src/lib.rs @@ -37,11 +37,15 @@ pub struct CompileOptions { /// Display output of `println` statements during tests #[arg(long)] pub show_output: bool, + + /// File name to write debug output of `trace` statements + #[arg(long)] + pub debug_file: Option, } impl Default for CompileOptions { fn default() -> Self { - Self { show_ssa: false, allow_warnings: false, show_output: true } + Self { show_ssa: false, allow_warnings: false, show_output: true, debug_file: None } } } @@ -214,13 +218,7 @@ impl Driver { let np_language = self.language.clone(); let blackbox_supported = acvm::default_is_black_box_supported(np_language.clone()); - match create_circuit( - program, - np_language, - blackbox_supported, - options.show_ssa, - options.show_output, - ) { + match create_circuit(program, np_language, blackbox_supported, options.show_ssa) { Ok((circuit, abi)) => Ok(CompiledProgram { circuit, abi }), Err(err) => { // The FileId here will be the file id of the file with the main file diff --git a/crates/noirc_evaluator/src/lib.rs b/crates/noirc_evaluator/src/lib.rs index 99db5fe7766..64fd2350d0e 100644 --- a/crates/noirc_evaluator/src/lib.rs +++ b/crates/noirc_evaluator/src/lib.rs @@ -55,12 +55,11 @@ pub fn create_circuit( np_language: Language, is_blackbox_supported: IsBlackBoxSupported, enable_logging: bool, - show_output: bool, ) -> Result<(Circuit, Abi), RuntimeError> { let mut evaluator = Evaluator::default(); // First evaluate the main function - evaluator.evaluate_main_alt(program.clone(), enable_logging, show_output)?; + evaluator.evaluate_main_alt(program.clone(), enable_logging)?; let witness_index = evaluator.current_witness_index(); @@ -119,7 +118,6 @@ impl Evaluator { &mut self, program: Program, enable_logging: bool, - show_output: bool, ) -> Result<(), RuntimeError> { let mut ir_gen = IrGenerator::new(program); self.parse_abi_alt(&mut ir_gen); @@ -128,7 +126,7 @@ impl Evaluator { ir_gen.ssa_gen_main()?; //Generates ACIR representation: - ir_gen.context.ir_to_acir(self, enable_logging, show_output)?; + ir_gen.context.ir_to_acir(self, enable_logging)?; Ok(()) } diff --git a/crates/noirc_evaluator/src/ssa/acir_gen.rs b/crates/noirc_evaluator/src/ssa/acir_gen.rs index 10fd3645280..402e51f511f 100644 --- a/crates/noirc_evaluator/src/ssa/acir_gen.rs +++ b/crates/noirc_evaluator/src/ssa/acir_gen.rs @@ -2,7 +2,6 @@ use crate::Evaluator; use crate::{ errors::RuntimeError, ssa::{ - builtin, context::SsaContext, node::{Instruction, Operation}, }, @@ -35,7 +34,6 @@ impl Acir { ins: &Instruction, evaluator: &mut Evaluator, ctx: &SsaContext, - show_output: bool, ) -> Result<(), RuntimeError> { use operations::{ binary, condition, constrain, intrinsics, load, not, r#return, store, truncate, @@ -59,16 +57,7 @@ impl Acir { truncate::evaluate(value, *bit_size, *max_bit_size, var_cache, evaluator, ctx) } Operation::Intrinsic(opcode, args) => { - let opcode = match opcode { - builtin::Opcode::Println(print_info) => { - builtin::Opcode::Println(builtin::PrintlnInfo { - is_string_output: print_info.is_string_output, - show_output, - }) - } - _ => *opcode, - }; - intrinsics::evaluate(args, ins, opcode, var_cache, acir_mem, ctx, evaluator) + intrinsics::evaluate(args, ins, *opcode, var_cache, acir_mem, ctx, evaluator) } Operation::Return(node_ids) => { r#return::evaluate(node_ids, acir_mem, var_cache, evaluator, ctx)? diff --git a/crates/noirc_evaluator/src/ssa/acir_gen/operations/intrinsics.rs b/crates/noirc_evaluator/src/ssa/acir_gen/operations/intrinsics.rs index 56c42428907..8aa4baef2a4 100644 --- a/crates/noirc_evaluator/src/ssa/acir_gen/operations/intrinsics.rs +++ b/crates/noirc_evaluator/src/ssa/acir_gen/operations/intrinsics.rs @@ -12,17 +12,15 @@ use crate::{ }, Evaluator, }; -use acvm::{ - acir::{ - circuit::{ - directives::{Directive, LogInfo}, - opcodes::{BlackBoxFuncCall, FunctionInput, Opcode as AcirOpcode}, - }, - native_types::{Expression, Witness}, +use acvm::acir::{ + circuit::{ + directives::{Directive, LogOutputInfo}, + opcodes::{BlackBoxFuncCall, FunctionInput, Opcode as AcirOpcode}, }, - FieldElement, + native_types::{Expression, Witness}, }; use iter_extended::vecmap; +use noirc_abi::{decode_string_value, format_field_string}; // Generate constraints for two types of functions: // - Builtin functions: These are functions that @@ -64,18 +62,24 @@ pub(crate) fn evaluate( memory_map.map_array(a, &outputs, ctx); } } - Opcode::Println(print_info) => { + Opcode::Println => { outputs = Vec::new(); // print statements do not output anything - if print_info.show_output { - evaluate_println( - var_cache, - memory_map, - print_info.is_string_output, - args, - ctx, - evaluator, - ); - } + let is_string_output = ctx.get_as_constant(args[1]).unwrap().to_u128(); + evaluate_logs(var_cache, memory_map, None, is_string_output != 0, args, ctx, evaluator); + } + Opcode::Trace => { + outputs = Vec::new(); // print statements do not output anything + let trace_label = args[0]; + let is_string_output = ctx.get_as_constant(args[2]).unwrap().to_u128(); + evaluate_logs( + var_cache, + memory_map, + Some(trace_label), + is_string_output != 0, + args, + ctx, + evaluator, + ); } Opcode::LowLevel(op) => { let inputs = prepare_inputs(var_cache, memory_map, args, ctx, evaluator); @@ -236,16 +240,22 @@ fn prepare_outputs( outputs } -fn evaluate_println( +fn evaluate_logs( var_cache: &mut InternalVarCache, memory_map: &mut AcirMem, + trace_label: Option, is_string_output: bool, args: &[NodeId], ctx: &SsaContext, evaluator: &mut Evaluator, ) { - assert_eq!(args.len(), 1, "print statements can only support one argument"); - let node_id = args[0]; + let (trace_label, node_id) = if let Some(trace_label_id) = trace_label { + assert_eq!(args.len(), 3, "trace log statements can only support three arguments: two supplied by the user and an `is_string_output` flag inserted by the compiler"); + (Some(evaluate_trace_label(var_cache, memory_map, trace_label_id, ctx)), args[1]) + } else { + assert_eq!(args.len(), 2, "println log statements can only support two arguments: one supplied by the user and an `is_string_output` flag inserted by the compiler"); + (None, args[0]) + }; let mut log_string = "".to_owned(); let mut log_witnesses = Vec::new(); @@ -253,49 +263,15 @@ fn evaluate_println( let obj_type = ctx.object_type(node_id); match obj_type { ObjectType::Pointer(array_id) => { - let mem_array = &ctx.mem[array_id]; - let mut field_elements = Vec::new(); - for idx in 0..mem_array.len { - let var = memory_map - .load_array_element_constant_index(mem_array, idx) - .expect("array element being logged does not exist in memory"); - let array_elem_expr = var.expression(); - if array_elem_expr.is_const() { - field_elements.push(array_elem_expr.q_c); - } else { - let var = match var_cache.get(&node_id) { - Some(var) => var.clone(), - _ => InternalVar::from(array_elem_expr.clone()), - }; - let w = var - .cached_witness() - .expect("array element to be logged is missing a witness"); - log_witnesses.push(w); - } - } - - if is_string_output { - let final_string = noirc_abi::decode_string_value(&field_elements); - log_string.push_str(&final_string); - } else if !field_elements.is_empty() { - let fields = vecmap(field_elements, format_field_string); - log_string = format!("[{}]", fields.join(", ")); - } + (log_string, log_witnesses) = + evaluate_array_log(ctx, array_id, memory_map, var_cache, node_id, is_string_output); } _ => match ctx.get_as_constant(node_id) { Some(field) => { log_string = format_field_string(field); } None => { - let mut var = var_cache - .get(&node_id) - .unwrap_or_else(|| { - panic!( - "invalid input for print statement: {:?}", - ctx.try_get_node(node_id).expect("node is missing from SSA") - ) - }) - .clone(); + let mut var = var_cache.get_or_compute_internal_var_unwrap(node_id, evaluator, ctx); if let Some(field) = var.to_const() { log_string = format_field_string(field); } else if let Some(w) = var.get_or_compute_witness(evaluator, false) { @@ -315,22 +291,72 @@ fn evaluate_println( assert!(log_witnesses.is_empty() ^ log_string.is_empty()); let log_directive = if !log_string.is_empty() { - Directive::Log(LogInfo::FinalizedOutput(log_string)) + Directive::Log { trace_label, output_info: LogOutputInfo::FinalizedOutput(log_string) } } else { - Directive::Log(LogInfo::WitnessOutput(log_witnesses)) + Directive::Log { trace_label, output_info: LogOutputInfo::WitnessOutput(log_witnesses) } }; evaluator.opcodes.push(AcirOpcode::Directive(log_directive)); } -/// This trims any leading zeroes. -/// A singular '0' will be prepended as well if the trimmed string has an odd length. -/// A hex string's length needs to be even to decode into bytes, as two digits correspond to -/// one byte. -fn format_field_string(field: FieldElement) -> String { - let mut trimmed_field = field.to_hex().trim_start_matches('0').to_owned(); - if trimmed_field.len() % 2 != 0 { - trimmed_field = "0".to_owned() + &trimmed_field +fn evaluate_trace_label( + var_cache: &mut InternalVarCache, + memory_map: &mut AcirMem, + trace_label_id: NodeId, + ctx: &SsaContext, +) -> String { + let obj_type = ctx.object_type(trace_label_id); + match obj_type { + ObjectType::Pointer(array_id) => { + let (evaluated_string, _) = + evaluate_array_log(ctx, array_id, memory_map, var_cache, trace_label_id, true); + evaluated_string + } + _ => { + unreachable!( + "a trace label must be a string (which is represented by a pointer and no other ObjectType)" + ); + } + } +} + +fn evaluate_array_log( + ctx: &SsaContext, + array_id: ArrayId, + memory_map: &mut AcirMem, + var_cache: &mut InternalVarCache, + node_id: NodeId, + is_string_output: bool, +) -> (String, Vec) { + let mut log_string = "".to_owned(); + let mut log_witnesses = Vec::new(); + + let mem_array = &ctx.mem[array_id]; + let mut field_elements = Vec::new(); + for idx in 0..mem_array.len { + let var = memory_map + .load_array_element_constant_index(mem_array, idx) + .expect("array element being logged does not exist in memory"); + let array_elem_expr = var.expression(); + if array_elem_expr.is_const() { + field_elements.push(array_elem_expr.q_c); + } else { + let var = match var_cache.get(&node_id) { + Some(var) => var.clone(), + _ => InternalVar::from(array_elem_expr.clone()), + }; + let w = var.cached_witness().expect("array element to be logged is missing a witness"); + log_witnesses.push(w); + } + } + + if is_string_output { + let final_string = decode_string_value(&field_elements); + log_string.push_str(&final_string); + } else if !field_elements.is_empty() { + let fields = vecmap(field_elements, format_field_string); + log_string = format!("[{}]", fields.join(", ")); }; - "0x".to_owned() + &trimmed_field + + (log_string, log_witnesses) } diff --git a/crates/noirc_evaluator/src/ssa/acir_gen/operations/sort.rs b/crates/noirc_evaluator/src/ssa/acir_gen/operations/sort.rs index 85dcc0803bf..a4be059fbdb 100644 --- a/crates/noirc_evaluator/src/ssa/acir_gen/operations/sort.rs +++ b/crates/noirc_evaluator/src/ssa/acir_gen/operations/sort.rs @@ -145,7 +145,7 @@ mod test { // compute the network output by solving the constraints let backend = MockBackend {}; backend - .solve(&mut solved_witness, eval.opcodes.clone()) + .solve(&mut solved_witness, eval.opcodes.clone(), &mut vec![]) .expect("Could not solve permutation constraints"); let mut b_val = Vec::new(); for i in 0..output.len() { diff --git a/crates/noirc_evaluator/src/ssa/builtin.rs b/crates/noirc_evaluator/src/ssa/builtin.rs index 2ca9effe5df..cd8e3143760 100644 --- a/crates/noirc_evaluator/src/ssa/builtin.rs +++ b/crates/noirc_evaluator/src/ssa/builtin.rs @@ -15,7 +15,8 @@ pub(crate) enum Opcode { LowLevel(BlackBoxFunc), ToBits(Endian), ToRadix(Endian), - Println(PrintlnInfo), + Println, + Trace, Sort, } @@ -37,9 +38,8 @@ impl Opcode { "to_be_bits" => Some(Opcode::ToBits(Endian::Big)), "to_le_radix" => Some(Opcode::ToRadix(Endian::Little)), "to_be_radix" => Some(Opcode::ToRadix(Endian::Big)), - "println" => { - Some(Opcode::Println(PrintlnInfo { is_string_output: false, show_output: true })) - } + "println" => Some(Opcode::Println), + "trace" => Some(Opcode::Trace), "arraysort" => Some(Opcode::Sort), _ => BlackBoxFunc::lookup(op_name).map(Opcode::LowLevel), } @@ -62,7 +62,8 @@ impl Opcode { "to_be_radix" } } - Opcode::Println(_) => "println", + Opcode::Println => "println", + Opcode::Trace => "trace", Opcode::Sort => "arraysort", } } @@ -92,9 +93,11 @@ impl Opcode { } } } - Opcode::ToBits(_) | Opcode::ToRadix(_) | Opcode::Println(_) | Opcode::Sort => { - BigUint::zero() - } //pointers do not overflow + Opcode::ToBits(_) + | Opcode::ToRadix(_) + | Opcode::Println + | Opcode::Trace + | Opcode::Sort => BigUint::zero(), //pointers do not overflow } } @@ -123,7 +126,8 @@ impl Opcode { } Opcode::ToBits(_) => (FieldElement::max_num_bits(), ObjectType::Boolean), Opcode::ToRadix(_) => (FieldElement::max_num_bits(), ObjectType::NativeField), - Opcode::Println(_) => (0, ObjectType::NotAnObject), + Opcode::Println => (0, ObjectType::NotAnObject), + Opcode::Trace => (0, ObjectType::NotAnObject), Opcode::Sort => { let a = super::mem::Memory::deref(ctx, args[0]).unwrap(); (ctx.mem[a].len, ctx.mem[a].element_type) @@ -132,15 +136,6 @@ impl Opcode { } } -#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq)] -pub(crate) struct PrintlnInfo { - // We store strings as arrays and there is no differentiation between them in the SSA. - // This bool simply states whether an array that is to be printed should be outputted as a utf8 string - pub(crate) is_string_output: bool, - // This is a flag used during `nargo test` to determine whether to display println output. - pub(crate) show_output: bool, -} - #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub(crate) enum Endian { Big, diff --git a/crates/noirc_evaluator/src/ssa/context.rs b/crates/noirc_evaluator/src/ssa/context.rs index caba3653b8a..70cb819340f 100644 --- a/crates/noirc_evaluator/src/ssa/context.rs +++ b/crates/noirc_evaluator/src/ssa/context.rs @@ -15,7 +15,7 @@ use crate::Evaluator; use acvm::FieldElement; use iter_extended::vecmap; use noirc_errors::Location; -use noirc_frontend::monomorphization::ast::{Definition, Expression, FuncId, Literal, Type}; +use noirc_frontend::monomorphization::ast::{Definition, FuncId, Type}; use num_bigint::BigUint; use num_traits::{One, Zero}; use std::collections::{HashMap, HashSet}; @@ -695,7 +695,6 @@ impl SsaContext { &mut self, evaluator: &mut Evaluator, enable_logging: bool, - show_output: bool, ) -> Result<(), RuntimeError> { //SSA self.log(enable_logging, "SSA:", "\ninline functions"); @@ -741,7 +740,7 @@ impl SsaContext { integer::overflow_strategy(self)?; self.log(enable_logging, "\noverflow:", ""); //ACIR - self.acir(evaluator, show_output)?; + self.acir(evaluator)?; if enable_logging { print_acir_circuit(&evaluator.opcodes); println!("DONE"); @@ -750,17 +749,13 @@ impl SsaContext { Ok(()) } - pub(crate) fn acir( - &self, - evaluator: &mut Evaluator, - show_output: bool, - ) -> Result<(), RuntimeError> { + pub(crate) fn acir(&self, evaluator: &mut Evaluator) -> Result<(), RuntimeError> { let mut acir = Acir::default(); let mut fb = Some(&self[self.first_block]); while let Some(block) = fb { for iter in &block.instructions { let ins = self.instruction(*iter); - acir.acir_gen_instruction(ins, evaluator, self, show_output)?; + acir.acir_gen_instruction(ins, evaluator, self)?; } //TODO we should rather follow the jumps fb = block.left.map(|block_id| &self[block_id]); @@ -1148,41 +1143,9 @@ impl SsaContext { NodeId(index) } - pub(crate) fn get_builtin_opcode( - &self, - node_id: NodeId, - arguments: &[Expression], - ) -> Option { + pub(crate) fn get_builtin_opcode(&self, node_id: NodeId) -> Option { match &self[node_id] { - NodeObject::Function(FunctionKind::Builtin(opcode), ..) => match opcode { - builtin::Opcode::Println(_) => { - // Compiler sanity check. This should be caught during typechecking - assert_eq!( - arguments.len(), - 1, - "print statements currently only support one argument" - ); - let is_string = match &arguments[0] { - Expression::Ident(ident) => match ident.typ { - Type::String(_) => true, - Type::Tuple(_) => { - unreachable!("logging structs/tuples is not supported") - } - Type::Function { .. } => { - unreachable!("logging functions is not supported") - } - _ => false, - }, - Expression::Literal(literal) => matches!(literal, Literal::Str(_)), - _ => unreachable!("logging this expression type is not supported"), - }; - Some(builtin::Opcode::Println(builtin::PrintlnInfo { - is_string_output: is_string, - show_output: true, - })) - } - _ => Some(*opcode), - }, + NodeObject::Function(FunctionKind::Builtin(opcode), ..) => Some(*opcode), _ => None, } } diff --git a/crates/noirc_evaluator/src/ssa/function.rs b/crates/noirc_evaluator/src/ssa/function.rs index 184762d3012..1ac9d7c82f5 100644 --- a/crates/noirc_evaluator/src/ssa/function.rs +++ b/crates/noirc_evaluator/src/ssa/function.rs @@ -230,7 +230,7 @@ impl IrGenerator { let func = self.ssa_gen_expression(&call.func)?.unwrap_id(); let arguments = self.ssa_gen_expression_list(&call.arguments); - if let Some(opcode) = self.context.get_builtin_opcode(func, &call.arguments) { + if let Some(opcode) = self.context.get_builtin_opcode(func) { return self.call_low_level(opcode, arguments); } diff --git a/crates/noirc_evaluator/src/ssa/optimizations.rs b/crates/noirc_evaluator/src/ssa/optimizations.rs index 1afac20b866..7ee5be1121a 100644 --- a/crates/noirc_evaluator/src/ssa/optimizations.rs +++ b/crates/noirc_evaluator/src/ssa/optimizations.rs @@ -467,9 +467,10 @@ fn cse_block_with_anchor( Operation::Intrinsic(opcode, args) => { //Add dummy load for function arguments and enable CSE only if no array in argument let mut activate_cse = true; - // We do not want to replace any print intrinsics as we want them to remain in order and unchanged - if let builtin::Opcode::Println(_) = opcode { - activate_cse = false + // We do not want to replace any intrinsics built upon the log directive as we want them to remain in order and unchanged + match opcode { + builtin::Opcode::Println | builtin::Opcode::Trace => activate_cse = false, + _ => (), } for arg in args { @@ -524,8 +525,8 @@ fn cse_block_with_anchor( //cannot simplify to_le_bits() in the previous call because it get replaced with multiple instructions if let Operation::Intrinsic(opcode, args) = &update2.operation { match opcode { - // We do not simplify print statements - builtin::Opcode::Println(_) => (), + // We do not simplify any opcode built upon the log directive + builtin::Opcode::Println | builtin::Opcode::Trace => (), _ => { let args = args.iter().map(|arg| { NodeEval::from_id(ctx, *arg).into_const_value().map(|f| f.to_u128()) diff --git a/crates/noirc_frontend/src/hir_def/types.rs b/crates/noirc_frontend/src/hir_def/types.rs index 88fe16d3fb9..86e86da8409 100644 --- a/crates/noirc_frontend/src/hir_def/types.rs +++ b/crates/noirc_frontend/src/hir_def/types.rs @@ -853,6 +853,8 @@ impl Type { elem_a.try_unify(elem_b, span) } + (String(len_a), String(len_b)) => len_a.try_unify(len_b, span), + (Tuple(elements_a), Tuple(elements_b)) => { if elements_a.len() != elements_b.len() { Err(SpanKind::None) @@ -984,6 +986,8 @@ impl Type { elem_a.is_subtype_of(elem_b, span) } + (String(len_a), String(len_b)) => len_a.is_subtype_of(len_b, span), + (Tuple(elements_a), Tuple(elements_b)) => { if elements_a.len() != elements_b.len() { Err(SpanKind::None) diff --git a/crates/noirc_frontend/src/monomorphization/mod.rs b/crates/noirc_frontend/src/monomorphization/mod.rs index 01a87f832ca..c20783dbc50 100644 --- a/crates/noirc_frontend/src/monomorphization/mod.rs +++ b/crates/noirc_frontend/src/monomorphization/mod.rs @@ -562,11 +562,21 @@ impl<'interner> Monomorphizer<'interner> { id: node_interner::ExprId, ) -> ast::Expression { let func = Box::new(self.expr_infer(call.func)); - let arguments = vecmap(&call.arguments, |id| self.expr_infer(*id)); + let mut arguments = vecmap(&call.arguments, |id| self.expr_infer(*id)); let return_type = self.interner.id_type(id); let return_type = Self::convert_type(&return_type); let location = call.location; + if let ast::Expression::Ident(ident) = func.as_ref() { + if let Definition::Builtin(opcode) = &ident.definition { + match opcode.as_str() { + "println" => self.append_string_output_arg(&mut arguments, 0), + "trace" => self.append_string_output_arg(&mut arguments, 1), + _ => (), + }; + } + } + self.try_evaluate_call(&func, &call.arguments).unwrap_or(ast::Expression::Call(ast::Call { func, arguments, @@ -575,6 +585,30 @@ impl<'interner> Monomorphizer<'interner> { })) } + fn append_string_output_arg( + &self, + arguments: &mut Vec, + index_to_check: usize, + ) { + let is_string = match &arguments[index_to_check] { + ast::Expression::Ident(ident) => match ident.typ { + ast::Type::String(_) => true, + ast::Type::Tuple(_) => { + unreachable!("logging structs/tuples is not supported") + } + ast::Type::Function { .. } => { + unreachable!("logging functions is not supported") + } + _ => false, + }, + ast::Expression::Literal(literal) => { + matches!(literal, ast::Literal::Str(_)) + } + _ => unreachable!("logging this expression type is not supported"), + }; + arguments.push(ast::Expression::Literal(ast::Literal::Bool(is_string))); + } + /// Try to evaluate certain builtin functions (currently only 'array_len' and field modulus methods) /// at their call site. /// NOTE: Evaluating at the call site means we cannot track aliased functions. diff --git a/noir_stdlib/src/lib.nr b/noir_stdlib/src/lib.nr index b3d903b7262..d4beaaf549c 100644 --- a/noir_stdlib/src/lib.nr +++ b/noir_stdlib/src/lib.nr @@ -10,3 +10,6 @@ mod field; #[builtin(println)] fn println(_input : T) {} + +#[builtin(trace)] +fn trace(_label : str, _input : T) {}