Skip to content

Commit aa9424f

Browse files
authored
Add debugger visualization for arrays (#2812)
Adds debugger visualization for array values <img width="1615" height="569" alt="image" src="https://github.com/user-attachments/assets/2809d3c4-dc99-4728-af01-760e22a6a7cc" /> Closes #2645
1 parent fdfff44 commit aa9424f

File tree

4 files changed

+294
-92
lines changed

4 files changed

+294
-92
lines changed

source/npm/qsharp/src/browser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export type {
190190
IStackFrame,
191191
IStructStepResult,
192192
ITestDescriptor,
193+
IVariable,
194+
IVariableChild,
193195
IWorkspaceEdit,
194196
VSDiagnostic,
195197
} from "../lib/web/qsc_wasm.js";

source/npm/qsharp/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,4 @@ export function getLanguageServiceWorker(): ILanguageServiceWorker {
9292
}
9393

9494
export * as utils from "./utils.js";
95+
export type { IVariable, IVariableChild } from "./browser.js";

source/vscode/src/debugger/session.ts

Lines changed: 215 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { DebugProtocol } from "@vscode/debugprotocol";
2525
import {
2626
IDebugServiceWorker,
2727
IStructStepResult,
28+
IVariable,
29+
IVariableChild,
2830
QscEventTarget,
2931
StepResultId,
3032
log,
@@ -64,16 +66,28 @@ interface IBreakpointLocationData {
6466
breakpoint: DebugProtocol.Breakpoint;
6567
}
6668

69+
type ScopeHandle =
70+
| { kind: "scope"; scope: "locals"; frameId: number }
71+
| { kind: "scope"; scope: "quantum" }
72+
| { kind: "scope"; scope: "circuit" };
73+
74+
type ArrayHandle = {
75+
kind: "array";
76+
previewChildren: IVariableChild[];
77+
truncated: boolean;
78+
totalLength: number | undefined;
79+
};
80+
81+
type VariableHandle = ScopeHandle | ArrayHandle;
82+
6783
export class QscDebugSession extends LoggingDebugSession {
6884
private static threadID = 1;
6985

7086
private readonly knownPaths = new Map<string, string>();
7187

7288
private breakpointLocations: Map<string, IBreakpointLocationData[]>;
7389
private breakpoints: Map<string, DebugProtocol.Breakpoint[]>;
74-
private variableHandles = new Handles<
75-
["locals" | "quantum" | "circuit", number]
76-
>();
90+
private variableHandles = new Handles<VariableHandle>();
7791
private failureMessage: string;
7892
private eventTarget: QscEventTarget;
7993
private supportsVariableType = false;
@@ -756,17 +770,21 @@ export class QscDebugSession extends LoggingDebugSession {
756770
scopes: [
757771
new Scope(
758772
"Locals",
759-
this.variableHandles.create(["locals", args.frameId]),
773+
this.variableHandles.create({
774+
kind: "scope",
775+
scope: "locals",
776+
frameId: args.frameId,
777+
}),
760778
false,
761779
),
762780
new Scope(
763781
"Quantum State",
764-
this.variableHandles.create(["quantum", -1]),
782+
this.variableHandles.create({ kind: "scope", scope: "quantum" }),
765783
true, // expensive - keeps scope collapsed in the UI by default
766784
),
767785
new Scope(
768786
"Quantum Circuit",
769-
this.variableHandles.create(["circuit", -1]),
787+
this.variableHandles.create({ kind: "scope", scope: "circuit" }),
770788
true, // expensive - keeps scope collapsed in the UI by default
771789
),
772790
],
@@ -786,103 +804,214 @@ export class QscDebugSession extends LoggingDebugSession {
786804
variables: [],
787805
};
788806

789-
const [handle, frameID] = this.variableHandles.get(args.variablesReference);
790-
791-
switch (handle) {
792-
case "locals":
793-
{
794-
const locals = await this.debugService.getLocalVariables(frameID);
795-
const variables = locals.map((local) => {
796-
const variable: DebugProtocol.Variable = {
797-
name: local.name,
798-
value: local.value,
799-
variablesReference: 0,
807+
const handleData = this.variableHandles.get(args.variablesReference);
808+
809+
if (!handleData) {
810+
log.warn(`variablesRequest: unknown handle ${args.variablesReference}`);
811+
} else if (handleData.kind === "scope") {
812+
switch (handleData.scope) {
813+
case "locals":
814+
{
815+
const locals = await this.debugService.getLocalVariables(
816+
handleData.frameId,
817+
);
818+
response.body = {
819+
variables: locals.map((local) =>
820+
this.createVariableFromLocal(local),
821+
),
800822
};
801-
if (this.supportsVariableType) {
802-
variable.type = local.var_type;
823+
}
824+
break;
825+
case "quantum":
826+
{
827+
const associationId = getRandomGuid();
828+
const start = performance.now();
829+
sendTelemetryEvent(
830+
EventType.RenderQuantumStateStart,
831+
{ associationId },
832+
{},
833+
);
834+
const state = await this.debugService.captureQuantumState();
835+
836+
// When there is no quantum state, return a single variable with a message
837+
// for the user.
838+
if (state.length == 0) {
839+
response.body = {
840+
variables: [
841+
{
842+
name: "None",
843+
value: "No qubits allocated",
844+
variablesReference: 0,
845+
},
846+
],
847+
};
848+
break;
803849
}
804-
return variable;
805-
});
806-
response.body = {
807-
variables: variables,
808-
};
809-
}
810-
break;
811-
case "quantum":
812-
{
813-
const associationId = getRandomGuid();
814-
const start = performance.now();
815-
sendTelemetryEvent(
816-
EventType.RenderQuantumStateStart,
817-
{ associationId },
818-
{},
819-
);
820-
const state = await this.debugService.captureQuantumState();
821850

822-
// When there is no quantum state, return a single variable with a message
823-
// for the user.
824-
if (state.length == 0) {
851+
const variables: DebugProtocol.Variable[] = state.map((entry) => {
852+
const variable: DebugProtocol.Variable = {
853+
name: entry.name,
854+
value: entry.value,
855+
variablesReference: 0,
856+
type: "Complex",
857+
};
858+
return variable;
859+
});
860+
sendTelemetryEvent(
861+
EventType.RenderQuantumStateEnd,
862+
{ associationId },
863+
{ timeToCompleteMs: performance.now() - start },
864+
);
865+
response.body = {
866+
variables: variables,
867+
};
868+
}
869+
break;
870+
case "circuit":
871+
{
872+
// This will get invoked when the "Quantum Circuit" scope is expanded
873+
// in the Variables view, but instead of showing any values in the variables
874+
// view, we can pop open the circuit diagram panel.
875+
if (!this.config.showCircuit) {
876+
// Keep updating the circuit for the rest of this session, even if
877+
// the Variables scope gets collapsed by the user. If we don't do this,
878+
// the diagram won't get updated with each step even though the circuit
879+
// panel is still being shown, which is misleading.
880+
this.config.showCircuit = true;
881+
await this.updateCircuit();
882+
}
825883
response.body = {
826884
variables: [
827885
{
828-
name: "None",
829-
value: "No qubits allocated",
886+
name: "Circuit",
887+
value: "See QDK Circuit panel",
830888
variablesReference: 0,
831889
},
832890
],
833891
};
834-
break;
835-
}
836-
837-
const variables: DebugProtocol.Variable[] = state.map((entry) => {
838-
const variable: DebugProtocol.Variable = {
839-
name: entry.name,
840-
value: entry.value,
841-
variablesReference: 0,
842-
type: "Complex",
843-
};
844-
return variable;
845-
});
846-
sendTelemetryEvent(
847-
EventType.RenderQuantumStateEnd,
848-
{ associationId },
849-
{ timeToCompleteMs: performance.now() - start },
850-
);
851-
response.body = {
852-
variables: variables,
853-
};
854-
}
855-
break;
856-
case "circuit":
857-
{
858-
// This will get invoked when the "Quantum Circuit" scope is expanded
859-
// in the Variables view, but instead of showing any values in the variables
860-
// view, we can pop open the circuit diagram panel.
861-
if (!this.config.showCircuit) {
862-
// Keep updating the circuit for the rest of this session, even if
863-
// the Variables scope gets collapsed by the user. If we don't do this,
864-
// the diagram won't get updated with each step even though the circuit
865-
// panel is still being shown, which is misleading.
866-
this.config.showCircuit = true;
867-
await this.updateCircuit();
868892
}
869-
response.body = {
870-
variables: [
871-
{
872-
name: "Circuit",
873-
value: "See QDK Circuit panel",
874-
variablesReference: 0,
875-
},
876-
],
877-
};
878-
}
879-
break;
893+
break;
894+
}
895+
} else if (handleData.kind === "array") {
896+
const allChildren = this.createArrayVariables(handleData);
897+
const totalChildren = allChildren.length;
898+
const rawStart = args.start ?? 0;
899+
const start = Math.max(0, Math.min(rawStart, totalChildren));
900+
const requestedCount = args.count;
901+
let end = totalChildren;
902+
if (requestedCount !== undefined) {
903+
end = Math.min(start + Math.max(requestedCount, 0), totalChildren);
904+
}
905+
const variables =
906+
start >= totalChildren ? [] : allChildren.slice(start, end);
907+
response.body = {
908+
variables,
909+
};
880910
}
881911

882912
log.trace(`variablesResponse: %O`, response);
883913
this.sendResponse(response);
884914
}
885915

916+
private createVariableFromLocal(local: IVariable): DebugProtocol.Variable {
917+
const variable: DebugProtocol.Variable = {
918+
name: local.name,
919+
value: local.value,
920+
variablesReference: 0,
921+
};
922+
923+
if (this.supportsVariableType) {
924+
variable.type = local.varType;
925+
}
926+
927+
if (local.varType === "Array") {
928+
const previewChildren = (local.previewChildren ?? []).map((child) => ({
929+
...child,
930+
}));
931+
const truncated = local.truncated === true;
932+
variable.value = this.formatArraySummary(
933+
local.length,
934+
previewChildren.length,
935+
truncated,
936+
local.value,
937+
);
938+
939+
if (this.supportsVariableType) {
940+
const elementType = local.elementType ?? "Unknown";
941+
variable.type = `Array<${elementType}>`;
942+
}
943+
944+
if (previewChildren.length > 0 || truncated) {
945+
variable.variablesReference = this.variableHandles.create({
946+
kind: "array",
947+
previewChildren,
948+
truncated,
949+
totalLength: local.length,
950+
});
951+
}
952+
}
953+
954+
return variable;
955+
}
956+
957+
private createArrayVariables(handle: ArrayHandle): DebugProtocol.Variable[] {
958+
const variables = handle.previewChildren.map((child) => {
959+
const variable: DebugProtocol.Variable = {
960+
name: `[${child.index}]`,
961+
value: child.value,
962+
variablesReference: 0,
963+
};
964+
if (this.supportsVariableType) {
965+
variable.type = child.varType;
966+
}
967+
return variable;
968+
});
969+
970+
if (handle.truncated) {
971+
const remaining =
972+
handle.totalLength !== undefined
973+
? Math.max(handle.totalLength - handle.previewChildren.length, 0)
974+
: undefined;
975+
const message =
976+
remaining !== undefined && remaining > 0
977+
? `${remaining} more items truncated`
978+
: "More items truncated";
979+
const sentinel: DebugProtocol.Variable = {
980+
name: "[...]",
981+
value: message,
982+
variablesReference: 0,
983+
};
984+
if (this.supportsVariableType) {
985+
sentinel.type = "Truncated";
986+
}
987+
variables.push(sentinel);
988+
}
989+
990+
return variables;
991+
}
992+
993+
private formatArraySummary(
994+
length: number | undefined,
995+
previewCount: number,
996+
truncated: boolean,
997+
fallback: string,
998+
): string {
999+
if (length !== undefined) {
1000+
if (truncated) {
1001+
return `length = ${length} (showing first ${previewCount})`;
1002+
}
1003+
return `length = ${length}`;
1004+
}
1005+
1006+
if (truncated) {
1007+
return previewCount > 0
1008+
? `showing first ${previewCount}`
1009+
: "values truncated";
1010+
}
1011+
1012+
return fallback;
1013+
}
1014+
8861015
private createBreakpoint(
8871016
id: number,
8881017
location: DebugProtocol.BreakpointLocation,

0 commit comments

Comments
 (0)