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