Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
6fd5af1
qubit size adjustment
svenjeschmitt-ops Nov 3, 2025
8accf50
removing runtime error
svenjeschmitt-ops Nov 10, 2025
f4b0038
change classical varaible
svenjeschmitt-ops Nov 19, 2025
f7f27bd
new adjustments
svenjeschmitt-ops Nov 19, 2025
0cca5ca
adjustment
svenjeschmitt-ops Nov 19, 2025
a5c3eca
adjustment
svenjeschmitt-ops Nov 19, 2025
5222682
work and progress
svenjeschmitt-ops Nov 19, 2025
7e82180
work and progress
svenjeschmitt-ops Nov 19, 2025
e4155c4
amplitude adjustments
svenjeschmitt-ops Nov 24, 2025
64de683
License text
svenjeschmitt-ops Dec 1, 2025
732ca9b
:rocket: new files
svenjeschmitt-ops Dec 1, 2025
1e99c96
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 1, 2025
88de5e5
docstrings
svenjeschmitt-ops Dec 2, 2025
057b5dd
_normalize_value updated
svenjeschmitt-ops Dec 2, 2025
1bed4cc
solving issue
svenjeschmitt-ops Dec 2, 2025
cc2c09b
solved DDSimDebug.cpp
svenjeschmitt-ops Dec 2, 2025
ad1fe09
next problem solved
svenjeschmitt-ops Dec 2, 2025
cd4dba9
last issue solved
svenjeschmitt-ops Dec 2, 2025
c3ca4b9
changement -Any-
svenjeschmitt-ops Dec 2, 2025
e43e740
last adjustment
svenjeschmitt-ops Dec 2, 2025
36fdd37
change_bit
svenjeschmitt-ops Dec 2, 2025
aa92b5f
DDSimDebug changement
svenjeschmitt-ops Dec 2, 2025
051fd1e
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 2, 2025
261d3b0
first CI issue solved
svenjeschmitt-ops Dec 3, 2025
f6a5878
second issue solved
svenjeschmitt-ops Dec 3, 2025
8018667
last adjustments
svenjeschmitt-ops Dec 3, 2025
3dbd364
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 3, 2025
2cd3395
fixed bug
svenjeschmitt-ops Dec 3, 2025
135fd94
adjustements
svenjeschmitt-ops Dec 3, 2025
b50cab1
adjustment checkorthrow
svenjeschmitt-ops Dec 3, 2025
bbc0cbb
Merge branch 'new_adjustments'
svenjeschmitt-ops Dec 3, 2025
ec901ce
linter issue
svenjeschmitt-ops Dec 3, 2025
1d1d848
Merge branch 'new_adjustments'
svenjeschmitt-ops Dec 3, 2025
ebe4cec
fix ci
svenjeschmitt-ops Dec 3, 2025
4b50146
fix lint
svenjeschmitt-ops Dec 3, 2025
465d447
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 3, 2025
50da634
remove permissions workflow
svenjeschmitt-ops Dec 4, 2025
7398f50
non toggle function
svenjeschmitt-ops Dec 4, 2025
ea89e07
bool text adjusted
svenjeschmitt-ops Dec 4, 2025
5d3f383
function names
svenjeschmitt-ops Dec 4, 2025
e8e9f40
dotstrings
svenjeschmitt-ops Dec 4, 2025
ffe1a38
new version
svenjeschmitt-ops Dec 8, 2025
817e49e
github review changes
svenjeschmitt-ops Dec 8, 2025
c6b102f
fix linter
svenjeschmitt-ops Dec 8, 2025
1cefe3e
workflow file, not changed
svenjeschmitt-ops Dec 8, 2025
e10b1a0
highlight error
svenjeschmitt-ops Dec 8, 2025
ceaedcf
highlight error v2
svenjeschmitt-ops Dec 8, 2025
52ad47a
highlight error v3
svenjeschmitt-ops Dec 8, 2025
84c38f1
highlight error v4
svenjeschmitt-ops Dec 8, 2025
0d12f43
higlight error v4
svenjeschmitt-ops Dec 8, 2025
3ca8940
highlight_error
svenjeschmitt-ops Dec 9, 2025
543bd22
big fix
svenjeschmitt-ops Dec 9, 2025
9f13bd2
bug fix
svenjeschmitt-ops Dec 9, 2025
4ff44db
bug fix 2
svenjeschmitt-ops Dec 9, 2025
8e3b801
shows issue
svenjeschmitt-ops Dec 9, 2025
d1c343e
entire row
svenjeschmitt-ops Dec 9, 2025
59680cb
bug fix
svenjeschmitt-ops Dec 9, 2025
5532817
fix bug
svenjeschmitt-ops Dec 10, 2025
b6e4152
linter fix
svenjeschmitt-ops Dec 10, 2025
ac21598
Merge remote-tracking branch 'upstream/main'
svenjeschmitt-ops Dec 13, 2025
3e02a97
work and progress
svenjeschmitt-ops Dec 13, 2025
a7b5ce1
work and progress
svenjeschmitt-ops Dec 13, 2025
c8b03ee
work and progress
svenjeschmitt-ops Dec 13, 2025
59af8a0
Merge branch 'main' into highlight_error
svenjeschmitt-ops Dec 13, 2025
687bc9b
Merge remote-tracking branch 'upstream/main'
svenjeschmitt-ops Dec 16, 2025
ddc8b45
adjusted dotstrings
svenjeschmitt-ops Dec 16, 2025
4e67409
adjusted test_data_retrieval
svenjeschmitt-ops Dec 16, 2025
7308dab
adjusted test_python_bindings
svenjeschmitt-ops Dec 16, 2025
42a4bcf
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 16, 2025
86cc5c9
work and progress
svenjeschmitt-ops Dec 16, 2025
5160144
adjusted test_data_retrieval
svenjeschmitt-ops Dec 16, 2025
7e0c3df
Merge remote-tracking branch 'origin/main' into highlight_error
svenjeschmitt-ops Dec 18, 2025
68a8ffe
highlight issue
svenjeschmitt-ops Dec 18, 2025
a7c6b72
assertion issue solved
svenjeschmitt-ops Dec 18, 2025
8ef979f
assertation issue solved
svenjeschmitt-ops Dec 18, 2025
83e928a
Merge branch 'main' into highlight_error
svenjeschmitt-ops Dec 18, 2025
47edfc0
highlight issue fix bug
svenjeschmitt-ops Dec 22, 2025
7b8fa09
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 23, 2025
ec64afd
⬆️👨‍💻 Update actions/attest-build-provenance action to v3.1.0 (#234)
renovate[bot] Dec 20, 2025
32d14d8
⬆️🔒️ Lock file maintenance (#236)
renovate[bot] Dec 22, 2025
b734fc1
Merge remote-tracking branch 'upstream/main'
svenjeschmitt-ops Dec 24, 2025
92f4b4b
Revert "Merge branch 'main' into highlight_error"
svenjeschmitt-ops Dec 24, 2025
81801fa
Merge remote-tracking branch 'upstream'
svenjeschmitt-ops Dec 30, 2025
e151e5d
cc fix
svenjeschmitt-ops Dec 30, 2025
d5cd2e7
cc fix 2
svenjeschmitt-ops Dec 30, 2025
ff0e4b3
cc fix 3
svenjeschmitt-ops Dec 30, 2025
f2d9c9d
cc fix 4
svenjeschmitt-ops Dec 30, 2025
8d6d7f0
cc fix 5
svenjeschmitt-ops Dec 30, 2025
fbb9bd1
cc fix 6
svenjeschmitt-ops Dec 30, 2025
3653c60
more docstrings
svenjeschmitt-ops Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion bindings/InterfaceBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <pybind11/cast.h>
#include <pybind11/detail/common.h>
#include <pybind11/iostream.h>
#include <pybind11/native_enum.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
Expand Down Expand Up @@ -174,7 +176,20 @@ 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 py::module io = py::module::import("io");
const py::object stringIo = io.attr("StringIO")();
Result result = OK;
{
const py::scoped_ostream_redirect redirect(std::cerr, stringIo);
result = self->loadCode(self, code);
}
if (result != OK) {
auto message = stringIo.attr("getvalue")().cast<std::string>();
if (message.empty()) {
message = "An error occurred while executing the operation";
}
throw std::runtime_error(message);
}
Comment on lines -177 to +192
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a really hacky solution.

I'd rather prefer if there was some other way of getting the error message that does not involve redirecting STDERR. Here are some potential solutions I can imagine:

  • rather than returning a Result, loadCode could be updated to return some other enum value that has a value for each type of error that could come up when parsing and pre-defined strings are defined for each error type.
  • loadCode receives an additional char* errorMessage parameter that expects a fixed-size (e.g. 255) char buffer to which the error message can be written.
  • We define a new struct called LoadResult that contains all important information that could come up with a parsing error (uint errorCode, char errorMessage[255], uint line...) and use that as a return type for loadCode

I personally feel like the third suggestion might be the best one.

},
R"(Loads the given code into the simulation state.

Expand Down
293 changes: 259 additions & 34 deletions python/mqt/debugger/dap/dap_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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)
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
Comment on lines +357 to +361
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a random thought, let me know if it doesn't make any sense:

This logic for sending HighlightError messages seems to appear twice: Once here and once in the handle_client method above. Can we reduce this duplication by instead just adding the entried to the pending_highlights list here, so that the error messages are only sent from handle_client?


def code_pos_to_coordinates(self, pos: int) -> tuple[int, int]:
"""Helper method to convert a code position to line and column.
Expand All @@ -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:
Expand Down Expand Up @@ -391,8 +428,156 @@ def format_error_cause(self, cause: mqt.debugger.ErrorCause) -> str:
else ""
)

def collect_highlight_entries(self, failing_instruction: int) -> list[dict[str, Any]]:
"""Collect highlight entries for the current assertion failure."""
highlights: list[dict[str, Any]] = []
if getattr(self, "source_code", ""):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this check? Is there no cleaner way to do it?

try:
diagnostics = self.simulation_state.get_diagnostics()
error_causes = diagnostics.potential_error_causes()
Comment on lines +436 to +437
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, this method is always called when handle_assertion_fail happens. However, in that method, there is already a call to potential_error_causes(). Since that call could, in some cases, be rather small, I'm not sure if we really want to do it twice.

Is there any way to instead pass the error causes on as parameters to this method?

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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe use a "cleaner" datatype - i.e., an enum - for these indicators?

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"<input>:(\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)
Comment on lines +500 to +511
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really doesn't seam like the cleanest way to implement the feature. Especially since we have access to the entire API and can change whatever function signatures we want.

Since all of this is the data required to be passed on from the Code parsing in C++ to the DAP in Python, it makes sense to just use the LoadResult struct I suggested in an earlier comment and expose it to python as well. That way, we don't have to pattern match the error messages (which, on future updates, could very easily be changed in a way to no longer be compatible with the pattern here).


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.

Expand All @@ -401,34 +586,74 @@ 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)
Comment on lines +591 to +600
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is a major overhaul on how the send_message_hierarchy method works (and potentially also on how it is displayed?).

Can you maybe quickly show me how that looks in practice now?


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)

def send_state(self, connection: socket.socket) -> None:
"""Send the state of the current execution to the client.

Args:
connection (socket.socket): The client socket.
"""
output_lines = []
if self.simulation_state.did_assertion_fail():
output_lines.append("Assertion failed")
if self.simulation_state.was_breakpoint_hit():
output_lines.append("Breakpoint hit")
if self.simulation_state.is_finished():
output_lines.append("Finished")
if not output_lines:
output_lines.append("Running")
for line_text in output_lines:
self.send_message_simple(line_text, None, None, 0, 0, connection)
Comment on lines +643 to +659
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the idea of this method? Where is it used (I can't seem to find any uses of it in the code)

Loading
Loading