From 8d9616bb2bababfba263ab639a02daa2c0be567e Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 1 May 2026 15:43:46 -0700 Subject: [PATCH 1/3] format debugger errors in the wasm layer --- source/wasm/src/debug_service.rs | 16 ++++++++-------- source/wasm/src/diagnostic.rs | 6 ++++++ source/wasm/src/lib.rs | 9 +-------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/source/wasm/src/debug_service.rs b/source/wasm/src/debug_service.rs index ed0e6f1cf8..dda6b62610 100644 --- a/source/wasm/src/debug_service.rs +++ b/source/wasm/src/debug_service.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::diagnostic::interpret_errors_to_run_result; +use crate::diagnostic::{interpret_errors_into_qsharp_errors_json, interpret_errors_to_run_result}; use crate::line_column::{Location, Range}; use crate::project_system::{ProgramConfig, into_qsc_args}; use crate::{ @@ -99,7 +99,7 @@ impl DebugService { &mut self, event_cb: &js_sys::Function, ids: &[u32], - ) -> Result { + ) -> Result { self.eval(event_cb, ids, StepAction::Next) } @@ -107,7 +107,7 @@ impl DebugService { &mut self, event_cb: &js_sys::Function, ids: &[u32], - ) -> Result { + ) -> Result { self.eval(event_cb, ids, StepAction::Continue) } @@ -115,7 +115,7 @@ impl DebugService { &mut self, event_cb: &js_sys::Function, ids: &[u32], - ) -> Result { + ) -> Result { self.eval(event_cb, ids, StepAction::In) } @@ -123,7 +123,7 @@ impl DebugService { &mut self, event_cb: &js_sys::Function, ids: &[u32], - ) -> Result { + ) -> Result { self.eval(event_cb, ids, StepAction::Out) } @@ -132,9 +132,9 @@ impl DebugService { event_cb: &js_sys::Function, ids: &[u32], step: StepAction, - ) -> Result { + ) -> Result { if !event_cb.is_function() { - return Err(JsError::new("Events callback function must be provided").into()); + return Err("Events callback function must be provided".into()); } let bps: Vec<_> = ids.iter().map(|f| StmtId::from(*f)).collect(); @@ -144,7 +144,7 @@ impl DebugService { }; match self.run_internal(event_cb, &bps, step) { Ok(value) => Ok(StructStepResult::from(value).into()), - Err(e) => Err(JsError::from(&e[0]).into()), + Err(e) => Err(interpret_errors_into_qsharp_errors_json(e)), } } diff --git a/source/wasm/src/diagnostic.rs b/source/wasm/src/diagnostic.rs index 69126aad59..666226bcec 100644 --- a/source/wasm/src/diagnostic.rs +++ b/source/wasm/src/diagnostic.rs @@ -258,6 +258,12 @@ pub fn interpret_errors_into_qsharp_errors(errs: &[interpret::Error]) -> Vec) -> String { + serde_json::to_string(&interpret_errors_into_qsharp_errors(&errs)) + .expect("serializing errors to json should succeed") +} + pub fn project_errors_into_qsharp_errors( project_dir: &str, errs: &[project::Error], diff --git a/source/wasm/src/lib.rs b/source/wasm/src/lib.rs index 252dfb3884..b7181c0114 100644 --- a/source/wasm/src/lib.rs +++ b/source/wasm/src/lib.rs @@ -4,7 +4,6 @@ #![allow(unknown_lints, clippy::empty_docs)] #![allow(non_snake_case)] -use diagnostic::interpret_errors_into_qsharp_errors; use katas::check_solution; use language_service::IOperationInfo; use num_bigint::BigUint; @@ -29,7 +28,7 @@ use serde_json::json; use std::{fmt::Write, sync::Arc}; use wasm_bindgen::prelude::*; -use crate::diagnostic::interpret_errors_to_run_result; +use crate::diagnostic::{interpret_errors_into_qsharp_errors_json, interpret_errors_to_run_result}; mod debug_service; mod diagnostic; @@ -230,12 +229,6 @@ pub fn get_circuit( } } -#[allow(clippy::needless_pass_by_value)] -fn interpret_errors_into_qsharp_errors_json(errs: Vec) -> String { - serde_json::to_string(&interpret_errors_into_qsharp_errors(&errs)) - .expect("serializing errors to json should succeed") -} - fn compile_errors_into_qsharp_errors_json(errs: Vec) -> String { interpret_errors_into_qsharp_errors_json(errs.into_iter().map(Into::into).collect()) } From d4cf00b89c19cb66023a29097c1b838a244edd86 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 1 May 2026 15:44:15 -0700 Subject: [PATCH 2/3] format debugger errors in the npm layer --- .../qsharp/src/debug-service/debug-service.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/source/npm/qsharp/src/debug-service/debug-service.ts b/source/npm/qsharp/src/debug-service/debug-service.ts index 36e002affe..4938a46b86 100644 --- a/source/npm/qsharp/src/debug-service/debug-service.ts +++ b/source/npm/qsharp/src/debug-service/debug-service.ts @@ -24,6 +24,7 @@ import { import { log } from "../log.js"; import type { IServiceProxy, ServiceProtocol } from "../workers/types.js"; import { toWasmProgramConfig } from "../compiler/compiler.js"; +import { callAndTransformExceptions } from "../diagnostics.js"; type QscWasm = typeof import("../../lib/web/qsc_wasm.js"); @@ -112,7 +113,9 @@ export class QSharpDebugService implements IDebugService { ): Promise { const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler); const ids = new Uint32Array(bps); - return this.debugService.eval_continue(event_cb, ids); + return await callAndTransformExceptions(async () => + this.debugService.eval_continue(event_cb, ids), + ); } async evalNext( @@ -121,7 +124,9 @@ export class QSharpDebugService implements IDebugService { ): Promise { const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler); const ids = new Uint32Array(bps); - return this.debugService.eval_next(event_cb, ids); + return await callAndTransformExceptions(async () => + this.debugService.eval_next(event_cb, ids), + ); } async evalStepIn( @@ -130,7 +135,9 @@ export class QSharpDebugService implements IDebugService { ): Promise { const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler); const ids = new Uint32Array(bps); - return this.debugService.eval_step_in(event_cb, ids); + return await callAndTransformExceptions(async () => + this.debugService.eval_step_in(event_cb, ids), + ); } async evalStepOut( @@ -139,7 +146,9 @@ export class QSharpDebugService implements IDebugService { ): Promise { const event_cb = (msg: string) => onCompilerEvent(msg, eventHandler); const ids = new Uint32Array(bps); - return this.debugService.eval_step_out(event_cb, ids); + return await callAndTransformExceptions(async () => + this.debugService.eval_step_out(event_cb, ids), + ); } async dispose() { From b7a13059cc034d63c4f265a5d112410dd11d5768 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 1 May 2026 15:50:29 -0700 Subject: [PATCH 3/3] format debugger errors in the vscode extension layer --- source/vscode/src/circuit.ts | 98 +-------------------------- source/vscode/src/common.ts | 96 ++++++++++++++++++++++++++ source/vscode/src/debugger/session.ts | 32 ++++++--- 3 files changed, 119 insertions(+), 107 deletions(-) diff --git a/source/vscode/src/circuit.ts b/source/vscode/src/circuit.ts index b4d111403c..4c072f1f3b 100644 --- a/source/vscode/src/circuit.ts +++ b/source/vscode/src/circuit.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { escapeHtml } from "markdown-it/lib/common/utils.mjs"; import { type CircuitData, ICompilerWorker, @@ -24,8 +23,8 @@ import { } from "./telemetry"; import { getRandomGuid } from "./utils"; import { sendMessageToPanel } from "./webviewPanel"; -import { ICircuitConfig, IPosition } from "../../npm/qsharp/lib/web/qsc_wasm"; -import { basename, loadCompilerWorker } from "./common"; +import { ICircuitConfig } from "../../npm/qsharp/lib/web/qsc_wasm"; +import { errorsToHtml, loadCompilerWorker } from "./common"; const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes @@ -405,46 +404,6 @@ function hasAdaptiveComplianceOrUnsupportedRirError(errors: IQSharpError[]) { return hasResultComparisonError; } -/** - * Formats an array of compiler/runtime errors into HTML to be presented to the user. - * - * @param errors The list of errors to format. - * @returns The HTML formatted errors, to be set as the inner contents of a container element. - */ -function errorsToHtml(errors: IQSharpError[]) { - let errorHtml = ""; - for (const error of errors) { - const { document, diagnostic: diag, stack: rawStack } = error; - - const location = documentHtml(false, document, diag.range.start); - const message = escapeHtml(`(${diag.code}) ${diag.message}`).replace( - /\n/g, - "

", - ); - - errorHtml += `

${location}: ${message}

`; - - if (rawStack) { - const stack = rawStack - .split("\n") - .map((l) => { - // Link-ify the document names in the stack trace - const match = l.match(/^(\s*)at (.*) in (.*):(\d+):(\d+)/); - if (match) { - const [, leadingWs, callable, doc] = match; - return `${leadingWs}at ${escapeHtml(callable)} in ${documentHtml(false, doc)}`; - } else { - return l; - } - }) - - .join("\n"); - errorHtml += `
${stack}
`; - } - } - return errorHtml; -} - export function updateCircuitPanel( targetProfile: string, projectName: string, @@ -480,56 +439,3 @@ export function updateCircuitPanel( }; sendMessageToPanel({ panelType: "circuit", id: panelId }, reveal, message); } - -/** - * If the input is a URI, turns it into a document open link. - * Otherwise returns the HTML-escaped input - */ -function documentHtml( - customCommand: boolean, - maybeUri: string, - position?: IPosition, -) { - try { - // If the error location is a document URI, create a link to that document. - // We use the `vscode.open` command (https://code.visualstudio.com/api/references/commands#commands) - // to open the document in the editor. - // The line and column information is displayed, but are not part of the link. - // - // At the time of writing this is the only way we know to create a direct - // link to a Q# document from a Web View. - // - // If we wanted to handle line/column information from the link, an alternate - // implementation might be having our own command that navigates to the correct - // location. Then this would be a link to that command instead. Yet another - // alternative is to have the webview pass a message back to the extension. - const uri = Uri.parse(maybeUri, true); - const fsPath = escapeHtml(basename(uri.path) ?? uri.fsPath); - const lineColumn = position - ? escapeHtml(`:${position.line + 1}:${position.character + 1}`) - : ""; - - const locations = [ - { - source: uri, - span: { - start: position, - end: position, - }, - }, - ]; - - const args = customCommand && position ? [locations] : [uri]; - const openCommand = - customCommand && position ? "qsharp-vscode.gotoLocations" : "vscode.open"; - - const argsStr = encodeURIComponent(JSON.stringify(args)); - const openCommandUri = Uri.parse(`command:${openCommand}?${argsStr}`, true); - const title = `${fsPath}${lineColumn}`; - return `${title}`; - } catch { - // Likely could not parse document URI - it must be a project level error - // or an error from stdlib, use the document name directly - return escapeHtml(maybeUri); - } -} diff --git a/source/vscode/src/common.ts b/source/vscode/src/common.ts index c39b675d46..2c2ca278be 100644 --- a/source/vscode/src/common.ts +++ b/source/vscode/src/common.ts @@ -2,11 +2,14 @@ // Licensed under the MIT License. declare const __PLATFORM__: "browser" | "node"; +import { escapeHtml } from "markdown-it/lib/common/utils.mjs"; import { TextDocument, Uri, Range, Location } from "vscode"; import { getCompilerWorker, ICompilerWorker, ILocation, + IPosition, + IQSharpError, IRange, IWorkspaceEdit, VSDiagnostic, @@ -156,3 +159,96 @@ export function loadCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker { export function getPlatformEnv(): string { return __PLATFORM__; } + +/** + * Formats an array of compiler/runtime errors into HTML to be presented to the user. + * + * @param errors The list of errors to format. + * @returns The HTML formatted errors, to be set as the inner contents of a container element. + */ +export function errorsToHtml(errors: IQSharpError[]) { + let errorHtml = ""; + for (const error of errors) { + const { document, diagnostic: diag, stack: rawStack } = error; + + const location = documentHtml(false, document, diag.range.start); + const message = escapeHtml(`(${diag.code}) ${diag.message}`).replace( + /\n/g, + "

", + ); + + errorHtml += `

${location}: ${message}

`; + + if (rawStack) { + const stack = rawStack + .split("\n") + .map((l) => { + // Link-ify the document names in the stack trace + const match = l.match(/^(\s*)at (.*) in (.*):(\d+):(\d+)/); + if (match) { + const [, leadingWs, callable, doc] = match; + return `${leadingWs}at ${escapeHtml(callable)} in ${documentHtml(false, doc)}`; + } else { + return l; + } + }) + + .join("\n"); + errorHtml += `
${stack}
`; + } + } + return errorHtml; +} + +/** + * If the input is a URI, turns it into a document open link. + * Otherwise returns the HTML-escaped input + */ +function documentHtml( + customCommand: boolean, + maybeUri: string, + position?: IPosition, +) { + try { + // If the error location is a document URI, create a link to that document. + // We use the `vscode.open` command (https://code.visualstudio.com/api/references/commands#commands) + // to open the document in the editor. + // The line and column information is displayed, but are not part of the link. + // + // At the time of writing this is the only way we know to create a direct + // link to a Q# document from a Web View. + // + // If we wanted to handle line/column information from the link, an alternate + // implementation might be having our own command that navigates to the correct + // location. Then this would be a link to that command instead. Yet another + // alternative is to have the webview pass a message back to the extension. + const uri = Uri.parse(maybeUri, true); + const fsPath = escapeHtml(basename(uri.path) ?? uri.fsPath); + const lineColumn = position + ? escapeHtml(`:${position.line + 1}:${position.character + 1}`) + : ""; + + const locations = [ + { + source: uri, + span: { + start: position, + end: position, + }, + }, + ]; + + const args = customCommand && position ? [locations] : [uri]; + const openCommand = + customCommand && position ? "qsharp-vscode.gotoLocations" : "vscode.open"; + + const argsStr = encodeURIComponent(JSON.stringify(args)); + const openCommandUri = Uri.parse(`command:${openCommand}?${argsStr}`, true); + const title = `${fsPath}${lineColumn}`; + return `${title}`; + } catch { + // Likely could not parse document URI - it must be a project level error + // or an error from stdlib, use the document name directly + return escapeHtml(maybeUri); + } +} diff --git a/source/vscode/src/debugger/session.ts b/source/vscode/src/debugger/session.ts index a275ae38ab..889813510f 100644 --- a/source/vscode/src/debugger/session.ts +++ b/source/vscode/src/debugger/session.ts @@ -27,12 +27,18 @@ import { IStructStepResult, IVariable, IVariableChild, + QdkDiagnostics, QscEventTarget, StepResultId, log, } from "qsharp-lang"; import { updateCircuitPanel } from "../circuit"; -import { basename, isQdkDocument, toVsCodeRange } from "../common"; +import { + basename, + isQdkDocument, + toVsCodeRange, + errorsToHtml, +} from "../common"; import { DebugEvent, EventType, @@ -1109,16 +1115,20 @@ export class QscDebugSession extends LoggingDebugSession { this.config.showCircuit || isPanelOpen("circuit", this.program.projectName) ) { - // Error returned from the debugger has a message and a stack (which also includes the message). - // We would ideally retrieve the original runtime error, and format it to be consistent - // with the other runtime errors that can be shown in the circuit panel, but that will require - // a bit of refactoring. - const stack = - error && typeof error === "object" && typeof error.stack === "string" - ? escapeHtml(error.stack) - : undefined; + let errorHtml: string | undefined; + if (error instanceof QdkDiagnostics) { + errorHtml = errorsToHtml(error.diagnostics); + } else if (error) { + const message = error instanceof Error ? error.message : String(error); + errorHtml = `
${escapeHtml(message)}
`; + } - const circuit = await this.debugService.getCircuit(); + let circuit; + try { + circuit = await this.debugService.getCircuit(); + } catch (e) { + log.error("Failed to retrieve circuit from debug service: %O", e); + } updateCircuitPanel( this.program.profile, @@ -1126,7 +1136,7 @@ export class QscDebugSession extends LoggingDebugSession { !this.revealedCircuit, { circuit, - errorHtml: stack ? `
${stack}
` : undefined, + errorHtml, simulated: true, }, );