tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

lib.js (6929B)


      1 /** Native tree calculus and IO via Haskell's C ABI. Roots are owned bigint IDs. */
      2 import { existsSync, realpathSync } from 'node:fs';
      3 import { dirname, join } from 'node:path';
      4 import { fileURLToPath } from 'node:url';
      5 import koffi from 'koffi';
      6 
      7 export function findLib() {
      8   if (process.env.TRICU_LIB) {
      9     if (!existsSync(process.env.TRICU_LIB)) throw new Error(`TRICU_LIB not found: ${process.env.TRICU_LIB}`);
     10     return realpathSync(process.env.TRICU_LIB);
     11   }
     12   const directory = dirname(fileURLToPath(import.meta.url));
     13   for (const name of ['libtricu-haskell.so', 'libtricu-haskell.dylib', 'tricu-haskell.dll']) {
     14     const candidate = join(directory, name);
     15     if (existsSync(candidate)) return realpathSync(candidate);
     16   }
     17   throw new Error('Set TRICU_LIB to the Haskell embedding library');
     18 }
     19 
     20 export class TricuError extends Error {
     21   constructor(status, message) {
     22     super(`tricu status ${status}: ${message}`);
     23     this.status = status;
     24   }
     25 }
     26 
     27 // One process-wide library, kept loaded for the lifetime of the process.
     28 let loaded;
     29 function native(path) {
     30   const resolved = realpathSync(path);
     31   if (loaded) {
     32     if (loaded.path !== resolved) throw new Error('One Haskell embedding library per process');
     33     return loaded.api;
     34   }
     35   const lib = koffi.load(resolved);
     36   const declarations = {
     37     init: 'void tricu_init(void)',
     38     abi_version: 'uint32_t tricu_abi_version(void)',
     39     context_new: 'uint32_t tricu_context_new(_Out_ uint64_t *out)',
     40     context_free: 'uint32_t tricu_context_free(uint64_t ctx)',
     41     release: 'uint32_t tricu_release(uint64_t ctx, uint64_t value)',
     42     buffer_free: 'void tricu_buffer_free(void *buffer)',
     43     run_io: 'uint32_t tricu_run_io(uint64_t ctx, uint64_t program, _Out_ uint32_t *outcome, _Out_ uint64_t *value, _Out_ uint64_t *cleanup)',
     44     load_bundle: 'uint32_t tricu_load_bundle(uint64_t ctx, const uint8_t *bytes, size_t len, const uint8_t *name, size_t name_len, _Out_ uint64_t *out)',
     45     last_error: 'uint32_t tricu_last_error(uint64_t ctx, _Out_ void **out, _Out_ size_t *len)',
     46   };
     47   for (const name of ['retain', 'stem', 'reduce']) declarations[name] =
     48     `uint32_t tricu_${name}(uint64_t ctx, uint64_t value, _Out_ uint64_t *out)`;
     49   for (const name of ['fork', 'apply']) declarations[name] =
     50     `uint32_t tricu_${name}(uint64_t ctx, uint64_t a, uint64_t b, _Out_ uint64_t *out)`;
     51   declarations.leaf = 'uint32_t tricu_leaf(uint64_t ctx, _Out_ uint64_t *out)';
     52   declarations.child = 'uint32_t tricu_child(uint64_t ctx, uint64_t value, uint32_t index, _Out_ uint64_t *out)';
     53   for (const name of ['kind', 'to_bool']) declarations[name] =
     54     `uint32_t tricu_${name}(uint64_t ctx, uint64_t value, _Out_ uint32_t *out)`;
     55   for (const name of ['from_u64', 'to_u64']) declarations[name] =
     56     `uint32_t tricu_${name}(uint64_t ctx, uint64_t value, _Out_ uint64_t *out)`;
     57   for (const name of ['bytes', 'utf8']) {
     58     declarations[`from_${name}`] = `uint32_t tricu_from_${name}(uint64_t ctx, const uint8_t *bytes, size_t len, _Out_ uint64_t *out)`;
     59     declarations[`to_${name}`] = `uint32_t tricu_to_${name}(uint64_t ctx, uint64_t value, _Out_ void **out, _Out_ size_t *len)`;
     60   }
     61   const api = Object.fromEntries(Object.entries(declarations).map(([name, declaration]) => [name, lib.func(declaration)]));
     62   api.init();
     63   if (api.abi_version() !== 1) throw new Error('Haskell embedding ABI 1 required');
     64   loaded = { path: resolved, lib, api };
     65   return api;
     66 }
     67 
     68 export class Library {
     69   constructor(path = findLib()) { this.api = native(path); }
     70   context() { return new Context(this); }
     71 }
     72 
     73 export class Context {
     74   constructor(library) {
     75     this.api = library.api;
     76     const out = [0n];
     77     const status = this.api.context_new(out);
     78     if (status) throw new TricuError(status, 'creating context');
     79     this.identity = BigInt(out[0]);
     80   }
     81   _copy(pointer, length) {
     82     try { return Buffer.from(koffi.decode(pointer, 'uint8_t', Number(length))); }
     83     finally { this.api.buffer_free(pointer); }
     84   }
     85   _check(status) {
     86     if (!status) return;
     87     const pointer = [null], length = [0];
     88     const diagnostic = this.api.last_error(this.identity, pointer, length);
     89     const message = diagnostic ? 'invalid context' : this._copy(pointer[0], length[0]).toString('utf8');
     90     throw new TricuError(status, message);
     91   }
     92   _out(name, ...args) {
     93     const out = [0n];
     94     this._check(this.api[name](this.identity, ...args, out));
     95     return BigInt(out[0]);
     96   }
     97   close() {
     98     if (this.identity !== 0n) {
     99       this._check(this.api.context_free(this.identity));
    100       this.identity = 0n;
    101     }
    102   }
    103   [Symbol.dispose]() { this.close(); }
    104   release(value) { this._check(this.api.release(this.identity, value)); }
    105   retain(value) { return this._out('retain', value); }
    106   leaf() { return this._out('leaf'); }
    107   stem(value) { return this._out('stem', value); }
    108   fork(a, b) { return this._out('fork', a, b); }
    109   apply(fn, arg) { return this._out('apply', fn, arg); }
    110   reduce(value) { return this._out('reduce', value); }
    111   child(value, index) {
    112     if (!Number.isInteger(index) || index < 0 || index > 0xffffffff) throw new RangeError('invalid child index');
    113     return this._out('child', value, index);
    114   }
    115   kind(value) { return Number(this._out('kind', value)); }
    116   number(value) {
    117     if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new RangeError('use bigint for large integers');
    118     const integer = BigInt(value);
    119     if (integer < 0n || integer > 0xffffffffffffffffn) throw new RangeError('number must fit uint64');
    120     return this._out('from_u64', integer);
    121   }
    122   toNumber(value) { return this._out('to_u64', value); }
    123   toBool(value) { return this._out('to_bool', value) !== 0n; }
    124   bytes(value) {
    125     const bytes = Buffer.from(value);
    126     return this._out('from_bytes', bytes, bytes.length);
    127   }
    128   string(value) {
    129     // Buffer UTF-8 encoding otherwise silently replaces unpaired surrogates.
    130     if (!value.isWellFormed()) throw new TypeError('string contains an unpaired surrogate');
    131     const bytes = Buffer.from(value, 'utf8');
    132     return this._out('from_utf8', bytes, bytes.length);
    133   }
    134   _buffer(name, value) {
    135     const pointer = [null], length = [0];
    136     this._check(this.api[name](this.identity, value, pointer, length));
    137     return this._copy(pointer[0], length[0]);
    138   }
    139   toBytes(value) { return this._buffer('to_bytes', value); }
    140   toString(value) { return this._buffer('to_utf8', value).toString('utf8'); }
    141   loadBundle(value, name) {
    142     if (!name.isWellFormed()) throw new TypeError('invalid export name');
    143     const bytes = Buffer.from(value), encoded = Buffer.from(name, 'utf8');
    144     return this._out('load_bundle', bytes, bytes.length, encoded, encoded.length);
    145   }
    146   runIO(program) {
    147     const outcome = [0], value = [0n], cleanup = [0n];
    148     this._check(this.api.run_io(this.identity, program, outcome, value, cleanup));
    149     return { outcome: outcome[0], value: BigInt(value[0]), cleanup: BigInt(cleanup[0]) };
    150   }
    151 }