diff --git a/website2/.gitignore b/website2/.gitignore index cb0da3d..52d6d38 100644 --- a/website2/.gitignore +++ b/website2/.gitignore @@ -21,3 +21,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Custom +/public/value_script_bg.wasm diff --git a/website2/package-lock.json b/website2/package-lock.json index ba0cd7c..e3cbddf 100644 --- a/website2/package-lock.json +++ b/website2/package-lock.json @@ -21,8 +21,10 @@ "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", + "monaco-editor": "^0.36.1", "sass": "^1.60.0", "typescript": "^4.9.3", + "valuescript": "^0.0.4", "vite": "^4.2.0" } }, @@ -2553,6 +2555,12 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.36.1.tgz", + "integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==", + "dev": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3386,6 +3394,15 @@ "punycode": "^2.1.0" } }, + "node_modules/valuescript": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/valuescript/-/valuescript-0.0.4.tgz", + "integrity": "sha512-fYzoanlq7loL6co6WEw5+5RwcoIN6vOoOkTaotkzmYIsWAuAf/rUlnOWteR962vQdnrVZDS0RUMvJiXzJbBS3Q==", + "dev": true, + "dependencies": { + "wait-your-turn": "^1.0.1" + } + }, "node_modules/vite": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", @@ -3435,6 +3452,12 @@ } } }, + "node_modules/wait-your-turn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wait-your-turn/-/wait-your-turn-1.0.1.tgz", + "integrity": "sha512-UejbIY32KXhghXGwH4J2pTKUNvgdrCjdDGrtrdfHHJUAwXCok1l9ptEp4n13lg6PuyQIgxPGkWyKeJvvKeAqsA==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5196,6 +5219,12 @@ "brace-expansion": "^1.1.7" } }, + "monaco-editor": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.36.1.tgz", + "integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5763,6 +5792,15 @@ "punycode": "^2.1.0" } }, + "valuescript": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/valuescript/-/valuescript-0.0.4.tgz", + "integrity": "sha512-fYzoanlq7loL6co6WEw5+5RwcoIN6vOoOkTaotkzmYIsWAuAf/rUlnOWteR962vQdnrVZDS0RUMvJiXzJbBS3Q==", + "dev": true, + "requires": { + "wait-your-turn": "^1.0.1" + } + }, "vite": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", @@ -5776,6 +5814,12 @@ "rollup": "^3.18.0" } }, + "wait-your-turn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wait-your-turn/-/wait-your-turn-1.0.1.tgz", + "integrity": "sha512-UejbIY32KXhghXGwH4J2pTKUNvgdrCjdDGrtrdfHHJUAwXCok1l9ptEp4n13lg6PuyQIgxPGkWyKeJvvKeAqsA==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/website2/package.json b/website2/package.json index 4281147..0ca96ef 100644 --- a/website2/package.json +++ b/website2/package.json @@ -22,8 +22,10 @@ "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", + "monaco-editor": "^0.36.1", "sass": "^1.60.0", "typescript": "^4.9.3", + "valuescript": "^0.0.4", "vite": "^4.2.0" } } diff --git a/website2/src/playground/Ripple-2s-200px.svg b/website2/src/playground/Ripple-2s-200px.svg new file mode 100644 index 0000000..f0306a2 --- /dev/null +++ b/website2/src/playground/Ripple-2s-200px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website2/src/playground/files.ts b/website2/src/playground/files.ts new file mode 100644 index 0000000..f22230e --- /dev/null +++ b/website2/src/playground/files.ts @@ -0,0 +1,501 @@ +import nil from './helpers/nil'; + +function blockTrim(text: string) { + let lines = text.split('\n'); + + while (lines.length > 0 && /^ *$/.test(lines[0])) { + lines.shift(); + } + + while (lines.length > 0 && /^ *$/.test(lines[lines.length - 1])) { + lines.pop(); + } + + let minIndent = Infinity; + + for (const line of lines) { + if (line.trim() === '') { + continue; + } + + const match = line.match(/^ */); + + if (match === null || match[0].length >= minIndent) { + continue; + } + + minIndent = match[0].length; + } + + lines = lines.map((line) => line.slice(minIndent)); + + return lines.join('\n'); +} + +const files: Record = { + 'tutorial/hello.ts': blockTrim(` + // Welcome to the ValueScript playground! + // + // This playground also acts as a tutorial by describing a variety of + // examples. All examples are editable with live updates to their outputs. + // + // Keeping with tradition, here is the hello world program. + + export default function main() { + return "Hello world!"; + } + + // When you're ready, click the next arrow ('>') above to continue. + `), + + 'tutorial/valueSemantics.ts': blockTrim(` + export default function main() { + const leftBowl = ['apple', 'mango']; + + let rightBowl = leftBowl; + rightBowl.push('peach'); + + return leftBowl.includes("peach"); + // TypeScript: true + // ValueScript: false + } + + // In TypeScript, \`leftBowl\` and \`rightBowl\` are the same object, and + // that object changes. In ValueScript, objects are just data, they don't + // change. When you change \`rightBowl\`, you are changing the *variable* + // and therefore \`leftBowl\` doesn't change. + `), + + 'tutorial/revertOnCatch.ts': blockTrim(` + export default function () { + let x = 0; + + try { + x++; + throw new Error("boom"); + } catch {} + + return x; + // TypeScript: 1 + // ValueScript: 0 + } + + // In ValueScript, a try block is a transaction - it either runs to + // completion, or it is reverted. This is impractical in TypeScript, + // but in ValueScript all we have to do is snapshot the variables and + // restore from them on catch. This works because all mutation is + // local - nothing else can be affected. + `), + + 'examples/factorial.ts': blockTrim(` + export default function main() { + return factorial(5); + } + + function factorial(n: number): number { + if (n === 0) { + return 1; + } + + return n * factorial(n - 1); + } + `), + + 'examples/counter.ts': blockTrim(` + export default function main() { + let c = new Counter(); + + return [c.get(), c.get(), c.get()]; + } + + class Counter { + next = 1; + + get() { + return this.next++; + } + } + `), + + 'examples/reverse.ts': blockTrim(` + export default function main() { + const values = ['a', 'b', 'c']; + + return [values, reverse(values)]; + } + + function reverse(arr: T[]) { + let left = 0; + let right = arr.length - 1; + + while (left < right) { + [arr[left], arr[right]] = [arr[right], arr[left]]; + + left++; + right--; + } + + return arr; + + // This version also works: + // arr.reverse(); + // return arr; + } + `), + + 'examples/binaryTree.ts': blockTrim(` + export default function main() { + let tree = new BinaryTree(); + + tree.insert(2); + tree.insert(5); + tree.insert(1); + + const treeSnapshot = tree; + + tree.insert(3); + tree.insert(4); + + return [treeSnapshot.toArray(), tree.toArray()]; + } + + class BinaryTree { + left?: BinaryTree; + value?: T; + right?: BinaryTree; + + insert(newValue: T) { + if (this.value === undefined) { + this.value = newValue; + return; + } + + if (newValue < this.value) { + this.left ??= new BinaryTree(); + this.left.insert(newValue); + } else { + this.right ??= new BinaryTree(); + this.right.insert(newValue); + } + } + + toArray() { + let res: T[] = []; + + if (this.left) { + res = res.concat(this.left.toArray()); + } + + if (this.value !== undefined) { + res.push(this.value); + } + + if (this.right) { + res = res.concat(this.right.toArray()); + } + + return res; + } + } + `), + + 'examples/mergeSort.ts': blockTrim(` + export default function main() { + const x = [7, 18, 9, 11, 16, 3, 8, 2, 5, 4, 6, 14, 15, 17, 10, 12, 1, 13]; + + return mergeSort(x, (a, b) => a - b); + } + + function mergeSort(vals: T[], cmp: (a: T, b: T) => number): T[] { + const len = vals.length; + + if (len <= 1) { + return vals; + } + + if (len === 2) { + if (cmp(vals[0], vals[1]) > 0) { + return [vals[1], vals[0]]; + } + + return vals; + } + + const mid = vals.length / 2; + + const leftSorted = mergeSort(vals.slice(0, mid), cmp); + const rightSorted = mergeSort(vals.slice(mid), cmp); + + let res: T[] = []; + + let left = 0; + const leftLen = leftSorted.length; + let right = 0; + const rightLen = rightSorted.length; + + while (left < leftLen && right < rightLen) { + if (cmp(leftSorted[left], rightSorted[right]) <= 0) { + res.push(leftSorted[left++]); + } else { + res.push(rightSorted[right++]); + } + } + + while (left < leftLen) { + res.push(leftSorted[left++]); + } + + while (right < rightLen) { + res.push(rightSorted[right++]); + } + + return res; + } + `), + + 'examples/quickSort.ts': blockTrim(` + export default function main() { + const x = [7, 18, 9, 11, 16, 3, 8, 2, 5, 4, 6, 14, 15, 17, 10, 12, 1, 13]; + + return quickSort(x, (a, b) => a - b); + } + + function quickSort(vals: T[], cmp: (a: T, b: T) => number) { + // Demonstrates the ability to do in-place updates in ValueScript. + // + // There's only one reference to \`vals\`, so we can mutate it in-place + // without violating value semantics. + // + // (At the time of writing the internals aren't very careful about + // minimizing ref counters so this might not be happening, but the logic + // to mutate in-place when the ref count is one is already there. This + // will be optimized/fixed in the future.) + // + // More on quickSort: + // https://www.youtube.com/watch?v=Hoixgm4-P4M + + const len = vals.length; + let ranges: [number, number][] = [[0, len - 1]]; + + while (true) { + const range = ranges.shift(); + + if (!range) { + return vals; + } + + const [start, end] = range; + + if (end - start <= 0) { + continue; + } + + let i = start; + let j = end; + + let pivotIndex = Math.floor((i + j) / 2); + [vals[pivotIndex], vals[j]] = [vals[j], vals[pivotIndex]]; + const pivot = vals[j]; + j--; + + while (true) { + while (cmp(vals[i], pivot) < 0) { + i++; + } + + while (cmp(vals[j], pivot) > 0) { + j--; + } + + if (i < j) { + [vals[i], vals[j]] = [vals[j], vals[i]]; + i++; + j--; + continue; + } + + [vals[i], vals[end]] = [vals[end], vals[i]]; + + ranges.push([start, i - 1]); + ranges.push([i + 1, end]); + + break; + } + } + } + `), + + 'examples/idGenerationError.ts': blockTrim(` + export default function main() { + let nextId = 1; + + function generateId() { + const result = nextId; + nextId++; + + return result; + } + + return [ + generateId(), + generateId(), + generateId(), + ]; + } + `), + + 'examples/idGeneration.ts': blockTrim(` + export default function main() { + let idGen = new IdGenerator(); + + return [ + idGen.generate(), + idGen.generate(), + idGen.generate(), + ]; + } + + class IdGenerator { + nextId: number; + + constructor() { + this.nextId = 1; + } + + generate() { + const result = this.nextId; + this.nextId++; + + return result; + } + } + `), + + 'examples/sideEffectsArticle/enablePirateError.ts': blockTrim(` + export default function main() { + let pirateEnabled = false; + + function greet() { + if (!pirateEnabled) { + return "Hi"; + } + + return "Ahoy"; + } + + function enablePirate() { + pirateEnabled = true; + return "Done"; + } + + return [ + greet(), + enablePirate(), + greet(), + ]; + } + `), + + 'examples/sideEffectsArticle/lyingAboutA.ts': blockTrim(` + export default function main() { + let a = 5; + a += 2; + + return a; + } + `), + + 'examples/sideEffectsArticle/add1To50WithMutation.ts': blockTrim(` + export default function main() { + let sum = 0; + + for (let i = 1; i <= 50; i++) { + sum += i; + } + + return sum; + } + `), + + 'examples/sideEffectsArticle/add1To50WithoutMutation.ts': blockTrim(` + export default function main() { + return makeRange(1, 51) + .reduce((a, b) => a + b); + } + + function makeRange(start: number, end: number): number[] { + if (start === end) { + return []; + } + + return [start].concat( + makeRange(start + 1, end), + ); + } + `), + + 'examples/sideEffectsArticle/enablePirateWorkaround.ts': blockTrim(` + export default function main() { + let pirateEnabled = false; + + function greet(pirateEnabled: boolean) { + if (!pirateEnabled) { + return "Hi"; + } + + return "Ahoy"; + } + + function enablePirate( + pirateEnabled: boolean, + ): [boolean, string] { + pirateEnabled = true; + return [pirateEnabled, "Done"]; + } + + const greetResponse1 = greet(pirateEnabled); + + let enablePirateResponse: string; + [pirateEnabled, enablePirateResponse] = enablePirate(pirateEnabled); + + const greetResponse2 = greet(pirateEnabled); + + return [ + greetResponse1, + enablePirateResponse, + greetResponse2, + ]; + } + `), + + 'examples/sideEffectsArticle/actorEnablePirate.ts': blockTrim(` + export default function main() { + let actor = new Actor(); + + return [ + actor.greet(), + actor.enablePirate(), + actor.greet(), + ]; + } + + class Actor { + pirateEnabled = false; + + greet() { + if (!this.pirateEnabled) { + return "Hi"; + } + + return "Ahoy"; + } + + enablePirate() { + this.pirateEnabled = true; + return "Done"; + } + } + `), +}; + +export default files; diff --git a/website2/src/playground/helpers/assert.ts b/website2/src/playground/helpers/assert.ts new file mode 100644 index 0000000..e4d4622 --- /dev/null +++ b/website2/src/playground/helpers/assert.ts @@ -0,0 +1,8 @@ +export default function assert( + value: boolean, + msg = "value was not true", +): asserts value { + if (value !== true) { + throw new Error(`Assertion failed: ${msg}`); + } +} diff --git a/website2/src/playground/helpers/nil.ts b/website2/src/playground/helpers/nil.ts new file mode 100644 index 0000000..9d392e5 --- /dev/null +++ b/website2/src/playground/helpers/nil.ts @@ -0,0 +1,4 @@ +const nil = undefined; +type nil = undefined; + +export default nil; diff --git a/website2/src/playground/helpers/notNil.ts b/website2/src/playground/helpers/notNil.ts new file mode 100644 index 0000000..f228958 --- /dev/null +++ b/website2/src/playground/helpers/notNil.ts @@ -0,0 +1,9 @@ +import nil from './nil'; + +export default function notNil(value: T | nil): T { + if (value === nil) { + throw new Error(); + } + + return value; +} diff --git a/website2/src/playground/index.html b/website2/src/playground/index.html new file mode 100644 index 0000000..84f9868 --- /dev/null +++ b/website2/src/playground/index.html @@ -0,0 +1,36 @@ + + + + + + + + + +
+
+
+
+
<
+
+ +
+
>
+
+ +
+
Outcome
+
"The playground is loading"
+ +
Diagnostics
+
+ +
Assembly
+
@main = function() { + mov "The playground is loading" %return +}
+
+ + + + diff --git a/website2/src/playground/index.ts b/website2/src/playground/index.ts new file mode 100644 index 0000000..502bf28 --- /dev/null +++ b/website2/src/playground/index.ts @@ -0,0 +1,256 @@ +import files from './files'; +import assert from './helpers/assert'; +import nil from './helpers/nil'; +import notNil from './helpers/notNil'; +import VslibPool, { + CompilerOutput, + Diagnostic, + Job, + RunResult, +} from './vslib/VslibPool'; +import monaco from './monaco'; + +function domQuery(query: string): T { + return notNil(document.querySelector(query) ?? nil); +} + +const editorEl = domQuery('#editor'); + +const selectEl = domQuery('#file-location select'); +const filePreviousEl = domQuery('#file-previous'); +const fileNextEl = domQuery('#file-next'); +const outcomeEl = domQuery('#outcome'); +const vsmEl = domQuery('#vsm'); +const diagnosticsEl = domQuery('#diagnostics'); + +for (const filename of Object.keys(files)) { + const option = document.createElement('option'); + option.textContent = filename; + selectEl.appendChild(option); +} + +let currentFile = ''; + +editorEl.innerHTML = ''; + +(async () => { + const vslibPool = new VslibPool(); + + (window as any).vslibPool = vslibPool; + + const editor = monaco.editor.create(editorEl, { + theme: 'vs-dark', + value: '', + language: 'typescript', + }); + + setTimeout(() => changeFile(location.hash.slice(1))); + + globalThis.addEventListener('hashchange', () => { + changeFile(location.hash.slice(1)); + }); + + globalThis.addEventListener('resize', () => editor.layout()); + + const model = notNil(editor.getModel() ?? nil); + + model.updateOptions({ tabSize: 2, insertSpaces: true }); + + function changeFile(newFile: string) { + if (currentFile === '') { + currentFile = Object.keys(files)[0]; + } else if (newFile === currentFile) { + return; + } + + if (newFile === '') { + newFile = Object.keys(files)[0]; + } + + const fileIdx = Object.keys(files).indexOf(newFile); + + if (fileIdx !== -1) { + currentFile = newFile; + } + + location.hash = currentFile; + selectEl.selectedIndex = fileIdx; + + const content = files[currentFile]; + assert(content !== nil); + + model.setValue(content); + } + + selectEl.addEventListener('change', () => { + changeFile(selectEl.value); + }); + + const moveFileIndex = (change: number) => () => { + const filenames = Object.keys(files); + let idx = filenames.indexOf(currentFile); + + if (idx === -1) { + throw new Error('This should not happen'); + } + + idx += change; + idx = Math.max(idx, 0); + idx = Math.min(idx, filenames.length - 1); + + changeFile(filenames[idx]); + }; + + filePreviousEl.addEventListener('click', moveFileIndex(-1)); + fileNextEl.addEventListener('click', moveFileIndex(1)); + + let timerId: undefined | number = undefined; + + model.onDidChangeContent(() => { + files[currentFile] = model.getValue(); + clearTimeout(timerId); + + timerId = setTimeout(handleUpdate, 100) as unknown as number; + }); + + let compileJob: Job | nil = nil; + let runJob: Job | nil = nil; + let updateId = 0; + + function handleUpdate() { + updateId++; + const currentUpdateId = updateId; + compileJob?.cancel(); + runJob?.cancel(); + + const source = model.getValue(); + + compileJob = vslibPool.compile(source); + runJob = vslibPool.run(source); + + renderJob( + compileJob, + vsmEl, + (el, compilerOutput) => { + el.textContent = compilerOutput.assembly.join('\n'); + }, + ); + + renderJob( + runJob, + outcomeEl, + (el, runResult) => { + if ('Ok' in runResult.output) { + el.textContent = runResult.output.Ok; + } else if ('Err' in runResult.output) { + el.textContent = `Uncaught exception: ${runResult.output.Err}`; + } else { + never(runResult.output); + } + + diagnosticsEl.innerHTML = ''; + + for (const diagnostic of runResult.diagnostics) { + const diagnosticEl = document.createElement('div'); + + diagnosticEl.classList.add( + 'diagnostic', + toKebabCase(diagnostic.level), + ); + + const { line, col } = toLineCol(source, diagnostic.span.start); + diagnosticEl.textContent = `${line}:${col}: ${diagnostic.message}`; + + diagnosticsEl.appendChild(diagnosticEl); + } + + monaco.editor.setModelMarkers( + model, + 'valuescript', + runResult.diagnostics.map((diagnostic) => { + const { line, col } = toLineCol(source, diagnostic.span.start); + const { line: endLine, col: endCol } = toLineCol( + source, + diagnostic.span.end, + ); + + return { + severity: toMonacoSeverity(diagnostic.level), + startLineNumber: line, + startColumn: col, + endLineNumber: endLine, + endColumn: endCol, + message: diagnostic.message, + }; + }), + ); + }, + ); + + function renderJob( + job: Job, + el: HTMLElement, + apply: (el: HTMLElement, jobResult: T) => void, + ) { + const startTime = Date.now(); + + const loadingInterval = setInterval(() => { + if (currentUpdateId === updateId) { + el.textContent = `Loading... ${ + ((Date.now() - startTime) / 1000).toFixed(1) + }s`; + } + }, 100); + + (async () => { + try { + apply(el, await job.wait()); + el.classList.remove('error'); + } catch (err: any) { + if (!(err instanceof Error)) { + // deno-lint-ignore no-ex-assign + err = new Error(`Non-error exception ${err}`); + } + + if (err.message !== 'Canceled') { + el.textContent = err.message; + el.classList.add('error'); + } + } finally { + clearInterval(loadingInterval); + } + })(); + } + } + + function toMonacoSeverity(level: Diagnostic['level']) { + switch (level) { + case 'Error': + return monaco.MarkerSeverity.Error; + case 'InternalError': + return monaco.MarkerSeverity.Error; + case 'Lint': + return monaco.MarkerSeverity.Warning; + case 'CompilerDebug': + return monaco.MarkerSeverity.Info; + } + } +})(); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function never(_: never): never { + throw new Error('This should not happen'); +} + +function toKebabCase(str: string): string { + // account for leading capital letters + str = str.replace(/^[A-Z]/, (match) => match.toLowerCase()); + + return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`); +} + +function toLineCol(str: string, index: number): { line: number; col: number } { + const lines = str.slice(0, index).split('\n'); + + return { line: lines.length, col: lines[lines.length - 1].length + 1 }; +} diff --git a/website2/src/playground/monaco.ts b/website2/src/playground/monaco.ts new file mode 100644 index 0000000..7de0461 --- /dev/null +++ b/website2/src/playground/monaco.ts @@ -0,0 +1,27 @@ +import * as monaco from 'monaco-editor'; +export { monaco as default }; +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; +import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; +import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; + +self.MonacoEnvironment = { + getWorker(_: any, label: string) { + if (label === 'json') { + return new jsonWorker(); + } + if (label === 'css' || label === 'scss' || label === 'less') { + return new cssWorker(); + } + if (label === 'html' || label === 'handlebars' || label === 'razor') { + return new htmlWorker(); + } + if (label === 'typescript' || label === 'javascript') { + return new tsWorker(); + } + return new editorWorker(); + }, +}; + +monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); diff --git a/website2/src/playground/styles.scss b/website2/src/playground/styles.scss new file mode 100644 index 0000000..934f13d --- /dev/null +++ b/website2/src/playground/styles.scss @@ -0,0 +1,214 @@ +body { + background-color: #1e1e1e; + color: #d4d4d4; + font-family: monospace; + font-size: 1.2em; + margin: 0; + overflow: hidden; + position: fixed; +} + +a, a:visited { + color: hsl(227, 83%, 69%); + text-decoration: none; +} + +#file-selector { + background-color: #181818; + position: absolute; + left: 0; + top: 0; + font-size: 1.5em; + display: flex; + width: 50vw; + border-bottom: 1px solid black; + user-select: none; +} + +#file-selector > div { + width: 2em; + padding: 0.5em; +} + +#file-location { + flex-grow: 1; +} + +#file-location select, #file-location option { + background-color: #181818; + border: none; + color: white; + font-family: monospace; + font-size: 1em; + width: 100%; + -webkit-appearance: none; + cursor: pointer; +} + +#file-previous, #file-next { + color: hsl(44, 100%, 50%); + font-weight: bold; + text-align: center; + cursor: pointer; +} + +#editor { + position: absolute; + left: 0; + top: 4em; + height: calc(100vh - 4em); + width: 50vw; +} + +#editor-loading { + width: 100%; + height: 100%; + background-image: url('Ripple-2s-200px.svg'); + background-repeat: no-repeat; + background-position: center; +} + +#display { + border-left: 1px solid black; + position: absolute; + left: 50vw; + top: 0em; + height: 100vh; + width: 50vw; + overflow-y: scroll; + overflow-x: hidden; +} + +#display > div, #state { + white-space: pre-wrap; + padding: 1.5em; +} + +#display > div:not(:first-child) { + border-top: 1px solid black; +} + +#display .display-title { + text-align: center; + font-size: 1.5em; + background-color: #181818; + padding: 0.5em; + white-space: normal; +} + +#display #diagnostics { + padding: 0; + display: flex; + flex-direction: column; +} + +#display .diagnostic { + padding: 0.5em 1.5em; +} + +#diagnostics > .diagnostic:not(:first-child) { + border-top: 1px solid black; +} + +.diagnostic.info { + background-color: hsla(240, 100%, 50%, 0.1); +} + +.diagnostic.warn, .diagnostic.lint { + background-color: hsla(30, 100%, 50%, 0.1); +} + +.diagnostic.error { + background-color: hsla(0, 100%, 50%, 0.1); +} + +#display .diagnostic .diagnostic { + border: 1px solid black; +} + +#display .diagnostic .diagnostic:first-child { + margin-top: 0.5em; +} + +#display .diagnostic .diagnostic:not(:first-child) { + border-top: 0; +} + +#display #application { + display: none; + padding: 0; +} + +#display #application.active { + display: block; +} + +input[type=text] { + background-color: transparent; + color: #d4d4d4; + font-family: monospace; + font-size: 1em; + border: 0; + border-right: 1px solid black; + padding: 0.5em 1.5em; + outline: none; +} + +.button { + background-color: rgba(0, 0, 255, 0.2); + padding: 0.5em; + cursor: pointer; +} + +.content > div { + border-top: 1px solid black; +} + +#state, .state-title { + border-top: 1px solid black; +} + +#state-refresh { + float: left; + transform: rotate(-45deg); + cursor: pointer; + user-select: none; + padding: 0 0.5em; +} + +#display > #stats { + padding: 1em; +} + +#stats .table-wrap { + display: inline-block; +} + +#stats table { + width: 100%; +} + +#stats td { + padding: 0.5em; +} + +#stats td:first-child { + text-align: left; +} + +#stats td { + text-align: right; +} + +#stats td:nth-child(2) { + width: 4em; +} + +.clickable:hover { + cursor: pointer; + user-select: none; +} + +#vsm.error { + background-color: hsla(0, 100%, 50%, 0.05); +} diff --git a/website2/src/playground/vslib/VslibPool.ts b/website2/src/playground/vslib/VslibPool.ts new file mode 100644 index 0000000..b39db9d --- /dev/null +++ b/website2/src/playground/vslib/VslibPool.ts @@ -0,0 +1,129 @@ +import * as valuescript from 'valuescript'; +import nil from '../helpers/nil'; +import { initVslib } from './index'; + +const workerScript = [ + initVslib.toString(), + (async function main() { + const vslib = await initVslib(); + + self.postMessage('ready'); + + self.onmessage = (evt) => { + const { method, args } = evt.data; + + if (method === 'compile') { + try { + self.postMessage({ ok: vslib.compile(args[0]) }); + } catch (err) { + self.postMessage({ err }); + } + } + + if (method === 'run') { + try { + self.postMessage({ ok: vslib.run(args[0]) }); + } catch (err) { + self.postMessage({ err }); + } + } + }; + }).toString(), + 'main();', +].join('\n\n'); + +const workerUrl = URL.createObjectURL( + new Blob([workerScript], { type: 'application/javascript' }), +); + +export type Diagnostic = { + level: 'Lint' | 'Error' | 'InternalError' | 'CompilerDebug'; + message: string; + span: { + start: number; + end: number; + ctxt: number; + }; +}; + +export type CompilerOutput = { + diagnostics: Diagnostic[]; + assembly: string[]; +}; + +export type RunResult = { + diagnostics: Diagnostic[]; + output: + | { Ok: string } + | { Err: string }; +}; + +export type Job = { + wait: () => Promise; + cancel: () => void; +}; + +export function mapJob(job: Job, f: (x: U) => V): Job { + return { + wait: () => job.wait().then(f), + cancel: job.cancel, + }; +} + +export default class VslibPool { + #pool = new valuescript.WorkerPool(workerUrl); + + run(source: string) { + return this.#Job('run', [source]) as Job; + } + + compile(source: string) { + return this.#Job('compile', [source]) as Job; + } + + #Job(method: string, args: unknown[]) { + let canceled = false; + let finished = false; + let outerTerminate: (() => void) | nil = nil; + + const resultPromise = this.#pool.use((worker, terminate) => { + if (canceled) { + finished = true; + return Promise.reject(new Error('canceled')); + } + + outerTerminate = terminate; + + return new Promise((resolve, reject) => { + worker.postMessage({ method, args }); + + worker.onmessage = (evt) => { + if ('ok' in evt.data) { + resolve(JSON.parse(evt.data.ok)); + } else if ('err' in evt.data) { + if (evt.data.err instanceof Error) { + reject(evt.data.err); + } else { + reject(new Error(`${evt.data.err}`)); + } + } else { + reject(new Error(`Unexpected message: ${evt.data}`)); + } + + finished = true; + }; + }); + }) as Promise; + + return { + wait: () => resultPromise, + cancel: () => { + canceled = true; + + if (!finished && outerTerminate) { + outerTerminate(); + } + }, + }; + } +} diff --git a/website2/src/playground/vslib/index.ts b/website2/src/playground/vslib/index.ts new file mode 100644 index 0000000..5a291ec --- /dev/null +++ b/website2/src/playground/vslib/index.ts @@ -0,0 +1,142 @@ +export async function initVslib() { + // deno-lint-ignore no-explicit-any + const wasm: Record = (await WebAssembly.instantiateStreaming( + fetch(`${location.origin}/value_script_bg.wasm`), + { + './valuescript_wasm_bg.js': { __wbindgen_throw }, + }, + )).instance.exports; + + let WASM_VECTOR_LEN = 0; + + let cachegetUint8Memory0: Uint8Array | null = null; + function getUint8Memory0() { + if ( + cachegetUint8Memory0 === null || + cachegetUint8Memory0.buffer !== wasm.memory.buffer + ) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; + } + + const cachedTextEncoder = new TextEncoder(); + + const encodeString = function (arg: string, view: Uint8Array) { + return cachedTextEncoder.encodeInto(arg, view); + }; + + function passStringToWasm0( + arg: string, + malloc: (len: number) => number, + realloc: (a: number, b: number, c: number) => number, + ) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written!; + } + + WASM_VECTOR_LEN = offset; + return ptr; + } + + let cachegetInt32Memory0: Int32Array | null = null; + function getInt32Memory0() { + if ( + cachegetInt32Memory0 === null || + cachegetInt32Memory0.buffer !== wasm.memory.buffer + ) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; + } + + const cachedTextDecoder = new TextDecoder('utf-8', { + ignoreBOM: true, + fatal: true, + }); + + cachedTextDecoder.decode(); + + function getStringFromWasm0(ptr: number, len: number) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); + } + + function compile(source: string) { + let r0, r1; + + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0( + source, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + const len0 = WASM_VECTOR_LEN; + wasm.compile(retptr, ptr0, len0); + r0 = getInt32Memory0()[retptr / 4 + 0]; + r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + + function run(source: string) { + let r0, r1; + + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0( + source, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + const len0 = WASM_VECTOR_LEN; + wasm.run(retptr, ptr0, len0); + r0 = getInt32Memory0()[retptr / 4 + 0]; + r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + + function __wbindgen_throw(arg0: number, arg1: number) { + throw new Error(getStringFromWasm0(arg0, arg1)); + } + + return { + compile, + run, + }; +} diff --git a/website2/vite.config.ts b/website2/vite.config.ts index dd0b65a..93e3aed 100644 --- a/website2/vite.config.ts +++ b/website2/vite.config.ts @@ -7,6 +7,7 @@ const outDir = resolve(__dirname, 'dist'); // https://vitejs.dev/config/ export default defineConfig({ + publicDir: resolve(__dirname, 'public'), root: src, plugins: [react()], build: { @@ -15,7 +16,7 @@ export default defineConfig({ rollupOptions: { input: { main: resolve(src, 'index.html'), - app: resolve(src, 'app.html'), + playground: resolve(src, 'playground', 'index.html'), }, }, },