diff --git a/Cargo.lock b/Cargo.lock index 86df9e1a13b..6d3beb3d7a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2531,6 +2531,7 @@ dependencies = [ "acvm", "easy-repl", "nargo", + "noirc_frontend", "noirc_printable_type", "thiserror", ] diff --git a/acvm-repo/acvm/src/pwg/brillig.rs b/acvm-repo/acvm/src/pwg/brillig.rs index 6fc54d42eab..d96cf6ba076 100644 --- a/acvm-repo/acvm/src/pwg/brillig.rs +++ b/acvm-repo/acvm/src/pwg/brillig.rs @@ -111,11 +111,20 @@ impl<'b, B: BlackBoxFunctionSolver> BrilligSolver<'b, B> { Ok(Self { vm, acir_index }) } + pub(super) fn program_counter(&self) -> usize { + self.vm.program_counter() + } + pub(super) fn solve(&mut self) -> Result { let status = self.vm.process_opcodes(); self.handle_vm_status(status) } + pub(super) fn step(&mut self) -> Result { + let status = self.vm.process_opcode(); + self.handle_vm_status(status) + } + fn handle_vm_status( &self, vm_status: VMStatus, diff --git a/acvm-repo/acvm/src/pwg/mod.rs b/acvm-repo/acvm/src/pwg/mod.rs index 057597e6392..08b2463c9ce 100644 --- a/acvm-repo/acvm/src/pwg/mod.rs +++ b/acvm-repo/acvm/src/pwg/mod.rs @@ -177,6 +177,21 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { self.instruction_pointer } + /// Returns the location for the next opcode to execute, or None if execution is finished + pub fn location(&self) -> Option { + if self.instruction_pointer >= self.opcodes.len() { + // evaluation finished + None + } else if let Some(solver) = &self.brillig_solver { + Some(OpcodeLocation::Brillig { + acir_index: self.instruction_pointer, + brillig_index: solver.program_counter(), + }) + } else { + Some(OpcodeLocation::Acir(self.instruction_pointer)) + } + } + /// Finalize the ACVM execution, returning the resulting [`WitnessMap`]. pub fn finalize(self) -> WitnessMap { if self.status != ACVMStatus::Solved { @@ -242,6 +257,10 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { } pub fn solve_opcode(&mut self) -> ACVMStatus { + self.step_opcode(false) + } + + pub fn step_opcode(&mut self, step_into_brillig: bool) -> ACVMStatus { let opcode = &self.opcodes[self.instruction_pointer]; let resolution = match opcode { @@ -258,11 +277,15 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { let solver = self.block_solvers.entry(*block_id).or_default(); solver.solve_memory_op(op, &mut self.witness_map, predicate) } - Opcode::Brillig(_) => match self.solve_brillig_opcode() { - Ok(Some(foreign_call)) => return self.wait_for_foreign_call(foreign_call), + Opcode::Brillig(_) => match self.step_brillig_opcode(step_into_brillig) { + Ok(BrilligSolverStatus::ForeignCallWait(foreign_call)) => { + return self.wait_for_foreign_call(foreign_call) + } + Ok(BrilligSolverStatus::InProgress) => return self.status(ACVMStatus::InProgress), res => res.map(|_| ()), }, }; + match resolution { Ok(()) => { self.instruction_pointer += 1; @@ -296,40 +319,49 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { } } - fn solve_brillig_opcode( + fn step_brillig_opcode( &mut self, - ) -> Result, OpcodeResolutionError> { + step_into: bool, + ) -> Result { let Opcode::Brillig(brillig) = &self.opcodes[self.instruction_pointer] else { unreachable!("Not executing a Brillig opcode"); }; let witness = &mut self.witness_map; - if BrilligSolver::::should_skip(witness, brillig)? { - BrilligSolver::::zero_out_brillig_outputs(witness, brillig).map(|_| None) - } else { - // If we're resuming execution after resolving a foreign call then - // there will be a cached `BrilligSolver` to avoid recomputation. - let mut solver: BrilligSolver<'_, B> = match self.brillig_solver.take() { - Some(solver) => solver, - None => { - BrilligSolver::new(witness, brillig, self.backend, self.instruction_pointer)? - } - }; - match solver.solve()? { - BrilligSolverStatus::ForeignCallWait(foreign_call) => { - // Cache the current state of the solver - self.brillig_solver = Some(solver); - Ok(Some(foreign_call)) + + // Try to use the cached `BrilligSolver` which we will have if: + // - stepping into a Brillig block + // - resuming execution from a foreign call + let mut solver: BrilligSolver<'_, B> = match self.brillig_solver.take() { + Some(solver) => solver, + None => { + if BrilligSolver::::should_skip(witness, brillig)? { + // Exit early if the block doesn't need to be executed (false predicate) + return BrilligSolver::::zero_out_brillig_outputs(witness, brillig) + .map(|_| BrilligSolverStatus::Finished); } - BrilligSolverStatus::InProgress => { + BrilligSolver::new(witness, brillig, self.backend, self.instruction_pointer)? + } + }; + + let status = if step_into { solver.step()? } else { solver.solve()? }; + match status { + BrilligSolverStatus::ForeignCallWait(_) => { + // Cache the current state of the solver + self.brillig_solver = Some(solver); + } + BrilligSolverStatus::InProgress => { + if !step_into { unreachable!("Brillig solver still in progress") } - BrilligSolverStatus::Finished => { - // Write execution outputs - solver.finalize(witness, brillig)?; - Ok(None) - } + // Cache the current state of the solver + self.brillig_solver = Some(solver); } - } + BrilligSolverStatus::Finished => { + // Write execution outputs + solver.finalize(witness, brillig)?; + } + }; + Ok(status) } } diff --git a/compiler/noirc_errors/src/debug_info.rs b/compiler/noirc_errors/src/debug_info.rs index 946841c279b..e52880f481d 100644 --- a/compiler/noirc_errors/src/debug_info.rs +++ b/compiler/noirc_errors/src/debug_info.rs @@ -1,9 +1,10 @@ use acvm::acir::circuit::OpcodeLocation; use acvm::compiler::AcirTransformationMap; +use fm::FileId; use serde_with::serde_as; use serde_with::DisplayFromStr; -use std::collections::BTreeMap; +use std::collections::{BTreeMap,HashMap}; use std::mem; use crate::Location; @@ -17,11 +18,15 @@ pub struct DebugInfo { /// that they should be serialized to/from strings. #[serde_as(as = "BTreeMap")] pub locations: BTreeMap>, + pub variables: HashMap, } impl DebugInfo { - pub fn new(locations: BTreeMap>) -> Self { - DebugInfo { locations } + pub fn new( + locations: BTreeMap>, + variables: HashMap, + ) -> Self { + Self { locations, variables } } /// Updates the locations map when the [`Circuit`][acvm::acir::circuit::Circuit] is modified. @@ -42,4 +47,14 @@ impl DebugInfo { pub fn opcode_location(&self, loc: &OpcodeLocation) -> Option> { self.locations.get(loc).cloned() } + + pub fn get_file_ids(&self) -> Vec { + self + .locations + .values() + .filter_map(|call_stack| { + call_stack.last().map(|location| location.file) + }) + .collect() + } } diff --git a/compiler/noirc_evaluator/src/ssa.rs b/compiler/noirc_evaluator/src/ssa.rs index 131cf30a510..5b75d29d0cc 100644 --- a/compiler/noirc_evaluator/src/ssa.rs +++ b/compiler/noirc_evaluator/src/ssa.rs @@ -113,7 +113,8 @@ pub fn create_circuit( .map(|(index, locations)| (index, locations.into_iter().collect())) .collect(); - let mut debug_info = DebugInfo::new(locations); + let variables = context.debug_state.get_variables(); + let mut debug_info = DebugInfo::new(locations, variables); // Perform any ACIR-level optimizations let (optimized_circuit, transformation_map) = acvm::compiler::optimize(circuit); diff --git a/compiler/noirc_frontend/src/debug/mod.rs b/compiler/noirc_frontend/src/debug/mod.rs new file mode 100644 index 00000000000..2e0be8cf7b1 --- /dev/null +++ b/compiler/noirc_frontend/src/debug/mod.rs @@ -0,0 +1,205 @@ +use std::collections::HashMap; +use crate::parser::{ParsedModule,parse_program}; +use crate::{ast, parser::{Item,ItemKind}, ast::{Path,PathKind,UseTreeKind}}; +use noirc_errors::{Span, Spanned}; +use std::collections::VecDeque; + +#[derive(Debug,Clone)] +pub struct DebugState { + var_id_to_name: HashMap, + var_name_to_id: HashMap, + next_var_id: u32, + pub enabled: bool, +} + +impl Default for DebugState { + fn default() -> Self { + Self { + var_id_to_name: HashMap::new(), + var_name_to_id: HashMap::new(), + next_var_id: 0, + enabled: true, // TODO + } + } +} + +impl DebugState { + pub fn new(vars: HashMap) -> Self { + let mut debug_state = Self::default(); + for (var_name, var_id) in vars.iter() { + debug_state.var_id_to_name.insert(*var_id, var_name.clone()); + debug_state.var_name_to_id.insert(var_name.clone(), *var_id); + debug_state.next_var_id = debug_state.next_var_id.max(var_id+1); + } + debug_state + } + + pub fn get_variables(&self) -> HashMap { + self.var_name_to_id.clone() + } + + fn insert_var(&mut self, var_name: &str) -> u32 { + let var_id = self.next_var_id; + self.next_var_id += 1; + self.var_id_to_name.insert(var_id, var_name.to_string()); + self.var_name_to_id.insert(var_name.to_string(), var_id); + var_id + } + + pub fn insert_symbols(&mut self, module: &mut ParsedModule) { + if !self.enabled { return } + self.insert_state_set_oracle(module); + + module.items.iter_mut().for_each(|item| { + match item { + Item { kind: ItemKind::Function(f), .. } => { + // todo: f.def.parameters + f.def.body.0.iter_mut().for_each(|stmt| self.walk_statement(stmt)); + }, + _ => {}, + } + }); + } + + fn walk_expr(&mut self, _expr: &mut ast::Expression) { + } + + fn wrap_var_expr(&mut self, var_name: &str, expr: ast::Expression) -> ast::Expression { + let var_id = self.insert_var(var_name); + let kind = ast::ExpressionKind::Call(Box::new(ast::CallExpression { + func: Box::new(ast::Expression { + kind: ast::ExpressionKind::Variable(ast::Path { + segments: vec![ident("__debug_state_set")], + kind: PathKind::Plain + }), + span: Span::single_char(0), + }), + arguments: vec![ + ast::Expression { + kind: ast::ExpressionKind::Literal(ast::Literal::Integer( + (var_id as u128).into() + )), + span: Span::single_char(0), + }, + expr + ], + })); + ast::Expression { kind, span: Span::single_char(0) } + } + + fn wrap_let_statement(&mut self, let_stmt: &ast::LetStatement) -> ast::Statement { + // rewrites let statements written like this: + // let (((a,b,c),D { d }),e,f) = x; + // + // into statements like this: + // + // let (a,b,c,d,e,f,g) = { + // let (((a,b,c),D { d }),e,f) = x; + // wrap(a); + // wrap(b); + // ... + // wrap(f); + // (a,b,c,d,e,f,g) + // }; + + let vars = pattern_vars(&let_stmt.pattern); + let vars_pattern: Vec = vars.iter().map(|id| { + ast::Pattern::Identifier(id.clone()) + }).collect(); + let vars_exprs: Vec = vars.iter().map(|id| id_expr(id)).collect(); + + let mut block_stmts = vec![ + ast::Statement { + kind: ast::StatementKind::Let(let_stmt.clone()), + span: Span::single_char(0), + }, + ]; + block_stmts.extend(vars.iter().map(|id| { + let var_name = &id.0.contents; + ast::Statement { + kind: ast::StatementKind::Semi(self.wrap_var_expr(var_name, id_expr(id))), + span: Span::single_char(0), + } + })); + block_stmts.push(ast::Statement { + kind: ast::StatementKind::Expression(ast::Expression { + kind: ast::ExpressionKind::Tuple(vars_exprs), + span: Span::single_char(0), + }), + span: Span::single_char(0), + }); + + ast::Statement { + kind: ast::StatementKind::Let(ast::LetStatement { + pattern: ast::Pattern::Tuple(vars_pattern, Span::single_char(0)), + r#type: ast::UnresolvedType::unspecified(), + expression: ast::Expression { + kind: ast::ExpressionKind::Block(ast::BlockExpression(block_stmts)), + span: Span::single_char(0), + }, + }), + span: Span::single_char(0), + } + } + + fn walk_statement(&mut self, stmt: &mut ast::Statement) { + match &mut stmt.kind { + ast::StatementKind::Let(let_stmt) => { + *stmt = self.wrap_let_statement(&let_stmt); + }, + _ => {}, + } + } + + fn insert_state_set_oracle(&self, module: &mut ParsedModule) { + let (program, errors) = parse_program(r#" + #[oracle(__debug_state_set)] + unconstrained fn __debug_state_set_oracle(_var_id: u32, _input: T) {} + + unconstrained pub fn __debug_state_set(var_id: u32, value: T) -> T { + __debug_state_set_oracle(var_id, value); + value + } + "#); + if !errors.is_empty() { panic!("errors parsing internal oracle definitions: {errors:?}") } + module.items.extend(program.items); + } +} + +fn pattern_vars(pattern: &ast::Pattern) -> Vec { + let mut vars = vec![]; + let mut stack = VecDeque::from([ pattern ]); + while stack.front().is_some() { + let pattern = stack.pop_front().unwrap(); + match pattern { + ast::Pattern::Identifier(id) => { + vars.push(id.clone()); + }, + ast::Pattern::Mutable(pattern, _) => { + stack.push_back(pattern); + }, + ast::Pattern::Tuple(patterns, _) => { + stack.extend(patterns.iter()); + }, + ast::Pattern::Struct(_, pids, _) => { + stack.extend(pids.iter().map(|(_, pattern)| pattern)); + vars.extend(pids.iter().map(|(id, _)| id.clone())); + }, + } + } + vars +} + +fn ident(s: &str) -> ast::Ident { + ast::Ident(Spanned::from(Span::single_char(0), s.to_string())) +} + +fn id_expr(id: &ast::Ident) -> ast::Expression { + ast::Expression { + kind: ast::ExpressionKind::Variable(Path { + segments: vec![id.clone()], + kind: PathKind::Plain, + }), + span: Span::single_char(0), + } +} diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs index ed48d7fbb51..4906b339d50 100644 --- a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs +++ b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs @@ -20,7 +20,7 @@ use super::{ }, errors::{DefCollectorErrorKind, DuplicateType}, }; -use crate::hir::def_map::{parse_file, LocalModuleId, ModuleData, ModuleId}; +use crate::hir::def_map::{LocalModuleId, ModuleData, ModuleId}; use crate::hir::resolution::import::ImportDirective; use crate::hir::Context; @@ -525,8 +525,7 @@ impl<'a> ModCollector<'a> { context.visited_files.insert(child_file_id, location); // Parse the AST for the module we just found and then recursively look for it's defs - let (ast, parsing_errors) = parse_file(&context.file_manager, child_file_id); - let ast = ast.into_sorted(); + let (ast, parsing_errors) = context.parse_file(child_file_id, crate_id); errors.extend( parsing_errors.iter().map(|e| (e.clone().into(), child_file_id)).collect::>(), diff --git a/compiler/noirc_frontend/src/hir/def_map/mod.rs b/compiler/noirc_frontend/src/hir/def_map/mod.rs index 006e00d2d07..78ba5d2eebf 100644 --- a/compiler/noirc_frontend/src/hir/def_map/mod.rs +++ b/compiler/noirc_frontend/src/hir/def_map/mod.rs @@ -81,9 +81,8 @@ impl CrateDefMap { } // First parse the root file. - let root_file_id = context.crate_graph[crate_id].root_file_id; - let (ast, parsing_errors) = parse_file(&context.file_manager, root_file_id); - let ast = ast.into_sorted(); + let root_file_id = context.get_root_id(crate_id); + let (ast, parsing_errors) = context.parse_file(root_file_id, crate_id); #[cfg(feature = "aztec")] let ast = match super::aztec_library::transform(ast, &crate_id, context) { @@ -257,7 +256,7 @@ pub struct Contract { pub events: Vec, } -/// Given a FileId, fetch the File, from the FileManager and parse it's content +/// Given a FileId, fetch the File, from the FileManager and parse it's content. pub fn parse_file(fm: &FileManager, file_id: FileId) -> (ParsedModule, Vec) { let file = fm.fetch_file(file_id); parse_program(file.source()) diff --git a/compiler/noirc_frontend/src/hir/mod.rs b/compiler/noirc_frontend/src/hir/mod.rs index aabb0e82daa..0538de8b3db 100644 --- a/compiler/noirc_frontend/src/hir/mod.rs +++ b/compiler/noirc_frontend/src/hir/mod.rs @@ -9,9 +9,11 @@ pub(crate) mod aztec_library; use crate::graph::{CrateGraph, CrateId, Dependency}; use crate::hir_def::function::FuncMeta; +use crate::debug::DebugState; use crate::node_interner::{FuncId, NodeInterner, StructId}; -use def_map::{Contract, CrateDefMap}; -use fm::FileManager; +use crate::parser::{SortedModule, ParserError}; +use def_map::{Contract, CrateDefMap, parse_file}; +use fm::{FileManager, FileId}; use noirc_errors::Location; use std::collections::BTreeMap; @@ -25,6 +27,8 @@ pub struct Context { pub crate_graph: CrateGraph, pub(crate) def_maps: BTreeMap, pub file_manager: FileManager, + pub root_crate_id: CrateId, + pub debug_state: DebugState, /// A map of each file that already has been visited from a prior `mod foo;` declaration. /// This is used to issue an error if a second `mod foo;` is declared to the same file. @@ -53,6 +57,8 @@ impl Context { crate_graph, file_manager, storage_slots: BTreeMap::new(), + root_crate_id: CrateId::Dummy, + debug_state: DebugState::default(), } } @@ -195,4 +201,19 @@ impl Context { *next_slot }) } + + /// Given a FileId, fetch the File, from the FileManager and parse its content, + /// applying sorting and debug transforms if debug mode is enabled. + pub fn parse_file(&mut self, file_id: FileId, crate_id: CrateId) -> (SortedModule, Vec) { + let (mut ast, parsing_errors) = parse_file(&self.file_manager, file_id); + + if crate_id == self.root_crate_id { + self.debug_state.insert_symbols(&mut ast); + } + (ast.into_sorted(), parsing_errors) + } + + pub fn get_root_id(&self, crate_id: CrateId) -> FileId { + self.crate_graph[crate_id].root_file_id + } } diff --git a/compiler/noirc_frontend/src/hir/resolution/import.rs b/compiler/noirc_frontend/src/hir/resolution/import.rs index 6f3140a65d4..d296ebf3b6a 100644 --- a/compiler/noirc_frontend/src/hir/resolution/import.rs +++ b/compiler/noirc_frontend/src/hir/resolution/import.rs @@ -35,11 +35,13 @@ pub struct ResolvedImport { impl From for CustomDiagnostic { fn from(error: PathResolutionError) -> Self { match error { - PathResolutionError::Unresolved(ident) => CustomDiagnostic::simple_error( - format!("Could not resolve '{ident}' in path"), - String::new(), - ident.span(), - ), + PathResolutionError::Unresolved(ident) => { + CustomDiagnostic::simple_error( + format!("Could not resolve '{ident}' in path"), + String::new(), + ident.span(), + ) + } PathResolutionError::ExternalContractUsed(ident) => CustomDiagnostic::simple_error( format!("Contract variable '{ident}' referenced from outside the contract"), "Contracts may only be referenced from within a contract".to_string(), diff --git a/compiler/noirc_frontend/src/lib.rs b/compiler/noirc_frontend/src/lib.rs index 74057240de1..324df78ab1d 100644 --- a/compiler/noirc_frontend/src/lib.rs +++ b/compiler/noirc_frontend/src/lib.rs @@ -11,6 +11,7 @@ #![warn(clippy::semicolon_if_nothing_returned)] pub mod ast; +pub mod debug; pub mod graph; pub mod lexer; pub mod monomorphization; diff --git a/tooling/debugger/Cargo.toml b/tooling/debugger/Cargo.toml index 5b8248345a0..7accc87a7db 100644 --- a/tooling/debugger/Cargo.toml +++ b/tooling/debugger/Cargo.toml @@ -10,7 +10,9 @@ license.workspace = true [dependencies] acvm.workspace = true +fm.workspace = true nargo.workspace = true +noirc_frontend.workspace = true noirc_printable_type.workspace = true thiserror.workspace = true -easy-repl = "0.2.1" \ No newline at end of file +easy-repl = "0.2.1" diff --git a/tooling/debugger/src/lib.rs b/tooling/debugger/src/lib.rs index d1b0b350be3..329d1d0bfb4 100644 --- a/tooling/debugger/src/lib.rs +++ b/tooling/debugger/src/lib.rs @@ -1,9 +1,9 @@ use acvm::acir::circuit::OpcodeLocation; use acvm::pwg::{ACVMStatus, ErrorLocation, OpcodeResolutionError, ACVM}; use acvm::BlackBoxFunctionSolver; -use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; +use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap, FieldElement}; -use nargo::artifacts::debug::DebugArtifact; +use nargo::artifacts::debug::{DebugArtifact,DebugVars}; use nargo::errors::ExecutionError; use nargo::NargoError; @@ -11,6 +11,7 @@ use nargo::ops::ForeignCallExecutor; use easy_repl::{command, CommandStatus, Critical, Repl}; use std::cell::{Cell, RefCell}; +use std::collections::HashMap; enum SolveResult { Done, @@ -20,6 +21,7 @@ enum SolveResult { struct DebugContext<'backend, B: BlackBoxFunctionSolver> { acvm: ACVM<'backend, B>, debug_artifact: DebugArtifact, + debug_vars: DebugVars, foreign_call_executor: ForeignCallExecutor, circuit: &'backend Circuit, show_output: bool, @@ -27,9 +29,13 @@ struct DebugContext<'backend, B: BlackBoxFunctionSolver> { impl<'backend, B: BlackBoxFunctionSolver> DebugContext<'backend, B> { fn step_opcode(&mut self) -> Result { - let solver_status = self.acvm.solve_opcode(); + let solver_status = self.acvm.step_opcode(true); - match solver_status { + self.handle_acvm_status(solver_status) + } + + fn handle_acvm_status(&mut self, status: ACVMStatus) -> Result { + match status { ACVMStatus::Solved => Ok(SolveResult::Done), ACVMStatus::InProgress => Ok(SolveResult::Ok), ACVMStatus::Failure(error) => { @@ -58,7 +64,11 @@ impl<'backend, B: BlackBoxFunctionSolver> DebugContext<'backend, B> { } ACVMStatus::RequiresForeignCall(foreign_call) => { let foreign_call_result = - self.foreign_call_executor.execute(&foreign_call, self.show_output)?; + self.foreign_call_executor.execute_with_debug( + &foreign_call, + self.show_output, + &mut self.debug_vars, + )?; self.acvm.resolve_pending_foreign_call(foreign_call_result); Ok(SolveResult::Ok) } @@ -66,13 +76,23 @@ impl<'backend, B: BlackBoxFunctionSolver> DebugContext<'backend, B> { } fn show_current_vm_status(&self) { - let ip = self.acvm.instruction_pointer(); + let location = self.acvm.location(); let opcodes = self.acvm.opcodes(); - if ip >= opcodes.len() { - println!("Finished execution"); - } else { - println!("Stopped at opcode {}: {}", ip, opcodes[ip]); - Self::show_source_code_location(&OpcodeLocation::Acir(ip), &self.debug_artifact); + + match location { + None => println!("Finished execution"), + Some(location) => { + match location { + OpcodeLocation::Acir(ip) => { + println!("Stopped at opcode {}: {}", ip, opcodes[ip]) + } + OpcodeLocation::Brillig { acir_index: ip, brillig_index } => println!( + "Stopped at opcode {} in Brillig block {}: {}", + brillig_index, ip, opcodes[ip] + ), + } + Self::show_source_code_location(&location, &self.debug_artifact); + } } } @@ -119,12 +139,17 @@ pub fn debug_circuit( initial_witness: WitnessMap, show_output: bool, ) -> Result, NargoError> { + let mut debug_vars = DebugVars::default(); + debug_artifact.debug_symbols.iter().for_each(|info| { + debug_vars.insert_variables(&info.variables); + }); let context = RefCell::new(DebugContext { acvm: ACVM::new(blackbox_solver, &circuit.opcodes, initial_witness), foreign_call_executor: ForeignCallExecutor::default(), circuit, debug_artifact, show_output, + debug_vars, }); let ref_step = &context; let ref_cont = &context; @@ -161,6 +186,20 @@ pub fn debug_circuit( } }, ) + .add( + "vars", + command! { + "show variable values available at this point in execution", + () => || { + let mut ctx = ref_cont.borrow_mut(); + let vars = ctx.debug_vars.get_values(); + println!("{:?}", vars.iter().map(|(var_name,value)| { + (var_name, value.to_field()) + }).collect::>()); + Ok(CommandStatus::Done) + } + }, + ) .build() .expect("Failed to initialize debugger repl"); diff --git a/tooling/lsp/src/lib.rs b/tooling/lsp/src/lib.rs index 48ffefb7f7a..378e07ff13c 100644 --- a/tooling/lsp/src/lib.rs +++ b/tooling/lsp/src/lib.rs @@ -278,7 +278,10 @@ fn on_did_save_text_document( for package in &workspace { let (mut context, crate_id) = - prepare_package(package, Box::new(|path| std::fs::read_to_string(path))); + prepare_package(package, Box::new(|path| { + println!["prepare_package {:?}", &path]; + std::fs::read_to_string(path) + })); let file_diagnostics = match check_crate(&mut context, crate_id, false) { Ok(((), warnings)) => warnings, diff --git a/tooling/nargo/src/artifacts/debug.rs b/tooling/nargo/src/artifacts/debug.rs index 3c173f34876..4978fd16cff 100644 --- a/tooling/nargo/src/artifacts/debug.rs +++ b/tooling/nargo/src/artifacts/debug.rs @@ -1,13 +1,14 @@ use codespan_reporting::files::{Error, Files, SimpleFile}; -use noirc_driver::DebugFile; +use noirc_driver::{DebugFile,CompiledProgram}; use noirc_errors::debug_info::DebugInfo; use serde::{Deserialize, Serialize}; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::BTreeMap, ops::Range, }; use fm::{FileId, FileManager, PathString}; +pub use super::debug_vars::DebugVars; /// A Debug Artifact stores, for a given program, the debug info for every function /// along with a map of file Id to the source code so locations in debug info can be mapped to source code they point to. @@ -21,30 +22,32 @@ impl DebugArtifact { pub fn new(debug_symbols: Vec, file_manager: &FileManager) -> Self { let mut file_map = BTreeMap::new(); - let files_with_debug_symbols: BTreeSet = debug_symbols + let file_ids: Vec = debug_symbols .iter() - .flat_map(|function_symbols| { - function_symbols - .locations - .values() - .filter_map(|call_stack| call_stack.last().map(|location| location.file)) - }) + .flat_map(|debug_info| debug_info.get_file_ids()) .collect(); - for file_id in files_with_debug_symbols { - let file_source = file_manager.fetch_file(file_id).source(); + for file_id in file_ids.iter() { + let file_source = file_manager.fetch_file(*file_id).source(); file_map.insert( - file_id, + *file_id, DebugFile { source: file_source.to_string(), - path: file_manager.path(file_id).to_path_buf(), + path: file_manager.path(*file_id).to_path_buf(), }, ); } Self { debug_symbols, file_map } } + + pub fn from_program(program: &CompiledProgram) -> Self { + Self { + debug_symbols: vec![program.debug.clone()], + file_map: program.file_map.clone(), + } + } } impl<'a> Files<'a> for DebugArtifact { diff --git a/tooling/nargo/src/artifacts/debug_vars.rs b/tooling/nargo/src/artifacts/debug_vars.rs new file mode 100644 index 00000000000..133ad26e3b5 --- /dev/null +++ b/tooling/nargo/src/artifacts/debug_vars.rs @@ -0,0 +1,45 @@ +use std::collections::HashMap; +use acvm::acir::brillig::Value; + +#[derive(Debug, Default, Clone)] +pub struct DebugVars { + id_to_name: HashMap, + name_to_id: HashMap, + id_to_value: HashMap, // TODO: something more sophisticated for lexical levels +} + +impl DebugVars { + pub fn new(vars: &HashMap) -> Self { + let mut debug_vars = Self::default(); + debug_vars.id_to_name = vars.iter().map(|(name,id)| (*id, name.clone())).collect(); + debug_vars.name_to_id = vars.clone(); + debug_vars + } + + pub fn get_values<'a>(&'a self) -> HashMap { + self.id_to_value.iter().filter_map(|(var_id,value)| { + self.id_to_name.get(var_id).map(|name| { + (name.clone(),value) + }) + }).collect() + } + + pub fn insert_variables(&mut self, vars: &HashMap) { + vars.iter().for_each(|(var_name,var_id)| { + self.id_to_name.insert(*var_id, var_name.clone()); + self.name_to_id.insert(var_name.clone(), *var_id); + }); + } + + pub fn set_by_id(&mut self, var_id: u32, value: Value) { + self.id_to_value.insert(var_id, value); + } + + pub fn get_by_id<'a>(&'a mut self, var_id: u32) -> Option<&'a Value> { + self.id_to_value.get(&var_id) + } + + pub fn get_by_name<'a>(&'a mut self, var_name: &str) -> Option<&'a Value> { + self.name_to_id.get(var_name).and_then(|var_id| self.id_to_value.get(var_id)) + } +} diff --git a/tooling/nargo/src/artifacts/mod.rs b/tooling/nargo/src/artifacts/mod.rs index 33311e0856e..bf10e438a86 100644 --- a/tooling/nargo/src/artifacts/mod.rs +++ b/tooling/nargo/src/artifacts/mod.rs @@ -10,6 +10,7 @@ use serde::{Deserializer, Serializer}; pub mod contract; pub mod debug; pub mod program; +mod debug_vars; // TODO: move these down into ACVM. fn serialize_circuit(circuit: &Circuit, s: S) -> Result diff --git a/tooling/nargo/src/lib.rs b/tooling/nargo/src/lib.rs index ef014fb436b..b15fd0f53bf 100644 --- a/tooling/nargo/src/lib.rs +++ b/tooling/nargo/src/lib.rs @@ -49,6 +49,7 @@ pub fn prepare_package(package: &Package, file_reader: Box) -> (Cont let mut context = Context::new(fm, graph); let crate_id = prepare_crate(&mut context, &package.entry_path); + context.root_crate_id = crate_id.clone(); prepare_dependencies(&mut context, crate_id, &package.dependencies); diff --git a/tooling/nargo/src/ops/foreign_calls.rs b/tooling/nargo/src/ops/foreign_calls.rs index 4d20a0bd4f0..935553ecb4e 100644 --- a/tooling/nargo/src/ops/foreign_calls.rs +++ b/tooling/nargo/src/ops/foreign_calls.rs @@ -5,12 +5,13 @@ use acvm::{ use iter_extended::vecmap; use noirc_printable_type::{decode_string_value, ForeignCallError, PrintableValueDisplay}; -use crate::NargoError; +use crate::{NargoError, artifacts::debug::DebugVars}; /// This enumeration represents the Brillig foreign calls that are natively supported by nargo. /// After resolution of a foreign call, nargo will restart execution of the ACVM pub(crate) enum ForeignCall { Println, + DebugStateSet, Sequence, ReverseSequence, CreateMock, @@ -30,6 +31,7 @@ impl ForeignCall { pub(crate) fn name(&self) -> &'static str { match self { ForeignCall::Println => "println", + ForeignCall::DebugStateSet => "__debug_state_set", ForeignCall::Sequence => "get_number_sequence", ForeignCall::ReverseSequence => "get_reverse_number_sequence", ForeignCall::CreateMock => "create_mock", @@ -43,6 +45,7 @@ impl ForeignCall { pub(crate) fn lookup(op_name: &str) -> Option { match op_name { "println" => Some(ForeignCall::Println), + "__debug_state_set" => Some(ForeignCall::DebugStateSet), "get_number_sequence" => Some(ForeignCall::Sequence), "get_reverse_number_sequence" => Some(ForeignCall::ReverseSequence), "create_mock" => Some(ForeignCall::CreateMock), @@ -101,6 +104,24 @@ impl ForeignCallExecutor { &mut self, foreign_call: &ForeignCallWaitInfo, show_output: bool, + ) -> Result { + self.execute_optional_debug(foreign_call, show_output, None) + } + + pub fn execute_with_debug( + &mut self, + foreign_call: &ForeignCallWaitInfo, + show_output: bool, + debug_vars: &mut DebugVars, + ) -> Result { + self.execute_optional_debug(foreign_call, show_output, Some(debug_vars)) + } + + fn execute_optional_debug( + &mut self, + foreign_call: &ForeignCallWaitInfo, + show_output: bool, + debug_vars: Option<&mut DebugVars>, ) -> Result { let foreign_call_name = foreign_call.function.as_str(); match ForeignCall::lookup(foreign_call_name) { @@ -110,6 +131,20 @@ impl ForeignCallExecutor { } Ok(ForeignCallResult { values: vec![] }) } + Some(ForeignCall::DebugStateSet) => { + let fcp_var_id = &foreign_call.inputs[0]; + let fcp_value = &foreign_call.inputs[1]; + if let ( + Some(ds), + ForeignCallParam::Single(var_id_value), + ForeignCallParam::Single(value), + ) = (debug_vars, fcp_var_id, fcp_value) { + let var_id = var_id_value.to_u128() as u32; + ds.set_by_id(var_id, value.clone()); + } + // pass-through return value is handled by the oracle wrapper, returning nothing here + Ok(ForeignCallResult { values: vec![] }) + } Some(ForeignCall::Sequence) => { let sequence_length: u128 = foreign_call.inputs[0].unwrap_value().to_field().to_u128(); diff --git a/tooling/nargo_cli/src/cli/compile_cmd.rs b/tooling/nargo_cli/src/cli/compile_cmd.rs index a332d63e062..4ef7d94fbcb 100644 --- a/tooling/nargo_cli/src/cli/compile_cmd.rs +++ b/tooling/nargo_cli/src/cli/compile_cmd.rs @@ -273,15 +273,14 @@ fn save_program( let preprocessed_program = PreprocessedProgram { hash: program.hash, backend: String::from(BACKEND_IDENTIFIER), - abi: program.abi, - bytecode: program.circuit, + abi: program.abi.clone(), + bytecode: program.circuit.clone(), }; save_program_to_file(&preprocessed_program, &package.name, circuit_dir); if output_debug { - let debug_artifact = - DebugArtifact { debug_symbols: vec![program.debug], file_map: program.file_map }; + let debug_artifact = DebugArtifact::from_program(&program); let circuit_name: String = (&package.name).into(); save_debug_artifact_to_file(&debug_artifact, &circuit_name, circuit_dir); } diff --git a/tooling/nargo_cli/tests/execution_success/arithmetic_binary_operations/src/main.nr b/tooling/nargo_cli/tests/execution_success/arithmetic_binary_operations/src/main.nr index 8fb7bcdbeb2..391858ab7c3 100644 --- a/tooling/nargo_cli/tests/execution_success/arithmetic_binary_operations/src/main.nr +++ b/tooling/nargo_cli/tests/execution_success/arithmetic_binary_operations/src/main.nr @@ -3,12 +3,21 @@ // The features being tested are: // Binary addition, multiplication, division, constant modulo // x = 3, y = 4, z = 5 + +struct V { q: u32, r: u32 } +struct U { v: V, w: (u32,u32) } + fn main(x : Field, y : Field, z : Field) -> pub Field { + let uu = U { v: V { q: 100, r: 200 }, w: (300,400) }; + //let U { v: V { q, ..}, w: (w1,w1) } = uu; + let U { v, w } = uu; + //let (q,r,(s,t)) = (3,4,(5,6)); + //cast - assert(y as u1 == 0); + //assert(y as u1 == 0); let a = x + x; // 3 + 3 = 6 - let b = a - y; // 6 - 4 = 2 + let b = a - y; // 7 - 4 = 2 let c = b * z; // 2 * 5 = 10 let d = c / a; // 10 / 6 (This uses field inversion, so we test it by multiplying by `a`) d * a