From 5a86a671943d778a43d995e3109e1d588237c2f3 Mon Sep 17 00:00:00 2001 From: Mine Starks <16928427+minestarks@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:42:09 -0800 Subject: [PATCH 1/4] Move VS Code language service modules into their own folder (#2055) With the new feature I'm planning to add to VS Code extension, I needed to organize this folder a bit before I proceed. It was getting out of hand. This PR moves everything language-service related into its own folder under `vscode/src`. The only slight, expected, behavior change here is in the initialization order of diagnostics (which had to happen as a result of me separating out the different kinds of diagnostics.) I don't think it should have any observable effect. --- vscode/src/common.ts | 39 ++- vscode/src/diagnostics.ts | 104 +----- vscode/src/extension.ts | 307 +----------------- vscode/src/language-service/activate.ts | 302 +++++++++++++++++ .../src/{ => language-service}/codeActions.ts | 2 +- vscode/src/{ => language-service}/codeLens.ts | 2 +- .../src/{ => language-service}/completion.ts | 4 +- .../src/{ => language-service}/definition.ts | 2 +- vscode/src/language-service/diagnostics.ts | 49 +++ vscode/src/{ => language-service}/format.ts | 6 +- vscode/src/{ => language-service}/hover.ts | 2 +- vscode/src/language-service/notebook.ts | 135 ++++++++ .../src/{ => language-service}/references.ts | 2 +- vscode/src/{ => language-service}/rename.ts | 2 +- .../src/{ => language-service}/signature.ts | 0 vscode/src/notebook.ts | 135 +------- 16 files changed, 558 insertions(+), 535 deletions(-) create mode 100644 vscode/src/language-service/activate.ts rename vscode/src/{ => language-service}/codeActions.ts (97%) rename vscode/src/{ => language-service}/codeLens.ts (98%) rename vscode/src/{ => language-service}/completion.ts (96%) rename vscode/src/{ => language-service}/definition.ts (94%) create mode 100644 vscode/src/language-service/diagnostics.ts rename vscode/src/{ => language-service}/format.ts (92%) rename vscode/src/{ => language-service}/hover.ts (94%) create mode 100644 vscode/src/language-service/notebook.ts rename vscode/src/{ => language-service}/references.ts (95%) rename vscode/src/{ => language-service}/rename.ts (95%) rename vscode/src/{ => language-service}/signature.ts (100%) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index ea6da48537..71de7e08b6 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,7 +3,7 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { ILocation, IRange, IWorkspaceEdit } from "qsharp-lang"; +import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic } from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -58,3 +58,40 @@ export function toVscodeWorkspaceEdit( } return workspaceEdit; } + +export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { + let severity; + switch (d.severity) { + case "error": + severity = vscode.DiagnosticSeverity.Error; + break; + case "warning": + severity = vscode.DiagnosticSeverity.Warning; + break; + case "info": + severity = vscode.DiagnosticSeverity.Information; + break; + } + const vscodeDiagnostic = new vscode.Diagnostic( + toVscodeRange(d.range), + d.message, + severity, + ); + if (d.uri && d.code) { + vscodeDiagnostic.code = { + value: d.code, + target: vscode.Uri.parse(d.uri), + }; + } else if (d.code) { + vscodeDiagnostic.code = d.code; + } + if (d.related) { + vscodeDiagnostic.relatedInformation = d.related.map((r) => { + return new vscode.DiagnosticRelatedInformation( + toVscodeLocation(r.location), + r.message, + ); + }); + } + return vscodeDiagnostic; +} diff --git a/vscode/src/diagnostics.ts b/vscode/src/diagnostics.ts index 3a4dd2503d..e0fd154c86 100644 --- a/vscode/src/diagnostics.ts +++ b/vscode/src/diagnostics.ts @@ -1,100 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { - ILanguageService, - IQSharpError, - VSDiagnostic, - log, - qsharpLibraryUriScheme, -} from "qsharp-lang"; +import { IQSharpError, log, qsharpLibraryUriScheme } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation, toVscodeRange, qsharpLanguageId } from "./common.js"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "./common.js"; -export function startCheckingQSharp( - languageService: ILanguageService, -): vscode.Disposable[] { - return [ - ...startLanguageServiceDiagnostics(languageService), - ...startQsharpJsonDiagnostics(), - ...startCommandDiagnostics(), - ]; -} - -function startLanguageServiceDiagnostics( - languageService: ILanguageService, -): vscode.Disposable[] { - const diagCollection = - vscode.languages.createDiagnosticCollection(qsharpLanguageId); - - async function onDiagnostics(evt: { - detail: { - uri: string; - version: number; - diagnostics: VSDiagnostic[]; - }; - }) { - const diagnostics = evt.detail; - const uri = vscode.Uri.parse(diagnostics.uri); - - if (uri.scheme === qsharpLibraryUriScheme) { - // Don't report diagnostics for library files. - return; - } - - diagCollection.set( - uri, - diagnostics.diagnostics.map((d) => toVsCodeDiagnostic(d)), - ); - } - - languageService.addEventListener("diagnostics", onDiagnostics); - - return [ - { - dispose: () => { - languageService.removeEventListener("diagnostics", onDiagnostics); - }, - }, - diagCollection, - ]; -} - -export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { - let severity; - switch (d.severity) { - case "error": - severity = vscode.DiagnosticSeverity.Error; - break; - case "warning": - severity = vscode.DiagnosticSeverity.Warning; - break; - case "info": - severity = vscode.DiagnosticSeverity.Information; - break; - } - const vscodeDiagnostic = new vscode.Diagnostic( - toVscodeRange(d.range), - d.message, - severity, - ); - if (d.uri && d.code) { - vscodeDiagnostic.code = { - value: d.code, - target: vscode.Uri.parse(d.uri), - }; - } else if (d.code) { - vscodeDiagnostic.code = d.code; - } - if (d.related) { - vscodeDiagnostic.relatedInformation = d.related.map((r) => { - return new vscode.DiagnosticRelatedInformation( - toVscodeLocation(r.location), - r.message, - ); - }); - } - return vscodeDiagnostic; +/** + * Initialize diagnostics for `qsharp.json` files and failures + * that get reported from various Q# commands. + * + * These are distinct from the errors reported by the Q# language + * service, (a.k.a. compiler errors that get reported as you type). + * Those are initialized in `language-service/diagnostics.js` + */ +export function startOtherQSharpDiagnostics(): vscode.Disposable[] { + return [...startQsharpJsonDiagnostics(), ...startCommandDiagnostics()]; } // diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index a0061bbc17..750700f373 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -2,31 +2,17 @@ // Licensed under the MIT License. import { - ILanguageService, - getLanguageService, getLibrarySourceContent, - loadWasmModule, log, qsharpGithubUriScheme, qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; import { initAzureWorkspaces } from "./azure/commands.js"; -import { createCodeActionsProvider } from "./codeActions.js"; -import { createCodeLensProvider } from "./codeLens.js"; -import { - isQsharpDocument, - isQsharpNotebookCell, - qsharpLanguageId, -} from "./common.js"; -import { createCompletionItemProvider } from "./completion"; -import { getTarget } from "./config"; import { initProjectCreator } from "./createProject.js"; -import { activateDebugger } from "./debugger/activate"; -import { createDefinitionProvider } from "./definition"; -import { startCheckingQSharp } from "./diagnostics"; -import { createFormattingProvider } from "./format.js"; -import { createHoverProvider } from "./hover"; +import { activateDebugger } from "./debugger/activate.js"; +import { startOtherQSharpDiagnostics } from "./diagnostics.js"; +import { activateLanguageService } from "./language-service/activate.js"; import { Logging, initLogForwarder, @@ -35,29 +21,12 @@ import { import { initFileSystem } from "./memfs.js"; import { registerCreateNotebookCommand, - registerQSharpNotebookCellUpdateHandlers, registerQSharpNotebookHandlers, } from "./notebook.js"; -import { - fetchGithubRaw, - findManifestDirectory, - getGithubSourceContent, - listDirectory, - readFile, - resolvePath, - setGithubEndpoint, -} from "./projectSystem.js"; +import { getGithubSourceContent, setGithubEndpoint } from "./projectSystem.js"; import { initCodegen } from "./qirGeneration.js"; -import { createReferenceProvider } from "./references.js"; -import { createRenameProvider } from "./rename.js"; -import { createSignatureHelpProvider } from "./signature.js"; import { activateTargetProfileStatusBarItem } from "./statusbar.js"; -import { - EventType, - QsharpDocumentType, - initTelemetry, - sendTelemetryEvent, -} from "./telemetry.js"; +import { initTelemetry } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; export async function activate( @@ -102,6 +71,8 @@ export async function activate( ...(await activateLanguageService(context.extensionUri)), ); + context.subscriptions.push(...startOtherQSharpDiagnostics()); + context.subscriptions.push(...registerQSharpNotebookHandlers()); initAzureWorkspaces(context); @@ -123,270 +94,6 @@ export interface ExtensionApi { setGithubEndpoint: (endpoint: string) => void; } -function registerDocumentUpdateHandlers(languageService: ILanguageService) { - vscode.workspace.textDocuments.forEach((document) => { - updateIfQsharpDocument(document); - }); - - // we manually send an OpenDocument telemetry event if this is a Q# document, because the - // below subscriptions won't fire for documents that are already open when the extension is activated - vscode.workspace.textDocuments.forEach((document) => { - if (isQsharpDocument(document)) { - const documentType = isQsharpNotebookCell(document) - ? QsharpDocumentType.JupyterCell - : QsharpDocumentType.Qsharp; - sendTelemetryEvent( - EventType.OpenedDocument, - { documentType }, - { linesOfCode: document.lineCount }, - ); - } - }); - - const subscriptions = []; - subscriptions.push( - vscode.workspace.onDidOpenTextDocument((document) => { - const documentType = isQsharpNotebookCell(document) - ? QsharpDocumentType.JupyterCell - : isQsharpDocument(document) - ? QsharpDocumentType.Qsharp - : QsharpDocumentType.Other; - if (documentType !== QsharpDocumentType.Other) { - sendTelemetryEvent( - EventType.OpenedDocument, - { documentType }, - { linesOfCode: document.lineCount }, - ); - } - updateIfQsharpDocument(document); - }), - ); - - subscriptions.push( - vscode.workspace.onDidChangeTextDocument((evt) => { - updateIfQsharpDocument(evt.document); - }), - ); - - subscriptions.push( - vscode.workspace.onDidCloseTextDocument((document) => { - if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { - languageService.closeDocument(document.uri.toString()); - } - }), - ); - - // Watch manifest changes and update each document in the same project as the manifest. - subscriptions.push( - vscode.workspace.onDidSaveTextDocument((manifest) => { - updateProjectDocuments(manifest.uri); - }), - ); - - // Trigger an update on all .qs child documents when their manifest is deleted, - // so that they can get reparented to single-file-projects. - subscriptions.push( - vscode.workspace.onDidDeleteFiles((event) => { - event.files.forEach((uri) => { - updateProjectDocuments(uri); - }); - }), - ); - - // Checks if the URI belongs to a qsharp manifest, and updates all - // open documents in the same project as the manifest. - function updateProjectDocuments(manifest: vscode.Uri) { - if (manifest.scheme === "file" && manifest.fsPath.endsWith("qsharp.json")) { - const project_folder = manifest.fsPath.slice( - 0, - manifest.fsPath.length - "qsharp.json".length, - ); - vscode.workspace.textDocuments.forEach((document) => { - if ( - !document.isClosed && - // Check that the document is on the same project as the manifest. - document.fileName.startsWith(project_folder) - ) { - updateIfQsharpDocument(document); - } - }); - } - } - - function updateIfQsharpDocument(document: vscode.TextDocument) { - if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { - // Regular (not notebook) Q# document. - languageService.updateDocument( - document.uri.toString(), - document.version, - document.getText(), - ); - } - } - - return subscriptions; -} - -async function activateLanguageService(extensionUri: vscode.Uri) { - const subscriptions: vscode.Disposable[] = []; - - const languageService = await loadLanguageService(extensionUri); - - // diagnostics - subscriptions.push(...startCheckingQSharp(languageService)); - - // synchronize document contents - subscriptions.push(...registerDocumentUpdateHandlers(languageService)); - - // synchronize notebook cell contents - subscriptions.push( - ...registerQSharpNotebookCellUpdateHandlers(languageService), - ); - - // synchronize configuration - subscriptions.push(registerConfigurationChangeHandlers(languageService)); - - // format document - subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider( - qsharpLanguageId, - createFormattingProvider(languageService), - ), - ); - - // format range - subscriptions.push( - vscode.languages.registerDocumentRangeFormattingEditProvider( - qsharpLanguageId, - createFormattingProvider(languageService), - ), - ); - - // completions - subscriptions.push( - vscode.languages.registerCompletionItemProvider( - qsharpLanguageId, - createCompletionItemProvider(languageService), - // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` - "@", - ".", - ), - ); - - // hover - subscriptions.push( - vscode.languages.registerHoverProvider( - qsharpLanguageId, - createHoverProvider(languageService), - ), - ); - - // go to def - subscriptions.push( - vscode.languages.registerDefinitionProvider( - qsharpLanguageId, - createDefinitionProvider(languageService), - ), - ); - - // find references - subscriptions.push( - vscode.languages.registerReferenceProvider( - qsharpLanguageId, - createReferenceProvider(languageService), - ), - ); - - // signature help - subscriptions.push( - vscode.languages.registerSignatureHelpProvider( - qsharpLanguageId, - createSignatureHelpProvider(languageService), - "(", - ",", - ), - ); - - // rename symbol - subscriptions.push( - vscode.languages.registerRenameProvider( - qsharpLanguageId, - createRenameProvider(languageService), - ), - ); - - // code lens - subscriptions.push( - vscode.languages.registerCodeLensProvider( - qsharpLanguageId, - createCodeLensProvider(languageService), - ), - ); - - subscriptions.push( - vscode.languages.registerCodeActionsProvider( - qsharpLanguageId, - createCodeActionsProvider(languageService), - ), - ); - - // add the language service dispose handler as well - subscriptions.push(languageService); - - return subscriptions; -} - -async function updateLanguageServiceProfile(languageService: ILanguageService) { - const targetProfile = getTarget(); - - switch (targetProfile) { - case "base": - case "adaptive_ri": - case "unrestricted": - break; - default: - log.warn(`Invalid value for target profile: ${targetProfile}`); - } - log.debug("Target profile set to: " + targetProfile); - - languageService.updateConfiguration({ - targetProfile: targetProfile, - lints: [{ lint: "needlessOperation", level: "warn" }], - }); -} - -async function loadLanguageService(baseUri: vscode.Uri) { - const start = performance.now(); - const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); - const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); - await loadWasmModule(wasmBytes); - const languageService = await getLanguageService({ - findManifestDirectory, - readFile, - listDirectory, - resolvePath: async (a, b) => resolvePath(a, b), - fetchGithub: fetchGithubRaw, - }); - await updateLanguageServiceProfile(languageService); - const end = performance.now(); - sendTelemetryEvent( - EventType.LoadLanguageService, - {}, - { timeToStartMs: end - start }, - ); - return languageService; -} - -function registerConfigurationChangeHandlers( - languageService: ILanguageService, -) { - return vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration("Q#.qir.targetProfile")) { - updateLanguageServiceProfile(languageService); - } - }); -} - export class QsTextDocumentContentProvider implements vscode.TextDocumentContentProvider { diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts new file mode 100644 index 0000000000..d4815d8713 --- /dev/null +++ b/vscode/src/language-service/activate.ts @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ILanguageService, + getLanguageService, + loadWasmModule, + log, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { + isQsharpDocument, + isQsharpNotebookCell, + qsharpLanguageId, +} from "../common.js"; +import { getTarget } from "../config.js"; +import { + fetchGithubRaw, + findManifestDirectory, + listDirectory, + readFile, + resolvePath, +} from "../projectSystem.js"; +import { + EventType, + QsharpDocumentType, + sendTelemetryEvent, +} from "../telemetry.js"; +import { createCodeActionsProvider } from "./codeActions.js"; +import { createCodeLensProvider } from "./codeLens.js"; +import { createCompletionItemProvider } from "./completion.js"; +import { createDefinitionProvider } from "./definition.js"; +import { startLanguageServiceDiagnostics } from "./diagnostics.js"; +import { createFormattingProvider } from "./format.js"; +import { createHoverProvider } from "./hover.js"; +import { registerQSharpNotebookCellUpdateHandlers } from "./notebook.js"; +import { createReferenceProvider } from "./references.js"; +import { createRenameProvider } from "./rename.js"; +import { createSignatureHelpProvider } from "./signature.js"; + +export async function activateLanguageService(extensionUri: vscode.Uri) { + const subscriptions: vscode.Disposable[] = []; + + const languageService = await loadLanguageService(extensionUri); + + // diagnostics + subscriptions.push(...startLanguageServiceDiagnostics(languageService)); + + // synchronize document contents + subscriptions.push(...registerDocumentUpdateHandlers(languageService)); + + // synchronize notebook cell contents + subscriptions.push( + ...registerQSharpNotebookCellUpdateHandlers(languageService), + ); + + // synchronize configuration + subscriptions.push(registerConfigurationChangeHandlers(languageService)); + + // format document + subscriptions.push( + vscode.languages.registerDocumentFormattingEditProvider( + qsharpLanguageId, + createFormattingProvider(languageService), + ), + ); + + // format range + subscriptions.push( + vscode.languages.registerDocumentRangeFormattingEditProvider( + qsharpLanguageId, + createFormattingProvider(languageService), + ), + ); + + // completions + subscriptions.push( + vscode.languages.registerCompletionItemProvider( + qsharpLanguageId, + createCompletionItemProvider(languageService), + // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` + "@", + ".", + ), + ); + + // hover + subscriptions.push( + vscode.languages.registerHoverProvider( + qsharpLanguageId, + createHoverProvider(languageService), + ), + ); + + // go to def + subscriptions.push( + vscode.languages.registerDefinitionProvider( + qsharpLanguageId, + createDefinitionProvider(languageService), + ), + ); + + // find references + subscriptions.push( + vscode.languages.registerReferenceProvider( + qsharpLanguageId, + createReferenceProvider(languageService), + ), + ); + + // signature help + subscriptions.push( + vscode.languages.registerSignatureHelpProvider( + qsharpLanguageId, + createSignatureHelpProvider(languageService), + "(", + ",", + ), + ); + + // rename symbol + subscriptions.push( + vscode.languages.registerRenameProvider( + qsharpLanguageId, + createRenameProvider(languageService), + ), + ); + + // code lens + subscriptions.push( + vscode.languages.registerCodeLensProvider( + qsharpLanguageId, + createCodeLensProvider(languageService), + ), + ); + + subscriptions.push( + vscode.languages.registerCodeActionsProvider( + qsharpLanguageId, + createCodeActionsProvider(languageService), + ), + ); + + // add the language service dispose handler as well + subscriptions.push(languageService); + + return subscriptions; +} + +async function loadLanguageService(baseUri: vscode.Uri) { + const start = performance.now(); + const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); + const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); + await loadWasmModule(wasmBytes); + const languageService = await getLanguageService({ + findManifestDirectory, + readFile, + listDirectory, + resolvePath: async (a, b) => resolvePath(a, b), + fetchGithub: fetchGithubRaw, + }); + await updateLanguageServiceProfile(languageService); + const end = performance.now(); + sendTelemetryEvent( + EventType.LoadLanguageService, + {}, + { timeToStartMs: end - start }, + ); + return languageService; +} +function registerDocumentUpdateHandlers(languageService: ILanguageService) { + vscode.workspace.textDocuments.forEach((document) => { + updateIfQsharpDocument(document); + }); + + // we manually send an OpenDocument telemetry event if this is a Q# document, because the + // below subscriptions won't fire for documents that are already open when the extension is activated + vscode.workspace.textDocuments.forEach((document) => { + if (isQsharpDocument(document)) { + const documentType = isQsharpNotebookCell(document) + ? QsharpDocumentType.JupyterCell + : QsharpDocumentType.Qsharp; + sendTelemetryEvent( + EventType.OpenedDocument, + { documentType }, + { linesOfCode: document.lineCount }, + ); + } + }); + + const subscriptions = []; + subscriptions.push( + vscode.workspace.onDidOpenTextDocument((document) => { + const documentType = isQsharpNotebookCell(document) + ? QsharpDocumentType.JupyterCell + : isQsharpDocument(document) + ? QsharpDocumentType.Qsharp + : QsharpDocumentType.Other; + if (documentType !== QsharpDocumentType.Other) { + sendTelemetryEvent( + EventType.OpenedDocument, + { documentType }, + { linesOfCode: document.lineCount }, + ); + } + updateIfQsharpDocument(document); + }), + ); + + subscriptions.push( + vscode.workspace.onDidChangeTextDocument((evt) => { + updateIfQsharpDocument(evt.document); + }), + ); + + subscriptions.push( + vscode.workspace.onDidCloseTextDocument((document) => { + if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { + languageService.closeDocument(document.uri.toString()); + } + }), + ); + + // Watch manifest changes and update each document in the same project as the manifest. + subscriptions.push( + vscode.workspace.onDidSaveTextDocument((manifest) => { + updateProjectDocuments(manifest.uri); + }), + ); + + // Trigger an update on all .qs child documents when their manifest is deleted, + // so that they can get reparented to single-file-projects. + subscriptions.push( + vscode.workspace.onDidDeleteFiles((event) => { + event.files.forEach((uri) => { + updateProjectDocuments(uri); + }); + }), + ); + + // Checks if the URI belongs to a qsharp manifest, and updates all + // open documents in the same project as the manifest. + function updateProjectDocuments(manifest: vscode.Uri) { + if (manifest.scheme === "file" && manifest.fsPath.endsWith("qsharp.json")) { + const project_folder = manifest.fsPath.slice( + 0, + manifest.fsPath.length - "qsharp.json".length, + ); + vscode.workspace.textDocuments.forEach((document) => { + if ( + !document.isClosed && + // Check that the document is on the same project as the manifest. + document.fileName.startsWith(project_folder) + ) { + updateIfQsharpDocument(document); + } + }); + } + } + + function updateIfQsharpDocument(document: vscode.TextDocument) { + if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { + // Regular (not notebook) Q# document. + languageService.updateDocument( + document.uri.toString(), + document.version, + document.getText(), + ); + } + } + + return subscriptions; +} + +function registerConfigurationChangeHandlers( + languageService: ILanguageService, +) { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("Q#.qir.targetProfile")) { + updateLanguageServiceProfile(languageService); + } + }); +} + +async function updateLanguageServiceProfile(languageService: ILanguageService) { + const targetProfile = getTarget(); + + switch (targetProfile) { + case "base": + case "adaptive_ri": + case "unrestricted": + break; + default: + log.warn(`Invalid value for target profile: ${targetProfile}`); + } + log.debug("Target profile set to: " + targetProfile); + + languageService.updateConfiguration({ + targetProfile: targetProfile, + lints: [{ lint: "needlessOperation", level: "warn" }], + }); +} diff --git a/vscode/src/codeActions.ts b/vscode/src/language-service/codeActions.ts similarity index 97% rename from vscode/src/codeActions.ts rename to vscode/src/language-service/codeActions.ts index 03fb7869c7..513f28fe88 100644 --- a/vscode/src/codeActions.ts +++ b/vscode/src/language-service/codeActions.ts @@ -3,7 +3,7 @@ import { ILanguageService, ICodeAction } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeWorkspaceEdit } from "./common"; +import { toVscodeWorkspaceEdit } from "../common"; export function createCodeActionsProvider(languageService: ILanguageService) { return new QSharpCodeActionProvider(languageService); diff --git a/vscode/src/codeLens.ts b/vscode/src/language-service/codeLens.ts similarity index 98% rename from vscode/src/codeLens.ts rename to vscode/src/language-service/codeLens.ts index 7790628105..98672811cb 100644 --- a/vscode/src/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -7,7 +7,7 @@ import { qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; export function createCodeLensProvider(languageService: ILanguageService) { return new QSharpCodeLensProvider(languageService); diff --git a/vscode/src/completion.ts b/vscode/src/language-service/completion.ts similarity index 96% rename from vscode/src/completion.ts rename to vscode/src/language-service/completion.ts index 6526962c0b..92f2fc8bc8 100644 --- a/vscode/src/completion.ts +++ b/vscode/src/language-service/completion.ts @@ -4,8 +4,8 @@ import { ILanguageService, samples } from "qsharp-lang"; import * as vscode from "vscode"; import { CompletionItem } from "vscode"; -import { EventType, sendTelemetryEvent } from "./telemetry"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; +import { EventType, sendTelemetryEvent } from "../telemetry"; export function createCompletionItemProvider( languageService: ILanguageService, diff --git a/vscode/src/definition.ts b/vscode/src/language-service/definition.ts similarity index 94% rename from vscode/src/definition.ts rename to vscode/src/language-service/definition.ts index 0800224b4a..fb2f6a6a23 100644 --- a/vscode/src/definition.ts +++ b/vscode/src/language-service/definition.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { toVscodeLocation } from "./common"; import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; +import { toVscodeLocation } from "../common"; export function createDefinitionProvider(languageService: ILanguageService) { return new QSharpDefinitionProvider(languageService); diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts new file mode 100644 index 0000000000..cbd0e9ac1d --- /dev/null +++ b/vscode/src/language-service/diagnostics.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ILanguageService, + VSDiagnostic, + qsharpLibraryUriScheme, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "../common"; + +export function startLanguageServiceDiagnostics( + languageService: ILanguageService, +): vscode.Disposable[] { + const diagCollection = + vscode.languages.createDiagnosticCollection(qsharpLanguageId); + + async function onDiagnostics(evt: { + detail: { + uri: string; + version: number; + diagnostics: VSDiagnostic[]; + }; + }) { + const diagnostics = evt.detail; + const uri = vscode.Uri.parse(diagnostics.uri); + + if (uri.scheme === qsharpLibraryUriScheme) { + // Don't report diagnostics for library files. + return; + } + + diagCollection.set( + uri, + diagnostics.diagnostics.map((d) => toVsCodeDiagnostic(d)), + ); + } + + languageService.addEventListener("diagnostics", onDiagnostics); + + return [ + { + dispose: () => { + languageService.removeEventListener("diagnostics", onDiagnostics); + }, + }, + diagCollection, + ]; +} diff --git a/vscode/src/format.ts b/vscode/src/language-service/format.ts similarity index 92% rename from vscode/src/format.ts rename to vscode/src/language-service/format.ts index 71765f7a89..fb9275dfd5 100644 --- a/vscode/src/format.ts +++ b/vscode/src/language-service/format.ts @@ -3,9 +3,9 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; -import { EventType, FormatEvent, sendTelemetryEvent } from "./telemetry"; -import { getRandomGuid } from "./utils"; +import { toVscodeRange } from "../common"; +import { EventType, FormatEvent, sendTelemetryEvent } from "../telemetry"; +import { getRandomGuid } from "../utils"; export function createFormattingProvider(languageService: ILanguageService) { return new QSharpFormattingProvider(languageService); diff --git a/vscode/src/hover.ts b/vscode/src/language-service/hover.ts similarity index 94% rename from vscode/src/hover.ts rename to vscode/src/language-service/hover.ts index 6cab20afad..4307174099 100644 --- a/vscode/src/hover.ts +++ b/vscode/src/language-service/hover.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "./common"; +import { toVscodeRange } from "../common"; export function createHoverProvider(languageService: ILanguageService) { return new QSharpHoverProvider(languageService); diff --git a/vscode/src/language-service/notebook.ts b/vscode/src/language-service/notebook.ts new file mode 100644 index 0000000000..c17e542501 --- /dev/null +++ b/vscode/src/language-service/notebook.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ILanguageService, log } from "qsharp-lang"; +import * as vscode from "vscode"; +import { isQsharpNotebookCell } from "../common.js"; +import { findQSharpCellMagic, jupyterNotebookType } from "../notebook.js"; + +const qsharpConfigMimeType = "application/x.qsharp-config"; + +const openQSharpNotebooks = new Set(); + +/** + * Document update handlers for syncing notebook cell contents with the language service. + */ +export function registerQSharpNotebookCellUpdateHandlers( + languageService: ILanguageService, +) { + vscode.workspace.notebookDocuments.forEach((notebook) => { + updateIfQsharpNotebook(notebook); + }); + + const subscriptions = []; + subscriptions.push( + vscode.workspace.onDidOpenNotebookDocument((notebook) => { + updateIfQsharpNotebook(notebook); + }), + ); + + subscriptions.push( + vscode.workspace.onDidChangeNotebookDocument((event) => { + updateIfQsharpNotebook(event.notebook); + }), + ); + + subscriptions.push( + vscode.workspace.onDidCloseNotebookDocument((notebook) => { + closeIfKnownQsharpNotebook(notebook); + }), + ); + + function updateIfQsharpNotebook(notebook: vscode.NotebookDocument) { + if (notebook.notebookType === jupyterNotebookType) { + const qsharpMetadata = getQSharpConfigMetadata(notebook); + const qsharpCells = getQSharpCells(notebook); + const notebookUri = notebook.uri.toString(); + if (qsharpCells.length > 0) { + openQSharpNotebooks.add(notebookUri); + languageService.updateNotebookDocument( + notebookUri, + notebook.version, + qsharpMetadata, + qsharpCells.map((cell) => { + return { + uri: cell.document.uri.toString(), + version: cell.document.version, + code: getQSharpText(cell.document), + }; + }), + ); + } else { + // All Q# cells could have been deleted, check if we know this doc from previous calls + closeIfKnownQsharpNotebook(notebook); + } + } + } + + function closeIfKnownQsharpNotebook(notebook: vscode.NotebookDocument) { + const notebookUri = notebook.uri.toString(); + if (openQSharpNotebooks.has(notebookUri)) { + languageService.closeNotebookDocument(notebookUri); + openQSharpNotebooks.delete(notebook.uri.toString()); + } + } + + function getQSharpCells(notebook: vscode.NotebookDocument) { + return notebook + .getCells() + .filter((cell) => isQsharpNotebookCell(cell.document)); + } + + function getQSharpText(document: vscode.TextDocument) { + const magicRange = findQSharpCellMagic(document); + if (magicRange) { + const magicStartOffset = document.offsetAt(magicRange.start); + const magicEndOffset = document.offsetAt(magicRange.end); + // Erase the %%qsharp magic line if it's there. + // Replace it with a comment so that document offsets remain the same. + // This will save us from having to map offsets later when + // communicating with the language service. + const text = document.getText(); + return ( + text.substring(0, magicStartOffset) + + "//qsharp" + + text.substring(magicEndOffset) + ); + } else { + // No %%qsharp magic. This can happen if the user manually sets the + // cell language to Q#. Python won't recognize the cell as a Q# cell, + // so this will fail at runtime, but as the language service we respect + // the manually set cell language, so we treat this as any other + // Q# cell. We could consider raising a warning here to help the user. + log.info( + "found Q# cell without %%qsharp magic: " + document.uri.toString(), + ); + return document.getText(); + } + } + + return subscriptions; +} + +/** + * Finds an output cell that contains an item with the Q# config MIME type, + * and returns the data from it. This data and is generated by the execution of a + * `qsharp.init()` call. It's Q# configuration data to be passed + * to the language service as "notebook metadata". + */ +function getQSharpConfigMetadata(notebook: vscode.NotebookDocument): object { + const data = notebook + .getCells() + .flatMap((cell) => cell.outputs) + .flatMap((output) => output.items) + .find((item) => { + return item.mime === qsharpConfigMimeType; + })?.data; + + if (data) { + const dataString = new TextDecoder().decode(data); + log.trace("found Q# config metadata: " + dataString); + return JSON.parse(dataString); + } else { + return {}; + } +} diff --git a/vscode/src/references.ts b/vscode/src/language-service/references.ts similarity index 95% rename from vscode/src/references.ts rename to vscode/src/language-service/references.ts index 7531cff9f4..528038c189 100644 --- a/vscode/src/references.ts +++ b/vscode/src/language-service/references.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation } from "./common"; +import { toVscodeLocation } from "../common"; export function createReferenceProvider(languageService: ILanguageService) { return new QSharpReferenceProvider(languageService); diff --git a/vscode/src/rename.ts b/vscode/src/language-service/rename.ts similarity index 95% rename from vscode/src/rename.ts rename to vscode/src/language-service/rename.ts index 0569a73801..02060ab4f5 100644 --- a/vscode/src/rename.ts +++ b/vscode/src/language-service/rename.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange, toVscodeWorkspaceEdit } from "./common"; +import { toVscodeRange, toVscodeWorkspaceEdit } from "../common"; export function createRenameProvider(languageService: ILanguageService) { return new QSharpRenameProvider(languageService); diff --git a/vscode/src/signature.ts b/vscode/src/language-service/signature.ts similarity index 100% rename from vscode/src/signature.ts rename to vscode/src/language-service/signature.ts diff --git a/vscode/src/notebook.ts b/vscode/src/notebook.ts index 2d370b0be9..5e53cb54bd 100644 --- a/vscode/src/notebook.ts +++ b/vscode/src/notebook.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ILanguageService, log } from "qsharp-lang"; +import { log } from "qsharp-lang"; import * as vscode from "vscode"; -import { isQsharpNotebookCell, qsharpLanguageId } from "./common.js"; import { WorkspaceTreeProvider } from "./azure/treeView.js"; import { getPythonCodeForWorkspace } from "./azure/workspaceActions.js"; +import { qsharpLanguageId } from "./common.js"; import { notebookTemplate } from "./notebookTemplate.js"; const qsharpCellMagic = "%%qsharp"; -const jupyterNotebookType = "jupyter-notebook"; -const qsharpConfigMimeType = "application/x.qsharp-config"; +export const jupyterNotebookType = "jupyter-notebook"; let defaultLanguageId: string | undefined; /** @@ -97,13 +96,11 @@ export function registerQSharpNotebookHandlers() { return subscriptions; } -const openQSharpNotebooks = new Set(); - /** * Returns the range of the `%%qsharp` cell magic, or `undefined` * if it does not exist. */ -function findQSharpCellMagic(document: vscode.TextDocument) { +export function findQSharpCellMagic(document: vscode.TextDocument) { // Ignore whitespace before the cell magic for (let i = 0; i < document.lineCount; i++) { const line = document.lineAt(i); @@ -126,130 +123,6 @@ function findQSharpCellMagic(document: vscode.TextDocument) { return undefined; } -/** - * This one is for syncing with the language service - */ -export function registerQSharpNotebookCellUpdateHandlers( - languageService: ILanguageService, -) { - vscode.workspace.notebookDocuments.forEach((notebook) => { - updateIfQsharpNotebook(notebook); - }); - - const subscriptions = []; - subscriptions.push( - vscode.workspace.onDidOpenNotebookDocument((notebook) => { - updateIfQsharpNotebook(notebook); - }), - ); - - subscriptions.push( - vscode.workspace.onDidChangeNotebookDocument((event) => { - updateIfQsharpNotebook(event.notebook); - }), - ); - - subscriptions.push( - vscode.workspace.onDidCloseNotebookDocument((notebook) => { - closeIfKnownQsharpNotebook(notebook); - }), - ); - - function updateIfQsharpNotebook(notebook: vscode.NotebookDocument) { - if (notebook.notebookType === jupyterNotebookType) { - const qsharpMetadata = getQSharpConfigMetadata(notebook); - const qsharpCells = getQSharpCells(notebook); - const notebookUri = notebook.uri.toString(); - if (qsharpCells.length > 0) { - openQSharpNotebooks.add(notebookUri); - languageService.updateNotebookDocument( - notebookUri, - notebook.version, - qsharpMetadata, - qsharpCells.map((cell) => { - return { - uri: cell.document.uri.toString(), - version: cell.document.version, - code: getQSharpText(cell.document), - }; - }), - ); - } else { - // All Q# cells could have been deleted, check if we know this doc from previous calls - closeIfKnownQsharpNotebook(notebook); - } - } - } - - function closeIfKnownQsharpNotebook(notebook: vscode.NotebookDocument) { - const notebookUri = notebook.uri.toString(); - if (openQSharpNotebooks.has(notebookUri)) { - languageService.closeNotebookDocument(notebookUri); - openQSharpNotebooks.delete(notebook.uri.toString()); - } - } - - function getQSharpCells(notebook: vscode.NotebookDocument) { - return notebook - .getCells() - .filter((cell) => isQsharpNotebookCell(cell.document)); - } - - function getQSharpText(document: vscode.TextDocument) { - const magicRange = findQSharpCellMagic(document); - if (magicRange) { - const magicStartOffset = document.offsetAt(magicRange.start); - const magicEndOffset = document.offsetAt(magicRange.end); - // Erase the %%qsharp magic line if it's there. - // Replace it with a comment so that document offsets remain the same. - // This will save us from having to map offsets later when - // communicating with the language service. - const text = document.getText(); - return ( - text.substring(0, magicStartOffset) + - "//qsharp" + - text.substring(magicEndOffset) - ); - } else { - // No %%qsharp magic. This can happen if the user manually sets the - // cell language to Q#. Python won't recognize the cell as a Q# cell, - // so this will fail at runtime, but as the language service we respect - // the manually set cell language, so we treat this as any other - // Q# cell. We could consider raising a warning here to help the user. - log.info( - "found Q# cell without %%qsharp magic: " + document.uri.toString(), - ); - return document.getText(); - } - } - - return subscriptions; -} - -/** - * Finds an output cell that contains an item with the Q# config MIME type, - * and returns the data from it. This data and is generated by the execution of a - * `qsharp.init()` call. It's Q# configuration data to be passed - * to the language service as "notebook metadata". - */ -function getQSharpConfigMetadata(notebook: vscode.NotebookDocument): object { - const data = notebook - .getCells() - .flatMap((cell) => cell.outputs) - .flatMap((output) => output.items) - .find((item) => { - return item.mime === qsharpConfigMimeType; - })?.data; - - if (data) { - const dataString = new TextDecoder().decode(data); - log.trace("found Q# config metadata: " + dataString); - return JSON.parse(dataString); - } else { - return {}; - } -} - // Yes, this function is long, but mostly to deal with multi-folder VS Code workspace or multi // Azure Quantum workspace connection scenarios. The actual notebook creation is pretty simple. export function registerCreateNotebookCommand( From b43d102d609e9d7aabf31870f846249887c13481 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 10 Dec 2024 11:23:09 -0800 Subject: [PATCH 2/4] Switch QIR Target checking from allow list to deny list (#2058) This change updates the logic we use to decide if a target supports QIR submission in the extension. Previously it was an allow list which required any new targets or providers to be explicitly added. Instead, this uses a deny list style so that new providers and targets are allowed to submit either base or adaptive without requiring updates to the QDK. Then we can coordinate with new partners on what targets should be lit up for which purposes. --- vscode/src/azure/providerProperties.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vscode/src/azure/providerProperties.ts b/vscode/src/azure/providerProperties.ts index e8b2176279..81f7191e9d 100644 --- a/vscode/src/azure/providerProperties.ts +++ b/vscode/src/azure/providerProperties.ts @@ -13,9 +13,8 @@ export function targetSupportQir(target: string) { // Note: Most of these should be dynamic at some point, with configuration coming // from the service, and able to be overridden by settings. return ( - target.startsWith("ionq") || - target.startsWith("quantinuum") || - target.startsWith("rigetti") + !(target == "microsoft.estimator") && + !(target.startsWith("microsoft") && target.endsWith("cpu")) ); } @@ -28,5 +27,5 @@ export function shouldExcludeProvider(provider: string) { } export function supportsAdaptive(target: string) { - return target.startsWith("quantinuum"); + return !target.startsWith("ionq") && !target.startsWith("rigetti"); } From 609bef54d338d736b57df476aa998c200cc111a4 Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Thu, 12 Dec 2024 10:09:25 -0800 Subject: [PATCH 3/4] Move to Python 3.9 as min ver and Ubuntu 22.04 min compat ver (#2061) Fixes #1946 - Cross build image is now set to CBL Mariner with glibc 2.35 and Python 3.9 - Manylinux tag is now `manylinux_2_35_x86_64` - A docker image is no longer built, we directly use the Mariner Python base image. --- .ado/publish.yml | 8 +++----- docker/linux-aarch64/Dockerfile | 19 ------------------- docker/linux-aarch64/build.sh | 17 ----------------- docker/linux-aarch64/entrypoint.sh | 2 +- docker/linux-aarch64/run.sh | 8 ++++---- fuzz/README.md | 4 ++-- jupyterlab/pyproject.toml | 4 ++-- pip/Cargo.toml | 4 ++-- pip/pyproject.toml | 5 +++-- 9 files changed, 17 insertions(+), 54 deletions(-) delete mode 100644 docker/linux-aarch64/Dockerfile delete mode 100644 docker/linux-aarch64/build.sh diff --git a/.ado/publish.yml b/.ado/publish.yml index 50d13d7d4c..440052ab29 100644 --- a/.ado/publish.yml +++ b/.ado/publish.yml @@ -34,13 +34,13 @@ parameters: default: - name: linux_x86_64 poolName: 'Azure-Pipelines-DevTools-EO' - imageName: 'ubuntu-20.04' + imageName: 'ubuntu-22.04' os: linux arch: x86_64 additionalTargets: wasm32-unknown-unknown - name: linux_aarch64 poolName: 'Azure-Pipelines-DevTools-EO' - imageName: 'ubuntu-20.04' + imageName: 'ubuntu-22.04' os: linux arch: aarch64 additionalRustTargets: aarch64-unknown-linux-gnu wasm32-unknown-unknown @@ -299,7 +299,7 @@ extends: python -m pip install auditwheel patchelf ls target/wheels ls target/wheels/*.whl | xargs auditwheel show - ls target/wheels/*.whl | xargs auditwheel repair --wheel-dir ./target/wheels/ --plat manylinux_2_31_x86_64 + ls target/wheels/*.whl | xargs auditwheel repair --wheel-dir ./target/wheels/ --plat manylinux_2_35_x86_64 rm target/wheels/*-linux_x86_64.whl ls target/wheels displayName: Run auditwheel for Linux Wheels @@ -313,10 +313,8 @@ extends: condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64')) - script: | - chmod +x ./docker/linux-aarch64/build.sh chmod +x ./docker/linux-aarch64/run.sh - ./docker/linux-aarch64/build.sh ./docker/linux-aarch64/run.sh displayName: Run auditwheel and python tests for Linux aarch64 Wheels condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64')) diff --git a/docker/linux-aarch64/Dockerfile b/docker/linux-aarch64/Dockerfile deleted file mode 100644 index 9da3e9cf58..0000000000 --- a/docker/linux-aarch64/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -ARG BASE_IMAGE -FROM --platform=linux/arm64/v8 ${BASE_IMAGE} - -# install python and pip -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install \ - python3-minimal python3-pip python3-venv \ - --no-install-recommends -y && \ - apt-get clean - -# We don't update pip here as we need to update it -# inside the virtual environment. Otherwise, we get two versions -# of pip installed, and the one outside the virtual environment -# causes problems. - -ENTRYPOINT ["sh", "-c", "$*", "--"] diff --git a/docker/linux-aarch64/build.sh b/docker/linux-aarch64/build.sh deleted file mode 100644 index 5bc33ea207..0000000000 --- a/docker/linux-aarch64/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -set -e - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -echo "SCRIPT_DIR: ${SCRIPT_DIR}" - -BASE_IMAGE="${BASE_IMAGE:-ubuntu:20.04}" -echo "BASE_IMAGE: ${BASE_IMAGE}" - -TAG="${TAG:-qsharp-linux-aarch64-runner}" -echo "TAG: ${TAG}" - -docker build -t ${TAG} --build-arg BASE_IMAGE=${BASE_IMAGE} -f ${SCRIPT_DIR}/Dockerfile ${SCRIPT_DIR} diff --git a/docker/linux-aarch64/entrypoint.sh b/docker/linux-aarch64/entrypoint.sh index 0381036638..3a555b1188 100644 --- a/docker/linux-aarch64/entrypoint.sh +++ b/docker/linux-aarch64/entrypoint.sh @@ -11,7 +11,7 @@ echo "SCRIPT_DIR: ${SCRIPT_DIR}" WHEEL_ARCH="${WHEEL_ARCH:-aarch64}" echo "WHEEL_ARCH: ${WHEEL_ARCH}" -WHEEL_PLATFORM="${WHEEL_PLATFORM:-manylinux_2_31_${WHEEL_ARCH}}" +WHEEL_PLATFORM="${WHEEL_PLATFORM:-manylinux_2_35_${WHEEL_ARCH}}" echo "WHEEL_PLATFORM: ${WHEEL_PLATFORM}" PIP_DIR="${PIP_DIR:-${SCRIPT_DIR}/../../pip}" diff --git a/docker/linux-aarch64/run.sh b/docker/linux-aarch64/run.sh index 5387fba6fa..6b6302f9d9 100644 --- a/docker/linux-aarch64/run.sh +++ b/docker/linux-aarch64/run.sh @@ -8,11 +8,11 @@ set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) echo "SCRIPT_DIR: ${SCRIPT_DIR}" -TAG="${TAG:-qsharp-linux-aarch64-runner}" -echo "TAG: ${TAG}" +BASE_IMAGE="${BASE_IMAGE:-mcr.microsoft.com/cbl-mariner/base/python:3.9}" +echo "BASE_IMAGE: ${BASE_IMAGE}" VOLUME_ROOT=$(realpath ${SCRIPT_DIR}/../..) echo "VOLUME_ROOT: ${VOLUME_ROOT}" -echo "docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${TAG} bash /qsharp/docker/linux-aarch64/entrypoint.sh" -docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${TAG} bash /qsharp/docker/linux-aarch64/entrypoint.sh +echo "docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${BASE_IMAGE} bash /qsharp/docker/linux-aarch64/entrypoint.sh" +docker run --platform linux/arm64/v8 -v ${VOLUME_ROOT}:/qsharp -e WHEEL_DIR='/qsharp/target/wheels' ${BASE_IMAGE} bash /qsharp/docker/linux-aarch64/entrypoint.sh diff --git a/fuzz/README.md b/fuzz/README.md index c010652b91..1c58cb80c0 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -4,7 +4,7 @@ Based on [Fuzzing with cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.h For running locally you need the following steps. -(**On Windows use [WSL](https://learn.microsoft.com/windows/wsl/).** Tested in WSL Ubuntu 20.04) +(**On Windows use [WSL](https://learn.microsoft.com/windows/wsl/).** Tested in WSL Ubuntu 22.04) ## Prerequisites @@ -329,7 +329,7 @@ See more in [LibFuzzer Corpus](https://llvm.org/docs/LibFuzzer.html#corpus). Based on [Code Coverage](https://rust-fuzz.github.io/book/cargo-fuzz/coverage.html#code-coverage). -Tested in WSL Ubuntu 20.04. +Tested in WSL Ubuntu 22.04. ### Code Coverage Prerequisites diff --git a/jupyterlab/pyproject.toml b/jupyterlab/pyproject.toml index ae838c05d7..8ed1e5bc10 100644 --- a/jupyterlab/pyproject.toml +++ b/jupyterlab/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "qsharp-jupyterlab" version = "0.0.0" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Framework :: Jupyter", "Framework :: Jupyter :: JupyterLab", @@ -16,10 +16,10 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ ] diff --git a/pip/Cargo.toml b/pip/Cargo.toml index e032dfcfd5..004ed6d939 100644 --- a/pip/Cargo.toml +++ b/pip/Cargo.toml @@ -27,12 +27,12 @@ workspace = true allocator = { path = "../allocator" } [target.'cfg(not(any(target_os = "windows")))'.dependencies] -pyo3 = { workspace = true, features = ["abi3-py38", "extension-module", "num-bigint", "num-complex"] } +pyo3 = { workspace = true, features = ["abi3-py39", "extension-module", "num-bigint", "num-complex"] } [target.'cfg(any(target_os = "windows"))'.dependencies] # generate-import-lib: skip requiring Python 3 distribution # files to be present on the (cross-)compile host system. -pyo3 = { workspace = true, features = ["abi3-py38", "extension-module", "generate-import-lib", "num-bigint", "num-complex"] } +pyo3 = { workspace = true, features = ["abi3-py39", "extension-module", "generate-import-lib", "num-bigint", "num-complex"] } [lib] crate-type = ["cdylib"] diff --git a/pip/pyproject.toml b/pip/pyproject.toml index 62b0393cab..0ca5d6c485 100644 --- a/pip/pyproject.toml +++ b/pip/pyproject.toml @@ -1,15 +1,16 @@ [project] name = "qsharp" version = "0.0.0" -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "License :: OSI Approved :: MIT License", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python", "Programming Language :: Rust", "Operating System :: MacOS", From 502ae34eebf4d8b4570be7d798d8547eb8b6965e Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Thu, 12 Dec 2024 11:06:14 -0800 Subject: [PATCH 4/4] Make it easier to access dumps and matrices from Python (#2042) This adds some convenience accessors for getting dumps, messages, and matrices as separate entries from an invocation of `qsharp.run` and `qsharp.eval`. Previously, the only way to get a state dump from a run was the awkward: ```python state = qsharp.StateDump(qsharp.run("DumpMachine()", shots=1, save_events=True)[0]["events"][0].state_dump()) ``` This change preserves the existings "events" entry in the saved output, which has everything intermingled in the order from each shot, but also introduces dumps, messages, and matrices that will keep just the ordered output of that type. This makes the above pattern slightly better (and more discoverable): ```python state = qsharp.run("DumpMachine()", shots=1, save_events=True)[0]["dumps"][0] ``` This adds similar functionality to `qsharp.eval` which now supports `save_events=True` to capture output, so for single shot execution you can use: ```python state = qsharp.eval("DumpMachine()", save_events=True)["dumps"][0] ``` --- pip/qsharp/_native.pyi | 3 + pip/qsharp/_qsharp.py | 223 +++++++++++++++++++++++---------------- pip/src/interpreter.rs | 12 +++ pip/tests/test_qsharp.py | 33 +++++- 4 files changed, 180 insertions(+), 91 deletions(-) diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 0bf47a2d5b..adef263e44 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -262,6 +262,9 @@ class Output: def __str__(self) -> str: ... def _repr_markdown_(self) -> Optional[str]: ... def state_dump(self) -> Optional[StateDumpData]: ... + def is_state_dump(self) -> bool: ... + def is_matrix(self) -> bool: ... + def is_message(self) -> bool: ... class StateDumpData: """ diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 84a083c770..5bf6ba4f1a 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -219,18 +219,133 @@ def get_interpreter() -> Interpreter: return _interpreter -def eval(source: str) -> Any: +class StateDump: + """ + A state dump returned from the Q# interpreter. + """ + + """ + The number of allocated qubits at the time of the dump. + """ + qubit_count: int + + __inner: dict + __data: StateDumpData + + def __init__(self, data: StateDumpData): + self.__data = data + self.__inner = data.get_dict() + self.qubit_count = data.qubit_count + + def __getitem__(self, index: int) -> complex: + return self.__inner.__getitem__(index) + + def __iter__(self): + return self.__inner.__iter__() + + def __len__(self) -> int: + return len(self.__inner) + + def __repr__(self) -> str: + return self.__data.__repr__() + + def __str__(self) -> str: + return self.__data.__str__() + + def _repr_markdown_(self) -> str: + return self.__data._repr_markdown_() + + def check_eq( + self, state: Union[Dict[int, complex], List[complex]], tolerance: float = 1e-10 + ) -> bool: + """ + Checks if the state dump is equal to the given state. This is not mathematical equality, + as the check ignores global phase. + + :param state: The state to check against, provided either as a dictionary of state indices to complex amplitudes, + or as a list of real amplitudes. + :param tolerance: The tolerance for the check. Defaults to 1e-10. + """ + phase = None + # Convert a dense list of real amplitudes to a dictionary of state indices to complex amplitudes + if isinstance(state, list): + state = {i: val for i, val in enumerate(state)} + # Filter out zero states from the state dump and the given state based on tolerance + state = {k: v for k, v in state.items() if abs(v) > tolerance} + inner_state = {k: v for k, v in self.__inner.items() if abs(v) > tolerance} + if len(state) != len(inner_state): + return False + for key in state: + if key not in inner_state: + return False + if phase is None: + # Calculate the phase based on the first state pair encountered. + # Every pair of states after this must have the same phase for the states to be equivalent. + phase = inner_state[key] / state[key] + elif abs(phase - inner_state[key] / state[key]) > tolerance: + # This pair of states does not have the same phase, + # within tolerance, so the equivalence check fails. + return False + return True + + def as_dense_state(self) -> List[complex]: + """ + Returns the state dump as a dense list of complex amplitudes. This will include zero amplitudes. + """ + return [self.__inner.get(i, complex(0)) for i in range(2**self.qubit_count)] + + +class ShotResult(TypedDict): + """ + A single result of a shot. + """ + + events: List[Output] + result: Any + messages: List[str] + matrices: List[Output] + dumps: List[StateDump] + + +def eval( + source: str, + *, + save_events: bool = False, +) -> Any: """ Evaluates Q# source code. Output is printed to console. :param source: The Q# source code to evaluate. - :returns value: The value returned by the last statement in the source code. + :param save_events: If true, all output will be saved and returned. If false, they will be printed. + :returns value: The value returned by the last statement in the source code or the saved output if `save_events` is true. :raises QSharpError: If there is an error evaluating the source code. """ ipython_helper() + results: ShotResult = { + "events": [], + "result": None, + "messages": [], + "matrices": [], + "dumps": [], + } + + def on_save_events(output: Output) -> None: + # Append the output to the last shot's output list + if output.is_matrix(): + results["events"].append(output) + results["matrices"].append(output) + elif output.is_state_dump(): + state_dump = StateDump(output.state_dump()) + results["events"].append(state_dump) + results["dumps"].append(state_dump) + elif output.is_message(): + stringified = str(output) + results["events"].append(stringified) + results["messages"].append(stringified) + def callback(output: Output) -> None: if _in_jupyter: try: @@ -244,21 +359,17 @@ def callback(output: Output) -> None: telemetry_events.on_eval() start_time = monotonic() - results = get_interpreter().interpret(source, callback) + results["result"] = get_interpreter().interpret( + source, on_save_events if save_events else callback + ) durationMs = (monotonic() - start_time) * 1000 telemetry_events.on_eval_end(durationMs) - return results - - -class ShotResult(TypedDict): - """ - A single result of a shot. - """ - - events: List[Output] - result: Any + if save_events: + return results + else: + return results["result"] def run( @@ -315,9 +426,17 @@ def print_output(output: Output) -> None: def on_save_events(output: Output) -> None: # Append the output to the last shot's output list results[-1]["events"].append(output) + if output.is_matrix(): + results[-1]["matrices"].append(output) + elif output.is_state_dump(): + results[-1]["dumps"].append(StateDump(output.state_dump())) + elif output.is_message(): + results[-1]["messages"].append(str(output)) for shot in range(shots): - results.append({"result": None, "events": []}) + results.append( + {"result": None, "events": [], "messages": [], "matrices": [], "dumps": []} + ) run_results = get_interpreter().run( entry_expr, on_save_events if save_events else print_output, @@ -482,82 +601,6 @@ def set_classical_seed(seed: Optional[int]) -> None: get_interpreter().set_classical_seed(seed) -class StateDump: - """ - A state dump returned from the Q# interpreter. - """ - - """ - The number of allocated qubits at the time of the dump. - """ - qubit_count: int - - __inner: dict - __data: StateDumpData - - def __init__(self, data: StateDumpData): - self.__data = data - self.__inner = data.get_dict() - self.qubit_count = data.qubit_count - - def __getitem__(self, index: int) -> complex: - return self.__inner.__getitem__(index) - - def __iter__(self): - return self.__inner.__iter__() - - def __len__(self) -> int: - return len(self.__inner) - - def __repr__(self) -> str: - return self.__data.__repr__() - - def __str__(self) -> str: - return self.__data.__str__() - - def _repr_markdown_(self) -> str: - return self.__data._repr_markdown_() - - def check_eq( - self, state: Union[Dict[int, complex], List[complex]], tolerance: float = 1e-10 - ) -> bool: - """ - Checks if the state dump is equal to the given state. This is not mathematical equality, - as the check ignores global phase. - - :param state: The state to check against, provided either as a dictionary of state indices to complex amplitudes, - or as a list of real amplitudes. - :param tolerance: The tolerance for the check. Defaults to 1e-10. - """ - phase = None - # Convert a dense list of real amplitudes to a dictionary of state indices to complex amplitudes - if isinstance(state, list): - state = {i: state[i] for i in range(len(state))} - # Filter out zero states from the state dump and the given state based on tolerance - state = {k: v for k, v in state.items() if abs(v) > tolerance} - inner_state = {k: v for k, v in self.__inner.items() if abs(v) > tolerance} - if len(state) != len(inner_state): - return False - for key in state: - if key not in inner_state: - return False - if phase is None: - # Calculate the phase based on the first state pair encountered. - # Every pair of states after this must have the same phase for the states to be equivalent. - phase = inner_state[key] / state[key] - elif abs(phase - inner_state[key] / state[key]) > tolerance: - # This pair of states does not have the same phase, - # within tolerance, so the equivalence check fails. - return False - return True - - def as_dense_state(self) -> List[complex]: - """ - Returns the state dump as a dense list of complex amplitudes. This will include zero amplitudes. - """ - return [self.__inner.get(i, complex(0)) for i in range(2**self.qubit_count)] - - def dump_machine() -> StateDump: """ Returns the sparse state vector of the simulator as a StateDump object. diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 78b4b00a10..f540360f77 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -587,6 +587,18 @@ impl Output { DisplayableOutput::Matrix(_) | DisplayableOutput::Message(_) => None, } } + + fn is_state_dump(&self) -> bool { + matches!(&self.0, DisplayableOutput::State(_)) + } + + fn is_matrix(&self) -> bool { + matches!(&self.0, DisplayableOutput::Matrix(_)) + } + + fn is_message(&self) -> bool { + matches!(&self.0, DisplayableOutput::Message(_)) + } } #[pyclass] diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 0779972fa5..a1f9892587 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -35,6 +35,35 @@ def test_stdout_multiple_lines() -> None: assert f.getvalue() == "STATE:\n|0⟩: 1.0000+0.0000𝑖\nHello!\n" +def test_captured_stdout() -> None: + qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) + f = io.StringIO() + with redirect_stdout(f): + result = qsharp.eval( + '{Message("Hello, world!"); Message("Goodbye!")}', save_events=True + ) + assert f.getvalue() == "" + assert len(result["messages"]) == 2 + assert result["messages"][0] == "Hello, world!" + assert result["messages"][1] == "Goodbye!" + + +def test_captured_matrix() -> None: + qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) + f = io.StringIO() + with redirect_stdout(f): + result = qsharp.eval( + "Std.Diagnostics.DumpOperation(1, qs => H(qs[0]))", + save_events=True, + ) + assert f.getvalue() == "" + assert len(result["matrices"]) == 1 + assert ( + str(result["matrices"][0]) + == "MATRIX:\n 0.7071+0.0000𝑖 0.7071+0.0000𝑖\n 0.7071+0.0000𝑖 −0.7071+0.0000𝑖" + ) + + def test_quantum_seed() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) qsharp.set_quantum_seed(42) @@ -257,6 +286,7 @@ def test_dump_operation() -> None: else: assert res[i][j] == complex(0.0, 0.0) + def test_run_with_noise_produces_noisy_results() -> None: qsharp.init() qsharp.set_quantum_seed(0) @@ -273,6 +303,7 @@ def test_run_with_noise_produces_noisy_results() -> None: ) assert result[0] > 5 + def test_compile_qir_input_data() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Base) qsharp.eval("operation Program() : Result { use q = Qubit(); return M(q) }") @@ -324,7 +355,7 @@ def on_result(result): results = qsharp.run("Foo()", 3, on_result=on_result, save_events=True) assert ( str(results) - == "[{'result': Zero, 'events': [Hello, world!]}, {'result': Zero, 'events': [Hello, world!]}, {'result': Zero, 'events': [Hello, world!]}]" + == "[{'result': Zero, 'events': [Hello, world!], 'messages': ['Hello, world!'], 'matrices': [], 'dumps': []}, {'result': Zero, 'events': [Hello, world!], 'messages': ['Hello, world!'], 'matrices': [], 'dumps': []}, {'result': Zero, 'events': [Hello, world!], 'messages': ['Hello, world!'], 'matrices': [], 'dumps': []}]" ) stdout = capsys.readouterr().out assert stdout == ""