tricu

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

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 }