tricu

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

lib.js (9715B)


      1 /**
      2  * lib.js — FFI wrapper around libarboricx.so via koffi.
      3  *
      4  * Exports low-level C ABI bindings and high-level helpers.
      5  */
      6 
      7 import { existsSync } from 'node:fs';
      8 import { dirname, join, resolve } from 'node:path';
      9 import { fileURLToPath } from 'node:url';
     10 import koffi from 'koffi';
     11 
     12 const __dirname = dirname(fileURLToPath(import.meta.url));
     13 
     14 koffi.opaque('arb_ctx_t');
     15 
     16 // ── Library discovery ───────────────────────────────────────────────────────
     17 
     18 export function findLib() {
     19   const env = process.env.ARBORICX_LIB;
     20   if (env) {
     21     if (existsSync(env)) return env;
     22     throw new Error(`ARBORICX_LIB set but file not found: ${env}`);
     23   }
     24 
     25   const candidates = [
     26     resolve(__dirname, 'libarboricx.so'),
     27     'libarboricx.so',
     28     './libarboricx.so',
     29     '/usr/local/lib/libarboricx.so',
     30     '/usr/lib/libarboricx.so',
     31   ];
     32 
     33   for (const p of candidates) {
     34     if (existsSync(p)) return p;
     35   }
     36 
     37   throw new Error('libarboricx.so not found. Set ARBORICX_LIB to its full path.');
     38 }
     39 
     40 // ── FFI setup ───────────────────────────────────────────────────────────────
     41 
     42 let _lib = null;
     43 let _libPath = null;
     44 
     45 function ensureLib() {
     46   if (_lib) return _lib;
     47   const path = findLib();
     48   _lib = koffi.load(path);
     49   _libPath = path;
     50   return _lib;
     51 }
     52 
     53 export function loadLib(path) {
     54   if (_lib && _libPath === path) return;
     55   _lib = koffi.load(path);
     56   _libPath = path;
     57 }
     58 
     59 function getLib() {
     60   if (_lib) return _lib;
     61   return ensureLib();
     62 }
     63 
     64 // ── Context lifecycle ───────────────────────────────────────────────────────
     65 
     66 export function init(libPath) {
     67   if (libPath) loadLib(libPath);
     68   const lib = getLib();
     69   if (lib.func('uint32_t arb_abi_version(void)')() !== 2) throw new Error('Arboricx ABI v2 required');
     70   const ctx = lib.func('arb_ctx_t *arboricx_init(void)')();
     71   if (!ctx) throw new Error('arboricx_init failed');
     72   return ctx;
     73 }
     74 
     75 export function free(ctx) {
     76   getLib().func('void arboricx_free(arb_ctx_t *ctx)')(ctx);
     77 }
     78 
     79 // Each handle returned below is owned until release() or context teardown.
     80 // Handles are always bigint; converting one to Number can lose identity bits.
     81 function owned(signature, ...args) {
     82   const out = [0n];
     83   const declaration = signature.replace(/^uint64_t /, 'int ').replace(/\)$/, ', _Out_ uint64_t *out)');
     84   const status = getLib().func(declaration)(...args, out);
     85   if (status !== 0) throw new Error(`${signature.split('(')[0]}: status ${status}`);
     86   return BigInt(out[0]);
     87 }
     88 
     89 export function retain(ctx, handle) {
     90   return owned('uint64_t arb_retain(arb_ctx_t *ctx, uint64_t handle)', ctx, handle);
     91 }
     92 export function release(ctx, handle) {
     93   const status = getLib().func('int arb_release(arb_ctx_t *ctx, uint64_t handle)')(ctx, handle);
     94   if (status !== 0) throw new Error(`arb_release: status ${status}`);
     95 }
     96 export function collect(ctx) {
     97   const status = getLib().func('int arb_collect(arb_ctx_t *ctx)')(ctx);
     98   if (status !== 0) throw new Error(`arb_collect: status ${status}`);
     99 }
    100 
    101 // ── Bundle loading ──────────────────────────────────────────────────────────
    102 
    103 export function loadBundle(ctx, bytes, name) {
    104   const result = owned('uint64_t arb_load_bundle(arb_ctx_t *ctx, _In_ uint8_t *bytes, size_t len, const char *name)', ctx, bytes, bytes.length, name);
    105   return result;
    106 }
    107 
    108 export function loadBundleDefault(ctx, bytes) {
    109   const result = owned('uint64_t arb_load_bundle_default(arb_ctx_t *ctx, _In_ uint8_t *bytes, size_t len)', ctx, bytes, bytes.length);
    110   return result;
    111 }
    112 
    113 // ── Reduction ───────────────────────────────────────────────────────────────
    114 
    115 export function reduce(ctx, root, fuel = 1_000_000_000n) {
    116   return owned('uint64_t arb_reduce(arb_ctx_t *ctx, uint64_t root, uint64_t fuel)', ctx, root, BigInt(fuel));
    117 }
    118 
    119 // ── Tree construction ───────────────────────────────────────────────────────
    120 
    121 export function leaf(ctx) {
    122   return owned('uint64_t arb_leaf(arb_ctx_t *ctx)', ctx);
    123 }
    124 
    125 export function stem(ctx, child) {
    126   return owned('uint64_t arb_stem(arb_ctx_t *ctx, uint64_t child)', ctx, child);
    127 }
    128 
    129 export function fork(ctx, left, right) {
    130   return owned('uint64_t arb_fork(arb_ctx_t *ctx, uint64_t left, uint64_t right)', ctx, left, right);
    131 }
    132 
    133 export function app(ctx, func, arg) {
    134   return owned('uint64_t arb_app(arb_ctx_t *ctx, uint64_t func, uint64_t arg)', ctx, func, arg);
    135 }
    136 
    137 // ── Codec constructors ──────────────────────────────────────────────────────
    138 
    139 export function ofNumber(ctx, n) {
    140   const big = typeof n === 'bigint' ? n : BigInt(n);
    141   return owned('uint64_t arb_of_number(arb_ctx_t *ctx, uint64_t n)', ctx, big);
    142 }
    143 
    144 export function ofString(ctx, s) {
    145   return owned('uint64_t arb_of_string(arb_ctx_t *ctx, const char *s)', ctx, s);
    146 }
    147 
    148 export function ofBytes(ctx, bytes) {
    149   return owned('uint64_t arb_of_bytes(arb_ctx_t *ctx, _In_ uint8_t *bytes, size_t len)', ctx, bytes, bytes.length);
    150 }
    151 
    152 export function ofList(ctx, items) {
    153   const arr = new BigUint64Array(items.map(BigInt));
    154   return owned('uint64_t arb_of_list(arb_ctx_t *ctx, _In_ uint64_t *items, size_t len)', ctx, arr, arr.length);
    155 }
    156 
    157 // ── Codec destructors ───────────────────────────────────────────────────────
    158 
    159 export function toNumber(ctx, root) {
    160   const out = [0];
    161   const ok = getLib().func('int arb_to_number(arb_ctx_t *ctx, uint64_t root, _Out_ uint64_t *out)')(ctx, root, out);
    162   if (ok !== 0) throw new Error('arb_to_number failed');
    163   return typeof out[0] === 'bigint' ? Number(out[0]) : out[0];
    164 }
    165 
    166 export function toString(ctx, root) {
    167   const ptrOut = [null];
    168   const lenOut = [0];
    169   const ok = getLib().func('int arb_to_string(arb_ctx_t *ctx, uint64_t root, _Out_ uint8_t **out_ptr, _Out_ size_t *out_len)')(ctx, root, ptrOut, lenOut);
    170   if (ok !== 0) throw new Error('arb_to_string failed');
    171 
    172   const bytes = koffi.decode(ptrOut[0], 'uint8_t', lenOut[0]);
    173   const str = Buffer.from(bytes).toString('utf-8');
    174   getLib().func('void arboricx_free_buf(arb_ctx_t *ctx, uint8_t *ptr, size_t len)')(ctx, ptrOut[0], lenOut[0]);
    175   return str;
    176 }
    177 
    178 export function toBytes(ctx, root) {
    179   const ptrOut = [null];
    180   const lenOut = [0];
    181   const ok = getLib().func('int arb_to_bytes(arb_ctx_t *ctx, uint64_t root, _Out_ uint8_t **out_ptr, _Out_ size_t *out_len)')(ctx, root, ptrOut, lenOut);
    182   if (ok !== 0) throw new Error('arb_to_bytes failed');
    183 
    184   const bytes = Buffer.from(koffi.decode(ptrOut[0], 'uint8_t', lenOut[0]));
    185   getLib().func('void arboricx_free_buf(arb_ctx_t *ctx, uint8_t *ptr, size_t len)')(ctx, ptrOut[0], lenOut[0]);
    186   return bytes;
    187 }
    188 
    189 export function toBool(ctx, root) {
    190   const out = [0];
    191   const ok = getLib().func('int arb_to_bool(arb_ctx_t *ctx, uint64_t root, _Out_ int *out)')(ctx, root, out);
    192   if (ok !== 0) throw new Error('arb_to_bool failed');
    193   return out[0] !== 0;
    194 }
    195 
    196 // ── Result unwrapping ───────────────────────────────────────────────────────
    197 
    198 export function unwrapResult(ctx, root) {
    199   const outOk = [0];
    200   const outValue = [0];
    201   const outRest = [0];
    202   const ok = getLib().func('int arb_unwrap_result(arb_ctx_t *ctx, uint64_t root, _Out_ int *out_ok, _Out_ uint64_t *out_value, _Out_ uint64_t *out_rest)')(ctx, root, outOk, outValue, outRest);
    203   if (ok !== 0) throw new Error('arb_unwrap_result failed');
    204   return { ok: outOk[0] !== 0, value: BigInt(outValue[0]), rest: BigInt(outRest[0]) };
    205 }
    206 
    207 export function unwrapHostValue(ctx, root) {
    208   const outTag = [0n];
    209   const outPayload = [0];
    210   const ok = getLib().func('int arb_unwrap_host_value(arb_ctx_t *ctx, uint64_t root, _Out_ uint64_t *out_tag, _Out_ uint64_t *out_payload)')(ctx, root, outTag, outPayload);
    211   if (ok !== 0) throw new Error('arb_unwrap_host_value failed');
    212   return { tag: outTag[0], payload: BigInt(outPayload[0]) };
    213 }
    214 
    215 // ── Kernel ──────────────────────────────────────────────────────────────────
    216 
    217 export function kernelRoot(ctx) {
    218   return owned('uint64_t arb_kernel_root(arb_ctx_t *ctx)', ctx);
    219 }
    220 
    221 // ── High-level helpers ──────────────────────────────────────────────────────
    222 
    223 export function decode(ctx, root) {
    224   try {
    225     return toBool(ctx, root) ? 'true' : 'false';
    226   } catch {
    227     try {
    228       return toString(ctx, root);
    229     } catch {
    230       try {
    231         return String(toNumber(ctx, root));
    232       } catch {
    233         throw new Error('could not decode result');
    234       }
    235     }
    236   }
    237 }
    238 
    239 export function decodeType(ctx, root) {
    240   try { toBool(ctx, root); return 'bool'; } catch {}
    241   try { toString(ctx, root); return 'string'; } catch {}
    242   try { toNumber(ctx, root); return 'number'; } catch {}
    243   return 'unknown (raw tree)';
    244 }