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