tricu

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

compiler-app.js (10851B)


      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 function el(id) {
      9     const node = document.getElementById(`compiler-${id}`);
     10     if (!node)
     11         throw new Error(`Missing compiler ${id}`);
     12     return node;
     13 }
     14 const source = el('source');
     15 let settings = readSettings('compiler');
     16 const deadline = new ActiveClock(() => finish('Stopped: active execution time limit exceeded.', true));
     17 const compileButton = el('compile'), runButton = el('run'), stopButton = el('stop');
     18 const names = el('export'), args = el('arguments'), output = el('output');
     19 const mode = el('mode'), step = el('step'), next = el('next');
     20 const input = el('input-form'), lineInput = el('line');
     21 let worker, ready = false, busy = false, preferredName = 'id';
     22 let compilationStats = '';
     23 let pending;
     24 function controls() {
     25     compileButton.disabled = busy;
     26     runButton.disabled = busy || !ready;
     27     stopButton.disabled = !worker;
     28     source.disabled = busy;
     29     names.disabled = busy || !ready;
     30     args.disabled = busy;
     31     output.disabled = busy;
     32     mode.disabled = busy;
     33     step.disabled = busy;
     34     for (const button of document.querySelectorAll('[data-compiler-example]'))
     35         button.disabled = busy;
     36 }
     37 function dispose() {
     38     deadline.pause();
     39     worker?.terminate();
     40     worker = undefined;
     41     ready = false;
     42     busy = false;
     43     pending = undefined;
     44     input.hidden = true;
     45     next.disabled = true;
     46     controls();
     47 }
     48 function clear() {
     49     el('terminal').textContent = '';
     50     el('result').textContent = '';
     51     el('result').className = '';
     52     el('graph').textContent = '';
     53     el('graph').hidden = true;
     54     el('trace').textContent = '';
     55     lineInput.value = '';
     56 }
     57 function finish(message, error = false, retain = false) {
     58     if (retain) {
     59         deadline.pause();
     60         busy = false;
     61         pending = undefined;
     62         input.hidden = true;
     63         next.disabled = true;
     64         controls();
     65     }
     66     else
     67         dispose();
     68     el('status').textContent = error ? 'Stopped' : 'Finished';
     69     el('result').textContent = message;
     70     el('result').className = error ? 'error' : '';
     71 }
     72 function invalidate() {
     73     dispose();
     74     names.replaceChildren();
     75     el('stats').textContent = 'Choose or edit source, then compile.';
     76     el('status').textContent = 'Ready to compile';
     77 }
     78 function send(message) { worker?.postMessage(message); }
     79 compileButton.onclick = () => {
     80     dispose();
     81     clear();
     82     names.replaceChildren();
     83     try {
     84         settings = readSettings('compiler');
     85         sourceText(source.value); // Validate only; encode exclusively in the Worker.
     86         window.dispatchEvent(new Event('compiler-start'));
     87         const current = new Worker(new URL('./compiler-worker.js', import.meta.url), { type: 'module' });
     88         worker = current;
     89         busy = true;
     90         controls();
     91         deadline.reset(activeMilliseconds(settings));
     92         deadline.start();
     93         el('status').textContent = 'Compiling';
     94         el('stats').textContent = 'Loading the tricu compiler → compiling definitions…';
     95         current.onmessage = ({ data }) => {
     96             if (worker !== current)
     97                 return;
     98             switch (data.type) {
     99                 case 'state':
    100                     if (data.state === 'running') {
    101                         deadline.start();
    102                         pending = undefined;
    103                         el('status').textContent = ready ? 'Running' : 'Compiling';
    104                     }
    105                     else {
    106                         deadline.pause();
    107                         pending = { id: data.id, kind: data.state };
    108                         input.hidden = data.state !== 'input';
    109                         next.disabled = data.state !== 'step';
    110                         el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action';
    111                         if (data.state === 'input')
    112                             lineInput.focus();
    113                     }
    114                     break;
    115                 case 'compiled':
    116                     deadline.pause();
    117                     ready = true;
    118                     busy = false;
    119                     for (const name of data.names) {
    120                         const option = document.createElement('option');
    121                         option.value = name;
    122                         option.textContent = name;
    123                         names.append(option);
    124                     }
    125                     if (data.names.includes(preferredName))
    126                         names.value = preferredName;
    127                     controls();
    128                     el('status').textContent = 'Compiled';
    129                     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`;
    130                     compilationStats = el('stats').textContent ?? '';
    131                     el('result').textContent = 'Your definitions are ready. Select one, choose its arguments and run it. Use [] when its inputs are already applied.';
    132                     break;
    133                 case 'trace':
    134                     el('trace').textContent += data.phase === 'before' ? `${data.event.step} ${data.event.name}: ${data.event.detail}\n` : `  → ${data.event.value}\n`;
    135                     break;
    136                 case 'output':
    137                     el('terminal').textContent += data.text;
    138                     break;
    139                 case 'done':
    140                     if (data.result.type === 'tree' || data.result.type === 'list') {
    141                         el('graph').hidden = false;
    142                         el('graph').textContent = JSON.stringify(data.result.value, null, 2);
    143                         finish(`Returned ${data.result.type}: tree graph`, false, true);
    144                     }
    145                     else
    146                         finish(`Returned ${data.result.type}: ${JSON.stringify(data.result.value)}`, false, true);
    147                     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`;
    148                     break;
    149                 case 'error':
    150                     finish(data.message, true);
    151                     break;
    152             }
    153         };
    154         current.onerror = event => { if (worker === current) {
    155             event.preventDefault();
    156             finish(event.message || 'Compiler Worker failed.', true);
    157         } };
    158         send({ type: 'compile', source: source.value, ...settings });
    159     }
    160     catch (error) {
    161         finish(error instanceof Error ? error.message : String(error), true);
    162     }
    163 };
    164 runButton.onclick = () => {
    165     if (!ready || !worker)
    166         return;
    167     try {
    168         if (args.value.length > 16_000)
    169             throw new Error('Argument JSON limit exceeded');
    170         const arguments_ = literals(JSON.parse(args.value)), codec = outputType(output.value);
    171         clear();
    172         busy = true;
    173         controls();
    174         deadline.reset(activeMilliseconds(settings));
    175         deadline.start();
    176         el('status').textContent = 'Running';
    177         send({ type: 'run', export: names.value, arguments: arguments_, output: codec, mode: mode.value === 'io' ? 'io' : 'pure', step: step.checked });
    178     }
    179     catch (error) {
    180         // Invalid literal input does not discard a successfully compiled heap.
    181         el('result').textContent = error instanceof Error ? error.message : String(error);
    182         el('result').className = 'error';
    183     }
    184 };
    185 function resume(value) {
    186     if (!pending || !worker)
    187         return;
    188     send(value === undefined ? { type: 'resume', id: pending.id } : { type: 'resume', id: pending.id, value });
    189     deadline.start();
    190     pending = undefined;
    191     input.hidden = true;
    192     next.disabled = true;
    193     el('status').textContent = 'Running';
    194 }
    195 input.onsubmit = event => {
    196     event.preventDefault();
    197     if (pending?.kind !== 'input')
    198         return;
    199     try {
    200         line(new Graph({ nodes: 16_000, fuel: 100_000 }), lineInput.value);
    201         resume(lineInput.value);
    202     }
    203     catch (error) {
    204         el('result').textContent = error instanceof Error ? error.message : String(error);
    205         el('result').className = 'error';
    206     }
    207 };
    208 next.onclick = () => { if (pending?.kind === 'step')
    209     resume(); };
    210 stopButton.onclick = () => finish('Stopped. Compiler session discarded.', true);
    211 el('reset').onclick = () => { invalidate(); clear(); };
    212 source.oninput = () => { invalidate(); clear(); };
    213 window.addEventListener('limits-change', event => { if (event.detail === 'compiler') {
    214     invalidate();
    215     clear();
    216 } });
    217 const examples = {
    218     identity: { name: 'id', source: 'id = (x : x)', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' },
    219     contracts: { name: 'id', source: 'id x =@nat? x', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' },
    220     pair: { name: 'pair', source: 'pair a b = t a b', args: [{ type: 'number', value: '7' }, { type: 'string', value: 'hi' }], output: 'tree', mode: 'pure' },
    221     definitions: { name: 'answer', source: 'id = (x : x)\nanswer = id 42', args: [], output: 'number', mode: 'pure' },
    222     input: { name: 'io', source: 'io=t "tricuIO" (t 1 (t 11 t))', args: [], output: 'string', mode: 'io' },
    223 };
    224 for (const button of document.querySelectorAll('[data-compiler-example]'))
    225     button.onclick = () => {
    226         const key = button.dataset['compilerExample'], example = examples[key];
    227         if (!example)
    228             return;
    229         invalidate();
    230         clear();
    231         preferredName = example.name;
    232         source.value = example.source;
    233         args.value = JSON.stringify(example.args, null, 2);
    234         output.value = example.output;
    235         mode.value = example.mode;
    236     };
    237 window.addEventListener('package-start', () => { if (worker)
    238     finish('Switched to the package playground. Compile again to start a new compiler session.'); });
    239 window.addEventListener('pane-change', () => { if (activePane() !== 'compiler' && busy)
    240     finish('Stopped. Open Compiler and compile again to start a new session.', true); });
    241 window.addEventListener('pagehide', dispose);
    242 controls();