Skip to content

Language service JavaScript API and web worker #426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
276864f
WASM wrapper for language service
minestarks Jun 15, 2023
221cc9b
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 15, 2023
7d6875e
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 15, 2023
9b3a504
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 15, 2023
a7420ec
Move worker stuff into compiler subfolder
minestarks Jun 15, 2023
9a2a2da
Move all the compiler related files to a subfolder
minestarks Jun 16, 2023
a55ce99
Add QSharpLanguageService to npm package
minestarks Jun 16, 2023
f4d2b00
Add language service web worker
minestarks Jun 16, 2023
fdddf90
tests passing
minestarks Jun 22, 2023
64c3201
refactor event stuff
minestarks Jun 22, 2023
62752b0
Clean up requests too
minestarks Jun 22, 2023
c1971cf
cleaned up almost everything
minestarks Jun 22, 2023
e296151
bit more cleanup
minestarks Jun 22, 2023
f8e5866
Really clean it up
minestarks Jun 23, 2023
3b4ba81
Add language service
minestarks Jun 23, 2023
5314af0
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 23, 2023
93c3a71
update .eslintrc
minestarks Jun 23, 2023
144f290
revert package.json
minestarks Jun 23, 2023
b7e5e23
Fix npm entrypoint
minestarks Jun 23, 2023
b49bea6
Update README.md
minestarks Jun 23, 2023
0345a2a
Remove TODO and run prettier
minestarks Jun 23, 2023
1b03fa3
Add dispose()
minestarks Jun 23, 2023
dad7af8
Merge branch 'main' into minestarks/language-service-webworker
minestarks Jun 23, 2023
f904b55
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 23, 2023
4fc0d06
Revert accidental change
minestarks Jun 23, 2023
3569eb9
Remove TODO
minestarks Jun 24, 2023
bec69c3
Remove code samples from README.md
minestarks Jun 24, 2023
619efda
Fix minor mistakes
minestarks Jun 27, 2023
a390347
Merge branch 'main' of https://github.com/microsoft/qsharp into mines…
minestarks Jun 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ __pycache__/
.cargo/
.github/
.vscode/
compiler/
jupyterlab/.pytest_cache/
jupyterlab/lib/
jupyterlab/.venv/
jupyterlab/qsharp_jupyterlab/labextension/
library/
npm/dist/
npm/lib/
npm/src/*.generated.ts
pip/.venv/
pip/.pytest_cache/
pip/src/**/*.html
playground/public/libs/
samples/
target/
vscode/out/
wasm/
.venv/
/compiler/
/jupyterlab/.pytest_cache/
/jupyterlab/lib/
/jupyterlab/qsharp_jupyterlab/labextension/
/library/
/npm/dist/
/npm/lib/
/npm/src/*.generated.ts
/pip/.pytest_cache/
/pip/src/**/*.html
/playground/public/libs/
/samples/
/target/
/vscode/out/
/wasm/
45 changes: 11 additions & 34 deletions npm/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# qsharp npm module

This package contains the qsharp compiler functionality shipped for consumption via npm.
This package contains the qsharp compiler and language service functionality shipped for consumption via npm.

The source is written in TypeScript, which is compiled to ECMAScript modules in the ./dist directory.
The wasm binaries from the Rust builds are copied to the ./lib directory.
Expand Down Expand Up @@ -36,42 +36,19 @@ the browser (see <https://esbuild.github.io/api/#how-conditions-work>).

## Design

The API for using this module is similar whether using a browser or Node.js, and whether running
in the main thread or a worker thread. You instantiate the compiler, and call operations on it
which complete in the order called.
This package provides two services, the compiler and the language service.

All operations return a Promise which resolves then the operation is complete. Some operations
may also emit events, such as debug messages or state dumps as they are processed. A call may
also be passed a CancellationToken so that if the result is no longer needed then the operation
may be cancelled before starting.

If the caller is not interested in any interim events or being able to cancel the request,
these arguments are optional. For example:

```js
const codeSample = "namespace Test { operation Main() {....} }";
const entryPoint = "Test.Main()";

const compiler = getCompilerWorker();
const cancelSrc = new CancellationTokenSource();
const runEvents = new QscEventTarget(false /* store record of events */);
The API for using these services is similar whether using a browser or Node.js,
and whether running in the main thread or a worker thread. You instantiate the service
and call operations on it which complete in the order called.

// Log any DumpMachine calls
runEvents.addEventListener('DumpMachine', (evt) => console.log("DumpMachine: %o", evt.detail));

compiler.run(codeSample, entryPoint, 1 /* shots */, runEvents, cancelSrc.token)
.then(result => console.log("Run result: %s", result));
.catch(err => console.err("Run failed with: %o", err));

cancelButton.addEventListener('click', () => {
// Try to cancel the operation if still pending
tokenSource.cancel();
});
All operations return a Promise which resolves then the operation is complete. Some operations
may also emit events, such as debug messages or state dumps as they are processed. The service
itself can also emit events which can be subscribed to using `addEventListener`.

// Also run the below request, but don't care about events or cancellation, just the result
const checkResult = await compiler.check(code);
console.log('check result was: %o', checkResult);
```
See the Q# playground code at <https://github.com/microsoft/qsharp/tree/main/playground> for
an example of code that uses this package. The unit tests at
<https://github.com/microsoft/qsharp/tree/main/npm/test> are also a good reference.

Promises, Events, and Cancellation are based on JavaScript or Web standards, or the VS Code API:

Expand Down
2 changes: 1 addition & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"node": "./dist/main.js",
"default": "./dist/browser.js"
},
"./worker": "./dist/worker-browser.js"
"./worker": "./dist/compiler/worker-browser.js"
},
"scripts": {
"build": "npm run generate && npm run build:tsc",
Expand Down
78 changes: 64 additions & 14 deletions npm/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@
// the "./main.js" module is the entry point.

import initWasm, * as wasm from "../lib/web/qsc_wasm.js";
import { Compiler, ICompiler, ICompilerWorker } from "./compiler/compiler.js";
import { createCompilerProxy } from "./compiler/worker-proxy.js";
import {
ILanguageService,
ILanguageServiceWorker,
QSharpLanguageService,
} from "./language-service/language-service.js";
import { createLanguageServiceProxy } from "./language-service/worker-proxy.js";
import { LogLevel, log } from "./log.js";
import { Compiler, ICompiler, ICompilerWorker } from "./compiler.js";
import { ResponseMsgType, createWorkerProxy } from "./worker-common.js";

// Create once. A module is stateless and can be efficiently passed to WebWorkers.
let wasmModule: WebAssembly.Module | null = null;

// Used to track if an instance is already instantiated
let wasmInstance: wasm.InitOutput;
let wasmPromise: Promise<wasm.InitOutput>;

export async function loadWasmModule(uriOrBuffer: string | ArrayBuffer) {
if (typeof uriOrBuffer === "string") {
Expand All @@ -27,7 +33,8 @@ export async function loadWasmModule(uriOrBuffer: string | ArrayBuffer) {

export async function getCompiler(): Promise<ICompiler> {
if (!wasmModule) throw "Wasm module must be loaded first";
if (!wasmInstance) wasmInstance = await initWasm(wasmModule);
if (!wasmPromise) wasmPromise = initWasm(wasmModule);
await wasmPromise;

return new Compiler(wasm);
}
Expand All @@ -51,24 +58,67 @@ export function getCompilerWorker(workerArg: string | Worker): ICompilerWorker {

// If you lose the 'this' binding, some environments have issues
const postMessage = worker.postMessage.bind(worker);
const setMsgHandler = (handler: (e: ResponseMsgType) => void) =>
(worker.onmessage = (ev) => handler(ev.data));
const onTerminate = () => worker.terminate();

return createWorkerProxy(postMessage, setMsgHandler, onTerminate);
// Create the proxy which will forward method calls to the worker
const proxy = createCompilerProxy(postMessage, onTerminate);

// Let proxy handle response and event messages from the worker
worker.onmessage = (ev) => proxy.onMsgFromWorker(ev.data);
return proxy;
}

export type { ICompilerWorker };
export { log, type LogLevel };
export { type Dump, type ShotResult, type VSDiagnostic } from "./common.js";
export { type CompilerState } from "./compiler.js";
export async function getLanguageService(): Promise<ILanguageService> {
if (!wasmModule) throw "Wasm module must be loaded first";
if (!wasmPromise) wasmPromise = initWasm(wasmModule);
await wasmPromise;

return new QSharpLanguageService(wasm);
}

// Create the compiler inside a WebWorker and proxy requests.
// If the Worker was already created via other means and is ready to receive
// messages, then the worker may be passed in and it will be initialized.
export function getLanguageServiceWorker(
workerArg: string | Worker
): ILanguageServiceWorker {
if (!wasmModule) throw "Wasm module must be loaded first";

// Create or use the WebWorker
const worker =
typeof workerArg === "string" ? new Worker(workerArg) : workerArg;

// Send it the Wasm module to instantiate
worker.postMessage({
type: "init",
wasmModule,
qscLogLevel: log.getLogLevel(),
});

// If you lose the 'this' binding, some environments have issues
const postMessage = worker.postMessage.bind(worker);
const onTerminate = () => worker.terminate();

// Create the proxy which will forward method calls to the worker
const proxy = createLanguageServiceProxy(postMessage, onTerminate);

// Let proxy handle response and event messages from the worker
worker.onmessage = (ev) => proxy.onMsgFromWorker(ev.data);
return proxy;
}

export { type Dump, type ShotResult } from "./compiler/common.js";
export { type CompilerState } from "./compiler/compiler.js";
export { QscEventTarget } from "./compiler/events.js";
export {
getAllKatas,
getKata,
type Kata,
type KataItem,
type Example,
type Exercise,
type Kata,
type KataItem,
} from "./katas.js";
export { default as samples } from "./samples.generated.js";
export { QscEventTarget } from "./events.js";
export { type VSDiagnostic } from "./vsdiagnostic.js";
export { log, type LogLevel };
export type { ICompilerWorker };
83 changes: 83 additions & 0 deletions npm/src/compiler/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { VSDiagnostic } from "../vsdiagnostic.js";

// Each DumpMachine output is represented as an object where each key is a basis
// state, e.g., "|3>" and the value is the [real, imag] parts of the complex amplitude.
export type Dump = {
[index: string]: [number, number];
};

export type Result =
| { success: true; value: string }
| { success: false; value: VSDiagnostic };

interface DumpMsg {
type: "DumpMachine";
state: Dump;
}

interface MessageMsg {
type: "Message";
message: string;
}

interface ResultMsg {
type: "Result";
result: Result;
}

type EventMsg = ResultMsg | DumpMsg | MessageMsg;

function outputAsResult(msg: string): ResultMsg | null {
try {
const obj = JSON.parse(msg);
if (obj?.type == "Result" && typeof obj.success == "boolean") {
return {
type: "Result",
result: {
success: obj.success,
value: obj.result,
},
};
}
} catch {
return null;
}
return null;
}

function outputAsMessage(msg: string): MessageMsg | null {
try {
const obj = JSON.parse(msg);
if (obj?.type == "Message" && typeof obj.message == "string") {
return obj as MessageMsg;
}
} catch {
return null;
}
return null;
}

function outputAsDump(msg: string): DumpMsg | null {
try {
const obj = JSON.parse(msg);
if (obj?.type == "DumpMachine" && typeof obj.state == "object") {
return obj as DumpMsg;
}
} catch {
return null;
}
return null;
}

export function eventStringToMsg(msg: string): EventMsg | null {
return outputAsResult(msg) || outputAsMessage(msg) || outputAsDump(msg);
}

export type ShotResult = {
success: boolean;
result: string | VSDiagnostic;
events: Array<MessageMsg | DumpMsg>;
};
24 changes: 9 additions & 15 deletions npm/src/compiler.ts → npm/src/compiler/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { IDiagnostic, ICompletionList } from "../lib/node/qsc_wasm.cjs";
import { log } from "./log.js";
import { eventStringToMsg, mapDiagnostics, VSDiagnostic } from "./common.js";
import type { IDiagnostic, ICompletionList } from "../../lib/node/qsc_wasm.cjs";
import { log } from "../log.js";
import { eventStringToMsg } from "./common.js";
import { mapDiagnostics, VSDiagnostic } from "../vsdiagnostic.js";
import { IQscEventTarget, QscEvents, makeEvent } from "./events.js";
import { IServiceProxy, ServiceState } from "../worker-proxy.js";

// The wasm types generated for the node.js bundle are just the exported APIs,
// so use those as the set used by the shared compiler
type Wasm = typeof import("../lib/node/qsc_wasm.cjs");
type Wasm = typeof import("../../lib/node/qsc_wasm.cjs");

// These need to be async/promise results for when communicating across a WebWorker, however
// for running the compiler in the same thread the result will be synchronous (a resolved promise).
export type CompilerState = "idle" | "busy";
export interface ICompiler {
checkCode(code: string): Promise<VSDiagnostic[]>;
getHir(code: string): Promise<string>;
Expand All @@ -28,11 +29,11 @@ export interface ICompiler {
verify_code: string,
eventHandler: IQscEventTarget
): Promise<boolean>;
onstatechange: ((state: CompilerState) => void) | null;
}

// WebWorker also support being explicitly terminated to tear down the worker thread
export type ICompilerWorker = ICompiler & { terminate: () => void };
export type ICompilerWorker = ICompiler & IServiceProxy;
export type CompilerState = ServiceState;

function errToDiagnostic(err: any): VSDiagnostic {
if (
Expand All @@ -56,8 +57,6 @@ function errToDiagnostic(err: any): VSDiagnostic {
export class Compiler implements ICompiler {
private wasm: Wasm;

onstatechange: ((state: CompilerState) => void) | null = null;

constructor(wasm: Wasm) {
log.info("Constructing a Compiler instance");
this.wasm = wasm;
Expand Down Expand Up @@ -99,16 +98,12 @@ export class Compiler implements ICompiler {
// All results are communicated as events, but if there is a compiler error (e.g. an invalid
// entry expression or similar), it may throw on run. The caller should expect this promise
// may reject without all shots running or events firing.
if (this.onstatechange) this.onstatechange("busy");

this.wasm.run(
code,
expr,
(msg: string) => onCompilerEvent(msg, eventHandler),
shots
);

if (this.onstatechange) this.onstatechange("idle");
}

async runKata(
Expand All @@ -119,7 +114,6 @@ export class Compiler implements ICompiler {
let success = false;
let err: any = null;
try {
if (this.onstatechange) this.onstatechange("busy");
success = this.wasm.run_kata_exercise(
verify_code,
user_code,
Expand All @@ -128,7 +122,6 @@ export class Compiler implements ICompiler {
} catch (e) {
err = e;
}
if (this.onstatechange) this.onstatechange("idle");
// Currently the kata wasm doesn't emit the success/failure events, so do those here.
if (!err) {
const evt = makeEvent("Result", {
Expand Down Expand Up @@ -169,5 +162,6 @@ export function onCompilerEvent(msg: string, eventTarget: IQscEventTarget) {
log.never(msgType);
throw "Unexpected message type";
}
log.debug("worker dispatching event " + JSON.stringify(qscEvent));
eventTarget.dispatchEvent(qscEvent);
}
Loading