@bytecodealliance/jco
Version:
JavaScript tooling for working with WebAssembly Components
1,633 lines (1,339 loc) • 380 kB
JavaScript
"use components";
import { environment, exit as exit$1, stderr, stdin, stdout, terminalInput, terminalOutput, terminalStderr, terminalStdin, terminalStdout } from '@bytecodealliance/preview2-shim/cli';
import { preopens, types } from '@bytecodealliance/preview2-shim/filesystem';
import { error, streams } from '@bytecodealliance/preview2-shim/io';
import { random } from '@bytecodealliance/preview2-shim/random';
const { getEnvironment } = environment;
if (getEnvironment=== undefined) {
const err = new Error("unexpectedly undefined local import 'getEnvironment', was 'getEnvironment' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getEnvironment._isHostProvided = true;
const { exit } = exit$1;
if (exit=== undefined) {
const err = new Error("unexpectedly undefined local import 'exit', was 'exit' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
exit._isHostProvided = true;
const { getStderr } = stderr;
if (getStderr=== undefined) {
const err = new Error("unexpectedly undefined local import 'getStderr', was 'getStderr' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getStderr._isHostProvided = true;
const { getStdin } = stdin;
if (getStdin=== undefined) {
const err = new Error("unexpectedly undefined local import 'getStdin', was 'getStdin' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getStdin._isHostProvided = true;
const { getStdout } = stdout;
if (getStdout=== undefined) {
const err = new Error("unexpectedly undefined local import 'getStdout', was 'getStdout' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getStdout._isHostProvided = true;
const { TerminalInput } = terminalInput;
if (TerminalInput=== undefined) {
const err = new Error("unexpectedly undefined local import 'TerminalInput', was 'TerminalInput' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
TerminalInput._isHostProvided = true;
const { TerminalOutput } = terminalOutput;
if (TerminalOutput=== undefined) {
const err = new Error("unexpectedly undefined local import 'TerminalOutput', was 'TerminalOutput' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
TerminalOutput._isHostProvided = true;
const { getTerminalStderr } = terminalStderr;
if (getTerminalStderr=== undefined) {
const err = new Error("unexpectedly undefined local import 'getTerminalStderr', was 'getTerminalStderr' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getTerminalStderr._isHostProvided = true;
const { getTerminalStdin } = terminalStdin;
if (getTerminalStdin=== undefined) {
const err = new Error("unexpectedly undefined local import 'getTerminalStdin', was 'getTerminalStdin' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getTerminalStdin._isHostProvided = true;
const { getTerminalStdout } = terminalStdout;
if (getTerminalStdout=== undefined) {
const err = new Error("unexpectedly undefined local import 'getTerminalStdout', was 'getTerminalStdout' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getTerminalStdout._isHostProvided = true;
const { getDirectories } = preopens;
if (getDirectories=== undefined) {
const err = new Error("unexpectedly undefined local import 'getDirectories', was 'getDirectories' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getDirectories._isHostProvided = true;
const { Descriptor,
DirectoryEntryStream,
filesystemErrorCode } = types;
if (Descriptor=== undefined) {
const err = new Error("unexpectedly undefined local import 'Descriptor', was 'Descriptor' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
Descriptor._isHostProvided = true;
if (DirectoryEntryStream=== undefined) {
const err = new Error("unexpectedly undefined local import 'DirectoryEntryStream', was 'DirectoryEntryStream' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
DirectoryEntryStream._isHostProvided = true;
if (filesystemErrorCode=== undefined) {
const err = new Error("unexpectedly undefined local import 'filesystemErrorCode', was 'filesystemErrorCode' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
filesystemErrorCode._isHostProvided = true;
const { Error: Error$1 } = error;
if (Error$1=== undefined) {
const err = new Error("unexpectedly undefined local import 'Error$1', was 'Error' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
Error$1._isHostProvided = true;
const { InputStream,
OutputStream } = streams;
if (InputStream=== undefined) {
const err = new Error("unexpectedly undefined local import 'InputStream', was 'InputStream' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
InputStream._isHostProvided = true;
if (OutputStream=== undefined) {
const err = new Error("unexpectedly undefined local import 'OutputStream', was 'OutputStream' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
OutputStream._isHostProvided = true;
const { getRandomBytes } = random;
if (getRandomBytes=== undefined) {
const err = new Error("unexpectedly undefined local import 'getRandomBytes', was 'getRandomBytes' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
getRandomBytes._isHostProvided = true;
function promiseWithResolvers() {
if (Promise.withResolvers) {
return Promise.withResolvers();
} else {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
}
const symbolDispose = Symbol.dispose || Symbol.for('dispose');
const symbolAsyncIterator = Symbol.asyncIterator;
const symbolIterator = Symbol.iterator;
const _debugLog = (...args) => {
if (!globalThis?.process?.env?.JCO_DEBUG) { return; }
console.debug(...args);
};
const ASYNC_DETERMINISM = 'random';
const GLOBAL_COMPONENT_MEMORY_MAP = new Map();
const CURRENT_TASK_META = {};
function _getGlobalCurrentTaskMeta(componentIdx) {
const v = CURRENT_TASK_META[componentIdx];
if (v === undefined) { return v; }
return { ...v };
}
function _setGlobalCurrentTaskMeta(args) {
if (!args) { throw new TypeError('args missing'); }
if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
const { taskID, componentIdx } = args;
return CURRENT_TASK_META[componentIdx] = { taskID, componentIdx };
}
function _withGlobalCurrentTaskMeta(args) {
_debugLog('[_withGlobalCurrentTaskMeta()] args', args);
if (!args) { throw new TypeError('args missing'); }
if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
if (!args.fn) { throw new TypeError('missing fn'); }
const { taskID, componentIdx, fn } = args;
try {
CURRENT_TASK_META[componentIdx] = { taskID, componentIdx };
return fn();
} catch (err) {
_debugLog("error while executing sync callee/callback", {
...args,
err,
});
throw err;
} finally {
CURRENT_TASK_META[componentIdx] = null;
}
}
async function _withGlobalCurrentTaskMetaAsync(args) {
_debugLog('[_withGlobalCurrentTaskMetaAsync()] args', args);
if (!args) { throw new TypeError('args missing'); }
if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
if (!args.fn) { throw new TypeError('missing fn'); }
const { taskID, componentIdx, fn } = args;
// If there is already an async task executing, we must wait for it
// to complete before we can can run the closure we were given
//
let current = CURRENT_TASK_META[componentIdx];
let cstate;
if (current && current.taskID !== taskID) {
cstate = getOrCreateAsyncState(componentIdx);
while (current && current.taskID !== taskID) {
const { promise, resolve } = Promise.withResolvers();
cstate.onNextExclusiveRelease(resolve);
await promise;
current = CURRENT_TASK_META[componentIdx];
}
// Since we've just waited for the component to not be locked, re-lock
// exclusivity so we can run the fn below (likely a callee/callback)
cstate.exclusiveLock();
}
try {
CURRENT_TASK_META[componentIdx] = { taskID, componentIdx };
return await fn();
} catch (err) {
_debugLog("error while executing async callee/callback", {
...args,
err,
});
throw err;
} finally {
CURRENT_TASK_META[componentIdx] = null;
}
}
async function _clearCurrentTask(args) {
_debugLog('[_clearCurrentTask()] args', args);
if (!args) { throw new TypeError('args missing'); }
if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
const { taskID, componentIdx } = args;
const meta = CURRENT_TASK_META[componentIdx];
if (!meta) { throw new Error(`missing current task meta for component idx [${componentIdx}]n`); }
if (meta.taskID !== taskID) {
throw new Error(`task ID [${meta.taskID}] != requested ID [${taskID}]`);
}
if (meta.componentIdx !== componentIdx) {
throw new Error(`component idx [${meta.componentIdx}] != requested idx [${componentIdx}]`);
}
CURRENT_TASK_META[componentIdx] = null;
}
function lookupMemoriesForComponent(args) {
const { componentIdx } = args ?? {};
if (args.componentIdx === undefined) { throw new TypeError("missing component idx"); }
const metas = GLOBAL_COMPONENT_MEMORY_MAP.get(componentIdx);
if (!metas) { return []; }
if (args.memoryIdx === undefined) {
return Object.values(metas);
}
const meta = metas[args.memoryIdx];
return meta?.memory;
}
function registerGlobalMemoryForComponent(args) {
const { componentIdx, memory, memoryIdx } = args ?? {};
if (componentIdx === undefined) { throw new TypeError('missing component idx'); }
if (memory === undefined && memoryIdx === undefined) { throw new TypeError('missing both memory & memory idx'); }
let inner = GLOBAL_COMPONENT_MEMORY_MAP.get(componentIdx);
if (!inner) {
inner = {};
GLOBAL_COMPONENT_MEMORY_MAP.set(componentIdx, inner);
}
inner[memoryIdx] = { memory, memoryIdx, componentIdx };
}
class RepTable {
#data = [0, null];
#target;
constructor(args) {
this.target = args?.target;
}
data() { return this.#data; }
insert(val) {
_debugLog('[RepTable#insert()] args', { val, target: this.target });
const freeIdx = this.#data[0];
if (freeIdx === 0) {
this.#data.push(val);
this.#data.push(null);
const rep = (this.#data.length >> 1) - 1;
_debugLog('[RepTable#insert()] inserted', { val, target: this.target, rep });
return rep;
}
this.#data[0] = this.#data[freeIdx << 1];
const placementIdx = freeIdx << 1;
this.#data[placementIdx] = val;
this.#data[placementIdx + 1] = null;
_debugLog('[RepTable#insert()] inserted', { val, target: this.target, rep: freeIdx });
return freeIdx;
}
get(rep) {
_debugLog('[RepTable#get()] args', { rep, target: this.target });
if (rep === 0) { throw new Error('invalid resource rep during get, (cannot be 0)'); }
const baseIdx = rep << 1;
const val = this.#data[baseIdx];
return val;
}
contains(rep) {
_debugLog('[RepTable#contains()] args', { rep, target: this.target });
if (rep === 0) { throw new Error('invalid resource rep during contains, (cannot be 0)'); }
const baseIdx = rep << 1;
return !!this.#data[baseIdx];
}
remove(rep) {
_debugLog('[RepTable#remove()] args', { rep, target: this.target });
if (rep === 0) { throw new Error('invalid resource rep during remove, (cannot be 0)'); }
if (this.#data.length === 2) { throw new Error('invalid'); }
const baseIdx = rep << 1;
const val = this.#data[baseIdx];
this.#data[baseIdx] = this.#data[0];
this.#data[0] = rep;
return val;
}
clear() {
_debugLog('[RepTable#clear()] args', { rep, target: this.target });
this.#data = [0, null];
}
}
const _coinFlip = () => { return Math.random() > 0.5; };
let SCOPE_ID = 0;
const I32_MIN = -2_147_483_648;
const I32_MAX = 2_147_483_647;
function _isValidNumericPrimitive(ty, v) {
if (v === undefined || v === null) { return false; }
switch (ty) {
case 'bool':
return v === 0 || v === 1;
break;
case 'u8':
return v >= 0 && v <= 255;
break;
case 's8':
return v >= -128 && v <= 127;
break;
case 'u16':
return v >= 0 && v <= 65535;
break;
case 's16':
return v >= -32768 && v <= 32767;
case 'u32':
return v >= 0 && v <= 4_294_967_295;
case 's32':
return v >= -2_147_483_648 && v <= 2_147_483_647;
case 'u64':
return typeof v === 'bigint' && v >= 0 && v <= 18_446_744_073_709_551_615n;
case 's64':
return typeof v === 'bigint' && v >= -9223372036854775808n && v <= 9223372036854775807n;
break;
case 'f32':
case 'f64': return typeof v === 'number';
default:
return false;
}
return true;
}
function _requireValidNumericPrimitive(ty, v) {
if (v === undefined || v === null || !_isValidNumericPrimitive(ty, v)) {
throw new TypeError(`invalid ${ty} value [${v}]`);
}
return true;
}
const _typeCheckValidI32 = (n) => typeof n === 'number' && n >= I32_MIN && n <= I32_MAX;
const _typeCheckAsyncFn= (f) => {
return f instanceof ASYNC_FN_CTOR;
};
let RESOURCE_CALL_BORROWS = [];const ASYNC_FN_CTOR = (async () => {}).constructor;
function clearCurrentTask(componentIdx, taskID) {
_debugLog('[clearCurrentTask()] args', { componentIdx, taskID });
if (componentIdx === undefined || componentIdx === null) {
throw new Error('missing/invalid component instance index while ending current task');
}
const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
if (!tasks || !Array.isArray(tasks)) {
throw new Error('missing/invalid tasks for component instance while ending task');
}
if (tasks.length == 0) {
throw new Error(`no current tasks for component instance [${componentIdx}] while ending task`);
}
if (taskID !== undefined) {
const last = tasks[tasks.length - 1];
if (last.id !== taskID) {
// throw new Error('current task does not match expected task ID');
return;
}
}
ASYNC_CURRENT_TASK_IDS.pop();
ASYNC_CURRENT_COMPONENT_IDXS.pop();
const taskMeta = tasks.pop();
return taskMeta.task;
}
const CURRENT_TASK_MAY_BLOCK = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
const ASYNC_CURRENT_TASK_IDS = [];
const ASYNC_CURRENT_COMPONENT_IDXS = [];
function unpackCallbackResult(result) {
if (!(_typeCheckValidI32(result))) { throw new Error('invalid callback return value [' + result + '], not a valid i32'); }
const eventCode = result & 0xF;
if (eventCode < 0 || eventCode > 3) {
throw new Error('invalid async return value [' + eventCode + '], outside callback code range');
}
if (result < 0 || result >= 2**32) { throw new Error('invalid callback result'); }
// TODO: table max length check?
const waitableSetRep = result >> 4;
return [eventCode, waitableSetRep];
}
class AsyncSubtask {
static _ID = 0n;
static State = {
STARTING: 0,
STARTED: 1,
RETURNED: 2,
CANCELLED_BEFORE_STARTED: 3,
CANCELLED_BEFORE_RETURNED: 4,
};
#id;
#state = AsyncSubtask.State.STARTING;
#componentIdx;
#parentTask;
#childTask = null;
#dropped = false;
#cancelRequested = false;
#memoryIdx = null;
#lenders = null;
#waitable = null;
#callbackFn = null;
#callbackFnName = null;
#postReturnFn = null;
#onProgressFn = null;
#pendingEventFn = null;
#callMetadata = {};
#resolved = false;
#onResolveHandlers = [];
#onStartHandlers = [];
#result = null;
#resultSet = false;
fnName;
target;
isAsync;
isManualAsync;
constructor(args) {
if (typeof args.componentIdx !== 'number') {
throw new Error('invalid componentIdx for subtask creation');
}
this.#componentIdx = args.componentIdx;
this.#id = ++AsyncSubtask._ID;
this.fnName = args.fnName;
if (!args.parentTask) { throw new Error('missing parent task during subtask creation'); }
this.#parentTask = args.parentTask;
if (args.childTask) { this.#childTask = args.childTask; }
if (args.memoryIdx) { this.#memoryIdx = args.memoryIdx; }
if (!args.waitable) { throw new Error("missing/invalid waitable"); }
this.#waitable = args.waitable;
if (args.callMetadata) { this.#callMetadata = args.callMetadata; }
this.#lenders = [];
this.target = args.target;
this.isAsync = args.isAsync;
this.isManualAsync = args.isManualAsync;
}
id() { return this.#id; }
parentTaskID() { return this.#parentTask?.id(); }
childTaskID() { return this.#childTask?.id(); }
state() { return this.#state; }
waitable() { return this.#waitable; }
waitableRep() { return this.#waitable.idx(); }
join() { return this.#waitable.join(...arguments); }
getPendingEvent() { return this.#waitable.getPendingEvent(...arguments); }
hasPendingEvent() { return this.#waitable.hasPendingEvent(...arguments); }
setPendingEvent() { return this.#waitable.setPendingEvent(...arguments); }
setTarget(tgt) { this.target = tgt; }
getResult() {
if (!this.#resultSet) { throw new Error("subtask result has not been set") }
return this.#result;
}
setResult(v) {
if (this.#resultSet) { throw new Error("subtask result has already been set"); }
this.#result = v;
this.#resultSet = true;
}
componentIdx() { return this.#componentIdx; }
setChildTask(t) {
if (!t) { throw new Error('cannot set missing/invalid child task on subtask'); }
if (this.#childTask) { throw new Error('child task is already set on subtask'); }
if (this.#parentTask === t) { throw new Error("parent cannot be child"); }
this.#childTask = t;
}
getChildTask(t) { return this.#childTask; }
getParentTask() { return this.#parentTask; }
setCallbackFn(f, name) {
if (!f) { return; }
if (this.#callbackFn) { throw new Error('callback fn can only be set once'); }
this.#callbackFn = f;
this.#callbackFnName = name;
}
getCallbackFnName() {
if (!this.#callbackFn) { return undefined; }
return this.#callbackFn.name;
}
setPostReturnFn(f) {
if (!f) { return; }
if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); }
this.#postReturnFn = f;
}
setOnProgressFn(f) {
if (this.#onProgressFn) { throw new Error('on progress fn can only be set once'); }
this.#onProgressFn = f;
}
isNotStarted() {
return this.#state == AsyncSubtask.State.STARTING;
}
registerOnStartHandler(f) {
this.#onStartHandlers.push(f);
}
onStart(args) {
_debugLog('[AsyncSubtask#onStart()] args', {
componentIdx: this.#componentIdx,
subtaskID: this.#id,
parentTaskID: this.parentTaskID(),
fnName: this.fnName,
});
if (this.#onProgressFn) { this.#onProgressFn(); }
this.#state = AsyncSubtask.State.STARTED;
let result;
// If we have been provided a helper start function as a result of
// component fusion performed by wasmtime tooling, then we can call that helper and lifts/lowers will
// be performed for us.
//
// See also documentation on `HostIntrinsic::PrepareCall`
//
if (this.#callMetadata.startFn) {
result = this.#callMetadata.startFn.apply(null, args?.startFnParams ?? []);
}
return result;
}
registerOnResolveHandler(f) {
this.#onResolveHandlers.push(f);
}
reject(subtaskErr) {
this.#childTask?.reject(subtaskErr);
}
onResolve(subtaskValue) {
_debugLog('[AsyncSubtask#onResolve()] args', {
componentIdx: this.#componentIdx,
subtaskID: this.#id,
isAsync: this.isAsync,
childTaskID: this.childTaskID(),
parentTaskID: this.parentTaskID(),
parentTaskFnName: this.#parentTask?.entryFnName(),
fnName: this.fnName,
});
if (this.#resolved) {
throw new Error('subtask has already been resolved');
}
if (this.#onProgressFn) { this.#onProgressFn(); }
if (subtaskValue === null) {
if (this.#cancelRequested) {
throw new Error('cancel was not requested, but no value present at return');
}
if (this.#state === AsyncSubtask.State.STARTING) {
this.#state = AsyncSubtask.State.CANCELLED_BEFORE_STARTED;
} else {
if (this.#state !== AsyncSubtask.State.STARTED) {
throw new Error('resolved subtask must have been started before cancellation');
}
this.#state = AsyncSubtask.State.CANCELLED_BEFORE_RETURNED;
}
} else {
if (this.#state !== AsyncSubtask.State.STARTED) {
throw new Error('resolved subtask must have been started before completion');
}
this.#state = AsyncSubtask.State.RETURNED;
}
this.setResult(subtaskValue);
for (const f of this.#onResolveHandlers) {
try {
f(subtaskValue);
} catch (err) {
console.error("error during subtask resolve handler", err);
throw err;
}
}
const callMetadata = this.getCallMetadata();
// TODO(fix): we should be able to easily have the caller's meomry
// to lower into here, but it's not present in PrepareCall
const memory = callMetadata.memory ?? this.#parentTask?.getReturnMemory() ?? lookupMemoriesForComponent({ componentIdx: this.#parentTask?.componentIdx() })[0];
if (callMetadata && !callMetadata.returnFn && this.isAsync && callMetadata.resultPtr && memory) {
const { resultPtr, realloc } = callMetadata;
const lowers = callMetadata.lowers; // may have been updated in task.return of the child
if (lowers && lowers.length > 0) {
lowers[0]({
componentIdx: this.#componentIdx,
memory,
realloc,
vals: [subtaskValue],
storagePtr: resultPtr,
stringEncoding: callMetadata.stringEncoding,
});
}
}
this.#resolved = true;
this.#parentTask.removeSubtask(this);
}
getStateNumber() { return this.#state; }
isReturned() { return this.#state === AsyncSubtask.State.RETURNED; }
getCallMetadata() { return this.#callMetadata; }
isResolved() {
if (this.#state === AsyncSubtask.State.STARTING
|| this.#state === AsyncSubtask.State.STARTED) {
return false;
}
if (this.#state === AsyncSubtask.State.RETURNED
|| this.#state === AsyncSubtask.State.CANCELLED_BEFORE_STARTED
|| this.#state === AsyncSubtask.State.CANCELLED_BEFORE_RETURNED) {
return true;
}
throw new Error('unrecognized internal Subtask state [' + this.#state + ']');
}
addLender(handle) {
_debugLog('[AsyncSubtask#addLender()] args', { handle });
if (!Number.isNumber(handle)) { throw new Error('missing/invalid lender handle [' + handle + ']'); }
if (this.#lenders.length === 0 || this.isResolved()) {
throw new Error('subtask has no lendors or has already been resolved');
}
handle.lends++;
this.#lenders.push(handle);
}
deliverResolve() {
_debugLog('[AsyncSubtask#deliverResolve()] args', {
lenders: this.#lenders,
parentTaskID: this.parentTaskID(),
subtaskID: this.#id,
childTaskID: this.childTaskID(),
resolved: this.isResolved(),
resolveDelivered: this.resolveDelivered(),
});
const cannotDeliverResolve = this.resolveDelivered() || !this.isResolved();
if (cannotDeliverResolve) {
throw new Error('subtask cannot deliver resolution twice, and the subtask must be resolved');
}
for (const lender of this.#lenders) {
lender.lends--;
}
this.#lenders = null;
}
resolveDelivered() {
_debugLog('[AsyncSubtask#resolveDelivered()] args', { });
if (this.#lenders === null && !this.isResolved()) {
throw new Error('invalid subtask state, lenders missing and subtask has not been resolved');
}
return this.#lenders === null;
}
drop() {
_debugLog('[AsyncSubtask#drop()] args', {
componentIdx: this.#componentIdx,
parentTaskID: this.#parentTask?.id(),
parentTaskFnName: this.#parentTask?.entryFnName(),
childTaskID: this.#childTask?.id(),
childTaskFnName: this.#childTask?.entryFnName(),
subtaskFnName: this.fnName,
});
if (!this.#waitable) { throw new Error('missing/invalid inner waitable'); }
if (!this.resolveDelivered()) {
throw new Error('cannot drop subtask before resolve is delivered');
}
if (this.#waitable) { this.#waitable.drop() }
this.#dropped = true;
}
#getComponentState() {
const state = getOrCreateAsyncState(this.#componentIdx);
if (!state) {
throw new Error('invalid/missing async state for component [' + componentIdx + ']');
}
return state;
}
getWaitableHandleIdx() {
_debugLog('[AsyncSubtask#getWaitableHandleIdx()] args', { });
if (!this.#waitable) { throw new Error('missing/invalid waitable'); }
return this.waitableRep();
}
}
function _prepareCall(
memoryIdx,
getMemoryFn,
startFn,
returnFn,
callerComponentIdx,
calleeComponentIdx,
taskReturnTypeIdx,
calleeIsAsyncInt,
stringEncoding,
resultCountOrAsync,
) {
_debugLog('[_prepareCall()]', {
memoryIdx,
callerComponentIdx,
calleeComponentIdx,
taskReturnTypeIdx,
calleeIsAsyncInt,
stringEncoding,
resultCountOrAsync,
});
const argArray = [...arguments];
// value passed in *may* be as large as u32::MAX which may be mangled into -2
resultCountOrAsync >>>= 0;
let isAsync = false;
let hasResultPointer = false;
if (resultCountOrAsync === 2**32 - 1) {
// prepare async with no result (u32::MAX)
isAsync = true;
hasResultPointer = false;
} else if (resultCountOrAsync === 2**32 - 2) {
// prepare async with result (u32::MAX - 1)
isAsync = true;
hasResultPointer = true;
}
const currentCallerTaskMeta = getCurrentTask(callerComponentIdx);
if (!currentCallerTaskMeta) {
throw new Error('invalid/missing current task for caller during prepare call');
}
const currentCallerTask = currentCallerTaskMeta.task;
if (!currentCallerTask) {
throw new Error('unexpectedly missing task in meta for caller during prepare call');
}
if (currentCallerTask.componentIdx() !== callerComponentIdx) {
throw new Error(`task component idx [${ currentCallerTask.componentIdx() }] !== [${ callerComponentIdx }] (callee ${ calleeComponentIdx })`);
}
let getCalleeParamsFn;
let resultPtr = null;
let directParamsArr;
if (hasResultPointer) {
directParamsArr = argArray.slice(10, argArray.length - 1);
getCalleeParamsFn = () => directParamsArr;
resultPtr = argArray[argArray.length - 1];
} else {
directParamsArr = argArray.slice(10);
getCalleeParamsFn = () => directParamsArr;
}
let encoding;
switch (stringEncoding) {
case 0:
encoding = 'utf8';
break;
case 1:
encoding = 'utf16';
break;
case 2:
encoding = 'compact-utf16';
break;
default:
throw new Error(`unrecognized string encoding enum [${stringEncoding}]`);
}
const subtask = currentCallerTask.createSubtask({
componentIdx: callerComponentIdx,
parentTask: currentCallerTask,
isAsync,
callMetadata: {
getMemoryFn,
memoryIdx,
resultPtr,
returnFn,
startFn,
stringEncoding,
}
});
const [newTask, newTaskID] = createNewCurrentTask({
componentIdx: calleeComponentIdx,
isAsync,
getCalleeParamsFn,
entryFnName: [
'task',
subtask.getParentTask().id(),
'subtask',
subtask.id(),
'new-prepared-async-task'
].join('/'),
stringEncoding,
});
newTask.setParentSubtask(subtask);
newTask.setReturnMemoryIdx(memoryIdx);
newTask.setReturnMemory(getMemoryFn);
subtask.setChildTask(newTask);
newTask.subtaskMeta = {
subtask,
calleeComponentIdx,
callerComponentIdx,
getCalleeParamsFn,
stringEncoding,
isAsync,
};
_setGlobalCurrentTaskMeta({
taskID: newTask.id(),
componentIdx: newTask.componentIdx(),
});
}
function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
const componentIdx = ASYNC_CURRENT_COMPONENT_IDXS.at(-1);
const globalTaskMeta = _getGlobalCurrentTaskMeta(componentIdx);
if (!globalTaskMeta) { throw new Error('missing global current task globalTaskMeta'); }
const taskID = globalTaskMeta.taskID;
_debugLog('[_asyncStartCall()] args', { args, componentIdx });
const { getCallbackFn, callbackIdx, getPostReturnFn, postReturnIdx } = args;
const preparedTaskMeta = getCurrentTask(componentIdx, taskID);
if (!preparedTaskMeta) { throw new Error('unexpectedly missing current task'); }
const preparedTask = preparedTaskMeta.task;
if (!preparedTask) { throw new Error('unexpectedly missing current task'); }
if (!preparedTask.subtaskMeta) { throw new Error('missing subtask meta from prepare'); }
const {
subtask,
returnMemoryIdx,
getReturnMemoryFn,
callerComponentIdx,
calleeComponentIdx,
getCalleeParamsFn,
isAsync,
stringEncoding,
} = preparedTask.subtaskMeta;
if (!subtask) { throw new Error("missing subtask from cstate during async start call"); }
if (calleeComponentIdx !== preparedTask.componentIdx()) {
throw new Error(`meta callee idx [${calleeComponentIdx}] != current task idx [${preparedTask.componentIdx()}] during async start call`);
}
if (calleeComponentIdx !== componentIdx) {
throw new Error("mismatched componentIdx for async start call (does not match prepare)");
}
const argArray = [...arguments];
if (resultCount < 0 || resultCount > 1) { throw new Error('invalid/unsupported result count'); }
const callbackFnName = 'callback_' + callbackIdx;
const callbackFn = getCallbackFn();
preparedTask.setCallbackFn(callbackFn, callbackFnName);
preparedTask.setPostReturnFn(getPostReturnFn());
if (resultCount < 0 || resultCount > 1) {
throw new Error(`unsupported result count [${ resultCount }]`);
}
const params = preparedTask.getCalleeParams();
if (paramCount !== params.length) {
throw new Error(`unexpected callee param count [${ params.length }], _asyncStartCall invocation expected [${ paramCount }]`);
}
const callerComponentState = getOrCreateAsyncState(subtask.componentIdx());
const calleeComponentState = getOrCreateAsyncState(preparedTask.componentIdx());
const calleeBackpressure = calleeComponentState.hasBackpressure();
// Set up a handler on subtask completion to lower results from the call into the caller's memory region.
//
// NOTE: during fused guest->guest calls this handler is triggered, but does not actually perform
// lowering manually, as fused modules provider helper functions that can
subtask.registerOnResolveHandler((res) => {
_debugLog('[_asyncStartCall()] handling subtask result', { res, subtaskID: subtask.id() });
let subtaskCallMeta = subtask.getCallMetadata();
// NOTE: in the case of guest -> guest async calls, there may be no memory/realloc present,
// as the host will intermediate the value storage/movement between calls.
//
// We can simply take the value and lower it as a parameter
if (subtaskCallMeta.memory || subtaskCallMeta.realloc) {
throw new Error("call metadata unexpectedly contains memory/realloc for guest->guest call");
}
const callerTask = subtask.getParentTask();
const calleeTask = preparedTask;
const callerMemoryIdx = callerTask.getReturnMemoryIdx();
const callerComponentIdx = callerTask.componentIdx();
// If a helper function was provided we are likely in a fused guest->guest call,
// and the result will be delivered (lift/lowered) via helper function
if (subtaskCallMeta && subtaskCallMeta.returnFn) {
_debugLog('[_asyncStartCall()] return function present while handling subtask result, returning early (skipping lower)');
// TODO: centralize calling of returnFn to *one place* (if possible)
if (subtaskCallMeta.returnFnCalled) { return; }
subtaskCallMeta.returnFn.apply(null, [subtaskCallMeta.resultPtr]);
return;
}
// If there is no where to lower the results, exit early
if (!subtaskCallMeta.resultPtr) {
_debugLog('[_asyncStartCall()] no result ptr during subtask result handling, returning early (skipping lower)');
return;
}
let callerMemory;
if (callerMemoryIdx !== null && callerMemoryIdx !== undefined) {
callerMemory = lookupMemoriesForComponent({ componentIdx: callerComponentIdx, memoryIdx: callerMemoryIdx });
} else {
const callerMemories = lookupMemoriesForComponent({ componentIdx: callerComponentIdx });
if (callerMemories.length !== 1) { throw new Error(`unsupported amount of caller memories`); }
callerMemory = callerMemories[0];
}
if (!callerMemory) {
_debugLog('[_asyncStartCall()] missing memory', { subtaskID: subtask.id(), res });
throw new Error(`missing memory for to guest->guest call result (subtask [${subtask.id()}])`);
}
const lowerFns = calleeTask.getReturnLowerFns();
if (!lowerFns || lowerFns.length === 0) {
_debugLog('[_asyncStartCall()] missing result lower metadata for guest->guest call', { subtaskID: subtask.id() });
throw new Error(`missing result lower metadata for guest->guest call (subtask [${subtask.id()}])`);
}
if (lowerFns.length !== 1) {
_debugLog('[_asyncStartCall()] only single result reportetd for guest->guest call', { subtaskID: subtask.id() });
throw new Error(`only single result supported for guest->guest calls (subtask [${subtask.id()}])`);
}
_debugLog('[_asyncStartCall()] lowering results', { subtaskID: subtask.id() });
lowerFns[0]({
realloc: undefined,
memory: callerMemory,
vals: [res],
storagePtr: subtaskCallMeta.resultPtr,
componentIdx: callerComponentIdx,
stringEncoding: subtaskCallMeta.stringEncoding,
});
});
subtask.setOnProgressFn(() => {
subtask.setPendingEvent(() => {
if (subtask.isResolved()) { subtask.deliverResolve(); }
const event = {
code: ASYNC_EVENT_CODE.SUBTASK,
payload0: subtask.waitableRep(),
payload1: subtask.getStateNumber(),
};
return event;
});
});
// Start the (event) driver loop that will resolve the task
queueMicrotask(async () => {
let startRes = subtask.onStart({ startFnParams: params });
startRes = Array.isArray(startRes) ? startRes : [startRes];
await calleeComponentState.suspendTask({
task: preparedTask,
readyFn: () => !calleeComponentState.isExclusivelyLocked(),
});
const started = await preparedTask.enter();
if (!started) {
_debugLog('[_asyncStartCall()] task failed early', {
taskID: preparedTask.id(),
subtaskID: subtask.id(),
});
throw new Error("task failed to start");
return;
}
let callbackResult;
try {
let jspiCallee = WebAssembly.promising(callee);
callbackResult = await _withGlobalCurrentTaskMetaAsync({
taskID: preparedTask.id(),
componentIdx: preparedTask.componentIdx(),
fn: () => {
return jspiCallee.apply(null, startRes);
}
});
} catch(err) {
_debugLog("[_asyncStartCall()] initial subtask callee run failed", err);
// NOTE: a good place to rejectt the parent task, if rejection API is enabled
// subtask.reject(err);
// subtask.getParentTask().reject(err);
subtask.getParentTask().setErrored(err);
return;
}
// If there was no callback function, we're dealing with a sync function
// that was lifted as async without one, there is only the callee.
if (!callbackFn) {
_debugLog("[_asyncStartCall()] no callback, resolving w/ callee result", {
taskID: preparedTask.id(),
componentIdx: preparedTask.componentIdx(),
preparedTask,
stateNumber: preparedTask.taskState(),
isResolved: preparedTask.isResolved(),
callbackFn,
});
preparedTask.resolve([callbackResult]);
return;
}
let fnName = callbackFn.fnName;
if (!fnName) {
fnName = [
'<task ',
subtask.parentTaskID(),
'/subtask ',
subtask.id(),
'/task ',
preparedTask.id(),
'>',
].join("");
}
try {
_debugLog("[_asyncStartCall()] starting driver loop", {
fnName,
componentIdx: preparedTask.componentIdx(),
subtaskID: subtask.id(),
childTaskID: subtask.childTaskID(),
parentTaskID: subtask.parentTaskID(),
});
await _driverLoop({
componentState: calleeComponentState,
task: preparedTask,
fnName,
isAsync: true,
callbackResult,
resolve,
reject
});
} catch (err) {
_debugLog("[AsyncStartCall] drive loop call failure", { err });
}
});
const subtaskState = subtask.getStateNumber();
if (subtaskState < 0 || subtaskState > 2**5) {
throw new Error('invalid subtask state, out of valid range');
}
_debugLog('[_asyncStartCall()] returning subtask rep & state', {
subtask: {
rep: subtask.waitableRep(),
state: subtaskState,
}
});
return Number(subtask.waitableRep()) << 4 | subtaskState;
}
function _syncStartCall(callbackIdx) {
_debugLog('[_syncStartCall()] args', { callbackIdx });
throw new Error('synchronous start call not implemented!');
}
class Waitable {
#componentIdx;
#pendingEventFn = null;
#promise;
#resolve;
#reject;
#waitableSet = null;
#idx = null; // to component-global waitables
target;
constructor(args) {
const { componentIdx, target } = args;
this.#componentIdx = componentIdx;
this.target = args.target;
this.#resetPromise();
}
componentIdx() { return this.#componentIdx; }
isInSet() { return this.#waitableSet !== null; }
idx() { return this.#idx; }
setIdx(idx) {
if (idx === 0) { throw new Error("waitable idx cannot be zero"); }
this.#idx = idx;
}
setTarget(tgt) { this.target = tgt; }
#resetPromise() {
const { promise, resolve, reject } = promiseWithResolvers()
this.#promise = promise;
this.#resolve = resolve;
this.#reject = reject;
}
resolve() { this.#resolve(); }
reject(err) { this.#reject(err); }
promise() { return this.#promise; }
hasPendingEvent() {
// _debugLog('[Waitable#hasPendingEvent()]', {
// componentIdx: this.#componentIdx,
// waitable: this,
// waitableSet: this.#waitableSet,
// hasPendingEvent: this.#pendingEventFn !== null,
// });
return this.#pendingEventFn !== null;
}
setPendingEvent(fn) {
_debugLog('[Waitable#setPendingEvent()] args', {
waitable: this,
inSet: this.#waitableSet,
});
this.#pendingEventFn = fn;
}
getPendingEvent() {
_debugLog('[Waitable#getPendingEvent()] args', {
waitable: this,
inSet: this.#waitableSet,
hasPendingEvent: this.#pendingEventFn !== null,
});
if (this.#pendingEventFn === null) { return null; }
const eventFn = this.#pendingEventFn;
this.#pendingEventFn = null;
const e = eventFn();
this.#resetPromise();
return e;
}
join(waitableSet) {
_debugLog('[Waitable#join()] args', {
waitable: this,
waitableSet: waitableSet,
});
if (this.#waitableSet) { this.#waitableSet.removeWaitable(this); }
if (!waitableSet) {
this.#waitableSet = null;
return;
}
waitableSet.addWaitable(this);
this.#waitableSet = waitableSet;
}
drop() {
_debugLog('[Waitable#drop()] args', {
componentIdx: this.#componentIdx,
waitable: this,
});
if (this.hasPendingEvent()) {
throw new Error('waitables with pending events cannot be dropped');
}
this.join(null);
}
}
const ERR_CTX_TABLES = {};
let dv = new DataView(new ArrayBuffer());
const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer);
function toUint64(val) {
const converted = BigInt(val)
return BigInt.asUintN(64, converted);
}
function toUint32(val) {
return val >>> 0;
}
const utf16Decoder = new TextDecoder('utf-16');
const TEXT_DECODER_UTF8 = new TextDecoder();
const TEXT_ENCODER_UTF8 = new TextEncoder();
function _utf8AllocateAndEncode(s, realloc, memory) {
if (typeof s !== 'string') {
throw new TypeError('expected a string, received [' + typeof s + ']');
}
if (s.length === 0) { return { ptr: 1, len: 0 }; }
let buf = TEXT_ENCODER_UTF8.encode(s);
let ptr = realloc(0, 0, 1, buf.length);
new Uint8Array(memory.buffer).set(buf, ptr);
const res = { ptr, len: buf.length, codepoints: [...s].length };
return res;
}
const T_FLAG = 1 << 30;
function rscTableCreateOwn(table, rep) {
const free = table[0] & ~T_FLAG;
if (free === 0) {
table.push(0);
table.push(rep | T_FLAG);
return (table.length >> 1) - 1;
}
table[0] = table[free << 1];
table[free << 1] = 0;
table[(free << 1) + 1] = rep | T_FLAG;
return free;
}
function rscTableRemove(table, handle) {
const scope = table[handle << 1];
const val = table[(handle << 1) + 1];
const own = (val & T_FLAG) !== 0;
const rep = val & ~T_FLAG;
if (val === 0 || (scope & T_FLAG) !== 0) {
throw new TypeError("Invalid handle");
}
table[handle << 1] = table[0] | T_FLAG;
table[0] = handle | T_FLAG;
return { rep, scope, own };
}
let curResourceBorrows = [];
function getCurrentTask(componentIdx, taskID) {
let usedGlobal = false;
if (componentIdx === undefined || componentIdx === null) {
throw new Error('missing component idx'); // TODO(fix)
// componentIdx = ASYNC_CURRENT_COMPONENT_IDXS.at(-1);
// usedGlobal = true;
}
const taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
if (taskMetas === undefined || taskMetas.length === 0) { return undefined; }
if (taskID) {
return taskMetas.find(meta => meta.task.id() === taskID);
}
const taskMeta = taskMetas[taskMetas.length - 1];
if (!taskMeta || !taskMeta.task) { return undefined; }
return taskMeta;
}
function createNewCurrentTask(args) {
_debugLog('[createNewCurrentTask()] args', args);
const {
componentIdx,
isAsync,
isManualAsync,
entryFnName,
parentSubtaskID,
callbackFnName,
getCallbackFn,
getParamsFn,
stringEncoding,
errHandling,
getCalleeParamsFn,
resultPtr,
callingWasmExport,
} = args;
if (componentIdx === undefined || componentIdx === null) {
throw new Error('missing/invalid component instance index while starting task');
}
let taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
const callbackFn = getCallbackFn ? getCallbackFn() : null;
const newTask = new AsyncTask({
componentIdx,
isAsync,
isManualAsync,
entryFnName,
callbackFn,
callbackFnName,
stringEncoding,
getCalleeParamsFn,
resultPtr,
errHandling,
});
const newTaskID = newTask.id();
const newTaskMeta = { id: newTaskID, componentIdx, task: newTask };
// NOTE: do not track host tasks
ASYNC_CURRENT_TASK_IDS.push(newTaskID);
ASYNC_CURRENT_COMPONENT_IDXS.push(componentIdx);
if (!taskMetas) {
taskMetas = [newTaskMeta];
ASYNC_TASKS_BY_COMPONENT_IDX.set(componentIdx, [newTaskMeta]);
} else {
taskMetas.push(newTaskMeta);
}
return [newTask, newTaskID];
}
const ASYNC_TASKS_BY_COMPONENT_IDX = new Map();
class AsyncTask {
static _ID = 0n;
static State = {
INITIAL: 'initial',
CANCELLED: 'cancelled',
CANCEL_PENDING: 'cancel-pending',
CANCEL_DELIVERED: 'cancel-delivered',
RESOLVED: 'resolved',
}
static BlockResult = {
CANCELLED: 'block.cancelled',
NOT_CANCELLED: 'block.not-cancelled',
}
#id;
#componentIdx;
#state;
#isAsync;
#isManualAsync;
#entryFnName = null;
#onResolveHandlers = [];
#completionPromise = null;
#rejected = false;
#exitPromise = null;
#onExitHandlers = [];
#memoryIdx = null;
#memory = null;
#callbackFn = null;
#callbackFnName = null;
#postReturnFn = null;
#getCalleeParamsFn = null;
#stringEncoding = null;
#parentSubtask = null;
#needsExclusiveLock = false;
#errHandling;
#backpressurePromise;
#backpressureWaiters = 0n;
#returnLowerFns = null;
#subtasks = [];
#entered = false;
#exited = false;
#errored = null;
cancelled = false;
cancelRequested = false;
alwaysTaskReturn = false;
returnCalls = 0;
storage = [0, 0];
borrowedHandles = {};
tmpRetI64HighBits = 0|0;
constructor(opts) {
this.#id = ++AsyncTask._ID;
if (opts?.componentIdx === undefined) {
throw new TypeError('missing component id during task creation');
}
this.#componentIdx = opts.componentIdx;
this.#state = AsyncTask.State.INITIAL;
this.#isAsync = opts?.isAsync ?? false;
this.#isManualAsync = opts?.isManualAsync ?? false;
this.#entryFnName = opts.entryFnName;
const {
promise: completionPromise,
resolve: resolveCompletionPromise,
reject: rejectCompletionPromise,
} = promiseWithResolvers();
this.#completionPromise = completionPromise;
this.#onResolveHandlers.push((results) => {
if (this.#errored !== null) {
rejectCompletionPromise(this.#errored);
return;
} else if (this.#rejected) {
rejectCompletionPromise(results);
return;
}
resolveCompletionPromise(results);
});
const {
promise: exitPromise,
resolve: resolveExitPromise,
reject: rejectExitPromise,
} = promiseWithResolvers();
this.#exitPromise = exitPromise;
this.#onExitHandlers.push(() => {
resolveExitPromise();
});
if (opts.callbackFn) { this.#callbackFn = opts.callbackFn; }
if (opts.callbackFnName) { this.#callbackFnName = opts.callbackFnName; }
if (opts.getCalleeParamsFn) { this.#getCalleeParamsFn = opts.getCalleeParamsFn; }
if (opts.stringEncoding) { this.#stringEncoding = opts.stringEncoding; }
if (opts.parentSubtask) { this.#parentSubtask = opts.parentSubtask; }
this.#needsExclusiveLock = this.isSync() || !this.hasCallback();
if (opts.errHandling) { this.#errHandling = opts.errHandling; }
}
taskState() { return this.#state; }
id() { return this.#id; }
componentIdx() { return this.#componentIdx; }
entryFnName() { return this.#entryFnName; }
completionPromise() { return this.#completionPromise; }
exitPromise() { return this.#exitPromise; }
isAsync() { return this.#isAsync; }
isSync() { return !this.isAsync(); }
getErrHandling() { return this.#errHandling; }
hasCallback() { return this.#callbackFn !== null; }
getReturnMemoryIdx() { return this.#memoryIdx; }
setReturnMemoryIdx(idx) {
if (idx === null) { return; }
this.#memoryIdx = idx;
}
getReturnMemory() { return this.#memory; }
setReturnMemory(m) {
if (m === null) { return; }
this.#memory = m;
}
setReturnLowerFns(fns) { this.#returnLowerFns = fns; }
getReturnLowerFns() { return this.#returnLowerFns; }
setParentSubtask(subtask) {
if (!subtask || !(subtask instanceof AsyncSubtask)) { return }
if (this.#parentSubtask) { throw new Error('parent subtask can only be set once'); }
this.#parentSubtask = subtask;
}
getParentSubtask() { return this.#parentSubtask; }
// TODO(threads): this is very inefficient, we can pass along a root task,
// and ideally do not need this once thread support is in place
getRootTask() {
let currentSubtask = this.getParentSubtask();
let task = this;
while (currentSubtask) {
task = currentSubtask.getParentTask();
currentSubtask = task.getParentSubtask();
}
return task;
}
setPostReturnFn(f) {
if (!f) { return; }
if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); }
this.#postReturnFn = f;
}
se