tricu

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

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 };