tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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