From c30585fb24e7997eb93dcd85e0c2e12e5d0efcf9 Mon Sep 17 00:00:00 2001 From: Adam Mohammed A Latif Date: Wed, 3 Jun 2026 15:18:16 +0000 Subject: [PATCH] lean_vm: add source-level failure stack traces --- .../src/b_compile_intermediate.rs | 6 + crates/lean_compiler/src/c_compile_final.rs | 35 +++- crates/lean_compiler/src/ir/instruction.rs | 20 +- crates/lean_compiler/src/lib.rs | 45 ++++ crates/lean_compiler/tests/test_compiler.rs | 196 +++++++++++++++++- crates/lean_vm/src/diagnostics/stack_trace.rs | 154 +++++++++++--- crates/lean_vm/src/execution/runner.rs | 195 ++++++++++++++--- crates/lean_vm/src/isa/bytecode.rs | 13 +- 8 files changed, 600 insertions(+), 64 deletions(-) diff --git a/crates/lean_compiler/src/b_compile_intermediate.rs b/crates/lean_compiler/src/b_compile_intermediate.rs index ed30442a1..9261a56ee 100644 --- a/crates/lean_compiler/src/b_compile_intermediate.rs +++ b/crates/lean_compiler/src/b_compile_intermediate.rs @@ -502,6 +502,12 @@ fn compile_lines( let new_fp_pos = compiler.stack_pos; compiler.stack_pos += 1; + instructions.push(IntermediateInstruction::CallSite { + caller: compiler.func_name.clone(), + callee: callee_function_name.clone(), + location: *location, + return_label: return_label.clone(), + }); instructions.extend(emit_call_frame( callee_function_name, args, diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 3585abd84..de4d02b4a 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -13,6 +13,7 @@ impl IntermediateInstruction { | Self::HintWitness { .. } | Self::Inverse { .. } | Self::LocationReport { .. } + | Self::CallSite { .. } | Self::DebugAssert { .. } | Self::DerefHint { .. } | Self::PanicHint { .. } @@ -54,6 +55,7 @@ pub fn compile_to_low_level_bytecode( .ok_or("Missing main function")?; let mut hints = BTreeMap::new(); + let mut call_sites_by_return_pc = BTreeMap::new(); let mut label_to_pc = BTreeMap::new(); let exit_point = intermediate_bytecode @@ -123,7 +125,14 @@ pub fn compile_to_low_level_bytecode( let mut instructions = Vec::new(); for (pc_start, block) in code_blocks { - compile_block(&compiler, &block, pc_start, &mut instructions, &mut hints); + compile_block( + &compiler, + &block, + pc_start, + &mut instructions, + &mut hints, + &mut call_sites_by_return_pc, + ); } debug_assert_eq!(instructions.len(), bytecode_size); @@ -186,6 +195,7 @@ pub fn compile_to_low_level_bytecode( source_code, filepaths, pc_to_location, + call_sites_by_return_pc, }) } @@ -195,6 +205,7 @@ fn compile_block( pc_start: CodeAddress, low_level_bytecode: &mut Vec, hints: &mut BTreeMap>, + call_sites_by_return_pc: &mut BTreeMap, ) { let try_as_mem_or_constant = |value: &IntermediateValue| { if let Some(cst) = try_as_constant(value, compiler) { @@ -357,6 +368,28 @@ fn compile_block( let hint = Hint::LocationReport { location }; hints.entry(pc).or_default().push(hint); } + IntermediateInstruction::CallSite { + caller, + callee, + location, + return_label, + } => { + let return_pc = compiler + .label_to_pc + .get(&return_label) + .copied() + .expect("Fatal: unresolved call return label"); + call_sites_by_return_pc.insert( + return_pc, + CallSite { + caller, + callee, + location, + call_pc: return_pc.saturating_sub(1), + return_pc, + }, + ); + } IntermediateInstruction::DebugAssert { expr, location, diff --git a/crates/lean_compiler/src/ir/instruction.rs b/crates/lean_compiler/src/ir/instruction.rs index 565e755c4..4dee9aaaf 100644 --- a/crates/lean_compiler/src/ir/instruction.rs +++ b/crates/lean_compiler/src/ir/instruction.rs @@ -1,6 +1,8 @@ use super::value::IntermediateValue; use crate::lang::{ConstExpression, MathOperation}; -use lean_vm::{BooleanExpr, CustomHint, HintWitnessDestination, Operation, PrecompileArgs, SourceLocation}; +use lean_vm::{ + BooleanExpr, CustomHint, FunctionName, HintWitnessDestination, Label, Operation, PrecompileArgs, SourceLocation, +}; use std::fmt::{Display, Formatter}; /// Core instruction type for the intermediate representation. @@ -58,6 +60,13 @@ pub enum IntermediateInstruction { LocationReport { location: SourceLocation, }, + // No-op metadata used to reconstruct source-level call stacks. + CallSite { + caller: FunctionName, + callee: FunctionName, + location: SourceLocation, + return_label: Label, + }, DebugAssert { expr: BooleanExpr, location: SourceLocation, @@ -192,6 +201,15 @@ impl Display for IntermediateInstruction { Ok(()) } Self::LocationReport { .. } => Ok(()), + Self::CallSite { + caller, + callee, + location, + return_label, + } => write!( + f, + "call_site {caller} -> {callee} at {location}, returns to {return_label}" + ), Self::DebugAssert { expr, .. } => { write!(f, "debug_assert {expr}") } diff --git a/crates/lean_compiler/src/lib.rs b/crates/lean_compiler/src/lib.rs index 3564cfaa6..d70780541 100644 --- a/crates/lean_compiler/src/lib.rs +++ b/crates/lean_compiler/src/lib.rs @@ -146,6 +146,23 @@ pub struct CompilationFlags { pub replacements: BTreeMap, } +#[derive(Debug, Clone, Copy)] +pub struct CompileAndRunOptions { + /// Include VM profiling metadata in the execution summary. + pub profiler: bool, + /// Print a source-level stack trace to stderr when VM execution fails. + pub stack_trace: bool, +} + +impl Default for CompileAndRunOptions { + fn default() -> Self { + Self { + profiler: false, + stack_trace: true, + } + } +} + pub fn try_compile_program_with_flags( input: &ProgramSource, flags: CompilationFlags, @@ -184,7 +201,35 @@ pub fn try_compile_and_run( Ok(result.metadata.display()) } +pub fn try_compile_and_run_with_options( + input: &ProgramSource, + public_input: &[F; PUBLIC_INPUT_LEN], + options: CompileAndRunOptions, +) -> Result { + let bytecode = try_compile_program(input)?; + let witness = ExecutionWitness::default(); + let result = try_execute_bytecode_with_options( + &bytecode, + public_input, + &witness, + ExecutionOptions { + profiling: options.profiler, + stack_trace: options.stack_trace, + }, + )?; + Ok(result.metadata.display()) +} + pub fn compile_and_run(input: &ProgramSource, public_input: &[F; PUBLIC_INPUT_LEN], profiler: bool) { let summary = try_compile_and_run(input, public_input, profiler).unwrap(); println!("{summary}"); } + +pub fn compile_and_run_with_options( + input: &ProgramSource, + public_input: &[F; PUBLIC_INPUT_LEN], + options: CompileAndRunOptions, +) { + let summary = try_compile_and_run_with_options(input, public_input, options).unwrap(); + println!("{summary}"); +} diff --git a/crates/lean_compiler/tests/test_compiler.rs b/crates/lean_compiler/tests/test_compiler.rs index deab65193..6d07487ae 100644 --- a/crates/lean_compiler/tests/test_compiler.rs +++ b/crates/lean_compiler/tests/test_compiler.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::{process::Command, time::Instant}; use backend::{BasedVectorSpace, PrimeCharacteristicRing}; use lean_compiler::*; @@ -257,6 +257,200 @@ def main(): compile_and_run(&ProgramSource::Raw(program.to_string()), &[F::ZERO; DIGEST_LEN], false); } +#[test] +fn nested_runtime_failure_prints_full_call_stack() { + let current_exe = std::env::current_exe().expect("current test executable path"); + let output = Command::new(current_exe) + .arg("--exact") + .arg("child_nested_runtime_failure_prints_stack_trace") + .arg("--nocapture") + .env("LEANVM_STACK_TRACE_CHILD", "1") + .output() + .expect("run child stack trace test"); + + assert!( + output.status.success(), + "child test failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert_stack_frames(&stderr, &["d()", "c()", "b()", "a()", "main()"]); + assert!( + stderr.contains("pc="), + "stack trace should include pc values\nstderr:\n{stderr}", + ); + assert!( + stderr.contains(":15"), + "stack trace should include the failing source line\nstderr:\n{stderr}", + ); +} + +#[test] +fn parallel_runtime_failure_prints_worker_call_stack() { + let current_exe = std::env::current_exe().expect("current test executable path"); + let output = Command::new(current_exe) + .arg("--exact") + .arg("child_parallel_runtime_failure_prints_stack_trace") + .arg("--nocapture") + .env("LEANVM_STACK_TRACE_CHILD", "1") + .output() + .expect("run child parallel stack trace test"); + + assert!( + output.status.success(), + "child test failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert_stack_frames(&stderr, &["fail()", "main()"]); + assert!( + stderr.contains("pc="), + "parallel stack trace should include pc values\nstderr:\n{stderr}", + ); +} + +fn assert_stack_frames(stderr: &str, expected_frames: &[&str]) { + let stack = stderr + .split("CALL STACK") + .nth(1) + .unwrap_or_else(|| panic!("stderr should contain a CALL STACK section\nstderr:\n{stderr}")); + let frame_lines: Vec<&str> = stack.lines().filter(|line| line.contains("pc=")).collect(); + + assert_eq!( + frame_lines.len(), + expected_frames.len(), + "stack trace should contain exactly the expected frames\nframes:\n{}\nstderr:\n{stderr}", + frame_lines.join("\n"), + ); + for (line, expected) in frame_lines.iter().zip(expected_frames) { + assert!( + line.contains(expected), + "stack frame should contain {expected}\nframe: {line}\nstderr:\n{stderr}", + ); + } +} + +#[test] +fn child_nested_runtime_failure_prints_stack_trace() { + if std::env::var_os("LEANVM_STACK_TRACE_CHILD").is_none() { + return; + } + + let program = r#"def main(): + _ = a() + return + +def a(): + return b() + +def b(): + return c() + +def c(): + return d() + +def d(): + debug_assert(0 == 1) + return 0 +"#; + + let result = try_compile_and_run_with_options( + &ProgramSource::Raw(program.to_string()), + &[F::ZERO; DIGEST_LEN], + CompileAndRunOptions { + profiler: false, + stack_trace: true, + }, + ); + + assert!( + matches!(result, Err(Error::Runtime(_))), + "program should fail at VM runtime inside d(), got {result:?}", + ); +} + +#[test] +fn child_parallel_runtime_failure_prints_stack_trace() { + if std::env::var_os("LEANVM_STACK_TRACE_CHILD").is_none() { + return; + } + + let program = r#"def main(): + n = 4 + for i in parallel_range(0, n): + fail(i) + return + +def fail(i): + if i == 2: + debug_assert(0 == 1) + return +"#; + + let result = try_compile_and_run_with_options( + &ProgramSource::Raw(program.to_string()), + &[F::ZERO; DIGEST_LEN], + CompileAndRunOptions { + profiler: false, + stack_trace: true, + }, + ); + + assert!( + matches!(result, Err(Error::Runtime(_))), + "parallel program should fail at VM runtime inside fail(), got {result:?}", + ); +} + +#[test] +fn legacy_runtime_failure_respects_stack_trace_env_var() { + let current_exe = std::env::current_exe().expect("current test executable path"); + let output = Command::new(current_exe) + .arg("--exact") + .arg("child_legacy_runtime_failure_respects_stack_trace_env_var") + .arg("--nocapture") + .env("LEANVM_STACK_TRACE_CHILD", "1") + .env("LEANVM_STACK_TRACE", "0") + .output() + .expect("run child legacy env-var stack trace test"); + + assert!( + output.status.success(), + "child test failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("CALL STACK"), + "LEANVM_STACK_TRACE=0 should disable legacy stack trace printing\nstderr:\n{stderr}", + ); +} + +#[test] +fn child_legacy_runtime_failure_respects_stack_trace_env_var() { + if std::env::var_os("LEANVM_STACK_TRACE_CHILD").is_none() { + return; + } + + let program = r#"def main(): + debug_assert(0 == 1) + return +"#; + + let result = try_compile_and_run(&ProgramSource::Raw(program.to_string()), &[F::ZERO; DIGEST_LEN], false); + + assert!( + matches!(result, Err(Error::Runtime(_))), + "program should fail at VM runtime, got {result:?}", + ); +} + #[test] #[rustfmt::skip] fn test_soundness_suite() { diff --git a/crates/lean_vm/src/diagnostics/stack_trace.rs b/crates/lean_vm/src/diagnostics/stack_trace.rs index 80eee5bd8..f0e810f2e 100644 --- a/crates/lean_vm/src/diagnostics/stack_trace.rs +++ b/crates/lean_vm/src/diagnostics/stack_trace.rs @@ -1,17 +1,32 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; +use utils::ToUsize; use utils::ansi::Colorize; +use crate::diagnostics::RunnerError; +use crate::execution::memory::MemoryAccess; use crate::isa::Bytecode; use crate::{CodeAddress, FunctionName, SourceLocation}; -pub(crate) fn pretty_stack_trace( +const MAX_STACK_FRAMES: usize = 256; + +struct StackFrame { + function: FunctionName, + location: SourceLocation, + pc: CodeAddress, + fp: usize, + return_pc: Option, +} + +pub(crate) fn pretty_stack_trace( bytecode: &Bytecode, error_pc: CodeAddress, - location_history: &[SourceLocation], + error_fp: usize, + memory: &M, + error: &RunnerError, ) -> String { let mut out = String::new(); - let error_loc = bytecode.pc_to_location.get(error_pc).copied(); + let error_loc = error_source_location(error).or_else(|| bytecode.pc_to_location.get(error_pc).copied()); out.push_str(&format!("{}\n\n", "ERROR".red().bold())); @@ -61,19 +76,32 @@ pub(crate) fn pretty_stack_trace( } } - // Call stack - let stack = build_call_stack(location_history, &bytecode.function_locations); - if stack.len() > 1 { + let (stack, truncated) = build_call_stack(bytecode, error_pc, error_fp, memory, error_loc); + let visible_stack: Vec<_> = stack + .iter() + .filter(|frame| !is_generated_loop_function(&frame.function)) + .collect(); + if !visible_stack.is_empty() { out.push_str(&format!("\n{}\n\n", "CALL STACK".yellow().bold())); - for (i, (func, call_loc)) in stack.iter().rev().enumerate() { - let path = filepath(bytecode, call_loc.file_id); + for (i, frame) in visible_stack.iter().enumerate() { + let path = filepath(bytecode, frame.location.file_id); let marker = if i == 0 { "→".red().to_string() } else { " ".into() }; + let return_pc = frame.return_pc.map(|pc| format!(" return_pc={pc}")).unwrap_or_default(); out.push_str(&format!( - " {} {} at {}:{}\n", + " {} {} at {}:{} pc={} fp={}{}\n", marker, - format!("{func}()").bold(), + format!("{}()", frame.function).bold(), path.dimmed(), - call_loc.line_number.to_string().dimmed() + frame.location.line_number.to_string().dimmed(), + frame.pc, + frame.fp, + return_pc, + )); + } + if truncated { + out.push_str(&format!( + " {}\n", + format!("stack trace truncated after {MAX_STACK_FRAMES} frames").dimmed() )); } } @@ -81,25 +109,88 @@ pub(crate) fn pretty_stack_trace( out } -fn build_call_stack( - history: &[SourceLocation], - func_locs: &BTreeMap, -) -> Vec<(String, SourceLocation)> { - let mut stack: Vec<(String, SourceLocation)> = Vec::new(); - - for (i, &loc) in history.iter().enumerate() { - let (_, func) = find_function_for_location(loc, func_locs); - - if stack.last().map(|(f, _)| f) != Some(&func) { - if let Some(pos) = stack.iter().position(|(f, _)| f == &func) { - stack.truncate(pos + 1); - } else { - let call_loc = if i > 0 { history[i - 1] } else { loc }; - stack.push((func, call_loc)); +fn build_call_stack( + bytecode: &Bytecode, + error_pc: CodeAddress, + error_fp: usize, + memory: &M, + error_loc: Option, +) -> (Vec, bool) { + let mut stack = Vec::new(); + let error_loc = error_loc.unwrap_or_else(unknown_location); + let (_, current_function) = find_function_for_location(error_loc, &bytecode.function_locations); + stack.push(StackFrame { + function: current_function, + location: error_loc, + pc: error_pc, + fp: error_fp, + return_pc: None, + }); + + let mut fp = error_fp; + let mut seen_fps = BTreeSet::new(); + while stack.len() < MAX_STACK_FRAMES { + if !seen_fps.insert(fp) { + break; + } + + let Ok(return_pc) = memory.get(fp).map(|value| value.to_usize()) else { + break; + }; + let Ok(saved_fp) = memory.get(fp + 1).map(|value| value.to_usize()) else { + break; + }; + + let frame = if let Some(call_site) = bytecode.call_sites_by_return_pc.get(&return_pc) { + StackFrame { + function: call_site.caller.clone(), + location: call_site.location, + pc: call_site.call_pc, + fp: saved_fp, + return_pc: Some(call_site.return_pc), } + } else { + let caller_pc = return_pc.saturating_sub(1); + let loc = bytecode + .pc_to_location + .get(caller_pc) + .copied() + .unwrap_or_else(unknown_location); + let (_, function) = find_function_for_location(loc, &bytecode.function_locations); + StackFrame { + function, + location: loc, + pc: caller_pc, + fp: saved_fp, + return_pc: Some(return_pc), + } + }; + stack.push(frame); + + if saved_fp == fp { + return (stack, false); + } + fp = saved_fp; + } + + let truncated = stack.len() == MAX_STACK_FRAMES + && !seen_fps.contains(&fp) + && memory.get(fp).is_ok() + && memory.get(fp + 1).is_ok(); + (stack, truncated) +} + +fn is_generated_loop_function(function: &str) -> bool { + function.starts_with("@loop_") || function.starts_with("@parallel_loop_") +} + +fn error_source_location(error: &RunnerError) -> Option { + match error { + RunnerError::DebugAssertFailed(_, location) | RunnerError::RangeCheckWithTooBigRange { location, .. } => { + Some(*location) } + _ => None, } - stack } pub(crate) fn find_function_for_location( @@ -119,6 +210,13 @@ pub(crate) fn find_function_for_location( )) } +fn unknown_location() -> SourceLocation { + SourceLocation { + file_id: 0, + line_number: 0, + } +} + fn filepath(bytecode: &Bytecode, file_id: usize) -> &str { bytecode .filepaths diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index f00e04880..bafce09de 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -17,6 +17,64 @@ use utils::ToUsize; use super::memory::SegmentMemory; +#[derive(Debug, Clone, Copy)] +pub struct ExecutionOptions { + /// Include VM profiling metadata in the execution result. + pub profiling: bool, + /// Print a source-level stack trace to stderr when execution fails. + pub stack_trace: bool, +} + +impl ExecutionOptions { + /// Preserve the legacy boolean profiler API while allowing `LEANVM_STACK_TRACE=0` + /// to disable stack traces without changing callers. + pub fn from_profiling(profiling: bool) -> Self { + Self { + profiling, + stack_trace: stack_trace_enabled_from_env(true), + } + } +} + +impl Default for ExecutionOptions { + fn default() -> Self { + Self { + profiling: false, + stack_trace: true, + } + } +} + +struct ExecutionFailure { + err: RunnerError, + stack_trace: Option, +} + +impl ExecutionFailure { + fn new( + bytecode: &Bytecode, + memory: &M, + pc: CodeAddress, + fp: usize, + err: RunnerError, + stack_trace: bool, + ) -> Self { + let stack_trace = stack_trace.then(|| crate::diagnostics::pretty_stack_trace(bytecode, pc, fp, memory, &err)); + Self { err, stack_trace } + } +} + +fn stack_trace_enabled_from_env(default: bool) -> bool { + match std::env::var("LEANVM_STACK_TRACE") { + Ok(value) => match value.trim().to_ascii_lowercase().as_str() { + "0" | "false" | "off" | "no" => false, + "1" | "true" | "on" | "yes" => true, + _ => default, + }, + Err(_) => default, + } +} + #[derive(Debug, Default)] pub struct ExecutionWitness { /// Length of the program's "preamble memory" — a region between public @@ -33,6 +91,20 @@ pub fn try_execute_bytecode( public_input: &[F; PUBLIC_INPUT_LEN], witness: &ExecutionWitness, profiling: bool, +) -> Result { + try_execute_bytecode_with_options( + bytecode, + public_input, + witness, + ExecutionOptions::from_profiling(profiling), + ) +} + +pub fn try_execute_bytecode_with_options( + bytecode: &Bytecode, + public_input: &[F; PUBLIC_INPUT_LEN], + witness: &ExecutionWitness, + options: ExecutionOptions, ) -> Result { let mut std_out = String::new(); let mut instruction_history = ExecutionHistory::new(); @@ -42,20 +114,19 @@ pub fn try_execute_bytecode( witness, &mut std_out, &mut instruction_history, - profiling, + options, ) - .map_err(|(last_pc, err)| { - eprintln!( - "\n{}", - crate::diagnostics::pretty_stack_trace(bytecode, last_pc, &instruction_history.lines) - ); + .map_err(|failure| { + if let Some(stack_trace) = failure.stack_trace { + eprintln!("\n{stack_trace}"); + } if !std_out.is_empty() { eprintln!("╔══════════════════════════════════════════════════════════════╗"); eprintln!("║ STD-OUT ║"); eprintln!("╚══════════════════════════════════════════════════════════════╝\n"); eprint!("{std_out}"); } - err + failure.err }) } @@ -120,6 +191,13 @@ struct ParallelBatchInfo { hint_indices_at_start: HashMap, } +struct ParallelSegmentFailure { + segment_id: usize, + pc: CodeAddress, + fp: usize, + err: RunnerError, +} + #[allow(clippy::too_many_arguments)] fn run_loop( bytecode: &Bytecode, @@ -246,8 +324,8 @@ fn execute_bytecode_helper( witness: &ExecutionWitness, std_out: &mut String, instruction_history: &mut ExecutionHistory, - profiling: bool, -) -> Result { + options: ExecutionOptions, +) -> Result { let mut named_hints: HashMap> = witness .hints .iter() @@ -286,7 +364,7 @@ fn execute_bytecode_helper( &mut hints, None, ) - .map_err(|e| (pc, e))? + .map_err(|e| ExecutionFailure::new(bytecode, &memory, pc, fp, e, options.stack_trace))? { LoopExit::Halted => break, LoopExit::ParallelBatch(batch) => { @@ -299,24 +377,29 @@ fn execute_bytecode_helper( &mut fp, &mut ap, &batch, - ) - .map_err(|e| (pc, e))?; + options.stack_trace, + )?; } LoopExit::LoopBack => unreachable!("main loop has no stop_pc"), } } - resolve_deref_hints(&mut memory, &trace.pending_deref_hints).map_err(|e| (pc, e))?; + resolve_deref_hints(&mut memory, &trace.pending_deref_hints) + .map_err(|e| ExecutionFailure::new(bytecode, &memory, pc, fp, e, options.stack_trace))?; assert_eq!(pc, bytecode.ending_pc); for (name, cursor) in &named_hints { if cursor.index != cursor.entries.len() { - return Err(( + return Err(ExecutionFailure::new( + bytecode, + &memory, pc, + fp, RunnerError::InvalidHintWitness(format!( "not all entries of named hint '{name}' were consumed ({} of {} used)", cursor.index, cursor.entries.len(), )), + options.stack_trace, )); } } @@ -324,7 +407,7 @@ fn execute_bytecode_helper( trace.fps.push(fp); let no_vec_runtime_memory = ap - initial_ap; - let profiling_report = if profiling { + let profiling_report = if options.profiling { Some(crate::diagnostics::profiling_report( instruction_history, &bytecode.function_locations, @@ -383,20 +466,38 @@ fn handle_parallel_batch( fp: &mut usize, ap: &mut usize, batch: &ParallelBatchInfo, -) -> Result<(), RunnerError> { - let start_value = memory.get(batch.batch_fp + 2)?.to_usize(); - let end_value = batch.end_value.read_value(memory, batch.batch_fp)?.to_usize(); + stack_trace: bool, +) -> Result<(), ExecutionFailure> { + let start_value = memory + .get(batch.batch_fp + 2) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace))? + .to_usize(); + let end_value = batch + .end_value + .read_value(memory, batch.batch_fp) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace))? + .to_usize(); let n_iters = end_value.saturating_sub(start_value); if n_iters <= 1 { return Ok(()); } let stride = *fp - batch.batch_fp; - let return_pc = memory.get(*fp)?.to_usize(); - let saved_fp = memory.get(*fp + 1)?.to_usize(); + let return_pc = memory + .get(*fp) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace))? + .to_usize(); + let saved_fp = memory + .get(*fp + 1) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace))? + .to_usize(); let args: Vec = (0..batch.n_args) - .map(|i| memory.get(batch.batch_fp + 2 + i).unwrap()) - .collect(); + .map(|i| { + memory + .get(batch.batch_fp + 2 + i) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace)) + }) + .collect::>()?; // Per-name deltas for named hints (measured from iteration 0). let named_per_iter: HashMap = named_hints @@ -413,12 +514,20 @@ fn handle_parallel_batch( saved_fp, iter_val, &args, - )?; + ) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace))?; } let max_addr = batch.batch_fp + (n_iters + 1) * stride; if max_addr > 1 << MAX_LOG_MEMORY_SIZE { - return Err(RunnerError::OutOfMemory); + return Err(ExecutionFailure::new( + bytecode, + memory, + *pc, + *fp, + RunnerError::OutOfMemory, + stack_trace, + )); } if max_addr > memory.0.len() { memory.0.resize(max_addr, None); @@ -434,7 +543,7 @@ fn handle_parallel_batch( let shared: &[Option] = &*left; let segment_slices: Vec<&mut [Option]> = right.chunks_mut(stride).take(n_par).collect(); - type SegResult = Result<(Trace, Vec<(usize, F)>), RunnerError>; + type SegResult = Result<(Trace, Vec<(usize, F)>), ParallelSegmentFailure>; let results: Vec = segment_slices .into_par_iter() .enumerate() @@ -467,13 +576,24 @@ fn handle_parallel_batch( &mut seg_ap, &mut hints, Some(batch.batch_pc), - )?; + ) + .map_err(|err| ParallelSegmentFailure { + segment_id: i + 1, + pc: seg_pc, + fp: seg_fp, + err, + })?; for (name, delta) in &named_per_iter { let consumed = seg_named_hints[name].index - seg_start_indices[name]; if consumed != *delta { - return Err(RunnerError::InvalidHintWitness(format!( - "hint '{name}' consumed {consumed} entries in a parallel iteration but {delta} in iteration 0; parallel iterations must consume hints uniformly" - ))); + return Err(ParallelSegmentFailure { + segment_id: i + 1, + pc: seg_pc, + fp: seg_fp, + err: RunnerError::InvalidHintWitness(format!( + "hint '{name}' consumed {consumed} entries in a parallel iteration but {delta} in iteration 0; parallel iterations must consume hints uniformly" + )), + }); } } let deferred = seg_mem.into_deferred_writes(); @@ -481,11 +601,22 @@ fn handle_parallel_batch( }) .collect(); - for (idx, result) in results.into_iter().enumerate() { - let (seg_trace, deferred) = result.map_err(|e| RunnerError::ParallelSegmentFailed(idx + 1, Box::new(e)))?; + for result in results { + let (seg_trace, deferred) = result.map_err(|failure| { + ExecutionFailure::new( + bytecode, + memory, + failure.pc, + failure.fp, + RunnerError::ParallelSegmentFailed(failure.segment_id, Box::new(failure.err)), + stack_trace, + ) + })?; trace.merge(seg_trace); for (addr, val) in deferred { - memory.set(addr, val)?; + memory + .set(addr, val) + .map_err(|e| ExecutionFailure::new(bytecode, memory, *pc, *fp, e, stack_trace))?; } } diff --git a/crates/lean_vm/src/isa/bytecode.rs b/crates/lean_vm/src/isa/bytecode.rs index b21e7e91a..6b4f04cbe 100644 --- a/crates/lean_vm/src/isa/bytecode.rs +++ b/crates/lean_vm/src/isa/bytecode.rs @@ -2,7 +2,7 @@ use backend::*; -use crate::{DIMENSION, F, FileId, FunctionName, Hint, N_INSTRUCTION_COLUMNS, SourceLocation}; +use crate::{CodeAddress, DIMENSION, F, FileId, FunctionName, Hint, N_INSTRUCTION_COLUMNS, SourceLocation}; use super::Instruction; use std::collections::BTreeMap; @@ -14,6 +14,15 @@ pub struct CodeEntry { pub instruction: Instruction, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CallSite { + pub caller: FunctionName, + pub callee: FunctionName, + pub location: SourceLocation, + pub call_pc: CodeAddress, + pub return_pc: CodeAddress, +} + /// `instructions_multilinear`, `hash`, and `ending_pc` must be checked at initialization to match `code`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Bytecode { @@ -29,6 +38,8 @@ pub struct Bytecode { pub source_code: BTreeMap, /// Maps each pc to its source location (for error reporting) pub pc_to_location: Vec, + /// Maps a function-call continuation pc to source-level call-site metadata. + pub call_sites_by_return_pc: BTreeMap, } impl Bytecode {