tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 }