diff --git a/runtime/src/cabi/flatten.ts b/runtime/src/cabi/flatten.ts index 2f11e00..ae8d6be 100644 --- a/runtime/src/cabi/flatten.ts +++ b/runtime/src/cabi/flatten.ts @@ -18,7 +18,9 @@ export const MAX_FLAT_PARAMS = 16; export const MAX_FLAT_ASYNC_PARAMS = 4; // Mutable to mirror run_tests.py toggling definitions.MAX_FLAT_RESULTS. export let MAX_FLAT_RESULTS = 1; -export function setMaxFlatResults(n: number): number { +// Test-only mirror of run_tests.py's constant toggling; not for production +// use (naming convention: see `schedulerSeedForTesting`). +export function setMaxFlatResultsForTesting(n: number): number { const prev = MAX_FLAT_RESULTS; MAX_FLAT_RESULTS = n; return prev; diff --git a/runtime/src/cabi/handles.ts b/runtime/src/cabi/handles.ts index b490d12..b7bbec0 100644 --- a/runtime/src/cabi/handles.ts +++ b/runtime/src/cabi/handles.ts @@ -191,9 +191,9 @@ function isThenable(v: unknown): v is PromiseLike { /** * Invoke a resource destructor through the reference's entry bracket. * - * definitions.py `canon_resource_drop` (line 2318) does not call `rt.dtor` + * definitions.py `canon_resource_drop` (line 2319) does not call `rt.dtor` * directly. It builds the dtor into a function instance and calls it through - * `Store.lift` / `Store.lower` (lines 2326-2333): + * `Store.lift` / `Store.lower` (lines 2330-2333): * * ```python * dtor = rt.dtor or (lambda rep: []) @@ -310,7 +310,7 @@ export function canonResourceDrop( trapIf(rh.numLends !== 0, "handle still lent out"); if (rh.own) { assert_(rh.borrowScope === null); - // definitions.py line 2325-2333: the dtor runs through the store's + // definitions.py line 2326-2333: the dtor runs through the store's // lift/lower bracket. SCOPE NOTE (#85): the call below is a JS frame // inside the drop trampoline, so a *guest*-initiated drop whose dtor // suspends traps under the JSPI frame rule. That is deterministic and diff --git a/runtime/src/cabi/strings.ts b/runtime/src/cabi/strings.ts index b9b50c6..852f6f5 100644 --- a/runtime/src/cabi/strings.ts +++ b/runtime/src/cabi/strings.ts @@ -232,16 +232,19 @@ export function storeString( ): void { const mem = requireMemory(cx.opts); const [begin, taggedCodeUnits] = storeStringIntoRange(cx, v); + // Write order matches the reference (store_string, definitions.py:1613-1616): + // begin pointer first, then tagged length. Unobservable here (no trap can + // intervene between the two writes), but kept in step for parity. storeInt( mem, - mem.ptrSize() === 4 ? Number(taggedCodeUnits) : taggedCodeUnits, - ptr + mem.ptrSize(), + mem.ptrSize() === 4 ? begin : BigInt(begin), + ptr, mem.ptrSize(), ); storeInt( mem, - mem.ptrSize() === 4 ? begin : BigInt(begin), - ptr, + mem.ptrSize() === 4 ? Number(taggedCodeUnits) : taggedCodeUnits, + ptr + mem.ptrSize(), mem.ptrSize(), ); } diff --git a/runtime/src/exec/boundary.ts b/runtime/src/exec/boundary.ts index faaa3a9..f5f21f9 100644 --- a/runtime/src/exec/boundary.ts +++ b/runtime/src/exec/boundary.ts @@ -1847,6 +1847,12 @@ const CALLBACK_CODE_MAX = 2; export function unpackCallbackResult( packed: number, ): [code: CallbackCode, waitableSetIndex: number] { + // Reference parity insurance only: callers already guarantee this range via + // core-result normalization before calling in. + assert_( + packed >= 0 && packed < 2 ** 32, + `unpack-callback-result: packed out of range: ${packed}`, + ); const code = packed & 0xf; trapIf(code > CALLBACK_CODE_MAX, `invalid callback code ${code}`); return [code as CallbackCode, packed >>> 4]; diff --git a/runtime/src/intrinsics/async_builtins.ts b/runtime/src/intrinsics/async_builtins.ts index cc8520c..b3b5a31 100644 --- a/runtime/src/intrinsics/async_builtins.ts +++ b/runtime/src/intrinsics/async_builtins.ts @@ -501,7 +501,8 @@ export function createSubtaskCancel( // reports the resolved state through the same tail as the // non-blocking path. Mirrors SITE 4 (stream_builtins.ts:305-323) // and `Waitable.waitForPendingEvent` (definitions.py:786-790, - // reached from canon_subtask_cancel :2491): `hasSyncWaiter` must + // reached from canon_subtask_cancel's `subtask.wait_for_pending_event()` + // call, :2484): `hasSyncWaiter` must // be set for the duration so a concurrent `waitable.join` on // this subtask traps (async_builtins.ts:362-365) instead of // racing the SUBTASK event away from this resume (#87). diff --git a/runtime/src/intrinsics/context.ts b/runtime/src/intrinsics/context.ts index 9173dd0..32e0996 100644 --- a/runtime/src/intrinsics/context.ts +++ b/runtime/src/intrinsics/context.ts @@ -19,7 +19,7 @@ import { UnsupportedFeatureError } from "./errors.ts"; /** * Number of `i32` context slots per thread. definitions.py `Thread.storage` - * is initialised `[0,0]` (line 346) and `canon_context_{get,set}` assert + * is initialised `[0,0]` (line 347) and `canon_context_{get,set}` assert * `i < len(thread.storage)` — so exactly two, matching the intrinsic names * `context-*-i32-0` and `context-*-i32-1`. */ diff --git a/runtime/src/intrinsics/fact_calls.ts b/runtime/src/intrinsics/fact_calls.ts index 446d5df..524807f 100644 --- a/runtime/src/intrinsics/fact_calls.ts +++ b/runtime/src/intrinsics/fact_calls.ts @@ -644,7 +644,13 @@ export function createSyncStartCall( mode: ctx.suspensionMode, canBlock: ctx.calleeCanBlock?.(callee) ?? false, onCallerResults: (r) => { - callerResults = r ?? []; + // sync-start-call's callee always uses the async ABI (comment above), + // but the *caller* side here is the sync `canon_lower` path: the + // reference's on_resolve(None) case is reached only when a + // cancellation was requested, and a sync-lowered subtask has no + // handle and hence no cancel channel — so `r` can never be null here. + assert_(r !== null, "sync-start-call: caller results missing"); + callerResults = r; }, lenderScope, }); diff --git a/runtime/src/intrinsics/mod.ts b/runtime/src/intrinsics/mod.ts index 501ec32..ef92640 100644 --- a/runtime/src/intrinsics/mod.ts +++ b/runtime/src/intrinsics/mod.ts @@ -203,7 +203,7 @@ export class SyncCallScope { this.lenders.push(h); } - /** definitions.py `Subtask.release_lenders`. */ + /** definitions.py `Subtask.deliver_resolve` (lines 902-906): releases lenders at delivery time. */ releaseLenders(): void { for (const h of this.lenders) h.numLends -= 1; this.lenders.length = 0; diff --git a/runtime/src/intrinsics/stream_builtins.ts b/runtime/src/intrinsics/stream_builtins.ts index 48684b7..58634da 100644 --- a/runtime/src/intrinsics/stream_builtins.ts +++ b/runtime/src/intrinsics/stream_builtins.ts @@ -171,6 +171,12 @@ function streamCopy(input: { const cx = new LiftLowerContext(cabiOptions(opts), inst, null); const buffer = new GuestBuffer(elem, cx, ptr, n); + // definitions.py `stream_copy`: `assert(not isinstance(stream_t, CharType))` + // — plan validation is expected to reject a char-typed stream before this + // point (streams of `char` are not a representable component type), so this + // documents that invariant rather than defending against a reachable case. + assert_(elem === null || elem.kind !== "char", "stream copy: char element"); + // definitions.py `stream_event`: the payload is computed at *delivery* time, // so `buffer.progress` reflects everything copied by the time the guest // looks — including copies that happened after the event was armed. @@ -187,6 +193,12 @@ function streamCopy(input: { buffer.progress <= BUFFER_MAX_LENGTH, "stream progress out of packing range", ); + // definitions.py `stream_copy`/`stream_event`: `assert(0 <= result < 2**4)`. + // `CopyResult` is a fixed 0..2 enum so this can't fire; kept for parity. + assert_( + result >= 0 && result < 2 ** 4, + "stream event: packed result out of 4-bit range", + ); return [eventCode, i, (result | (buffer.progress << 4)) >>> 0]; }; @@ -391,7 +403,7 @@ function takeCancelEvent( ); // UPSTREAM DIVERGENCE (definitions.py is wrong here; wasmtime is right). // - // `cancel_copy` (definitions.py line 2645) returns an already-armed pending + // `cancel_copy` (definitions.py line 2654) returns an already-armed pending // event verbatim, so cancelling a stream write that had been partially // satisfied yields COMPLETED with the copied count. wasmtime instead // *supersedes* an undelivered stream COMPLETED with CANCELLED, keeping the diff --git a/runtime/src/task/scheduler.ts b/runtime/src/task/scheduler.ts index 88c8811..61f5655 100644 --- a/runtime/src/task/scheduler.ts +++ b/runtime/src/task/scheduler.ts @@ -744,7 +744,6 @@ export interface SchedulableThread { */ export class Store { readonly waiting: SchedulableThread[] = []; - nestingDepth = 0; /** * Host-import promises this store is waiting on. Non-empty means progress diff --git a/runtime/src/task/streams.ts b/runtime/src/task/streams.ts index d1ff356..9688056 100644 --- a/runtime/src/task/streams.ts +++ b/runtime/src/task/streams.ts @@ -469,7 +469,8 @@ export class SharedFutureImpl implements SharedBase { read(inst: unknown, dstBuffer: GuestBuffer, onCopyDone: OnCopyDone): void { // #84 leg (c): the reader arrives AFTER the writable side was abandoned. - // definitions.py:1141 asserts `not self.dropped` here because the drop + // definitions.py:1154 (SharedFutureImpl.read) asserts `not self.dropped` + // here because the drop // trap keeps that unreachable; for our abandoned state the honest answer // is the same trap the parked reader gets, delivered synchronously. if (this.dropped && this.abandonReason !== null) { diff --git a/runtime/src/task/subtask.ts b/runtime/src/task/subtask.ts index 804fc5a..a62488b 100644 --- a/runtime/src/task/subtask.ts +++ b/runtime/src/task/subtask.ts @@ -144,10 +144,16 @@ export class Subtask extends Waitable { /** * definitions.py `canon_lower`'s `on_progress`/`subtask_event` closure - * (line 2296). The event payload is computed **at delivery time** and + * (lines 2297-2298). The event payload is computed **at delivery time** and * delivering it is what runs `deliver_resolve` — so the lent handles are * released exactly when the guest observes the resolution, not when it * happens. + * + * The `!this.resolveDelivered()` guard has no reference analogue: it exists + * so this can coexist with `unwindLenders()` (which may itself have already + * delivered the resolve on an abandoned path). The reference's + * `subtask_event` calls `deliver_resolve()` unconditionally and would + * assert on a double delivery. */ setSubtaskPendingEvent(subtaski: number): void { this.setPendingEvent(() => { @@ -159,7 +165,7 @@ export class Subtask extends Waitable { /** * Pack a `canon_lower` async return value: `state | (subtaski << 4)` - * (definitions.py line 2308, with the accompanying asserts on the ranges). + * (definitions.py line 2306, with the accompanying asserts on the ranges). */ export function packSubtaskResult( state: SubtaskState, diff --git a/runtime/src/task/thread.ts b/runtime/src/task/thread.ts index 96bbbdd..d254f5e 100644 --- a/runtime/src/task/thread.ts +++ b/runtime/src/task/thread.ts @@ -42,6 +42,10 @@ export class Thread implements SchedulableThread { * write *this*, not per-task state: two threads of the same task have * independent context. wit-bindgen 0.60 keeps its async task pointer in * slot 0. + * + * Slots are plain JS numbers: a `context.set` of an i64 value above + * 2^53-1 would lose precision. Moot while memory64/threads support is + * deferred (issue #12) — revisit this when that issue's closure lands. */ readonly storage: number[] = [0, 0];