tricu

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

haskell-session.ts (7159B)


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