Skip to content

Commit

Permalink
Circuit panel in VS Code (#1269)
Browse files Browse the repository at this point in the history
Circuit code lens and webview showing rendered circuit in VS Code.

Debugging not included - that will be in the next PR.

---------

Co-authored-by: orpuente-MS <156957451+orpuente-MS@users.noreply.github.com>
  • Loading branch information
minestarks and orpuente-MS committed Mar 22, 2024
1 parent 397307c commit ea7a8bd
Show file tree
Hide file tree
Showing 18 changed files with 498 additions and 64 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"allocator/mimalloc-sys",
"compiler/qsc",
"compiler/qsc_ast",
"compiler/qsc_circuit",
"compiler/qsc_codegen",
"compiler/qsc_data_structures",
"compiler/qsc_doc_gen",
Expand Down
31 changes: 19 additions & 12 deletions npm/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ export function getLanguageServiceWorker(
return createProxy(worker, wasmModule, languageServiceProtocol);
}

export { StepResultId, type IStructStepResult } from "../lib/web/qsc_wasm.js";
export type {
IBreakpointSpan,
ICodeLens,
ILocation,
IOperationInfo,
IPosition,
IRange,
IStackFrame,
VSDiagnostic,
} from "../lib/web/qsc_wasm.js";
export { type Dump, type ShotResult } from "./compiler/common.js";
export { type CompilerState } from "./compiler/compiler.js";
export { QscEventTarget } from "./compiler/events.js";
Expand All @@ -176,20 +187,16 @@ export {
type LessonItem,
type Question,
} from "./katas.js";
export { type LanguageServiceEvent } from "./language-service/language-service.js";
export { default as samples } from "./samples.generated.js";
export { log, type LogLevel, type TargetProfile };
export type { ICompilerWorker, ICompiler };
export type { ILanguageServiceWorker, ILanguageService };
export type { IDebugServiceWorker, IDebugService };
export type {
IBreakpointSpan,
IStackFrame,
IPosition,
IRange,
ILocation,
VSDiagnostic,
} from "../lib/web/qsc_wasm.js";
export { type IStructStepResult, StepResultId } from "../lib/web/qsc_wasm.js";
export { type LanguageServiceEvent } from "./language-service/language-service.js";
ICompiler,
ICompilerWorker,
IDebugService,
IDebugServiceWorker,
ILanguageService,
ILanguageServiceWorker,
};

export * as utils from "./utils.js";
25 changes: 24 additions & 1 deletion npm/src/compiler/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { type VSDiagnostic } from "../../lib/web/qsc_wasm.js";
import {
IOperationInfo,
TargetProfile,
type VSDiagnostic,
} from "../../lib/web/qsc_wasm.js";
import { log } from "../log.js";
import {
IServiceProxy,
Expand Down Expand Up @@ -70,6 +74,11 @@ export interface ICompiler {
languageFeatures?: string[],
): Promise<string>;
getEstimates(config: ProgramConfig, params: string): Promise<string>;
getCircuit(
config: ProgramConfig,
target: TargetProfile,
operation?: IOperationInfo,
): Promise<object>;

checkExerciseSolution(
userCode: string,
Expand Down Expand Up @@ -210,6 +219,19 @@ export class Compiler implements ICompiler {
);
}

async getCircuit(
config: ProgramConfig,
target: TargetProfile,
operation?: IOperationInfo,
): Promise<object> {
return this.wasm.get_circuit(
config.sources,
target,
operation,
config.languageFeatures || [],
);
}

async checkExerciseSolution(
userCode: string,
exerciseSources: string[],
Expand Down Expand Up @@ -264,6 +286,7 @@ export const compilerProtocol: ServiceProtocol<ICompiler, QscEventData> = {
getHir: "request",
getQir: "request",
getEstimates: "request",
getCircuit: "request",
run: "requestWithProgress",
checkExerciseSolution: "requestWithProgress",
},
Expand Down
28 changes: 28 additions & 0 deletions npm/ux/circuit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,31 @@ export function Circuit(props: { circuit: qviz.Circuit }) {

return <div class="qs-circuit" ref={circuitDiv}></div>;
}

export function CircuitPanel(props: {
title: string;
subtitle: string;
circuit?: qviz.Circuit;
errorHtml?: string;
}) {
return (
<div>
<div>
<h1>{props.title}</h1>
<h2>{props.subtitle}</h2>
</div>
{props.circuit ? <Circuit circuit={props.circuit}></Circuit> : null}
<div class="qs-circuit-error">
{props.errorHtml ? (
<div dangerouslySetInnerHTML={{ __html: props.errorHtml }}></div>
) : null}
</div>
<div>
Tip: you can generate a circuit diagram for any operation that takes
qubits or arrays of qubits as input.
</div>
</div>
);
}

export type CircuitData = qviz.Circuit;
2 changes: 1 addition & 1 deletion npm/ux/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export { SpaceChart } from "./spaceChart.js";
export { ScatterChart } from "./scatterChart.js";
export { EstimatesOverview } from "./estimatesOverview.js";
export { EstimatesPanel } from "./estimatesPanel.js";
export { Circuit } from "./circuit.js";
export { Circuit, CircuitPanel, type CircuitData } from "./circuit.js";
7 changes: 6 additions & 1 deletion npm/ux/qsharp-circuit.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
/* Copyright (c) Microsoft Corporation.
Licensed under the MIT License. */

/* Error div in circuit panel */
.qs-circuit-error {
padding: 10px 0px;
}

/*
Styles for Q# Circuits.
Styles for Q# circuits.
Copied and modified from the default CSS generated by
https://github.com/microsoft/quantum-viz.js/blob/288e0cb506ee866ecca1b414c522e2d634f7b36d/src/styles.ts
*/
Expand Down
14 changes: 14 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@
"type": "boolean",
"default": "true",
"description": "Enables the Q# formatter."
},
"Q#.showCircuitCodeLens": {
"type": "boolean",
"default": true,
"description": "Enables the Circuit code lens to synthesize circuit diagrams for Q# programs and operations."
}
}
},
Expand Down Expand Up @@ -174,6 +179,10 @@
"command": "qsharp-vscode.showHelp",
"when": "resourceLangId == qsharp"
},
{
"command": "qsharp-vscode.showCircuit",
"when": "resourceLangId == qsharp"
},
{
"command": "qsharp-vscode.setTargetProfile",
"when": "resourceLangId == qsharp"
Expand Down Expand Up @@ -281,6 +290,11 @@
"title": "Help",
"category": "Q#"
},
{
"command": "qsharp-vscode.showCircuit",
"title": "Show circuit",
"category": "Q#"
},
{
"command": "qsharp-vscode.workspacesRefresh",
"category": "Q#",
Expand Down
172 changes: 172 additions & 0 deletions vscode/src/circuit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
IOperationInfo,
VSDiagnostic,
getCompilerWorker,
log,
} from "qsharp-lang";
import { Uri, window } from "vscode";
import { basename, isQsharpDocument } from "./common";
import { getTarget, getTargetFriendlyName } from "./config";
import { loadProject } from "./projectSystem";
import { EventType, UserFlowStatus, sendTelemetryEvent } from "./telemetry";
import { getRandomGuid } from "./utils";
import { sendMessageToPanel } from "./webviewPanel";

const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes

export async function showCircuitCommand(
extensionUri: Uri,
operation: IOperationInfo | undefined,
) {
const associationId = getRandomGuid();
sendTelemetryEvent(EventType.TriggerCircuit, { associationId }, {});

const compilerWorkerScriptPath = Uri.joinPath(
extensionUri,
"./out/compilerWorker.js",
).toString();

const editor = window.activeTextEditor;
if (!editor || !isQsharpDocument(editor.document)) {
throw new Error("The currently active window is not a Q# file");
}

sendMessageToPanel("circuit", true, undefined);

let timeout = false;

// Start the worker, run the code, and send the results to the webview
const worker = getCompilerWorker(compilerWorkerScriptPath);
const compilerTimeout = setTimeout(() => {
timeout = true;
sendTelemetryEvent(EventType.CircuitEnd, {
associationId,
reason: "timeout",
flowStatus: UserFlowStatus.Aborted,
});
log.info("terminating circuit worker due to timeout");
worker.terminate();
}, compilerRunTimeoutMs);
let title;
let subtitle;
const targetProfile = getTarget();
const sources = await loadProject(editor.document.uri);
if (operation) {
title = `${operation.operation} with ${operation.totalNumQubits} input qubits`;
subtitle = `${getTargetFriendlyName(targetProfile)} `;
} else {
title = basename(editor.document.uri.path) || "Circuit";
subtitle = `${getTargetFriendlyName(targetProfile)}`;
}

try {
sendTelemetryEvent(EventType.CircuitStart, { associationId }, {});
const circuit = await worker.getCircuit(sources, targetProfile, operation);
clearTimeout(compilerTimeout);

const message = {
command: "circuit",
circuit,
title,
subtitle,
};
sendMessageToPanel("circuit", false, message);

sendTelemetryEvent(EventType.CircuitEnd, {
associationId,
flowStatus: UserFlowStatus.Succeeded,
});
} catch (e: any) {
log.error("Circuit error. ", e.toString());
clearTimeout(compilerTimeout);

const errors: [string, VSDiagnostic][] =
typeof e === "string" ? JSON.parse(e) : undefined;
let errorHtml = "There was an error generating the circuit.";
if (errors) {
errorHtml = errorsToHtml(errors);
}

if (!timeout) {
sendTelemetryEvent(EventType.CircuitEnd, {
associationId,
reason: errors && errors[0] ? errors[0][1].code : undefined,
flowStatus: UserFlowStatus.Failed,
});
}

const message = {
command: "circuit",
title,
subtitle,
errorHtml,
};
sendMessageToPanel("circuit", false, message);
} finally {
log.info("terminating circuit worker");
worker.terminate();
}
}

/**
* Formats an array of compiler/runtime errors into HTML to be presented to the user.
*
* @param {[string, VSDiagnostic][]} errors
* The string is the document URI or "<project>" if the error isn't associated with a specific document.
* The VSDiagnostic is the error information.
*
* @returns {string} - The HTML formatted errors, to be set as the inner contents of a container element.
*/
function errorsToHtml(errors: [string, VSDiagnostic][]): string {
let errorHtml = "";
for (const error of errors) {
let location;
const document = error[0];
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.
const uri = Uri.parse(document, true);
const openCommandUri = Uri.parse(
`command:vscode.open?${encodeURIComponent(JSON.stringify([uri]))}`,
true,
);
const fsPath = escapeHtml(uri.fsPath);
const lineColumn = escapeHtml(
`:${error[1].range.start.line}:${error[1].range.start.character}`,
);
location = `<a href="${openCommandUri}">${fsPath}</a>${lineColumn}`;
} catch (e) {
// Likely could not parse document URI - it must be a project level error,
// use the document name directly
location = escapeHtml(error[0]);
}

const message = escapeHtml(
`(${error[1].code}) ${error[1].message}`,
).replace("\n", "<br/>");

errorHtml += `${location}: ${message}<br/>`;
}
return errorHtml;
}

function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
Loading

0 comments on commit ea7a8bd

Please sign in to comment.