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