Repository: microsoft/qsharp
Pull request #1039 (2024-01-18 20:45:43)

Separate Debugger into a distinct struct from Interpreter
The immediate, selfish motivation for this change is that I want to add a parameter to the `Interpreter` constructor (in #1038) that is really only relevant to debugger scenarios, and I didn't want to update every single call to `Interpreter::new`.

But I think this is a good change regardless. Some methods and fields in `Interpreter` are only applicable to debugging, and the rest are only applicable to not-debugging. It's really two separate use cases and two separate sets of data. I wanted to make this distinction clearer.

Very open to feedback and opinions, and suggestions for further cleanup.

github-actions[bot]: ## Benchmark for 4c4886d
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 815.7±9.27µs | **804.9±15.78µs** | **-1.32%** |
| Array update evaluation | 892.6±26.40µs | **861.8±5.93µs** | **-3.45%** |
| Deutsch-Jozsa evaluation | 29.0±0.69ms | 28.9±0.42ms | -0.34% |
| Large file parity evaluation | 33.4±0.21ms | **33.2±0.14ms** | **-0.60%** |
| Large input file | 35.5±0.79ms | 35.8±0.71ms | +0.85% |
| Standard library | 19.2±0.19ms | 19.2±0.25ms | 0.00% |
| Teleport evaluation | 97.0±2.29µs | 96.0±1.58µs | -1.03% |

  </details>
  
github-actions[bot]: ## Benchmark for e719ca5
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **866.1±11.10µs** | 877.4±5.05µs | **+1.30%** |
| Array update evaluation | **943.7±5.66µs** | 954.3±7.86µs | **+1.12%** |
| Deutsch-Jozsa evaluation | 31.2±0.21ms | 31.1±0.22ms | -0.32% |
| Large file parity evaluation | **35.8±0.10ms** | 36.0±0.49ms | **+0.56%** |
| Large input file | 41.1±1.10ms | 41.1±1.04ms | 0.00% |
| Standard library | 21.5±0.43ms | 21.4±0.46ms | -0.47% |
| Teleport evaluation | 101.4±1.89µs | 101.3±1.60µs | -0.10% |

  </details>
  
minestarks: @cesarzc would you mind taking a look? I'm touching a couple of folders where you're the owner.

Patch:
diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs
index 1768ad28..2db6e9e3 100644
--- a/compiler/qsc/src/interpret.rs
+++ b/compiler/qsc/src/interpret.rs
@@ -7,7 +7,7 @@ mod debug;
 mod tests;
 
 #[cfg(test)]
-mod stepping_tests;
+mod debugger_tests;
 
 pub use qsc_eval::{
     debug::Frame,
@@ -106,8 +106,6 @@ pub struct Interpreter {
     classical_seed: Option<u64>,
     /// The evaluator environment.
     env: Env,
-    /// The current state of the evaluator.
-    state: State,
 }
 
 #[allow(clippy::module_name_repetitions)]
@@ -149,7 +147,6 @@ impl Interpreter {
             sim: SparseSim::new(),
             quantum_seed: None,
             classical_seed: None,
-            state: State::new(map_hir_package_to_fir(source_package_id), None),
             package: map_hir_package_to_fir(package_id),
             source_package: map_hir_package_to_fir(source_package_id),
         })
@@ -163,46 +160,6 @@ impl Interpreter {
     pub fn set_classical_seed(&mut self, seed: Option<u64>) {
         self.classical_seed = seed;
     }
-
-    /// Loads the entry expression to the top of the evaluation stack.
-    /// This is needed for debugging so that when begging to debug with
-    /// a step action the system is already in the correct state.
-    /// # Errors
-    /// Returns a vector of errors if loading the entry point fails.
-    pub fn set_entry(&mut self) -> Result<(), Vec<Error>> {
-        let expr = self.get_entry_expr()?;
-        qsc_eval::eval_push_expr(&mut self.state, expr);
-        Ok(())
-    }
-
-    /// Resumes execution with specified `StepAction`.
-    /// # Errors
-    /// Returns a vector of errors if evaluating the entry point fails.
-    pub fn eval_step(
-        &mut self,
-        receiver: &mut impl Receiver,
-        breakpoints: &[StmtId],
-        step: StepAction,
-    ) -> Result<StepResult, Vec<Error>> {
-        self.state
-            .eval(
-                &self.fir_store,
-                &mut self.env,
-                &mut self.sim,
-                receiver,
-                breakpoints,
-                step,
-            )
-            .map_err(|(error, call_stack)| {
-                eval_error(
-                    self.compiler.package_store(),
-                    &self.fir_store,
-                    call_stack,
-                    error,
-                )
-            })
-    }
-
     /// Executes the entry expression until the end of execution.
     /// # Errors
     /// Returns a vector of errors if evaluating the entry point fails.
@@ -382,18 +339,72 @@ impl Interpreter {
             .lower_and_update_package(fir_package, &unit_addition.hir)
     }
 
-    fn source_package(&self) -> &CompileUnit {
-        self.compiler
-            .package_store()
-            .get(map_fir_package_to_hir(self.source_package))
-            .expect("Could not load package")
-    }
-
     fn next_line_label(&mut self) -> String {
         let label = format!("line_{}", self.lines);
         self.lines += 1;
         label
     }
+}
+
+/// A debugger that enables step-by-step evaluation of code
+/// and inspecting state in the interpreter.
+pub struct Debugger {
+    interpreter: Interpreter,
+    /// The current state of the evaluator.
+    state: State,
+}
+
+impl Debugger {
+    pub fn new(
+        sources: SourceMap,
+        capabilities: RuntimeCapabilityFlags,
+    ) -> Result<Self, Vec<Error>> {
+        let interpreter = Interpreter::new(true, sources, PackageType::Exe, capabilities)?;
+        let source_package_id = interpreter.source_package;
+        Ok(Self {
+            interpreter,
+            state: State::new(source_package_id, None),
+        })
+    }
+
+    /// Loads the entry expression to the top of the evaluation stack.
+    /// This is needed for debugging so that when begging to debug with
+    /// a step action the system is already in the correct state.
+    /// # Errors
+    /// Returns a vector of errors if loading the entry point fails.
+    pub fn set_entry(&mut self) -> Result<(), Vec<Error>> {
+        let expr = self.interpreter.get_entry_expr()?;
+        qsc_eval::eval_push_expr(&mut self.state, expr);
+        Ok(())
+    }
+
+    /// Resumes execution with specified `StepAction`.
+    /// # Errors
+    /// Returns a vector of errors if evaluating the entry point fails.
+    pub fn eval_step(
+        &mut self,
+        receiver: &mut impl Receiver,
+        breakpoints: &[StmtId],
+        step: StepAction,
+    ) -> Result<StepResult, Vec<Error>> {
+        self.state
+            .eval(
+                &self.interpreter.fir_store,
+                &mut self.interpreter.env,
+                &mut self.interpreter.sim,
+                receiver,
+                breakpoints,
+                step,
+            )
+            .map_err(|(error, call_stack)| {
+                eval_error(
+                    self.interpreter.compiler.package_store(),
+                    &self.interpreter.fir_store,
+                    call_stack,
+                    error,
+                )
+            })
+    }
 
     #[must_use]
     pub fn get_stack_frames(&self) -> Vec<StackFrame> {
@@ -402,6 +413,7 @@ impl Interpreter {
             .iter()
             .map(|frame| {
                 let callable = self
+                    .interpreter
                     .fir_store
                     .get_global(frame.id)
                     .expect("frame should exist");
@@ -412,6 +424,7 @@ impl Interpreter {
                 };
 
                 let hir_package = self
+                    .interpreter
                     .compiler
                     .package_store()
                     .get(map_fir_package_to_hir(frame.id.package))
@@ -434,7 +447,7 @@ impl Interpreter {
     }
 
     pub fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex<f64>)>, usize) {
-        self.sim.capture_quantum_state()
+        self.interpreter.sim.capture_quantum_state()
     }
 
     #[must_use]
@@ -443,8 +456,9 @@ impl Interpreter {
 
         if let Some(source) = unit.sources.find_by_name(path) {
             let package = self
+                .interpreter
                 .fir_store
-                .get(self.source_package)
+                .get(self.interpreter.source_package)
                 .expect("package should have been lowered");
             let mut collector = BreakpointCollector::new(&unit.sources, source.offset, package);
             collector.visit_package(package);
@@ -466,12 +480,21 @@ impl Interpreter {
 
     #[must_use]
     pub fn get_locals(&self) -> Vec<VariableInfo> {
-        self.env
+        self.interpreter
+            .env
             .get_variables_in_top_frame()
             .into_iter()
             .filter(|v| !v.name.starts_with('@'))
             .collect()
     }
+
+    fn source_package(&self) -> &CompileUnit {
+        self.interpreter
+            .compiler
+            .package_store()
+            .get(map_fir_package_to_hir(self.interpreter.source_package))
+            .expect("Could not load package")
+    }
 }
 
 /// Wrapper function for `qsc_eval::eval` that handles error conversion.
diff --git a/npm/ux/estimatesOverview.tsx b/npm/ux/estimatesOverview.tsx
index 6a0069e2..a1c220a3 100644
--- a/npm/ux/estimatesOverview.tsx
+++ b/npm/ux/estimatesOverview.tsx
@@ -4,6 +4,7 @@
 // A component including the results table and the scatter chart together.
 // The results table is also a legend for the scatter chart.
 
+import { useState } from "preact/hooks";
 import { ColorMap } from "./colormap.js";
 import {
   CreateSingleEstimateResult,
@@ -18,6 +19,7 @@ import {
   PlotItem,
   ScatterChart,
   ScatterSeries,
+  SelectPoint,
 } from "./scatterChart.js";
 
 const columnNames = [
@@ -46,7 +48,7 @@ const xAxis: Axis = {
 
 const yAxis: Axis = {
   isTime: false,
-  label: "Physical Qubits",
+  label: "Physical qubits",
 };
 
 function reDataToRow(input: ReData, color: string): Row {
@@ -123,6 +125,8 @@ export function EstimatesOverview(props: {
   onRowDeleted: (rowId: string) => void;
   setEstimate: (estimate: SingleEstimateResult | null) => void;
 }) {
+  const [selectedRow, setSelectedRow] = useState<string | null>(null);
+
   props.estimatesData.forEach((item, idx) => {
     if (
       props.runNames != null &&
@@ -140,26 +144,31 @@ export function EstimatesOverview(props: {
   function onPointSelected(seriesIndex: number, pointIndex: number): void {
     const data = props.estimatesData[seriesIndex];
     props.setEstimate(CreateSingleEstimateResult(data, pointIndex));
+    const rowId = props.estimatesData[seriesIndex].jobParams.runName;
+    setSelectedRow(rowId);
   }
 
   function onRowSelected(rowId: string) {
+    setSelectedRow(rowId);
     // On any selection, clear the "new" flag on all rows. This ensures that
     // new rows do not steal focus from the user selected row.
     props.estimatesData.forEach((data) => (data.new = false));
+    HideTooltip();
     if (!rowId) {
       props.setEstimate(null);
     } else {
-      const estimateFound =
-        props.estimatesData.find((data) => data.jobParams.runName === rowId) ||
-        null;
-      if (estimateFound == null) {
+      const index = props.estimatesData.findIndex(
+        (data) => data.jobParams.runName === rowId,
+      );
+
+      if (index == -1) {
         props.setEstimate(null);
       } else {
+        const estimateFound = props.estimatesData[index];
         props.setEstimate(CreateSingleEstimateResult(estimateFound, 0));
+        SelectPoint(index, 0);
       }
     }
-
-    HideTooltip();
   }
 
   const colormap =
@@ -177,8 +186,9 @@ export function EstimatesOverview(props: {
           )}
           initialColumns={initialColumns}
           ensureSelected={true}
-          onRowSelected={onRowSelected}
           onRowDeleted={props.onRowDeleted}
+          selectedRow={selectedRow}
+          setSelectedRow={onRowSelected}
         />
         <ScatterChart
           xAxis={xAxis}
@@ -204,8 +214,9 @@ export function EstimatesOverview(props: {
             reDataToRow(dataItem, colormap[index]),
           )}
           initialColumns={initialColumns}
+          selectedRow={selectedRow}
+          setSelectedRow={onRowSelected}
           ensureSelected={true}
-          onRowSelected={onRowSelected}
           onRowDeleted={props.onRowDeleted}
         />
       </details>
diff --git a/npm/ux/qsharp-ux.css b/npm/ux/qsharp-ux.css
index 5e0fe6bb..8ea7fd22 100644
--- a/npm/ux/qsharp-ux.css
+++ b/npm/ux/qsharp-ux.css
@@ -641,9 +641,11 @@ html {
   stroke-width: 4px;
 }
 
+.qs-scatterChart-point-selected,
 .qs-scatterChart-point:hover {
-  r: 3px;
-  stroke-width: 6px;
+  r: 6px;
+  stroke-width: 3px;
+  fill: white;
 }
 
 .qs-scatterChart-watermark {
diff --git a/npm/ux/resultsTable.tsx b/npm/ux/resultsTable.tsx
index df93d01d..bab3d5f8 100644
--- a/npm/ux/resultsTable.tsx
+++ b/npm/ux/resultsTable.tsx
@@ -15,15 +15,16 @@ export function ResultsTable(props: {
   rows: Row[];
   initialColumns: number[];
   ensureSelected: boolean;
-  onRowSelected(rowId: string): void;
   onRowDeleted(rowId: string): void;
+  selectedRow: string | null; // type selected to confirm with the useState pattern on the parent component
+  setSelectedRow(rowId: string): void;
 }) {
   const [showColumns, setShowColumns] = useState(props.initialColumns);
   const [sortColumn, setSortColumn] = useState<{
     columnId: number;
     ascending: boolean;
   } | null>(null);
-  const [selectedRow, setSelectedRow] = useState<string>("");
+
   const [showColumnMenu, setShowColumnMenu] = useState(false);
   const [showRowMenu, setShowRowMenu] = useState("");
 
@@ -36,11 +37,13 @@ export function ResultsTable(props: {
   if (newest && props.ensureSelected) {
     const rowId = newest.cells[0].toString();
     setSelectedRow(rowId);
-    props.onRowSelected(rowId);
-  } else if (!selectedRow && props.ensureSelected && props.rows.length > 0) {
+  } else if (
+    !props.selectedRow &&
+    props.ensureSelected &&
+    props.rows.length > 0
+  ) {
     const rowId = props.rows[0].cells[0].toString();
     setSelectedRow(rowId);
-    props.onRowSelected(rowId);
   }
 
   // Use to track the column being dragged
@@ -80,6 +83,10 @@ export function ResultsTable(props: {
     }
   }
 
+  function setSelectedRow(rowId: string) {
+    props.setSelectedRow(rowId);
+  }
+
   function onDragOver(ev: DragEvent) {
     if (!(ev.target instanceof HTMLElement)) return;
     const thisColId = ev.target.closest("th")?.dataset["colid"];
@@ -194,11 +201,10 @@ export function ResultsTable(props: {
   }
 
   function rowClicked(rowId: string) {
-    if (selectedRow === rowId && props.ensureSelected) return;
+    if (props.selectedRow === rowId && props.ensureSelected) return;
 
-    const newSelectedRow = selectedRow === rowId ? "" : rowId;
+    const newSelectedRow = props.selectedRow === rowId ? "" : rowId;
     setSelectedRow(newSelectedRow);
-    props.onRowSelected(newSelectedRow);
   }
 
   function onClickRowMenu(ev: MouseEvent, rowid: string) {
@@ -247,9 +253,8 @@ export function ResultsTable(props: {
     e.stopPropagation();
     // Clear out any menus or selections for the row if needed
     setShowRowMenu("");
-    if (selectedRow === rowId) {
+    if (props.selectedRow === rowId) {
       setSelectedRow("");
-      props.onRowSelected("");
     }
     props.onRowDeleted(rowId);
   }
@@ -352,7 +357,7 @@ export function ResultsTable(props: {
               onClick={() => rowClicked(rowId)}
               data-rowid={rowId}
               class={
-                rowId === selectedRow
+                rowId === props.selectedRow
                   ? "qs-resultsTable-sortedTableSelectedRow"
                   : undefined
               }
diff --git a/npm/ux/scatterChart.tsx b/npm/ux/scatterChart.tsx
index f7b4636a..3d99eab5 100644
--- a/npm/ux/scatterChart.tsx
+++ b/npm/ux/scatterChart.tsx
@@ -31,6 +31,45 @@ export function HideTooltip() {
   }
 }
 
+function drawTooltip(target: SVGCircleElement, clicked: boolean = false) {
+  const xAttr = target.getAttribute("cx");
+  const x = xAttr ? parseInt(xAttr) : 0;
+  const yAttr = target.getAttribute("cy");
+  const y = yAttr ? parseInt(yAttr) : -0;
+  const text = target.getAttribute("data-label");
+  const tooltipTextLeftPadding = 5;
+  const tooltipRectanglePaddingHeight = 10;
+  const tooltipTextPaddingHeight = 25;
+  const tooltip = document.getElementById("tooltip");
+  const tooltipRect = document.getElementById("tooltipRect");
+  const tooltipText = document.getElementById(
+    "tooltipText",
+  ) as unknown as SVGTextElement;
+
+  if (tooltipText) {
+    tooltipText.setAttribute("x", (x + tooltipTextLeftPadding).toString());
+    tooltipText.setAttribute("y", (y + tooltipTextPaddingHeight).toString());
+    tooltipText.textContent = text;
+  }
+  if (tooltipRect && tooltipText) {
+    const box = tooltipText.getBBox();
+    const textWidth = box.width;
+    tooltipRect.setAttribute(
+      "width",
+      (textWidth + 2 * tooltipTextLeftPadding).toString(),
+    );
+    tooltipRect.setAttribute("x", x.toString());
+    tooltipRect.setAttribute(
+      "y",
+      (y + tooltipRectanglePaddingHeight).toString(),
+    );
+  }
+  if (tooltip) {
+    tooltip.setAttribute("visibility", "visible");
+    tooltip.setAttribute("clicked", clicked.toString());
+  }
+}
+
 function hideTooltipIfNotClicked() {
   const tooltip = document.getElementById("tooltip");
   if (tooltip) {
@@ -40,6 +79,31 @@ function hideTooltipIfNotClicked() {
   }
 }
 
+function deselectPoint() {
+  const chart = document.getElementById(`scatterChart`);
+  if (chart) {
+    if (chart.getAttribute("selectedPoint")) {
+      const point = document.getElementById(
+        chart.getAttribute("selectedPoint") as string,
+      );
+      if (point) {
+        point.classList.remove("qs-scatterChart-point-selected");
+      }
+    }
+  }
+}
+
+export function SelectPoint(seriesIndex: number, pointIndex: number) {
+  deselectPoint();
+  const point = document.getElementById(`point-${seriesIndex}-${pointIndex}`);
+  const chart = document.getElementById(`scatterChart`);
+  if (point && chart) {
+    point.classList.add("qs-scatterChart-point-selected");
+    chart.setAttribute("selectedPoint", point.id);
+    drawTooltip(point as unknown as SVGCircleElement, true);
+  }
+}
+
 export function ScatterChart(props: {
   data: ScatterSeries[];
   xAxis: Axis;
@@ -110,8 +174,9 @@ export function ScatterChart(props: {
   const yAxisTickCaptionMaxWidth = 100;
   const axisTickLength = 5;
   const axisLineWidth = 1;
-  const xMargin =
+  const xLeftMargin =
     yAxisTitleWidth + yAxisTickCaptionMaxWidth + axisTickLength + axisLineWidth;
+  const xRightMargin = 100; // to show tooltips on the right hand side. If we can move tooltips dynamicslly, we can get rid of this.
 
   const axisTitleHeight = 20;
   const xAxisTickCaptionMaxHeight = 16;
@@ -121,13 +186,13 @@ export function ScatterChart(props: {
     axisTickLength +
     axisLineWidth;
 
-  const svgWidth = 844;
+  const svgWidth = 960;
   const svgViewBoxWidthPadding = 50;
   const svgHeight = 480;
   const svgViewBoxHeightPadding = 10;
-  const svgXMin = -xMargin;
+  const svgXMin = -xLeftMargin;
 
-  const plotAreaWidth = svgWidth - xMargin;
+  const plotAreaWidth = svgWidth - xLeftMargin - xRightMargin;
   const plotAreaHeight = svgHeight - yMargin;
 
   const viewBox = `${svgXMin - svgViewBoxWidthPadding} ${
@@ -136,52 +201,9 @@ export function ScatterChart(props: {
     svgHeight + svgViewBoxHeightPadding
   }`;
 
-  const tooltipTextLeftPadding = 5;
-  const tooltipRectanglePaddingHeight = 10;
-  const tooltipTextPaddingHeight = 25;
-
   const yAxisTextPaddingFromTicks = 5;
   const yAxisTextYPadding = 6;
 
-  function drawTooltip(
-    text: string,
-    x: number,
-    y: number,
-    seriesIndex: number,
-    pointIndex: number,
-    clicked: boolean = false,
-  ) {
-    const tooltip = document.getElementById("tooltip");
-    const tooltipRect = document.getElementById("tooltipRect");
-    const tooltipText = document.getElementById(
-      "tooltipText",
-    ) as unknown as SVGTextElement;
-
-    if (tooltipText) {
-      tooltipText.setAttribute("x", (x + tooltipTextLeftPadding).toString());
-      tooltipText.setAttribute("y", (y + tooltipTextPaddingHeight).toString());
-      tooltipText.textContent = text;
-    }
-    if (tooltipRect && tooltipText) {
-      const box = tooltipText.getBBox();
-      const textWidth = box.width;
-      tooltipRect.setAttribute(
-        "width",
-        (textWidth + 2 * tooltipTextLeftPadding).toString(),
-      );
-      tooltipRect.setAttribute("x", x.toString());
-      tooltipRect.setAttribute(
-        "y",
-        (y + tooltipRectanglePaddingHeight).toString(),
-      );
-    }
-    if (tooltip) {
-      tooltip.setAttribute("visibility", "visible");
-      tooltip.setAttribute("clicked", clicked.toString());
-    }
-    props.onPointSelected(seriesIndex, pointIndex);
-  }
-
   return (
     <div style="display: flex; flex-wrap: wrap; margin-top: 8px;">
       <div class="chart-container">
@@ -272,7 +294,7 @@ export function ScatterChart(props: {
           </text>
 
           <text
-            x={xMargin - axisTitleHeight}
+            x={xLeftMargin - axisTitleHeight}
             y={plotAreaHeight / 2}
             class="qs-scatterChart-y-axisTitle"
           >
@@ -281,7 +303,7 @@ export function ScatterChart(props: {
 
           <text
             class="qs-scatterChart-watermark"
-            x={xMargin - axisTitleHeight}
+            x={xLeftMargin - axisTitleHeight}
             y={-svgHeight + yMargin}
           >
             Created with Azure Quantum Resource Estimator
@@ -305,21 +327,17 @@ export function ScatterChart(props: {
                   id={`point-${seriesIndex}-${pointIndex}`}
                   cx={x}
                   cy={y}
+                  data-label={item.label}
                   class="qs-scatterChart-point"
                   stroke={data.color}
-                  onMouseOver={() =>
-                    drawTooltip(
-                      item.label,
-                      x,
-                      y,
-                      seriesIndex,
-                      pointIndex,
-                      false,
-                    )
-                  }
-                  onClick={() =>
-                    drawTooltip(item.label, x, y, seriesIndex, pointIndex, true)
-                  }
+                  onMouseOver={(e) => {
+                    drawTooltip(e.currentTarget, false);
+                    deselectPoint();
+                  }}
+                  onClick={() => {
+                    SelectPoint(seriesIndex, pointIndex);
+                    props.onPointSelected(seriesIndex, pointIndex);
+                  }}
                   onMouseOut={() => hideTooltipIfNotClicked()}
                 />
               );
diff --git a/wasm/src/debug_service.rs b/wasm/src/debug_service.rs
index eac5db6a..41c94d05 100644
--- a/wasm/src/debug_service.rs
+++ b/wasm/src/debug_service.rs
@@ -4,8 +4,8 @@
 use std::str::FromStr;
 
 use qsc::fir::StmtId;
-use qsc::interpret::{Error, Interpreter, StepAction, StepResult};
-use qsc::{fmt_complex, target::Profile, PackageType, SourceMap};
+use qsc::interpret::{Debugger, Error, StepAction, StepResult};
+use qsc::{fmt_complex, target::Profile};
 
 use crate::{get_source_map, serializable_type, CallbackReceiver};
 use serde::{Deserialize, Serialize};
@@ -13,23 +13,16 @@ use serde_json::json;
 use wasm_bindgen::prelude::*;
 
 #[wasm_bindgen]
+#[derive(Default)]
 pub struct DebugService {
-    interpreter: Interpreter,
+    debugger: Option<Debugger>,
 }
 
 #[wasm_bindgen]
 impl DebugService {
     #[wasm_bindgen(constructor)]
     pub fn new() -> Self {
-        Self {
-            interpreter: Interpreter::new(
-                false,
-                SourceMap::default(),
-                PackageType::Lib,
-                Profile::Unrestricted.into(),
-            )
-            .expect("Couldn't create interpreter"),
-        }
+        Self::default()
     }
 
     pub fn load_source(
@@ -41,10 +34,10 @@ impl DebugService {
         let source_map = get_source_map(sources, entry);
         let target = Profile::from_str(&target_profile)
             .unwrap_or_else(|_| panic!("Invalid target : {}", target_profile));
-        match Interpreter::new(true, source_map, PackageType::Exe, target.into()) {
-            Ok(interpreter) => {
-                self.interpreter = interpreter;
-                match self.interpreter.set_entry() {
+        match Debugger::new(source_map, target.into()) {
+            Ok(debugger) => {
+                self.debugger = Some(debugger);
+                match self.debugger_mut().set_entry() {
                     Ok(()) => "".to_string(),
                     Err(e) => render_errors(e),
                 }
@@ -54,7 +47,7 @@ impl DebugService {
     }
 
     pub fn capture_quantum_state(&mut self) -> IQuantumStateList {
-        let state = self.interpreter.capture_quantum_state();
+        let state = self.debugger_mut().capture_quantum_state();
         let entries = state
             .0
             .iter()
@@ -68,7 +61,7 @@ impl DebugService {
     }
 
     pub fn get_stack_frames(&self) -> IStackFrameList {
-        let frames = self.interpreter.get_stack_frames();
+        let frames = self.debugger().get_stack_frames();
 
         StackFrameList {
             frames: frames
@@ -150,7 +143,7 @@ impl DebugService {
         F: Fn(&str),
     {
         let mut out = CallbackReceiver { event_cb };
-        let result = self.interpreter.eval_step(&mut out, bps, step);
+        let result = self.debugger_mut().eval_step(&mut out, bps, step);
         let mut success = true;
 
         let msg: Option<serde_json::Value> = match &result {
@@ -183,7 +176,7 @@ impl DebugService {
     }
 
     pub fn get_breakpoints(&self, path: &str) -> IBreakpointSpanList {
-        let bps = self.interpreter.get_breakpoints(path);
+        let bps = self.debugger().get_breakpoints(path);
 
         BreakpointSpanList {
             spans: bps
@@ -199,7 +192,7 @@ impl DebugService {
     }
 
     pub fn get_locals(&self) -> IVariableList {
-        let locals = self.interpreter.get_locals();
+        let locals = self.debugger().get_locals();
         let variables: Vec<_> = locals
             .into_iter()
             .map(|local| Variable {
@@ -210,11 +203,17 @@ impl DebugService {
             .collect();
         VariableList { variables }.into()
     }
-}
 
-impl Default for DebugService {
-    fn default() -> Self {
-        Self::new()
+    fn debugger(&self) -> &Debugger {
+        self.debugger
+            .as_ref()
+            .expect("debugger should be initialized")
+    }
+
+    fn debugger_mut(&mut self) -> &mut Debugger {
+        self.debugger
+            .as_mut()
+            .expect("debugger should be initialized")
     }
 }
 


Test patch:
diff --git a/compiler/qsc/src/interpret/stepping_tests.rs b/compiler/qsc/src/interpret/debugger_tests.rs
similarity index 50%
rename from compiler/qsc/src/interpret/stepping_tests.rs
rename to compiler/qsc/src/interpret/debugger_tests.rs
index 415e4983..91cb3dc0 100644
--- a/compiler/qsc/src/interpret/stepping_tests.rs
+++ b/compiler/qsc/src/interpret/debugger_tests.rs
@@ -3,22 +3,21 @@
 
 #![allow(clippy::needless_raw_string_hashes)]
 
-use crate::interpret::Interpreter;
+use crate::interpret::Debugger;
 use qsc_eval::{output::CursorReceiver, StepAction, StepResult};
 use qsc_fir::fir::StmtId;
 use qsc_frontend::compile::{RuntimeCapabilityFlags, SourceMap};
-use qsc_passes::PackageType;
 use std::io::Cursor;
 
-fn get_breakpoint_ids(interpreter: &Interpreter, path: &str) -> Vec<StmtId> {
-    let mut bps = interpreter.get_breakpoints(path);
+fn get_breakpoint_ids(debugger: &Debugger, path: &str) -> Vec<StmtId> {
+    let mut bps = debugger.get_breakpoints(path);
     bps.sort_by_key(|f| f.id);
     let ids = bps.iter().map(|f| f.id.into()).collect::<Vec<_>>();
     ids
 }
 
-fn expect_return(mut interpreter: Interpreter, expected: &str) {
-    let r = step_next(&mut interpreter, &[]);
+fn expect_return(mut debugger: Debugger, expected: &str) {
+    let r = step_next(&mut debugger, &[]);
     match r.0 {
         Ok(StepResult::Return(value)) => assert_eq!(value.to_string(), expected),
         Ok(v) => panic!("Expected Return, got {v:?}"),
@@ -26,8 +25,8 @@ fn expect_return(mut interpreter: Interpreter, expected: &str) {
     }
 }
 
-fn expect_bp(interpreter: &mut Interpreter, ids: &[StmtId], expected_id: StmtId) {
-    let r = step_next(interpreter, ids);
+fn expect_bp(debugger: &mut Debugger, ids: &[StmtId], expected_id: StmtId) {
+    let r = step_next(debugger, ids);
     match r.0 {
         Ok(StepResult::BreakpointHit(actual_id)) => assert!(actual_id == expected_id),
         Ok(v) => panic!("Expected BP, got {v:?}"),
@@ -36,41 +35,41 @@ fn expect_bp(interpreter: &mut Interpreter, ids: &[StmtId], expected_id: StmtId)
 }
 
 fn step_in(
-    interpreter: &mut Interpreter,
+    debugger: &mut Debugger,
     breakpoints: &[StmtId],
 ) -> (Result<StepResult, Vec<crate::interpret::Error>>, String) {
-    step(interpreter, breakpoints, qsc_eval::StepAction::In)
+    step(debugger, breakpoints, qsc_eval::StepAction::In)
 }
 
 fn step_next(
-    interpreter: &mut Interpreter,
+    debugger: &mut Debugger,
     breakpoints: &[StmtId],
 ) -> (Result<StepResult, Vec<crate::interpret::Error>>, String) {
-    step(interpreter, breakpoints, qsc_eval::StepAction::Next)
+    step(debugger, breakpoints, qsc_eval::StepAction::Next)
 }
 
 fn step_out(
-    interpreter: &mut Interpreter,
+    debugger: &mut Debugger,
     breakpoints: &[StmtId],
 ) -> (Result<StepResult, Vec<crate::interpret::Error>>, String) {
-    step(interpreter, breakpoints, qsc_eval::StepAction::Out)
+    step(debugger, breakpoints, qsc_eval::StepAction::Out)
 }
 
 fn step(
-    interpreter: &mut Interpreter,
+    debugger: &mut Debugger,
     breakpoints: &[StmtId],
     step: StepAction,
 ) -> (Result<StepResult, Vec<crate::interpret::Error>>, String) {
     let mut cursor = Cursor::new(Vec::<u8>::new());
     let mut receiver = CursorReceiver::new(&mut cursor);
     (
-        interpreter.eval_step(&mut receiver, breakpoints, step),
+        debugger.eval_step(&mut receiver, breakpoints, step),
         receiver.dump(),
     )
 }
 
-fn expect_next(interpreter: &mut Interpreter) {
-    let result = step_next(interpreter, &[]);
+fn expect_next(debugger: &mut Debugger) {
+    let result = step_next(debugger, &[]);
     match result.0 {
         Ok(StepResult::Next) => (),
         Ok(v) => panic!("Expected Next, got {v:?}"),
@@ -78,8 +77,8 @@ fn expect_next(interpreter: &mut Interpreter) {
     }
 }
 
-fn expect_in(interpreter: &mut Interpreter) {
-    let result = step_in(interpreter, &[]);
+fn expect_in(debugger: &mut Debugger) {
+    let result = step_in(debugger, &[]);
     match result.0 {
         Ok(StepResult::StepIn) => (),
         Ok(v) => panic!("Expected StepIn, got {v:?}"),
@@ -87,8 +86,8 @@ fn expect_in(interpreter: &mut Interpreter) {
     }
 }
 
-fn expect_out(interpreter: &mut Interpreter) {
-    let result = step_out(interpreter, &[]);
+fn expect_out(debugger: &mut Debugger) {
+    let result = step_out(debugger, &[]);
     match result.0 {
         Ok(StepResult::StepOut) => (),
         Ok(v) => panic!("Expected StepOut, got {v:?}"),
@@ -97,7 +96,7 @@ fn expect_out(interpreter: &mut Interpreter) {
 }
 
 #[cfg(test)]
-mod given_interpreter_with_sources {
+mod given_debugger {
     use super::*;
 
     static STEPPING_SOURCE: &str = r#"
@@ -127,96 +126,76 @@ mod given_interpreter_with_sources {
         #[test]
         fn in_one_level_operation_works() -> Result<(), Vec<crate::interpret::Error>> {
             let sources = SourceMap::new([("test".into(), STEPPING_SOURCE.into())], None);
-            let mut interpreter = Interpreter::new(
-                true,
-                sources,
-                PackageType::Exe,
-                RuntimeCapabilityFlags::all(),
-            )?;
-            interpreter.set_entry()?;
-            let ids = get_breakpoint_ids(&interpreter, "test");
+            let mut debugger = Debugger::new(sources, RuntimeCapabilityFlags::all())?;
+            debugger.set_entry()?;
+            let ids = get_breakpoint_ids(&debugger, "test");
             let expected_id = ids[0];
-            expect_bp(&mut interpreter, &ids, expected_id);
-            expect_in(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
+            expect_bp(&mut debugger, &ids, expected_id);
+            expect_in(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
             let expected = "42";
-            expect_return(interpreter, expected);
+            expect_return(debugger, expected);
             Ok(())
         }
 
         #[test]
         fn next_crosses_operation_works() -> Result<(), Vec<crate::interpret::Error>> {
             let sources = SourceMap::new([("test".into(), STEPPING_SOURCE.into())], None);
-            let mut interpreter = Interpreter::new(
-                true,
-                sources,
-                PackageType::Exe,
-                RuntimeCapabilityFlags::all(),
-            )?;
-            interpreter.set_entry()?;
-            let ids = get_breakpoint_ids(&interpreter, "test");
+            let mut debugger = Debugger::new(sources, RuntimeCapabilityFlags::all())?;
+            debugger.set_entry()?;
+            let ids = get_breakpoint_ids(&debugger, "test");
             let expected_id = ids[0];
-            expect_bp(&mut interpreter, &ids, expected_id);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
+            expect_bp(&mut debugger, &ids, expected_id);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
             let expected = "42";
-            expect_return(interpreter, expected);
+            expect_return(debugger, expected);
             Ok(())
         }
 
         #[test]
         fn in_multiple_operations_works() -> Result<(), Vec<crate::interpret::Error>> {
             let sources = SourceMap::new([("test".into(), STEPPING_SOURCE.into())], None);
-            let mut interpreter = Interpreter::new(
-                true,
-                sources,
-                PackageType::Exe,
-                RuntimeCapabilityFlags::all(),
-            )?;
-            interpreter.set_entry()?;
-            let ids = get_breakpoint_ids(&interpreter, "test");
+            let mut debugger = Debugger::new(sources, RuntimeCapabilityFlags::all())?;
+            debugger.set_entry()?;
+            let ids = get_breakpoint_ids(&debugger, "test");
             let expected_id = ids[0];
-            expect_bp(&mut interpreter, &ids, expected_id);
-            expect_in(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_in(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
+            expect_bp(&mut debugger, &ids, expected_id);
+            expect_in(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_in(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
             let expected = "42";
-            expect_return(interpreter, expected);
+            expect_return(debugger, expected);
             Ok(())
         }
 
         #[test]
         fn out_multiple_operations_works() -> Result<(), Vec<crate::interpret::Error>> {
             let sources = SourceMap::new([("test".into(), STEPPING_SOURCE.into())], None);
-            let mut interpreter = Interpreter::new(
-                true,
-                sources,
-                PackageType::Exe,
-                RuntimeCapabilityFlags::all(),
-            )?;
-            interpreter.set_entry()?;
-            let ids = get_breakpoint_ids(&interpreter, "test");
+            let mut debugger = Debugger::new(sources, RuntimeCapabilityFlags::all())?;
+            debugger.set_entry()?;
+            let ids = get_breakpoint_ids(&debugger, "test");
             let expected_id = ids[0];
-            expect_bp(&mut interpreter, &ids, expected_id);
-            expect_in(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_next(&mut interpreter);
-            expect_in(&mut interpreter);
-            expect_out(&mut interpreter);
-            expect_out(&mut interpreter);
-            expect_next(&mut interpreter);
+            expect_bp(&mut debugger, &ids, expected_id);
+            expect_in(&mut debugger);
+            expect_next(&mut debugger);
+            expect_next(&mut debugger);
+            expect_in(&mut debugger);
+            expect_out(&mut debugger);
+            expect_out(&mut debugger);
+            expect_next(&mut debugger);
             let expected = "42";
-            expect_return(interpreter, expected);
+            expect_return(debugger, expected);
             Ok(())
         }
     }
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index e2706e2a..cddf4fa4 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -1127,6 +1127,7 @@ mod given_interpreter {
         use std::sync::Arc;
 
         use super::*;
+        use crate::interpret::Debugger;
         use expect_test::expect;
         use indoc::indoc;
         use qsc_frontend::compile::{RuntimeCapabilityFlags, SourceMap};
@@ -1223,6 +1224,7 @@ mod given_interpreter {
                     r#"
             namespace Test2 {
                 open Test;
+                @EntryPoint()
                 operation Main() : String {
                     Hello();
                     Hello()
@@ -1233,16 +1235,11 @@ mod given_interpreter {
             ];
 
             let sources = SourceMap::new(sources, None);
-            let interpreter = Interpreter::new(
-                true,
-                sources,
-                PackageType::Lib,
-                RuntimeCapabilityFlags::all(),
-            )
-            .expect("interpreter should be created");
-            let bps = interpreter.get_breakpoints("a.qs");
+            let debugger = Debugger::new(sources, RuntimeCapabilityFlags::all())
+                .expect("debugger should be created");
+            let bps = debugger.get_breakpoints("a.qs");
             assert_eq!(1, bps.len());
-            let bps = interpreter.get_breakpoints("b.qs");
+            let bps = debugger.get_breakpoints("b.qs");
             assert_eq!(2, bps.len());
         }
 
diff --git a/npm/test/basics.js b/npm/test/basics.js
index 0245dbba..808005dc 100644
--- a/npm/test/basics.js
+++ b/npm/test/basics.js
@@ -697,16 +697,6 @@ async function testCompilerError(useWorker) {
 test("compiler error on run", () => testCompilerError(false));
 test("compiler error on run - worker", () => testCompilerError(true));
 
-test("debug service get breakpoints without service loaded returns empty - web worker", async () => {
-  const debugService = getDebugServiceWorker();
-  try {
-    const emptyBps = await debugService.getBreakpoints("test.qs");
-    assert.equal(0, emptyBps.length);
-  } finally {
-    debugService.terminate();
-  }
-});
-
 test("debug service loading source without entry point attr fails - web worker", async () => {
   const debugService = getDebugServiceWorker();
   try {