diff --git a/npm/src/browser.ts b/npm/src/browser.ts index af99ab8183..032157367c 100644 --- a/npm/src/browser.ts +++ b/npm/src/browser.ts @@ -47,6 +47,8 @@ export function getCompilerWorker(script: string): ICompilerWorker { return createWorkerProxy(postMessage, setMsgHandler, onTerminate); } +export type { ICompilerWorker } +export { log } export { renderDump, exampleDump } from "./state-table.js" export { type Dump, type ShotResult, type VSDiagnostic } from "./common.js"; export { getAllKatas, getKata, type Kata, type KataItem, type Exercise } from "./katas.js"; diff --git a/npm/src/events.ts b/npm/src/events.ts index f27552e413..b008f035c8 100644 --- a/npm/src/events.ts +++ b/npm/src/events.ts @@ -10,6 +10,7 @@ interface QscEventMap { "Message": QscEvent; "DumpMachine": QscEvent; "Result": QscEvent; + "uiResultsRefresh": QscEvent; } // Union of valid message names @@ -47,7 +48,9 @@ function makeResultObj(): ShotResult { export class QscEventTarget extends EventTarget implements IQscEventTarget { private results: ShotResult[] = []; - private currentResult: ShotResult = makeResultObj(); + private shotActive = false; + private animationFrameId = 0; + private supportsUiRefresh = false; // Overrides for the base EventTarget methods to limit to expected event types addEventListener( @@ -70,6 +73,8 @@ export class QscEventTarget extends EventTarget implements IQscEventTarget { */ constructor(captureEvents: boolean) { super(); + this.supportsUiRefresh = typeof globalThis.requestAnimationFrame === 'function'; + if (captureEvents) { this.addEventListener('Message', (ev) => this.onMessage(ev.detail)); this.addEventListener('DumpMachine', (ev) => this.onDumpMachine(ev.detail)); @@ -78,21 +83,60 @@ export class QscEventTarget extends EventTarget implements IQscEventTarget { } private onMessage(msg: string) { - this.currentResult.events.push({ "type": "Message", "message": msg }); + this.ensureActiveShot(); + + const shotIdx = this.results.length - 1; + this.results[shotIdx].events.push({ "type": "Message", "message": msg }); + + this.queueUiRefresh(); } private onDumpMachine(dump: Dump) { - this.currentResult.events.push({ "type": "DumpMachine", "state": dump }); + this.ensureActiveShot(); + + const shotIdx = this.results.length - 1; + this.results[shotIdx].events.push({ "type": "DumpMachine", "state": dump }); + + this.queueUiRefresh(); } private onResult(result: Result) { - // Save result and move to the next - this.currentResult.success = result.success; - this.currentResult.result = result.value; - this.results.push(this.currentResult); - this.currentResult = makeResultObj(); + this.ensureActiveShot(); + + const shotIdx = this.results.length - 1; + + this.results[shotIdx].success = result.success; + this.results[shotIdx].result = result.value; + this.shotActive = false; + + this.queueUiRefresh(); + } + + private ensureActiveShot() { + if (!this.shotActive) { + this.results.push(makeResultObj()); + this.shotActive = true; + } + } + + private queueUiRefresh() { + if (this.supportsUiRefresh && !this.animationFrameId) { + this.animationFrameId = requestAnimationFrame(() => {this.onUiRefresh()}); + } + } + + private onUiRefresh() { + this.animationFrameId = 0; + const uiRefreshEvent = makeEvent('uiResultsRefresh', undefined); + this.dispatchEvent(uiRefreshEvent); } getResults(): ShotResult[] { return this.results; } - clearResults(): void { this.results.length = 0; } + + resultCount(): number { + // May be one less than length if the last is still in flight + return this.shotActive ? this.results.length - 1 : this.results.length; + } + + clearResults(): void { this.results = []; } } diff --git a/package-lock.json b/package-lock.json index e48f4cc116..4846ed8273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "devDependencies": { "@types/node": "^18.15.3", "esbuild": "^0.17.12", + "github-markdown-css": "^5.2.0", "marked": "^4.3.0", "mathjax": "^3.2.2", "monaco-editor": "^0.36.1", @@ -418,6 +419,15 @@ "@esbuild/win32-x64": "0.17.12" } }, + "node_modules/github-markdown-css": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.2.0.tgz", + "integrity": "sha512-hq5RaCInSUZ48bImOZpkppW2/MT44StRgsbsZ8YA4vJFwLKB/Vo3k7R2t+pUGqO+ThG0QDMi96TewV/B3vyItg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -678,6 +688,12 @@ "@esbuild/win32-x64": "0.17.12" } }, + "github-markdown-css": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.2.0.tgz", + "integrity": "sha512-hq5RaCInSUZ48bImOZpkppW2/MT44StRgsbsZ8YA4vJFwLKB/Vo3k7R2t+pUGqO+ThG0QDMi96TewV/B3vyItg==", + "dev": true + }, "marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", diff --git a/package.json b/package.json index a5a17866c3..2f29cb3f11 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "devDependencies": { "@types/node": "^18.15.3", "esbuild": "^0.17.12", + "github-markdown-css": "^5.2.0", + "marked": "^4.3.0", "mathjax": "^3.2.2", "monaco-editor": "^0.36.1", "preact": "^10.13.1", - "marked": "^4.3.0", "typescript": "^5.0.2" } } diff --git a/playground/build.js b/playground/build.js index b074f6d4e6..63d1f96a4d 100644 --- a/playground/build.js +++ b/playground/build.js @@ -18,7 +18,7 @@ const outdir = join(thisDir, 'public/libs'); /** @type {import("esbuild").BuildOptions} */ const buildOptions = { - entryPoints: [join(thisDir, "src/main.ts"), join(thisDir, "src/worker.ts")], + entryPoints: [join(thisDir, "src/main.tsx"), join(thisDir, "src/worker.ts")], outdir, bundle: true, target: ['es2020', 'chrome64', 'edge79', 'firefox62' ,'safari11.1'], @@ -43,6 +43,10 @@ function copyLibs() { mkdirSync(mathjaxDest, { recursive: true}); cpSync(mathjaxBase, mathjaxDest, {recursive: true}); + let githubMarkdown = join(libsDir, "github-markdown-css/github-markdown-light.css"); + let githubMarkdownDest = join(thisDir, 'public/libs/github-markdown.css'); + copyFileSync(githubMarkdown, githubMarkdownDest); + let qsharpWasm = join(thisDir, "..", "npm/lib/web/qsc_wasm_bg.wasm"); let qsharpDest = join(thisDir, `public/libs/qsharp`); diff --git a/playground/public/index.html b/playground/public/index.html index a82c96650d..f096163123 100644 --- a/playground/public/index.html +++ b/playground/public/index.html @@ -6,11 +6,13 @@ + Q# playground - - - -

Q# playground

-
-
-
-
-
- - - -
-
- Copied to clipboard. - -
-
-
-
-
-
- -
-

Katas

-
- -
-
-
- - + + + \ No newline at end of file diff --git a/playground/public/style.css b/playground/public/style.css index 11e62cb733..5572ae8d88 100644 --- a/playground/public/style.css +++ b/playground/public/style.css @@ -1,124 +1,284 @@ /* Copyright (c) Microsoft Corporation. Licensed under the MIT License. */ +:root { + --heading-background: #262679; + --main-background: #ececf0; + --main-color: #202020; + --nav-background: #bed1f4; + --nav-hover-background: #b3bede; + --nav-current-background: #b5c5f2; + --border-color: #768f9c; + --error-background-color: #ffe3e3; + --bar-selected-outline: #587ddd; +} + +html { + box-sizing: border-box; + font-size: 16px; +} + +*, *::before, *::after { + box-sizing: inherit; + margin: 0; + padding: 0; +} + body { - font-family: Helvetica, Arial, Verdana; + min-height: 100vh; + font-family: system-ui, "Segoe UI", "SegoeUI", Roboto, Helvetica, Arial, sans-serif; + color: var(--main-color); + background-color: var(--main-background); + display: grid; + grid-template-areas: + "header header header" + "nav editor results"; + grid-template-rows: auto 1fr; + grid-template-columns: minmax(180px, auto) 8fr 8fr; + column-gap: 16px; } -#container { - display: flex; +.header { + grid-area: header; + background: var(--heading-background); + color: var(--main-background); + font-size: 2rem; + font-weight: 600; + text-align: center; + padding-top: 4px; + padding-bottom: 8px; } -#output { - width: 540px; - padding-left: 40px; +.nav-column { + grid-area: nav; + background-color: var(--nav-background); } -#editor { - width: 600px; - height: 480px; - border: 1px solid gray; +.editor-column { + grid-area: editor; + margin: 16px; + margin-top: 32px; } -#button-row { - display:flex; - margin:16px 0; +.results-column { + grid-area: results; + margin-left: 0px; + margin-top: 32px; + margin-right: 120px; } -#share-div { - text-align:right; - flex-grow:1; +.nav-1 { + font-size: 1.2rem; + font-weight: 200; + color: var(--main-color); + border-top: 1px solid var(--border-color); + padding-top: 4px; + padding-bottom: 4px; + padding-left: 8px; } -#share-confirmation { - display: none; +.nav-2 { + font-size: 1.0rem; + font-weight: 200; + padding: 4px; + padding-left: 16px; } -#errors { - font-family: 'Courier New', Courier, monospace; - color: darkred; - margin-left: 50px; - white-space: pre; - width: 600px; - height: 400px; +.nav-selectable:hover { + background-color: var(--nav-hover-background); + cursor: pointer; } -.err-span { - background: transparent!important; - border-bottom: 4px double #e47777; +.nav-current { + font-weight: 700; + background-color: var(--nav-current-background); +} + +.file-name { + border: 1px solid var(--border-color); + border-bottom: 0px; + width: 100px; + text-align: center; + height: 32px; + line-height: 32px; + background-color: white;; } -table { +.error-list { + border: 1px solid var(--border-color); + border-bottom: 0px; + margin-top: 24px; +} + +.error-row { + background-color: var(--error-background-color); + padding: 4px; + border-bottom: 0.5px solid gray; + font-size: 0.9rem; +} + +.error-row > span { + font-weight: 200; +} + +.results-labels { + display: flex; + height: 32px; +} +.results-labels > div { + margin-right: 40px; + text-align: left; + vertical-align: middle; + cursor: pointer; +} + +.results-active-tab { + font-size: 1.1rem; + font-weight: 600; + text-decoration: underline; +} + +.output-header { + font-size: 1.1rem; + font-weight: 400; + margin-top: 16px; + margin-bottom: 16px; + display: flex; + justify-content: space-between; +} + +.prev-next { + font-weight: 200; + cursor: pointer; +} + +.state-table { border-collapse: collapse; - margin-top: 25px; + font-size: 0.9rem; + width: 100%; + min-width: 400px; + margin-bottom: 16px; } -thead tr { - background: #f0f0f0; +.state-table thead tr { + background: var(--nav-background) } -tbody tr { +.state-table tbody tr { border-top: 1px solid gray; } -td, th { - text-align: left; +.state-table td, .state-table th { + text-align: center; padding: 6px; white-space: nowrap; } -.bar { - fill: steelblue; +.state-table progress { + margin-right: 2px; } -.bar-label { - font-family: Verdana; - font-size: 3pt; - fill: #404040; - text-anchor: middle; +#editor { + height: 40vh; + min-height: 400px; + border: 1px solid var(--border-color); +} + +#button-row { + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 8px; +} + +#button-row > * { + margin-left: 10px; + font-size: 1rem; +} + +.icon-row > * { + margin-left: 4px; + font-size: 1.4rem; + cursor: pointer; +} + +#expr { + width: 160px; +} + +#shot { + width: 80px; +} + +.main-button { + background-color: var(--nav-background); + font-size: 1rem; + color: var(--main-color); + width: 72px; + height: 24px; + border-radius: 8px; + border: 1px solid var(--border-color); + cursor: pointer; +} + +.main-button:disabled { + background-color: gray; +} + +.histogram { + width: 100%; + height: 300px; + border: 1px solid var(--border-color); + background-color: white; +} + +.result-label { + margin-bottom: 24px; + font-style: italic; + font-weight: 300; +} + +.bar { + fill: var(--nav-background); } .bar:hover { - fill: lightblue; + fill: var(--nav-hover-background); } .bar-selected { - stroke: #ffc080; + stroke: var(--bar-selected-outline); + fill: var(--nav-current-background); } -#histo-label { - font-family: Verdana; - font-size: 4pt; - fill: gray; +.bar-label { + font-size: 3pt; + fill: var(--main-color); + text-anchor: middle; } -#hover-text { - font-family: Verdana; - font-size: 4pt; +.histo-label { + font-size: 3.5pt; + fill: var(--main-color) +} + +.hover-text { + font-size: 3.5pt; fill: gray; text-anchor: middle; } - button { - margin: 2px; - } - .result-header { - font-size: 16px; - font-weight: 600; - margin-left: 16px; - margin-top: 16px; - } - - .message-output { - font-family: 'Courier New', Courier, monospace; - font-size: 14pt; - background-color: #635858; - color:#f0f0f0; - border: 1px solid blue; - margin: 10px; - width: 520px; - } - - .message-output::before { - content: "> "; - } \ No newline at end of file +.err-span { + background: transparent!important; + border-bottom: 4px double #e47777; +} + +.message-output { + font-weight: 300; + font-size: 1.1rem; + margin-bottom: 16px; +} + +.kata-override { + background-color: var(--main-background); +} diff --git a/playground/src/editor.tsx b/playground/src/editor.tsx new file mode 100644 index 0000000000..1a335fcd41 --- /dev/null +++ b/playground/src/editor.tsx @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useEffect, useRef, useState } from "preact/hooks"; +import { ICompilerWorker, QscEventTarget, VSDiagnostic, log } from "qsharp"; +import { codeToBase64 } from "./utils.js"; + +export function Editor(props: { + code: string, + compiler: ICompilerWorker, + evtTarget: QscEventTarget, + showExpr: boolean, + defaultShots: number, + showShots: boolean, + kataVerify?: string, + shotError?: VSDiagnostic, + }) { + const editorRef = useRef(null); + const shotsRef = useRef(null); + + const [editor, setEditor] = useState(null); + const [errors, setErrors] = useState<{location: string, msg: string}[]>([]); + const [initialCode, setInitialCode] = useState(props.code); + + // Check if the initial code changed (i.e. sample selected) since first created + // If so, need to load it into the editor and save as the new initial code. + if (initialCode !== props.code) { + editor?.getModel()?.setValue(props.code || ""); + editor?.revealLineNearTop(1); + setInitialCode(props.code); + } + + // On reset, reload the initial code + function onReset() { + editor?.getModel()?.setValue(initialCode || ""); + } + + function onGetLink() { + const code = editor?.getModel()?.getValue(); + if (!code) return; + + const encodedCode = codeToBase64(code); + const escapedCode = encodeURIComponent(encodedCode); + + // Get current URL without query parameters to use as the base URL + const newUrl = `${window.location.href.split('?')[0]}?code=${escapedCode}`; + // Copy link to clipboard and update url without reloading the page + navigator.clipboard.writeText(newUrl); + window.history.pushState({}, '', newUrl); + // TODO: Alert user somehow link is on the clipboard + } + + useEffect(() => { + // Create the monaco editor + log.info("Creating a monaco editor"); + const editorDiv: HTMLDivElement = editorRef.current as any; + const editor = monaco.editor.create(editorDiv, {minimap: {enabled: false}, lineNumbersMinChars:3}); + const srcModel = monaco.editor.createModel(props.code, 'qsharp'); + editor.setModel(srcModel); + setEditor(editor); + + // If the browser window resizes, tell the editor to update it's layout + window.addEventListener('resize', _ => editor.layout()); + + // As code is edited check it for errors and update the error list + async function check() { + // TODO: As this is async, code may be being edited while earlier check calls are still running. + // Need to ensure that if this occurs, wait and try again on the next animation frame. + // i.e. Don't queue a bunch of checks if some are still outstanding + diagnosticsFrame = 0; + let code = srcModel.getValue(); + let errs = await props.compiler.checkCode(code); + + // Note that as this is async, the code may have changed since checkCode was called. + // TODO: Account for this scenario (e.g. delta positions with old source version) + squiggleDiagnostics(errs); + // TODO: Disable run button on errors: errs.length ? + // runButton.setAttribute("disabled", "true") : runButton.removeAttribute("disabled"); + + } + + // Helpers to turn errors into editor squiggles + function squiggleDiagnostics(errors: VSDiagnostic[]) { + let errList: {location: string, msg: string}[] = []; + let newMarkers = errors?.map(err => { + let startPos = srcModel.getPositionAt(err.start_pos); + let endPos = srcModel.getPositionAt(err.end_pos); + const marker: monaco.editor.IMarkerData = { + severity: monaco.MarkerSeverity.Error, + message: err.message, + startLineNumber: startPos.lineNumber, + startColumn: startPos.column, + endLineNumber: endPos.lineNumber, + endColumn: endPos.column, + } + errList.push({ + location: `main.qs@(${startPos.lineNumber},${startPos.column})`, + msg: err.message // TODO: Handle line breaks and 'help' notes + }); + return marker; + }); + monaco.editor.setModelMarkers(srcModel, "qsharp", newMarkers); + setErrors(errList); + } + + // While the code is changing, update the diagnostics as fast as the browser will render frames + let diagnosticsFrame = requestAnimationFrame(check); + + srcModel.onDidChangeContent(ev => { + if (!diagnosticsFrame) { + diagnosticsFrame = requestAnimationFrame(check); + } + }); + + return () => { + log.info("Disposing a monaco editor"); + editor.dispose(); + } + }, []); + + useEffect( () => { + // This code highlights the error in the editor if you move to a shot result that has an error + const srcModel = editor?.getModel(); + if (!srcModel) return; + + if (props.shotError) { + const err = props.shotError; + const startPos = srcModel.getPositionAt(err.start_pos); + const endPos = srcModel.getPositionAt(err.end_pos); + + const marker: monaco.editor.IMarkerData = { + severity: monaco.MarkerSeverity.Error, + message: err.message, + startLineNumber: startPos.lineNumber, + startColumn: startPos.column, + endLineNumber: endPos.lineNumber, + endColumn: endPos.column, + } + monaco.editor.setModelMarkers(srcModel, "qsharp", [marker]); + setErrors([{ + location: `main.qs@(${startPos.lineNumber},${startPos.column})`, + msg: err.message // TODO: Handle line breaks and 'help' notes + }]); + } else { + monaco.editor.setModelMarkers(srcModel, "qsharp", []); + setErrors([]); + } + }, [props.shotError]) + + async function onRun() { + const code = editor?.getModel()?.getValue(); + const shotsInput: HTMLInputElement = shotsRef.current as any; + const shots = shotsInput ? parseInt(shotsInput.value) || 1 : props.defaultShots; + if (!code) return; + props.evtTarget.clearResults(); + if (props.kataVerify) { + // This is for a kata. Provide the verification code. + await props.compiler.runKata(code, props.kataVerify, props.evtTarget); + } else { + await props.compiler.run(code, "", shots, props.evtTarget); + } + } + + return ( +
+
+
main.qs
+
+ + Get a link to this code + + + + Reset code to initial state + + + + + + + + +
+
+
+
+ { props.showExpr ? <> + Start + + : null + } + { props.showShots ? <> + Shots + + : null} + +
+{ errors.length ? + (
+ {errors.map(err => ( +
{err.location}: {err.msg}
+ ))} +
) : null +} +
); +} diff --git a/playground/src/histo.tsx b/playground/src/histo.tsx new file mode 100644 index 0000000000..0c2806e898 --- /dev/null +++ b/playground/src/histo.tsx @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useState } from "preact/hooks"; + +export function Histogram(props: {data: Map, filter: string, onFilter: (filter:string) => void }) { + const [hoverLabel, setHoverLabel] = useState(""); + + let barArray = [...props.data.entries()].sort( (a, b) => { + // If they can be converted to numbers, then sort as numbers, else lexically + const ax = Number(a[0]); + const bx = Number(b[0]); + if (!isNaN(ax) && !isNaN(bx)) return ax < bx ? -1 : 1; + return a[0] < b[0] ? -1: 1 + }); + + let totalCount = 0; + let maxCount = 0; + barArray.forEach(x => { + totalCount += x[1]; + maxCount = Math.max(x[1], maxCount); + }); + + function onMouseOverRect(evt: MouseEvent) { + const target = evt.target! as SVGRectElement; + const title = target.querySelector('title')?.textContent; + setHoverLabel(title || ""); + } + + function onMouseOutRect(evt: MouseEvent) { + setHoverLabel(""); + } + + function onClickRect(evt: MouseEvent) { + const targetElem = evt.target! as SVGRectElement; + const labelClicked = (targetElem.nextSibling! as SVGTextElement).textContent!; + + if (labelClicked === props.filter) { + // Clicked the already selected bar. Clear the filter + props.onFilter(""); + } else { + props.onFilter(labelClicked!) + } + } + + // Add a bar for each entry. Total width should be 0 to 160, with 4 space. Bar height max 72. + let barOffset = 160 / barArray.length; + let barWidth = barOffset * 0.8; + + let histogramLabel = ""; + + return ( + + + { + barArray.map( (entry, idx) => { + let height = 72 * (entry[1] / maxCount); + let x = barOffset * idx; + let y = 87 - height; + let barClass = "bar"; + let barLabel = `${entry[0]} at ${(entry[1] / totalCount * 100).toFixed(2)}%`; + + if (entry[0] === props.filter) { + barClass += " bar-selected"; + histogramLabel = barLabel; + } + + return (<> + + {barLabel} + + {entry[0]} + ); + }) + } + + {histogramLabel ? `Filter: ${histogramLabel}` : null} + {hoverLabel} + + ); +} diff --git a/playground/src/histogram.ts b/playground/src/histogram.ts deleted file mode 100644 index 4fca2b4e9e..0000000000 --- a/playground/src/histogram.ts +++ /dev/null @@ -1,143 +0,0 @@ -// Except an array of results (strings) and generate a histogram - -export type HistogramData = Array<{ - label: string; - count: number; -}>; - -// Takes an array of results (string labels) and buckets them for a histogram -export function generateHistogramData(input: string[]): HistogramData { - // Create an array of objects with label and frequency - let processedData: {[label: string]: number} = {}; - - input.forEach(x => { - if (x in processedData) { - processedData[x] += 1; - } else { - processedData[x] = 1; - } - }); - - let arrData: HistogramData = []; - - for(const elem in processedData) { - arrData.push({label: elem, count: processedData[elem]}); - } - arrData.sort( (a, b) => (a.label > b.label ? 1 : -1)); - return arrData; -} - -export function generateHistogramSvg(data: HistogramData, onFilter: (filter:string) => void) : SVGSVGElement { - const xmlns = "http://www.w3.org/2000/svg"; - let svgElem = document.createElementNS(xmlns, "svg") as SVGSVGElement; - svgElem.classList.add("histogram"); - svgElem.setAttributeNS(null, "viewBox", "0 0 165 100"); - - if (!data.length) return svgElem; - - // Add the initial child elements - const g = document.createElementNS(xmlns, "g"); - g.setAttributeNS(null, "transform", "translate(3, 5)"); - svgElem.appendChild(g); - - const histoLabel = document.createElementNS(xmlns, "text"); - histoLabel.setAttributeNS(null, "id", "histo-label"); - histoLabel.setAttributeNS(null, "x", "10"); - histoLabel.setAttributeNS(null, "y", "98"); - svgElem.appendChild(histoLabel); - - const hoverText = document.createElementNS(xmlns, "text"); - hoverText.setAttributeNS(null, "id", "hover-text"); - hoverText.setAttributeNS(null, "x", "90"); - hoverText.setAttributeNS(null, "y", "10"); - svgElem.appendChild(hoverText); - - let totalCount = 0; - let maxCount = 0; - data.forEach(x => { - totalCount += x.count; - maxCount = Math.max(x.count, maxCount); - }); - - // Add a bar for each entry. Total width should be 0 to 160, with 4 space. Bar height max 72. - let barOffset = 160 / data.length; - let barWidth = barOffset * 0.8; - data.forEach( (entry, idx) => { - let height = 72 * (entry.count / maxCount); - let x = barOffset * idx; - let y = 87 - height; - - // Add the bar rect - let rect = document.createElementNS(xmlns, "rect"); - rect.setAttributeNS(null, "class", "bar"); - rect.setAttributeNS(null, "x", `${x}`); - rect.setAttributeNS(null, "y", `${y}`); - rect.setAttributeNS(null, "width", `${barWidth}`); - rect.setAttributeNS(null, "height", `${height}`); - g.appendChild(rect); - - // Title for the rect - let title = document.createElementNS(xmlns, "title"); - title.textContent = `${entry.label} at ${(entry.count / totalCount * 100).toFixed(2)}%`; - rect.appendChild(title); - - // Add the text label - let text = document.createElementNS(xmlns, "text"); - text.setAttributeNS(null, "class", "bar-label"); - text.setAttributeNS(null, "x", `${x + barWidth / 2}`); - text.setAttributeNS(null, "y", `${y - 3}`); - text.textContent = entry.label; - g.appendChild(text); - }); - - let currentSelected: SVGRectElement | null = null - g.addEventListener('click', ev => { - if (ev.target instanceof SVGRectElement) { - let targetElem = ev.target; - if (targetElem.classList.contains('bar')) { - - if (currentSelected) currentSelected.classList.remove('bar-selected'); - if (targetElem == currentSelected) { - currentSelected = null; - histoLabel.textContent = ""; - onFilter(""); - return; - } - - targetElem.classList.add('bar-selected'); - currentSelected = targetElem; - histoLabel.textContent = - "Filter: " + targetElem.querySelector('title')!.textContent; - - // Filter is the text of the sibling text node (the bar label) - let textNode = targetElem.nextSibling; - onFilter(textNode?.textContent || ""); - } - } - }); - - g.addEventListener('mouseover', ev => { - if (ev.target instanceof SVGRectElement) { - let title = ev.target.querySelector('title')!.textContent; - hoverText.textContent = title; - } - }) - g.addEventListener('mouseout', ev => { - if (ev.target instanceof SVGRectElement) { - hoverText.textContent = ""; - } - }) - - return svgElem; -} - -export const sampleData: string[] = [ - "|000⟩","|000⟩", - "|001⟩","|001⟩","|001⟩","|001⟩","|001⟩","|001⟩","|001⟩", - "|010⟩","|010⟩", - "|011⟩","|011⟩", - "|100⟩","|100⟩", - "|101⟩", - "|110⟩","|110⟩", - "|111⟩","|111⟩", -]; diff --git a/playground/src/kata.tsx b/playground/src/kata.tsx new file mode 100644 index 0000000000..e4e968278b --- /dev/null +++ b/playground/src/kata.tsx @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useEffect, useRef } from "preact/hooks"; +import { ICompilerWorker, Kata, QscEventTarget } from "qsharp"; +import { Editor } from "./editor.js"; +import { Results } from "./results.js"; + +export function Kata(props: {kata: Kata, compiler: ICompilerWorker}) { + const kataContent = useRef(null); + const itemContent = useRef<(HTMLDivElement | null)[]>([]); + + useEffect(() => { + // MathJax rendering inside of React components seems to mess them up a bit, + // so we'll take control of it here and ensure the contents are replaced. + if (!kataContent.current) return; + kataContent.current.innerHTML = props.kata.contentAsHtml; + + props.kata.items.forEach( (item, idx) => { + const parentDiv = itemContent.current[idx]!; + parentDiv.querySelector('.kata-item-title')!.innerHTML = item.title; + parentDiv.querySelector('.kata-item-content')!.innerHTML = item.contentAsHtml; + }); + // In case we're now rendering less items than before, be sure to truncate + itemContent.current.length = props.kata.items.length; + + MathJax.typeset(); + }, [props.kata]); + + return ( +
+
+

+ { + props.kata.items.map((item, idx) => { + const evtTarget = new QscEventTarget(true); + return ( +
+
itemContent.current[idx] = elem}> +

+
+
+ + +
); + }) + } +
+); +} diff --git a/playground/src/katas.ts b/playground/src/katas.ts deleted file mode 100644 index fe68606f86..0000000000 --- a/playground/src/katas.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { getAllKatas, getKata, renderDump, type Kata, type KataItem, type Exercise } from "qsharp"; - -// MathJax will already be loaded on the page. Need to call `typeset` when LaTeX content changes. -declare var MathJax: { typeset: () => void; }; - -interface VerificationResult { - kind: "VerificationResult"; - result: boolean; -} - -interface KataError { - kind: "KataError"; - error: string; -} - -type KataOutput = VerificationResult | KataError; - -function renderKataOutput(output: KataOutput): HTMLDivElement { - let outputDiv = document.createElement("div"); - if (output.kind === "VerificationResult") { - outputDiv.textContent = `Kata Verification: ${output.result}`; - } else if (output.kind === "KataError") { - outputDiv.textContent = `${output.kind}: ${output.error}`; - } - - return outputDiv; -} - -function renderExercise(exercise: Exercise): HTMLDivElement { - let exerciseDiv = document.createElement("div"); - let exerciseContent = document.createElement("div"); - exerciseContent.innerHTML = exercise.contentAsHtml; - exerciseDiv.append(exerciseContent); - let sourceCodeArea = document.createElement("textarea"); - sourceCodeArea.id = `source_${exercise.id}`; - sourceCodeArea.rows = 30; - sourceCodeArea.cols = 80; - sourceCodeArea.value = exercise.placeholderImplementation; - exerciseDiv.append(sourceCodeArea); - let outputDiv = document.createElement("div"); - outputDiv.id = `ouput_${exercise.id}`; - exerciseDiv.append(outputDiv); - let verifyButtonDiv = document.createElement("div"); - exerciseDiv.append(verifyButtonDiv); - let verifyButton = document.createElement("button"); - verifyButton.textContent = "Verify"; - verifyButton.id = `verify_${exercise.id}`; - verifyButtonDiv.append(verifyButton); - - // This callback is the one that processes output produced when running the kata. - let outputCallback = (ev: string) => { - let result = "" as any; // eventStringToMsg(ev); - if (!result) { - console.error("Unrecognized message: " + ev); - return; - } - let paragraph = document.createElement('p') as HTMLParagraphElement; - switch (result.type) { - case "Message": - paragraph.textContent = `MESSAGE: ${result.message}`; - break; - case "DumpMachine": - let table = document.createElement("table"); - table.innerHTML = renderDump(result.state); - paragraph.appendChild(table); - break; - } - - outputDiv.append(paragraph); - } - - // Run the exercise when clicking the verify button. - verifyButton.addEventListener('click', async _ => { - outputDiv.innerHTML = ""; - let exerciseImplementation = sourceCodeArea.value; - try { - let result = true; // await runExercise(exercise, exerciseImplementation, outputCallback); - let verificationResult: VerificationResult = { kind: "VerificationResult", result: result }; - let renderedResult = renderKataOutput(verificationResult); - outputDiv.prepend(renderedResult); - } catch (e) { - if (e instanceof Error) { - let kataError: KataError = { kind: "KataError", error: e.message }; - let renderedError = renderKataOutput(kataError); - outputDiv.prepend(renderedError); - } - } - }); - - return exerciseDiv; -} - -function renderItem(item: KataItem): HTMLDivElement { - let itemDiv = document.createElement("div"); - itemDiv.className = "kata-item"; - if (item.type === "exercise") { - const exerciseDiv = renderExercise(item as Exercise); - itemDiv.append(exerciseDiv); - } - - return itemDiv; -} - -function renderKata(kata: Kata): HTMLDivElement { - let kataDiv = document.createElement("div"); - - // Render the content. - let kataContent = document.createElement("div"); - kataContent.innerHTML = kata.contentAsHtml; - kataDiv.append(kataContent); - - // Render each one of the items. - for (let item of kata.items) { - let renderedItem = renderItem(item); - kataDiv.append(renderedItem); - } - return kataDiv; -} - -export async function RenderKatas() { - /* - // Katas are rendered inside a div element with "katas-canvas" as id. - let canvasDiv = document.querySelector('#katas-canvas') as HTMLDivElement; - - // Clear the katas' canvas every time before re-rendering. - canvasDiv.innerHTML = ""; - - // Render the selected kata. - let katasDropdown = document.querySelector('#katas-list') as HTMLSelectElement; - let selectedOption = katasDropdown.item(katasDropdown.selectedIndex)!; - let kata = await getKata(selectedOption.value); - let renderedKata = renderKata(kata); - canvasDiv.append(renderedKata); - - // Render math stuff. - MathJax.typeset(); - */ -} - -export async function PopulateKatasList() { - /* - let katasDropdown = document.querySelector('#katas-list') as HTMLSelectElement; - let katas = await getAllKatas(); - for (let kata of await getAllKatas()) { - let option = document.createElement("option"); - option.value = kata.id; - option.text = kata.title; - katasDropdown.add(option); - } - */ -} \ No newline at end of file diff --git a/playground/src/main.ts b/playground/src/main.ts deleted file mode 100644 index 4db76e8ae4..0000000000 --- a/playground/src/main.ts +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/// - -import { getCompilerWorker, loadWasmModule, ShotResult, renderDump, VSDiagnostic, QscEventTarget } from "qsharp"; -import { generateHistogramData, generateHistogramSvg } from "./histogram.js"; -import { base64ToCode, codeToBase64 } from "./utils.js"; -import { PopulateKatasList, RenderKatas } from "./katas.js"; - -const wasmPromise = loadWasmModule("libs/qsharp/qsc_wasm_bg.wasm"); - -const sampleCode = `namespace Sample { - open Microsoft.Quantum.Diagnostics; - - @EntryPoint() - operation Main() : Result[] { - use q1 = Qubit(); - use q2 = Qubit(); - - H(q1); - CNOT(q1, q2); - DumpMachine(); - - let m1 = M(q1); - let m2 = M(q2); - - Reset(q1); - Reset(q2); - - return [m1, m2]; - } -} -`; - -// MathJax will already be loaded on the page. Need to call `typeset` when LaTeX content changes. -declare var MathJax: { typeset: () => void; }; - -let runResults: ShotResult[] = []; -let currentFilter = ""; -let editor: monaco.editor.IStandaloneCodeEditor; - -// Helpers to turn errors into editor squiggles -let currentsquiggles: string[] = []; -function squiggleDiagnostics(errors: VSDiagnostic[]) { - let srcModel = editor.getModel()!; - let newDecorations = errors?.map(err => { - let startPos = srcModel.getPositionAt(err.start_pos); - let endPos = srcModel.getPositionAt(err.end_pos); - let range = monaco.Range.fromPositions(startPos, endPos); - let decoration: monaco.editor.IModelDeltaDecoration = { - range, - options: { className: 'err-span', hoverMessage: { value: err.message } } - } - return decoration; - }); - currentsquiggles = srcModel.deltaDecorations(currentsquiggles, newDecorations || []); -} - -// This runs after the Monaco editor is initialized -async function loaded() { - await wasmPromise; // Ensure the Wasm Module is done loading - const evtHander = new QscEventTarget(true); - const compiler = await getCompilerWorker("libs/worker.js"); - - // Assign the various UI controls into variables - let editorDiv = document.querySelector('#editor') as HTMLDivElement; - let errorsDiv = document.querySelector('#errors') as HTMLDivElement; - let exprInput = document.querySelector('#expr') as HTMLInputElement; - let shotCount = document.querySelector('#shot') as HTMLInputElement; - let runButton = document.querySelector('#run') as HTMLButtonElement; - let shareButton = document.querySelector('#share') as HTMLButtonElement; - let shareConfirmation = document.querySelector('#share-confirmation') as HTMLDivElement; - - // Create the monaco editor - editor = monaco.editor.create(editorDiv); - - // If URL is a sharing link, populate the editor with the code from the link. - // Otherwise, populate with sample code. - const params = new URLSearchParams(window.location.search); - - let code = sampleCode; - if (params.get("code")) { - const base64code = decodeURIComponent(params.get("code")!); - code = base64ToCode(base64code); - } - - let srcModel = monaco.editor.createModel(code, 'qsharp'); - editor.setModel(srcModel); - - // As code is edited check it for errors and update the error list - async function check() { - // TODO: As this is async, code may be being edited while earlier check calls are still running. - // Need to ensure that if this occurs, wait and try again on the next animation frame. - // i.e. Don't queue a bunch of checks if some are still outstanding - diagnosticsFrame = 0; - let code = srcModel.getValue(); - let errs = await compiler.checkCode(code); - errorsDiv.innerText = JSON.stringify(errs, null, 2); - - // Note that as this is async, the code may have changed since checkCode was called. - // TODO: Account for this scenario (e.g. delta positions with old source version) - squiggleDiagnostics(errs); - errs.length ? - runButton.setAttribute("disabled", "true") : runButton.removeAttribute("disabled"); - } - - // While the code is changing, update the diagnostics as fast as the browser will render frames - let diagnosticsFrame = requestAnimationFrame(check); - - srcModel.onDidChangeContent(ev => { - if (!diagnosticsFrame) { - diagnosticsFrame = requestAnimationFrame(check); - } - }); - - // If the browser window resizes, tell the editor to update it's layout - window.addEventListener('resize', _ => editor.layout()); - - // Try to evaluate the code when the run button is clicked - runButton.addEventListener('click', async _ => { - // TODO: Handle if compiler is already running. - // TODO: Make the editor read only while code is running. (Maybe?) Perhaps - // the simulator and the editor intellisense should run in different workers? - let code = srcModel.getValue(); - let expr = exprInput.value; - let shots = parseInt(shotCount.value); - - // State for tracking as shot results are reported - let currentShotResult: ShotResult = { - success: false, - result: "pending", - events: [] - }; - - try { - performance.mark("start-shots"); - evtHander.clearResults(); - // TODO: Update the handler to show results as they come in. - await compiler.run(code, expr, shots, evtHander); - runResults = evtHander.getResults(); - } catch (e: any) { - // TODO: Should only happen on crash. Telmetry? - } - performance.mark("end-shots"); - let measure = performance.measure("shots-duration", "start-shots", "end-shots"); - console.info(`Ran ${shots} shots in ${measure.duration}ms`); - runComplete(); - }); - - // Example of getting results from a call into the WASM module - monaco.languages.registerCompletionItemProvider("qsharp", { - provideCompletionItems(model, position, context, token) { - // @ts-ignore : This is required in the defintion, but not needed. - var range: monaco.IRange = undefined; - - // TODO: CancellationToken - return compiler.getCompletions().then(rawList => { - let mapped: monaco.languages.CompletionList = { - suggestions: rawList?.items?.map(item => ({ - label: item.label, - kind: item.kind, // TODO: Monaco seems to use different values than VS Code. - insertText: item.label, - range - })) - }; - return mapped; - }); - } - }); - - // Render katas. - PopulateKatasList() - .then(() => RenderKatas()) - .then(() => { - let modulesSelect = document.querySelector('#katas-list') as HTMLSelectElement; - modulesSelect.addEventListener('change', _ => { - RenderKatas(); - }); - }); - - shareButton.addEventListener('click', _ => { - const code = srcModel.getValue(); - const encodedCode = codeToBase64(code); - const escapedCode = encodeURIComponent(encodedCode); - - // Get current URL without query parameters to use as the base URL - const newUrl = `${window.location.href.split('?')[0]}?code=${escapedCode}`; - // Copy link to clipboard and update url without reloading the page - navigator.clipboard.writeText(newUrl); - window.history.pushState({}, '', newUrl); - shareConfirmation.style.display = "inline"; - }); -} - -const reKetResult = /^\[(?:(Zero|One), *)*(Zero|One)\]$/ -function resultToKet(result: string | VSDiagnostic): string { - if (typeof result !== 'string') return "ERROR"; - - if (reKetResult.test(result)) { - // The result is a simple array of Zero and One - // The below will return an array of "Zero" or "One" in the order found - let matches = result.match(/(One|Zero)/g); - matches?.reverse(); - let ket = "|"; - matches?.forEach(digit => ket += (digit == "One" ? "1" : "0")); - ket += "⟩"; - return ket; - } else { - return result; - } -} - -function renderOutputs(container: HTMLDivElement) { - container.innerHTML = ""; - let mappedResults = runResults.map(result => ({ - result: resultToKet(result.result), - events: result.events, - error: (typeof result.result === 'string') ? undefined : result.result - })); - - let filteredResults = currentFilter == "" ? mappedResults : - mappedResults.filter(entry => entry.result === currentFilter); - - if (filteredResults.length === 0) return; - - // Show the current result and navigation. - let header = document.createElement("div"); - let prev = document.createElement("button"); - prev.textContent = "Prev"; - let next = document.createElement("button"); - next.textContent = "Next"; - let title = document.createElement("span"); - title.className = "result-header"; - let dumpTables = document.createElement("div"); - - header.appendChild(prev); - header.appendChild(next); - header.appendChild(title); - - container.appendChild(header); - container.appendChild(dumpTables); - - let currentIndex = 0; - function showOutput(move: number) { - currentIndex += move; - if (currentIndex < 0) currentIndex = 0; - if (currentIndex >= filteredResults.length) currentIndex = filteredResults.length - 1; - - let current = filteredResults[currentIndex]; - title.innerText = `Output for shot #${currentIndex + 1} of ${filteredResults.length}`; - - let resultHeader = `

Result: ${current.result}`; - if (current.error) { - let pos = editor.getModel()?.getPositionAt(current.error.start_pos); - resultHeader += ` - "${current.error.message}" at line ${pos?.lineNumber}, col ${pos?.column}`; - squiggleDiagnostics([current.error]); - } else { - squiggleDiagnostics([]); - } - resultHeader += "

"; - dumpTables.innerHTML = resultHeader; - - filteredResults[currentIndex].events.forEach(event => { - switch (event.type) { - case "Message": - // A Message output - let div = document.createElement("div"); - div.className = "message-output"; - div.innerText = event.message - dumpTables.appendChild(div); - break; - case "DumpMachine": - // A DumpMachine output - let table = document.createElement("table"); - table.innerHTML = renderDump(event.state); - dumpTables.appendChild(table); - } - }); - } - - prev.addEventListener('click', _ => showOutput(-1)); - next.addEventListener('click', _ => showOutput(1)); - showOutput(0); -} - -function runComplete() { - let outputDiv = document.querySelector('#output') as HTMLDivElement; - outputDiv.innerHTML = ""; - currentFilter = ""; - if (!runResults.length) return; - - // Get an array of results, preferably in ket form - let histogramData = runResults.map(result => resultToKet(result.result)); - let bucketData = generateHistogramData(histogramData); - let histogram = generateHistogramSvg(bucketData, (label) => { - currentFilter = label; - renderOutputs(outputContainer); - }); - outputDiv.appendChild(histogram); - - let outputContainer = document.createElement("div"); - outputDiv.appendChild(outputContainer); - renderOutputs(outputContainer); -} - -// Monaco provides the 'require' global for loading modules. -declare var require: any; -require.config({ paths: { vs: 'libs/monaco/vs' } }); -require(['vs/editor/editor.main'], loaded); diff --git a/playground/src/main.tsx b/playground/src/main.tsx new file mode 100644 index 0000000000..ced383b1b3 --- /dev/null +++ b/playground/src/main.tsx @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// + +import { render } from "preact"; +import { ICompilerWorker, QscEventTarget, getCompilerWorker, loadWasmModule, getAllKatas, Kata, VSDiagnostic } from "qsharp"; + +import { Nav } from "./nav.js"; +import { Editor } from "./editor.js"; +import { Results } from "./results.js"; +import { useState } from "preact/hooks"; +import { samples } from "./samples.js"; +import { Kata as Katas } from "./kata.js"; +import { base64ToCode } from "./utils.js"; + +const basePath = (window as any).qscBasePath || ""; +const monacoPath = basePath + "libs/monaco/vs"; +const modulePath = basePath + "libs/qsharp/qsc_wasm_bg.wasm"; +const workerPath = basePath + "libs/worker.js"; + +declare global { + var MathJax: { typeset: () => void; }; +} + +const wasmPromise = loadWasmModule(modulePath); // Start loading but don't wait on it + +function App(props: {compiler: ICompilerWorker, evtTarget: QscEventTarget, katas: Kata[], linkedCode?: string}) { + const [currentNavItem, setCurrentNavItem] = useState(props.linkedCode ? "linked" : "Minimal"); + const [shotError, setShotError] = useState(undefined); + + const kataTitles = props.katas.map(elem => elem.title); + const sampleTitles = Object.keys(samples); + + let sampleCode: string = (samples as any)[currentNavItem] || props.linkedCode; + + + const activeKata = kataTitles.includes(currentNavItem) ? + props.katas.find(kata => kata.title === currentNavItem) + : undefined; + + function onNavItemSelected(name: string) { + // If there was a ?code link on the URL before, clear it out + const params = new URLSearchParams(window.location.search); + if (params.get("code")) { + // Get current URL without query parameters to use as the URL + const newUrl = `${window.location.href.split('?')[0]}`; + window.history.pushState({}, '', newUrl); + } + setCurrentNavItem(name); + } + + function onShotError(diag?: VSDiagnostic) { + // TODO: Should this be for katas too and not just the main editor? + setShotError(diag); + } + + return (<> +
Q# playground
+ +{ + sampleCode ? <> + + + : + +} + ); +} + +// Called once Monaco is ready +async function loaded() { + await wasmPromise; // Block until the wasm module is loaded + const katas = await getAllKatas(); + const evtHander = new QscEventTarget(true); + const compiler = await getCompilerWorker(workerPath); + + // If URL is a sharing link, populate the editor with the code from the link. + // Otherwise, populate with sample code. + let linkedCode: string | undefined; + const params = new URLSearchParams(window.location.search); + if (params.get("code")) { + const base64code = decodeURIComponent(params.get("code")!); + linkedCode = base64ToCode(base64code); + } + + render(, document.body); +} + +// Monaco provides the 'require' global for loading modules. +declare var require: any; +require.config({ paths: { vs: monacoPath } }); +require(['vs/editor/editor.main'], loaded); diff --git a/playground/src/nav.tsx b/playground/src/nav.tsx new file mode 100644 index 0000000000..c6bff4e32c --- /dev/null +++ b/playground/src/nav.tsx @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export function Nav(props: { + selected: string, + navSelected: (name: string) => void, + katas: string[], + samples: string[]}) { + + function onSelected(name: string) { + props.navSelected(name); + } + + return ( + ); +} diff --git a/playground/src/results.tsx b/playground/src/results.tsx new file mode 100644 index 0000000000..0fa9c2757a --- /dev/null +++ b/playground/src/results.tsx @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { QscEventTarget, ShotResult, VSDiagnostic } from "qsharp"; +import { useEffect, useState } from "preact/hooks" + +import { Histogram } from "./histo.js"; +import { StateTable } from "./state.js"; + +const reKetResult = /^\[(?:(Zero|One), *)*(Zero|One)\]$/ +function resultToKet(result: string | VSDiagnostic): string { + if (typeof result !== 'string') return "ERROR"; + + if (reKetResult.test(result)) { + // The result is a simple array of Zero and One + // The below will return an array of "Zero" or "One" in the order found + let matches = result.match(/(One|Zero)/g); + matches?.reverse(); + let ket = "|"; + matches?.forEach(digit => ket += (digit == "One" ? "1" : "0")); + ket += "⟩"; + return ket; + } else { + return result; + } +} + +type ResultsState = { + shotCount: number; // How many shots have started + resultCount: number; // How many have completed (may be one less than above) + currIndex: number; // Which is currently being displayed + currResult: ShotResult | undefined; // The shot data to display + buckets: Map; // Histogram buckets + filterValue: string; // Any filter that is in effect (or "") + filterIndex: number; // The index into the filtered set + currArray: ShotResult[] // Used to detect a new run +}; + +function newRunState() { + return { + shotCount: 0, + resultCount: 0, + currIndex: 0, + currResult: undefined, + buckets: new Map(), + filterValue: "", + filterIndex: 0, + currArray: [] + }; +} + +function resultIsSame(a: ShotResult, b: ShotResult): boolean { + // If the length changed, any entries are different objects, or the final result has changed. + if (a.success !== b.success || + a.result !== b.result || + a.events.length !== b.events.length) return false; + + for(let i = 0; i < a.events.length; ++i) { + if (a.events[i] !== b.events[i]) return false; + } + + return true; +} + +export function Results(props: {evtTarget: QscEventTarget, showPanel: boolean, + onShotError?: (err?: VSDiagnostic) => void, kataMode?: boolean}) { + const [resultState, setResultState] = useState(newRunState()); + + // This is more complex than ideal for performance reasons. During a run, results may be getting + // updated thousands of times a second, but there is no point trying to render at more than 60fps. + // Therefore this subscribes to an event that happens once a frame if changes to results occur. + // As the results are mutated array, they don't make good props or state, so need to manually + // check for changes that would impact rendering and update state by creating new objects. + const evtTarget = props.evtTarget; + useEffect( () => { + const resultUpdateHandler = () => { + const results = evtTarget.getResults(); + + // If it's a new run, the entire results array will be a new object + const isNewResults = results !== resultState.currArray; + + // If the results object has changed then reset the current index and filter. + let newIndex = isNewResults ? 0 : resultState.currIndex; + let newFilterValue = isNewResults ? "" : resultState.filterValue; + let newFilterIndex = isNewResults ? 0 : resultState.filterIndex; + + const currentResult = resultState.currResult; + const updatedResult = newIndex < results.length ? + results[newIndex] : undefined; + + const replaceResult = isNewResults || + // One is defined but the other isn't + (!currentResult !== !updatedResult) || + // Or they both exist but are different (e.g. may have new events of have completed) + (currentResult && updatedResult && !resultIsSame(currentResult, updatedResult)); + + // Keep the old object if no need to replace it, else construct a new one + const newResult = !replaceResult ? currentResult : + !updatedResult ? undefined : { + success: updatedResult.success, + result: updatedResult.result, + events: [...updatedResult.events] + }; + + // Update the histogram if new results have come in. + // For now, just completely recreate the bucket map + const resultCount = evtTarget.resultCount(); + let buckets = resultState.buckets; + // If there are entirely new results, or if new results have been added, recalculate. + if (isNewResults || resultState.resultCount !== resultCount) { + buckets = new Map(); + for(let i = 0; i < resultCount; ++i) { + const key = results[i].result; + const strKey = resultToKet(key); + const newValue = (buckets.get(strKey) || 0) + 1; + buckets.set(strKey, newValue); + } + } + + // If anything needs updating, construct the new state object and store + if (replaceResult || + resultState.shotCount !== results.length || + resultState.resultCount !== resultCount || + resultState.currIndex !== newIndex) { + setResultState({ + shotCount: results.length, + resultCount: resultCount, + currIndex: newIndex, + currResult: newResult, + filterValue: newFilterValue, + filterIndex: newFilterIndex, + buckets, + currArray: results + }); + updateEditorError(newResult); + } + }; + + evtTarget.addEventListener('uiResultsRefresh', resultUpdateHandler); + + // Remove the event listener when this component is destroyed + return () => evtTarget.removeEventListener('uiResultsRefresh', resultUpdateHandler) + }, [evtTarget]) + + // If there's a filter set, there must have been at least one item for that result. + // If there's no filter set, may well be no results at all yet. + + const filterValue = resultState.filterValue; + const countForFilter = filterValue ? resultState.buckets.get(filterValue)! : resultState.shotCount; + const currIndex = filterValue ? resultState.filterIndex : resultState.currIndex; + const resultLabel = typeof resultState.currResult?.result === 'string' ? + resultToKet(resultState.currResult?.result || "") : + `ERROR: ${resultState.currResult?.result.message.replace(/\\n/g, "\n")}`; + + function moveToIndex(idx: number, filter: string) { + const results = evtTarget.getResults(); + + // The non-filtered default case + let currIndex = idx; + let currResult = results[idx]; + + // If a filter is in effect, need to find the filtered index + if (filter !== "") { + let found = 0; + for(let i = 0; i < results.length; ++i) { + // The buckets to filter on have been converted to kets where possible + if (resultToKet(results[i].result) !== filter) continue; + if (found === idx) { + currIndex = i; + currResult = results[i]; + break; + } + ++found; + } + } + setResultState({...resultState, filterValue: filter, filterIndex: idx, currIndex, currResult}); + updateEditorError(currResult); + } + + function updateEditorError(result?: ShotResult) { + if (!props.onShotError) return; + if (!result || result.success || typeof result.result === 'string') { + props.onShotError(); + } else { + props.onShotError(result.result) + } + } + + function onPrev() { + if (currIndex > 0) moveToIndex(currIndex - 1, filterValue); + } + + function onNext() { + if (currIndex < countForFilter - 1) moveToIndex(currIndex + 1, filterValue); + } + + return ( +
+{ props.showPanel ? +
+
RESULTS
+
AST
+
LOGS
+
+ : null +} + { !resultState.shotCount ? null : <> + {resultState.buckets.size > 1 ? + moveToIndex(0, val)}> + : null + } + { props.kataMode ? null : <> +
+
Shot {currIndex + 1} of {countForFilter}
+
Prev | Next
+
+
Result: {resultLabel}
+ } +
+ {resultState.currResult?.events.map(evt => { + return evt.type === "Message" ? + (
> {evt.message}
) : + () + })} +
+ } +
); +} diff --git a/playground/src/samples.ts b/playground/src/samples.ts new file mode 100644 index 0000000000..004fb28cf3 --- /dev/null +++ b/playground/src/samples.ts @@ -0,0 +1,481 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// TODO: This should be generated from the /samples directory, not hard-coded. + +const minimal = `namespace Sample { + open Microsoft.Quantum.Diagnostics; + + @EntryPoint() + operation Main() : Result[] { + // TODO + return []; + } +}`; + +const bellState = `namespace Sample { + open Microsoft.Quantum.Diagnostics; + + @EntryPoint() + operation Main() : Result[] { + use q1 = Qubit(); + use q2 = Qubit(); + + H(q1); + CNOT(q1, q2); + DumpMachine(); + + let m1 = M(q1); + let m2 = M(q2); + + Reset(q1); + Reset(q2); + + return [m1, m2]; + } +}`; + +const teleportation = `namespace Microsoft.Quantum.Samples.Teleportation { + open Microsoft.Quantum.Canon; + open Microsoft.Quantum.Intrinsic; + open Microsoft.Quantum.Random; + + ////////////////////////////////////////////////////////////////////////// + // Introduction ////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////// + + // Quantum teleportation provides a way of moving a quantum state from one + // location to another without having to move physical particle(s) along + // with it. This is done with the help of previously shared quantum + // entanglement between the sending and the receiving locations and + // classical communication. + + ////////////////////////////////////////////////////////////////////////// + // Teleportation ///////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////// + + /// # Summary + /// Sends the state of one qubit to a target qubit by using + /// teleportation. + /// + /// Notice that after calling Teleport, the state of "msg" is + /// collapsed. + /// + /// # Input + /// ## msg + /// A qubit whose state we wish to send. + /// ## target + /// A qubit initially in the |0〉 state that we want to send + /// the state of msg to. + operation Teleport (msg : Qubit, target : Qubit) : Unit { + use register = Qubit(); + // Create some entanglement that we can use to send our message. + H(register); + CNOT(register, target); + + // Encode the message into the entangled pair. + CNOT(msg, register); + H(msg); + + // Measure the qubits to extract the classical data we need to + // decode the message by applying the corrections on + // the target qubit accordingly. + if M(msg) == One { Z(target); } + // Correction step + if M(register) == One { + X(target); + // Reset register to Zero state before releasing + X(register); + } + } + + // One can use quantum teleportation circuit to send an unobserved + // (unknown) classical message from source qubit to target qubit + // by sending specific (known) classical information from source + // to target. + + /// # Summary + /// Uses teleportation to send a classical message from one qubit + /// to another. + /// + /// # Input + /// ## message + /// If \`true\`, the source qubit (\`here\`) is prepared in the + /// |1〉 state, otherwise the source qubit is prepared in |0〉. + /// + /// ## Output + /// The result of a Z-basis measurement on the teleported qubit, + /// represented as a Bool. + operation TeleportClassicalMessage (message : Bool) : Bool { + // Ask for some qubits that we can use to teleport. + use (msg, target) = (Qubit(), Qubit()); + + // Encode the message we want to send. + if message { + X(msg); + } + + // Use the operation we defined above. + Teleport(msg, target); + + // Check what message was received. + let result = (M(target) == One); + + // Reset qubits to Zero state before releasing + Reset(msg); + Reset(target); + + return result; + } + + /// # Summary + /// Sets the qubit's state to |+⟩. + operation SetToPlus(q: Qubit) : Unit { + Reset(q); + H(q); + } + + /// # Summary + /// Sets the qubit's state to |−⟩. + operation SetToMinus(q: Qubit) : Unit { + Reset(q); + X(q); + H(q); + } + + /// # Summary + /// Returns true if qubit is |+⟩ (assumes qubit is either |+⟩ or |−⟩) + operation MeasureIsPlus(q: Qubit) : Bool { + Measure([PauliX], [q]) == Zero + } + + /// # Summary + /// Returns true if qubit is |−⟩ (assumes qubit is either |+> or |−⟩) + operation MeasureIsMinus(q: Qubit) : Bool { + Measure([PauliX], [q]) == One + } + + /// # Summary + /// Randomly prepares the qubit into |+⟩ or |−⟩ + operation PrepareRandomMessage(q: Qubit) : Unit { + let choice = DrawRandomInt(0, 1) == 1; + + if choice { + Message("Sending |->"); + SetToMinus(q); + } else { + Message("Sending |+>"); + SetToPlus(q); + } + } + + // One can also use quantum teleportation to send any quantum state + // without losing any information. The following sample shows + // how a randomly picked non-trivial state (|-> or |+>) + // gets moved from one qubit to another. + + /// # Summary + /// Uses teleportation to send a randomly picked |-> or |+> state + /// to another. + operation TeleportRandomMessage () : Unit { + // Ask for some qubits that we can use to teleport. + use (msg, target) = (Qubit(), Qubit()); + PrepareRandomMessage(msg); + + // Use the operation we defined above. + Teleport(msg, target); + + // Report message received: + if MeasureIsPlus(target) { Message("Received |+>"); } + if MeasureIsMinus(target) { Message("Received |->"); } + + // Reset all of the qubits that we used before releasing + // them. + Reset(msg); + Reset(target); + } + + @EntryPoint() + operation Main () : Unit { + for idxRun in 1 .. 10 { + let sent = DrawRandomInt(0, 1) == 1; + let received = TeleportClassicalMessage(sent); + Message( + "Round " + AsString(idxRun) + + ": Sent " + AsString(sent) + + ", got " + AsString(received) + "."); + Message(sent == received ? "Teleportation successful!" | ""); + } + for idxRun in 1 .. 10 { + TeleportRandomMessage(); + } + + } +} + +// //////////////////////////////////////////////////////////////////////// +// Other teleportation scenarios not illustrated here +// //////////////////////////////////////////////////////////////////////// + +// ● Teleport a rotation. Rotate a basis state by a certain angle φ ∈ [0, 2π), +// for example by preparing Rₓ(φ) |0〉, and teleport the rotated state to the target qubit. +// When successful, the target qubit captures the angle φ [although, on course one does +// not have classical access to its value]. +// ● "Super dense coding". Given an EPR state |β〉 shared between the source and target +// qubits, the source can encode two classical bits a,b by applying Z^b X^a to its half +// of |β〉. Both bits can be recovered on the target by measurement in the Bell basis. +// For details refer to discussion and code in Unit Testing Sample, in file SuperdenseCoding.qs. +// ////////////////////////////////////////////////////////////////////////`; + +const qrng = `namespace Microsoft.Quantum.Samples.Qrng { + open Microsoft.Quantum.Math; + open Microsoft.Quantum.Diagnostics; + + operation SampleQuantumRandomNumberGenerator() : Result { + use q = Qubit(); // Allocate a qubit. + H(q); // Put the qubit to superposition. It now has a 50% chance of being 0 or 1. + let result = M(q); // Measure the qubit value, but don't look at the result yet. + Reset(q); // Reset qubit to Zero state. + return result; // Return result of the measurement. + } + + operation SampleRandomNumberInRange(max : Int) : Int { + mutable bits = []; + for idxBit in 1..BitSizeI(max) { + set bits += [SampleQuantumRandomNumberGenerator()]; + } + let sample = ResultArrayAsInt(bits); + return sample > max + ? SampleRandomNumberInRange(max) + | sample; + } + + /// Produces a non-negative integer from a string of bits in little endian format. + function ResultArrayAsInt(input : Result[]) : Int { + let nBits = Length(input); + // We are constructing a 64-bit integer, and we won't use the highest (sign) bit. + Fact(nBits < 64, "Input length must be less than 64."); + mutable number = 0; + for i in 0..nBits-1 { + if input[i] == One { + // If we assume loop unrolling, 2^i will be optimized to a constant. + set number |||= 2^i; + } + } + return number; + } + + @EntryPoint() + operation Main() : Int { + let max = 50; + Message("Sampling a random number between 0 and " + + AsString(max) + ": "); + return SampleRandomNumberInRange(max); + } +}`; + +const deutsch = `// First, note that every Q# function must have a namespace. We define +// a new one for this purpose. +namespace Microsoft.Quantum.Samples.DeutschJozsa { + open Microsoft.Quantum.Diagnostics; + open Microsoft.Quantum.Math; + + + ////////////////////////////////////////////////////////////////////////// + // Deutsch–Jozsa Quantum Algorithm /////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////// + + /// # Summary + /// Deutsch–Jozsa is a quantum algorithm that decides whether a given Boolean function + /// 𝑓 that is promised to either be constant or to be balanced — i.e., taking the + /// values 0 and 1 the exact same number of times — is actually constant or balanced. + /// The operation \`IsConstantBooleanFunction\` answers this question by returning the + /// Boolean value \`true\` if the function is constant and \`false\` if it is not. Note + /// that the promise that the function is either constant or balanced is assumed. + /// + /// # Input + /// ## Uf + /// A quantum operation that implements |𝑥〉|𝑦〉 ↦ |𝑥〉|𝑦 ⊕ 𝑓(𝑥)〉, + /// where 𝑓 is a Boolean function, 𝑥 is an 𝑛 bit register and 𝑦 is a single qubit. + /// ## n + /// The number of bits of the input register |𝑥〉. + /// + /// # Output + /// A boolean value \`true\` that indicates that the function is constant and \`false\` + /// that indicates that the function is balanced. + /// + /// # See Also + /// - For details see Section 1.4.3 of Nielsen & Chuang. + /// + /// # References + /// - [ *Michael A. Nielsen , Isaac L. Chuang*, + /// Quantum Computation and Quantum Information ](http://doi.org/10.1017/CBO9780511976667) + operation IsConstantBooleanFunction (Uf : ((Qubit[], Qubit) => Unit), n : Int) : Bool { + // Now, we allocate n + 1 clean qubits. Note that the function Uf is defined + // on inputs of the form (x, y), where x has n bits and y has 1 bit. + use queryRegister = Qubit[n]; + use target = Qubit(); + // The last qubit needs to be flipped so that the function will + // actually be computed into the phase when Uf is applied. + X(target); + + // Now, a Hadamard transform is applied to each of the qubits. + + H(target); + // We use a within-apply block to ensure that the Hadamard transform is + // correctly inverted on |𝑥〉 register. + within { + for q in queryRegister { + H(q); + } + } apply { + // We now apply Uf to the n + 1 qubits, computing |𝑥, 𝑦〉 ↦ |𝑥, 𝑦 ⊕ 𝑓(𝑥)〉. + Uf(queryRegister, target); + } + + // The following for-loop measures all qubits and resets them to + // zero so that they can be safely returned at the end of the using-block. + // The loop also leaves result as \`true\` if all measurement results + // are \`Zero\`, i.e., if the function was a constant function and sets + // result to \`false\` if not, which according to the promise on 𝑓 means + // that it must have been balanced. + mutable result = true; + for q in queryRegister { + if M(q) == One { + X(q); + set result = false; + } + } + + // Finally, the last qubit, which held the 𝑦-register, is reset. + Reset(target); + + return result; + } + + // Simple constant Boolean function + operation SimpleConstantBooleanFunction(args: Qubit[], target: Qubit): Unit { + X(target); + } + + // A more complex constant Boolean function. It applies X for every input basis vector. + operation ConstantBooleanFunction(args: Qubit[], target: Qubit): Unit { + for i in 0..(2^Length(args))-1 { + ApplyControlledOnInt(i, args, X, target); + } + } + + // A more complex balanced Boolean function. It applies X for half of the input basis verctors. + operation BalancedBooleanFunction(args: Qubit[], target: Qubit): Unit { + for i in 0..2..(2^Length(args))-1 { + ApplyControlledOnInt(i, args, X, target); + } + } + + // Applies operator \`op\` on each qubit in the \`qubits\` array if the corresponding + // bit in the LittleEndian \`number\` matches the given \`bitApply\`. + operation ApplyOpFromInt( + number: Int, + bitApply: Bool, + op:(Qubit => Unit is Adj), + qubits: Qubit[]): Unit is Adj { + + Fact(number>=0, "number must be non-negative"); + + for i in 0..qubits::Length-1 { + // If we assume loop unrolling, 2^i will be optimized to a constant. + if (((number &&& 2^i) != 0) == bitApply) { + op(qubits[i]); + } + } + } + + // Applies a unitary operation \`oracle\` on the target qubit if the control + // register state corresponds to a specified nonnegative integer \`numberState\`. + operation ApplyControlledOnInt( + numberState: Int, + controls: Qubit[], + oracle:(Qubit => Unit is Ctl), + target: Qubit): Unit { + + within { + ApplyOpFromInt(numberState, false, X, controls); + } apply { + Controlled oracle(controls, target); + } + } + + @EntryPoint() + operation Main() : Unit { + // Constant versus Balanced Functions with the Deutsch–Jozsa Algorithm: + + // A Boolean function is a function that maps bitstrings to a + // bit, + // + // 𝑓 : {0, 1}^n → {0, 1}. + // + // We say that 𝑓 is constant if 𝑓(𝑥⃗) = 𝑓(𝑦⃗) for all bitstrings + // 𝑥⃗ and 𝑦⃗, and that 𝑓 is balanced if 𝑓 evaluates to true (1) for + // exactly half of its inputs. + + // If we are given a function 𝑓 as a quantum operation 𝑈 |𝑥〉|𝑦〉 + // = |𝑥〉|𝑦 ⊕ 𝑓(𝑥)〉, and are promised that 𝑓 is either constant or + // is balanced, then the Deutsch–Jozsa algorithm decides between + // these cases with a single application of 𝑈. + + // In SimpleAlgorithms.qs, we implement this algorithm as + // RunDeutschJozsa, following the pattern above. + // We check by ensuring that RunDeutschJozsa returns true + // for constant functions and false for balanced functions. + + if (not IsConstantBooleanFunction(SimpleConstantBooleanFunction, 5)) { + fail "SimpleConstantBooleanFunction should be detected as constant"; + } + Message("SimpleConstantBooleanFunction detected as constant"); + + if (not IsConstantBooleanFunction(ConstantBooleanFunction, 5)) { + fail "ConstantBooleanFunction should be detected as constant"; + } + Message("ConstantBooleanFunction detected as constant"); + + if (IsConstantBooleanFunction(BalancedBooleanFunction, 5)) { + fail "BalancedBooleanFunction should be detected as balanced"; + } + Message("BalancedBooleanFunction detected as balanced"); + + Message("All functions measured successfully!"); + } +}`; + +const grover = `// This is NOT Gover's, but does show phases nicely. + +namespace Sample { + @EntryPoint() + + operation AllBasisVectorsWithPhases_TwoQubits() : Unit { + use q1 = Qubit(); + use q4 = Qubit(); + + H(q1); + R1(0.3, q1); + H(q4); + + use q5 = Qubit(); + use q6 = Qubit(); + S(q5); + + Rxx(1.0, q5, q6); + + Microsoft.Quantum.Diagnostics.DumpMachine(); + } +}` + +export const samples = { + "Minimal": minimal, + "Bell state": bellState, + "Teleportation": teleportation, + "Random numbers": qrng, + "Deutsch-Josza": deutsch, + "Grover's search": grover, + "Shor's algorithm": "// TODO" +}; diff --git a/playground/src/state.tsx b/playground/src/state.tsx new file mode 100644 index 0000000000..599568b2bc --- /dev/null +++ b/playground/src/state.tsx @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dump } from "qsharp"; + +function probability(real: number, imag: number) { + return (real * real + imag * imag); +} + +function formatComplex(real: number, imag: number) { + // toLocaleString() correctly identifies -0 in JavaScript + // String interpolation drops minus sign from -0 + // − is the unicode minus sign, 𝑖 is the mathematical i + const realPart = `${real.toLocaleString()[0] === "-" ? "−" : ""}${Math.abs(real).toFixed(4)}`; + const imagPart = `${imag.toLocaleString()[0] === "-" ? "−" : "+"}${Math.abs(imag).toFixed(4)}𝑖`; + return `${realPart}${imagPart}`; +} + +export function StateTable(props: {dump: Dump}) { + return ( + + + + + + + + + + +{ Object.keys(props.dump).map(basis => { + const [real, imag] = props.dump[basis]; + const complex = formatComplex(real, imag) + const probabilityPercent = probability(real, imag) * 100; + const phase = Math.atan2(imag, real); + const phaseStyle = `transform: rotate(${phase.toFixed(4)}rad)`; + return ( + + + + + + + ); +})} + +
Basis State
(|𝜓ₙ…𝜓₁⟩)
AmplitudeMeasurement ProbabilityPhase
{basis}{complex} + + {probabilityPercent.toFixed(4)}% + {phase.toFixed(4)}
); +} diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 6e0f09be0d..141f792539 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -213,14 +213,18 @@ where let len = label.as_ref().map_or(1, |lbl| lbl.len().max(1)); let severity = err.severity().unwrap_or(Severity::Error); - let mut message = err.to_string(); + let mut pre_message = err.to_string(); for source in iter::successors(err.source(), |e| e.source()) { - write!(message, ": {source}").expect("message should be writable"); + write!(pre_message, ": {source}").expect("message should be writable"); } if let Some(help) = err.help() { - write!(message, "\n\nhelp: {help}").expect("message should be writable"); + write!(pre_message, "\n\nhelp: {help}").expect("message should be writable"); } + // Newlines in JSON need to be double escaped + // TODO: Maybe some other chars too: https://stackoverflow.com/a/5191059 + let message = pre_message.replace('\n', "\\\\n"); + VSDiagnostic { start_pos: offset, end_pos: offset + len, @@ -418,7 +422,7 @@ mod test { assert_eq!(err.start_pos, 32); assert_eq!(err.end_pos, 33); - assert_eq!(err.message, "type error: missing type in item signature\n\nhelp: types cannot be inferred for global declarations"); + assert_eq!(err.message, "type error: missing type in item signature\\\\n\\\\nhelp: types cannot be inferred for global declarations"); } #[test]