tricu

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

io-v2-output.js (3160B)


      1 // Presentation only. Byte acceptance and strict source UTF-8 Results are separate.
      2 // No evaluator handles, host callbacks, IO acknowledgements or terminal escapes.
      3 import { decodeUTF8, UTF8Error } from './io-v2-utf8.js';
      4 function view(bytes, final) {
      5     const tokens = [];
      6     for (let offset = 0; offset < bytes.length;) {
      7         const lead = bytes[offset];
      8         const width = lead <= 0x7f ? 1 : lead >= 0xc2 && lead <= 0xdf ? 2 :
      9             lead >= 0xe0 && lead <= 0xef ? 3 : lead >= 0xf0 && lead <= 0xf4 ? 4 : 1;
     10         const candidate = bytes.slice(offset, offset + width);
     11         try {
     12             const decoded = decodeUTF8(candidate, false);
     13             if (decoded.carry.length) {
     14                 tokens.push({ kind: 'bytes', offset, bytes: candidate, reason: final ? 'incomplete' : 'pending' });
     15                 break;
     16             }
     17             const previous = tokens.at(-1);
     18             if (previous?.kind === 'text')
     19                 previous.text += decoded.text;
     20             else
     21                 tokens.push({ kind: 'text', offset, text: decoded.text });
     22             offset += width;
     23         }
     24         catch (error) {
     25             if (!(error instanceof UTF8Error))
     26                 throw error;
     27             // Escape one offending byte; reconsider following bytes independently.
     28             // Thus invalid prefixes cannot swallow subsequent valid text.
     29             tokens.push({ kind: 'bytes', offset, bytes: [lead], reason: 'invalid' });
     30             offset++;
     31         }
     32     }
     33     return { bytes, hex: bytes.map(b => b.toString(16).padStart(2, '0')).join(' '), tokens };
     34 }
     35 // Owned by one UI run. The session separately enforces attempted-output budgets;
     36 // this buffer bounds retained accepted bytes, without refunds or history.
     37 export class OutputBuffer {
     38     limit;
     39     streams = { output: [], error: [] };
     40     size = 0;
     41     ended = false;
     42     constructor(limit = 16384) {
     43         this.limit = limit;
     44         if (!Number.isSafeInteger(limit) || limit < 0 || limit > 16384)
     45             throw new Error('invalid output limit');
     46     }
     47     append(stream, bytes) {
     48         if (this.ended)
     49             throw new Error('output presentation ended');
     50         if (stream !== 'output' && stream !== 'error')
     51             throw new Error('invalid output stream');
     52         if (!Array.isArray(bytes) || bytes.length === 0 || bytes.length > 4096)
     53             throw new Error('invalid output bytes');
     54         for (const byte of bytes)
     55             if (!Number.isInteger(byte) || byte < 0 || byte > 255)
     56                 throw new Error('invalid output bytes');
     57         if (bytes.length > this.limit - this.size)
     58             throw new Error('output presentation limit');
     59         this.streams[stream].push(...bytes);
     60         this.size += bytes.length;
     61     }
     62     // End-of-presentation, including after structured cancellation, not closing a
     63     // borrowed stream. Incomplete suffixes become visible bytes, never IO failures.
     64     finish() { this.ended = true; }
     65     snapshot() {
     66         return { done: this.ended, size: this.size,
     67             output: view([...this.streams.output], this.ended), error: view([...this.streams.error], this.ended) };
     68     }
     69 }