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