tricu

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

module-bundle.js (4625B)


      1 // Explicit publication boundary: snapshots observe finite constructor trees.
      2 // Canonical indexing preserves DAG sharing.
      3 import { BUNDLE_LIMIT, decodeBundle } from './bundle.js';
      4 import { snapshotBundle } from './tree-result.js';
      5 const encoder = new TextEncoder();
      6 const join = (parts) => {
      7     const length = parts.reduce((n, p) => n + p.length, 0);
      8     if (length > BUNDLE_LIMIT)
      9         throw new Error('Module bundle exceeds 16 MiB');
     10     const out = new Uint8Array(length);
     11     let at = 0;
     12     for (const part of parts) {
     13         out.set(part, at);
     14         at += part.length;
     15     }
     16     return out;
     17 };
     18 function word(n, size = 4) {
     19     const bytes = new Uint8Array(size), view = new DataView(bytes.buffer);
     20     if (size === 8)
     21         view.setBigUint64(0, BigInt(n));
     22     else if (size === 2)
     23         view.setUint16(0, n);
     24     else
     25         view.setUint32(0, n);
     26     return bytes;
     27 }
     28 const string = (s) => { const bytes = encoder.encode(s); return join([word(bytes.length), bytes]); };
     29 export function encodeModule(bundle) {
     30     if (!bundle.exports.length || bundle.exports.length > 4096)
     31         throw new Error('Invalid module export count');
     32     const mapped = new Map(), interned = new Map(), records = [];
     33     for (const entry of bundle.exports) {
     34         if (!/^[A-Za-z_][A-Za-z0-9_?$%'.-]*$/u.test(entry.name) || encoder.encode(entry.name).length > 512)
     35             throw new Error('Invalid module export name');
     36         const stack = [[entry.root, false]];
     37         while (stack.length) {
     38             const [id, leave] = stack.pop();
     39             if (mapped.has(id))
     40                 continue;
     41             const node = bundle.nodes[id];
     42             if (!node)
     43                 throw new Error('Missing module node');
     44             const children = node.kind === 'leaf' ? [] : node.kind === 'stem' ? [node.child] : [node.left, node.right];
     45             if (children.some(c => c < 0 || c >= id))
     46                 throw new Error('Invalid module edge');
     47             if (!leave) {
     48                 stack.push([id, true]);
     49                 for (const c of [...children].reverse())
     50                     stack.push([c, false]);
     51             }
     52             else {
     53                 const row = children.map(c => mapped.get(c)), key = row.length + ':' + row.join(',');
     54                 let index = interned.get(key);
     55                 if (index === undefined) {
     56                     index = records.length;
     57                     interned.set(key, index);
     58                     records.push(row);
     59                 }
     60                 if (records.length > 100_000)
     61                     throw new Error('Module node limit exceeded');
     62                 mapped.set(id, index);
     63             }
     64         }
     65     }
     66     const manifest = join([encoder.encode('ARBMNFST'), word(1, 2), word(1, 2),
     67         ...['arboricx.bundle.manifest.v1', 'tree-calculus-executable-object', 'tree-calculus.v1', 'indexed',
     68             'arboricx.indexed.node.v1', 'arboricx.indexed.payload.v1', 'tree-calculus.v1', 'normal-order', 'arboricx.abi.tree.v1'].map(string),
     69         word(0), new Uint8Array([0]), word(bundle.exports.length),
     70         ...bundle.exports.flatMap((e, i) => [word(mapped.get(e.root)), string(i ? 'root' : 'default')]),
     71         word(bundle.exports.length), ...bundle.exports.flatMap(e => [string(e.name), word(mapped.get(e.root)), string('term'), string('arboricx.abi.tree.v1')]),
     72         word(0), word(0)]);
     73     const nodes = join([word(records.length, 8), ...records.map(row => {
     74             const payload = join([new Uint8Array([row.length]), ...row.map(n => word(n))]);
     75             return join([word(payload.length), payload]);
     76         })]);
     77     const section = (type, at, size) => join([word(type), word(1, 2), word(1, 2), word(0), word(at, 8), word(size, 8), word(0)]);
     78     const bytes = join([encoder.encode('ARBORICX'), word(1, 2), word(0, 2), word(2), word(0, 8), word(32, 8),
     79         section(1, 96, manifest.length), section(2, 96 + manifest.length, nodes.length), manifest, nodes]);
     80     decodeBundle(bytes); // Independent reader enforces the actual portable profile.
     81     return bytes;
     82 }
     83 export function moduleSnapshot(names, snapshot) {
     84     const bundle = snapshotBundle(snapshot);
     85     let spine = bundle.roots[0];
     86     bundle.exports = names.map(name => {
     87         const cell = bundle.nodes[spine];
     88         if (cell?.kind !== 'fork')
     89             throw new Error('Invalid module snapshot spine');
     90         spine = cell.right;
     91         return { name, root: cell.left, kind: 'term', abi: 'arboricx.abi.tree.v1' };
     92     });
     93     if (bundle.nodes[spine]?.kind !== 'leaf')
     94         throw new Error('Invalid module snapshot tail');
     95     return encodeModule(bundle);
     96 }