Skip to content

Conversation

@svenjeschmitt-ops
Copy link
Contributor

@svenjeschmitt-ops svenjeschmitt-ops commented Dec 18, 2025

Description

This pull request improves the debugging highlight functionality and fixes an issue where greyed-out highlights were not correctly reset between executions.

When an issue is detected in the code, the extension highlights the affected line in red and displays a descriptive error message explaining the nature of the problem. This visual feedback helps users quickly identify both the location of the error and its underlying cause.

A second part of the implementation concerns the assertion functionality. After an assertion failure, the extension greys out all lines of code that cannot have caused the error. While this behaviour is intended, the greyed-out highlights were previously not cleared when a new execution started. As a result, lines remained greyed out until the editor was closed and reopened.

Overall, the highlighting functionality provides an efficient way to show where an error occurred and what caused it. This change introduces an explicit reset of all greyed-out highlights on launch and restart events, ensuring that each execution starts with a clean editor state.

Fixes #(issue)

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 518-552: The function _build_parse_error_highlight has untested
edge cases and a clarity bug: change the condition currently written as `column
<= 1` to `column == 1` (since column is already clamped to >=1) in the branch
that walks backwards across blank lines, and add unit tests that exercise parse
errors on an empty-first-line file, on a whitespace-only line, and on multiple
consecutive empty lines to assert the returned highlight uses the previous
non-empty line, computes the column correctly (indent start) and sets a
reasonable end_column/snippet/message; target tests at the same behavior covered
by _build_parse_error_highlight so they fail if the branch is incorrect.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9563c25 and 4c34277.

📒 Files selected for processing (1)
  • python/mqt/debugger/dap/dap_server.py
🧰 Additional context used
🧬 Code graph analysis (1)
python/mqt/debugger/dap/dap_server.py (2)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
  • HighlightError (22-179)
  • encode (62-70)
python/mqt/debugger/dap/messages/output_dap_event.py (2)
  • encode (54-69)
  • OutputDAPEvent (18-69)
🔇 Additional comments (17)
python/mqt/debugger/dap/dap_server.py (17)

14-14: LGTM: Import additions are appropriate.

The re module is used for parsing error messages (line 507), and Any is used for type hints throughout the new methods.

Also applies to: 17-17


118-119: LGTM: State fields support the highlight error feature.

pending_highlights queues highlight data for deferred emission, and _prevent_exit prevents premature termination when errors are displayed to the user.


172-186: LGTM: Restores normal exit behavior after user interaction.

Resetting _prevent_exit after stepping or restart commands allows the debugger to exit normally once the user has acknowledged and acted on highlighted errors.


257-261: LGTM: Deferred highlight emission works correctly.

Pending highlights (queued by parse errors) are emitted after command processing, and _prevent_exit is set to ensure the user can view them before the session terminates.


271-275: LGTM: Exit condition properly respects the prevent-exit flag.

Adding not self._prevent_exit ensures the debugger remains active when error highlights are displayed, preventing premature session termination.


355-355: LGTM: Appropriate category for error output.

Using "stderr" for assertion failure messages correctly categorizes them as errors rather than normal console output.


357-361: LGTM: Assertion failures now emit structured highlights.

The new code collects highlight entries for error causes and emits them as HighlightError events, enabling rich UI feedback for debugging assertion failures.


431-462: LGTM: Robust highlight collection with fallback.

The method correctly collects highlights for error causes and provides a sensible fallback (highlighting the failing instruction) when diagnostics are unavailable or produce no causes. Exception handling at line 443 prevents crashes.


464-486: LGTM: Highlight entry construction is correct.

The method carefully distinguishes between:

  • end_position_exclusive (line 471-474): For UI highlight range, excludes trailing newlines
  • Snippet extraction (line 476): Includes the character at end_pos, then strips whitespace

This ensures precise UI highlighting while capturing the full instruction text for display.


488-495: LGTM: Simple and safe error cause mapping.

The method maps error cause types to string identifiers with a safe fallback to "unknown".


497-502: Verify that only one parse error can occur per launch.

Line 502 replaces the entire pending_highlights list with a single entry. If multiple parse errors could occur during a single launch/restart, only the last one would be displayed.

If multiple parse errors are possible, consider appending instead:

if entry is not None:
    self.pending_highlights.append(entry)

Otherwise, document that only the first/last parse error is shown.


504-516: LGTM: Robust parse error location extraction.

The regex correctly parses compiler error format, and the fallback to (1, 1, error_message) ensures the method never fails even with unexpected error formats.


554-577: LGTM: Robust recursive message flattening.

The method correctly handles nested message structures with appropriate filtering of empty strings and None values. The recursive approach cleanly processes dictionaries, lists, and primitive types.


579-606: LGTM: Message hierarchy refactoring preserves functionality.

The refactored method correctly extracts message components, flattens nested structures, and delegates to the new send_message_simple method. The added category parameter is backward compatible with its default value.


607-646: LGTM: New message sending method is clean and focused.

The method consolidates message formatting and transmission into a single, straightforward function with clear parameter separation. The early return (line 636) handles empty messages gracefully.


118-119: The grey-out clearing mechanism is correctly implemented, though not through explicit clear events. Instead, grey-outs are implicitly reset by recalculation:

  1. On restart: RestartDAPMessage.handle() calls reset_simulation() to clear state, then loads new code
  2. Grey-out reset occurs automatically: regular_checks() recalculates gray_out_areas from scratch based on the current instruction's dependencies, and sends a fresh GrayOutDAPEvent with the updated areas
  3. This happens after every command including Launch and Restart, ensuring old grey-outs from a previous execution are replaced with correct grey-outs for the new state

The implementation correctly clears assertion-related highlights via pending_highlights (emitted as HighlightError events) which is separate from the grey-out mechanism. No changes needed.

Likely an incorrect or invalid review comment.


373-389: The coordinate conversion changes are correct and well-tested.

The modifications properly handle edge cases:

  • Line 373: Initializes to line 1 for non-empty source
  • Line 376: The <= condition correctly places positions at newlines on the current line (verified by test_code_pos_to_coordinates_handles_line_end())
  • Lines 381-384: Fallback for positions extending beyond the file

All improvements are substantiated and the critical boundary condition change is covered by tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/common/parsing/CodePreprocessing.cpp:
- Around line 163-208: The catch in validateTargets around the std::stoul call
is too broad (catching std::exception); change it to catch the specific
exceptions thrown by std::stoul—catch const std::invalid_argument& and const
std::out_of_range& (either as two catches or a single multi-catch) and in each
handler rethrow the ParsingError as currently done; keep the surrounding logic
and error message intact and do not swallow other exception types.
- Around line 62-76: The loop that counts newlines in LineColumn
lineColumnForOffset should be replaced with std::count for clarity and potential
performance: compute auto newlineCount = std::count(code.begin(), code.begin() +
lineStart, '\n') and set location.line = 1 + newlineCount instead of iterating
up to lineStart; keep the existing calculation of lineStart and location.column
unchanged and ensure to include <algorithm> if not already included.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c34277 and ccfd5bb.

📒 Files selected for processing (1)
  • src/common/parsing/CodePreprocessing.cpp
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/parsing/CodePreprocessing.cpp (3)
include/common/parsing/ParsingError.hpp (1)
  • ParsingError (32-32)
src/common/parsing/ParsingError.cpp (1)
  • ParsingError (23-23)
include/common/parsing/CodePreprocessing.hpp (1)
  • isVariableDeclaration (296-296)
🪛 Cppcheck (2.19.0)
src/common/parsing/CodePreprocessing.cpp

[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)

🔇 Additional comments (8)
src/common/parsing/CodePreprocessing.cpp (8)

23-25: LGTM: Includes support new validation logic.

The added includes are appropriate for the new digit checking and exception handling functionality.


43-49: LGTM: Robust digit validation.

The function correctly validates non-empty digit-only strings with proper casting for std::isdigit.


85-109: LGTM: Appropriate fallback for error location.

The function correctly locates the target token or falls back to the first non-space character, which is reasonable for error reporting.


119-152: LGTM: Clear error formatting utilities.

The error formatting follows standard compiler conventions and provides helpful context for users.


509-513: LGTM: Improved error location accuracy.

The adjusted trueStart calculation correctly identifies the actual start of the instruction by finding the first non-space character, improving error reporting precision.


539-550: LGTM: Robust register validation with clear error messages.

The validation correctly checks for empty names and non-digit sizes before parsing. The exception handling is consistent with validateTargets (lines 189-196), though both could benefit from more specific exception types as noted in that function's review.


615-616: LGTM: Comprehensive assertion validation.

The call to validateTargets is appropriately placed after unfolding and internal validation, with clear context for error messages.


621-624: LGTM: Appropriate target validation for instructions.

The validation correctly skips variable declarations (which have dedicated handling at lines 534-552) and validates targets for all other instructions.

Comment on lines +163 to +208
void validateTargets(const std::string& code, size_t instructionStart,
const std::vector<std::string>& targets,
const std::map<std::string, size_t>& definedRegisters,
const std::vector<std::string>& shadowedRegisters,
const std::string& context) {
for (const auto& target : targets) {
if (target.empty()) {
continue;
}
const auto open = target.find('[');
if (open == std::string::npos) {
continue;
}
const auto close = target.find(']', open + 1);
if (open == 0 || close == std::string::npos || close != target.size() - 1) {
throw ParsingError(formatParseError(code, instructionStart,
invalidTargetDetail(target, context),
target));
}
const auto registerName = target.substr(0, open);
const auto indexText = target.substr(open + 1, close - open - 1);
if (!isDigits(indexText)) {
throw ParsingError(formatParseError(code, instructionStart,
invalidTargetDetail(target, context),
target));
}
size_t registerIndex = 0;
try {
registerIndex = std::stoul(indexText);
} catch (const std::exception&) {
throw ParsingError(formatParseError(code, instructionStart,
invalidTargetDetail(target, context),
target));
}
if (std::ranges::find(shadowedRegisters, registerName) !=
shadowedRegisters.end()) {
continue;
}
const auto found = definedRegisters.find(registerName);
if (found == definedRegisters.end() || found->second <= registerIndex) {
throw ParsingError(formatParseError(code, instructionStart,
invalidTargetDetail(target, context),
target));
}
}
}
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 more specific exception catching.

The catch block at line 192 catches std::exception, which is quite broad. std::stoul specifically throws std::invalid_argument and std::out_of_range. While catching the base class is safe, being more specific improves code clarity.

♻️ Proposed refactor
     size_t registerIndex = 0;
     try {
       registerIndex = std::stoul(indexText);
-    } catch (const std::exception&) {
+    } catch (const std::invalid_argument&) {
+      throw ParsingError(formatParseError(code, instructionStart,
+                                          invalidTargetDetail(target, context),
+                                          target));
+    } catch (const std::out_of_range&) {
       throw ParsingError(formatParseError(code, instructionStart,
                                           invalidTargetDetail(target, context),
                                           target));
     }
🤖 Prompt for AI Agents
In @src/common/parsing/CodePreprocessing.cpp around lines 163 - 208, The catch
in validateTargets around the std::stoul call is too broad (catching
std::exception); change it to catch the specific exceptions thrown by
std::stoul—catch const std::invalid_argument& and const std::out_of_range&
(either as two catches or a single multi-catch) and in each handler rethrow the
ParsingError as currently done; keep the surrounding logic and error message
intact and do not swallow other exception types.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
python/mqt/debugger/dap/dap_server.py (2)

579-646: send_message_hierarchy/send_message_simple refactor looks good; adjust docstrings + avoid str(None) title.
Currently title = str(message.get("title", "")) can produce "None".

Proposed tweak
@@
-        title = str(message.get("title", ""))
+        raw_title = message.get("title")
+        title = raw_title if isinstance(raw_title, str) else ""
         self.send_message_simple(title, body, end, line, column, connection, category)

412-463: Handle unhandled ErrorCauseType values with a default message instead of returning empty string.

The format_error_cause() method uses a nested ternary operator that returns "" for any ErrorCauseType not explicitly handled (currently only MissingInteraction and ControlAlwaysZero are covered). If mqt-core adds new ErrorCauseType variants in the future, those would be silently formatted as empty messages, potentially violating DAP protocol message constraints.

Proposed fix
@@
             for cause in error_causes:
-                message = self.format_error_cause(cause)
+                message = self.format_error_cause(cause).strip()
+                if not message:
+                    # Keep HighlightError payload valid even when we don't have a dedicated formatter yet.
+                    message = "Potential error cause at this instruction."
                 reason = self._format_highlight_reason(cause.type)
                 entry = self._build_highlight_entry(cause.instruction, reason, message)
                 if entry is not None:
                     highlights.append(entry)

Also applies to: 464-487

python/mqt/debugger/dap/messages/restart_dap_message.py (1)

57-76: Fix NameError (parsed_successfully is undefined) and handle load_code RuntimeError as non-fatal (align with Launch).

RestartDAPMessage.handle() on line 73 references undefined variable parsed_successfully, causing a NameError at runtime. Additionally, unlike LaunchDAPMessage, the restart handler doesn't catch RuntimeError from load_code() (line 70) and will crash on parse failures. The response also incorrectly returns "command": "launch" instead of "restart" (line 80).

Wrap load_code() in try-except, define parsed_successfully, return proper error response on parse failure, and fix the response command field to "restart".

Proposed fix
@@
     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)
+        server.source_file = {"name": program_path.name, "path": str(program_path)}
+        parsed_successfully = True
+        try:
+            parsed_successfully = bool(server.simulation_state.load_code(code))
+        except RuntimeError as exc:
+            parsed_successfully = False
+            server.queue_parse_error(str(exc))
+            return {
+                "type": "response",
+                "request_seq": self.sequence_number,
+                "success": False,
+                "command": "restart",
+                "message": "An error occurred while parsing the code.",
+            }
         if 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,
+            "success": True,
+            "command": "restart",
+        }
python/mqt/debugger/dap/messages/launch_dap_message.py (1)

66-83: Fix undefined variable parsed_successfully on line 81 and ensure server.source_file is set.

Line 81 references parsed_successfully which is never defined, causing a NameError at runtime. Additionally, server.source_file is not assigned in the handle() method, breaking downstream calls to HighlightError(...) in DAPServer (which requires source with "name" and "path" keys per line 157-159 of highlight_error_dap_message.py).

Set server.source_file to {"name": program_path.name, "path": str(program_path)} after line 68, and initialize parsed_successfully = True before the try block (or remove the unused check on line 81-83 if it serves no purpose given that exceptions return early on line 72).

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 497-552: The parse error detail can be empty causing
HighlightError.validate() to fail; update _parse_error_location(error_message)
to return a non-empty fallback (e.g., "parse error" or "unknown parse error")
when detail is empty or only whitespace, and ensure
queue_parse_error/_build_parse_error_highlight will propagate that non-empty
string (alternatively also guard in _build_parse_error_highlight by replacing an
empty detail with the same fallback) so the "message" field in the highlight
dict is never empty.

In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py:
- Around line 111-137: The _normalize_position signature says it accepts
Mapping[str, Any] | None but the implementation doesn't handle None; either
remove | None from the type hint on _normalize_position to disallow None, or
explicitly handle None at the start of the function (e.g., if position is None:
raise TypeError("Highlight positions must be mappings with 'line' and
'column'.") ) so the runtime behavior matches the signature; update the function
annotation or add the explicit None check inside _normalize_position
accordingly.

In @src/backend/dd/DDSimDebug.cpp:
- Line 1153: In ddsimRunAll, the local pointer 'ddsim' assigned via
toDDSimulationState(self) is never used; either remove the unused declaration
entirely or, if you plan to use it later, add a clear comment above it
explaining it's intentionally unused or explicitly mark it as used (e.g.,
(void)ddsim) to avoid warnings; update the code around the 'ddsim' declaration
accordingly.

In @test/python/test_dap_server.py:
- Around line 28-44: The fake diagnostics object uses
potential_error_causes=list (the type) which is confusing; change
fake_diagnostics to set potential_error_causes to an actual empty list (e.g.,
potential_error_causes=[]) so the
test_build_highlight_entry_does_not_span_next_instruction more clearly supplies
a diagnostics object with no causes when server.simulation_state.get_diagnostics
is used by DAPServer.collect_highlight_entries.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccfd5bb and 6639b9d.

📒 Files selected for processing (12)
  • bindings/InterfaceBindings.cpp
  • include/backend/dd/DDSimDebug.hpp
  • include/backend/debug.h
  • python/mqt/debugger/dap/dap_server.py
  • python/mqt/debugger/dap/messages/__init__.py
  • python/mqt/debugger/dap/messages/highlight_error_dap_message.py
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
  • src/backend/dd/DDSimDebug.cpp
  • src/common/parsing/CodePreprocessing.cpp
  • src/frontend/cli/CliFrontEnd.cpp
  • test/python/test_dap_server.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/common/parsing/CodePreprocessing.cpp
  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (8)
bindings/InterfaceBindings.cpp (2)
include/frontend/cli/CliFrontEnd.hpp (2)
  • code (46-46)
  • code (68-68)
test/utils/common_fixtures.hpp (6)
  • code (211-215)
  • code (211-213)
  • code (236-244)
  • code (236-237)
  • code (254-256)
  • code (254-254)
python/mqt/debugger/dap/messages/__init__.py (1)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (1)
  • HighlightError (22-179)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)
python/mqt/debugger/pydebugger.pyi (1)
  • reset_simulation (193-198)
python/mqt/debugger/dap/messages/launch_dap_message.py (1)
python/mqt/debugger/pydebugger.pyi (1)
  • reset_simulation (193-198)
src/common/parsing/CodePreprocessing.cpp (3)
include/common/parsing/ParsingError.hpp (1)
  • ParsingError (32-32)
src/common/parsing/ParsingError.cpp (1)
  • ParsingError (23-23)
include/common/parsing/CodePreprocessing.hpp (1)
  • isVariableDeclaration (296-296)
test/python/test_dap_server.py (2)
python/mqt/debugger/dap/dap_server.py (2)
  • code_pos_to_coordinates (363-389)
  • collect_highlight_entries (431-462)
python/mqt/debugger/pydebugger.pyi (3)
  • potential_error_causes (527-535)
  • get_instruction_position (274-284)
  • get_diagnostics (419-424)
src/backend/dd/DDSimDebug.cpp (1)
include/backend/dd/DDSimDebug.hpp (1)
  • preprocessAssertionCode (678-678)
python/mqt/debugger/dap/dap_server.py (3)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
  • HighlightError (22-179)
  • encode (62-70)
python/mqt/debugger/dap/messages/output_dap_event.py (2)
  • encode (54-69)
  • OutputDAPEvent (18-69)
python/mqt/debugger/pydebugger.pyi (4)
  • ErrorCause (436-445)
  • get_diagnostics (419-424)
  • get_instruction_position (274-284)
  • ErrorCauseType (25-33)
🪛 Cppcheck (2.19.0)
src/common/parsing/CodePreprocessing.cpp

[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)

🪛 Ruff (0.14.10)
python/mqt/debugger/dap/messages/restart_dap_message.py

73-73: Undefined name parsed_successfully

(F821)

python/mqt/debugger/dap/messages/launch_dap_message.py

81-81: Undefined name parsed_successfully

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (24)
src/common/parsing/CodePreprocessing.cpp (11)

23-25: LGTM!

The new includes are appropriate for the added functionality: <cctype> for std::isdigit and <exception> for the exception base class used in catch handlers.


43-49: LGTM!

Good defensive implementation. The cast to unsigned char before calling std::isdigit is the correct approach to avoid undefined behavior with negative char values on platforms where char is signed.


62-76: LGTM!

The line/column calculation logic is correct. The use of rfind to find the start of the current line and the subsequent iteration to count newlines handles edge cases properly.


85-109: LGTM!

The function correctly locates targets within a line and provides a sensible fallback to the first non-whitespace column when the target isn't found. This improves error message precision.


119-125: LGTM!

The error format follows the standard compiler diagnostic style (<input>:line:column: message), which enables IDE integration for clickable error locations.


133-152: LGTM!

The helper functions provide consistent error message formatting for target and register validation errors.


163-208: LGTM!

The validation function is comprehensive and correctly handles:

  • Bracket syntax validation
  • Numeric index validation with overflow protection via isDigits + std::stoul with try/catch
  • Shadowed register bypass for function parameters
  • Bounds checking against defined register sizes

The belt-and-suspenders approach of checking isDigits before std::stoul is appropriate since stoul can still throw on overflow for very large numbers.


509-513: LGTM!

Good improvement to skip leading whitespace when computing trueStart, resulting in more accurate error locations that point to actual code rather than indentation.


539-551: LGTM!

Robust parsing with proper validation. The isDigits check followed by std::stoul with exception handling protects against both non-numeric input and integer overflow, providing clear error messages in both cases.


615-616: LGTM!

The validation order is correct: first unfold register references to individual qubits, then perform semantic validation via a->validate(), and finally validate target references against defined registers.


621-624: LGTM!

Correctly skips validation for variable declarations (which define registers rather than reference them) while validating targets for all other instruction types.

src/frontend/cli/CliFrontEnd.cpp (1)

72-80: LGTM! Improved error reporting for code loading failures.

The implementation correctly checks for the presence of the getLastErrorMessage function pointer before calling it, and provides a helpful fallback message when no detailed error is available. This significantly improves the debugging experience when code loading fails.

include/backend/debug.h (1)

57-66: LGTM! Well-documented error retrieval API.

The new getLastErrorMessage interface method is clearly documented with appropriate lifetime semantics. The placement after loadCode is logical, and the ownership model (implementation-owned pointer) is clearly stated.

bindings/InterfaceBindings.cpp (1)

177-187: LGTM! Enhanced Python binding error messages.

The error handling correctly captures the result, queries for a detailed error message when available, and provides an appropriate fallback. This brings the Python bindings in line with the improved error reporting available in other interfaces.

include/backend/dd/DDSimDebug.hpp (1)

122-125: LGTM! Storage for error messages added.

The addition of the lastErrorMessage member provides the necessary storage to implement the error retrieval interface. Using std::string is appropriate for managing the lifetime and memory of error messages.

src/backend/dd/DDSimDebug.cpp (5)

78-84: LGTM! Correct implementation of error message retrieval.

The function correctly returns nullptr when no error message is available and otherwise returns the C-string from the owned std::string. The lifetime semantics are safe since the string is owned by the DDSimulationState instance.


524-524: LGTM! Interface wiring is correct.

The assignment of ddsimGetLastErrorMessage to the interface function pointer follows the established pattern for other interface methods.


584-584: LGTM! Error state cleared during initialization.

Clearing lastErrorMessage during initialization ensures a clean state for each session.


603-635: LGTM! Comprehensive error handling with proper state management.

The changes correctly:

  • Set ready = false early to prevent use of partially-loaded state
  • Clear all state containers including lastErrorMessage for a clean slate
  • Capture exception messages from both specific and generic exceptions
  • Provide appropriate fallback messages when exception details are unavailable
  • Return ERROR after recording the error message

The error handling is robust and will significantly improve the debugging experience.


1424-1428: This breakpoint snapping logic is intentional and correct, but verify if this code was newly added in the current PR.

The logic at lines 1424-1428 implements standard debugger behavior: when a breakpoint is requested at a position before an instruction starts, it snaps to that instruction. This is consistent with the documented behavior stating "MQT Debugger will then determine the instruction that corresponds to this location in the code" and is confirmed by the DAP implementation which returns the actual instruction position to the client.

However, if this is newly added code, confirm it's related to the PR's objectives. If pre-existing, this review comment can be resolved.

python/mqt/debugger/dap/messages/__init__.py (1)

24-62: Export of HighlightError looks consistent with existing package API patterns.

test/python/test_dap_server.py (1)

18-25: Good regression coverage for newline boundary (pos at \n) in code_pos_to_coordinates.

python/mqt/debugger/dap/messages/highlight_error_dap_message.py (1)

22-70: Solid, defensive event payload normalization + stable body shape (highlights/source).

python/mqt/debugger/dap/dap_server.py (1)

271-275: Verify the intended DAP lifecycle when highlights are shown: The _prevent_exit flag suppresses the Exited event when set to True (after sending highlights) and is only reset upon specific client commands (execution/restart). If the client closes the connection without issuing another command after highlights are shown, the Exited event will never be sent, which may violate DAP protocol expectations. Confirm whether this is intentional for UX purposes or if the flag should be reset differently (e.g., after highlights are acknowledged by the client, or on disconnect).

Comment on lines +118 to 120
self.pending_highlights: list[dict[str, Any]] = []
self._prevent_exit = False

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and locate the relevant code
cd python/mqt/debugger/dap
ls -la

Repository: munich-quantum-toolkit/debugger

Length of output: 434


🏁 Script executed:

# Check lines 118-120 in dap_server.py (initialization section)
head -130 dap_server.py | tail -20

Repository: munich-quantum-toolkit/debugger

Length of output: 147


🏁 Script executed:

# Check lines 257-261 in dap_server.py (highlight emission section)
sed -n '250,270p' dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 131


🏁 Script executed:

# Search for source_file initialization/assignments
rg -n 'source_file\s*=' dap_server.py -A 2 -B 2

Repository: munich-quantum-toolkit/debugger

Length of output: 128


🏁 Script executed:

# Check HighlightError class definition and validate method
rg -n 'class HighlightError\|def validate' messages.py -A 5

Repository: munich-quantum-toolkit/debugger

Length of output: 126


🏁 Script executed:

# Check DAPEvent class and encode method
rg -n 'class DAPEvent\|def encode' messages.py -A 3

Repository: munich-quantum-toolkit/debugger

Length of output: 126


🏁 Script executed:

# Check lines 118-120 in dap_server.py (initialization section)
sed -n '110,130p' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 902


🏁 Script executed:

# Check lines 257-261 in dap_server.py (highlight emission section)
sed -n '250,270p' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1141


🏁 Script executed:

# Search for source_file initialization/assignments
rg -n 'source_file\s*=' python/mqt/debugger/dap/dap_server.py -A 2 -B 2

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Check HighlightError class definition in messages
rg -n 'class HighlightError' python/mqt/debugger/dap/messages -A 20

Repository: munich-quantum-toolkit/debugger

Length of output: 2336


🏁 Script executed:

# Check DAPEvent class and encode method
rg -n 'class DAPEvent' python/mqt/debugger/dap/messages -A 10

Repository: munich-quantum-toolkit/debugger

Length of output: 865


🏁 Script executed:

# Look for validate method in HighlightError or its parent
rg -n 'def validate' python/mqt/debugger/dap/messages -B 2 -A 10

Repository: munich-quantum-toolkit/debugger

Length of output: 39056


🏁 Script executed:

# Get the complete HighlightError.validate() method
sed -n '47,75p' python/mqt/debugger/dap/messages/highlight_error_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1206


🏁 Script executed:

# Search for where source_file is assigned in dap_server.py
rg -n 'self\.source_file' python/mqt/debugger/dap/dap_server.py -B 2 -A 2

Repository: munich-quantum-toolkit/debugger

Length of output: 1217


🏁 Script executed:

# Check if source_file is set in launch or initialize handlers
rg -n 'source_file' python/mqt/debugger/dap/messages -B 1 -A 1 | head -50

Repository: munich-quantum-toolkit/debugger

Length of output: 1324


🏁 Script executed:

# Search for where source_file is assigned (initialization)
rg -n 'source_file\s*=' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Check the full context around line 258-259 and see if source_file is checked before use
sed -n '240,270p' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1752


🏁 Script executed:

# Check in messages/set_breakpoints_dap_message.py where source_file is accessed
sed -n '45,75p' python/mqt/debugger/dap/messages/set_breakpoints_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1477


🏁 Script executed:

# Search for initialization in launch or other startup handlers
rg -n 'self.source_file\s*=' python/mqt/debugger/dap/messages/ -B 2 -A 2

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Search more broadly for where source_file might be set in the entire dap module
rg -n 'source_file' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 469


🏁 Script executed:

# Check the LaunchDAPMessage or InitializeDAPMessage handlers to see if they set source_file
rg -n 'def handle' python/mqt/debugger/dap/messages/launch_dap_message.py -A 30 | head -50

Repository: munich-quantum-toolkit/debugger

Length of output: 1354


🏁 Script executed:

# Search for where source_file gets populated
rg -n 'source_file.*:' python/mqt/debugger/dap/dap_server.py -B 3 -A 3

Repository: munich-quantum-toolkit/debugger

Length of output: 277


🏁 Script executed:

# Look for where source_file is set - check the full __init__ and other methods
sed -n '100,200p' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 4151


🏁 Script executed:

# Check the initialize handler which likely sets source_file
rg -n 'def handle' python/mqt/debugger/dap/messages/initialize_dap_message.py -A 40 | head -60

Repository: munich-quantum-toolkit/debugger

Length of output: 859


🏁 Script executed:

# Search for initialization of source_file attribute
rg -n 'server\.source_file\s*=' python/mqt/debugger/dap/messages -B 2 -A 2

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Let's check if source_file is ever populated anywhere - search entire dap module
rg -n 'source_file\s*=' python/mqt/debugger/dap

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Look at the complete launch_dap_message handle method
sed -n '57,95p' python/mqt/debugger/dap/messages/launch_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1304


🏁 Script executed:

# Check if maybe source_file is set in LaunchDAPMessage's handle or elsewhere
cat python/mqt/debugger/dap/messages/launch_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 2889


🏁 Script executed:

# Search across the entire codebase for any assignment to source_file
rg -rn 'source_file.*=' python/mqt/debugger/

Repository: munich-quantum-toolkit/debugger

Length of output: 209


🏁 Script executed:

# Check the complete HighlightError.__init__ to see how it normalizes source
sed -n '36,50p' python/mqt/debugger/dap/messages/highlight_error_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 741


🏁 Script executed:

# Let's check the _normalize_highlight and related methods in HighlightError
sed -n '60,100p' python/mqt/debugger/dap/messages/highlight_error_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1692


🏁 Script executed:

# Check the _normalize_source method
sed -n '100,140p' python/mqt/debugger/dap/messages/highlight_error_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1625


🏁 Script executed:

# Check what happens when source_file is accessed - let's trace initialization flow
grep -n 'def __init__' python/mqt/debugger/dap/dap_server.py -A 20

Repository: munich-quantum-toolkit/debugger

Length of output: 1038


🏁 Script executed:

# Search if source_file is perhaps initialized later in a superclass or elsewhere
rg -n 'class DAPServer' python/mqt/debugger/dap/dap_server.py -A 50 | head -80

Repository: munich-quantum-toolkit/debugger

Length of output: 1987


🏁 Script executed:

# Check the complete _normalize_source method
sed -n '140,180p' python/mqt/debugger/dap/messages/highlight_error_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1724


🏁 Script executed:

# Let's trace where source_file might be set - check all files in messages folder
find python/mqt/debugger/dap/messages -name '*.py' -exec grep -l 'source_file' {} \;

Repository: munich-quantum-toolkit/debugger

Length of output: 253


🏁 Script executed:

# Search to see if source_file is set in InitializeDAPMessage or anywhere before highlight emission
rg -n 'source_file' python/mqt/debugger/dap/messages/initialize_dap_message.py -B 2 -A 2

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Check if there's an exception handler around the highlight emission code
sed -n '245,275p' python/mqt/debugger/dap/dap_server.py

Repository: munich-quantum-toolkit/debugger

Length of output: 1618


🏁 Script executed:

# Look for all usages of self.source_file to see the pattern
rg -n 'self\.source_file' python/mqt/debugger/dap/dap_server.py -B 1 -A 1

Repository: munich-quantum-toolkit/debugger

Length of output: 777


🏁 Script executed:

# Check restart_dap_message to see if source_file is set there
cat python/mqt/debugger/dap/messages/restart_dap_message.py

Repository: munich-quantum-toolkit/debugger

Length of output: 2673


🏁 Script executed:

# Let's check the entire flow - does any handler set server.source_file?
rg -rn 'server\.source' python/mqt/debugger/dap/messages/ | grep -v '.pyc'

Repository: munich-quantum-toolkit/debugger

Length of output: 1054


🏁 Script executed:

# Check if maybe source_file gets populated in __init__ or somewhere we haven't seen
rg -n '\["name"\].*\["path"\]' python/mqt/debugger/dap -B 3 -A 1

Repository: munich-quantum-toolkit/debugger

Length of output: 660


🏁 Script executed:

# Let's verify - are there any tests that show how source_file should be initialized?
find . -name '*test*.py' -path '*/debugger/*' | head -5

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

# Check if there's a way source_file gets populated that we're missing
rg -n 'source_file\s*:' python/mqt/debugger/dap/dap_server.py -B 5 -A 15

Repository: munich-quantum-toolkit/debugger

Length of output: 765


🏁 Script executed:

# Let's verify the actual crash scenario - access self.source_file when it's not set
python3 << 'EOF'
# Simulate what happens when accessing an uninitialized attribute
class DAPServer:
    host: str
    source_file: dict  # Type annotation only, no initialization
    
    def __init__(self):
        self.host = "127.0.0.1"
        # source_file is NOT initialized here
        
try:
    server = DAPServer()
    print(f"Trying to access source_file: {server.source_file}")
except AttributeError as e:
    print(f"ERROR: {e}")
EOF

Repository: munich-quantum-toolkit/debugger

Length of output: 161


Initialize source_file and add exception handling for highlight emission to prevent server crashes.

source_file is declared but never initialized in __init__, so accessing self.source_file at line 258 will raise AttributeError. Even if initialized, HighlightError.__init__ calls _normalize_source() which validates that source has required 'name' and 'path' keys (raises TypeError/ValueError if missing), and validate() requires each highlight to have a non-empty 'message' field. Any of these exceptions will crash the debug session because there is no exception handling around the highlight emission.

Wrap highlight creation and emission in try/except to catch TypeError and ValueError, and use finally to clear pending_highlights even on failure:

Proposed fix
         self.columns_start_at_one = True
         self.pending_highlights: list[dict[str, Any]] = []
         self._prevent_exit = False
+        self.source_file = {"name": "", "path": ""}
             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
+                try:
+                    highlight_event = mqt.debugger.dap.messages.HighlightError(self.pending_highlights, self.source_file)
+                    send_message(json.dumps(highlight_event.encode()), connection)
+                    self._prevent_exit = True
+                except (TypeError, ValueError):
+                    pass
+                finally:
+                    self.pending_highlights = []

Also applies to: 327, 359

Comment on lines +28 to +44
def test_build_highlight_entry_does_not_span_next_instruction() -> None:
"""Ensure highlight ranges stop at the end of the instruction."""
server = DAPServer()
server.source_code = "measure q[0] -> c[0];\nmeasure q[1] -> c[1];\n"
first_line_end = server.source_code.index("\n")
fake_diagnostics = SimpleNamespace(potential_error_causes=list)
fake_state = SimpleNamespace(
get_instruction_position=lambda _instr: (0, first_line_end),
get_diagnostics=lambda: fake_diagnostics,
)
server.simulation_state = fake_state # type: ignore[assignment]

entries = server.collect_highlight_entries(0)
assert entries
entry = entries[0]
assert entry["range"]["start"]["line"] == 1
assert entry["range"]["end"]["line"] == 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 | 🟡 Minor

Test intent is solid; consider making potential_error_causes explicitly return [] for readability.

Optional tweak
-    fake_diagnostics = SimpleNamespace(potential_error_causes=list)
+    fake_diagnostics = SimpleNamespace(potential_error_causes=lambda: [])
📝 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
def test_build_highlight_entry_does_not_span_next_instruction() -> None:
"""Ensure highlight ranges stop at the end of the instruction."""
server = DAPServer()
server.source_code = "measure q[0] -> c[0];\nmeasure q[1] -> c[1];\n"
first_line_end = server.source_code.index("\n")
fake_diagnostics = SimpleNamespace(potential_error_causes=list)
fake_state = SimpleNamespace(
get_instruction_position=lambda _instr: (0, first_line_end),
get_diagnostics=lambda: fake_diagnostics,
)
server.simulation_state = fake_state # type: ignore[assignment]
entries = server.collect_highlight_entries(0)
assert entries
entry = entries[0]
assert entry["range"]["start"]["line"] == 1
assert entry["range"]["end"]["line"] == 1
def test_build_highlight_entry_does_not_span_next_instruction() -> None:
"""Ensure highlight ranges stop at the end of the instruction."""
server = DAPServer()
server.source_code = "measure q[0] -> c[0];\nmeasure q[1] -> c[1];\n"
first_line_end = server.source_code.index("\n")
fake_diagnostics = SimpleNamespace(potential_error_causes=lambda: [])
fake_state = SimpleNamespace(
get_instruction_position=lambda _instr: (0, first_line_end),
get_diagnostics=lambda: fake_diagnostics,
)
server.simulation_state = fake_state # type: ignore[assignment]
entries = server.collect_highlight_entries(0)
assert entries
entry = entries[0]
assert entry["range"]["start"]["line"] == 1
assert entry["range"]["end"]["line"] == 1
🤖 Prompt for AI Agents
In @test/python/test_dap_server.py around lines 28 - 44, The fake diagnostics
object uses potential_error_causes=list (the type) which is confusing; change
fake_diagnostics to set potential_error_causes to an actual empty list (e.g.,
potential_error_causes=[]) so the
test_build_highlight_entry_does_not_span_next_instruction more clearly supplies
a diagnostics object with no causes when server.simulation_state.get_diagnostics
is used by DAPServer.collect_highlight_entries.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/dap_server.py (1)

373-389: Verify edge case: empty file with lines_start_at_one=False.

The refactored logic handles most cases well, but there's a potential edge case:

  • When lines is empty and lines_start_at_one=False, the function would return (-1, col) after the adjustment on line 388.
  • This could cause issues if negative line numbers are not expected by the client.

Consider adding an explicit guard for empty files or ensuring the line number is never negative:

🛡️ Suggested fix to prevent negative line numbers
 def code_pos_to_coordinates(self, pos: int) -> tuple[int, int]:
     """Helper method to convert a code position to line and column.
 
     Args:
         pos (int): The 0-indexed position in the code.
 
     Returns:
         tuple[int, int]: The line and column, 0-or-1-indexed.
     """
     lines = self.source_code.split("\n")
     line = 1 if lines else 0
     col = 0
     for i, line_code in enumerate(lines):
         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:
         line -= 1
+        line = max(0, line)  # Ensure non-negative
     return (line, col)
🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 521-555: The unreachable ternary in _build_parse_error_highlight
should be simplified: remove the redundant "if end_column > 0 else column"
because end_column is computed as max(column, len(line_text) + 1) and column is
already clamped to >=1, so end_column will always be >0; update the returned
range end column to use end_column directly (or compute end_column differently
if you intended a different fallback) and remove the unnecessary conditional
expression to clean up the code.
- Around line 464-489: In _build_highlight_entry, make the snippet slice
consistent with the range calculation: compute end_position_exclusive as already
done and then set snippet =
self.source_code[start_pos:end_position_exclusive].replace("\r", "") (and keep
the .strip() downstream), so the text used for "code" matches the computed
range; update the reference to snippet accordingly and ensure message handling
remains unchanged.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 895325d and d62fbce.

📒 Files selected for processing (1)
  • python/mqt/debugger/dap/dap_server.py
🔇 Additional comments (12)
python/mqt/debugger/dap/dap_server.py (12)

118-119: LGTM! Clean state initialization.

The new instance variables for managing highlights and exit prevention are properly typed and initialized.


172-186: LGTM! Exit prevention flag properly managed.

The reset logic ensures that after execution commands (step, continue, restart, launch), the exit prevention flag is cleared, allowing the highlight emission logic to set it if needed.


257-261: LGTM! Highlight emission logic is correct.

The logic properly emits pending highlights, clears the queue, and prevents premature exit, ensuring users can see error highlights before the debugger terminates.


271-275: LGTM! Exit condition properly guards against premature termination.

The added _prevent_exit check ensures the debugger doesn't exit immediately after emitting error highlights, giving the client time to display them.


355-361: LGTM! Assertion failure handling improved with structured highlights.

The change to "stderr" category is appropriate for error messages, and the new highlight collection logic provides structured error information to the client.


431-462: LGTM! Robust highlight collection with good fallback handling.

The method properly handles edge cases (missing source_code, RuntimeError from diagnostics) and provides a sensible fallback when no specific error causes are found.


491-498: LGTM! Clean mapping of error types to reason identifiers.

The static method provides a clear mapping from internal error types to client-facing identifiers, with an appropriate default for unknown types.


500-505: Verify: Parse error replacement vs accumulation.

The method replaces pending_highlights entirely (self.pending_highlights = [entry]) rather than appending. This means:

  • Only the most recent parse error is retained
  • Any previously queued highlights are discarded

If multiple parse errors can occur before emission, consider whether accumulation would be more appropriate:

# Current behavior: replace
self.pending_highlights = [entry]

# Alternative: accumulate
self.pending_highlights.append(entry)

If parse errors are mutually exclusive or stop execution immediately, the current approach is correct.


507-519: LGTM! Robust error message parsing with sensible defaults.

The regex pattern correctly parses standard compiler error format, and the fallback to line 1, column 1 ensures the method always returns valid coordinates.


557-580: LGTM! Robust recursive flattening with good filtering.

The method correctly handles nested structures and filters out empty/null values, ensuring clean message output.


582-608: LGTM! Clean refactor with improved message routing.

The addition of the category parameter enables proper message categorization (console/stdout/stderr), and the delegation to send_message_simple improves code organization.


610-649: LGTM! Well-structured simple message sender.

The method provides a clean API for sending simple messages with optional title, body, and end segments, properly integrating source location and output category.

Comment on lines 464 to 486
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", "")
message = message.strip() if isinstance(message, str) else ""
if not message:
message = "Potential error cause."
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,
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Note: Minor inconsistency between snippet and range calculation.

The method is well-structured and handles errors properly. However, there's a subtle difference:

  • The snippet on line 476 always includes source_code[start_pos:end_pos+1]
  • The end_position_exclusive calculation (lines 471-475) varies based on whether end_pos is at a newline

Since the snippet is .strip()-ed, this likely doesn't cause issues in practice, but for perfect consistency, consider using the same end position for both:

♻️ Optional refactor for consistency
 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", "")
+    snippet = self.source_code[start_pos : end_position_exclusive].replace("\r", "")
     message = message.strip() if isinstance(message, str) else ""
     if not message:
         message = "Potential error cause."
     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,
     }
📝 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
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", "")
message = message.strip() if isinstance(message, str) else ""
if not message:
message = "Potential error cause."
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,
}
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_position_exclusive].replace("\r", "")
message = message.strip() if isinstance(message, str) else ""
if not message:
message = "Potential error cause."
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,
}
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 464 - 489, In
_build_highlight_entry, make the snippet slice consistent with the range
calculation: compute end_position_exclusive as already done and then set snippet
= self.source_code[start_pos:end_position_exclusive].replace("\r", "") (and keep
the .strip() downstream), so the text used for "code" matches the computed
range; update the reference to snippet accordingly and ensure message handling
remains unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 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".

In @src/backend/dd/DDSimDebug.cpp:
- Around line 1153-1159: The file contains unresolved Git merge markers
(<<<<<<<, =======, >>>>>>>) that stop compilation; remove the conflict markers
and keep the readiness check: call toDDSimulationState(self), then if
(!ddsim->ready) return ERROR; ensuring that the ready-check block (ddsim->ready)
is retained and merged into the surrounding function where the markers appear so
compilation and the intended state validation are preserved.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d62fbce and c3e51bc.

📒 Files selected for processing (2)
  • python/mqt/debugger/dap/dap_server.py
  • src/backend/dd/DDSimDebug.cpp
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (2)
python/mqt/debugger/dap/dap_server.py (3)
python/mqt/debugger/dap/messages/next_dap_message.py (1)
  • NextDAPMessage (21-47)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
  • HighlightError (22-179)
  • encode (62-70)
python/mqt/debugger/dap/messages/output_dap_event.py (2)
  • encode (54-69)
  • OutputDAPEvent (18-69)
src/backend/dd/DDSimDebug.cpp (2)
include/frontend/cli/CliFrontEnd.hpp (2)
  • code (46-46)
  • code (68-68)
include/backend/dd/DDSimDebug.hpp (1)
  • preprocessAssertionCode (678-678)
🪛 Cppcheck (2.19.0)
src/backend/dd/DDSimDebug.cpp

[error] 1154-1154: syntax error

(syntaxError)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (19)
src/backend/dd/DDSimDebug.cpp (6)

78-84: LGTM: Clean implementation of error message retrieval.

The function correctly returns nullptr when no error message is present and provides access to the error string otherwise. The lifetime of the returned pointer is safely tied to the DDSimulationState object.


524-524: LGTM: Properly exposes the new API.

The assignment correctly wires up the getLastErrorMessage function pointer in the public interface.


584-584: LGTM: Proper error state reset.

Clearing the error message on initialization ensures a clean starting state.


603-619: LGTM: Comprehensive state reset before code loading.

The implementation correctly:

  • Sets ready = false to prevent operations during loading
  • Clears all state-tracking containers
  • Resets the error message for a clean loading state

626-636: LGTM: Robust exception handling with error message capture.

The implementation properly:

  • Captures exception messages from std::exception
  • Provides a fallback message when the exception message is empty
  • Handles unexpected exceptions with a catch-all clause
  • Returns ERROR consistently to signal failure

1430-1434: LGTM: Handles edge case for breakpoint positioning.

The early return when desiredPosition < start correctly handles the case where a breakpoint is requested before an instruction begins, setting it at the nearest subsequent instruction.

python/mqt/debugger/dap/dap_server.py (13)

14-14: LGTM!

The re import is correctly added to support parse error location extraction.


118-119: LGTM!

The new runtime state fields are properly typed and initialized to support the highlight queuing and exit prevention logic.


172-186: LGTM!

The logic correctly resets exit prevention when user-initiated commands are executed, allowing normal program flow to resume.


257-261: LGTM!

The highlight emission logic correctly sends pending highlights after command processing and prevents premature exit to ensure the user can review the error locations.


271-275: LGTM!

The exit condition correctly incorporates the _prevent_exit flag to ensure the debugger remains active when error highlights need to be displayed.


355-361: LGTM!

The assertion failure handler now correctly collects and emits highlight entries for error causes, preventing premature exit to ensure visibility of the highlighted issues.


363-389: LGTM!

The refactored coordinate conversion correctly handles edge cases:

  • Provides safe defaults for empty source
  • Uses <= to properly include end-of-line positions for highlighting
  • Adds fallback logic for out-of-bounds positions

These changes improve robustness of position-to-coordinate mapping.


431-457: LGTM!

The highlight collection logic is well-structured with appropriate defensive checks:

  • Verifies source code availability
  • Handles diagnostic retrieval errors gracefully
  • Provides sensible fallback highlighting when no error causes are identified

459-484: LGTM!

The highlight entry builder correctly handles:

  • Position retrieval errors with graceful None return
  • Newline boundary cases for accurate highlighting ranges
  • Message fallback for robustness
  • Proper structure for downstream HighlightError consumption

486-493: LGTM!

The highlight reason formatter provides clear, structured identifiers for error cause types with an appropriate fallback.


552-575: LGTM!

The message flattening helper correctly handles nested structures (strings, dicts, lists) and safely converts all types to strings, providing robust message formatting.


577-603: LGTM!

The refactored send_message_hierarchy cleanly delegates to send_message_simple after normalizing the message structure. The addition of the category parameter improves flexibility for different output types.


605-644: LGTM!

The new send_message_simple method provides a clean interface for sending structured messages via OutputDAPEvent. The implementation correctly assembles message segments and handles empty cases gracefully.

Comment on lines 516 to 552
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,
}
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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 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.
- Around line 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.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3e51bc and 901eaf7.

📒 Files selected for processing (1)
  • python/mqt/debugger/dap/dap_server.py
🧰 Additional context used
🧬 Code graph analysis (1)
python/mqt/debugger/dap/dap_server.py (3)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
  • HighlightError (22-179)
  • encode (62-70)
python/mqt/debugger/dap/messages/output_dap_event.py (1)
  • encode (54-69)
python/mqt/debugger/pydebugger.pyi (3)
  • get_diagnostics (419-424)
  • potential_error_causes (527-535)
  • get_instruction_position (274-284)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (14)
python/mqt/debugger/dap/dap_server.py (14)

14-14: LGTM: Necessary imports added.

The re module is used for parse error location extraction, and Any is required for the new type annotations.

Also applies to: 17-17


118-119: LGTM: New state fields for highlight management.

The pending_highlights and _prevent_exit fields support the new highlight emission workflow.


172-186: LGTM: Proper reset of exit prevention flag.

The flag is correctly cleared after execution commands, ensuring the debugger can eventually exit after the user continues execution.


271-275: LGTM: Exit prevention integrated into regular checks.

The exit condition now respects the _prevent_exit flag, ensuring the debugger remains active while highlights are being processed by the client.


347-361: LGTM: Assertion failure handling enhanced with highlighting.

The category is appropriately set to "stderr" for assertion failures, and the new highlight collection provides better error context to the client.


431-457: LGTM: Robust highlight collection with fallback.

The method includes proper error handling and provides a sensible fallback when no specific error causes are identified, ensuring the user always gets feedback about the assertion failure location.


459-481: LGTM: Correct highlight entry construction.

The method properly handles instruction positions and edge cases, including newline characters at the end of instructions. The snippet extraction and coordinate calculation are consistent.


483-490: LGTM: Clear mapping of error cause types.

The static method provides clean string identifiers for error causes that can be consumed by the DAP client.


492-497: Note: Parse error replaces pending highlights.

Line 497 replaces pending_highlights with a single entry rather than appending. This means if multiple parse errors occur in quick succession, only the last one will be queued. This is likely intentional since parse errors typically occur during initial compilation, but verify this aligns with expected behavior.

💡 Alternative: Append instead of replace

If multiple parse errors should be shown simultaneously:

 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]
+        self.pending_highlights.append(entry)

513-547: LGTM: Sophisticated parse error highlighting with heuristics.

The method includes a helpful heuristic (lines 525-534) to find the previous non-empty line when an error is reported on an empty line at column 1. This improves the user experience by highlighting meaningful code rather than blank lines.

Minor note: The ternary check if end_column > 0 else column at line 542 is redundant since end_column is guaranteed to be positive (it's max(column, ...) where column >= 1), but it doesn't hurt to have defensive code.


549-572: LGTM: Correct recursive message flattening.

The method properly handles nested message structures and converts them to a flat list of strings for output. The recursive approach is clean and handles all expected types.


602-641: LGTM: Clean simple message sending implementation.

The new method provides a straightforward API for sending simple messages without hierarchical structure. The implementation correctly handles optional title, body, and end components.


574-600: LGTM: Refactored message hierarchy sending.

The method now accepts a category parameter and delegates to send_message_simple after flattening the message structure. This is a good refactoring that centralizes the message sending logic. The only caller (at line 347) has been properly updated to pass the category parameter as "stderr", and the send_message_simple method correctly accepts and forwards this parameter.


363-389: The edge case concern is invalid; negative line numbers are not possible.

When source_code is empty, "".split("\n") returns [''] (a single-element list containing an empty string), not an empty list. This means:

  • line = 1 if lines else 0 evaluates to 1
  • The loop executes at least once and the condition pos <= len(line_code) is satisfied for pos=0 and empty string
  • line = i + 1 assigns 1 before any final adjustment
  • With the default lines_start_at_one=True, line stays 1; with lines_start_at_one=False, it becomes 0

The method guarantees line ≥ 0 in all cases. The proposed line = max(0, line) guard is unnecessary.

Comment on lines +257 to +261
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.

Comment on lines +499 to +516
@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

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/dap_server.py (1)

579-606: send_state() sends invalid 1-based DAP output events with line=0, column=0 and source.

The DAP server is configured with lines_start_at_one = True and columns_start_at_one = True, but send_state() passes line=0, column=0 along with self.source_file to send_message_simple() (line 664), which creates an OutputDAPEvent with these zero-based indices and a source location. This violates DAP protocol expectations—clients interpreting the output event may treat this as an invalid source reference.

Omit source when sending generic state messages without meaningful code location, or use valid 1-based sentinel values (e.g., line=1, column=0).

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 118-119: pending_highlights is currently emitted after every
command (including TerminateDAPMessage/DisconnectDAPMessage) and _prevent_exit
is toggled in multiple places; change the logic so highlight emission is skipped
when handling terminate/disconnect/close messages and consolidate _prevent_exit
transitions to only the explicit terminate/close handling paths. Specifically,
before sending or flushing self.pending_highlights check the incoming message
type (e.g. TerminateDAPMessage, DisconnectDAPMessage, close/exit handlers) and
return/skip emission if one of those terminal messages is being processed;
remove or relocate other scattered assignments to self._prevent_exit so it is
only set/cleared in the dedicated terminate/close handler(s) to make session
lifecycle deterministic, and ensure pending_highlights is cleared only after a
successful non-terminal flush.
- Around line 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.

In @src/backend/dd/DDSimDebug.cpp:
- Around line 626-636: Replace the generic fallback messages with
context-specific ones for the ddsimLoadCode operation: in the catch(const
std::exception& e) block, if ddsim->lastErrorMessage is empty set it to a
descriptive string like "ddsimLoadCode: failed to load code" (keeping the actual
e.what() when present), and in the catch(...) block set ddsim->lastErrorMessage
to a similar context-aware message such as "ddsimLoadCode: unknown error while
loading code" so that ddsim->lastErrorMessage clearly indicates the failure
occurred during ddsimLoadCode.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 901eaf7 and d5afcf0.

📒 Files selected for processing (2)
  • python/mqt/debugger/dap/dap_server.py
  • src/backend/dd/DDSimDebug.cpp
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (1)
src/backend/dd/DDSimDebug.cpp (1)
include/backend/dd/DDSimDebug.hpp (1)
  • preprocessAssertionCode (678-678)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (5)
src/backend/dd/DDSimDebug.cpp (3)

78-84: LGTM! Clean implementation of error message accessor.

The function correctly returns nullptr for empty messages and the C-string otherwise. Note that the returned pointer's lifetime is tied to ddsim->lastErrorMessage and becomes invalid when the state is modified—this is standard for C APIs where callers are expected to use the pointer immediately.


603-619: Excellent state initialization.

The comprehensive clearing of all state-tracking containers (instructionTypes, instructionStarts, instructionEnds, functionDefinitions, assertionInstructions, successorInstructions, classicalRegisters, qubitRegisters, dataDependencies, functionCallers, targetQubits, instructionObjects) along with lastErrorMessage ensures a clean state between code loads. Setting ready = false at the start is correct since the state isn't ready until code loading completes successfully.


1423-1427: The early-return breakpoint behavior is intentional and correctly implements position snapping.

When desiredPosition is before the start of an instruction, the breakpoint is set at that instruction and the function returns OK. This is intentional snapping behavior: if the user attempts to set a breakpoint in whitespace or comments before an instruction, it snaps to the next actual instruction. The test case BreakpointOutside (setting a breakpoint at position 9999) confirms the overall design is correct—positions beyond all instructions properly return ERROR.

python/mqt/debugger/dap/dap_server.py (2)

373-385: code_pos_to_coordinates() boundary handling looks improved.
The pos <= len(line_code) and final “clamp to last line” fallback should reduce out-of-range coordinates for end-of-line / EOF positions.


355-362: Off-by-one error in snippet extraction within _build_highlight_entry().

get_instruction_position() returns positions with an exclusive end (despite documentation claiming "inclusive"), treating them as Python slice indices. Line 346 correctly uses source_code[start:end], but line 476 incorrectly adds 1: source_code[start_pos : end_pos + 1], including the character at the exclusive boundary. This causes the extracted snippet to over-select when end_pos lands on a terminal character (e.g., ; or \n).

The conditional logic at lines 471–475 compounds the issue by adding another + 1 offset only in the else branch for range calculation, creating inconsistency between snippet content and range coordinates.

Proposed fix
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", "")
+   end_line, end_column = self.code_pos_to_coordinates(end_pos)
+   snippet = self.source_code[start_pos : end_pos].replace("\r", "")

Likely an incorrect or invalid review comment.

Comment on lines +118 to +119
self.pending_highlights: list[dict[str, Any]] = []
self._prevent_exit = False
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

Guard highlight emission + _prevent_exit transitions to avoid “extra” events after terminate/close.
Right now pending_highlights is emitted after every command (including TerminateDAPMessage), which can result in a HighlightError event being sent after termination/exited events. Also _prevent_exit is toggled in multiple places; tightening the conditions makes session behavior more deterministic.

Proposed fix (skip highlight emission after terminate/disconnect; keep `_prevent_exit` behavior intentional)
@@
-            if self.pending_highlights:
+            if self.pending_highlights and not isinstance(
+                cmd,
+                (
+                    mqt.debugger.dap.messages.TerminateDAPMessage,
+                    mqt.debugger.dap.messages.DisconnectDAPMessage,
+                ),
+            ):
                 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
@@
-        if (
+        if (
             self.simulation_state.is_finished()
             and self.simulation_state.get_instruction_count() != 0
             and not self._prevent_exit
         ):

Also applies to: 172-187, 257-261, 271-275

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/dap_server.py around lines 118 - 119,
pending_highlights is currently emitted after every command (including
TerminateDAPMessage/DisconnectDAPMessage) and _prevent_exit is toggled in
multiple places; change the logic so highlight emission is skipped when handling
terminate/disconnect/close messages and consolidate _prevent_exit transitions to
only the explicit terminate/close handling paths. Specifically, before sending
or flushing self.pending_highlights check the incoming message type (e.g.
TerminateDAPMessage, DisconnectDAPMessage, close/exit handlers) and return/skip
emission if one of those terminal messages is being processed; remove or
relocate other scattered assignments to self._prevent_exit so it is only
set/cleared in the dedicated terminate/close handler(s) to make session
lifecycle deterministic, and ensure pending_highlights is cleared only after a
successful non-terminal flush.

Comment on lines 497 to 553
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)

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

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.

Comment on lines 626 to 636
} 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;
}
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 more context-specific error message.

The exception handling correctly captures error messages and provides a fallback. However, the fallback message "An error occurred while executing the operation" is generic and doesn't clearly indicate the context. Since this is in ddsimLoadCode, a more accurate message would be:

 } catch (const std::exception& e) {
   ddsim->lastErrorMessage = e.what();
   if (ddsim->lastErrorMessage.empty()) {
     ddsim->lastErrorMessage =
-        "An error occurred while executing the operation";
+        "An error occurred while loading the code";
   }
   return ERROR;
 } catch (...) {
-  ddsim->lastErrorMessage = "An error occurred while executing the operation";
+  ddsim->lastErrorMessage = "An error occurred while loading the code";
   return ERROR;
 }

This provides clearer diagnostics when code loading fails.

🤖 Prompt for AI Agents
In @src/backend/dd/DDSimDebug.cpp around lines 626 - 636, Replace the generic
fallback messages with context-specific ones for the ddsimLoadCode operation: in
the catch(const std::exception& e) block, if ddsim->lastErrorMessage is empty
set it to a descriptive string like "ddsimLoadCode: failed to load code"
(keeping the actual e.what() when present), and in the catch(...) block set
ddsim->lastErrorMessage to a similar context-aware message such as
"ddsimLoadCode: unknown error while loading code" so that
ddsim->lastErrorMessage clearly indicates the failure occurred during
ddsimLoadCode.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py:
- Around line 81-83: The variable parsed_successfully is undefined and the
conditional is illogical; either (A) if you intend to always clear highlights on
launch, remove the if and contextlib.suppress and call
server.simulation_state.reset_simulation() unconditionally (also remove the
now-unused contextlib import), or (B) if you only want to reset when parsing
succeeded, introduce and set a parsed_successfully boolean where parsing happens
(e.g., in the load_code/parse block), use that variable in the if and keep the
reset inside the conditional, ensuring parsed_successfully is initialized in all
code paths; update references to parsed_successfully, load_code, and
reset_simulation accordingly.

In @python/mqt/debugger/dap/messages/restart_dap_message.py:
- Around line 73-75: parsed_successfully is undefined and load_code lacks error
handling in restart handler; wrap the load_code call in the same try/except
RuntimeError pattern used in launch_dap_message.py to catch parse errors and
return a proper error response (use the "restart" action in the response), and
fix the conditional around server.simulation_state.reset_simulation by either
removing the undefined parsed_successfully check and always calling
reset_simulation (if intent is to clear highlights regardless) or introducing a
correctly scoped parsed_successfully boolean set from load_code success before
checking it; ensure you mirror the launch handler's control flow and error
messaging to avoid crashing the DAP server.

In @src/backend/dd/DDSimDebug.cpp:
- Around line 627-636: The fallback error string used when catching exceptions
in ddsimLoadCode is too generic; update the assignments to
ddsim->lastErrorMessage in both the std::exception and catch(...) handlers to a
more specific message such as "An error occurred while loading code" (or
"Unknown error while loading code") so the CLI's "Error loading code:" prefix
yields a clearer combined message; locate the two places where
ddsim->lastErrorMessage is set in the ddsimLoadCode error handlers and replace
the generic text accordingly.
- Line 1153: Remove the unused local pointer declaration "auto* ddsim =
toDDSimulationState(self);" — callers already use the "self" pointer and
interface methods directly; delete this line (and any now-empty include or using
if introduced solely for that conversion) to eliminate the unused-variable
warning, leaving the rest of the function to operate on "self" and its methods.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5afcf0 and a95050b.

📒 Files selected for processing (4)
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
  • src/backend/dd/DDSimDebug.cpp
  • src/frontend/cli/CliFrontEnd.cpp
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (2)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)
python/mqt/debugger/pydebugger.pyi (1)
  • reset_simulation (193-198)
python/mqt/debugger/dap/messages/launch_dap_message.py (1)
python/mqt/debugger/pydebugger.pyi (1)
  • reset_simulation (193-198)
🪛 Ruff (0.14.10)
python/mqt/debugger/dap/messages/restart_dap_message.py

73-73: Undefined name parsed_successfully

(F821)

python/mqt/debugger/dap/messages/launch_dap_message.py

81-81: Undefined name parsed_successfully

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (6)
src/frontend/cli/CliFrontEnd.cpp (1)

71-82: LGTM! Proper defensive error handling.

The implementation correctly handles the C-style function pointer interface with appropriate nullptr checks before calling getLastErrorMessage and validating the returned message. This enables users to see specific error details when code loading fails.

src/backend/dd/DDSimDebug.cpp (5)

78-84: LGTM! Clean error message accessor.

The implementation correctly returns nullptr for empty error messages rather than a pointer to an empty string, which is good API design and matches the caller's expectations in CliFrontEnd.cpp.


524-524: LGTM! Correct API wiring.

The new getLastErrorMessage function pointer is properly assigned during interface initialization.


584-584: LGTM! Proper error state initialization.

Clearing lastErrorMessage in ddsimInit ensures a clean starting state.


603-619: LGTM! Comprehensive load-time state reset.

The code properly clears all internal state containers including the error message at the beginning of ddsimLoadCode, ensuring each code load starts with a clean slate. Setting ready = false upfront is also correct.


1424-1428: The snap-forward behavior is intentional, well-designed, and adequately tested.

All three concerns from the original review are verified:

  1. Monotonic ordering confirmed: instructionStarts is populated sequentially via push_back(instruction.originalCodeStartPosition) (line 1691), guaranteeing strict monotonic increase.

  2. Behavioral change intentional and tested: The snap-forward logic (lines 1424-1428) is deliberately implemented to handle breakpoints in gaps between instruction ranges. Test coverage validates this indirectly—tests like TopLevelBreakpoints and InGateDefinitionBreakpoints use real source positions that may fall before instructions and verify the correct target instruction is selected.

  3. Edge cases properly handled:

    • Before first instruction: snaps to instruction 0 via early-return (lines 1424-1428)
    • Between function definitions: handled explicitly (lines 1430-1449)
    • Beyond all instructions: loop completes and returns ERROR (line 1456), preserving the BreakpointOutside test expectation

The snap-forward implementation is logically sound and consistent with typical debugger behavior (positioning breakpoints at the nearest valid instruction when an exact position is unavailable).

Comment on lines +627 to 636
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;
}
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 a more specific fallback error message.

The exception handling is solid, but the fallback message "An error occurred while executing the operation" is quite generic for a code loading context. Since this is in ddsimLoadCode and the CLI will prepend "Error loading code:", consider a more descriptive fallback:

Suggested improvement
     ddsim->lastErrorMessage = e.what();
     if (ddsim->lastErrorMessage.empty()) {
-      ddsim->lastErrorMessage =
-          "An error occurred while executing the operation";
+      ddsim->lastErrorMessage = "Failed to parse or import the provided code";
     }
     return ERROR;
   } catch (...) {
-    ddsim->lastErrorMessage = "An error occurred while executing the operation";
+    ddsim->lastErrorMessage = "Failed to parse or import the provided code";
     return ERROR;
   }
🤖 Prompt for AI Agents
In @src/backend/dd/DDSimDebug.cpp around lines 627 - 636, The fallback error
string used when catching exceptions in ddsimLoadCode is too generic; update the
assignments to ddsim->lastErrorMessage in both the std::exception and catch(...)
handlers to a more specific message such as "An error occurred while loading
code" (or "Unknown error while loading code") so the CLI's "Error loading code:"
prefix yields a clearer combined message; locate the two places where
ddsim->lastErrorMessage is set in the ddsimLoadCode error handlers and replace
the generic text accordingly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)

81-86: Response command should reflect request type and success should reflect parse result.

The response incorrectly returns "command": "launch" for a restart request and hardcodes "success": True despite the parsed_successfully variable tracking the actual outcome. When code parsing fails (lines 72-75), parsed_successfully is set to False and a parse error is queued, but the response still indicates success. This prevents DAP clients from detecting restart failures.

Fix
         return {
             "type": "response",
             "request_seq": self.sequence_number,
-            "success": True,
-            "command": "launch",
+            "success": parsed_successfully,
+            "command": "restart",
         }
🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py:
- Around line 69-70: The call to locale.getpreferredencoding uses a boolean
positional argument which triggers Ruff FBT003; update the call in
launch_dap_message.py where you read the file (the expression passed to
program_path.read_text) to use the keyword parameter form
locale.getpreferredencoding(do_setlocale=False) so the encoding argument is
explicit—this change affects the line assigning code =
program_path.read_text(encoding=...) before setting server.source_code.
- Around line 67-70: server.source_file currently stores a potentially relative
path via self.program which can cause editor highlights and breakpoints to
mis-map; update the assignment that sets server.source_file (the block assigning
server.source_file = {"name": program_path.name, "path": self.program}) to store
an absolute/normalized path instead, e.g., replace the value for "path" with the
resolved string form of program_path (use program_path.resolve() and convert to
str) so source_file.path is stable across CWD changes.

In @python/mqt/debugger/dap/messages/restart_dap_message.py:
- Line 70: The call to locale.getpreferredencoding uses a positional boolean;
update the call inside the line assigning to code (the
program_path.read_text(...) call) to pass the flag as a keyword: use
locale.getpreferredencoding(do_setlocale=False) so the encoding argument to
program_path.read_text(encoding=...) is supplied via the named parameter.
- Around line 78-80: The current code silently suppresses RuntimeError when
parsed_successfully is False by using contextlib.suppress(RuntimeError);
instead, replace this with targeted handling: either catch a more specific
exception (e.g., SimulationNotRunningError or whichever specific error
reset_simulation() raises) or use try/except RuntimeError as e and log the
exception at debug level before continuing (e.g., logger.debug or
logging.exception) so real bugs aren’t masked; update the block around
parsed_successfully / server.simulation_state.reset_simulation() to perform
narrow exception matching or log the caught RuntimeError with context rather
than suppressing it.

In @src/backend/dd/DDSimDebug.cpp:
- Around line 685-695: Update the documentation comment for the
LoadResult.message field in include/backend/debug.h to state that the returned
char* is owned by the implementation and has the same lifetime constraints as
getLastErrorMessage: it remains valid only until the next interface call that
may modify the error state (i.e., until DDSimulationState::lastErrorMessage is
changed). Reference the LoadResult.message field and mirror the exact lifetime
wording used by getLastErrorMessage so callers understand the pointer is not
heap-owned and must be copied if needed beyond the next interface call.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a95050b and cf270eb.

📒 Files selected for processing (13)
  • bindings/InterfaceBindings.cpp
  • include/backend/dd/DDSimDebug.hpp
  • include/backend/debug.h
  • python/mqt/debugger/dap/dap_server.py
  • python/mqt/debugger/dap/messages/__init__.py
  • python/mqt/debugger/dap/messages/highlight_error_dap_message.py
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
  • python/mqt/debugger/pydebugger.pyi
  • src/backend/dd/DDSimDebug.cpp
  • src/common/parsing/CodePreprocessing.cpp
  • src/frontend/cli/CliFrontEnd.cpp
  • test/python/test_dap_server.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
  • src/common/parsing/CodePreprocessing.cpp
📚 Learning: 2025-11-05T09:23:46.540Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: src/qdmi/dd/Device.cpp:492-521
Timestamp: 2025-11-05T09:23:46.540Z
Learning: In the munich-quantum-toolkit/core repository, the `qasm3::Importer::imports()` function is backwards compatible with OpenQASM 2 programs. Therefore, it can be used to parse both QASM2 and QASM3 program formats without requiring separate importers for each version.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (7)
include/backend/debug.h (2)
include/frontend/cli/CliFrontEnd.hpp (2)
  • code (46-46)
  • code (68-68)
test/utils/common_fixtures.hpp (6)
  • code (211-215)
  • code (211-213)
  • code (236-244)
  • code (236-237)
  • code (254-256)
  • code (254-254)
src/backend/dd/DDSimDebug.cpp (1)
include/backend/dd/DDSimDebug.hpp (2)
  • ddsimLoadCodeWithResult (302-302)
  • ddsimLoadCode (294-294)
src/common/parsing/CodePreprocessing.cpp (3)
include/common/parsing/ParsingError.hpp (1)
  • ParsingError (32-32)
src/common/parsing/ParsingError.cpp (1)
  • ParsingError (23-23)
include/common/parsing/CodePreprocessing.hpp (1)
  • isVariableDeclaration (296-296)
include/backend/dd/DDSimDebug.hpp (1)
test/utils/common_fixtures.hpp (6)
  • code (211-215)
  • code (211-213)
  • code (236-244)
  • code (236-237)
  • code (254-256)
  • code (254-254)
test/python/test_dap_server.py (2)
python/mqt/debugger/dap/dap_server.py (2)
  • code_pos_to_coordinates (362-388)
  • collect_highlight_entries (430-461)
python/mqt/debugger/pydebugger.pyi (3)
  • potential_error_causes (552-560)
  • get_instruction_position (299-309)
  • get_diagnostics (444-449)
src/frontend/cli/CliFrontEnd.cpp (1)
include/frontend/cli/CliFrontEnd.hpp (2)
  • state (62-63)
  • state (75-75)
python/mqt/debugger/dap/messages/__init__.py (1)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (1)
  • HighlightError (22-179)
🪛 Cppcheck (2.19.0)
src/backend/dd/DDSimDebug.cpp

[information] 37-37: Include file

(missingInclude)

src/common/parsing/CodePreprocessing.cpp

[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)

🪛 Ruff (0.14.10)
python/mqt/debugger/dap/messages/launch_dap_message.py

69-69: Boolean positional value in function call

(FBT003)

python/mqt/debugger/dap/messages/restart_dap_message.py

70-70: Boolean positional value in function call

(FBT003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (42)
python/mqt/debugger/dap/messages/launch_dap_message.py (1)

71-79: The proposed None-checks are unnecessary. The LoadResult.line and LoadResult.column fields are typed as int (not Optional[int]), and 0 is a documented sentinel value for "unknown location"—not an invalid state. The code already handles 0 defensively: _build_parse_error_highlight clamps line to [1, num_lines] and column to [1, ∞), so sentinel values are safely converted to valid positions. The current implementation is correct and requires no changes.

python/mqt/debugger/dap/messages/restart_dap_message.py (2)

13-13: contextlib import is fine, but keep exception suppression tightly scoped.
You’re adding contextlib solely to suppress RuntimeError later; that suppression should ideally be narrowed/logged (see comment on Line 78-80).


68-77: Parse-result handling: ensure restart clears prior diagnostics and stopOnEntry is truly boolean.
Good: setting server.source_file before load improves traceability, and gating run_simulation() on parse success avoids cascading failures. Please ensure any previously queued parse errors / highlight state are cleared on restart somewhere (not shown here), otherwise old diagnostics may persist in the new run. Also self.stop_on_entry is declared bool; verify it can’t be a non-bool truthy value from request parsing, since this now controls whether execution starts.

python/mqt/debugger/dap/messages/highlight_error_dap_message.py (5)

47-61: Verify message validation handles None values correctly.

The validation on line 58 converts the message to a string before checking if it's empty. If highlight["message"] is None, str(None) returns the string "None", which would pass the validation check even though it's not a valid message.

Consider adding an explicit check for None:

🛡️ Proposed fix to handle None values
         for highlight in self.highlights:
-            if "message" not in highlight or not str(highlight["message"]).strip():
+            message = highlight.get("message")
+            if message is None or not str(message).strip():
                 msg = "Each highlight entry must contain a descriptive 'message'."
                 raise ValueError(msg)

22-45: LGTM: Clean initialization with proper normalization.

The constructor properly normalizes highlights and source data before calling the parent constructor, ensuring all data is validated and in the correct format.


62-70: LGTM: Standard DAP encoding pattern.

The encode method follows the standard DAP event encoding pattern by extending the parent's encoded output with a body containing highlights and source.


111-137: LGTM: Robust position validation.

The position normalization includes proper type checking, KeyError handling with informative error messages, and integer conversion.


139-162: LGTM: Defensive source normalization.

The method creates a defensive copy and validates required fields (name and path) are present, with clear error messages for violations.

python/mqt/debugger/dap/messages/__init__.py (1)

24-24: LGTM: Proper public API exposure.

The new HighlightError class is correctly imported and exported via __all__, making it available as part of the module's public API. The placement maintains alphabetical order in the export list.

Also applies to: 61-61

src/frontend/cli/CliFrontEnd.cpp (1)

72-80: LGTM: Robust error message handling.

The enhanced error reporting includes proper defensive checks:

  • Validates the getLastErrorMessage function pointer exists before calling it
  • Checks both that the message is not nullptr and not an empty string
  • Falls back to a generic error message when detailed information is unavailable

This follows good practices for optional function pointer usage in C.

test/python/test_dap_server.py (2)

18-25: LGTM: Correctly tests newline position handling.

The test verifies that code_pos_to_coordinates correctly handles positions at newline characters, ensuring they stay on the current line with the correct 1-based column index.


28-44: LGTM: Effective mocking for highlight range verification.

The test uses SimpleNamespace to create a minimal mock of the simulation state, focusing on testing that highlight entries don't incorrectly span multiple instructions. The type: ignore comment on line 38 is appropriate for test mocking scenarios.

python/mqt/debugger/pydebugger.pyi (2)

98-111: LGTM: Well-documented LoadResult class.

The new LoadResult class provides a clean interface for code loading diagnostics with clear documentation:

  • Boolean success indicator
  • 1-based line and column positions (with 0 indicating unknown)
  • Error message string

The documentation explicitly notes the indexing convention, which helps prevent off-by-one errors.


152-160: LGTM: Clear method signature with diagnostic return.

The load_code_with_result method provides an enhanced version of load_code that returns structured diagnostic information via LoadResult. The docstring clearly describes the purpose and return value.

include/backend/debug.h (2)

41-61: Well-designed LoadResult struct with proper documentation.

The LoadResult struct provides a clean API for returning diagnostic information from code loading operations. The 1-based line/column convention with 0 indicating "unavailable" is clearly documented.


79-97: New API entries properly extend the SimulationState interface.

The loadCodeWithResult and getLastErrorMessage function pointers are well-documented. The ownership semantics for the returned const char* from getLastErrorMessage are clearly specified ("remains valid until the next interface call that modifies the error state").

include/backend/dd/DDSimDebug.hpp (2)

122-133: Error state fields properly added to DDSimulationState.

The new lastErrorMessage, lastErrorLine, and lastErrorColumn fields extend the simulation state to track error details. The default initializers (= 0) for line and column ensure a well-defined initial state.


295-302: New API declaration aligns with header contract.

The ddsimLoadCodeWithResult function declaration is properly documented and matches the LoadResult return type defined in debug.h.

bindings/InterfaceBindings.cpp (3)

169-184: LoadResult binding safely handles nullable message field.

The message property uses a lambda that properly handles the case where self.message is nullptr, returning an empty string instead. This prevents potential null pointer dereferences in Python code.


194-204: Improved error handling with descriptive messages.

The updated load_code binding now retrieves error messages via getLastErrorMessage when available, providing more informative exceptions to Python callers. The null check on the function pointer (self->getLastErrorMessage ? ...) is good defensive programming for backward compatibility.


210-225: New load_code_with_result binding with proper availability check.

The binding correctly checks if loadCodeWithResult is available before calling it, throwing a clear runtime_error if not. This handles the case where older implementations may not support this method.

src/common/parsing/CodePreprocessing.cpp (6)

38-49: Clean input validation helper.

The isDigits function properly handles the empty string case and uses std::ranges::all_of idiomatically. The cast to unsigned char before calling std::isdigit is correct practice to avoid undefined behavior with negative char values.


51-76: Line/column computation has quadratic complexity for repeated calls.

The lineColumnForOffset function iterates through all characters up to lineStart to count newlines. If called repeatedly for many offsets, this results in O(n²) complexity overall. Consider caching line start positions if performance becomes a concern.

For now, this is acceptable given typical file sizes, but worth noting for future optimization.


154-208: Robust target validation with comprehensive error handling.

The validateTargets function properly validates:

  1. Target format (register name with bracket notation)
  2. Index is digits-only
  3. Index is within register bounds
  4. Respects shadowed registers in function scopes

The early continue for empty targets and targets without brackets allows non-indexed references to pass through, which is intentional.


509-513: Improved trueStart calculation accounts for leading whitespace.

The updated logic correctly finds the first non-whitespace character within the block and adds blocksOffset to account for expanded block content. This ensures more accurate error position reporting.


539-550: Strengthened register declaration parsing with proper error handling.

The register size parsing now:

  1. Validates the size text contains only digits via isDigits
  2. Uses std::stoul with exception handling
  3. Throws ParsingError with detailed location information on failure

This prevents crashes from malformed register declarations.


615-624: Centralized target validation applied to assertions and general instructions.

The validateTargets calls ensure consistent validation across both assertion targets and general instruction targets. The context parameter (" in assertion" vs "") provides appropriate error message differentiation.

src/backend/dd/DDSimDebug.cpp (6)

79-85: Correct null return for empty error message.

The ddsimGetLastErrorMessage function correctly returns nullptr when the error message is empty, matching the documented behavior that callers should check for null.


525-526: Interface function pointers properly wired.

Both loadCodeWithResult and getLastErrorMessage are correctly assigned to their implementation functions during state initialization.


586-588: Error state properly reset on init.

Clearing lastErrorMessage and resetting line/column to 0 in ddsimInit ensures a clean error state when the simulation is initialized.


607-625: Comprehensive state clearing in loadCode.

All relevant state containers are cleared before loading new code, ensuring no stale data persists between loads. This directly addresses the PR objective of clearing state on restart.


632-670: Comprehensive exception handling with error state capture.

The exception handlers properly capture error information from various exception types:

  • CompilerError: Uses debugInfo for line/column when available
  • ConstEvalError/TypeCheckError: Captures message, defaults line/column to 1
  • Generic std::exception: Captures message with fallback
  • Catch-all: Sets generic error message

This ensures meaningful error feedback regardless of the failure mode.


1471-1475: Breakpoint handling fix for positions before first instruction.

The new early return handles the case where desiredPosition falls before the start of any instruction. Instead of failing, it now sets the breakpoint at the first instruction whose start is greater than the desired position. This is a reasonable fallback behavior.

python/mqt/debugger/dap/dap_server.py (9)

117-118: State fields for highlight event management.

The pending_highlights list queues highlight data for emission after command handling, while _prevent_exit ensures the client receives highlight events before seeing exit events. This coordination is essential for proper UX.


171-185: Reset _prevent_exit after stepping/execution commands.

Resetting _prevent_exit to False after processing stepping commands ensures the flag only persists for one command cycle. This prevents the exit event from being indefinitely blocked.


256-260: Highlight event emission with proper state management.

The pending highlights are emitted as a HighlightError event, then cleared. Setting _prevent_exit = True ensures the client has time to process the highlight before receiving any exit event.


270-274: Exit event gated by _prevent_exit flag.

Adding not self._prevent_exit to the condition prevents the ExitedDAPEvent from being sent when highlights are pending. This ensures the client receives and displays the error highlighting before the session terminates.


372-388: Improved edge case handling in code_pos_to_coordinates.

The updated logic:

  1. Initializes line = 1 for empty files (line 372)
  2. Uses for...else to handle positions past the end of the file (lines 380-383)
  3. Returns the last line/column when position exceeds file length

This prevents index errors and provides sensible defaults.


430-461: Highlight entry collection with fallback behavior.

The collect_highlight_entries method:

  1. Collects highlights for each error cause
  2. Falls back to highlighting the failing instruction if no specific causes are found
  3. Gracefully handles RuntimeError when diagnostics are unavailable

This ensures meaningful feedback even when detailed diagnostics aren't available.


502-536: Parse error highlight with smart line detection.

The _build_parse_error_highlight method includes logic to find the previous non-empty line when the error is reported on an empty line at column 1 (lines 514-523). This improves the user experience by highlighting where the actual error likely occurred rather than an empty line.


538-561: Recursive message flattening for complex structures.

The _flatten_message_parts method handles nested message structures (strings, dicts with title/body/end, lists) and flattens them into a simple list of strings. The recursion handles arbitrary nesting depths.


591-630: Refactored message sending with clear separation.

The send_message_simple method provides a clean interface for sending output events. The separation from send_message_hierarchy improves code organization - the hierarchy method now handles structure normalization while the simple method handles the actual sending.

Comment on lines +67 to 70
server.source_file = {"name": program_path.name, "path": self.program}
parsed_successfully = True
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
server.source_code = code
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefer storing an absolute/normalized source_file.path for stable editor mapping.

If self.program can be relative, highlights/breakpoints can mis-map across CWD changes. Consider str(program_path.resolve()).

Proposed diff
         program_path = Path(self.program)
-        server.source_file = {"name": program_path.name, "path": self.program}
+        server.source_file = {"name": program_path.name, "path": str(program_path.resolve())}
         parsed_successfully = True
📝 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
server.source_file = {"name": program_path.name, "path": self.program}
parsed_successfully = True
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
server.source_code = code
server.source_file = {"name": program_path.name, "path": str(program_path.resolve())}
parsed_successfully = True
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
server.source_code = code
🧰 Tools
🪛 Ruff (0.14.10)

69-69: Boolean positional value in function call

(FBT003)

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py around lines 67 - 70,
server.source_file currently stores a potentially relative path via self.program
which can cause editor highlights and breakpoints to mis-map; update the
assignment that sets server.source_file (the block assigning server.source_file
= {"name": program_path.name, "path": self.program}) to store an
absolute/normalized path instead, e.g., replace the value for "path" with the
resolved string form of program_path (use program_path.resolve() and convert to
str) so source_file.path is stable across CWD changes.

Comment on lines 69 to 70
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
server.source_code = code
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Ruff FBT003: avoid boolean positional arg in locale.getpreferredencoding.

Proposed diff
-        code = program_path.read_text(encoding=locale.getpreferredencoding(False))
+        code = program_path.read_text(encoding=locale.getpreferredencoding(do_setlocale=False))
📝 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
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
server.source_code = code
code = program_path.read_text(encoding=locale.getpreferredencoding(do_setlocale=False))
server.source_code = code
🧰 Tools
🪛 Ruff (0.14.10)

69-69: Boolean positional value in function call

(FBT003)

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py around lines 69 - 70,
The call to locale.getpreferredencoding uses a boolean positional argument which
triggers Ruff FBT003; update the call in launch_dap_message.py where you read
the file (the expression passed to program_path.read_text) to use the keyword
parameter form locale.getpreferredencoding(do_setlocale=False) so the encoding
argument is explicit—this change affects the line assigning code =
program_path.read_text(encoding=...) before setting server.source_code.

program_path = Path(self.program)
server.source_file = {"name": program_path.name, "path": self.program}
parsed_successfully = True
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ruff FBT003: use keyword argument for getpreferredencoding.

Proposed fix
-        code = program_path.read_text(encoding=locale.getpreferredencoding(False))
+        code = program_path.read_text(encoding=locale.getpreferredencoding(do_setlocale=False))
📝 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
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
code = program_path.read_text(encoding=locale.getpreferredencoding(do_setlocale=False))
🧰 Tools
🪛 Ruff (0.14.10)

70-70: Boolean positional value in function call

(FBT003)

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/restart_dap_message.py at line 70, The call
to locale.getpreferredencoding uses a positional boolean; update the call inside
the line assigning to code (the program_path.read_text(...) call) to pass the
flag as a keyword: use locale.getpreferredencoding(do_setlocale=False) so the
encoding argument to program_path.read_text(encoding=...) is supplied via the
named parameter.

Comment on lines 685 to 695
LoadResult ddsimLoadCodeWithResult(SimulationState* self, const char* code) {
const auto result = ddsimLoadCode(self, code);
const auto* ddsim = toDDSimulationState(self);
LoadResult loadResult{};
loadResult.success = (result == OK);
loadResult.line = ddsim->lastErrorLine;
loadResult.column = ddsim->lastErrorColumn;
loadResult.message =
ddsim->lastErrorMessage.empty() ? "" : ddsim->lastErrorMessage.c_str();
return loadResult;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -A5 "load_code_with_result" bindings/InterfaceBindings.cpp

Repository: munich-quantum-toolkit/debugger

Length of output: 531


🏁 Script executed:

rg -n "struct LoadResult" src/

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

rg -n "lastErrorMessage" src/backend/dd/

Repository: munich-quantum-toolkit/debugger

Length of output: 1096


🏁 Script executed:

rg -n "LoadResult" src/backend/dd/ --type cpp --type h

Repository: munich-quantum-toolkit/debugger

Length of output: 247


🏁 Script executed:

fd "DDSimDebug.h" src/

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

cat src/backend/dd/DDSimDebug.h

Repository: munich-quantum-toolkit/debugger

Length of output: 134


🏁 Script executed:

fd "\.h$" src/ | head -20

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

rg -n "LoadResult" --type h src/

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

head -50 src/backend/dd/DDSimDebug.cpp

Repository: munich-quantum-toolkit/debugger

Length of output: 1369


🏁 Script executed:

find src/ -name "DDSimDebug.hpp"

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

rg -n "struct LoadResult|using LoadResult|typedef.*LoadResult" src/ bindings/

Repository: munich-quantum-toolkit/debugger

Length of output: 57


🏁 Script executed:

rg -n "LoadResult" bindings/

Repository: munich-quantum-toolkit/debugger

Length of output: 665


🏁 Script executed:

head -170 bindings/InterfaceBindings.cpp | tail -30

Repository: munich-quantum-toolkit/debugger

Length of output: 1453


🏁 Script executed:

rg -n "LoadResult" . --type h --type hpp

Repository: munich-quantum-toolkit/debugger

Length of output: 102


🏁 Script executed:

find . -name "*.hpp" -o -name "*.h" | xargs rg -l "LoadResult" 2>/dev/null

Repository: munich-quantum-toolkit/debugger

Length of output: 136


🏁 Script executed:

cat ./include/backend/dd/DDSimDebug.hpp

Repository: munich-quantum-toolkit/debugger

Length of output: 26773


🏁 Script executed:

cat ./include/backend/debug.h

Repository: munich-quantum-toolkit/debugger

Length of output: 17649


🏁 Script executed:

rg -A10 "typedef struct {" include/backend/debug.h | grep -A10 "LoadResult"

Repository: munich-quantum-toolkit/debugger

Length of output: 170


Add lifetime documentation to LoadResult.message field.

The LoadResult.message field holds a pointer to the internal buffer of DDSimulationState::lastErrorMessage, with the same lifetime constraint as the related getLastErrorMessage function. While the current implementation is safe because Python bindings immediately copy the string, this constraint is not documented in the struct definition, creating an inconsistency with getLastErrorMessage which explicitly documents: "The returned pointer is owned by the implementation and remains valid until the next interface call that modifies the error state."

Update the documentation comment for LoadResult.message in include/backend/debug.h to include the same lifetime constraint notice.

🤖 Prompt for AI Agents
In @src/backend/dd/DDSimDebug.cpp around lines 685 - 695, Update the
documentation comment for the LoadResult.message field in
include/backend/debug.h to state that the returned char* is owned by the
implementation and has the same lifetime constraints as getLastErrorMessage: it
remains valid only until the next interface call that may modify the error state
(i.e., until DDSimulationState::lastErrorMessage is changed). Reference the
LoadResult.message field and mirror the exact lifetime wording used by
getLastErrorMessage so callers understand the pointer is not heap-owned and must
be copied if needed beyond the next interface call.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)

87-87: Fix incorrect command field in response.

The response returns "command": "launch" but this is a restart request handler. It should return "command": "restart" to correctly identify the command being responded to.

🐛 Proposed fix
         return {
             "type": "response",
             "request_seq": self.sequence_number,
             "success": True,
-            "command": "launch",
+            "command": "restart",
         }
🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py:
- Around line 68-81: The file-loading/parse/run/reset sequence duplicated in
launch_dap_message.py and restart_dap_message.py should be extracted into a
DAPServer helper (e.g., DAPServer.load_program_file(self, program_path: Path,
stop_on_entry: bool) -> bool) that opens the file with
locale.getpreferredencoding(False), assigns self.source_code, calls
self.simulation_state.load_code(code) and on RuntimeError calls
self.queue_parse_error(str(exc)), runs self.simulation_state.run_simulation()
when parsed and not stop_on_entry, and resets the simulation on failure with
contextlib.suppress(RuntimeError) before returning the parsed_successfully
boolean; replace the duplicated blocks in both
LaunchDAPMessage/RestartDAPMessage handlers with setting server.source_file =
{"name": program_path.name, "path": self.program} and a call to
server.load_program_file(program_path, self.stop_on_entry) (or equivalent
attribute names used in those classes).

In @python/mqt/debugger/dap/messages/restart_dap_message.py:
- Around line 69-82: The code that opens program_path, reads code, sets
server.source_code, calls server.simulation_state.load_code, handles
RuntimeError by setting parsed_successfully and calling
server.queue_parse_error, then conditionally calling
server.simulation_state.run_simulation or reset_simulation is duplicated with
launch_dap_message.py; extract a shared helper (e.g., a function like
load_and_start_simulation(server, program_path, stop_on_entry)) that
encapsulates reading the file, assigning server.source_code, calling
simulation_state.load_code with exception handling (queue_parse_error on
failure) and running or resetting the simulation based on parsed_successfully
and stop_on_entry, then replace the duplicated block in restart_dap_message.py
and launch_dap_message.py with a call to that helper to ensure consistency.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf270eb and 489592b.

📒 Files selected for processing (8)
  • bindings/InterfaceBindings.cpp
  • include/backend/dd/DDSimDebug.hpp
  • include/backend/debug.h
  • python/mqt/debugger/dap/dap_server.py
  • python/mqt/debugger/dap/messages/highlight_error_dap_message.py
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
  • src/backend/dd/DDSimDebug.cpp
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (4)
python/mqt/debugger/dap/messages/launch_dap_message.py (2)
python/mqt/debugger/pydebugger.pyi (3)
  • load_code (130-135)
  • run_simulation (183-188)
  • reset_simulation (193-198)
python/mqt/debugger/dap/dap_server.py (1)
  • queue_parse_error (497-502)
src/backend/dd/DDSimDebug.cpp (2)
include/frontend/cli/CliFrontEnd.hpp (2)
  • code (46-46)
  • code (68-68)
include/backend/dd/DDSimDebug.hpp (1)
  • preprocessAssertionCode (678-678)
python/mqt/debugger/dap/messages/restart_dap_message.py (2)
python/mqt/debugger/pydebugger.pyi (3)
  • load_code (130-135)
  • run_simulation (183-188)
  • reset_simulation (193-198)
python/mqt/debugger/dap/dap_server.py (1)
  • queue_parse_error (497-502)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (3)
python/mqt/debugger/dap/messages/dap_event.py (1)
  • DAPEvent (17-41)
python/mqt/debugger/dap/messages/launch_dap_message.py (1)
  • validate (44-55)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)
  • validate (44-55)
🪛 Ruff (0.14.10)
python/mqt/debugger/dap/messages/launch_dap_message.py

69-69: Boolean positional value in function call

(FBT003)

python/mqt/debugger/dap/messages/restart_dap_message.py

70-70: Boolean positional value in function call

(FBT003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (22)
include/backend/dd/DDSimDebug.hpp (1)

122-125: LGTM! Error message storage added.

The lastErrorMessage field integrates cleanly with the new getLastErrorMessage API to provide detailed error diagnostics.

bindings/InterfaceBindings.cpp (1)

177-187: Excellent error handling improvement.

The binding now retrieves detailed error messages via getLastErrorMessage when available, providing much better debugging information to Python callers. The defensive null check ensures backward compatibility.

python/mqt/debugger/dap/messages/launch_dap_message.py (1)

69-69: Static analysis false positive - standard library API.

The FBT003 warning about the boolean positional argument is a false positive. The locale.getpreferredencoding(False) call follows the standard library API where False disables locale aliasing.

include/backend/debug.h (1)

57-66: Well-documented API addition.

The getLastErrorMessage function pointer is a clean extension to the public interface. The documentation clearly specifies ownership, lifetime, and return semantics, which is essential for C API consumers.

python/mqt/debugger/dap/messages/restart_dap_message.py (1)

70-70: Static analysis false positive - standard library API.

The FBT003 warning about the boolean positional argument is a false positive. The locale.getpreferredencoding(False) call follows the standard library API where False disables locale aliasing.

src/backend/dd/DDSimDebug.cpp (5)

78-84: Verify lifetime guarantees for returned C-string pointer.

The function returns a pointer to the internal buffer of lastErrorMessage via c_str(). This pointer becomes invalid if lastErrorMessage is modified or cleared. While the header documentation mentions the caller doesn't own the string, ensure all call sites understand the pointer is only valid until the next operation that might modify lastErrorMessage (e.g., loadCode, init).

Based on the header comment in include/backend/debug.h that documents ownership.


621-636: Robust error handling implementation.

The exception handling properly captures error messages from both std::exception and unknown exceptions, with a sensible fallback for empty messages. This ensures users always receive actionable feedback when code loading fails.


603-619: Comprehensive state reset on code load.

The extensive clearing of all instruction-related state containers ensures no stale data persists between code loads. Setting ready = false early prevents operations on partially-loaded state.


1153-1153: LGTM - Early retrieval of typed simulation state.

Retrieving the DDSimulationState pointer at the function start is a reasonable refactoring that makes the typed state available for the entire function scope.


1424-1428: The early-return logic for desiredPosition < start is intentional and correct. This condition handles character positions that fall in gaps between instructions, mapping them to the next instruction. The comprehensive test cases (e.g., TopLevelBreakpoints, InGateDefinitionBreakpoints) verify this behavior: positions like 174 map to instruction 4, position 451 to instruction 12, etc. The design is sound—any valid character position maps to either its containing instruction or the next one. Positions outside all instructions return ERROR (after the loop completes without match).

python/mqt/debugger/dap/messages/highlight_error_dap_message.py (4)

47-60: LGTM - Validation ensures meaningful error feedback.

The validation correctly ensures at least one highlight exists and each contains a descriptive message. The structural validation of ranges and positions is appropriately handled during normalization in the constructor.


72-109: Excellent defensive normalization with sensible defaults.

The method thoroughly validates the highlight structure while providing reasonable defaults for optional fields. The type checks for Mapping and range validation ensure malformed data is caught early with clear error messages.


111-137: LGTM - Position normalization with clear error messages.

The method properly validates position structure and converts keys to integers. The error handling provides actionable feedback when required fields are missing. The absence of non-negative validation is acceptable since range ordering is validated separately.


164-179: LGTM - Elegant range ordering validation.

The tuple comparison provides clean lexicographic ordering of positions. Using get() with defaults is safe here since _normalize_position ensures positions are valid before this check is performed.

python/mqt/debugger/dap/dap_server.py (8)

118-119: LGTM - Clear state initialization.

The new runtime state fields are properly initialized with appropriate types and default values.


172-186: LGTM - Appropriate reset of exit prevention on user actions.

Resetting _prevent_exit when the user explicitly steps, continues, or restarts ensures the debugger doesn't stay open unnecessarily after user interaction.


257-261: LGTM - Proper highlight emission and state management.

The sequence correctly emits pending highlights, clears the queue to prevent duplicates, and sets the exit prevention flag to ensure users can view the highlighted errors.


373-389: LGTM - Improved edge case handling for out-of-bounds positions.

The modified logic now handles positions beyond the file end by setting the location to the end of the last line. The for-else construct correctly differentiates between finding a position (with break) and reaching the end without finding it.


431-462: LGTM - Robust highlight collection with sensible fallback.

The method properly handles missing diagnostics and ensures at least the failing instruction is highlighted when no specific error causes are identified. The defensive attribute check for source_code prevents errors when called before initialization.


504-516: LGTM - Robust parse error location extraction.

The regex correctly extracts line, column, and detail from standard compiler error format, with appropriate fallback for non-standard messages.


554-646: Excellent refactoring - improved separation of concerns.

The new message sending architecture cleanly separates:

  • Message structure flattening (_flatten_message_parts)
  • Hierarchy-specific processing (send_message_hierarchy)
  • Core sending logic (send_message_simple)

This improves maintainability and makes the code easier to test and extend.


518-552: Excellent parse error positioning with smart fallback logic.

The method intelligently handles edge cases like empty lines by searching backwards for meaningful content. The clamping of coordinates prevents out-of-bounds errors.

Minor: Redundant check in column calculation

Line 539's ternary if stripped else 1 is redundant since it's already inside an if stripped: block at line 538. The condition will always be true. Consider simplifying:

-                column = max(1, len(line_text) - len(stripped) + 1) if stripped else 1
+                column = max(1, len(line_text) - len(stripped) + 1)

Likely an incorrect or invalid review comment.

Comment on lines 69 to 82
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()
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 extracting shared code loading logic.

This error handling pattern is duplicated from launch_dap_message.py (lines 68-81). Consider extracting a shared helper method to reduce duplication and ensure consistency across launch and restart flows.

See the suggested refactor in the review comment for launch_dap_message.py lines 68-81.

🧰 Tools
🪛 Ruff (0.14.10)

70-70: Boolean positional value in function call

(FBT003)

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/restart_dap_message.py around lines 69 -
82, The code that opens program_path, reads code, sets server.source_code, calls
server.simulation_state.load_code, handles RuntimeError by setting
parsed_successfully and calling server.queue_parse_error, then conditionally
calling server.simulation_state.run_simulation or reset_simulation is duplicated
with launch_dap_message.py; extract a shared helper (e.g., a function like
load_and_start_simulation(server, program_path, stop_on_entry)) that
encapsulates reading the file, assigning server.source_code, calling
simulation_state.load_code with exception handling (queue_parse_error on
failure) and running or resetting the simulation based on parsed_successfully
and stop_on_entry, then replace the duplicated block in restart_dap_message.py
and launch_dap_message.py with a call to that helper to ensure consistency.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)

82-88: DAP response is inconsistent with a restart request (and reports success on parse failure).
For DAP, the response "command" should match the request ("restart"), and "success" should reflect parsed_successfully (otherwise clients may treat failures as successful restarts). This is likely a protocol-level bug.

Proposed diff
         return {
             "type": "response",
             "request_seq": self.sequence_number,
-            "success": True,
-            "command": "launch",
+            "success": parsed_successfully,
+            "command": "restart",
         }
🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py:
- Around line 78-80: Replace the silent contextlib.suppress around
server.simulation_state.reset_simulation() so that when parsed_successfully is
False any RuntimeError is caught and logged at debug level (including the
exception info) before being ignored; e.g., wrap the call to
server.simulation_state.reset_simulation() in a try/except RuntimeError as e and
call the module or server logger (e.g., logger.debug or server.logger.debug)
with a short message referencing reset_simulation and the caught exception.

In @python/mqt/debugger/dap/messages/restart_dap_message.py:
- Line 70: The call to locale.getpreferredencoding uses a boolean positional
arg; update the assignment that reads the file with program_path.read_text to
call locale.getpreferredencoding using the keyword form (e.g.,
locale.getpreferredencoding(do_setlocale=False)) so the intent is explicit and
Ruff FBT003 is satisfied; keep the rest of the code unchanged and only replace
the positional boolean in the code = program_path.read_text(encoding=...) line.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 489592b and 4de1403.

📒 Files selected for processing (2)
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
🧰 Additional context used
🧬 Code graph analysis (1)
python/mqt/debugger/dap/messages/launch_dap_message.py (2)
python/mqt/debugger/pydebugger.pyi (3)
  • load_code (130-135)
  • run_simulation (183-188)
  • reset_simulation (193-198)
python/mqt/debugger/dap/dap_server.py (1)
  • queue_parse_error (497-502)
🪛 Ruff (0.14.10)
python/mqt/debugger/dap/messages/restart_dap_message.py

70-70: Boolean positional value in function call

(FBT003)

python/mqt/debugger/dap/messages/launch_dap_message.py

69-69: Boolean positional value in function call

(FBT003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (3)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)

68-81: Verify parse-failure cleanup doesn’t wipe the queued parse error/highlights.
On RuntimeError, you queue_parse_error(...) and then (conditionally) reset_simulation() again. If reset_simulation() clears any queued DAP events / highlight state, the user may never see the parse error.

If the second reset is only meant to clear stale state, consider either (a) not resetting again on parse failure, or (b) moving reset earlier / making reset_simulation() not affect queued events.

python/mqt/debugger/dap/messages/launch_dap_message.py (2)

67-68: LGTM! Good initialization order.

Setting source_file before attempting to parse ensures it's available for error handling and highlight generation. The parsed_successfully flag cleanly tracks parse state for the conditional logic below.


81-86: This deviates from the DAP specification—launch responses should have success: false on parse failures.

The handler intentionally returns success: True even when parsing fails, deferring error reporting to the custom highlightError event. While this design is consistent (also used in restart messages) and allows the debugger to continue running with inline error highlights, it violates the Microsoft DAP protocol specification, which requires success: false when a launch request fails to load code.

Clients must be explicitly built to handle the non-standard highlightError custom event to surface parse errors to users; standard DAP clients relying on the success field will not detect launch failures.

Comment on lines +78 to +80
if not parsed_successfully:
with contextlib.suppress(RuntimeError):
server.simulation_state.reset_simulation()
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 logging the suppressed exception for debuggability.

The contextlib.suppress(RuntimeError) defensively handles the case where reset_simulation() might fail after a failed parse (e.g., if nothing was loaded). While this is reasonable, silently swallowing exceptions can make debugging harder. Consider logging at debug level before suppressing.

💡 Optional: Log before suppressing
+        import logging
+
+        _logger = logging.getLogger(__name__)
+
         if not parsed_successfully:
-            with contextlib.suppress(RuntimeError):
-                server.simulation_state.reset_simulation()
+            try:
+                server.simulation_state.reset_simulation()
+            except RuntimeError:
+                _logger.debug("reset_simulation() failed after parse error (expected)")
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/launch_dap_message.py around lines 78 - 80,
Replace the silent contextlib.suppress around
server.simulation_state.reset_simulation() so that when parsed_successfully is
False any RuntimeError is caught and logged at debug level (including the
exception info) before being ignored; e.g., wrap the call to
server.simulation_state.reset_simulation() in a try/except RuntimeError as e and
call the module or server logger (e.g., logger.debug or server.logger.debug)
with a short message referencing reset_simulation and the caught exception.

program_path = Path(self.program)
server.source_file = {"name": program_path.name, "path": self.program}
parsed_successfully = True
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Ruff FBT003: avoid boolean positional argument in locale.getpreferredencoding().
Use the keyword form to keep intent clear and satisfy Ruff.

Proposed diff
-        code = program_path.read_text(encoding=locale.getpreferredencoding(False))
+        code = program_path.read_text(
+            encoding=locale.getpreferredencoding(do_setlocale=False)
+        )
📝 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
code = program_path.read_text(encoding=locale.getpreferredencoding(False))
code = program_path.read_text(
encoding=locale.getpreferredencoding(do_setlocale=False)
)
🧰 Tools
🪛 Ruff (0.14.10)

70-70: Boolean positional value in function call

(FBT003)

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/restart_dap_message.py at line 70, The call
to locale.getpreferredencoding uses a boolean positional arg; update the
assignment that reads the file with program_path.read_text to call
locale.getpreferredencoding using the keyword form (e.g.,
locale.getpreferredencoding(do_setlocale=False)) so the intent is explicit and
Ruff FBT003 is satisfied; keep the rest of the code unchanged and only replace
the positional boolean in the code = program_path.read_text(encoding=...) line.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/messages/variables_dap_message.py (1)

30-31: Inconsistency: Parameters documented and accepted but ignored.

The start and count parameters are documented as "used for paging" and extracted from the request, but they're overridden with hardcoded values on lines 150-151. If the hardcoded pagination is intentional (which seems unlikely given the PR objectives), these parameters should be removed entirely. However, the correct fix is to revert the changes on lines 150-153.

Also applies to: 42-43

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/messages/variables_dap_message.py:
- Around line 150-153: The loop hardcodes start and count, ignoring the values
passed to the object/method and breaking pagination; restore use of the actual
parameters instead of hardcoded values by removing the lines that set start = 0
and count = 10, compute num_variables = 2**num_q if count == 0 else count using
the passed-in count, and iterate for i in range(start, start + num_variables) so
the start and count provided via __init__ (and documented in the API) are
honored and the dead `count == 0` branch remains meaningful.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4de1403 and 29607af.

📒 Files selected for processing (1)
  • python/mqt/debugger/dap/messages/variables_dap_message.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py:
- Around line 139-162: The _normalize_source signature declares source:
Mapping[str, Any] | None but the implementation immediately rejects None; update
the code to make the type and behavior consistent by either removing the
optional union (change the signature to source: Mapping[str, Any]) if callers
never pass None, or explicitly handle None (e.g., accept None and return an
empty normalized dict or raise with a clear message) to mirror the approach used
by _normalize_position; adjust the signature and body of _normalize_source (and
update any __init__ call sites if you choose to require non-None) so the type
hint matches the actual behavior.
- Around line 111-137: The type hint for _normalize_position is inconsistent: it
declares position: Mapping[str, Any] | None but immediately rejects None; remove
the | None from the signature so _normalize_position(position: Mapping[str,
Any]) conveys that callers must validate None first, then update callers (e.g.,
_normalize_highlight and any other callers of _normalize_position) to explicitly
handle a None value before calling (either by raising a clear
ValueError/TypeError or by returning a default), and keep the current internal
checks for Mapping and missing keys as-is to validate non-None inputs.
- Around line 164-179: The validation in _start_comes_after_end currently allows
start == end because it returns (end_line, end_column) < (start_line,
start_column); change the comparison to use <= so the function returns True when
end is less-than-or-equal-to start, matching the error message "Highlight range
'end' must be after 'start' and preventing zero-length ranges; update the return
expression in _start_comes_after_end accordingly.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29607af and 5a7413f.

📒 Files selected for processing (1)
  • python/mqt/debugger/dap/messages/highlight_error_dap_message.py
🧰 Additional context used
🧬 Code graph analysis (1)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
python/mqt/debugger/dap/messages/dap_event.py (1)
  • DAPEvent (17-41)
python/mqt/debugger/dap/messages/launch_dap_message.py (1)
  • validate (44-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (6)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (6)

1-20: LGTM!

The module header, imports, and type checking setup follow Python best practices. The use of TYPE_CHECKING to conditionally import Sequence avoids unnecessary runtime overhead.


22-34: LGTM!

The class definition properly extends DAPEvent, and the attributes are well-documented with clear type hints.


36-45: LGTM!

The constructor properly normalizes inputs before calling super().__init__(), ensuring validation runs on clean data. This follows the established pattern from other DAP message classes.


47-60: LGTM!

The validation logic is sound, ensuring both the presence of highlights and non-empty messages. Error messages are clear and actionable.


62-70: LGTM!

The encoding method correctly extends the base class implementation and properly structures the event body according to DAP protocol expectations.


72-109: The concern about int() conversion raising ValueError is not applicable to the actual usage pattern.

The instruction field is always created as an integer in both _build_highlight_entry() and _build_parse_error_highlight() in dap_server.py before being passed to _normalize_highlight(). Since highlight entries originate from internal code (not external/untrusted input), the int() conversion at line 101 is safe and will not encounter non-convertible types in practice.

Likely an incorrect or invalid review comment.

Comment on lines +139 to +162
@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
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Similar type hint consideration as _normalize_position.

The function signature includes | None, but the implementation rejects None. Unlike _normalize_position, the caller in __init__ (line 44) passes a non-None source parameter, so this is less likely to be an issue in practice. However, the same type safety considerations apply.

⚙️ Optional: Align type hint with implementation
     @staticmethod
-    def _normalize_source(source: Mapping[str, Any] | None) -> dict[str, Any]:
+    def _normalize_source(source: Mapping[str, Any]) -> dict[str, Any]:
         """Create a defensive copy of the provided DAP Source information.
📝 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
@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 _normalize_source(source: Mapping[str, Any]) -> 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
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py around lines
139 - 162, The _normalize_source signature declares source: Mapping[str, Any] |
None but the implementation immediately rejects None; update the code to make
the type and behavior consistent by either removing the optional union (change
the signature to source: Mapping[str, Any]) if callers never pass None, or
explicitly handle None (e.g., accept None and return an empty normalized dict or
raise with a clear message) to mirror the approach used by _normalize_position;
adjust the signature and body of _normalize_source (and update any __init__ call
sites if you choose to require non-None) so the type hint matches the actual
behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @python/mqt/debugger/dap/dap_server.py:
- Around line 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.

In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py:
- Around line 180-195: The method _start_comes_after_end currently defensively
uses start.get(..., 0) and end.get(..., 0) which can mask bugs; update the
function to include a brief comment (or a one-line assertion) stating that
callers must pass positions already normalized by _normalize_position() and that
the zero defaults are only a last-resort safety; reference the
_start_comes_after_end function and the earlier call site that normalizes
positions to make the assumption explicit.
- Around line 127-153: The _normalize_position static method currently only
catches KeyError when extracting position["line"] and position["column"], but
int(...) can raise ValueError for non-numeric inputs; update _normalize_position
to also catch ValueError from the int() conversions and re-raise a clear
ValueError (e.g., "Highlight positions require integer 'line' and 'column'.")
using the original exception as the cause so invalid but present values like
"abc" produce a descriptive error.
- Around line 84-125: The normalization currently calls
int(normalized.get("instruction", -1)) which can raise an opaque ValueError;
update HighlightError._normalize_highlight to defensively validate and convert
the "instruction" field: retrieve raw = normalized.get("instruction", -1), allow
ints directly, allow numeric strings (strip and check .isdigit() or use a safe
try/except), and if conversion fails raise a clear TypeError/ValueError with a
descriptive message like "highlight instruction must be an integer or numeric
string" referencing the offending value; implement this conversion before
assigning normalized["instruction"] so other fields remain unchanged.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a7413f and a8c25e2.

📒 Files selected for processing (3)
  • python/mqt/debugger/dap/dap_server.py
  • python/mqt/debugger/dap/messages/__init__.py
  • python/mqt/debugger/dap/messages/highlight_error_dap_message.py
🧰 Additional context used
🧬 Code graph analysis (2)
python/mqt/debugger/dap/messages/__init__.py (1)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
  • HighlightError (34-195)
  • HighlightReason (20-27)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (1)
python/mqt/debugger/dap/messages/dap_event.py (1)
  • DAPEvent (17-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (11)
python/mqt/debugger/dap/messages/__init__.py (1)

24-24: LGTM!

The new exports follow the existing pattern and maintain alphabetical order in __all__. The integration is clean and consistent with the module's structure.

Also applies to: 61-62

python/mqt/debugger/dap/dap_server.py (10)

172-186: LGTM! This implements the fix for persisting greyed-out highlights.

The _prevent_exit flag is properly reset after stepping/execution commands, ensuring that each execution starts with a clean state. This addresses the bug described in the PR where greyed-out highlights persisted between executions.


257-261: LGTM! Clean implementation of deferred highlight emission.

The queueing pattern allows parse errors (or other errors) to be collected during command processing and then emitted after the response. Setting _prevent_exit = True ensures the user sees the highlights before the debugger exits.


271-275: LGTM! Exit prevention logic is correct.

The additional check for not self._prevent_exit ensures the debugger remains active when highlights are displayed, allowing users to view error information before the session ends.


363-389: LGTM! Edge case handling improvements for position calculations.

The changes improve robustness:

  • Line 373: Handles empty source code gracefully
  • Line 376: Using <= instead of < correctly handles positions at line boundaries
  • Lines 381-384: The else clause on the for loop handles positions beyond the end of the file, setting them to the last valid position

These are good defensive programming practices for coordinate conversion.


431-462: LGTM! Robust highlight collection with good fallback logic.

The method includes solid defensive programming:

  • Line 438: Guards against missing source_code attribute
  • Lines 443-444: Handles potential RuntimeError from diagnostics
  • Lines 453-460: Ensures at least one highlight entry is returned as a fallback

This guarantees users always see error location information, even when detailed diagnostics are unavailable.


476-481: Verify snippet extraction consistency with range boundaries.

Lines 476-479 carefully exclude newlines from the display range, setting end_position_exclusive = end_pos when the character at end_pos is \n. However, line 481 extracts the snippet using end_pos + 1 (always inclusive of end_pos), which means newlines may be included in the snippet even when excluded from the highlight range.

This is likely acceptable for display purposes since line 481 also uses .replace("\r", ""), but verify that including newlines in the snippet doesn't cause rendering issues in the client.


504-523: LGTM! Parse error handling with good fallback strategy.

The regex pattern on line 514 correctly parses compiler error messages in the format <input>:line:column: detail. The fallback to line 1, column 1 when the pattern doesn't match ensures the function always returns valid coordinates.


537-546: LGTM! Smart UX enhancement for blank line handling.

When a parse error points to a blank line at column 1, the code looks backward to find the previous non-blank line and adjusts the highlight accordingly. This provides better visual feedback to users by pointing to the actual code rather than empty space.


561-584: LGTM! Recursive flattening with comprehensive type handling.

The method correctly handles nested message structures with strings, dictionaries, and lists. The fallback to str() conversion for unknown types (line 583) ensures robustness, though consider logging a warning if unexpected types are encountered in debug builds.


586-653: LGTM! Clean refactoring with improved modularity.

The introduction of send_message_simple() as a lower-level helper and the addition of the category parameter to send_message_hierarchy() improve code reusability and enable proper message categorization (console/stdout/stderr). The type handling in lines 603-612 is careful and defensive.

Comment on lines +548 to +554
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},
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.

Comment on lines +84 to +125
@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._start_comes_after_end(start, end):
msg = "Highlight range 'end' must be after 'start'."
raise ValueError(msg)

normalized = dict(entry)
normalized["instruction"] = int(normalized.get("instruction", -1))
reason = normalized.get("reason", HighlightReason.UNKNOWN)
if isinstance(reason, HighlightReason):
normalized["reason"] = reason.value
else:
normalized["reason"] = str(reason)
normalized["code"] = str(normalized.get("code", ""))
normalized["message"] = str(normalized.get("message", "")).strip()
normalized["range"] = {
"start": start,
"end": end,
}
return normalized
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 more defensive type conversion in normalization.

Line 113 performs int(normalized.get("instruction", -1)) which will raise a ValueError if the value cannot be converted to an integer. While this may be intentional for validation purposes, it produces a less descriptive error than the explicit type checks used elsewhere in the class.

♻️ Proposed enhancement for error clarity
 normalized = dict(entry)
-normalized["instruction"] = int(normalized.get("instruction", -1))
+try:
+    normalized["instruction"] = int(normalized.get("instruction", -1))
+except (ValueError, TypeError) as exc:
+    msg = "Highlight 'instruction' field must be convertible to an integer."
+    raise ValueError(msg) from exc
📝 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
@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._start_comes_after_end(start, end):
msg = "Highlight range 'end' must be after 'start'."
raise ValueError(msg)
normalized = dict(entry)
normalized["instruction"] = int(normalized.get("instruction", -1))
reason = normalized.get("reason", HighlightReason.UNKNOWN)
if isinstance(reason, HighlightReason):
normalized["reason"] = reason.value
else:
normalized["reason"] = str(reason)
normalized["code"] = str(normalized.get("code", ""))
normalized["message"] = str(normalized.get("message", "")).strip()
normalized["range"] = {
"start": start,
"end": end,
}
return normalized
@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._start_comes_after_end(start, end):
msg = "Highlight range 'end' must be after 'start'."
raise ValueError(msg)
normalized = dict(entry)
try:
normalized["instruction"] = int(normalized.get("instruction", -1))
except (ValueError, TypeError) as exc:
msg = "Highlight 'instruction' field must be convertible to an integer."
raise ValueError(msg) from exc
reason = normalized.get("reason", HighlightReason.UNKNOWN)
if isinstance(reason, HighlightReason):
normalized["reason"] = reason.value
else:
normalized["reason"] = str(reason)
normalized["code"] = str(normalized.get("code", ""))
normalized["message"] = str(normalized.get("message", "")).strip()
normalized["range"] = {
"start": start,
"end": end,
}
return normalized
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py around lines
84 - 125, The normalization currently calls int(normalized.get("instruction",
-1)) which can raise an opaque ValueError; update
HighlightError._normalize_highlight to defensively validate and convert the
"instruction" field: retrieve raw = normalized.get("instruction", -1), allow
ints directly, allow numeric strings (strip and check .isdigit() or use a safe
try/except), and if conversion fails raise a clear TypeError/ValueError with a
descriptive message like "highlight instruction must be an integer or numeric
string" referencing the offending value; implement this conversion before
assigning normalized["instruction"] so other fields remain unchanged.

Comment on lines +127 to +153
@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,
}
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 catching ValueError from int() conversion.

Lines 145-146 use int() to convert position fields, which can raise either KeyError (caught and handled) or ValueError (not caught). If line or column values are present but not convertible to integers (e.g., "abc"), the resulting ValueError will be less descriptive than the explicit TypeError and ValueError messages provided elsewhere.

♻️ Proposed enhancement for comprehensive error handling
     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
+    except (ValueError, TypeError) as exc:
+        msg = "Highlight position 'line' and 'column' must be integers."
+        raise ValueError(msg) from exc
🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py around lines
127 - 153, The _normalize_position static method currently only catches KeyError
when extracting position["line"] and position["column"], but int(...) can raise
ValueError for non-numeric inputs; update _normalize_position to also catch
ValueError from the int() conversions and re-raise a clear ValueError (e.g.,
"Highlight positions require integer 'line' and 'column'.") using the original
exception as the cause so invalid but present values like "abc" produce a
descriptive error.

Comment on lines +180 to +195
@staticmethod
def _start_comes_after_end(start: Mapping[str, Any], end: Mapping[str, Any]) -> bool:
"""Return True if 'start' describes a position after 'end'.
Args:
start (Mapping[str, Any]): The start position mapping.
end (Mapping[str, Any]): The end position mapping.
Returns:
bool: True when the start position is after the end 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)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Defensive use of .get() is safe but could be documented.

Lines 191-194 use .get() with a default value of 0 even though the positions should already be normalized by _normalize_position(). This defensive approach is safe, but the default value of 0 could theoretically mask bugs if somehow un-normalized positions reach this method.

This is not an issue in practice since _start_comes_after_end() is only called after normalization (line 108), but a brief comment explaining this assumption would improve maintainability.

🤖 Prompt for AI Agents
In @python/mqt/debugger/dap/messages/highlight_error_dap_message.py around lines
180 - 195, The method _start_comes_after_end currently defensively uses
start.get(..., 0) and end.get(..., 0) which can mask bugs; update the function
to include a brief comment (or a one-line assertion) stating that callers must
pass positions already normalized by _normalize_position() and that the zero
defaults are only a last-resort safety; reference the _start_comes_after_end
function and the earlier call site that normalizes positions to make the
assumption explicit.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
python/mqt/debugger/dap/messages/restart_dap_message.py (2)

83-88: Fix: Response command should be "restart", not "launch".

The response payload incorrectly specifies "command": "launch" for a restart request. This should match the actual request type.

Proposed fix
         return {
             "type": "response",
             "request_seq": self.sequence_number,
             "success": True,
-            "command": "launch",
+            "command": "restart",
         }

46-57: Fix docstring: references wrong class name.

The docstring says "Validates the 'LaunchDAPMessage' instance" but this is the RestartDAPMessage class.

Proposed fix
     def validate(self) -> None:
-        """Validates the 'LaunchDAPMessage' instance.
+        """Validates the 'RestartDAPMessage' instance.
 
         Raises:
             ValueError: If the 'program' field is missing or the file does not exist.
         """
python/mqt/debugger/__init__.py (1)

47-48: Duplicate entry in __all__ list.

"create_ddsim_simulation_state" appears twice on lines 47 and 48.

🔧 Proposed fix
     "__version__",
     "check",
     "create_ddsim_simulation_state",
-    "create_ddsim_simulation_state",
     "dap",
     "destroy_ddsim_simulation_state",
 ]
🤖 Fix all issues with AI agents
In @bindings/InterfaceBindings.cpp:
- Around line 230-241: The binding for "load_code_with_result" calls
self->loadCodeWithResult without a null check; add a defensive check like the
one used in the "load_code" binding so you verify that the function pointer
(self->loadCodeWithResult) is non-null before calling it, and handle the null
case consistently (e.g., return an appropriate error/empty LoadResult or raise
an exception) — ensure the check aligns with how createDDSimulationState sets
loadCodeWithResult and mirrors the existing getLastErrorMessage/null handling
pattern used elsewhere.

In @include/backend/debug.h:
- Around line 41-65: LoadResult's string fields `detail` and `message` currently
lack ownership/lifetime documentation; update the struct comment to state that
these pointers are owned by the implementation (same semantics as
`getLastErrorMessage`), that they point into internal storage and may be
invalidated by subsequent calls into the library (or by destroying the relevant
context), and that callers must copy the strings if they need them to outlive
future interface calls.

In @src/backend/dd/DDSimDebug.cpp:
- Around line 87-92: The function clearLastError has an accidental recursive
call to itself causing infinite recursion; remove the self-call and instead
perform the intended clearing operations directly: ensure
clearLastError(DDSimulationState* ddsim) does not call clearLastError(ddsim) and
only clears ddsim->lastErrorDetail, sets ddsim->lastErrorLine = 0 and
ddsim->lastErrorColumn = 0 (and reset any other related fields if present) so
the function simply resets the DDSimulationState error state without recursion.

In @src/common/parsing/CodePreprocessing.cpp:
- Around line 78-109: The code duplicates the logic to compute the start of a
line in lineColumnForTarget that already exists in lineColumnForOffset; add a
small helper (e.g., findLineStart(code, offset)) that returns the 0-based index
of the first character of the line containing offset and replace the duplicated
rfind/range logic in both lineColumnForTarget and lineColumnForOffset to call
this helper, keeping behavior identical (return 0 when rfind returns npos,
otherwise rfind+1).
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8c25e2 and 15bb5d9.

📒 Files selected for processing (12)
  • bindings/InterfaceBindings.cpp
  • include/backend/dd/DDSimDebug.hpp
  • include/backend/debug.h
  • include/common/parsing/ParsingError.hpp
  • python/mqt/debugger/__init__.py
  • python/mqt/debugger/dap/dap_server.py
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
  • python/mqt/debugger/pydebugger.pyi
  • src/backend/dd/DDSimDebug.cpp
  • src/common/parsing/CodePreprocessing.cpp
  • src/common/parsing/ParsingError.cpp
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/common/parsing/CodePreprocessing.cpp
  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
🧬 Code graph analysis (4)
include/backend/debug.h (2)
include/frontend/cli/CliFrontEnd.hpp (2)
  • code (46-46)
  • code (68-68)
test/utils/common_fixtures.hpp (6)
  • code (211-215)
  • code (211-213)
  • code (236-244)
  • code (236-237)
  • code (254-256)
  • code (254-254)
python/mqt/debugger/__init__.py (2)
python/mqt/debugger/pydebugger.pyi (2)
  • LoadResult (106-118)
  • Result (25-31)
python/mqt/debugger/check/result_checker.py (1)
  • Result (37-74)
python/mqt/debugger/dap/messages/restart_dap_message.py (2)
python/mqt/debugger/pydebugger.pyi (2)
  • load_code_with_result (159-167)
  • Result (25-31)
python/mqt/debugger/dap/dap_server.py (1)
  • queue_parse_error_result (503-510)
python/mqt/debugger/dap/dap_server.py (11)
python/mqt/debugger/dap/messages/next_dap_message.py (1)
  • NextDAPMessage (21-47)
python/mqt/debugger/dap/messages/step_back_dap_message.py (1)
  • StepBackDAPMessage (21-48)
python/mqt/debugger/dap/messages/step_in_dap_message.py (1)
  • StepInDAPMessage (21-47)
python/mqt/debugger/dap/messages/step_out_dap_message.py (1)
  • StepOutDAPMessage (21-47)
python/mqt/debugger/dap/messages/continue_dap_message.py (1)
  • ContinueDAPMessage (21-49)
python/mqt/debugger/dap/messages/reverse_continue_dap_message.py (1)
  • ReverseContinueDAPMessage (21-49)
python/mqt/debugger/dap/messages/restart_frame_dap_message.py (1)
  • RestartFrameDAPMessage (21-53)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)
  • RestartDAPMessage (26-88)
python/mqt/debugger/dap/messages/launch_dap_message.py (1)
  • LaunchDAPMessage (26-87)
python/mqt/debugger/dap/messages/highlight_error_dap_message.py (2)
  • HighlightError (34-195)
  • encode (74-82)
python/mqt/debugger/dap/messages/output_dap_event.py (2)
  • encode (54-69)
  • OutputDAPEvent (18-69)
🪛 Cppcheck (2.19.0)
src/common/parsing/CodePreprocessing.cpp

[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)

src/common/parsing/ParsingError.cpp

[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[style] 34-34: The function 'location' is never used.

(unusedFunction)

src/backend/dd/DDSimDebug.cpp

[information] 28-28: Include file

(missingIncludeSystem)


[information] 28-28: Include file

(missingIncludeSystem)

🪛 Ruff (0.14.10)
python/mqt/debugger/dap/messages/launch_dap_message.py

71-71: Boolean positional value in function call

(FBT003)

python/mqt/debugger/dap/messages/restart_dap_message.py

72-72: Boolean positional value in function call

(FBT003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Release) / 🍎 macos-14 clang Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04-arm, gcc, Release) / 🐧 ubuntu-24.04-arm gcc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-14, clang, Debug) / 🍎 macos-14 clang Debug
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Release) / 🏁 windows-2022 msvc Release
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Debug) / 🐧 ubuntu-24.04 gcc Debug
  • GitHub Check: 🇨‌ Test 🏁 (windows-11-arm, msvc, Release) / 🏁 windows-11-arm msvc Release
  • GitHub Check: 🇨‌ Test 🍎 (macos-15-intel, clang, Release) / 🍎 macos-15-intel clang Release
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🇨‌ Test 🐧 (ubuntu-24.04, gcc, Release) / 🐧 ubuntu-24.04 gcc Release
  • GitHub Check: 🇨‌ Test 🏁 (windows-2022, msvc, Debug) / 🏁 windows-2022 msvc Debug
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (30)
src/common/parsing/CodePreprocessing.cpp (9)

23-25: LGTM!

The new includes are appropriate: <cctype> for std::isdigit and <exception> for the catch blocks using std::exception. The static analysis hints about missing system includes are false positives.


38-49: LGTM!

The isDigits helper is well-implemented. The unsigned char cast is the correct approach for std::isdigit to avoid undefined behavior with negative char values.


56-76: LGTM!

The line/column calculation is correct. The O(n) newline counting is acceptable since this is only invoked during error reporting.


111-151: LGTM!

The error-building helper functions are well-structured and provide clear, descriptive error messages with location information.


153-203: LGTM!

The validateTargets function is robust with comprehensive bracket format validation (line 176) and proper bounds checking. The defensive try-catch around stoul (lines 187-192) after the isDigits check is good practice for handling potential overflow of very large numbers.


504-508: LGTM!

The improved trueStart calculation correctly skips leading whitespace to provide more accurate error locations, while properly handling edge cases with the bounds check.


534-546: LGTM!

The strengthened register declaration validation provides better error handling with descriptive messages. The defensive try-catch around stoul after isDigits validation correctly handles potential overflow edge cases.


610-611: LGTM!

Good integration of the centralized target validation for assertions with context-specific error messages.


616-619: LGTM!

Correctly validates targets for non-declaration instructions while appropriately skipping variable declarations which are handled separately.

python/mqt/debugger/dap/messages/launch_dap_message.py (1)

68-87: LGTM! Proper error handling flow for parse failures.

The new implementation correctly:

  1. Sets source file metadata before loading
  2. Uses load_code_with_result for structured error reporting
  3. Queues parse errors for later highlighting
  4. Conditionally runs simulation only on success
  5. Resets simulation on failure (suppressing potential RuntimeError)

The response always returns success: True which is appropriate per DAP protocol—parse errors are communicated via separate events rather than failing the launch response.

include/backend/dd/DDSimDebug.hpp (2)

122-137: LGTM! Well-documented error-tracking fields.

The new error-tracking members follow the existing conventions:

  • Clear documentation with 1-based line/column semantics and 0 indicating unavailable
  • Default initialization for line/column
  • Proper placement alongside other state fields

299-306: LGTM! New API follows existing patterns.

The ddsimLoadCodeWithResult function declaration is consistent with the existing ddsimLoadCode API and provides structured error reporting via LoadResult, which is properly defined in include/backend/debug.h with fields for error result, location (line/column), and detailed error information.

include/common/parsing/ParsingError.hpp (1)

28-57: LGTM! Clean optional location design.

The ParsingErrorLocation struct and its integration into ParsingError is well-designed:

  • Using std::optional appropriately for potentially absent location data
  • Exposing via const pointer with noexcept accessor
  • Taking std::string by value in the constructor enables move semantics
python/mqt/debugger/dap/dap_server.py (7)

171-185: LGTM! Proper exit prevention flow.

The _prevent_exit flag correctly ensures that the debugger doesn't immediately exit after emitting parse error highlights, giving the client time to display the error state.


362-388: LGTM! Improved edge case handling in coordinate conversion.

The updates properly handle:

  • Empty source code (returns line 1)
  • Positions beyond source code length (returns last line/column)
  • Proper 0/1-based indexing adjustments

The else clause after the for loop (lines 380-383) correctly handles positions that exceed the total source code length by returning the last valid position.


430-501: LGTM! Robust highlight entry collection.

The implementation:

  • Defensively checks for source_code existence
  • Properly handles end position for newline characters
  • Falls back to highlighting the failing instruction if no error causes found
  • Maps error cause types to appropriate highlight reasons

503-546: LGTM! Smart parse error location heuristic.

The _build_parse_error_highlight method includes a useful heuristic (lines 524-533) that adjusts the highlight location when the parser reports an error on an empty line—it looks back to find the previous non-empty line, which typically contains the actual error. This improves user experience for common parser error reporting patterns.

Using instruction: -1 clearly indicates this is a parse-time error, not associated with any instruction.


548-571: LGTM! Clean recursive message flattening.

The method properly handles the nested message structure with support for:

  • Plain strings
  • Dictionaries with title/body/end keys
  • Nested lists
  • Fallback to string conversion for other types

573-640: LGTM! Clean separation of message handling concerns.

The refactoring:

  • Adds category parameter for output type control (console/stdout/stderr)
  • Extracts message assembly into send_message_simple
  • Uses _flatten_message_parts for recursive structure handling
  • Properly handles empty message case with early return

354-360: LGTM! Assertion failure highlighting integration.

The highlight entries are properly collected and emitted after the existing gray-out and output messages, providing structured error location data to the client.

python/mqt/debugger/pydebugger.pyi (1)

25-31: LGTM!

The new Result enum, LoadResult class, and load_code_with_result method are well-defined with appropriate documentation. The type stubs correctly mirror the C++ interface definitions.

Also applies to: 106-118, 159-167

include/backend/debug.h (1)

83-100: LGTM!

The new loadCodeWithResult and getLastErrorMessage function pointers are well-documented with clear ownership semantics for the returned string pointer.

src/common/parsing/ParsingError.cpp (1)

26-39: LGTM!

The new constructor correctly builds a formatted error message with location information, and the location() accessor properly handles the optional value. The static analysis warning about location() being unused is a false positive—it's called from DDSimDebug.cpp at line 636.

bindings/InterfaceBindings.cpp (2)

77-83: LGTM!

The Result enum binding correctly exposes OK and ERROR values to Python.


177-204: LGTM!

The LoadResult binding correctly handles nullable C string pointers by returning py::none() when they are null, preventing null pointer issues in Python.

src/backend/dd/DDSimDebug.cpp (4)

79-85: LGTM!

The ddsimGetLastErrorMessage implementation correctly returns nullptr for empty messages and a C-string pointer otherwise.


605-668: LGTM!

The ddsimLoadCodeInternal function properly handles different exception types, extracting location information from ParsingError and providing fallback messages for other exceptions. The error state is correctly populated for the new structured error reporting.


670-685: LGTM!

The ddsimLoadCode and ddsimLoadCodeWithResult wrappers correctly delegate to the internal implementation and construct the appropriate return types.


1461-1465: The breakpoint insertion behavior is correct and intentional.

When desiredPosition falls before an instruction's start position, the breakpoint is placed at that instruction (round-up behavior). This is verified by the existing test cases that validate both valid positions from breakpointPositions and out-of-bounds cases (expecting ERROR when desiredPosition exceeds all instructions). All callers (CLI, Python bindings, DAP handler) properly handle the returned result and actual position.

python/mqt/debugger/__init__.py (1)

38-39: No action needed. The Result enum from pydebugger and the Result class from check.result_checker are in different module namespaces (mqt.debugger.Result vs mqt.debugger.check.Result) and do not create a naming conflict. Users will access them through different import paths.

Likely an incorrect or invalid review comment.

Comment on lines 230 to 241
.def(
"load_code_with_result",
[](SimulationState* self, const char* code) {
return self->loadCodeWithResult(self, code);
},
R"(Loads the given code into the simulation state and returns details.
Args:
code (str): The code to load.
Returns:
LoadResult: The structured load result.)")
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 adding a null check for loadCodeWithResult.

Unlike load_code which defensively checks getLastErrorMessage for null (line 216-217), load_code_with_result calls self->loadCodeWithResult without verification. While this function pointer is set in createDDSimulationState, adding a defensive check would maintain consistency.

💡 Suggested defensive check
       .def(
           "load_code_with_result",
           [](SimulationState* self, const char* code) {
+            if (self->loadCodeWithResult == nullptr) {
+              throw std::runtime_error("loadCodeWithResult not implemented");
+            }
             return self->loadCodeWithResult(self, code);
           },
📝 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
.def(
"load_code_with_result",
[](SimulationState* self, const char* code) {
return self->loadCodeWithResult(self, code);
},
R"(Loads the given code into the simulation state and returns details.
Args:
code (str): The code to load.
Returns:
LoadResult: The structured load result.)")
.def(
"load_code_with_result",
[](SimulationState* self, const char* code) {
if (self->loadCodeWithResult == nullptr) {
throw std::runtime_error("loadCodeWithResult not implemented");
}
return self->loadCodeWithResult(self, code);
},
R"(Loads the given code into the simulation state and returns details.
Args:
code (str): The code to load.
Returns:
LoadResult: The structured load result.)")
🤖 Prompt for AI Agents
In @bindings/InterfaceBindings.cpp around lines 230 - 241, The binding for
"load_code_with_result" calls self->loadCodeWithResult without a null check; add
a defensive check like the one used in the "load_code" binding so you verify
that the function pointer (self->loadCodeWithResult) is non-null before calling
it, and handle the null case consistently (e.g., return an appropriate
error/empty LoadResult or raise an exception) — ensure the check aligns with how
createDDSimulationState sets loadCodeWithResult and mirrors the existing
getLastErrorMessage/null handling pattern used elsewhere.

Comment on lines 41 to 65
/**
* @brief Represents the structured result of a load operation.
*/
typedef struct {
/**
* @brief The result of the load operation.
*/
Result result;
/**
* @brief The 1-based line of the error location, or 0 if unavailable.
*/
size_t line;
/**
* @brief The 1-based column of the error location, or 0 if unavailable.
*/
size_t column;
/**
* @brief The error detail text, or nullptr if unavailable.
*/
const char* detail;
/**
* @brief The full error message, or nullptr if unavailable.
*/
const char* message;
} LoadResult;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Document pointer lifetime semantics for LoadResult string fields.

The detail and message pointers in LoadResult are owned by the implementation (similar to getLastErrorMessage), but this isn't documented. Since LoadResult is returned by value, callers need to know these pointers may become invalid after subsequent interface calls.

📝 Suggested documentation enhancement
   /**
    * @brief The error detail text, or nullptr if unavailable.
+   *
+   * The returned pointer is owned by the implementation and remains valid
+   * until the next interface call that modifies the error state.
    */
   const char* detail;
   /**
    * @brief The full error message, or nullptr if unavailable.
+   *
+   * The returned pointer is owned by the implementation and remains valid
+   * until the next interface call that modifies the error state.
    */
   const char* message;
 } LoadResult;
📝 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
/**
* @brief Represents the structured result of a load operation.
*/
typedef struct {
/**
* @brief The result of the load operation.
*/
Result result;
/**
* @brief The 1-based line of the error location, or 0 if unavailable.
*/
size_t line;
/**
* @brief The 1-based column of the error location, or 0 if unavailable.
*/
size_t column;
/**
* @brief The error detail text, or nullptr if unavailable.
*/
const char* detail;
/**
* @brief The full error message, or nullptr if unavailable.
*/
const char* message;
} LoadResult;
/**
* @brief Represents the structured result of a load operation.
*/
typedef struct {
/**
* @brief The result of the load operation.
*/
Result result;
/**
* @brief The 1-based line of the error location, or 0 if unavailable.
*/
size_t line;
/**
* @brief The 1-based column of the error location, or 0 if unavailable.
*/
size_t column;
/**
* @brief The error detail text, or nullptr if unavailable.
*
* The returned pointer is owned by the implementation and remains valid
* until the next interface call that modifies the error state.
*/
const char* detail;
/**
* @brief The full error message, or nullptr if unavailable.
*
* The returned pointer is owned by the implementation and remains valid
* until the next interface call that modifies the error state.
*/
const char* message;
} LoadResult;
🤖 Prompt for AI Agents
In @include/backend/debug.h around lines 41 - 65, LoadResult's string fields
`detail` and `message` currently lack ownership/lifetime documentation; update
the struct comment to state that these pointers are owned by the implementation
(same semantics as `getLastErrorMessage`), that they point into internal storage
and may be invalidated by subsequent calls into the library (or by destroying
the relevant context), and that callers must copy the strings if they need them
to outlive future interface calls.

Comment on lines 87 to 92
void clearLastError(DDSimulationState* ddsim) {
clearLastError(ddsim);
ddsim->lastErrorDetail.clear();
ddsim->lastErrorLine = 0;
ddsim->lastErrorColumn = 0;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Infinite recursion in clearLastError.

The function calls itself recursively on line 88, causing a stack overflow when invoked. This will crash the application when loading code (called at line 627).

🐛 Proposed fix
 void clearLastError(DDSimulationState* ddsim) {
-  clearLastError(ddsim);
+  ddsim->lastErrorMessage.clear();
   ddsim->lastErrorDetail.clear();
   ddsim->lastErrorLine = 0;
   ddsim->lastErrorColumn = 0;
 }
📝 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
void clearLastError(DDSimulationState* ddsim) {
clearLastError(ddsim);
ddsim->lastErrorDetail.clear();
ddsim->lastErrorLine = 0;
ddsim->lastErrorColumn = 0;
}
void clearLastError(DDSimulationState* ddsim) {
ddsim->lastErrorMessage.clear();
ddsim->lastErrorDetail.clear();
ddsim->lastErrorLine = 0;
ddsim->lastErrorColumn = 0;
}
🤖 Prompt for AI Agents
In @src/backend/dd/DDSimDebug.cpp around lines 87 - 92, The function
clearLastError has an accidental recursive call to itself causing infinite
recursion; remove the self-call and instead perform the intended clearing
operations directly: ensure clearLastError(DDSimulationState* ddsim) does not
call clearLastError(ddsim) and only clears ddsim->lastErrorDetail, sets
ddsim->lastErrorLine = 0 and ddsim->lastErrorColumn = 0 (and reset any other
related fields if present) so the function simply resets the DDSimulationState
error state without recursion.

Comment on lines +78 to +109
/**
* @brief Compute the 1-based line and column for a target within a line.
* @param code The source code to inspect.
* @param instructionStart The zero-based offset of the instruction start.
* @param target The target token to locate on the line.
* @return The line and column of the target, or the first non-space column.
*/
LineColumn lineColumnForTarget(const std::string& code, size_t instructionStart,
const std::string& target) {
LineColumn location = lineColumnForOffset(code, instructionStart);
const auto lineStartPos = code.rfind('\n', instructionStart);
const size_t lineStart = (lineStartPos == std::string::npos)
? 0
: static_cast<size_t>(lineStartPos + 1);
auto lineEndPos = code.find('\n', instructionStart);
const size_t lineEnd = (lineEndPos == std::string::npos)
? code.size()
: static_cast<size_t>(lineEndPos);
const auto lineText = code.substr(lineStart, lineEnd - lineStart);
if (!target.empty()) {
const auto targetPos = lineText.find(target);
if (targetPos != std::string::npos) {
location.column = targetPos + 1;
return location;
}
}
const auto nonSpace = lineText.find_first_not_of(" \t");
if (nonSpace != std::string::npos) {
location.column = nonSpace + 1;
}
return location;
}
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 extracting lineStart calculation to reduce duplication.

The lineStart calculation logic (lines 88-91) is duplicated from lineColumnForOffset (lines 64-67). Consider extracting a small helper function to compute lineStart from an offset.

♻️ Suggested helper extraction
/**
 * @brief Find the start position of the line containing the given offset.
 */
size_t findLineStart(const std::string& code, size_t offset) {
  const auto lineStartPos = code.rfind('\n', offset);
  return (lineStartPos == std::string::npos) ? 0 : lineStartPos + 1;
}

Then use in both functions:

 LineColumn lineColumnForOffset(const std::string& code, size_t offset) {
   LineColumn location;
-  const auto lineStartPos = code.rfind('\n', offset);
-  const size_t lineStart = (lineStartPos == std::string::npos)
-                               ? 0
-                               : static_cast<size_t>(lineStartPos + 1);
+  const size_t lineStart = findLineStart(code, offset);
🤖 Prompt for AI Agents
In @src/common/parsing/CodePreprocessing.cpp around lines 78 - 109, The code
duplicates the logic to compute the start of a line in lineColumnForTarget that
already exists in lineColumnForOffset; add a small helper (e.g.,
findLineStart(code, offset)) that returns the 0-based index of the first
character of the line containing offset and replace the duplicated rfind/range
logic in both lineColumnForTarget and lineColumnForOffset to call this helper,
keeping behavior identical (return 0 when rfind returns npos, otherwise
rfind+1).

This reverts commit 15bb5d9.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/mqt/debugger/dap/messages/restart_dap_message.py (1)

82-87: Change response "command" from "launch" to "restart" to comply with DAP protocol.

According to the DAP specification, a Response message's "command" field must be the name of the request being replied to. Since this handler processes a "restart" request, the response must echo that with "command": "restart", not "command": "launch". This is a protocol violation that will cause client-side issues.

Fix
         return {
             "type": "response",
             "request_seq": self.sequence_number,
             "success": True,
-            "command": "launch",
+            "command": "restart",
         }
🤖 Fix all issues with AI agents
In @src/backend/dd/DDSimDebug.cpp:
- Around line 1152-1153: The local pointer ddsim created by calling
toDDSimulationState(self) inside ddsimRunAll is unused; remove the unused
variable declaration (the call to toDDSimulationState and the auto* ddsim
assignment) from ddsimRunAll to avoid the unused-variable warning, or if the
underlying DDSimulationState is actually needed later, replace usages of self
with ddsim where appropriate; ensure you keep the function signature and
behavior intact.

In @src/common/parsing/CodePreprocessing.cpp:
- Around line 97-103: The current substring search using lineText.find(target)
can match inside other tokens (e.g., "q" in "qreg"); update the search to find a
whole-token match instead: scan for occurrences of target in lineText and verify
character boundaries (or use a regex with word boundaries like "\\btarget\\b")
so the found position corresponds to a standalone token; change the logic around
target, lineText, targetPos and location.column to use this token-aware check
before assigning location.column and returning.
- Around line 62-76: The function LineColumn lineColumnForOffset has an
off-by-one edge when offset points at a '\n' because rfind('\n', offset) can
return offset itself, producing column 0; fix by ensuring rfind searches before
the offset (e.g., use rfind('\n', offset - 1) when offset > 0 or otherwise
adjust the search) and recompute lineStart/column accordingly so column remains
1-based; update the logic inside lineColumnForOffset to handle offset==0 and
offset pointing at a newline (or alternatively clamp offset to the previous
byte) and keep all callers' semantics unchanged.
- Around line 184-196: The code redundantly checks isDigits(indexText) then
wraps std::stoul in a broad try/catch; consolidate by keeping the isDigits check
and replacing the try/catch with a deterministic range check: parse with
std::stoull (or use a checked conversion), compare the resulting unsigned long
long against std::numeric_limits<size_t>::max(), and if it exceeds that limit
throw the same ParsingError (using formatParseError/invalidTargetDetail/target);
otherwise assign the value to registerIndex (static_cast<size_t>) — this removes
the broad exception handling while preserving correctness for out-of-range
values.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15bb5d9 and 175bb08.

📒 Files selected for processing (8)
  • bindings/InterfaceBindings.cpp
  • include/backend/dd/DDSimDebug.hpp
  • include/backend/debug.h
  • python/mqt/debugger/dap/dap_server.py
  • python/mqt/debugger/dap/messages/launch_dap_message.py
  • python/mqt/debugger/dap/messages/restart_dap_message.py
  • src/backend/dd/DDSimDebug.cpp
  • src/common/parsing/CodePreprocessing.cpp
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T07:42:45.507Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/error_handling_test.cpp:118-194
Timestamp: 2025-11-05T07:42:45.507Z
Learning: In the munich-quantum-toolkit/core QDMI device API, session parameters can only be set before calling `device_session_init()`. Once a session is initialized, any attempt to set a parameter returns `QDMI_ERROR_BADSTATE`. Since `SessionGuard` (in test/qdmi/dd/helpers/test_utils.hpp) automatically initializes the session in its constructor, tests that need to verify session parameter setting behavior before initialization must allocate a separate uninitialized session rather than reusing the `SessionGuard`'s session.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • src/backend/dd/DDSimDebug.cpp
  • src/common/parsing/CodePreprocessing.cpp
🔇 Additional comments (31)
src/common/parsing/CodePreprocessing.cpp (5)

43-49: LGTM!

The isDigits implementation correctly handles the empty string case and properly casts to unsigned char before calling std::isdigit, avoiding undefined behavior with potentially negative char values.


119-125: LGTM!

The error formatting follows a standard compiler-style format (<input>:line:col: message) which is helpful for IDE integration and user readability.


509-513: LGTM!

The trueStart calculation correctly identifies the first non-whitespace character, improving error location accuracy by pointing to actual code rather than leading whitespace.


539-551: LGTM!

The register parsing now properly validates that the size is a valid numeric string before parsing, providing clear error messages with location information. This is consistent with the validation approach used in validateTargets.


615-624: LGTM!

The centralized validateTargets calls provide consistent validation for both assertion targets and regular instruction targets, with appropriate context strings for clearer error messages. The guard at line 621 correctly excludes variable declarations from target validation.

python/mqt/debugger/dap/messages/restart_dap_message.py (3)

13-13: LGTM!

The contextlib import is appropriately added to support the contextlib.suppress() usage in the error handling cleanup path.


68-69: LGTM!

Good additions: setting server.source_file ensures the server tracks the current source, and the parsed_successfully flag provides clean state tracking for the parse outcome.


72-81: LGTM!

Solid error handling pattern:

  • Catching RuntimeError from load_code and queuing the error for separate display is clean.
  • Using contextlib.suppress(RuntimeError) for cleanup is appropriately defensive since reset may also fail if parsing failed.
  • The conditional guards correctly prevent running the simulation when parsing fails.
include/backend/dd/DDSimDebug.hpp (1)

122-125: LGTM!

The lastErrorMessage field is well-documented and appropriately placed within the struct. The default std::string initialization to an empty string aligns with the error-checking logic in ddsimGetLastErrorMessage which returns nullptr when the message is empty.

python/mqt/debugger/dap/messages/launch_dap_message.py (1)

67-80: LGTM!

The refactored error handling flow is well-structured:

  1. Setting source_file early ensures highlight events can reference the correct file
  2. Queuing parse errors instead of returning an error response integrates with the new highlighting mechanism
  3. The contextlib.suppress for reset is appropriate since the simulation may be in an inconsistent state after a parse failure

The pattern of returning success=True while queuing errors for later display via HighlightError events is consistent with the DAP server's new highlighting architecture.

python/mqt/debugger/dap/dap_server.py (13)

118-119: LGTM!

The new state fields are appropriately initialized and typed. The _prevent_exit flag ensures that ExitedDAPEvent is not emitted prematurely when highlight events are pending.


172-186: LGTM!

Resetting _prevent_exit before processing execution commands ensures the flag doesn't persist across user actions, allowing proper ExitedDAPEvent emission when appropriate.


257-261: LGTM!

The pending highlights emission logic correctly clears the queue after sending and sets _prevent_exit to prevent premature exit events while the user views the highlights.


271-275: LGTM!

The _prevent_exit check correctly gates the ExitedDAPEvent emission, ensuring users can view error highlights before the debugging session terminates.


355-361: LGTM!

The assertion failure handling correctly categorizes the output as stderr and efficiently reuses the already-fetched error_causes for building highlight entries.


373-384: LGTM!

The edge case handling improvements are well-considered:

  • Empty source code returns (0, ...) instead of potentially incorrect values
  • The <= check allows positions at the end of a line (e.g., after the last character)
  • The else clause gracefully handles positions beyond the source code length by returning the last valid position

431-462: LGTM!

The collect_highlight_entries method is well-designed with:

  • Defensive getattr check for source_code availability
  • Exception handling around diagnostics calls
  • A fallback to highlight the failing instruction when no specific error causes are found

464-491: LGTM!

The _build_highlight_entry method correctly handles:

  • Error recovery via try/except returning None
  • End position edge cases for newline characters
  • Bounds checking with min() to prevent out-of-range access
  • Code snippet extraction with whitespace trimming

493-502: LGTM!

Clean static mapping method with appropriate fallback to UNKNOWN for unrecognized error cause types.


504-523: LGTM!

The parse error handling correctly extracts source location from compiler error messages using a regex pattern. The fallback to (1, 1) for unrecognized formats ensures the error is still highlighted.


525-559: LGTM!

Excellent edge case handling for parse error highlighting:

  • Line/column clamping prevents out-of-bounds issues
  • The backward search for empty lines improves UX by highlighting the actual problematic code rather than an empty line
  • Using instruction=-1 as a sentinel for parse errors (no associated instruction) is a clean design choice

561-584: LGTM!

The _flatten_message_parts method comprehensively handles nested message structures with appropriate type checking and recursion. The filtering of empty strings ensures clean output.


586-653: LGTM!

The refactored message handling:

  • Adds the category parameter to support stderr/stdout/console differentiation
  • Cleanly separates message assembly (send_message_hierarchy) from emission (send_message_simple)
  • Properly flattens nested message structures before concatenation
bindings/InterfaceBindings.cpp (1)

177-187: LGTM!

The enhanced error handling provides significantly better user experience:

  • Defensively checks if getLastErrorMessage is available before calling
  • Handles nullptr return gracefully via the ternary operator
  • Falls back to a generic message if the specific error message is empty
  • The std::runtime_error is correctly caught as RuntimeError in Python, aligning with the DAP server's exception handling
include/backend/debug.h (1)

57-66: LGTM!

The getLastErrorMessage function pointer is well-documented with clear ownership semantics. The note about pointer validity until the next error-state-modifying call is important for callers to understand the lifetime constraints.

src/backend/dd/DDSimDebug.cpp (6)

78-84: LGTM!

The implementation correctly returns nullptr for empty messages and provides a pointer to the internal string storage via c_str(). The lifetime matches the documented behavior since the pointer remains valid until the next call modifies lastErrorMessage.


524-524: LGTM!

The function pointer is correctly assigned to complete the interface wiring.


584-584: LGTM!

Clearing lastErrorMessage during initialization ensures a clean state for each session.


603-619: LGTM!

The comprehensive state clearing ensures a clean slate before loading new code. Setting ready = false early prevents any operations from proceeding on stale state during the load process.


621-636: LGTM!

The error handling is robust:

  • Captures the exception message via what() with a fallback for empty messages
  • The catch-all handler ensures unexpected exception types don't crash and still produce an error
  • Storing the message rather than printing to stderr enables proper error propagation to the frontend

1424-1428: Manual verification required for breakpoint edge case handling.

The review requests verification of edge case behavior when users click in whitespace or comments before code (positions before the first instruction). Specifically, confirm that:

  • Positions within file headers/comments before any instructions are handled correctly
  • The very first character position (0) produces expected behavior
  • Existing tests adequately cover these edge cases

This verification could not be completed automatically due to repository access issues.

Comment on lines 1152 to +1153
Result ddsimRunAll(SimulationState* self, size_t* failedAssertions) {
auto* ddsim = toDDSimulationState(self);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unused variable.

The ddsim pointer is obtained but not used in the ddsimRunAll function. The function only uses self for interface calls.

♻️ Suggested fix
 Result ddsimRunAll(SimulationState* self, size_t* failedAssertions) {
-  auto* ddsim = toDDSimulationState(self);
   size_t errorCount = 0;
📝 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
Result ddsimRunAll(SimulationState* self, size_t* failedAssertions) {
auto* ddsim = toDDSimulationState(self);
Result ddsimRunAll(SimulationState* self, size_t* failedAssertions) {
size_t errorCount = 0;
🤖 Prompt for AI Agents
In @src/backend/dd/DDSimDebug.cpp around lines 1152 - 1153, The local pointer
ddsim created by calling toDDSimulationState(self) inside ddsimRunAll is unused;
remove the unused variable declaration (the call to toDDSimulationState and the
auto* ddsim assignment) from ddsimRunAll to avoid the unused-variable warning,
or if the underlying DDSimulationState is actually needed later, replace usages
of self with ddsim where appropriate; ensure you keep the function signature and
behavior intact.

Comment on lines +62 to +76
LineColumn lineColumnForOffset(const std::string& code, size_t offset) {
LineColumn location;
const auto lineStartPos = code.rfind('\n', offset);
const size_t lineStart = (lineStartPos == std::string::npos)
? 0
: static_cast<size_t>(lineStartPos + 1);
location.line = 1;
for (size_t i = 0; i < lineStart; i++) {
if (code[i] == '\n') {
location.line++;
}
}
location.column = offset - lineStart + 1;
return location;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential edge case when offset points at a newline character.

When offset points directly at a \n, rfind('\n', offset) returns offset itself. This means lineStart = offset + 1, and column = offset - (offset + 1) + 1 = 0, which would be incorrect (column should be 1-based).

Consider whether this edge case can occur in practice, or add a guard:

Suggested fix
 LineColumn lineColumnForOffset(const std::string& code, size_t offset) {
   LineColumn location;
+  // Clamp offset to valid range
+  if (offset > 0 && offset <= code.size() && code[offset - 1] == '\n') {
+    // If we're at the start of a line, adjust to report column 1
+  }
   const auto lineStartPos = code.rfind('\n', offset);
   const size_t lineStart = (lineStartPos == std::string::npos)
                                ? 0
                                : static_cast<size_t>(lineStartPos + 1);

Alternatively, ensure callers never pass an offset pointing to a newline.

🤖 Prompt for AI Agents
In @src/common/parsing/CodePreprocessing.cpp around lines 62 - 76, The function
LineColumn lineColumnForOffset has an off-by-one edge when offset points at a
'\n' because rfind('\n', offset) can return offset itself, producing column 0;
fix by ensuring rfind searches before the offset (e.g., use rfind('\n', offset -
1) when offset > 0 or otherwise adjust the search) and recompute
lineStart/column accordingly so column remains 1-based; update the logic inside
lineColumnForOffset to handle offset==0 and offset pointing at a newline (or
alternatively clamp offset to the previous byte) and keep all callers' semantics
unchanged.

Comment on lines +97 to +103
if (!target.empty()) {
const auto targetPos = lineText.find(target);
if (targetPos != std::string::npos) {
location.column = targetPos + 1;
return location;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Substring matching may report incorrect column for certain targets.

Using lineText.find(target) can match a substring rather than the exact token. For example, if target = "q" and the line contains qreg q[2], the function returns column 1 (matching the q in qreg) rather than the column of the actual q register reference.

This is a minor concern since the line number will still be correct, and the column is primarily for developer convenience.

🤖 Prompt for AI Agents
In @src/common/parsing/CodePreprocessing.cpp around lines 97 - 103, The current
substring search using lineText.find(target) can match inside other tokens
(e.g., "q" in "qreg"); update the search to find a whole-token match instead:
scan for occurrences of target in lineText and verify character boundaries (or
use a regex with word boundaries like "\\btarget\\b") so the found position
corresponds to a standalone token; change the logic around target, lineText,
targetPos and location.column to use this token-aware check before assigning
location.column and returning.

Comment on lines +184 to +196
if (!isDigits(indexText)) {
throw ParsingError(formatParseError(code, instructionStart,
invalidTargetDetail(target, context),
target));
}
size_t registerIndex = 0;
try {
registerIndex = std::stoul(indexText);
} catch (const std::exception&) {
throw ParsingError(formatParseError(code, instructionStart,
invalidTargetDetail(target, context),
target));
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Defensive error handling is appropriate but consider consolidating the error path.

The isDigits check at line 184 ensures the string contains only digits, so std::stoul at line 191 can only throw std::out_of_range for extremely large values (exceeding unsigned long max). The try-catch is thus defensive coding for edge cases.

Consider combining this into a single validation path for clarity:

Optional: Consolidate validation
     if (!isDigits(indexText)) {
       throw ParsingError(formatParseError(code, instructionStart,
                                           invalidTargetDetail(target, context),
                                           target));
     }
     size_t registerIndex = 0;
     try {
       registerIndex = std::stoul(indexText);
-    } catch (const std::exception&) {
+    } catch (const std::out_of_range&) {
+      // Index too large to represent - treat as invalid
       throw ParsingError(formatParseError(code, instructionStart,
                                           invalidTargetDetail(target, context),
                                           target));
     }

The current implementation is functionally correct and safely handles all edge cases.

🤖 Prompt for AI Agents
In @src/common/parsing/CodePreprocessing.cpp around lines 184 - 196, The code
redundantly checks isDigits(indexText) then wraps std::stoul in a broad
try/catch; consolidate by keeping the isDigits check and replacing the try/catch
with a deterministic range check: parse with std::stoull (or use a checked
conversion), compare the resulting unsigned long long against
std::numeric_limits<size_t>::max(), and if it exceeds that limit throw the same
ParsingError (using formatParseError/invalidTargetDetail/target); otherwise
assign the value to registerIndex (static_cast<size_t>) — this removes the broad
exception handling while preserving correctness for out-of-range values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants