tricu

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

io.ts (3194B)


      1 import { Graph, type NodeId } from './graph.js';
      2 import { readNatural, readText, line } from './codecs.js';
      3 export interface Trace { step: number; name: string; detail: string; depth: number; }
      4 export interface IOHost {
      5   before(event: Trace): Promise<void>;
      6   after(event: { step: number; name: string; value: string }): void;
      7   write(text: string): void;
      8   read(): Promise<string>;
      9   running(): void;
     10 }
     11 // collectActions requires exclusive ownership: unrelated caller handles may be
     12 // invalidated unless included in retainedRoots. Normal callers retain the
     13 // non-collecting API by default. Snapshot extra roots before the first action.
     14 export async function runIO(graph: Graph, handle: NodeId, host: IOHost, collectActions = false, retainedRoots: Iterable<NodeId> = []): Promise<NodeId> {
     15   const retained = [...retainedRoots];
     16   const [sentinel, body] = graph.pair(handle, 'IO envelope');
     17   if (readText(graph, sentinel, 7) !== 'tricuIO') throw new Error('Missing tricuIO sentinel');
     18   const [version, initial] = graph.pair(body, 'IO version/action');
     19   if (readNatural(graph, version) !== 1n) throw new Error('Unsupported tricuIO version');
     20   const frames: NodeId[] = []; let action = initial, output = 0;
     21   const names = new Map([[0n, 'pure'], [1n, 'bind'], [2n, 'abortIO'], [10n, 'putStr'], [11n, 'getLine']]);
     22   for (let step = 1; step <= 128; step++) {
     23     if (collectActions) graph.collect([action, ...frames, ...retained]);
     24     const [tag, payload] = graph.pair(action, 'action');
     25     const code = readNatural(graph, tag), name = names.get(code);
     26     if (!name) throw new Error(`Unsupported browser IO action ${code}`);
     27     // Don't inspect arbitrary pure/abort payloads merely to build a trace.
     28     const detail = code === 10n ? readText(graph, payload) : `node #${payload}`;
     29     await host.before({ step, name, detail, depth: frames.length });
     30     if (code === 1n) {
     31       const [left, continuation] = graph.pair(payload, 'bind');
     32       frames.push(continuation); action = left;
     33       host.after({ step, name, value: 'continuation handle saved' }); continue;
     34     }
     35     if (code === 2n) throw new Error(`Execution aborted (diagnostic node #${payload}; no payload forcing).`);
     36     let value: NodeId;
     37     if (code === 0n) value = payload;
     38     else if (code === 10n) {
     39       output += Array.from(detail).length;
     40       if (output > 16_384) throw new Error('Terminal output limit exceeded');
     41       host.write(detail); value = graph.leaf;
     42     } else value = line(graph, await host.read());
     43     host.after({ step, name, value: `node #${value}` });
     44     if (!frames.length) return value;
     45     host.running(); action = graph.app(frames.pop()!, value);
     46   }
     47   throw new Error('IO action limit exceeded');
     48 }
     49 
     50 // Ignore malformed/wrong-kind/stale resumes without consuming the waiting token.
     51 // The page validates interactively; the Worker must also retain a retryable wait.
     52 export function resumeValue(kind: 'input' | 'step', value: unknown): string | undefined {
     53   if (kind === 'step') return value === undefined ? '' : undefined;
     54   if (typeof value !== 'string' || value.length > 512 || /[\r\n\uD800-\uDFFF]/u.test(value) || Array.from(value).length > 256) return undefined;
     55   return value;
     56 }