tricu

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

haskell-session.ts (6230B)


      1 import type { ModuleStage } from './module-stage.js';
      2 import { moduleSnapshot } from './module-bundle.js';
      3 import type { LocalModuleInput } from './local-modules.js';
      4 import { isTreeResult, treeResult } from './tree-result.js';
      5 import type { GraphPreview } from './graph.js';
      6 import { expressionUnit, expressionDiagnostic } from './expression.js';
      7 // Backend-private protocol mapping. No WASM pointer, Haskell heap checkpoint or
      8 // fabricated graph statistic crosses into the shared site protocol.
      9 import { compilerDescriptor } from './compiler.js';
     10 import { fetchBytes, type BundleHash } from './bundle.js';
     11 import type { Literal, OutputType, ValueView } from './codecs.js';
     12 import type { IOHost } from './io.js';
     13 import type { RuntimeStats } from './runtime-backends.js';
     14 interface Reply { text: string; linearBytes: number; compilerHash?: string | null; scopeNames?: string[]; }
     15 interface Client {
     16   disposed: boolean;
     17   request(message: object, timeout?: number | null): Promise<Reply>;
     18   dispose(reason?: string): void;
     19 }
     20 export class HaskellSession {
     21   private linearMemoryBytes = 0;
     22   private constructor(private readonly client: Client) {}
     23   static async create(compiler: boolean): Promise<HaskellSession> {
     24     const module = await import(new URL('../runtime/haskell/client.mjs', import.meta.url).href) as { CompilerWorker: new () => Client };
     25     const session = new HaskellSession(new module.CompilerWorker());
     26     try {
     27       const ready = await session.request({ type: 'init', compiler });
     28       if (compiler) {
     29         const bytes = await fetchBytes(new URL('../compiler.json', import.meta.url), 4096);
     30         const expected = compilerDescriptor(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)) as unknown).hash;
     31         if (ready.compilerHash !== expected) throw new Error('Runtime compiler pin mismatch');
     32       }
     33       session.compilerHash = ready.compilerHash ?? null;
     34       return session;
     35     } catch (error) { session.dispose(); throw error; }
     36   }
     37   private compilerHash: string | null = null;
     38   private names: string[] = [];
     39   get disposed(): boolean { return this.client.disposed; }
     40   private async request(message: object): Promise<Reply> {
     41     const reply = await this.client.request(message, null);
     42     this.linearMemoryBytes = reply.linearBytes;
     43     return reply;
     44   }
     45   private async json<T>(message: object): Promise<T> { return JSON.parse((await this.request(message)).text) as T; }
     46   async load(bytes: Uint8Array, hash: BundleHash): Promise<void> { this.names = await this.json<string[]>({ type: 'load', bytes: new Uint8Array(bytes).buffer, hash }); }
     47   async compile(source: string, profileName?: string, localModules: LocalModuleInput[] = []): Promise<string[]> {
     48     this.names = [];
     49     const reply = await this.request({ type: 'compile', source, localModules, base: new URL('../', import.meta.url).href,
     50       ...(profileName === undefined ? {} : { profileName }) });
     51     const names = reply.text.trimEnd().split('\n').filter(Boolean);
     52     this.names = reply.scopeNames ?? names;
     53     return names;
     54   }
     55 
     56   async inspectModule(source: string): Promise<ModuleStage> {
     57     return this.json<ModuleStage>({ type: 'module-header', source });
     58   }
     59   async exportModule(): Promise<Uint8Array> {
     60     const value = await this.json<{ names: string[]; snapshot: GraphPreview }>({ type: 'module-snapshot' });
     61     return moduleSnapshot(value.names, value.snapshot);
     62   }
     63   async run(name: string, args: Literal[], output: OutputType, mode: 'pure' | 'io', host: IOHost): Promise<ValueView> {
     64     return this.execute({ type: mode === 'pure' ? 'evaluate' : 'io-start', name, arguments: args, output }, output, mode, host);
     65   }
     66   async runExpression(source: string, output: OutputType, mode: 'pure' | 'io', host: IOHost): Promise<ValueView> {
     67     const unit = expressionUnit(source, this.names);
     68     if (!this.compilerHash) {
     69       const bytes = await fetchBytes(new URL('../compiler.json', import.meta.url), 4096);
     70       const expected = compilerDescriptor(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)) as unknown).hash;
     71       const ready = await this.request({ type: 'compiler-init' });
     72       if (ready.compilerHash !== expected) { this.dispose(); throw new Error('Runtime compiler pin mismatch'); }
     73       this.compilerHash = expected;
     74     }
     75     try { return await this.execute({ type: 'expression', ...unit, output, mode }, output, mode, host); }
     76     catch (error) { throw expressionDiagnostic(error); }
     77   }
     78   private async execute(request: object, output: OutputType, mode: 'pure' | 'io', host: IOHost): Promise<ValueView> {
     79     const displayed = async (message: object): Promise<ValueView> => {
     80       if (isTreeResult(output)) {
     81         const snapshot = await this.json<{ value: GraphPreview }>(message);
     82         return treeResult(snapshot.value, output);
     83       }
     84       return this.json<ValueView>(message);
     85     };
     86     if (mode === 'pure') return displayed(request);
     87     type Event = { kind: string; token: number; step: number; name: string; detail: string; depth: number; value: string; text: string };
     88     try {
     89       let event = await this.json<Event>(request);
     90       while (event.kind !== 'done') {
     91         let value = '';
     92         switch (event.kind) {
     93           case 'before': await host.before({ step: event.step, name: event.name, detail: event.detail, depth: event.depth }); break;
     94           case 'read': value = await host.read(); break;
     95           case 'write': host.write(event.text); break;
     96           case 'after': host.after({ step: event.step, name: event.name, value: event.value }); break;
     97           default: throw new Error('Unknown runtime IO event');
     98         }
     99         host.running();
    100         event = await this.json<Event>({ type: 'io-resume', token: event.token, value });
    101       }
    102       return await displayed({ type: 'io-view', output });
    103     } finally {
    104       // Release result/continuations even when decoding or a host hook fails.
    105       // A poisoned instance cannot be recovered by cancelling the interaction.
    106       if (!this.disposed) await this.request({ type: 'io-cancel' });
    107     }
    108   }
    109   stats(): RuntimeStats { return { backend: 'haskell-wasm', linearMemoryBytes: this.linearMemoryBytes }; }
    110   dispose(): void { this.client.dispose(); }
    111 }