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 }