detlef.mjsjavascript
import { instantiatePackage } from './packages.mjs';
/**
* Create the embedding API for an initialized wasm-bindgen module.
*
* Browser modules must first finish their default initialization function. Node's
* generated CommonJS module is initialized when required. Classes from different
* createAPI calls must not be mixed, even when they use the same Wasm module.
*
* @param {Object} wasm Initialized generated Wasm bindings.
* @returns {DetlefAPI} Runtime and value/access classes for these bindings.
*/
export function createAPI(wasm) {
const synchronous = result => {
if (result && typeof result.then === 'function') {
throw new TypeError('Detlef callbacks must be synchronous');
}
return result;
};
/**
* A handle to a DETLEF value or evaluation result.
*
* Instances are returned by execution and access operations; they must not be
* constructed manually. Host-owned instances must be disposed. Callback-local
* instances are disposed automatically; retain() creates a separately owned copy.
*
* @alias Value
* @class
* @hideconstructor
*/
class Value {
constructor(runtime, handle) {
this.runtime = runtime;
this.handle = handle;
this.closed = false;
}
/**
* Internal implementation helper.
*
* @private
*/
_handle(runtime) {
if (this.closed) throw new Error('value is disposed');
if (runtime !== this.runtime) throw new Error('value belongs to another runtime');
return this.handle;
}
/**
* Read a primitive value. Arrays and records are not copied into JavaScript objects.
*
* @memberof Value
* @instance
* @function
* @returns {Primitive} BigInt for integer, Number for float, null for none, or undefined for no result.
* @throws {Error} An object/descriptor cannot be read as a primitive, or the handle is disposed.
*/
read() {
return this.runtime._use(a => a.raw.read(this._handle(this.runtime)));
}
/**
* Format the value as text. Arrays and records use JSON representation;
* primitive strings yield their contents.
*
* @memberof Value
* @instance
* @function
* @returns {string} Text representation.
* @throws {Error} The value is disposed, belongs to an unavailable runtime, or cannot be formatted.
*/
format() {
return this.runtime._use(a => a.raw.format(this._handle(this.runtime)));
}
/**
* Read the number of elements in an array.
*
* @memberof Value
* @instance
* @member
* @type {number}
* @readonly
* @throws {Error} The value is not an array or its handle is disposed.
*/
get length() {
return this.runtime._use(a => a.raw.array_length(this._handle(this.runtime)));
}
/**
* Read an array element or a named record field.
*
* The returned wrapper must be disposed outside a registered callback. Inside
* a Function/Control callback it is temporary and disposed automatically.
*
* @memberof Value
* @instance
* @function
* @param {(number|string)} key Zero-based array index or record field name.
* @returns {Value} A new wrapper to the selected value.
* @throws {Error} Invalid index/field, wrong object type, or a disposed handle.
*/
get(key) {
if (
typeof key === 'number' &&
(!Number.isSafeInteger(key) || key < 0 || key > 0xffffffff)
) {
throw new RangeError('array index must be a nonnegative 32-bit integer');
}
return this.runtime._use(a =>
a.wrap(
typeof key === 'number'
? a.raw.array_get(this._handle(this.runtime), key)
: a.raw.record_get(this._handle(this.runtime), key)
)
);
}
/**
* Replace an array element or record field after checking its declared type.
* The supplied Value is borrowed and is not consumed.
*
* @memberof Value
* @instance
* @function
* @param {(number|string)} key Zero-based array index or record field name.
* @param {HostValue} value Replacement value.
* @returns {undefined} No value.
* @throws {Error} Invalid index/field, type mismatch, or unavailable handles.
*/
set(key, value) {
if (
typeof key === 'number' &&
(!Number.isSafeInteger(key) || key < 0 || key > 0xffffffff)
) {
throw new RangeError('array index must be a nonnegative 32-bit integer');
}
this.runtime._use(a =>
a.withValue(value, v =>
typeof key === 'number'
? a.raw.array_set(this._handle(this.runtime), key, v._handle(this.runtime))
: a.raw.record_set(this._handle(this.runtime), key, v._handle(this.runtime))
)
);
}
/**
* Create a separately owned wrapper to the same value. Objects remain shared.
*
* The returned wrapper must be disposed explicitly, including when created
* inside a registered callback.
*
* @memberof Value
* @instance
* @function
* @returns {Value} Retained wrapper.
* @throws {Error} The source handle or runtime is unavailable.
*/
retain() {
return this.runtime._use(
a => new Value(this.runtime, a.raw.retain(this._handle(this.runtime)))
);
}
/**
* Release this wrapper once. Repeated disposal has no effect.
* Must be called before runtime disposal; a released wrapper must not be used again.
*
* @memberof Value
* @instance
* @function
* @returns {undefined} No value.
* @throws {Error} The runtime is already disposed or the handle cannot be released.
*/
dispose() {
if (this.closed) return;
this.runtime._use(a => a.raw.release(this.handle));
this.closed = true;
}
}
/**
* Borrowed value/object access for an eager Function callback.
*
* Must not be constructed, disposed, or saved for later use. Access expires when
* the callback returns. Native Functions cannot execute code, mutate variables,
* or register definitions, including through a captured Runtime.
*
* @alias FunctionAccess
* @class
* @hideconstructor
*/
class FunctionAccess {
constructor(runtime, raw, temporaries = null) {
this.runtime = runtime;
this.raw = raw;
this.temporaries = temporaries;
this.active = true;
}
/**
* Internal implementation helper.
*
* @private
*/
check() {
if (!this.active) throw new Error('access has expired');
}
/**
* Internal implementation helper.
*
* @private
*/
wrap(handle) {
this.check();
const value = new Value(this.runtime, handle);
this.temporaries?.add(value);
return value;
}
/**
* Wrap a JavaScript primitive as a DETLEF value, or borrow an existing Value.
*
* Numbers become floats; integers require BigInt within the signed 64-bit range.
* A new wrapper is temporary inside a registered callback. Otherwise it must be
* disposed explicitly. Existing wrappers keep their original ownership.
*
* @memberof FunctionAccess
* @instance
* @function
* @param {HostValue} value Primitive or Value from this runtime.
* @returns {Value} New primitive wrapper, or the supplied Value unchanged.
* @throws {Error} Unsupported input, out-of-range integer, disposed handle, or expired access.
*/
value(value) {
this.check();
if (value instanceof Value) {
value._handle(this.runtime);
return value;
}
let handle;
if (value === undefined) handle = 0n;
else if (value === null) handle = this.raw.none();
else if (typeof value === 'bigint') {
if (value < -(1n << 63n) || value >= 1n << 63n)
throw new RangeError('integer is outside signed 64-bit range');
handle = this.raw.integer(value);
} else if (typeof value === 'number') handle = this.raw.float(value);
else if (typeof value === 'boolean') handle = this.raw.boolean(value);
else if (typeof value === 'string') handle = this.raw.string(value);
else throw new TypeError('expected a primitive or Detlef Value');
return this.wrap(handle);
}
/**
* Internal implementation helper.
*
* @private
*/
withValue(input, fn) {
const value = this.value(input);
try {
return fn(value);
} finally {
if (!(input instanceof Value)) value.dispose();
}
}
/**
* Create an array by copying the supplied values. Input wrappers are not consumed.
* The result follows the same temporary/host-owned lifetime as value().
*
* @memberof FunctionAccess
* @instance
* @function
* @param {TypeSpec} elementType Element type, not the enclosing array type.
* @param {HostValue[]} [items=[]] Elements to copy.
* @returns {Value} Array wrapper.
* @throws {Error} Invalid type, non-array items, element type mismatch, or expired access.
*/
array(elementType, items = []) {
this.check();
return this._objects(items, handles =>
this.raw.create_array(elementType, handles)
);
}
/**
* Create an instance of an existing record type. Input wrappers are not consumed.
* The result follows the same temporary/host-owned lifetime as value().
*
* @memberof FunctionAccess
* @instance
* @function
* @param {string} type Name of a defined record type.
* @param {HostValue[]} [fields=[]] Values in field declaration order.
* @returns {Value} Record wrapper.
* @throws {Error} Unknown record, field count/type mismatch, or expired access.
*/
record(type, fields = []) {
this.check();
return this._objects(fields, handles => this.raw.create_record(type, handles));
}
/**
* Internal implementation helper.
*
* @private
*/
_objects(items, create) {
if (!Array.isArray(items)) throw new TypeError('expected an array of values');
const values = [];
try {
for (const item of items) values.push(this.value(item));
return this.wrap(create(values.map(value => value._handle(this.runtime))));
} finally {
for (let i = 0; i < values.length; i++)
if (!(items[i] instanceof Value)) values[i].dispose();
}
}
}
/**
* Borrowed context access for host setup and lazy Controls.
*
* Includes the value/object methods of FunctionAccess. Must not be constructed,
* disposed, or used after its callback returns. An ancestor access must not be
* used while a nested operation is executing.
*
* @alias Access
* @class
* @augments FunctionAccess
* @hideconstructor
*/
class Access extends FunctionAccess {
/**
* Evaluate source in the current context and return its last evaluation.
* Earlier side effects remain if a later expression fails.
*
* @memberof Access
* @instance
* @function
* @param {string} source DETLEF source.
* @returns {Value} Result wrapper; must be disposed in host setup, but is temporary in a Control callback.
* @throws {Error} Parsing/evaluation failure or expired/busy access.
*/
execute(source) {
this.check();
return this.wrap(this.raw.execute(source));
}
/**
* Read an existing mutable variable. This operation does not look up a callable.
* The result must be disposed in host setup, but is temporary in a Control callback.
*
* @memberof Access
* @instance
* @function
* @param {string} name Variable name.
* @returns {Value} Current value wrapper.
* @throws {Error} Unknown/uninitialized variable or expired access.
*/
getVariable(name) {
this.check();
return this.wrap(this.raw.get_variable(name));
}
/**
* Assign an existing mutable variable after checking its declared type.
* The supplied Value is borrowed and not consumed.
*
* @memberof Access
* @instance
* @function
* @param {string} name Variable name.
* @param {HostValue} value Replacement value.
* @returns {undefined} No value.
* @throws {Error} Unknown variable, type mismatch, or expired access.
*/
setVariable(name, value) {
this.check();
this.withValue(value, v => this.raw.set_variable(name, v.handle));
}
/**
* Install stdlib operations using synchronous I/O callbacks.
* Output is passed through without an added newline. Input returns a line or EOF.
*
* @memberof Access
* @instance
* @function
* @param {IOOptions} [options] Synchronous input and output callbacks.
* @returns {undefined} No value.
* @throws {Error} Installation failure or invalid callback use.
*/
installStdlib({ output = () => {}, input = () => null } = {}) {
this.check();
this.raw.install_stdlib(
text => synchronous(output(text)),
() => synchronous(input())
);
}
/**
* Register an eager Function with limited value/object access.
* Arguments are evaluated before the host callback is invoked.
*
* @memberof Access
* @instance
* @function
* @param {string} name Callable name.
* @param {TypeSpec[]} types Fixed parameter types in declaration order.
* @param {FunctionCallback} callback Synchronous host implementation.
* @param {Object} [options] Signature options.
* @param {TypeSpec} [options.variadic] Type accepted by each additional argument.
* @returns {undefined} No value.
* @throws {Error} Invalid types, signature conflicts, expired access, or an unavailable runtime.
*/
registerFunction(name, types, callback, { variadic } = {}) {
this.check();
this.raw.register_function(name, types, variadic, raw =>
this.runtime._callback(
raw,
a => {
const values = [];
for (let i = 0; i < raw.argument_count(); i++) {
const value = a.wrap(raw.argument(i));
values.push(raw.kind(value.handle) === 6 ? value : value.read());
}
return callback(a, ...values);
},
true
)
);
}
/**
* Define a record type. Records must be registered before signatures that use them.
*
* @memberof Access
* @instance
* @function
* @param {string} name Record type name.
* @param {RecordField[]} fields Fields in declaration order; field types are named types.
* @returns {undefined} No value.
* @throws {Error} Invalid fields, unknown field types, or an already defined record.
*/
registerRecord(name, fields) {
this.check();
this.raw.register_record(name, fields);
}
/**
* Register a lazy Control. Its callback chooses which expressions to evaluate.
*
* @memberof Access
* @instance
* @function
* @param {string} name Callable name.
* @param {ControlCallback} callback Synchronous host implementation.
* @returns {undefined} No value.
* @throws {Error} Invalid registration, expired access, or an unavailable runtime.
*/
registerControl(name, callback) {
this.check();
this.raw.register_control(name, raw =>
this.runtime._callback(raw, a =>
callback(a, {
length: raw.argument_count(),
evaluate: index => a.wrap(raw.argument(index)),
identifier: index => {
a.check();
return raw.argument_identifier(index);
},
})
)
);
}
}
/**
* An interpreter instance and the host callbacks and packages installed in it.
*
* Create with new Runtime() after createAPI(). No stdlib is installed initially.
* Must be disposed explicitly; JavaScript garbage collection does not replace
* disposal or break every host-callback reference cycle.
*
* @alias Runtime
* @class
*/
class Runtime {
constructor() {
this.raw = new wasm.WasmRuntime();
this.active = null;
this.closed = false;
this.packages = new Map();
this.loading = false;
}
/**
* Load a separately compiled Wasm artifact from a package archive.
*
* Downloads and file access belong to the host. A failed initializer may have
* registered definitions; its instance is retained and cannot be loaded again
* under the same package name. Loading must finish before another load or disposal.
*
* @memberof Runtime
* @instance
* @function
* @param {(ArrayBuffer|Uint8Array)} bytes Complete .dtfpkg archive.
* @returns {Promise<PackageInfo>} Package name and version after initialization.
* @throws {Error} Malformed archive, duplicate package, incompatible protocol/imports, or initialization failure.
*/
async loadPackage(bytes) {
if (this.closed || this.active || this.loading)
throw new Error('runtime is unavailable for package loading');
const archive = wasm.unpack_package(new Uint8Array(bytes));
if (this.packages.has(archive.name))
throw new Error('package already loaded: ' + archive.name);
this.loading = true;
try {
const packageInstance = await instantiatePackage(this, archive.binary);
this.packages.set(archive.name, packageInstance);
packageInstance.install();
return { name: archive.name, version: archive.version };
} finally {
this.loading = false;
}
}
/**
* Invoke a synchronous setup callback with borrowed Access.
*
* Values created here are host-owned and must be disposed, even though access
* expires on callback return. A Promise result is rejected. Reentry through this
* method is rejected; inside a Control use the supplied Access instead.
*
* @memberof Runtime
* @instance
* @function
* @param {SetupCallback} callback Synchronous setup operation.
* @returns {*} The callback result.
* @throws {Error} Callback failure, Promise result, or busy/disposed runtime.
*/
withAccess(callback) {
if (this.closed) throw new Error('runtime is closed');
if (this.active) throw new Error('runtime is busy; use the current access');
return this.raw.access(raw => this._frame(raw, a => synchronous(callback(a))));
}
/**
* Internal implementation helper.
*
* @private
*/
_frame(raw, callback, temporaries = null, functionOnly = false) {
const previous = this.active;
const access = functionOnly
? new FunctionAccess(this, raw, temporaries)
: new Access(this, raw, temporaries);
this.active = access;
try {
return callback(access);
} finally {
try {
if (temporaries) for (const value of temporaries) value.dispose();
} finally {
access.active = false;
this.active = previous;
raw.free();
}
}
}
/**
* Internal implementation helper.
*
* @private
*/
_callback(raw, callback, functionOnly = false) {
return this._frame(
raw,
a => {
const result = synchronous(callback(a));
// Transfer a clone so returning a retained host value preserves it.
return a.raw.retain(a.value(result)._handle(this));
},
new Set(),
functionOnly
);
}
/**
* Internal implementation helper.
*
* @private
*/
_use(callback) {
if (this.closed) throw new Error('runtime is closed');
return this.active ? callback(this.active) : this.withAccess(callback);
}
/**
* Evaluate source and return the last evaluation. The result must be disposed.
* Calling this through a captured runtime inside an eager Function is rejected.
*
* @memberof Runtime
* @instance
* @function
* @param {string} source DETLEF source.
* @returns {Value} Host-owned result wrapper; callback use follows the active callback scope.
* @throws {Error} Parsing/evaluation failure or unavailable context.
*/
execute(source) {
return this._context(a => a.execute(source));
}
/**
* Install stdlib operations with synchronous I/O.
* Equivalent to Access.installStdlib() in a context access.
*
* @memberof Runtime
* @instance
* @function
* @param {IOOptions} [options] Synchronous input and output callbacks.
* @returns {undefined} No value.
* @throws {Error} Installation failure or invalid callback use.
*/
installStdlib(options) {
this._context(a => a.installStdlib(options));
}
/**
* Register an eager Function. Equivalent to Access.registerFunction().
*
* @memberof Runtime
* @instance
* @function
* @param {string} name Callable name.
* @param {TypeSpec[]} types Fixed parameter types in declaration order.
* @param {FunctionCallback} callback Synchronous host implementation.
* @param {Object} [options] Signature options.
* @param {TypeSpec} [options.variadic] Type accepted by each additional argument.
* @returns {undefined} No value.
* @throws {Error} Invalid types, signature conflicts, expired access, or an unavailable runtime.
*/
registerFunction(...args) {
this._context(a => a.registerFunction(...args));
}
/**
* Define a record type. Equivalent to Access.registerRecord().
*
* @memberof Runtime
* @instance
* @function
* @param {string} name Record type name.
* @param {RecordField[]} fields Fields in declaration order; field types are named types.
* @returns {undefined} No value.
* @throws {Error} Invalid fields, unknown field types, or an already defined record.
*/
registerRecord(...args) {
this._context(a => a.registerRecord(...args));
}
/**
* Register a lazy Control. Equivalent to Access.registerControl().
*
* @memberof Runtime
* @instance
* @function
* @param {string} name Callable name.
* @param {ControlCallback} callback Synchronous host implementation.
* @returns {undefined} No value.
* @throws {Error} Invalid registration, expired access, or an unavailable runtime.
*/
registerControl(...args) {
this._context(a => a.registerControl(...args));
}
/**
* Internal implementation helper.
*
* @private
*/
_context(callback) {
return this._use(a => {
if (!(a instanceof Access))
throw new Error('context operations are not available to native Functions');
return callback(a);
});
}
/**
* Release package-retained values, runtime handles, callbacks, and package instances.
*
* Must be called explicitly. Repeated calls after successful disposal have no
* effect. Disposal while a callback or package load is active is rejected.
* All remaining Value and access wrappers become unusable.
*
* @memberof Runtime
* @instance
* @function
* @returns {undefined} No value.
* @throws {Error} Active runtime/package load or package cleanup failure.
*/
dispose() {
if (this.closed) return;
if (this.active || this.loading)
throw new Error('cannot dispose an active runtime');
let cleanupError;
for (const pkg of this.packages.values()) {
try {
pkg.release();
} catch (error) {
cleanupError ??= error;
}
}
this.raw.close();
this.raw.free();
this.closed = true;
for (const pkg of this.packages.values()) {
try {
pkg.close();
} catch (error) {
cleanupError ??= error;
}
}
this.packages.clear();
if (cleanupError) throw cleanupError;
}
}
return { Runtime, Value, FunctionAccess, Access };
}
/**
* The classes returned by createAPI(). Access classes and Value are instantiated by
* the wrapper; only Runtime is constructed directly by an embedding application.
* @typedef {Object} DetlefAPI
* @property {Function} Runtime Runtime constructor.
* @property {Function} Value Value class, for identity checks.
* @property {Function} FunctionAccess Limited access class.
* @property {Function} Access Full context access class.
*/
/**
* Primitive bridge values: BigInt integers, Number floats, booleans, strings,
* null for none, and undefined for no result.
* @typedef {(bigint|number|boolean|string|null|undefined)} Primitive
*/
/**
* A primitive or a Value belonging to the same runtime.
* @typedef {(Primitive|Value)} HostValue
*/
/**
* A named type such as 'integer', or an object with one recursive key:
* { array: TypeSpec } or { optional: TypeSpec }.
* @typedef {(string|Object)} TypeSpec
*/
/**
* A record field. This bridge currently accepts named field types.
* @typedef {Object} RecordField
* @property {string} name Field name.
* @property {string} type Existing named field type.
*/
/**
* Synchronous stdlib I/O. Output receives UTF-8 text without an implicit newline.
* Input returns a string (including an empty line), or null/undefined for EOF.
* Line endings are stripped by stdlib. Promises are rejected. An output exception
* is reported after the enclosing access returns; side effects may already exist.
* @typedef {Object} IOOptions
* @property {function(string): void} [output] Output callback; default discards text.
* @property {function(): (string|null|undefined)} [input] Input callback; default EOF.
*/
/**
* Metadata returned after successful package loading.
* @typedef {Object} PackageInfo
* @property {string} name Manifest package name.
* @property {string} version Manifest version string.
*/
/**
* Unevaluated Control arguments. Must not be saved beyond the callback.
* @typedef {Object} ControlArguments
* @property {number} length Argument count.
* @property {function(number): Value} evaluate Evaluates one argument by zero-based
* index. Repeated calls repeat side effects. The result is callback-local.
* @property {function(number): string} identifier Reads an identifier argument
* without evaluating it; other expression kinds fail.
*/
/**
* Synchronous host setup. Access expires on return. Values created here must be
* disposed explicitly. Returning a Promise is rejected.
* @callback SetupCallback
* @param {Access} access Borrowed context access.
* @returns {*} Result passed back through withAccess().
*/
/**
* Eager native Function implementation. Object arguments are temporary Value
* wrappers; primitive arguments are JavaScript primitives. Callback-local wrappers
* are disposed automatically. A returned retained Value remains separately owned.
* @callback FunctionCallback
* @param {FunctionAccess} access Borrowed limited access.
* @param {...HostValue} values Evaluated arguments.
* @returns {HostValue} Result; undefined supplies no value. Promises are rejected.
*/
/**
* Lazy Control implementation. Argument evaluation is explicitly requested through
* args. Callback-local wrappers are disposed automatically; retain before saving.
* @callback ControlCallback
* @param {Access} access Borrowed context access.
* @param {ControlArguments} args Borrowed expression access.
* @returns {HostValue} Result; undefined supplies no value. Promises are rejected.
*/