Skip to content

Commit a1ecd06

Browse files
authored
Track nested scopes during debugging (#1442)
This adds new execution graph nodes for pushing and popping variable scopes that are used only while debugging (like statement nodes) so that the evaluator can correctly respond to local variable requests during debugging. Fixes #1436
1 parent 0c92116 commit a1ecd06

File tree

3 files changed

+64
-21
lines changed

3 files changed

+64
-21
lines changed

compiler/qsc_eval/src/lib.rs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -563,27 +563,9 @@ impl State {
563563
self.idx += 1;
564564
self.current_span = globals.get_stmt((self.package, *stmt).into()).span;
565565

566-
if let Some(bp) = breakpoints
567-
.iter()
568-
.find(|&bp| *bp == *stmt && self.package == self.source_package)
569-
{
570-
StepResult::BreakpointHit(*bp)
571-
} else {
572-
if self.current_span == Span::default() {
573-
// if there is no span, we are in generated code, so we should skip
574-
continue;
575-
}
576-
// no breakpoint, but we may stop here
577-
if step == StepAction::In {
578-
StepResult::StepIn
579-
} else if step == StepAction::Next && current_frame >= self.call_stack.len()
580-
{
581-
StepResult::Next
582-
} else if step == StepAction::Out && current_frame > self.call_stack.len() {
583-
StepResult::StepOut
584-
} else {
585-
continue;
586-
}
566+
match self.check_for_break(breakpoints, *stmt, step, current_frame) {
567+
Some(value) => value,
568+
None => continue,
587569
}
588570
}
589571
Some(ExecGraphNode::Jump(idx)) => {
@@ -623,6 +605,16 @@ impl State {
623605
env.leave_scope();
624606
continue;
625607
}
608+
Some(ExecGraphNode::PushScope) => {
609+
self.push_scope(env);
610+
self.idx += 1;
611+
continue;
612+
}
613+
Some(ExecGraphNode::PopScope) => {
614+
env.leave_scope();
615+
self.idx += 1;
616+
continue;
617+
}
626618
None => {
627619
// We have reached the end of the current graph without reaching an explicit return node,
628620
// usually indicating the partial execution of a single sub-expression.
@@ -644,6 +636,38 @@ impl State {
644636
Ok(StepResult::Return(self.get_result()))
645637
}
646638

639+
fn check_for_break(
640+
&self,
641+
breakpoints: &[StmtId],
642+
stmt: StmtId,
643+
step: StepAction,
644+
current_frame: usize,
645+
) -> Option<StepResult> {
646+
Some(
647+
if let Some(bp) = breakpoints
648+
.iter()
649+
.find(|&bp| *bp == stmt && self.package == self.source_package)
650+
{
651+
StepResult::BreakpointHit(*bp)
652+
} else {
653+
if self.current_span == Span::default() {
654+
// if there is no span, we are in generated code, so we should skip
655+
return None;
656+
}
657+
// no breakpoint, but we may stop here
658+
if step == StepAction::In {
659+
StepResult::StepIn
660+
} else if step == StepAction::Next && current_frame >= self.call_stack.len() {
661+
StepResult::Next
662+
} else if step == StepAction::Out && current_frame > self.call_stack.len() {
663+
StepResult::StepOut
664+
} else {
665+
return None;
666+
}
667+
},
668+
)
669+
}
670+
647671
pub fn get_result(&mut self) -> Value {
648672
// Some executions don't have any statements to execute,
649673
// such as a fragment that has only item definitions.

compiler/qsc_fir/src/fir.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,10 @@ pub enum ExecGraphNode {
900900
Unit,
901901
/// The end of the control flow graph.
902902
Ret,
903+
/// A push of a new scope, used when tracking variables for debugging.
904+
PushScope,
905+
/// A pop of the current scope, used when tracking variables for debugging.
906+
PopScope,
903907
}
904908

905909
/// A sequenced block of statements.

compiler/qsc_lowerer/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,18 @@ impl Lowerer {
274274

275275
fn lower_block(&mut self, block: &hir::Block) -> BlockId {
276276
let id = self.assigner.next_block();
277+
// When lowering for debugging, we need to be more strict about scoping for variables
278+
// otherwise variables that are not in scope will be visible in the locals view.
279+
// We push a scope entry marker, `PushScope`, here and then a `PopScope` marker at the
280+
// end of the block, which will cause the evaluation logic to track local variables
281+
// for this block in the innermost scope matching their actual accessibility.
282+
// When not in debug mode, variables may persist across block boundaries, but all access
283+
// is performed via their lowered local variable ID, so they cannot be accessed outside of
284+
// their scope. Associated memory is still cleaned up at callable exit rather than block
285+
// exit.
286+
if self.enable_debug {
287+
self.exec_graph.push(ExecGraphNode::PushScope);
288+
}
277289
let set_unit = block.stmts.is_empty()
278290
|| !matches!(
279291
block.stmts.last().expect("block should be non-empty").kind,
@@ -288,6 +300,9 @@ impl Lowerer {
288300
if set_unit {
289301
self.exec_graph.push(ExecGraphNode::Unit);
290302
}
303+
if self.enable_debug {
304+
self.exec_graph.push(ExecGraphNode::PopScope);
305+
}
291306
self.blocks.insert(id, block);
292307
id
293308
}

0 commit comments

Comments
 (0)