Skip to content

Commit b3a4022

Browse files
authored
Improve runtime error debugging (#2592)
This change updates the way that runtime errors are reported from debug execution, allowing the VS Code debugger to break in and treat those as "exceptions" with detailed information. The tweaks avoid extra checks during the tight loop for evaluation, and keeps behavior the same for non-debug execution (like what Python native binaries do).
1 parent 50bbed2 commit b3a4022

File tree

10 files changed

+129
-61
lines changed

10 files changed

+129
-61
lines changed

source/compiler/qsc/src/interpret.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use qsc_data_structures::{
5151
target::TargetCapabilityFlags,
5252
};
5353
use qsc_eval::{
54-
Env, State, VariableInfo,
54+
Env, ErrorBehavior, State, VariableInfo,
5555
backend::{Backend, Chain as BackendChain, SparseSim},
5656
output::Receiver,
5757
val,
@@ -1051,7 +1051,12 @@ impl Debugger {
10511051
Ok(Self {
10521052
interpreter,
10531053
position_encoding,
1054-
state: State::new(source_package_id, entry_exec_graph, None),
1054+
state: State::new(
1055+
source_package_id,
1056+
entry_exec_graph,
1057+
None,
1058+
ErrorBehavior::StopOnError,
1059+
),
10551060
})
10561061
}
10571062

@@ -1062,7 +1067,12 @@ impl Debugger {
10621067
Self {
10631068
interpreter,
10641069
position_encoding,
1065-
state: State::new(source_package_id, entry_exec_graph, None),
1070+
state: State::new(
1071+
source_package_id,
1072+
entry_exec_graph,
1073+
None,
1074+
ErrorBehavior::StopOnError,
1075+
),
10661076
}
10671077
}
10681078

source/compiler/qsc_eval/src/lib.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ pub fn eval(
290290
sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
291291
receiver: &mut impl Receiver,
292292
) -> Result<Value, (Error, Vec<Frame>)> {
293-
let mut state = State::new(package, exec_graph, seed);
293+
let mut state = State::new(package, exec_graph, seed, ErrorBehavior::FailOnError);
294294
let res = state.eval(globals, env, sim, receiver, &[], StepAction::Continue)?;
295295
let StepResult::Return(value) = res else {
296296
panic!("eval should always return a value");
@@ -314,7 +314,7 @@ pub fn invoke(
314314
callable: Value,
315315
args: Value,
316316
) -> Result<Value, (Error, Vec<Frame>)> {
317-
let mut state = State::new(package, Vec::new().into(), seed);
317+
let mut state = State::new(package, Vec::new().into(), seed, ErrorBehavior::FailOnError);
318318
// Push the callable value into the state stack and then the args value so they are ready for evaluation.
319319
state.set_val_register(callable);
320320
state.push_val();
@@ -360,7 +360,7 @@ pub enum StepResult {
360360
StepIn,
361361
StepOut,
362362
Return(Value),
363-
Fail,
363+
Fail(String),
364364
}
365365

366366
trait AsIndex {
@@ -560,6 +560,14 @@ struct Scope {
560560

561561
type CallableCountKey = (StoreItemId, bool, bool);
562562

563+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
564+
pub enum ErrorBehavior {
565+
/// Fail execution if an error is encountered.
566+
FailOnError,
567+
/// Stop execution on the first error encountered.
568+
StopOnError,
569+
}
570+
563571
pub struct State {
564572
exec_graph_stack: Vec<ExecGraph>,
565573
idx: u32,
@@ -573,11 +581,18 @@ pub struct State {
573581
rng: RefCell<StdRng>,
574582
call_counts: FxHashMap<CallableCountKey, i64>,
575583
qubit_counter: Option<QubitCounter>,
584+
error_behavior: ErrorBehavior,
585+
last_error: Option<(Error, Vec<Frame>)>,
576586
}
577587

578588
impl State {
579589
#[must_use]
580-
pub fn new(package: PackageId, exec_graph: ExecGraph, classical_seed: Option<u64>) -> Self {
590+
pub fn new(
591+
package: PackageId,
592+
exec_graph: ExecGraph,
593+
classical_seed: Option<u64>,
594+
error_behavior: ErrorBehavior,
595+
) -> Self {
581596
let rng = match classical_seed {
582597
Some(seed) => RefCell::new(StdRng::seed_from_u64(seed)),
583598
None => RefCell::new(StdRng::from_entropy()),
@@ -595,6 +610,8 @@ impl State {
595610
rng,
596611
call_counts: FxHashMap::default(),
597612
qubit_counter: None,
613+
error_behavior,
614+
last_error: None,
598615
}
599616
}
600617

@@ -671,6 +688,23 @@ impl State {
671688
frames
672689
}
673690

691+
fn set_last_error(&mut self, error: Error, frames: Vec<Frame>) {
692+
assert!(
693+
self.last_error.replace((error, frames)).is_none(),
694+
"last error should not be set twice"
695+
);
696+
}
697+
698+
fn get_last_error(&mut self) -> Result<(), (Error, Vec<Frame>)> {
699+
// Use `is_none` to check for last error, as it avoids the unconditional
700+
// `mem::replace` call that `take` would perform.
701+
if self.last_error.is_none() {
702+
Ok(())
703+
} else {
704+
Err(self.last_error.take().expect("last error should be set"))
705+
}
706+
}
707+
674708
/// # Errors
675709
/// Returns the first error encountered during execution.
676710
/// # Panics
@@ -698,9 +732,20 @@ impl State {
698732
}
699733
Some(ExecGraphNode::Expr(expr)) => {
700734
self.idx += 1;
701-
self.eval_expr(env, sim, globals, out, *expr)
702-
.map_err(|e| (e, self.get_stack_frames()))?;
703-
continue;
735+
match self.eval_expr(env, sim, globals, out, *expr) {
736+
Ok(()) => continue,
737+
Err(e) => {
738+
if self.error_behavior == ErrorBehavior::StopOnError {
739+
let error_str = e.to_string();
740+
self.set_last_error(e, self.get_stack_frames());
741+
// Clear the execution graph stack to indicate that execution has failed.
742+
// This will prevent further execution steps.
743+
self.exec_graph_stack.clear();
744+
return Ok(StepResult::Fail(error_str));
745+
}
746+
return Err((e, self.get_stack_frames()));
747+
}
748+
}
704749
}
705750
Some(ExecGraphNode::Stmt(stmt)) => {
706751
self.idx += 1;
@@ -711,10 +756,6 @@ impl State {
711756
None => continue,
712757
}
713758
}
714-
Some(ExecGraphNode::Fail) => {
715-
self.idx += 1;
716-
return Ok(StepResult::Fail);
717-
}
718759
Some(ExecGraphNode::Jump(idx)) => {
719760
self.idx = *idx;
720761
continue;
@@ -785,6 +826,11 @@ impl State {
785826
return Ok(res);
786827
}
787828

829+
// If we made it out of the execution loop, we either reached the end of the graph,
830+
// a return expression, or hit a runtime error. Check here for the error case
831+
// and return it if it exists.
832+
self.get_last_error()?;
833+
788834
Ok(StepResult::Return(self.get_result()))
789835
}
790836

@@ -1175,6 +1221,7 @@ impl State {
11751221
out: &mut impl Receiver,
11761222
) -> Result<(), Error> {
11771223
self.push_frame(Vec::new().into(), callee_id, functor);
1224+
self.current_span = callee_span.span;
11781225
self.increment_call_count(callee_id, functor);
11791226
let name = &callee.name.name;
11801227
let val = match name.as_ref() {

source/compiler/qsc_eval/src/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
use crate::{
5-
Env, Error, State, StepAction, StepResult, Value,
5+
Env, Error, ErrorBehavior, State, StepAction, StepResult, Value,
66
backend::{Backend, SparseSim},
77
debug::Frame,
88
exec_graph_section,
@@ -30,7 +30,7 @@ pub(super) fn eval_graph(
3030
env: &mut Env,
3131
out: &mut impl Receiver,
3232
) -> Result<Value, (Error, Vec<Frame>)> {
33-
let mut state = State::new(package, graph, None);
33+
let mut state = State::new(package, graph, None, ErrorBehavior::FailOnError);
3434
let StepResult::Return(value) =
3535
state.eval(globals, env, sim, out, &[], StepAction::Continue)?
3636
else {

source/compiler/qsc_fir/src/fir.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -929,8 +929,6 @@ pub enum ExecGraphNode {
929929
PushScope,
930930
/// A pop of the current scope, used when tracking variables for debugging.
931931
PopScope,
932-
/// A failure node, inserted just before a `fail` expression to halt execution for debugging.
933-
Fail,
934932
}
935933

936934
/// A sequenced block of statements.

source/compiler/qsc_lowerer/src/lib.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -560,11 +560,7 @@ impl Lowerer {
560560
hir::ExprKind::Fail(message) => {
561561
// Ensure the right-hand side expression is lowered first so that it
562562
// is executed before the fail node, if any.
563-
let fail = fir::ExprKind::Fail(self.lower_expr(message));
564-
if self.enable_debug {
565-
self.exec_graph.push(ExecGraphNode::Fail);
566-
}
567-
fail
563+
fir::ExprKind::Fail(self.lower_expr(message))
568564
}
569565
hir::ExprKind::Field(container, field) => {
570566
let container = self.lower_expr(container);

source/compiler/qsc_partial_eval/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use management::{QuantumIntrinsicsChecker, ResourceManager};
1717
use miette::Diagnostic;
1818
use qsc_data_structures::{functors::FunctorApp, span::Span, target::TargetCapabilityFlags};
1919
use qsc_eval::{
20-
self, Error as EvalError, PackageSpan, State, StepAction, StepResult, Variable,
20+
self, Error as EvalError, ErrorBehavior, PackageSpan, State, StepAction, StepResult, Variable,
2121
are_ctls_unique, exec_graph_section,
2222
intrinsic::qubit_relabel,
2323
output::GenericReceiver,
@@ -1084,7 +1084,12 @@ impl<'a> PartialEvaluator<'a> {
10841084
let scope_exec_graph = self.get_current_scope_exec_graph().clone();
10851085
let scope = self.eval_context.get_current_scope_mut();
10861086
let exec_graph = exec_graph_section(&scope_exec_graph, expr.exec_graph_range.clone());
1087-
let mut state = State::new(current_package_id, exec_graph, None);
1087+
let mut state = State::new(
1088+
current_package_id,
1089+
exec_graph,
1090+
None,
1091+
ErrorBehavior::FailOnError,
1092+
);
10881093
let classical_result = state.eval(
10891094
self.package_store,
10901095
&mut scope.env,

source/compiler/qsc_passes/src/replace_qubit_allocation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ fn create_general_dealloc_stmt(
515515
span: Span::default(),
516516
kind: StmtKind::Semi(Expr {
517517
id: assigner.next_node(),
518-
span: Span::default(),
518+
span: ident.span,
519519
ty: Ty::UNIT,
520520
kind: ExprKind::Call(Box::new(call_expr), Box::new(ident.gen_local_ref(assigner))),
521521
}),

0 commit comments

Comments
 (0)