tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

wasm-worker.ts (8572B)


      1 import { autoInterpretation } from './auto-interpretation.js';
      2 import { importPlan } from './import-plan.js';
      3 import { localModuleInputs, validateLocalModule } from './local-modules.js';
      4 import { validStepResume } from './io.js';
      5 import { createRuntimeSession, type RuntimeSession } from './runtime-session.js';
      6 const selectedBackend = new URL(globalThis.location.href).searchParams.get('backend');
      7 import { publishedBundle } from './published-bundle.js';
      8 import { bundleHash } from './bundle.js';
      9 import { literals, outputType } from './codecs.js';
     10 import { sourceText } from './compiler.js';
     11 import { exportTree, treeFormat } from './tree-export.js';
     12 import type { CompilerMessage } from './compiler-protocol.js';
     13 import type { WorkerMessage, RequestId } from './protocol.js';
     14 const scope = globalThis as unknown as { postMessage(m: CompilerMessage | WorkerMessage): void; onmessage: ((e: MessageEvent<unknown>) => void) | null };
     15 const send = (m: CompilerMessage | WorkerMessage) => scope.postMessage(m);
     16 const running = () => send({ type: 'state', state: 'running' });
     17 let session: RuntimeSession | undefined, stored: Awaited<ReturnType<typeof publishedBundle>> | undefined;
     18 let busy = false, requestId = 0;
     19 let pending: { id: RequestId; resolve(): void; reject(error: Error): void } | undefined;
     20 function requestStep(): Promise<void> {
     21   if (pending) throw new Error('Driver already waiting');
     22   if (requestId >= Number.MAX_SAFE_INTEGER) throw new Error('Driver request identity exhausted');
     23   const id = ++requestId as RequestId;
     24   return new Promise((resolve, reject) => { pending = { id, resolve, reject }; send({ type: 'state', state: 'step', id }); });
     25 }
     26 let ioCommand: ((command: import('./io-session.js').Command) => void) | undefined, ioRun: string | undefined, stopRequested = false;
     27 function host(stepping: boolean): import('./io.js').IOHost {
     28   stopRequested = false;
     29   return {
     30     selected(version) { send({ type: 'io-version', version }); },
     31     async before(event) { send({ type: 'trace', phase: 'before', event }); if (stepping) await requestStep(); running(); },
     32     structured: {
     33       stepping,
     34       open(run, command) { ioRun = run; ioCommand = command; send({ type: 'io-open', run }); if (stopRequested) command({ run, kind: 'stop' }); },
     35       event(event) { send({ type: 'io-event', event }); },
     36       close(run, settled) { ioCommand = undefined; ioRun = undefined; pending = undefined; send({ type: 'io-close', run, settled }); },
     37     },
     38   };
     39 }
     40 scope.onmessage = ({ data }) => {
     41   if (!data || typeof data !== 'object' || Array.isArray(data)) return;
     42   const m = data as Record<string, unknown>;
     43   if (m['type'] === 'io-stop') {
     44     if (!busy) return;
     45     stopRequested = true;
     46     if (ioRun) ioCommand?.({ run: ioRun, kind: 'stop' });
     47     if (pending) { const waiter = pending; pending = undefined; waiter.resolve(); }
     48     return;
     49   }
     50   if (m['type'] === 'io-command') {
     51     const command = m['command'];
     52     if (command && typeof command === 'object') ioCommand?.(command as import('./io-session.js').Command);
     53     return;
     54   }
     55   if (m['type'] === 'resume') {
     56     if (!pending || m['id'] !== pending.id) return;
     57     if (!validStepResume(m['value'])) return;
     58     const waiter = pending; pending = undefined; waiter.resolve();
     59     return;
     60   }
     61   if (busy) return;
     62   busy = true;
     63   void (async () => {
     64     if (m['type'] === 'check-import') {
     65       if (session) throw new Error('Already initialized');
     66       const source = sourceText(m['source']);
     67       if (typeof m['manifest'] !== 'string' || typeof m['alias'] !== 'string') throw new Error('Invalid import request');
     68       session = await createRuntimeSession(selectedBackend, true);
     69       const stage = await session.inspectModule(source);
     70       const plan = importPlan(source, stage, m['manifest'], m['alias']);
     71       if (!plan.existing) await session.compile(plan.source, 'empty', localModuleInputs(m['localModules']));
     72       send({ type: 'import-checked', plan });
     73     } else if (m['type'] === 'load') {
     74       if (session || stored) throw new Error('Already initialized');
     75       const localId = m['localId'];
     76       if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) throw new Error('Invalid catalogue ID');
     77       running(); stored = await publishedBundle(bundleHash(m['hash']), localId);
     78       session = await createRuntimeSession(selectedBackend, false);
     79       await session.load(stored.bytes, stored.hash);
     80       send({ type: 'loaded', hash: stored.hash, exports: stored.bundle.exports, nodes: stored.bundle.nodes.length, metadata: stored.bundle.metadata, source: stored.source });
     81     } else if (m['type'] === 'compile') {
     82       if (session) throw new Error('Already initialized');
     83       const source = sourceText(m['source']), profile = m['environment'];
     84       if (profile !== undefined && (typeof profile !== 'string' || profile.length > 128)) throw new Error('Invalid demo environment');
     85       running(); const start = performance.now();
     86       session = await createRuntimeSession(selectedBackend, true);
     87       const names = await session.compile(source, profile, localModuleInputs(m['localModules']));
     88       send({ type: 'compiled', names, stats: session.stats(), milliseconds: performance.now() - start });
     89     } else if (m['type'] === 'export-module') {
     90       if (!session || stored) throw new Error('Compile a draft first');
     91       running();
     92       try { const module = await validateLocalModule(await session.exportModule()); send({ type: 'module-exported', bytes: module.bytes, hash: module.hash, manifest: module.manifest }); }
     93       catch (e) { if (session.disposed) throw e; send({ type: 'module-export-error', message: String(e) }); }
     94     } else if (m['type'] === 'inspect') {
     95       if (!session) throw new Error('Load or compile first');
     96       if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 4096) throw new Error('Invalid inspection');
     97       const id = m['id'] as number;
     98       try {
     99         // Observe the existing binding with no arguments, expression compilation,
    100         // or IO interpreter. Observation/decoding can still require reduction.
    101         const name = m['name'];
    102         if (m['output'] === 'auto' && await session.probeIO(name)) { send({ type: 'inspected', id, io: true }); return; }
    103         const result = m['output'] === 'auto'
    104           ? await autoInterpretation(output => session!.inspectDefinition(name, output), () => session!.disposed)
    105           : await session.inspectDefinition(name, outputType(m['output']));
    106         send({ type: 'inspected', id, result });
    107       } catch (e) { send({ type: 'inspection-error', id, message: e instanceof Error ? e.message : String(e), fatal: session.disposed }); }
    108     } else if (m['type'] === 'run-io') {
    109       if (!session || typeof m['name'] !== 'string' || m['name'].length > 4096) throw new Error('Invalid IO definition');
    110       const output = outputType(m['output']); running();
    111       const result = await session.runDefinitionIO(m['name'], output, host(m['step'] === true));
    112       send({ type: 'done', result, stats: session.stats() });
    113     } else if (m['type'] === 'run') {
    114       if (!session) throw new Error('Load or compile first');
    115       if (typeof m['export'] !== 'string' || m['export'].length > 512) throw new Error('Invalid export');
    116       if (m['mode'] !== 'pure' && m['mode'] !== 'io') throw new Error('Unsupported execution mode');
    117       const args = literals(m['arguments']), output = outputType(m['output']); running();
    118       const ioHost = host(m['step'] === true);
    119       const result = m['expression'] === undefined
    120         ? await session.run(m['export'], args, output, m['mode'], ioHost)
    121         : await session.runExpression(sourceText(m['expression']), output, m['mode'], ioHost);
    122       send({ type: 'done', result, stats: session.stats() });
    123     } else if (m['type'] === 'export' && stored) {
    124       try {
    125         if (typeof m['export'] !== 'string') throw new Error('Invalid export');
    126         const format = treeFormat(m['format']);
    127         send({ type: 'exported', hash: stored.hash, export: m['export'], format, file: exportTree(stored.bundle, m['export'], format) });
    128       } catch (e) { send({ type: 'export-error', message: String(e) }); }
    129     } else if (m['type'] === 'download' && stored) send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes });
    130     else throw new Error('Unsupported backend operation');
    131   })().catch((e: unknown) => send({ type: 'error', message: e instanceof Error ? e.message : String(e), fatal: session?.disposed ?? true }))
    132     .finally(() => { busy = false; });
    133 };