diff --git a/bindings/InterfaceBindings.cpp b/bindings/InterfaceBindings.cpp
index 9b2ea20..1608625 100644
--- a/bindings/InterfaceBindings.cpp
+++ b/bindings/InterfaceBindings.cpp
@@ -174,7 +174,17 @@ Contains one element for each of the `num_states` states in the state vector.)")
.def(
"load_code",
[](SimulationState* self, const char* code) {
- checkOrThrow(self->loadCode(self, code));
+ const Result result = self->loadCode(self, code);
+ if (result != OK) {
+ const char* messagePtr = self->getLastErrorMessage
+ ? self->getLastErrorMessage(self)
+ : nullptr;
+ std::string message = messagePtr ? messagePtr : "";
+ if (message.empty()) {
+ message = "An error occurred while executing the operation";
+ }
+ throw std::runtime_error(message);
+ }
},
R"(Loads the given code into the simulation state.
diff --git a/include/backend/dd/DDSimDebug.hpp b/include/backend/dd/DDSimDebug.hpp
index 84fc4fc..13a3e9b 100644
--- a/include/backend/dd/DDSimDebug.hpp
+++ b/include/backend/dd/DDSimDebug.hpp
@@ -119,6 +119,10 @@ struct DDSimulationState {
* @brief The code being executed, after preprocessing.
*/
std::string processedCode;
+ /**
+ * @brief The last error message produced by the interface.
+ */
+ std::string lastErrorMessage;
/**
* @brief Indicates whether the debugger is ready to start simulation.
*/
diff --git a/include/backend/debug.h b/include/backend/debug.h
index 2fd7c21..0eacae2 100644
--- a/include/backend/debug.h
+++ b/include/backend/debug.h
@@ -54,6 +54,17 @@ struct SimulationStateStruct {
*/
Result (*loadCode)(SimulationState* self, const char* code);
+ /**
+ * @brief Gets the last error message from the interface.
+ *
+ * The returned pointer is owned by the implementation and remains valid
+ * until the next interface call that modifies the error state.
+ *
+ * @param self The instance to query.
+ * @return A null-terminated error message, or nullptr if none is available.
+ */
+ const char* (*getLastErrorMessage)(SimulationState* self);
+
/**
* @brief Steps the simulation forward by one instruction.
* @param self The instance to step forward.
diff --git a/python/mqt/debugger/dap/dap_server.py b/python/mqt/debugger/dap/dap_server.py
index 27f99ea..2aa6c43 100644
--- a/python/mqt/debugger/dap/dap_server.py
+++ b/python/mqt/debugger/dap/dap_server.py
@@ -11,9 +11,10 @@
from __future__ import annotations
import json
+import re
import socket
import sys
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Any
import mqt.debugger
@@ -114,6 +115,8 @@ def __init__(self, host: str = "127.0.0.1", port: int = 4711) -> None:
self.simulation_state = mqt.debugger.SimulationState()
self.lines_start_at_one = True
self.columns_start_at_one = True
+ self.pending_highlights: list[dict[str, Any]] = []
+ self._prevent_exit = False
def start(self) -> None:
"""Start the DAP server and listen for one connection."""
@@ -166,6 +169,21 @@ def handle_client(self, connection: socket.socket) -> None:
result, cmd = self.handle_command(payload)
result_payload = json.dumps(result)
send_message(result_payload, connection)
+ if isinstance(
+ cmd,
+ (
+ mqt.debugger.dap.messages.NextDAPMessage,
+ mqt.debugger.dap.messages.StepBackDAPMessage,
+ mqt.debugger.dap.messages.StepInDAPMessage,
+ mqt.debugger.dap.messages.StepOutDAPMessage,
+ mqt.debugger.dap.messages.ContinueDAPMessage,
+ mqt.debugger.dap.messages.ReverseContinueDAPMessage,
+ mqt.debugger.dap.messages.RestartFrameDAPMessage,
+ mqt.debugger.dap.messages.RestartDAPMessage,
+ mqt.debugger.dap.messages.LaunchDAPMessage,
+ ),
+ ):
+ self._prevent_exit = False
e: mqt.debugger.dap.messages.DAPEvent | None = None
if isinstance(cmd, mqt.debugger.dap.messages.LaunchDAPMessage):
@@ -236,6 +254,11 @@ def handle_client(self, connection: socket.socket) -> None:
)
event_payload = json.dumps(e.encode())
send_message(event_payload, connection)
+ if self.pending_highlights:
+ highlight_event = mqt.debugger.dap.messages.HighlightError(self.pending_highlights, self.source_file)
+ send_message(json.dumps(highlight_event.encode()), connection)
+ self.pending_highlights = []
+ self._prevent_exit = True
self.regular_checks(connection)
def regular_checks(self, connection: socket.socket) -> None:
@@ -245,7 +268,11 @@ def regular_checks(self, connection: socket.socket) -> None:
connection (socket.socket): The client socket.
"""
e: mqt.debugger.dap.messages.DAPEvent | None = None
- if self.simulation_state.is_finished() and self.simulation_state.get_instruction_count() != 0:
+ if (
+ self.simulation_state.is_finished()
+ and self.simulation_state.get_instruction_count() != 0
+ and not self._prevent_exit
+ ):
e = mqt.debugger.dap.messages.ExitedDAPEvent(0)
event_payload = json.dumps(e.encode())
send_message(event_payload, connection)
@@ -325,7 +352,13 @@ def handle_assertion_fail(self, connection: socket.socket) -> None:
line,
column,
connection,
+ "stderr",
)
+ highlight_entries = self.collect_highlight_entries(current_instruction, error_causes)
+ if highlight_entries:
+ highlight_event = mqt.debugger.dap.messages.HighlightError(highlight_entries, self.source_file)
+ send_message(json.dumps(highlight_event.encode()), connection)
+ self._prevent_exit = True
def code_pos_to_coordinates(self, pos: int) -> tuple[int, int]:
"""Helper method to convert a code position to line and column.
@@ -337,14 +370,18 @@ def code_pos_to_coordinates(self, pos: int) -> tuple[int, int]:
tuple[int, int]: The line and column, 0-or-1-indexed.
"""
lines = self.source_code.split("\n")
- line = 0
+ line = 1 if lines else 0
col = 0
for i, line_code in enumerate(lines):
- if pos < len(line_code):
+ if pos <= len(line_code):
line = i + 1
col = pos
break
pos -= len(line_code) + 1
+ else:
+ if lines:
+ line = len(lines)
+ col = len(lines[-1])
if self.columns_start_at_one:
col += 1
if not self.lines_start_at_one:
@@ -391,8 +428,161 @@ def format_error_cause(self, cause: mqt.debugger.ErrorCause) -> str:
else ""
)
+ def collect_highlight_entries(
+ self,
+ failing_instruction: int,
+ error_causes: list[mqt.debugger.ErrorCause] | None = None,
+ ) -> list[dict[str, Any]]:
+ """Collect highlight entries for the current assertion failure."""
+ highlights: list[dict[str, Any]] = []
+ if getattr(self, "source_code", ""):
+ try:
+ if error_causes is None:
+ diagnostics = self.simulation_state.get_diagnostics()
+ error_causes = diagnostics.potential_error_causes()
+ except RuntimeError:
+ error_causes = []
+
+ for cause in error_causes:
+ message = self.format_error_cause(cause)
+ reason = self._format_highlight_reason(cause.type)
+ entry = self._build_highlight_entry(cause.instruction, reason, message)
+ if entry is not None:
+ highlights.append(entry)
+
+ if not highlights:
+ entry = self._build_highlight_entry(
+ failing_instruction,
+ "assertionFailed",
+ "Assertion failed at this instruction.",
+ )
+ if entry is not None:
+ highlights.append(entry)
+
+ return highlights
+
+ def _build_highlight_entry(self, instruction: int, reason: str, message: str) -> dict[str, Any] | None:
+ """Create a highlight entry for a specific instruction."""
+ try:
+ start_pos, end_pos = self.simulation_state.get_instruction_position(instruction)
+ except RuntimeError:
+ return None
+ start_line, start_column = self.code_pos_to_coordinates(start_pos)
+ if end_pos < len(self.source_code) and self.source_code[end_pos] == "\n":
+ end_position_exclusive = end_pos
+ else:
+ end_position_exclusive = min(len(self.source_code), end_pos + 1)
+ end_line, end_column = self.code_pos_to_coordinates(end_position_exclusive)
+ snippet = self.source_code[start_pos : end_pos + 1].replace("\r", "")
+ return {
+ "instruction": int(instruction),
+ "range": {
+ "start": {"line": start_line, "column": start_column},
+ "end": {"line": end_line, "column": end_column},
+ },
+ "reason": reason,
+ "code": snippet.strip(),
+ "message": message,
+ }
+
+ @staticmethod
+ def _format_highlight_reason(cause_type: mqt.debugger.ErrorCauseType | None) -> str:
+ """Return a short identifier for the highlight reason."""
+ if cause_type == mqt.debugger.ErrorCauseType.MissingInteraction:
+ return "missingInteraction"
+ if cause_type == mqt.debugger.ErrorCauseType.ControlAlwaysZero:
+ return "controlAlwaysZero"
+ return "unknown"
+
+ def queue_parse_error(self, error_message: str) -> None:
+ """Store highlight data for a parse error to be emitted later."""
+ line, column, detail = self._parse_error_location(error_message)
+ entry = self._build_parse_error_highlight(line, column, detail)
+ if entry is not None:
+ self.pending_highlights = [entry]
+
+ @staticmethod
+ def _parse_error_location(error_message: str) -> tuple[int, int, str]:
+ """Parse a compiler error string and extract the source location."""
+ match = re.match(r":(\d+):(\d+):\s*(.*)", error_message.strip())
+ if match:
+ line = int(match.group(1))
+ column = int(match.group(2))
+ detail = match.group(3).strip()
+ else:
+ line = 1
+ column = 1
+ detail = error_message.strip()
+ return (line, column, detail)
+
+ def _build_parse_error_highlight(self, line: int, column: int, detail: str) -> dict[str, Any] | None:
+ """Create a highlight entry for a parse error."""
+ if not getattr(self, "source_code", ""):
+ return None
+ lines = self.source_code.split("\n")
+ if not lines:
+ return None
+ line = max(1, min(line, len(lines)))
+ column = max(1, column)
+ line_index = line - 1
+ line_text = lines[line_index]
+
+ if column <= 1 and line_index > 0 and not line_text.strip():
+ prev_index = line_index - 1
+ while prev_index >= 0 and not lines[prev_index].strip():
+ prev_index -= 1
+ if prev_index >= 0:
+ line_index = prev_index
+ line = line_index + 1
+ line_text = lines[line_index]
+ stripped = line_text.lstrip()
+ column = max(1, len(line_text) - len(stripped) + 1) if stripped else 1
+
+ end_column = max(column, len(line_text) + 1)
+ snippet = line_text.strip() or line_text
+ return {
+ "instruction": -1,
+ "range": {
+ "start": {"line": line, "column": column},
+ "end": {"line": line, "column": end_column if end_column > 0 else column},
+ },
+ "reason": "parseError",
+ "code": snippet,
+ "message": detail,
+ }
+
+ def _flatten_message_parts(self, parts: list[Any]) -> list[str]:
+ """Flatten nested message structures into plain text lines."""
+ flattened: list[str] = []
+ for part in parts:
+ if isinstance(part, str):
+ if part:
+ flattened.append(part)
+ elif isinstance(part, dict):
+ title = part.get("title")
+ if isinstance(title, str) and title:
+ flattened.append(title)
+ body = part.get("body")
+ if isinstance(body, list):
+ flattened.extend(self._flatten_message_parts(body))
+ elif isinstance(body, str) and body:
+ flattened.append(body)
+ end = part.get("end")
+ if isinstance(end, str) and end:
+ flattened.append(end)
+ elif isinstance(part, list):
+ flattened.extend(self._flatten_message_parts(part))
+ elif part is not None:
+ flattened.append(str(part))
+ return flattened
+
def send_message_hierarchy(
- self, message: dict[str, str | list[Any] | dict[str, Any]], line: int, column: int, connection: socket.socket
+ self,
+ message: dict[str, str | list[Any] | dict[str, Any]],
+ line: int,
+ column: int,
+ connection: socket.socket,
+ category: str = "console",
) -> None:
"""Send a hierarchy of messages to the client.
@@ -401,34 +591,56 @@ def send_message_hierarchy(
line (int): The line number.
column (int): The column number.
connection (socket.socket): The client socket.
+ category (str): The output category (console/stdout/stderr).
"""
- if "title" in message:
- title_event = mqt.debugger.dap.messages.OutputDAPEvent(
- "console", cast("str", message["title"]), "start", line, column, self.source_file
- )
- send_message(json.dumps(title_event.encode()), connection)
-
- if "body" in message:
- body = message["body"]
- if isinstance(body, list):
- for msg in body:
- if isinstance(msg, dict):
- self.send_message_hierarchy(msg, line, column, connection)
- else:
- output_event = mqt.debugger.dap.messages.OutputDAPEvent(
- "console", msg, None, line, column, self.source_file
- )
- send_message(json.dumps(output_event.encode()), connection)
- elif isinstance(body, dict):
- self.send_message_hierarchy(body, line, column, connection)
- elif isinstance(body, str):
- output_event = mqt.debugger.dap.messages.OutputDAPEvent(
- "console", body, None, line, column, self.source_file
- )
- send_message(json.dumps(output_event.encode()), connection)
+ raw_body = message.get("body")
+ body: list[str] | None = None
+ if isinstance(raw_body, list):
+ body = self._flatten_message_parts(raw_body)
+ elif isinstance(raw_body, str):
+ body = [raw_body]
+ end_value = message.get("end")
+ end = end_value if isinstance(end_value, str) else None
+ title = str(message.get("title", ""))
+ self.send_message_simple(title, body, end, line, column, connection, category)
+
+ def send_message_simple(
+ self,
+ title: str,
+ body: list[str] | None,
+ end: str | None,
+ line: int,
+ column: int,
+ connection: socket.socket,
+ category: str = "console",
+ ) -> None:
+ """Send a simple message to the client.
- if "end" in message or "title" in message:
- end_event = mqt.debugger.dap.messages.OutputDAPEvent(
- "console", cast("str", message.get("end")), "end", line, column, self.source_file
- )
- send_message(json.dumps(end_event.encode()), connection)
+ Args:
+ title (str): The title of the message.
+ body (list[str]): The body of the message.
+ end (str | None): The end of the message.
+ line (int): The line number.
+ column (int): The column number.
+ connection (socket.socket): The client socket.
+ category (str): The output category (console/stdout/stderr).
+ """
+ segments: list[str] = []
+ if title:
+ segments.append(title)
+ if body:
+ segments.extend(body)
+ if end:
+ segments.append(end)
+ if not segments:
+ return
+ output_text = "\n".join(segments)
+ event = mqt.debugger.dap.messages.OutputDAPEvent(
+ category,
+ output_text,
+ None,
+ line,
+ column,
+ self.source_file,
+ )
+ send_message(json.dumps(event.encode()), connection)
diff --git a/python/mqt/debugger/dap/messages/__init__.py b/python/mqt/debugger/dap/messages/__init__.py
index da8744c..3b0709a 100644
--- a/python/mqt/debugger/dap/messages/__init__.py
+++ b/python/mqt/debugger/dap/messages/__init__.py
@@ -21,6 +21,7 @@
from .exception_info_message import ExceptionInfoDAPMessage
from .exited_dap_event import ExitedDAPEvent
from .gray_out_event import GrayOutDAPEvent
+from .highlight_error_dap_message import HighlightError
from .initialize_dap_message import InitializeDAPMessage
from .initialized_dap_event import InitializedDAPEvent
from .launch_dap_message import LaunchDAPMessage
@@ -57,6 +58,7 @@
"ExceptionInfoDAPMessage",
"ExitedDAPEvent",
"GrayOutDAPEvent",
+ "HighlightError",
"InitializeDAPMessage",
"InitializedDAPEvent",
"LaunchDAPMessage",
diff --git a/python/mqt/debugger/dap/messages/highlight_error_dap_message.py b/python/mqt/debugger/dap/messages/highlight_error_dap_message.py
new file mode 100644
index 0000000..64d8ae5
--- /dev/null
+++ b/python/mqt/debugger/dap/messages/highlight_error_dap_message.py
@@ -0,0 +1,179 @@
+# Copyright (c) 2024 - 2026 Chair for Design Automation, TUM
+# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
+# All rights reserved.
+#
+# SPDX-License-Identifier: MIT
+#
+# Licensed under the MIT License
+
+"""Represents the custom 'highlightError' DAP event."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any
+
+from .dap_event import DAPEvent
+
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+
+
+class HighlightError(DAPEvent):
+ """Represents the 'highlightError' custom DAP event.
+
+ Attributes:
+ event_name (str): DAP event identifier emitted by this message.
+ highlights (list[dict[str, Any]]): Normalized highlight entries with ranges and metadata.
+ source (dict[str, Any]): Normalized DAP source information for the highlighted file.
+ """
+
+ event_name = "highlightError"
+
+ highlights: list[dict[str, Any]]
+ source: dict[str, Any]
+
+ def __init__(self, highlights: Sequence[Mapping[str, Any]], source: Mapping[str, Any]) -> None:
+ """Create a new 'highlightError' DAP event message.
+
+ Args:
+ highlights (Sequence[Mapping[str, Any]]): Highlight entries describing the problematic ranges.
+ source (Mapping[str, Any]): Information about the current source file.
+ """
+ self.highlights = [self._normalize_highlight(entry) for entry in highlights]
+ self.source = self._normalize_source(source)
+ super().__init__()
+
+ def validate(self) -> None:
+ """Validate the 'highlightError' DAP event message after creation.
+
+ Raises:
+ ValueError: If required highlight fields are missing or empty.
+ """
+ if not self.highlights:
+ msg = "At least one highlight entry is required to show the issue location."
+ raise ValueError(msg)
+
+ for highlight in self.highlights:
+ if "message" not in highlight or not str(highlight["message"]).strip():
+ msg = "Each highlight entry must contain a descriptive 'message'."
+ raise ValueError(msg)
+
+ def encode(self) -> dict[str, Any]:
+ """Encode the 'highlightError' DAP event message as a dictionary.
+
+ Returns:
+ dict[str, Any]: The encoded DAP event payload.
+ """
+ encoded = super().encode()
+ encoded["body"] = {"highlights": self.highlights, "source": self.source}
+ return encoded
+
+ @staticmethod
+ def _normalize_highlight(entry: Mapping[str, Any]) -> dict[str, Any]:
+ """Return a shallow copy of a highlight entry with guaranteed structure.
+
+ Args:
+ entry (Mapping[str, Any]): Highlight metadata including a range mapping.
+
+ Returns:
+ dict[str, Any]: A normalized highlight entry suitable for serialization.
+
+ Raises:
+ TypeError: If the range mapping or its positions are not mappings.
+ ValueError: If required fields are missing or malformed.
+ """
+ if "range" not in entry:
+ msg = "A highlight entry must contain a 'range'."
+ raise ValueError(msg)
+ highlight_range = entry["range"]
+ if not isinstance(highlight_range, Mapping):
+ msg = "Highlight range must be a mapping with 'start' and 'end'."
+ raise TypeError(msg)
+
+ start = HighlightError._normalize_position(highlight_range.get("start"))
+ end = HighlightError._normalize_position(highlight_range.get("end"))
+ if HighlightError._end_before_start(start, end):
+ msg = "Highlight range 'end' must be after 'start'."
+ raise ValueError(msg)
+
+ normalized = dict(entry)
+ normalized["instruction"] = int(normalized.get("instruction", -1))
+ normalized["reason"] = str(normalized.get("reason", "unknown"))
+ normalized["code"] = str(normalized.get("code", ""))
+ normalized["message"] = str(normalized.get("message", "")).strip()
+ normalized["range"] = {
+ "start": start,
+ "end": end,
+ }
+ return normalized
+
+ @staticmethod
+ def _normalize_position(position: Mapping[str, Any] | None) -> dict[str, int]:
+ """Normalize a position mapping, ensuring it includes a line and column.
+
+ Args:
+ position (Mapping[str, Any] | None): The position mapping to normalize.
+
+ Returns:
+ dict[str, int]: A normalized position with integer line and column.
+
+ Raises:
+ TypeError: If the provided position is not a mapping.
+ ValueError: If required keys are missing.
+ """
+ if not isinstance(position, Mapping):
+ msg = "Highlight positions must be mappings with 'line' and 'column'."
+ raise TypeError(msg)
+ try:
+ line = int(position["line"])
+ column = int(position["column"])
+ except KeyError as exc:
+ msg = "Highlight positions require 'line' and 'column'."
+ raise ValueError(msg) from exc
+ return {
+ "line": line,
+ "column": column,
+ }
+
+ @staticmethod
+ def _normalize_source(source: Mapping[str, Any] | None) -> dict[str, Any]:
+ """Create a defensive copy of the provided DAP Source information.
+
+ Args:
+ source (Mapping[str, Any] | None): The source mapping to normalize.
+
+ Returns:
+ dict[str, Any]: Normalized source information with string fields.
+
+ Raises:
+ TypeError: If the source is not a mapping.
+ ValueError: If required keys are missing.
+ """
+ if not isinstance(source, Mapping):
+ msg = "Source information must be provided as a mapping."
+ raise TypeError(msg)
+ normalized = dict(source)
+ if "name" not in normalized or "path" not in normalized:
+ msg = "Source mappings must at least provide 'name' and 'path'."
+ raise ValueError(msg)
+ normalized["name"] = str(normalized["name"])
+ normalized["path"] = str(normalized["path"])
+ return normalized
+
+ @staticmethod
+ def _end_before_start(start: Mapping[str, Any], end: Mapping[str, Any]) -> bool:
+ """Return True if 'end' describes a position before 'start'.
+
+ Args:
+ start (Mapping[str, Any]): The start position mapping.
+ end (Mapping[str, Any]): The end position mapping.
+
+ Returns:
+ bool: True when the end position is before the start position.
+ """
+ start_line = int(start.get("line", 0))
+ start_column = int(start.get("column", 0))
+ end_line = int(end.get("line", 0))
+ end_column = int(end.get("column", 0))
+ return (end_line, end_column) < (start_line, start_column)
diff --git a/python/mqt/debugger/dap/messages/launch_dap_message.py b/python/mqt/debugger/dap/messages/launch_dap_message.py
index be3fd6e..c153723 100644
--- a/python/mqt/debugger/dap/messages/launch_dap_message.py
+++ b/python/mqt/debugger/dap/messages/launch_dap_message.py
@@ -10,6 +10,7 @@
from __future__ import annotations
+import contextlib
import locale
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -63,21 +64,21 @@ def handle(self, server: DAPServer) -> dict[str, Any]:
dict[str, Any]: The response to the request.
"""
program_path = Path(self.program)
- code = program_path.read_text(encoding=locale.getpreferredencoding(False))
- server.source_code = code
- try:
- server.simulation_state.load_code(code)
- except RuntimeError:
- return {
- "type": "response",
- "request_seq": self.sequence_number,
- "success": False,
- "command": "launch",
- "message": "An error occurred while parsing the code.",
- }
- if not self.stop_on_entry:
- server.simulation_state.run_simulation()
server.source_file = {"name": program_path.name, "path": self.program}
+ parsed_successfully = True
+ with program_path.open("r", encoding=locale.getpreferredencoding(False)) as f:
+ code = f.read()
+ server.source_code = code
+ try:
+ server.simulation_state.load_code(code)
+ except RuntimeError as exc:
+ parsed_successfully = False
+ server.queue_parse_error(str(exc))
+ if parsed_successfully and not self.stop_on_entry:
+ server.simulation_state.run_simulation()
+ if not parsed_successfully:
+ with contextlib.suppress(RuntimeError):
+ server.simulation_state.reset_simulation()
return {
"type": "response",
"request_seq": self.sequence_number,
diff --git a/python/mqt/debugger/dap/messages/restart_dap_message.py b/python/mqt/debugger/dap/messages/restart_dap_message.py
index 7411887..7f84658 100644
--- a/python/mqt/debugger/dap/messages/restart_dap_message.py
+++ b/python/mqt/debugger/dap/messages/restart_dap_message.py
@@ -10,6 +10,7 @@
from __future__ import annotations
+import contextlib
import locale
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -64,12 +65,21 @@ def handle(self, server: DAPServer) -> dict[str, Any]:
"""
server.simulation_state.reset_simulation()
program_path = Path(self.program)
- code = program_path.read_text(encoding=locale.getpreferredencoding(False))
- server.source_code = code
- server.simulation_state.load_code(code)
- if not self.stop_on_entry:
- server.simulation_state.run_simulation()
server.source_file = {"name": program_path.name, "path": self.program}
+ parsed_successfully = True
+ with program_path.open("r", encoding=locale.getpreferredencoding(False)) as f:
+ code = f.read()
+ server.source_code = code
+ try:
+ server.simulation_state.load_code(code)
+ except RuntimeError as exc:
+ parsed_successfully = False
+ server.queue_parse_error(str(exc))
+ if parsed_successfully and not self.stop_on_entry:
+ server.simulation_state.run_simulation()
+ if not parsed_successfully:
+ with contextlib.suppress(RuntimeError):
+ server.simulation_state.reset_simulation()
return {
"type": "response",
"request_seq": self.sequence_number,
diff --git a/src/backend/dd/DDSimDebug.cpp b/src/backend/dd/DDSimDebug.cpp
index c807ad6..a8d0e3d 100644
--- a/src/backend/dd/DDSimDebug.cpp
+++ b/src/backend/dd/DDSimDebug.cpp
@@ -75,6 +75,14 @@ DDSimulationState* toDDSimulationState(SimulationState* state) {
// NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
}
+const char* ddsimGetLastErrorMessage(SimulationState* self) {
+ const auto* ddsim = toDDSimulationState(self);
+ if (ddsim->lastErrorMessage.empty()) {
+ return nullptr;
+ }
+ return ddsim->lastErrorMessage.c_str();
+}
+
/**
* @brief Generate a random number between 0 and 1.
*
@@ -513,6 +521,7 @@ Result createDDSimulationState(DDSimulationState* self) {
self->interface.init = ddsimInit;
self->interface.loadCode = ddsimLoadCode;
+ self->interface.getLastErrorMessage = ddsimGetLastErrorMessage;
self->interface.stepForward = ddsimStepForward;
self->interface.stepBackward = ddsimStepBackward;
self->interface.stepOverForward = ddsimStepOverForward;
@@ -572,6 +581,7 @@ Result ddsimInit(SimulationState* self) {
ddsim->breakpoints.clear();
ddsim->lastFailedAssertion = -1ULL;
ddsim->lastMetBreakpoint = -1ULL;
+ ddsim->lastErrorMessage.clear();
destroyDDDiagnostics(&ddsim->diagnostics);
createDDDiagnostics(&ddsim->diagnostics, ddsim);
@@ -590,9 +600,23 @@ Result ddsimLoadCode(SimulationState* self, const char* code) {
ddsim->callReturnStack.clear();
ddsim->callSubstitutions.clear();
ddsim->restoreCallReturnStack.clear();
+ ddsim->ready = false;
ddsim->code = code;
ddsim->variables.clear();
ddsim->variableNames.clear();
+ ddsim->instructionTypes.clear();
+ ddsim->instructionStarts.clear();
+ ddsim->instructionEnds.clear();
+ ddsim->functionDefinitions.clear();
+ ddsim->assertionInstructions.clear();
+ ddsim->successorInstructions.clear();
+ ddsim->classicalRegisters.clear();
+ ddsim->qubitRegisters.clear();
+ ddsim->dataDependencies.clear();
+ ddsim->functionCallers.clear();
+ ddsim->targetQubits.clear();
+ ddsim->instructionObjects.clear();
+ ddsim->lastErrorMessage.clear();
try {
std::stringstream ss{preprocessAssertionCode(code, ddsim)};
@@ -600,7 +624,14 @@ Result ddsimLoadCode(SimulationState* self, const char* code) {
ddsim->qc = std::make_unique(imported);
qc::CircuitOptimizer::flattenOperations(*ddsim->qc, true);
} catch (const std::exception& e) {
- std::cerr << e.what() << "\n";
+ ddsim->lastErrorMessage = e.what();
+ if (ddsim->lastErrorMessage.empty()) {
+ ddsim->lastErrorMessage =
+ "An error occurred while executing the operation";
+ }
+ return ERROR;
+ } catch (...) {
+ ddsim->lastErrorMessage = "An error occurred while executing the operation";
return ERROR;
}
@@ -1119,6 +1150,7 @@ Result ddsimStepBackward(SimulationState* self) {
}
Result ddsimRunAll(SimulationState* self, size_t* failedAssertions) {
+ auto* ddsim = toDDSimulationState(self);
size_t errorCount = 0;
while (!self->isFinished(self)) {
const Result result = self->runSimulation(self);
@@ -1389,6 +1421,11 @@ Result ddsimSetBreakpoint(SimulationState* self, size_t desiredPosition,
for (auto i = 0ULL; i < ddsim->instructionTypes.size(); i++) {
const size_t start = ddsim->instructionStarts[i];
const size_t end = ddsim->instructionEnds[i];
+ if (desiredPosition < start) {
+ *targetInstruction = i;
+ ddsim->breakpoints.insert(i);
+ return OK;
+ }
if (desiredPosition >= start && desiredPosition <= end) {
if (ddsim->functionDefinitions.contains(i)) {
// Breakpoint may be located in a sub-gate of the gate definition.
diff --git a/src/common/parsing/CodePreprocessing.cpp b/src/common/parsing/CodePreprocessing.cpp
index c0ce03f..af8d0ae 100644
--- a/src/common/parsing/CodePreprocessing.cpp
+++ b/src/common/parsing/CodePreprocessing.cpp
@@ -20,7 +20,9 @@
#include "common/parsing/Utils.hpp"
#include
+#include
#include
+#include
#include
#include