Skip to content

Commit ec25196

Browse files
authored
Playground fixes (#302)
Lots of improvements here. Cancellation, shot count doesn't get reset after a run, editor.tsx re-written to fix some edge cases, error list is prettier when it has help text, etc.
1 parent 29c6fd5 commit ec25196

File tree

12 files changed

+461
-705
lines changed

12 files changed

+461
-705
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ __pycache__/
33
.ado/
44
.cargo/
55
.github/
6+
.vscode/
67
compiler/
78
library/
89
npm/dist/

makeNpmDrop.mjs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// @ts-check
2+
3+
/* This file creates a drop of the npm package, along with the playground as an example site.
4+
5+
Basically it copies the root `package.*` files along with the ./npm and ./playground directories,
6+
but excludes files that should be installed via npm or copied from there (such as ./node_modules
7+
and ./playground/public/libs).
8+
9+
It excludes all the Rust/.github/katas/samples/etc., files and puts a usage README.md at the root.
10+
*/
11+
12+
import {
13+
copyFileSync,
14+
cpSync,
15+
mkdirSync,
16+
statSync,
17+
writeFileSync,
18+
} from "node:fs";
19+
import { dirname, join } from "node:path";
20+
import { exit } from "node:process";
21+
import { fileURLToPath } from "node:url";
22+
23+
const thisDir = dirname(fileURLToPath(import.meta.url));
24+
const targetDir = join(thisDir, "..", "qsharp-drop");
25+
26+
const dirStat = statSync(targetDir, { throwIfNoEntry: false });
27+
if (dirStat) {
28+
console.error("Target directory already exists. Please delete first.");
29+
exit(1);
30+
}
31+
32+
const buildStat = statSync(`${thisDir}/npm/dist/browser.js`);
33+
if (!buildStat) {
34+
console.error("npm dist files not found. Build before running");
35+
exit(1);
36+
}
37+
38+
mkdirSync(targetDir);
39+
40+
copyFileSync(`${thisDir}/package.json`, `${targetDir}/package.json`);
41+
copyFileSync(`${thisDir}/package-lock.json`, `${targetDir}/package-lock.json`);
42+
43+
/**
44+
* @param {string} src The source file being copied
45+
*/
46+
function copyFilter(src) {
47+
// Don't copy over the unnecessary files
48+
if (src.includes("/npm/generate_")) return false;
49+
if (src.includes("/playground/public/libs")) return false;
50+
return true;
51+
}
52+
53+
cpSync(`${thisDir}/npm`, `${targetDir}/npm`, {
54+
recursive: true,
55+
filter: copyFilter,
56+
});
57+
cpSync(`${thisDir}/playground`, `${targetDir}/playground`, {
58+
recursive: true,
59+
filter: copyFilter,
60+
});
61+
62+
writeFileSync(
63+
`${targetDir}/README.md`,
64+
`# README
65+
66+
The npm package containing the Q# compiler is in the "./npm" directory
67+
68+
The sample playground site that shows it wired up is in the "./playground" directory.
69+
70+
To get the playground up and running:
71+
72+
- In the root directory run "npm install"
73+
- In the "./playground" run "npm run build"
74+
- To start the site, from within "./playground" run "npx serve ./public"
75+
76+
The main source file to show hooking up the compiler and Monaco on a web page is at "./playground/src/main.tsx"
77+
78+
The test files in "./npm/test/basics.js" show minimal examples of the qsharp compiler API usage.
79+
`,
80+
{ encoding: "utf8" }
81+
);

npm/src/browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function getCompilerWorker(workerArg: string | Worker): ICompilerWorker {
6161
export type { ICompilerWorker };
6262
export { log };
6363
export { type Dump, type ShotResult, type VSDiagnostic } from "./common.js";
64+
export { type CompilerState } from "./compiler.js";
6465
export {
6566
getAllKatas,
6667
getKata,

npm/src/compiler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Wasm = typeof import("../lib/node/qsc_wasm.cjs");
1212

1313
// These need to be async/promise results for when communicating across a WebWorker, however
1414
// for running the compiler in the same thread the result will be synchronous (a resolved promise).
15+
export type CompilerState = "idle" | "busy";
1516
export interface ICompiler {
1617
checkCode(code: string): Promise<VSDiagnostic[]>;
1718
getCompletions(): Promise<ICompletionList>;
@@ -26,6 +27,7 @@ export interface ICompiler {
2627
verify_code: string,
2728
eventHandler: IQscEventTarget
2829
): Promise<boolean>;
30+
onstatechange: ((state: CompilerState) => void) | null;
2931
}
3032

3133
// WebWorker also support being explicitly terminated to tear down the worker thread
@@ -54,6 +56,8 @@ function errToDiagnostic(err: any): VSDiagnostic {
5456
export class Compiler implements ICompiler {
5557
private wasm: Wasm;
5658

59+
onstatechange: ((state: CompilerState) => void) | null = null;
60+
5761
constructor(wasm: Wasm) {
5862
log.info("Constructing a Compiler instance");
5963
this.wasm = wasm;
@@ -77,12 +81,16 @@ export class Compiler implements ICompiler {
7781
// All results are communicated as events, but if there is a compiler error (e.g. an invalid
7882
// entry expression or similar), it may throw on run. The caller should expect this promise
7983
// may reject without all shots running or events firing.
84+
if (this.onstatechange) this.onstatechange("busy");
85+
8086
this.wasm.run(
8187
code,
8288
expr,
8389
(msg: string) => onCompilerEvent(msg, eventHandler),
8490
shots
8591
);
92+
93+
if (this.onstatechange) this.onstatechange("idle");
8694
}
8795

8896
async runKata(
@@ -93,6 +101,7 @@ export class Compiler implements ICompiler {
93101
let success = false;
94102
let err: any = null; // eslint-disable-line @typescript-eslint/no-explicit-any
95103
try {
104+
if (this.onstatechange) this.onstatechange("busy");
96105
success = this.wasm.run_kata_exercise(
97106
verify_code,
98107
user_code,
@@ -101,6 +110,7 @@ export class Compiler implements ICompiler {
101110
} catch (e) {
102111
err = e;
103112
}
113+
if (this.onstatechange) this.onstatechange("idle");
104114
// Currently the kata wasm doesn't emit the success/failure events, so do those here.
105115
if (!err) {
106116
const evt = makeEvent("Result", {

npm/src/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,6 @@ export class QscEventTarget extends EventTarget implements IQscEventTarget {
158158

159159
clearResults(): void {
160160
this.results = [];
161+
this.shotActive = false;
161162
}
162163
}

npm/src/worker-common.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { log } from "./log.js";
55
import { ICompletionList } from "../lib/web/qsc_wasm.js";
66
import { DumpMsg, MessageMsg, VSDiagnostic } from "./common.js";
7-
import { ICompiler, ICompilerWorker } from "./compiler.js";
7+
import { CompilerState, ICompiler, ICompilerWorker } from "./compiler.js";
88
import { CancellationToken } from "./cancellation.js";
99
import { IQscEventTarget, QscEventTarget, makeEvent } from "./events.js";
1010

@@ -46,6 +46,13 @@ export function createWorkerProxy(
4646
): ICompilerWorker {
4747
const queue: RequestState[] = [];
4848
let curr: RequestState | undefined;
49+
let state: CompilerState = "idle";
50+
51+
function setState(newState: CompilerState) {
52+
if (state === newState) return;
53+
state = newState;
54+
if (proxy.onstatechange) proxy.onstatechange(state);
55+
}
4956

5057
function queueRequest(
5158
type: string,
@@ -73,7 +80,12 @@ export function createWorkerProxy(
7380
break;
7481
}
7582
}
76-
if (!curr) return;
83+
if (!curr) {
84+
// Nothing else queued, signal that we're now idle and exit.
85+
log.debug("Worker queue is empty");
86+
setState("idle");
87+
return;
88+
}
7789

7890
let msg: CompilerReqMsg | null = null;
7991
switch (curr.type) {
@@ -84,6 +96,8 @@ export function createWorkerProxy(
8496
msg = { type: "getCompletions" };
8597
break;
8698
case "run":
99+
// run and runKata can take a long time, so set state to busy
100+
setState("busy");
87101
msg = {
88102
type: "run",
89103
code: curr.args[0],
@@ -92,6 +106,7 @@ export function createWorkerProxy(
92106
};
93107
break;
94108
case "runKata":
109+
setState("busy");
95110
msg = {
96111
type: "runKata",
97112
user_code: curr.args[0],
@@ -182,9 +197,20 @@ export function createWorkerProxy(
182197
runKata(user_code, verify_code, evtHandler) {
183198
return queueRequest("runKata", [user_code, verify_code], evtHandler);
184199
},
200+
onstatechange: null,
185201
// Kill the worker without a chance to shutdown. May be needed if it is not responding.
186202
terminate: () => {
187203
log.info("Terminating the worker");
204+
if (curr) {
205+
log.debug("Terminating running worker item of type: %s", curr.type);
206+
curr.reject("terminated");
207+
}
208+
// Reject any outstanding items
209+
while (queue.length) {
210+
const item = queue.shift();
211+
log.debug("Terminating outstanding work item of type: %s", item?.type);
212+
item?.reject("terminated");
213+
}
188214
terminator();
189215
},
190216
};

npm/test/basics.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,75 @@ test("Run samples", async () => {
249249
assert(result.success);
250250
});
251251
});
252+
253+
test("state change", async () => {
254+
const compiler = getCompilerWorker();
255+
const resultsHandler = new QscEventTarget(false);
256+
const stateChanges = [];
257+
258+
compiler.onstatechange = (state) => {
259+
stateChanges.push(state);
260+
};
261+
const code = `namespace Test {
262+
@EntryPoint()
263+
operation MyEntry() : Result {
264+
use q1 = Qubit();
265+
return M(q1);
266+
}
267+
}`;
268+
await compiler.run(code, "", 10, resultsHandler);
269+
compiler.terminate();
270+
// There SHOULDN'T be a race condition here between the 'run' promise completing and the
271+
// statechange events firing, as the run promise should 'resolve' in the next microtask,
272+
// whereas the idle event should fire synchronously when the queue is empty.
273+
// For more details, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#task_queues_vs._microtasks
274+
assert(stateChanges.length === 2);
275+
assert(stateChanges[0] === "busy");
276+
assert(stateChanges[1] === "idle");
277+
});
278+
279+
test("cancel worker", () => {
280+
return new Promise((resolve) => {
281+
const code = `namespace MyQuantumApp {
282+
open Microsoft.Quantum.Diagnostics;
283+
284+
@EntryPoint()
285+
operation Main() : Result[] {
286+
repeat {} until false;
287+
return [];
288+
}
289+
}`;
290+
291+
const cancelledArray = [];
292+
const compiler = getCompilerWorker();
293+
const resultsHandler = new QscEventTarget(false);
294+
295+
// Queue some tasks that will never complete
296+
compiler.run(code, "", 10, resultsHandler).catch((err) => {
297+
cancelledArray.push(err);
298+
});
299+
compiler.checkCode(code).catch((err) => {
300+
cancelledArray.push(err);
301+
});
302+
303+
// Ensure those tasks are running/queued before terminating.
304+
setTimeout(async () => {
305+
// Terminate the compiler, which should reject the queued promises
306+
compiler.terminate();
307+
308+
// Start a new compiler and ensure that works fine
309+
const compiler2 = getCompilerWorker();
310+
const result = await compiler2.checkCode(code);
311+
compiler2.terminate();
312+
313+
// New 'check' result is good
314+
assert(Array.isArray(result) && result.length === 0);
315+
316+
// Old requests were cancelled
317+
assert(cancelledArray.length === 2);
318+
assert(cancelledArray[0] === "terminated");
319+
assert(cancelledArray[1] === "terminated");
320+
resolve(null);
321+
}, 4);
322+
});
323+
});

playground/public/style.css

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,22 +108,29 @@ body {
108108
}
109109

110110
.error-list {
111-
border: 1px solid var(--border-color);
112-
border-bottom: 0px;
113111
margin-top: 24px;
112+
margin-bottom: 24px;
113+
min-height: 48px;
114114
}
115115

116116
.error-row {
117+
border: 1px solid var(--border-color);
117118
background-color: var(--error-background-color);
118119
padding: 4px;
119120
border-bottom: 0.5px solid gray;
120121
font-size: 0.9rem;
122+
margin-bottom: -1px;
121123
}
122124

123125
.error-row > span {
124126
font-weight: 200;
125127
}
126128

129+
.error-help {
130+
font-weight: 200;
131+
font-style: italic;
132+
}
133+
127134
.results-labels {
128135
display: flex;
129136
height: 32px;
@@ -182,20 +189,20 @@ body {
182189
margin-right: 2px;
183190
}
184191

185-
#editor {
192+
.code-editor {
186193
height: 40vh;
187194
min-height: 400px;
188195
border: 1px solid var(--border-color);
189196
}
190197

191-
#button-row {
198+
.button-row {
192199
display: flex;
193200
justify-content: flex-end;
194201
align-items: center;
195202
margin-top: 8px;
196203
}
197204

198-
#button-row > * {
205+
.button-row > * {
199206
margin-left: 10px;
200207
font-size: 1rem;
201208
}
@@ -206,14 +213,6 @@ body {
206213
cursor: pointer;
207214
}
208215

209-
#expr {
210-
width: 160px;
211-
}
212-
213-
#shot {
214-
width: 80px;
215-
}
216-
217216
.main-button {
218217
background-color: var(--nav-background);
219218
font-size: 1rem;
@@ -227,6 +226,7 @@ body {
227226

228227
.main-button:disabled {
229228
background-color: gray;
229+
cursor: default;
230230
}
231231

232232
.histogram {

0 commit comments

Comments
 (0)