Port playground

This commit is contained in:
Andrew Morris
2023-03-27 13:32:40 +11:00
parent 168992f3b4
commit 29b5125b91
15 changed files with 1378 additions and 1 deletions

3
website2/.gitignore vendored
View File

@@ -21,3 +21,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Custom
/public/value_script_bg.wasm

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -0,0 +1 @@
<svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-ripple" style="background: none;"><circle cx="50" cy="50" r="36.9703" fill="none" ng-attr-stroke="{{config.c1}}" ng-attr-stroke-width="{{config.width}}" stroke="#f3dcb2" stroke-width="2"><animate attributeName="r" calcMode="spline" values="0;40" keyTimes="0;1" dur="2" keySplines="0 0.2 0.8 1" begin="-1s" repeatCount="indefinite"></animate><animate attributeName="opacity" calcMode="spline" values="1;0" keyTimes="0;1" dur="2" keySplines="0.2 0 0.8 1" begin="-1s" repeatCount="indefinite"></animate></circle><circle cx="50" cy="50" r="19.2068" fill="none" ng-attr-stroke="{{config.c2}}" ng-attr-stroke-width="{{config.width}}" stroke="#cacbc5" stroke-width="2"><animate attributeName="r" calcMode="spline" values="0;40" keyTimes="0;1" dur="2" keySplines="0 0.2 0.8 1" begin="0s" repeatCount="indefinite"></animate><animate attributeName="opacity" calcMode="spline" values="1;0" keyTimes="0;1" dur="2" keySplines="0.2 0 0.8 1" begin="0s" repeatCount="indefinite"></animate></circle></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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<string, string | nil> = {
'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<T>(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<number>();
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<T> {
left?: BinaryTree<T>;
value?: T;
right?: BinaryTree<T>;
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<T>(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<T>(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;

View File

@@ -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}`);
}
}

View File

@@ -0,0 +1,4 @@
const nil = undefined;
type nil = undefined;
export default nil;

View File

@@ -0,0 +1,9 @@
import nil from './nil';
export default function notNil<T>(value: T | nil): T {
if (value === nil) {
throw new Error();
}
return value;
}

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.scss" />
</head>
<body>
<div id="editor">
<div id="editor-loading"></div>
</div>
<div id="file-selector">
<div id="file-previous">&lt;</div>
<div id="file-location">
<select></select>
</div>
<div id="file-next">&gt;</div>
</div>
<div id="display">
<div class="display-title">Outcome</div>
<div id="outcome">"The playground is loading"</div>
<div class="display-title">Diagnostics</div>
<div id="diagnostics"></div>
<div class="display-title">Assembly</div>
<div id="vsm">@main = function() {
mov "The playground is loading" %return
}</div>
</div>
<script src="index.ts" type="module" defer></script>
</body>
</html>

View File

@@ -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<T = HTMLElement>(query: string): T {
return <T> <unknown> notNil(document.querySelector(query) ?? nil);
}
const editorEl = domQuery('#editor');
const selectEl = domQuery<HTMLSelectElement>('#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<CompilerOutput> | nil = nil;
let runJob: Job<RunResult> | 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<T>(
job: Job<T>,
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 };
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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<T> = {
wait: () => Promise<T>;
cancel: () => void;
};
export function mapJob<U, V>(job: Job<U>, f: (x: U) => V): Job<V> {
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<RunResult>;
}
compile(source: string) {
return this.#Job('compile', [source]) as Job<CompilerOutput>;
}
#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<unknown>;
return {
wait: () => resultPromise,
cancel: () => {
canceled = true;
if (!finished && outerTerminate) {
outerTerminate();
}
},
};
}
}

View File

@@ -0,0 +1,142 @@
export async function initVslib() {
// deno-lint-ignore no-explicit-any
const wasm: Record<string, any> = (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,
};
}

View File

@@ -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'),
},
},
},