UNPKG

@bytecodealliance/jco

Version:

JavaScript tooling for working with WebAssembly Components

1,581 lines (1,311 loc) 251 kB
"use jco";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; getEnvironment._isHostProvided = true; if (getEnvironment=== undefined) { const err = new Error("unexpectedly undefined local import 'getEnvironment', was 'getEnvironment' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { exit } = exit$1; exit._isHostProvided = true; if (exit=== undefined) { const err = new Error("unexpectedly undefined local import 'exit', was 'exit' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getStderr } = stderr; getStderr._isHostProvided = true; if (getStderr=== undefined) { const err = new Error("unexpectedly undefined local import 'getStderr', was 'getStderr' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getStdin } = stdin; getStdin._isHostProvided = true; if (getStdin=== undefined) { const err = new Error("unexpectedly undefined local import 'getStdin', was 'getStdin' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getStdout } = stdout; getStdout._isHostProvided = true; if (getStdout=== undefined) { const err = new Error("unexpectedly undefined local import 'getStdout', was 'getStdout' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { TerminalInput } = terminalInput; TerminalInput._isHostProvided = true; if (TerminalInput=== undefined) { const err = new Error("unexpectedly undefined local import 'TerminalInput', was 'TerminalInput' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { TerminalOutput } = terminalOutput; TerminalOutput._isHostProvided = true; if (TerminalOutput=== undefined) { const err = new Error("unexpectedly undefined local import 'TerminalOutput', was 'TerminalOutput' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getTerminalStderr } = terminalStderr; getTerminalStderr._isHostProvided = true; if (getTerminalStderr=== undefined) { const err = new Error("unexpectedly undefined local import 'getTerminalStderr', was 'getTerminalStderr' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getTerminalStdin } = terminalStdin; getTerminalStdin._isHostProvided = true; if (getTerminalStdin=== undefined) { const err = new Error("unexpectedly undefined local import 'getTerminalStdin', was 'getTerminalStdin' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getTerminalStdout } = terminalStdout; getTerminalStdout._isHostProvided = true; if (getTerminalStdout=== undefined) { const err = new Error("unexpectedly undefined local import 'getTerminalStdout', was 'getTerminalStdout' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { getDirectories } = preopens; getDirectories._isHostProvided = true; if (getDirectories=== undefined) { const err = new Error("unexpectedly undefined local import 'getDirectories', was 'getDirectories' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } const { Descriptor, DirectoryEntryStream, filesystemErrorCode } = types; Descriptor._isHostProvided = true; if (Descriptor=== undefined) { const err = new Error("unexpectedly undefined local import 'Descriptor', was 'Descriptor' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } DirectoryEntryStream._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; } filesystemErrorCode._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; } const { Error: Error$1 } = error; Error$1._isHostProvided = true; 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; } const { InputStream, OutputStream } = streams; InputStream._isHostProvided = true; if (InputStream=== undefined) { const err = new Error("unexpectedly undefined local import 'InputStream', was 'InputStream' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } OutputStream._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; } const { getRandomBytes } = random; getRandomBytes._isHostProvided = true; if (getRandomBytes=== undefined) { const err = new Error("unexpectedly undefined local import 'getRandomBytes', was 'getRandomBytes' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } let dv = new DataView(new ArrayBuffer()); const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer); const toUint64 = val => BigInt.asUintN(64, BigInt(val)); function toUint32(val) { return val >>> 0; } 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); return { ptr, len: buf.length, codepoints: [...s].length }; } 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) { if (componentIdx === undefined || componentIdx === null) { throw new Error('missing/invalid component instance index [' + componentIdx + '] while getting current task'); } const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx); if (tasks === undefined) { return undefined; } if (tasks.length === 0) { return undefined; } return tasks[tasks.length - 1]; } function createNewCurrentTask(args) { _debugLog('[createNewCurrentTask()] args', args); const { componentIdx, isAsync, 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'); } const taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx); const callbackFn = getCallbackFn ? getCallbackFn() : null; const newTask = new AsyncTask({ componentIdx, isAsync, entryFnName, callbackFn, callbackFnName, stringEncoding, getCalleeParamsFn, resultPtr, errHandling, }); const newTaskID = newTask.id(); const newTaskMeta = { id: newTaskID, componentIdx, task: newTask }; ASYNC_CURRENT_TASK_IDS.push(newTaskID); ASYNC_CURRENT_COMPONENT_IDXS.push(componentIdx); if (!taskMetas) { ASYNC_TASKS_BY_COMPONENT_IDX.set(componentIdx, [newTaskMeta]); } else { taskMetas.push(newTaskMeta); } return [newTask, newTaskID]; } function endCurrentTask(componentIdx, taskID) { componentIdx ??= ASYNC_CURRENT_COMPONENT_IDXS.at(-1); taskID ??= ASYNC_CURRENT_TASK_IDS.at(-1); _debugLog('[endCurrentTask()] 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 task(s) for component instance while ending task'); } if (taskID) { 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 ASYNC_TASKS_BY_COMPONENT_IDX = new Map(); const ASYNC_CURRENT_TASK_IDS = []; const ASYNC_CURRENT_COMPONENT_IDXS = []; 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; #entryFnName = null; #subtasks = []; #onResolveHandlers = []; #completionPromise = null; #memoryIdx = null; #callbackFn = null; #callbackFnName = null; #postReturnFn = null; #getCalleeParamsFn = null; #stringEncoding = null; #parentSubtask = null; #needsExclusiveLock = false; #errHandling; #backpressurePromise; #backpressureWaiters = 0n; #returnLowerFns = null; cancelled = false; requested = false; alwaysTaskReturn = false; returnCalls = 0; storage = [0, 0]; borrowedHandles = {}; awaitableResume = null; awaitableCancel = null; 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.#entryFnName = opts.entryFnName; const { promise: completionPromise, resolve: resolveCompletionPromise, reject: rejectCompletionPromise, } = promiseWithResolvers(); this.#completionPromise = completionPromise; this.#onResolveHandlers.push((results) => { resolveCompletionPromise(results); }) 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; } isAsync() { return this.#isAsync; } entryFnName() { return this.#entryFnName; } completionPromise() { return this.#completionPromise; } isAsync() { return this.#isAsync; } isSync() { return !this.isAsync(); } getErrHandling() { return this.#errHandling; } hasCallback() { return this.#callbackFn !== null; } setReturnMemoryIdx(idx) { this.#memoryIdx = idx; } getReturnMemoryIdx() { return this.#memoryIdx; } 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; } 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.#callbackFnName) { return undefined; } return this.#callbackFnName; } runCallbackFn(...args) { if (!this.#callbackFn) { throw new Error('on callback function has been set for task'); } return this.#callbackFn.apply(null, args); } getCalleeParams() { if (!this.#getCalleeParamsFn) { throw new Error('missing/invalid getCalleeParamsFn'); } return this.#getCalleeParamsFn(); } mayEnter(task) { const cstate = getOrCreateAsyncState(this.#componentIdx); if (cstate.hasBackpressure()) { _debugLog('[AsyncTask#mayEnter()] disallowed due to backpressure', { taskID: this.#id }); return false; } if (!cstate.callingSyncImport()) { _debugLog('[AsyncTask#mayEnter()] disallowed due to sync import call', { taskID: this.#id }); return false; } const callingSyncExportWithSyncPending = cstate.callingSyncExport && !task.isAsync; if (!callingSyncExportWithSyncPending) { _debugLog('[AsyncTask#mayEnter()] disallowed due to sync export w/ sync pending', { taskID: this.#id }); return false; } return true; } async enter() { _debugLog('[AsyncTask#enter()] args', { taskID: this.#id }); const cstate = getOrCreateAsyncState(this.#componentIdx); if (this.isSync()) { return true; } if (cstate.hasBackpressure()) { cstate.addBackpressureWaiter(); const result = await this.waitUntil({ readyFn: () => !cstate.hasBackpressure(), cancellable: true, }); cstate.removeBackpressureWaiter(); if (result === AsyncTask.BlockResult.CANCELLED) { this.cancel(); return false; } } if (this.needsExclusiveLock()) { cstate.exclusiveLock(); } return true; } isRunning() { return this.#state !== AsyncTask.State.RESOLVED; } async waitUntil(opts) { const { readyFn, waitableSetRep, cancellable } = opts; _debugLog('[AsyncTask#waitUntil()] args', { taskID: this.#id, waitableSetRep, cancellable }); const state = getOrCreateAsyncState(this.#componentIdx); const wset = state.waitableSets.get(waitableSetRep); let event; wset.incrementNumWaiting(); const keepGoing = await this.suspendUntil({ readyFn: () => { const hasPendingEvent = wset.hasPendingEvent(); return readyFn() && hasPendingEvent; }, cancellable, }); if (keepGoing) { event = wset.getPendingEvent(); } else { event = { code: ASYNC_EVENT_CODE.TASK_CANCELLED, index: 0, result: 0, }; } wset.decrementNumWaiting(); return event; } async onBlock(awaitable) { _debugLog('[AsyncTask#onBlock()] args', { taskID: this.#id, awaitable }); if (!(awaitable instanceof Awaitable)) { throw new Error('invalid awaitable during onBlock'); } // Build a promise that this task can await on which resolves when it is awoken const { promise, resolve, reject } = promiseWithResolvers(); this.awaitableResume = () => { _debugLog('[AsyncTask] resuming after onBlock', { taskID: this.#id }); resolve(); }; this.awaitableCancel = (err) => { _debugLog('[AsyncTask] rejecting after onBlock', { taskID: this.#id, err }); reject(err); }; // Park this task/execution to be handled later const state = getOrCreateAsyncState(this.#componentIdx); state.parkTaskOnAwaitable({ awaitable, task: this }); try { await promise; return AsyncTask.BlockResult.NOT_CANCELLED; } catch (err) { // rejection means task cancellation return AsyncTask.BlockResult.CANCELLED; } } async asyncOnBlock(awaitable) { _debugLog('[AsyncTask#asyncOnBlock()] args', { taskID: this.#id, awaitable }); if (!(awaitable instanceof Awaitable)) { throw new Error('invalid awaitable during onBlock'); } // TODO: watch for waitable AND cancellation // TODO: if it WAS cancelled: // - return true // - only once per subtask // - do not wait on the scheduler // - control flow should go to the subtask (only once) // - Once subtask blocks/resolves, reqlinquishControl() will tehn resolve request_cancel_end (without scheduler lock release) // - control flow goes back to request_cancel // // Subtask cancellation should work similarly to an async import call -- runs sync up until // the subtask blocks or resolves // throw new Error('AsyncTask#asyncOnBlock() not yet implemented'); } async yieldUntil(opts) { const { readyFn, cancellable } = opts; _debugLog('[AsyncTask#yield()] args', { taskID: this.#id, cancellable }); const keepGoing = await this.suspendUntil({ readyFn, cancellable }); if (!keepGoing) { return { code: ASYNC_EVENT_CODE.TASK_CANCELLED, index: 0, result: 0, }; } return { code: ASYNC_EVENT_CODE.NONE, index: 0, result: 0, }; } async suspendUntil(opts) { const { cancellable, readyFn } = opts; _debugLog('[AsyncTask#suspendUntil()] args', { cancellable }); const pendingCancelled = this.deliverPendingCancel({ cancellable }); if (pendingCancelled) { return false; } const completed = await this.immediateSuspendUntil({ readyFn, cancellable }); return completed; } // TODO(threads): equivalent to thread.suspend_until() async immediateSuspendUntil(opts) { const { cancellable, readyFn } = opts; _debugLog('[AsyncTask#immediateSuspendUntil()] args', { cancellable, readyFn }); const ready = readyFn(); if (ready && !ASYNC_DETERMINISM && _coinFlip()) { return true; } const cstate = getOrCreateAsyncState(this.#componentIdx); cstate.addPendingTask(this); const keepGoing = await this.immediateSuspend({ cancellable, readyFn }); return keepGoing; } async immediateSuspend(opts) { // NOTE: equivalent to thread.suspend() // TODO(threads): store readyFn on the thread const { cancellable, readyFn } = opts; _debugLog('[AsyncTask#immediateSuspend()] args', { cancellable, readyFn }); const pendingCancelled = this.deliverPendingCancel({ cancellable }); if (pendingCancelled) { return false; } const cstate = getOrCreateAsyncState(this.#componentIdx); setTimeout(() => cstate.tick(), 0); const taskWait = await cstate.suspendTask({ task: this, readyFn }); const keepGoing = await taskWait; return keepGoing; } deliverPendingCancel(opts) { const { cancellable } = opts; _debugLog('[AsyncTask#deliverPendingCancel()] args', { cancellable }); if (cancellable && this.#state === AsyncTask.State.PENDING_CANCEL) { this.#state = Task.State.CANCEL_DELIVERED; return true; } return false; } isCancelled() { return this.cancelled } cancel() { _debugLog('[AsyncTask#cancel()] args', { }); if (!this.taskState() !== AsyncTask.State.CANCEL_DELIVERED) { throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] invalid task state for cancellation`); } if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); } this.cancelled = true; this.onResolve(new Error('cancelled')); this.#state = AsyncTask.State.RESOLVED; } onResolve(taskValue) { for (const f of this.#onResolveHandlers) { try { f(taskValue); } catch (err) { console.error("error during task resolve handler", err); throw err; } } if (this.#postReturnFn) { _debugLog('[AsyncTask#onResolve()] running post return ', { componentIdx: this.#componentIdx, taskID: this.#id, }); this.#postReturnFn(); } } registerOnResolveHandler(f) { this.#onResolveHandlers.push(f); } resolve(results) { _debugLog('[AsyncTask#resolve()] args', { results, componentIdx: this.#componentIdx, taskID: this.#id, }); if (this.#state === AsyncTask.State.RESOLVED) { throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] is already resolved (did you forget to wait for an import?)`); } if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); } switch (results.length) { case 0: this.onResolve(undefined); break; case 1: this.onResolve(results[0]); break; default: throw new Error('unexpected number of results'); } this.#state = AsyncTask.State.RESOLVED; } exit() { _debugLog('[AsyncTask#exit()] args', { }); // TODO: ensure there is only one task at a time (scheduler.lock() functionality) if (this.#state !== AsyncTask.State.RESOLVED) { // TODO(fix): only fused, manually specified post returns seem to break this invariant, // as the TaskReturn trampoline is not activated it seems. // // see: test/p3/ported/wasmtime/component-async/post-return.js // // We *should* be able to upgrade this to be more strict and throw at some point, // which may involve rewriting the upstream test to surface task return manually somehow. // //throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] exited without resolution`); _debugLog('[AsyncTask#exit()] task exited without resolution', { componentIdx: this.#componentIdx, taskID: this.#id, subtask: this.getParentSubtask(), subtaskID: this.getParentSubtask()?.id(), }); this.#state = AsyncTask.State.RESOLVED; } if (this.borrowedHandles > 0) { throw new Error('task [${this.#id}] exited without clearing borrowed handles'); } const state = getOrCreateAsyncState(this.#componentIdx); if (!state) { throw new Error('missing async state for component [' + this.#componentIdx + ']'); } if (!this.#isAsync && !state.inSyncExportCall) { throw new Error('sync task must be run from components known to be in a sync export call'); } state.inSyncExportCall = false; if (this.needsExclusiveLock() && !state.isExclusivelyLocked()) { throw new Error('task [' + this.#id + '] exit: component [' + this.#componentIdx + '] should have been exclusively locked'); } state.exclusiveRelease(); } needsExclusiveLock() { return this.#needsExclusiveLock; } createSubtask(args) { _debugLog('[AsyncTask#createSubtask()] args', args); const { componentIdx, childTask, callMetadata } = args; const newSubtask = new AsyncSubtask({ componentIdx, childTask, parentTask: this, callMetadata, }); this.#subtasks.push(newSubtask); return newSubtask; } getLatestSubtask() { return this.#subtasks.at(-1); } currentSubtask() { _debugLog('[AsyncTask#currentSubtask()]'); if (this.#subtasks.length === 0) { return undefined; } return this.#subtasks.at(-1); } endCurrentSubtask() { _debugLog('[AsyncTask#endCurrentSubtask()]'); if (this.#subtasks.length === 0) { throw new Error('cannot end current subtask: no current subtask'); } const subtask = this.#subtasks.pop(); subtask.drop(); return subtask; } } function unpackCallbackResult(result) { _debugLog('[unpackCallbackResult()] args', { 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]; } function _lowerImport(args, exportFn) { const params = [...arguments].slice(2); _debugLog('[_lowerImport()] args', { args, params, exportFn }); const { functionIdx, componentIdx, isAsync, paramLiftFns, resultLowerFns, metadata, memoryIdx, getMemoryFn, getReallocFn, } = args; const parentTaskMeta = getCurrentTask(componentIdx); const parentTask = parentTaskMeta?.task; if (!parentTask) { throw new Error('missing parent task during lower of import'); } const cstate = getOrCreateAsyncState(componentIdx); const subtask = parentTask.createSubtask({ componentIdx, parentTask, callMetadata: { memoryIdx, memory: getMemoryFn(), realloc: getReallocFn(), resultPtr: params[0], } }); parentTask.setReturnMemoryIdx(memoryIdx); const rep = cstate.subtasks.insert(subtask); subtask.setRep(rep); subtask.setOnProgressFn(() => { subtask.setPendingEventFn(() => { if (subtask.resolved()) { subtask.deliverResolve(); } return { code: ASYNC_EVENT_CODE.SUBTASK, index: rep, result: subtask.getStateNumber(), } }); }); // Set up a handler on subtask completion to lower results from the call into the caller's memory region. subtask.registerOnResolveHandler((res) => { _debugLog('[_lowerImport()] handling subtask result', { res, subtaskID: subtask.id() }); const { memory, resultPtr, realloc } = subtask.getCallMetadata(); resultLowerFns[0]({ componentIdx, memory, realloc, vals: [res], storagePtr: resultPtr }); }); const subtaskState = subtask.getStateNumber(); if (subtaskState < 0 || subtaskState > 2**5) { throw new Error('invalid subtask state, out of valid range'); } // NOTE: we must wait a bit before calling the export function, // to ensure the subtask state is not modified before the lower call return // // TODO: we should trigger via subtask state changing, rather than a static wait? setTimeout(async () => { try { _debugLog('[_lowerImport()] calling lowered import', { exportFn, params }); exportFn.apply(null, params); const task = subtask.getChildTask(); task.registerOnResolveHandler((res) => { _debugLog('[_lowerImport()] cascading subtask completion', { childTaskID: task.id(), subtaskID: subtask.id(), parentTaskID: parentTask.id(), }); subtask.onResolve(res); cstate.tick(); }); } catch (err) { console.error("post-lower import fn error:", err); throw err; } }, 100); return Number(subtask.waitableRep()) << 4 | subtaskState; } function _liftFlatU8(ctx) { _debugLog('[_liftFlatU8()] args', { ctx }); let val; if (ctx.useDirectParams) { if (ctx.params.length === 0) { throw new Error('expected at least a single i32 argument'); } val = ctx.params[0]; ctx.params = ctx.params.slice(1); } else { if (ctx.storageLen < ctx.storagePtr + 1) { throw new Error('not enough storage remaining for lift'); } val = new DataView(ctx.memory.buffer).getUint8(ctx.storagePtr); ctx.storagePtr += 1; ctx.storageLen -= 1; } return [val, ctx]; } function _liftFlatU16(ctx) { _debugLog('[_liftFlatU16()] args', { ctx }); let val; if (ctx.useDirectParams) { if (params.length === 0) { throw new Error('expected at least a single i32 argument'); } val = ctx.params[0]; ctx.params = ctx.params.slice(1); } else { if (ctx.storageLen < ctx.storagePtr + 2) { throw new Error('not enough storage remaining for lift'); } val = new DataView(ctx.memory.buffer).getUint16(ctx.storagePtr); ctx.storagePtr += 2; ctx.storageLen -= 2; } return [val, ctx]; } function _liftFlatU32(ctx) { _debugLog('[_liftFlatU32()] args', { ctx }); let val; if (ctx.useDirectParams) { if (ctx.params.length === 0) { throw new Error('expected at least a single i34 argument'); } val = ctx.params[0]; ctx.params = ctx.params.slice(1); } else { if (ctx.storageLen < ctx.storagePtr + 4) { throw new Error('not enough storage remaining for lift'); } val = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr); ctx.storagePtr += 4; ctx.storageLen -= 4; } return [val, ctx]; } function _liftFlatU64(ctx) { _debugLog('[_liftFlatU64()] args', { ctx }); let val; if (ctx.useDirectParams) { if (ctx.params.length === 0) { throw new Error('expected at least one single i64 argument'); } if (typeof ctx.params[0] !== 'bigint') { throw new Error('expected bigint'); } val = ctx.params[0]; ctx.params = ctx.params.slice(1); } else { if (ctx.storageLen < ctx.storagePtr + 8) { throw new Error('not enough storage remaining for lift'); } val = new DataView(ctx.memory.buffer).getUint64(ctx.storagePtr); ctx.storagePtr += 8; ctx.storageLen -= 8; } return [val, ctx]; } function _liftFlatStringUTF8(ctx) { _debugLog('[_liftFlatStringUTF8()] args', { ctx }); let val; if (ctx.useDirectParams) { if (ctx.params.length < 2) { throw new Error('expected at least two u32 arguments'); } const offset = ctx.params[0]; if (!Number.isSafeInteger(offset)) { throw new Error('invalid offset'); } const len = ctx.params[1]; if (!Number.isSafeInteger(len)) { throw new Error('invalid len'); } val = TEXT_DECODER_UTF8.decode(new DataView(ctx.memory.buffer, offset, len)); ctx.params = ctx.params.slice(2); } else { const start = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr, params[0], true); const codeUnits = new DataView(memory.buffer).getUint32(ctx.storagePtr, params[0] + 4, true); val = TEXT_DECODER_UTF8.decode(new Uint8Array(ctx.memory.buffer, start, codeUnits)); ctx.storagePtr += codeUnits; ctx.storageLen -= codeUnits; } return [val, ctx]; } function _liftFlatVariant(casesAndLiftFns) { return function _liftFlatVariantInner(ctx) { _debugLog('[_liftFlatVariant()] args', { ctx }); const origUseParams = ctx.useDirectParams; let caseIdx; if (casesAndLiftFns.length < 256) { let discriminantByteLen = 1; const [idx, newCtx] = _liftFlatU8(ctx); caseIdx = idx; ctx = newCtx; } else if (casesAndLiftFns.length > 256 && discriminantByteLen < 65536) { discriminantByteLen = 2; const [idx, newCtx] = _liftFlatU16(ctx); caseIdx = idx; ctx = newCtx; } else if (casesAndLiftFns.length > 65536 && discriminantByteLen < 4_294_967_296) { discriminantByteLen = 4; const [idx, newCtx] = _liftFlatU32(ctx); caseIdx = idx; ctx = newCtx; } else { throw new Error('unsupported number of cases [' + casesAndLIftFns.legnth + ']'); } const [ tag, liftFn, size32, alignment32 ] = casesAndLiftFns[caseIdx]; let val; if (liftFn === null) { val = { tag }; return [val, ctx]; } const [newVal, newCtx] = liftFn(ctx); ctx = newCtx; val = { tag, val: newVal }; return [val, ctx]; } } function _liftFlatList(elemLiftFn, alignment32, knownLen) { function _liftFlatListInner(ctx) { _debugLog('[_liftFlatList()] args', { ctx }); let metaPtr; let dataPtr; let len; if (ctx.useDirectParams) { if (knownLen) { dataPtr = _liftFlatU32(ctx); } else { metaPtr = _liftFlatU32(ctx); } } else { if (knownLen) { dataPtr = _liftFlatU32(ctx); } else { metaPtr = _liftFlatU32(ctx); } } if (metaPtr) { if (dataPtr !== undefined) { throw new Error('both meta and data pointers should not be set yet'); } if (ctx.useDirectParams) { ctx.useDirectParams = false; ctx.storagePtr = metaPtr; ctx.storageLen = 8; dataPtr = _liftFlatU32(ctx); len = _liftFlatU32(ctx); ctx.useDirectParams = true; ctx.storagePtr = null; ctx.storageLen = null; } else { dataPtr = _liftFlatU32(ctx); len = _liftFlatU32(ctx); } } const val = []; for (var i = 0; i < len; i++) { ctx.storagePtr = Math.ceil(ctx.storagePtr / alignment32) * alignment32; const [res, nextCtx] = elemLiftFn(ctx); val.push(res); ctx = nextCtx; } return [val, ctx]; } } function _liftFlatFlags(cases) { return function _liftFlatFlagsInner(ctx) { _debugLog('[_liftFlatFlags()] args', { ctx }); throw new Error('flat lift for flags not yet implemented!'); } } function _liftFlatResult(casesAndLiftFns) { return function _liftFlatResultInner(ctx) { _debugLog('[_liftFlatResult()] args', { ctx }); return _liftFlatVariant(casesAndLiftFns)(ctx); } } function _liftFlatOwn(componentTableIdx, size, memory, vals, storagePtr, storageLen) { _debugLog('[_liftFlatOwn()] args', { size, memory, vals, storagePtr, storageLen }); throw new Error('flat lift for owned resources not yet implemented!'); } function _lowerFlatU8(ctx) { _debugLog('[_lowerFlatU8()] args', ctx); const { memory, realloc, vals, storagePtr, storageLen } = ctx; if (vals.length !== 1) { throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); } if (vals[0] > 255 || vals[0] < 0) { throw new Error('invalid value for core value representing u8'); } if (!memory) { throw new Error("missing memory for lower"); } new DataView(memory.buffer).setUint32(storagePtr, vals[0], true); return 1; } function _lowerFlatU16(memory, vals, storagePtr, storageLen) { _debugLog('[_lowerFlatU16()] args', { memory, vals, storagePtr, storageLen }); if (vals.length !== 1) { throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); } if (vals[0] > 65_535 || vals[0] < 0) { throw new Error('invalid value for core value representing u16'); } new DataView(memory.buffer).setUint16(storagePtr, vals[0], true); return 2; } function _lowerFlatU32(ctx) { _debugLog('[_lowerFlatU32()] args', ctx); const { memory, realloc, vals, storagePtr, storageLen } = ctx; if (vals.length !== 1) { throw new Error('expected single value to lower, got (' + vals.length + ')'); } if (vals[0] > 4_294_967_295 || vals[0] < 0) { throw new Error('invalid value for core value representing u32'); } // TODO(fix): fix misaligned writes properly const rem = ctx.storagePtr % 4; if (rem !== 0) { ctx.storagePtr += (4 - rem); } new DataView(memory.buffer).setUint32(storagePtr, vals[0], true); return 4; } function _lowerFlatU64(memory, vals, storagePtr, storageLen) { _debugLog('[_lowerFlatU64()] args', { memory, vals, storagePtr, storageLen }); if (vals.length !== 1) { throw new Error('unexpected number of core vals'); } if (vals[0] > 18_446_744_073_709_551_615n || vals[0] < 0n) { throw new Error('invalid value for core value representing u64'); } new DataView(memory.buffer).setBigUint64(storagePtr, vals[0], true); return 8; } function _lowerFlatRecord(fieldMetas) { return (size, memory, vals, storagePtr, storageLen) => { const params = [...arguments].slice(5); _debugLog('[_lowerFlatRecord()] args', { size, memory, vals, storagePtr, storageLen, params, fieldMetas }); const [start] = vals; if (storageLen !== undefined && size !== undefined && size > storageLen) { throw new Error('not enough storage remaining for record flat lower'); } const data = new Uint8Array(memory.buffer, start, size); new Uint8Array(memory.buffer, storagePtr, size).set(data); return data.byteLength; } } function _lowerFlatVariant(metadata, extra) { const { discriminantSizeBytes, lowerMetas } = metadata; return function _lowerFlatVariantInner(ctx) { _debugLog('[_lowerFlatVariant()] args', ctx); const { memory, realloc, vals, storageLen, componentIdx } = ctx; let storagePtr = ctx.storagePtr; const { tag, val } = vals[0]; const variant = lowerMetas.find(vm => vm.tag === tag); if (!variant) { throw new Error(`missing/invalid variant, no tag matches [${tag}] (options were ${variantMetas.map(vm => vm.tag)})`); } if (!variant.discriminant) { throw new Error(`missing/invalid discriminant for variant [${variant}]`); } let bytesWritten; let discriminantLowerArgs = { memory, realloc, vals: [variant.discriminant], storagePtr, componentIdx } switch (discriminantSizeBytes) { case 1: bytesWritten = _lowerFlatU8(discriminantLowerArgs); break; case 2: bytesWritten = _lowerFlatU16(discriminantLowerArgs); break; case 4: bytesWritten = _lowerFlatU32(discriminantLowerArgs); break; default: throw new Error(`unexpected discriminant size bytes [${discriminantSizeBytes}]`); } if (bytesWritten !== discriminantSizeBytes) { throw new Error("unexpectedly wrote more bytes than discriminant"); } storagePtr += bytesWritten; bytesWritten += variant.lowerFn({ memory, realloc, vals: [val], storagePtr, storageLen, componentIdx }); return bytesWritten; } } function _lowerFlatList(size, memory, vals, storagePtr, storageLen) { _debugLog('[_lowerFlatList()] args', { size, memory, vals, storagePtr, storageLen }); let [start, len] = vals; const totalSizeBytes = len * size; if (storageLen !== undefined && totalSizeBytes > storageLen) { throw new Error('not enough storage remaining for list flat lower'); } const data = new Uint8Array(memory.buffer, start, totalSizeBytes); new Uint8Array(memory.buffer, storagePtr, totalSizeBytes).set(data); return data.byteLength; } function _lowerFlatEnum(size, memory, vals, storagePtr, storageLen) { _debugLog('[_lowerFlatEnum()] args', { size, memory, vals, storagePtr, storageLen }); let [start] = vals; if (storageLen !== undefined && size !== undefined && size > storageLen) { throw new Error('not enough storage remaining for enum flat lower'); } const data = new Uint8Array(memory.buffer, start, size); new Uint8Array(memory.buffer, storagePtr, size).set(data); return data.byteLength; } function _lowerFlatOption(size, memory, vals, storagePtr, storageLen) { _debugLog('[_lowerFlatOption()] args', { size, memory, vals, storagePtr, storageLen }); let [start] = vals; if (storageLen !== undefined && size !== undefined && size > storageLen) { throw new Error('not enough storage remaining for option flat lower'); } const data = new Uint8Array(memory.buffer, start, size); new Uint8Array(memory.buffer, storagePtr, size).set(data); return data.byteLength; } function _lowerFlatResult(lowerMetas) { const invalidTag = lowerMetas.find(t => t.tag !== 'ok' && t.tag !== 'error') if (invalidTag) { throw new Error(`invalid variant tag [${invalidTag}] found for result`); } return function _lowerFlatResultInner() { _debugLog('[_lowerFlatResult()] args', { lowerMetas }); let lowerFn = _lowerFlatVariant({ discriminantSizeBytes: 1, lowerMetas }, { forResult: true }); return lowerFn.apply(null, arguments); }; } function _lowerFlatOwn(size, memory, vals, storagePtr, storageLen) { _debugLog('[_lowerFlatOwn()] args', { size, memory, vals, storagePtr, storageLen }); throw new Error('flat lower for owned resources not yet implemented!'); } const ASYNC_STATE = new Map(); function getOrCreateAsyncState(componentIdx, init) { if (!ASYNC_STATE.has(componentIdx)) { const newState = new ComponentAsyncState({ componentIdx }); ASYNC_STATE.set(componentIdx, newState); } return ASYNC_STATE.get(componentIdx); } class ComponentAsyncState { static EVENT_HANDLER_EVENTS = [ 'backpressure-change' ]; #componentIdx; #callingAsyncImport = false; #syncImportWait = promiseWithResolvers(); #locked = false; #parkedTasks = new Map(); #suspendedTasksByTaskID = new Map(); #suspendedTaskIDs = []; #pendingTasks = []; #errored = null; #backpressure = 0; #backpressureWaiters = 0n; #handlerMap = new Map(); #nextHandlerID = 0n; mayLeave = true; waitableSets; waitables; subtasks; constructor(args) { this.#componentIdx = args.componentIdx; this.waitableSets = new RepTable({ target: `component [${this.#componentIdx}] waitable sets` }); this.waitables = new RepTable({ target: `component [${this.#componentIdx}] waitables` }); this.subtasks = new RepTable({ target: `component [${this.#componentIdx}] subtasks` }); }; componentIdx() { return this.#componentIdx; } errored() { return this.#errored !== null; } setErrored(err) { _debugLog('[ComponentAsyncState#setErrored()] component errored', { err, componentIdx: this.#componentIdx }); if (this.#errored) { return; } if (!err) { err = new Error('error elswehere (see other component instance error)') err.componentIdx = this.#componentIdx; } this.#errored = err; } callingSyncImport(val) { if (val === undefined) { return this.#callingAsyncImport; } if (typeof val !== 'boolean') { throw new TypeError('invalid setting for async import'); } const prev = this.#callingAsyncImport; this.#callingAsyncImport = val; if (prev === true && this.#callingAsyncImport === false) { this.#notifySyncImportEnd(); } } #notifySyncImportEnd() { const existing = this.#syncImportWait; this.#syncImportWait = promiseWithResolvers(); existing.resolve(); } async waitForSyncImportCallEnd() { await this.#syncImportWait.promise; } setBackpressure(v) { this.#backpressure = v; } getBackpressure(v) { return this.#backpressure; } incrementBackpressure() { const newValue = this.getBackpressure() + 1; if (newValue > 2**16) { throw new Error("invalid backpressure value, overflow"); } this.setBackpressure(newValue); } decrementBackpressure() { this.setBackpressure(Math.max(0, this.getBackpressure() - 1)); } hasBackpressure() { return this.#backpressure > 0; } waitForBackpressure() { let backpressureCleared = false; const cstate = this; cstate.addBackpressureWaiter(); const handlerID = this.registerHandler({ event: 'backpressure-change', fn: (bp) => { if (bp === 0) { cstate.removeHandler(handlerID); backpressureCleared = true; } } }); return new Promise((resolve) => { const interval = setInterval(() => { if (backpressureCleared) { return; } clearInterval(interval); cstate.removeBackpressureWaiter(); resolve(null); }, 0); }); } registerHandler(args) { const { event, fn } = args; if (!event) { throw new Error("missing handler event"); } if (!fn) { throw new Error("missing handler fn"); } if (!ComponentAsyncState.EVENT_HANDLER_EVENTS.includes(event)) { throw new Error(`unrecognized event handler [${event}]`); } const handlerID = this.#nextHandlerID++; let handlers = this.#handlerMap.get(event); if (!handlers) { handlers = []; this.#handlerMap.set(event, handlers) } handlers.push({ id: handlerID, fn, event }); return handlerID; } removeHandler(args) { const { event, handlerID } = args; const registeredHandlers = this.#handlerMap.get(event); if (!registeredHandlers) { return; } const found = registeredHandlers.find(h => h.id === handlerID); if (!found) { return; } this.#handlerMap.set(event, this.#handlerMap.get(event).filter(h => h.id !== handlerID)); } getBackpressureWaiters() { return this.#backpressureWaiters; } addBackpressureWaiter() { this.#backpressureWaiters++; } removeBackpressureWaiter() { this.#backpressureWaiters--; if (this.#backpressureWaiters < 0) { throw new Error("unexepctedly negative number of backpressure waiters"); } } parkTaskOnAwaitable(args) { if (!args.awaitable) { throw new TypeError('missing awaitable when trying to park'); } if (!args.task) { throw new TypeError('missing task when trying to park'); } const { awaitable, task } = args; let taskList = this.#parkedTasks.get(awaitable.id()); if (!taskList) { taskList = []; this.#parkedTasks.set(awaitable.id(), taskList); } taskList.push(task); this.wakeNextTaskForAwaitable(awaitable); } wakeNextTaskForAwaitable(awaitable) { if (!awaitable) { throw new TypeError('missing awaitable when waking next task'); } const awaitableID = awaitable.id(); const taskList = this.#parkedTasks.get(awaitableID); if (!taskList || taskList.length === 0) { _debugLog('[ComponentAsyncState] no tasks waiting for awaitable', { awaitableID: awaitable.id() }); return; } let task = taskList.shift(); // todo(perf) if (!task) { throw new Error('no task in parked list despite previous check'); } if (!task.awaitableResume) { throw new Error('task ready due to awaitable is missing resume', { taskID: task.id(), awaitableID }); } task.awaitableResume(); } // TODO: we might want to check for pre-locked status here exclusiveLock() { this.#locked = true; } exclusiveRelease() { _debugLog('[ComponentAsyncState#exclusiveRelease()] releasing', { locked: this.#locked, componentIdx: this.#componentIdx, }); this.#locked = false } isExclusivelyLocked() { return this.#locked === true; } #getSuspendedTaskMeta(taskID) { return this.#suspendedTasksByTaskID.get(taskID); } #removeSuspendedTaskMeta(taskID) { _debugLog('[ComponentAsyncState#removeSuspendedTaskMeta()] removing suspended task', { taskID }); const idx = this.#suspendedTaskIDs.findIndex(t => t === taskID); const meta = this.#suspendedTasksByTaskID.get(taskID); this.#suspendedTaskIDs[idx] = null; this.#suspendedTasksByTaskID.delete(taskID); return meta; } #addSuspendedTaskMeta(meta) { if (!meta) { throw new Error('missing task meta'); } const taskID = meta.taskID; this.#suspendedTasksByTaskID.set(taskID, meta); this.#suspendedTaskIDs.push(taskID); if (this.#suspendedTasksByTaskID.size < this.#suspendedTaskIDs.length - 10) { this.#suspendedTaskIDs = this.#suspendedTaskIDs.filter(t => t !== null); } } suspendTask(args) { // TODO(threads): readyFn is normally on the thread const { task, readyFn } = args; const taskID = task.id(); _debugLog('[ComponentAsyncState#suspendTask()]', { taskID }); if (this.#getSuspendedTaskMeta(taskID)) { throw new Error('task [' + taskID + '] already suspended'); } const { promise, resolve } = Promise.withResolvers(); this.#addSuspendedTaskMeta({ task, taskID, readyFn, resume: () => { _debugLog('[ComponentAsyncState#suspendTask()] resuming suspended task', { taskID }); // TODO(threads): it's thread cancellation we should be checking for below, not task resolve(!task.isCancelled()); }, }); return promise; } resumeTaskByID(taskID) { const meta = this.#removeSuspendedTaskMeta(taskID); if (!meta) { return; } if (meta.taskID !== taskID) { throw new Error('task ID does not match'); } meta.resume(); } tick() { _debugLog('[ComponentAsyncState#tick()]', { suspendedTaskIDs: this.#suspendedTaskIDs }); let resumedTask = false; for (const taskID of this.#suspendedTaskIDs.filter(t => t !== null)) { const meta = this.#suspendedTasksByTaskID.get(taskID); if (!meta || !meta.readyFn) { throw new Error('missing/invalid task despite ID [' + taskID + '] being present'); } if (!meta.readyFn()) { continue; } resumedTask = true; this.resumeTaskByID(taskID); } return resumedTask; } addPendingTask(task) { this.#pendingTasks.push(task); } } function _prepareCall( memoryIdx, getMemoryFn, startFn, returnFn, callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, isCalleeAsyncInt, stringEncoding, resultCountOrAsync, ) { _debugLog('[_prepareCall()]', { callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, isCalleeAsyncInt, stringEncoding, resultCountOrAsync, }); const argArray = [...arguments]; // Since Rust will happily