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 }