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