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 }