compiler-app.ts (10040B)
1 import { readSettings } from './limits-ui.js'; 2 import { activeMilliseconds } from './execution.js'; 3 import { ActiveClock } from './clock.js'; 4 import { activePane } from './tabs.js'; 5 import { Graph } from './graph.js'; 6 import { literals, outputType, line } from './codecs.js'; 7 import { sourceText } from './compiler.js'; 8 import type { CompilerClient, CompilerMessage } from './compiler-protocol.js'; 9 import type { RequestId } from './protocol.js'; 10 function el<T extends HTMLElement = HTMLElement>(id: string): T { 11 const node = document.getElementById(`compiler-${id}`); if (!node) throw new Error(`Missing compiler ${id}`); return node as T; 12 } 13 const source = el<HTMLTextAreaElement>('source'); 14 let settings = readSettings('compiler'); 15 const deadline = new ActiveClock(() => finish('Stopped: active execution time limit exceeded.', true)); 16 const compileButton = el<HTMLButtonElement>('compile'), runButton = el<HTMLButtonElement>('run'), stopButton = el<HTMLButtonElement>('stop'); 17 const names = el<HTMLSelectElement>('export'), args = el<HTMLTextAreaElement>('arguments'), output = el<HTMLSelectElement>('output'); 18 const mode = el<HTMLSelectElement>('mode'), step = el<HTMLInputElement>('step'), next = el<HTMLButtonElement>('next'); 19 const input = el<HTMLFormElement>('input-form'), lineInput = el<HTMLInputElement>('line'); 20 let worker: Worker | undefined, ready = false, busy = false, preferredName = 'id'; 21 let compilationStats = ''; 22 let pending: { id: RequestId; kind: 'input' | 'step' } | undefined; 23 function controls(): void { 24 compileButton.disabled = busy; runButton.disabled = busy || !ready; 25 stopButton.disabled = !worker; source.disabled = busy; 26 names.disabled = busy || !ready; args.disabled = busy; output.disabled = busy; mode.disabled = busy; step.disabled = busy; 27 for (const button of document.querySelectorAll<HTMLButtonElement>('[data-compiler-example]')) button.disabled = busy; 28 } 29 function dispose(): void { 30 deadline.pause(); worker?.terminate(); worker = undefined; ready = false; busy = false; pending = undefined; 31 input.hidden = true; next.disabled = true; controls(); 32 } 33 function clear(): void { 34 el('terminal').textContent = ''; el('result').textContent = ''; el('result').className = ''; 35 el('graph').textContent = ''; el('graph').hidden = true; el('trace').textContent = ''; lineInput.value = ''; 36 } 37 function finish(message: string, error = false, retain = false): void { 38 if (retain) { 39 deadline.pause(); busy = false; pending = undefined; input.hidden = true; next.disabled = true; controls(); 40 } else dispose(); el('status').textContent = error ? 'Stopped' : 'Finished'; el('result').textContent = message; el('result').className = error ? 'error' : ''; 41 } 42 function invalidate(): void { 43 dispose(); names.replaceChildren(); el('stats').textContent = 'Choose or edit source, then compile.'; 44 el('status').textContent = 'Ready to compile'; 45 } 46 function send(message: CompilerClient): void { worker?.postMessage(message); } 47 compileButton.onclick = () => { 48 dispose(); clear(); names.replaceChildren(); 49 try { 50 settings = readSettings('compiler'); 51 sourceText(source.value); // Validate only; encode exclusively in the Worker. 52 window.dispatchEvent(new Event('compiler-start')); 53 const current = new Worker(new URL('./compiler-worker.js', import.meta.url), { type: 'module' }); worker = current; 54 busy = true; controls(); deadline.reset(activeMilliseconds(settings)); deadline.start(); el('status').textContent = 'Compiling'; 55 el('stats').textContent = 'Loading the tricu compiler → compiling definitions…'; 56 current.onmessage = ({ data }: MessageEvent<CompilerMessage>): void => { 57 if (worker !== current) return; 58 switch (data.type) { 59 case 'state': 60 if (data.state === 'running') { deadline.start(); pending = undefined; el('status').textContent = ready ? 'Running' : 'Compiling'; } 61 else { 62 deadline.pause(); pending = { id: data.id, kind: data.state }; input.hidden = data.state !== 'input'; next.disabled = data.state !== 'step'; 63 el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action'; if (data.state === 'input') lineInput.focus(); 64 } 65 break; 66 case 'compiled': 67 deadline.pause(); ready = true; busy = false; 68 for (const name of data.names) { const option = document.createElement('option'); option.value = name; option.textContent = name; names.append(option); } 69 if (data.names.includes(preferredName)) names.value = preferredName; 70 controls(); el('status').textContent = 'Compiled'; 71 el('stats').textContent = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms including load · ${data.stats.nodes.toLocaleString()} live nodes · ${data.stats.allocations.toLocaleString()} allocated / ${data.stats.peakNodes.toLocaleString()} peak live · ${data.stats.collections} GC(s) · ${settings.uncapped ? 'execution limits disabled' : 'bounded profile'} · ${data.stats.work.toLocaleString()} work units`; 72 compilationStats = el('stats').textContent ?? ''; 73 el('result').textContent = 'Your definitions are ready. Select one, choose its arguments and run it. Use [] when its inputs are already applied.'; 74 break; 75 case 'trace': 76 el('trace').textContent += data.phase === 'before' ? `${data.event.step} ${data.event.name}: ${data.event.detail}\n` : ` → ${data.event.value}\n`; 77 break; 78 case 'output': el('terminal').textContent += data.text; break; 79 case 'done': 80 if (data.result.type === 'tree' || data.result.type === 'list') { 81 el('graph').hidden = false; el('graph').textContent = JSON.stringify(data.result.value, null, 2); 82 finish(`Returned ${data.result.type}: tree graph`, false, true); 83 } else finish(`Returned ${data.result.type}: ${JSON.stringify(data.result.value)}`, false, true); 84 el('stats').textContent = `${compilationStats} · current ${data.stats.nodes.toLocaleString()} live nodes / ${data.stats.allocations.toLocaleString()} allocated / ${data.stats.work.toLocaleString()} cumulative work · ${Number.isFinite(data.stats.fuel) ? `${data.stats.fuel.toLocaleString()} run work remaining` : 'unlimited run work'} · ready to run again`; 85 break; 86 case 'error': finish(data.message, true); break; 87 } 88 }; 89 current.onerror = event => { if (worker === current) { event.preventDefault(); finish(event.message || 'Compiler Worker failed.', true); } }; 90 send({ type: 'compile', source: source.value, ...settings }); 91 } catch (error) { finish(error instanceof Error ? error.message : String(error), true); } 92 }; 93 runButton.onclick = () => { 94 if (!ready || !worker) return; 95 try { 96 if (args.value.length > 16_000) throw new Error('Argument JSON limit exceeded'); 97 const arguments_ = literals(JSON.parse(args.value) as unknown), codec = outputType(output.value); 98 clear(); busy = true; controls(); deadline.reset(activeMilliseconds(settings)); deadline.start(); el('status').textContent = 'Running'; 99 send({ type: 'run', export: names.value, arguments: arguments_, output: codec, mode: mode.value === 'io' ? 'io' : 'pure', step: step.checked }); 100 } catch (error) { 101 // Invalid literal input does not discard a successfully compiled heap. 102 el('result').textContent = error instanceof Error ? error.message : String(error); el('result').className = 'error'; 103 } 104 }; 105 function resume(value?: string): void { 106 if (!pending || !worker) return; 107 send(value === undefined ? { type: 'resume', id: pending.id } : { type: 'resume', id: pending.id, value }); 108 deadline.start(); pending = undefined; input.hidden = true; next.disabled = true; el('status').textContent = 'Running'; 109 } 110 input.onsubmit = event => { 111 event.preventDefault(); if (pending?.kind !== 'input') return; 112 try { line(new Graph({ nodes: 16_000, fuel: 100_000 }), lineInput.value); resume(lineInput.value); } 113 catch (error) { el('result').textContent = error instanceof Error ? error.message : String(error); el('result').className = 'error'; } 114 }; 115 next.onclick = () => { if (pending?.kind === 'step') resume(); }; 116 stopButton.onclick = () => finish('Stopped. Compiler session discarded.', true); 117 el<HTMLButtonElement>('reset').onclick = () => { invalidate(); clear(); }; 118 source.oninput = () => { invalidate(); clear(); }; 119 window.addEventListener('limits-change', event => { if ((event as CustomEvent<string>).detail === 'compiler') { invalidate(); clear(); } }); 120 const examples = { 121 identity: { name: 'id', source: 'id = (x : x)', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' }, 122 contracts: { name: 'id', source: 'id x =@nat? x', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' }, 123 pair: { name: 'pair', source: 'pair a b = t a b', args: [{ type: 'number', value: '7' }, { type: 'string', value: 'hi' }], output: 'tree', mode: 'pure' }, 124 definitions: { name: 'answer', source: 'id = (x : x)\nanswer = id 42', args: [], output: 'number', mode: 'pure' }, 125 input: { name: 'io', source: 'io=t "tricuIO" (t 1 (t 11 t))', args: [], output: 'string', mode: 'io' }, 126 }; 127 for (const button of document.querySelectorAll<HTMLButtonElement>('[data-compiler-example]')) button.onclick = () => { 128 const key = button.dataset['compilerExample'] as keyof typeof examples, example = examples[key]; if (!example) return; 129 invalidate(); clear(); preferredName = example.name; source.value = example.source; args.value = JSON.stringify(example.args, null, 2); output.value = example.output; mode.value = example.mode; 130 }; 131 window.addEventListener('package-start', () => { if (worker) finish('Switched to the package playground. Compile again to start a new compiler session.'); }); 132 window.addEventListener('pane-change', () => { if (activePane() !== 'compiler' && busy) finish('Stopped. Open Compiler and compile again to start a new session.', true); }); 133 window.addEventListener('pagehide', dispose); 134 controls();