compiler-worker.js (6921B)
1 import { compileExpression } from './expression.js'; 2 import { resumeValue } from './io.js'; 3 import { environmentProfile } from './packages.js'; 4 import { BROWSER_BUDGET, collectionPolicy } from './execution.js'; 5 import { contractEnvironment } from './contracts.js'; 6 import { Graph } from './graph.js'; 7 import { COMPILER_BUNDLE_LIMIT, fetchBundle, fetchBytes, loadBundle } from './bundle.js'; 8 import { literals, makeLiteral, outputType, view, list, text } from './codecs.js'; 9 import { runIO } from './io.js'; 10 import { COMPILER_EXPORT, compilerDescriptor, sourceLiteral, sourceText, compiledDefinitions } from './compiler.js'; 11 const scope = globalThis; 12 const base = new URL('../', import.meta.url); 13 const send = (message) => scope.postMessage(message); 14 const running = () => send({ type: 'state', state: 'running' }); 15 let phase = 'empty'; 16 let graph, exports; 17 let requestId = 0; 18 let pending; 19 function request(kind) { 20 if (pending) 21 throw new Error('Driver already waiting'); 22 const id = ++requestId; 23 return new Promise((resolve, reject) => { pending = { id, kind, resolve, reject }; send({ type: 'state', state: kind, id }); }); 24 } 25 async function compile(source, profileName) { 26 phase = 'compiling'; 27 running(); 28 const start = performance.now(); 29 const budget = BROWSER_BUDGET; 30 graph = new Graph(budget, collectionPolicy(budget)); 31 const input = sourceLiteral(graph, source); // Validate before downloading a compiler. 32 const bytes = await fetchBytes(new URL('compiler.json', base), 4096); 33 const raw = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); 34 const { hash } = compilerDescriptor(raw); 35 const bundle = await fetchBundle(new URL(`/_arboricx/bundle/hash/${hash}`, base), hash, COMPILER_BUNDLE_LIMIT); 36 const library = loadBundle(graph, bundle), compiler = library.get(COMPILER_EXPORT); 37 if (compiler === undefined) 38 throw new Error('Missing stable compiler entrypoint'); 39 let environment; 40 if (profileName === undefined) 41 environment = contractEnvironment(graph, library); 42 else { 43 const bytes = await fetchBytes(new URL('environments.json', base), 32_768); 44 const selected = environmentProfile(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)), profileName); 45 if (selected.compiler !== hash) 46 throw new Error('Demo environment/compiler pin mismatch'); 47 const bindings = []; 48 if (selected.profile) { 49 const support = await fetchBundle(new URL(`/_arboricx/bundle/hash/${selected.profile.hash}`, base), selected.profile.hash); 50 const values = loadBundle(graph, support); 51 for (const name of selected.profile.names) { 52 const tree = values.get(name); 53 if (tree === undefined) 54 throw new Error(`Missing demo environment binding ${name}`); 55 bindings.push(graph.fork(text(graph, name), tree)); 56 } 57 } 58 environment = list(graph, bindings); 59 } 60 exports = compiledDefinitions(graph, graph.apply(compiler, [environment, input])); 61 graph.collect(exports.values()); // Safe phase boundary; compiler/input temporaries are no longer caller roots. 62 phase = 'ready'; 63 send({ type: 'compiled', names: [...exports.keys()], draft: { graph: graph.checkpoint(), exports: [...exports] }, stats: graph.stats, milliseconds: performance.now() - start }); 64 } 65 async function run(message) { 66 if (!graph || !exports) 67 throw new Error('Compile a unit first'); 68 phase = 'running'; 69 running(); 70 const fn = message.expression === undefined ? exports.get(message.export) : await compileExpression(graph, exports, message.expression); 71 if (fn === undefined) 72 throw new Error('Unknown compiled definition'); 73 if (message.mode !== 'pure' && message.mode !== 'io') 74 throw new Error('Unsupported execution mode'); 75 const type = outputType(message.output), args = literals(message.arguments).map(value => makeLiteral(graph, value)); 76 let result = message.expression === undefined ? graph.apply(fn, args) : fn; 77 if (message.mode === 'io') 78 result = await runIO(graph, result, { 79 async before(event) { send({ type: 'trace', phase: 'before', event }); if (message.step) 80 await request('step'); running(); }, 81 after(event) { send({ type: 'trace', phase: 'after', event }); }, write(text) { send({ type: 'output', text }); }, 82 read() { return request('input'); }, running, 83 }, true, exports.values()); 84 const displayed = view(graph, result, type); 85 graph.collect(exports.values()); // Drop this run's temporaries, retaining every definition. 86 phase = 'ready'; 87 send({ type: 'done', result: displayed, stats: graph.stats }); 88 } 89 scope.onmessage = ({ data }) => { 90 if (!data || typeof data !== 'object' || Array.isArray(data)) 91 return; 92 const message = data; 93 if (message['type'] === 'resume') { 94 if (!pending || message['id'] !== pending.id) 95 return; 96 const value = resumeValue(pending.kind, message['value']); 97 if (value === undefined) 98 return; 99 const waiter = pending; 100 pending = undefined; 101 waiter.resolve(value); 102 return; 103 } 104 let operation; 105 try { 106 if (message['type'] === 'restore' && phase === 'empty') { 107 // Only the owning page supplies this trusted in-memory checkpoint. 108 const restored = data; 109 const budget = BROWSER_BUDGET; 110 graph = Graph.restore(restored.draft.graph, budget, collectionPolicy(budget)); 111 exports = new Map(restored.draft.exports); 112 phase = 'ready'; 113 return; 114 } 115 else if (message['type'] === 'compile' && phase === 'empty') { 116 if (message['environment'] !== undefined && (typeof message['environment'] !== 'string' || message['environment'].length > 128)) 117 throw new Error('Invalid demo environment'); 118 operation = compile(message['source'], message['environment']); 119 } 120 else if (message['type'] === 'run' && phase === 'ready') { 121 if (typeof message['export'] !== 'string' || message['export'].length > 128) 122 throw new Error('Invalid compiled selection'); 123 operation = run({ type: 'run', export: message['export'], arguments: message['arguments'], 124 ...(message['expression'] === undefined ? {} : { expression: sourceText(message['expression']) }), 125 output: outputType(message['output']), mode: message['mode'], step: message['step'] === true }); 126 } 127 else 128 return; 129 } 130 catch (error) { 131 operation = Promise.reject(error); 132 } 133 operation.catch((error) => { phase = 'done'; send({ type: 'error', message: error instanceof Error ? error.message : String(error) }); }); 134 };