Skip to content
Open
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
141 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
4bb141e
adjustemnts - coderabbit
svenjeschmitt-ops Jan 10, 2026
62ea2cc
solution without STDERR
svenjeschmitt-ops Jan 10, 2026
9563c25
adjustments ddSimAll
svenjeschmitt-ops Jan 10, 2026
4c34277
removed unused methods
svenjeschmitt-ops Jan 10, 2026
ccfd5bb
docstring CodePreprocessing
svenjeschmitt-ops Jan 10, 2026
8663fbe
merge newest version
svenjeschmitt-ops Jan 11, 2026
c256690
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 11, 2026
cf270eb
launch,restart, LoadResult
svenjeschmitt-ops Jan 11, 2026
489592b
Revert "launch,restart, LoadResult"
svenjeschmitt-ops Jan 11, 2026
4de1403
fix launch and restart
svenjeschmitt-ops Jan 11, 2026
29607af
Revert "qubit size adjustment"
svenjeschmitt-ops Jan 11, 2026
5a7413f
_end_before_start
svenjeschmitt-ops Jan 11, 2026
9d98879
enum implemented
svenjeschmitt-ops Jan 11, 2026
a8c25e2
Reapply "qubit size adjustment"
svenjeschmitt-ops Jan 11, 2026
15bb5d9
LoadResult
svenjeschmitt-ops Jan 11, 2026
175bb08
Revert "LoadResult"
svenjeschmitt-ops Jan 11, 2026
f53a848
adjustments LoadResult
svenjeschmitt-ops Jan 14, 2026
d406d13
fix cl
svenjeschmitt-ops Jan 14, 2026
65f230c
adjustments
svenjeschmitt-ops Jan 14, 2026
55ad6b9
Merge remote-tracking branch 'upstream'
svenjeschmitt-ops Jan 14, 2026
3169e22
cl bug fix
svenjeschmitt-ops Jan 14, 2026
090de19
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 14, 2026
c6faa23
fix but
svenjeschmitt-ops Jan 14, 2026
b2c0b2b
Merge remote-tracking branch 'upstream/main'
svenjeschmitt-ops Jan 15, 2026
6b15941
assortation fix + classical issue
svenjeschmitt-ops Jan 15, 2026
6fec6a9
pre- commit fix
svenjeschmitt-ops Jan 15, 2026
af49c19
♻️ Replace `pybind11` with `nanobind` (#248)
denialhaag Jan 15, 2026
03920d7
fix issue
svenjeschmitt-ops Jan 15, 2026
efbcd5c
Cl issue fix
svenjeschmitt-ops Jan 15, 2026
160c294
Code rabbit issue
svenjeschmitt-ops Jan 15, 2026
70897c1
Cl fix bug
svenjeschmitt-ops Jan 15, 2026
4695b89
Cl fix bug 2
svenjeschmitt-ops Jan 15, 2026
7e40ed9
Merge remote-tracking branch 'upstream'
svenjeschmitt-ops Jan 19, 2026
9349194
fix loadResult
svenjeschmitt-ops Jan 21, 2026
2f77e27
categorizing different errors
svenjeschmitt-ops Jan 21, 2026
6b9c1d7
improvement doctring and cleaner version
svenjeschmitt-ops Jan 21, 2026
c1eaccd
improvement launch dap
svenjeschmitt-ops Jan 21, 2026
fa522a6
fix Cl
svenjeschmitt-ops Jan 21, 2026
1f5e0ca
cl fix 2
svenjeschmitt-ops Jan 21, 2026
0e6bfa8
cl fix 3
svenjeschmitt-ops Jan 21, 2026
09f5ace
cl fix 4
svenjeschmitt-ops Jan 21, 2026
4f3e00d
remove file
svenjeschmitt-ops Jan 21, 2026
f2d905c
fix cl
svenjeschmitt-ops Jan 22, 2026
819d0f4
remove destroyDDSimulationState
svenjeschmitt-ops Jan 22, 2026
a70891c
Merge remote-tracking branch 'upstream/main'
svenjeschmitt-ops Jan 22, 2026
dd89f76
fix pre-commit
svenjeschmitt-ops Jan 22, 2026
5822fbd
cl fix 5
svenjeschmitt-ops Jan 22, 2026
adecd22
fix codecov
svenjeschmitt-ops Jan 22, 2026
0f9fd53
avoid boolean positional arg in locale.getpreferredencoding
svenjeschmitt-ops Jan 22, 2026
50abd27
pre-commit fix
svenjeschmitt-ops Jan 22, 2026
8e20311
Revert "fix pre-commit"
svenjeschmitt-ops Jan 22, 2026
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);
}
},
R"(Loads the given code into the simulation state.

Expand Down
290 changes: 256 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
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM: Pending highlights are emitted correctly.

The highlights are sent, cleared, and the exit prevention flag is set to ensure the user sees the highlights before the debugger exits.

Note: The pattern self._prevent_exit = True after sending highlight events is duplicated here and at line 361. Consider extracting this into a helper method if more emission points are added in the future.

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 257 - 261, Duplicate
pattern of sending pending highlights, clearing self.pending_highlights and
setting self._prevent_exit = True should be extracted into a helper; create a
method (e.g., _emit_pending_highlights(self, connection)) that checks
self.pending_highlights, builds the mqt.debugger.dap.messages.HighlightError
with self.source_file, calls send_message(json.dumps(...).encode()) or reuses
existing send_message call, clears self.pending_highlights and sets
self._prevent_exit = True, then replace the code block at the current location
and the similar block at the other site (line with duplicated logic) with calls
to this new helper.

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

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,153 @@ 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without it, accessing self.source_code would raise AttributeError, and the diagnostics call doesn’t make sense if we can’t map positions back to text anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What if we instead set self.source_code = None in __init__ so that the field is always at least defined? That seems a lot cleaner than using getattr

try:
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)
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"<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)
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).

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM: Parse error location extraction with fallback.

The regex pattern assumes the format <input>:line:column: detail. The fallback to (1, 1, error_message) ensures robustness if the format changes, though consider logging a warning if the pattern doesn't match to aid debugging.

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 499 - 511, The
_parse_error_location function currently falls back to (1, 1, error_message)
when the regex doesn't match; update it to emit a warning containing the
unmatched error_message to aid debugging: import and use the standard logging
module (logger = logging.getLogger(__name__)) and call logger.warning with a
clear message and the original error_message in the else branch of
_parse_error_location so you still return the same fallback tuple but also
record that the expected "<input>:line:column: detail" format was not matched.


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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major: Incorrect end_column calculation can create invalid ranges.

When column exceeds len(line_text), the calculation max(column, len(line_text) + 1) produces an end_column that extends even further beyond the line. This creates a highlight range that extends past the line's actual content.

📐 Proposed fix: Clamp end_column to line length
-        end_column = max(column, len(line_text) + 1)
+        end_column = max(column + 1, min(len(line_text) + 1, column + 1)) if column <= len(line_text) else column + 1

Or more simply, always highlight to the end of the line:

-        end_column = max(column, len(line_text) + 1)
+        end_column = len(line_text) + 1

This ensures the highlight extends to the end of the line regardless of where the column points.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
end_column = max(column, len(line_text) + 1)
end_column = max(column + 1, min(len(line_text) + 1, column + 1)) if column <= len(line_text) else column + 1
Suggested change
end_column = max(column, len(line_text) + 1)
end_column = len(line_text) + 1
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py at line 541, The end_column
calculation currently uses "end_column = max(column, len(line_text) + 1)" which
can push the range past the line when column > len(line_text); change the logic
around the end_column assignment (where end_column, column, and line_text are
used) to clamp the end to the actual line length (e.g., set end_column =
len(line_text) + 1 or otherwise min(column, len(line_text) + 1) so the highlight
never extends beyond the line); update any related range creation to rely on
this clamped end_column.

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},
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: Remove unnecessary ternary condition.

Given that column = max(1, column) (line 526) and end_column = max(column, len(line_text) + 1) (line 541), end_column is always ≥ 1, making the condition end_column > 0 always true.

🧹 Simplification
-            "end": {"line": line, "column": end_column if end_column > 0 else column},
+            "end": {"line": line, "column": end_column},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"end": {"line": line, "column": end_column if end_column > 0 else column},
"end": {"line": line, "column": end_column},
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py at line 547, The ternary in the dict
construction is redundant because earlier code ensures column = max(1, column)
and end_column = max(column, len(line_text) + 1), so end_column is always > 0;
update the object at the "end" key to simply use end_column (replace "end_column
if end_column > 0 else column" with "end_column") so the code is simpler and
clearer; the involved symbols are column, end_column and the dict entry "end" in
the function around dap_server.py lines where column and end_column are
computed.

Comment on lines +560 to +566
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant condition check on line 554.

Line 548 computes end_column = max(column, len(line_text) + 1), and earlier (line 533) column = max(1, column) ensures column is at least 1. Therefore, end_column is always ≥ 1, making the check if end_column > 0 else column on line 554 redundant—it will always use end_column.

♻️ Proposed simplification
         "range": {
             "start": {"line": line, "column": column},
-            "end": {"line": line, "column": end_column if end_column > 0 else column},
+            "end": {"line": line, "column": end_column},
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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},
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},
},
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 548 - 554, The end_column
conditional is redundant: since column is already constrained (column = max(1,
column)) and end_column is computed as end_column = max(column, len(line_text) +
1), the ternary "end_column if end_column > 0 else column" can be simplified to
just end_column; update the return range end column to use end_column directly
(referencing the variables end_column, column, line_text in dap_server.py) and
remove the unnecessary > 0 check.

},
"reason": "parseError",
"code": snippet,
"message": detail,
}
Comment on lines 537 to 571
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider highlighting at least one column or to end of line.

The parse error highlight builder is well-structured and handles edge cases defensively. However, at Line 539, when column is large or equals the line length, end_column = max(column, len(line_text) + 1) may create a zero-width highlight (when column == end_column). While technically valid per HighlightError validation, this might not provide clear visual feedback.

Consider ensuring a minimum highlight width or always highlighting to the end of the line for parse errors:

♻️ Suggested refinement
-        end_column = max(column, len(line_text) + 1)
+        # Highlight from error column to end of line, ensuring at least 1 character width
+        end_column = max(column + 1, len(line_text) + 1)

Additionally, the conditional at Line 545 is redundant since end_column is guaranteed to be >= 1:

-            "end": {"line": line, "column": end_column if end_column > 0 else column},
+            "end": {"line": line, "column": end_column},
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 516 - 550,
_build_parse_error_highlight may produce a zero-width highlight when column ==
end_column; change the end_column calculation to ensure at least one column is
highlighted and drop the redundant conditional. Specifically, in
_build_parse_error_highlight compute end_column = max(column + 1, len(line_text)
+ 1) (so highlights at least one column or to end of line) and remove the later
ternary that checks "if end_column > 0 else column".


Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Parse-error highlight indexing/clamping likely ignores lines_start_at_one / columns_start_at_one and can produce out-of-bounds columns.
_build_parse_error_highlight() assumes 1-based inputs and never clamps column to len(line)+1, but the rest of the server explicitly supports toggling 0/1-based lines/columns.

Proposed fix (clamp column; convert output to current DAP indexing)
@@
     def _build_parse_error_highlight(self, line: int, column: int, detail: str) -> dict[str, Any] | None:
@@
-        line = max(1, min(line, len(lines)))
-        column = max(1, column)
+        # Normalize to 1-based for internal line lookup
+        line = max(1, min(line, len(lines)))
+        column = max(1, column)
@@
-        end_column = max(column, len(line_text) + 1)
+        # Clamp to [1, len(line)+1] so we never emit out-of-range columns.
+        column = min(column, len(line_text) + 1)
+        end_column = len(line_text) + 1
         snippet = line_text.strip() or line_text
+
+        out_line = line if self.lines_start_at_one else line - 1
+        out_column = column if self.columns_start_at_one else column - 1
+        out_end_column = end_column if self.columns_start_at_one else end_column - 1
         return {
@@
             "range": {
-                "start": {"line": line, "column": column},
-                "end": {"line": line, "column": end_column if end_column > 0 else column},
+                "start": {"line": out_line, "column": out_column},
+                "end": {"line": out_line, "column": out_end_column},
             },
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 497 - 553, Summary:
_build_parse_error_highlight currently assumes 1-based inputs and can emit
out-of-bounds column ranges and ignore the server's lines_start_at_one /
columns_start_at_one settings. Fix: in _build_parse_error_highlight (and ensure
compatibility with values from _parse_error_location/queue_parse_error) clamp
the computed column to the valid range [1, len(line_text)+1] (or [0,
len(line_text)] when columns_start_at_one is False) and similarly clamp
end_column to the same max; then convert the start/end line and column values to
the DAP indexing the server expects using self.lines_start_at_one and
self.columns_start_at_one before placing them into the returned "range" object
so emitted highlights never go out of bounds and respect the server's 0/1-based
settings.

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 +583,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)

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)
Loading
Loading