diff --git a/.github/justfile b/.github/justfile index 0a4a4a6..12f79a8 100644 --- a/.github/justfile +++ b/.github/justfile @@ -56,6 +56,7 @@ _step-tolerated recipe: core: @just gha::_step version-guard-local @just gha::_step version-guard-pr + @just gha::_step fmt-check @just gha::_step build @just gha::_step test-rust @just gha::_step shim diff --git a/justfile b/justfile index 8be7fa8..9bc94bd 100644 --- a/justfile +++ b/justfile @@ -16,15 +16,21 @@ ci: (gha::core) (gha::browser) # Includes the consumer smokes CI cannot run (they need the polymorph # checkouts; docs/consumers.md). # The full pre-commit pass (AGENTS.md "Gates"): everything. -gates: version-guard-local build test-rust test-protocol test-runtime test-wasi test-sockets-node test-ct-runner test-bundle test-version-guard publish-check test-npm examples test-translate conformance sched-seeds shells browsers smoke-tls smoke-c0 +gates: version-guard-local fmt-check build test-rust test-protocol test-runtime test-wasi test-sockets-node test-ct-runner test-bundle test-version-guard publish-check test-npm examples test-translate conformance sched-seeds shells browsers smoke-tls smoke-c0 # Fast sanity: builds + native tests + type-checks, no suites. -check: build test-rust +check: fmt-check build test-rust cd protocol && deno task check cd runtime && deno task check cd wasi && deno task check cd ct-runner && deno task check +# The runtime package is formatter-clean (`deno fmt`, stock settings; the +# generated bindgen snapshots/envelopes and generator output are excluded in +# runtime/deno.json). Fix with `cd runtime && deno fmt`. +fmt-check: + cd runtime && deno fmt --check + # ----- builders --------------------------------------------------------------- build: diff --git a/runtime/README.md b/runtime/README.md index e91364d..0b8c6ac 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -1,16 +1,16 @@ # runtime — TS core runtime -Platform-neutral TypeScript runtime core (docs/architecture.md §4.3). Two layers exist -today: +Platform-neutral TypeScript runtime core (docs/architecture.md §4.3). Two layers +exist today: 1. the **canonical-ABI v1 value interpreter** (`src/cabi/`) with the - **definitions.py test ports** (`tests/`): docs/architecture.md §11 row 2, feeding §7 - (canonical-ABI decisions) and §8 (the descriptor-IR interpreter); -2. the **plan executor on the task-model skeleton** (`src/plan/`, - `src/task/`, `src/exec/`, `src/intrinsics/`, `src/shim/`): loads the - translator shim's plan v0 (contracts/plan-format.md), instantiates and - links the component, and routes every lifted-export call through - Task/Thread structures (degenerate sync path, docs/architecture.md §6). + **definitions.py test ports** (`tests/`): docs/architecture.md §11 row 2, + feeding §7 (canonical-ABI decisions) and §8 (the descriptor-IR interpreter); +2. the **plan executor on the task-model skeleton** (`src/plan/`, `src/task/`, + `src/exec/`, `src/intrinsics/`, `src/shim/`): loads the translator shim's + plan v0 (contracts/plan-format.md), instantiates and links the component, and + routes every lifted-export call through Task/Thread structures (degenerate + sync path, docs/architecture.md §6). ## Layout @@ -46,8 +46,9 @@ tests/integration/ full-pipeline e2e: shim wasm32 under Deno -> plan -> ``` Run tests: `deno task test` (from `runtime/`; it is `deno test ---allow-read=..` — the integration tests read build artifacts from the repo). -Type-check: `deno task check`. +--allow-read=..` +— the integration tests read build artifacts from the repo). Type-check: +`deno task check`. Regenerate fixtures: `deno task gen-fixtures` (needs `python3`; imports `third_party/component-model/design/mvp/canonical-abi/definitions.py` @@ -66,7 +67,7 @@ needed when the submodule's definitions.py changes. | NaN canonicalization (deterministic profile) | `test_nan32/64` | `nan_test.ts` | | char validation (surrogates, > 0x10FFFF trap) | `test_pairs(CharType...)` | `flat_test.ts` | | strings: full utf8/utf16/latin1+utf16 matrix, both address types, byte-exact incl. realloc traffic | `test_string` matrix | `string_test.ts` (fixture-driven: 168 lift + 150 lower byte-exact checks + 504 roundtrips) | -| USVString lone-surrogate replacement (docs/architecture.md §7) | n/a in Python (strings always well-formed) | `string_test.ts` (TS-authored) | +| USVString lone-surrogate replacement (docs/architecture.md §7) | n/a in Python (strings always well-formed) | `string_test.ts` (TS-authored) | | lists/records/variants/flags/map over heap memory, misalignment traps, i64 memories | `test_heap` | `heap_test.ts` (35 cases) | | lift/lower_flat_values spilling (17 params, retp out-param, alignment traps) | reached via `test_roundtrips`/`canon_lower` upstream | `values_test.ts` (TS-authored) | | handle Table (slab, free list LIFO, traps), resource.new/rep/drop, own transfer, borrow lend counting | `test_handles` (pure parts) | `handles_test.ts` | @@ -80,9 +81,9 @@ through `Store`/`Task`/`Thread`: cross-component realloc, the full cancellation, `thread.*`/`context.*` built-ins, and the error-context/stream/future _value types_ (their layout/flatten is implemented; their lift/lower throws `NotImplemented`). Each has an ignored placeholder in -`tests/deferred_test.ts` with the reason. Per docs/architecture.md §6 the scheduler is the -core deliverable and gets built as the runtime's spine — these ports become its -acceptance tests, not the other way round. +`tests/deferred_test.ts` with the reason. Per docs/architecture.md §6 the +scheduler is the core deliverable and gets built as the runtime's spine — these +ports become its acceptance tests, not the other way round. ## Decisions forced by JS semantics (not already settled by docs/architecture.md §7) @@ -118,23 +119,24 @@ distinctions); flagged for plan review: 7. **Variant/record/flags value shapes** mirror definitions.py's semantics (variants as `{kind, value}` objects — contracts/descriptor-ir.md §"Host value shapes" — despecialized tuple records, label→bool maps); `list` is - `Uint8Array` per docs/architecture.md §7. Final host-facing representations for bindgen - remain open (below). + `Uint8Array` per docs/architecture.md §7. Final host-facing representations + for bindgen remain open (below). -Also plan-relevant: docs/architecture.md §7 defers **latin1+utf16** "until a test forces it" -— the ported definitions.py string matrix forces it, so the v1 interpreter now -implements it fully (both directions). If we prefer to keep the runtime surface -minimal, the store path can be re-deferred by ignoring the fixture subset again. +Also plan-relevant: docs/architecture.md §7 defers **latin1+utf16** "until a +test forces it" — the ported definitions.py string matrix forces it, so the v1 +interpreter now implements it fully (both directions). If we prefer to keep the +runtime surface minimal, the store path can be re-deferred by ignoring the +fixture subset again. ## Open questions (types.ts is provisional) - Wire format of the descriptor IR: `types.ts` is the in-memory sketch; the - translator shim (docs/architecture.md §4.2) will define the serialized form and likely - intern labels/types by index. -- Host-facing value representations for bindings (docs/architecture.md §9): tuples as arrays? - variants as `{ tag, val }`? `option` as `T | undefined` with a - `Some`/`None` escape hatch for nesting? The interpreter's despecialized shapes - are faithful to the reference but not ergonomic. + translator shim (docs/architecture.md §4.2) will define the serialized form + and likely intern labels/types by index. +- Host-facing value representations for bindings (docs/architecture.md §9): + tuples as arrays? variants as `{ tag, val }`? `option` as `T | undefined` + with a `Some`/`None` escape hatch for nesting? The interpreter's despecialized + shapes are faithful to the reference but not ergonomic. - Whether host-provided `char`/integer values get validated (trap) or asserted at the boundary — definitions.py asserts (guest-side values are trusted by construction); a host API needs a decision. @@ -144,10 +146,10 @@ minimal, the store path can be re-deferred by ignoring the fixture subset again. ## Upstream discrepancies found (definitions.py vs CanonicalABI.md) - `canon_backpressure_set` existed in definitions.py but CanonicalABI.md - documents only `backpressure.inc`/`backpressure.dec`; the repo's own - `diff.py` flagged it. Vestigial back-compat shim — resolved upstream - independently (CM PR #690 removed it; findings tracker CM-2). The stale - `$async?` immediate on `resource.drop` (CanonicalABI.md ~line 4013, - noted in docs/architecture.md §7) is still open. + documents only `backpressure.inc`/`backpressure.dec`; the repo's own `diff.py` + flagged it. Vestigial back-compat shim — resolved upstream independently (CM + PR #690 removed it; findings tracker CM-2). The stale `$async?` immediate on + `resource.drop` (CanonicalABI.md ~line 4013, noted in docs/architecture.md §7) + is still open. - `python3 run_tests.py` passes upstream unmodified (Python 3.13.7), so no reference-snapshot copy was needed. diff --git a/runtime/deno.json b/runtime/deno.json index 74fb6b6..f0ec766 100644 --- a/runtime/deno.json +++ b/runtime/deno.json @@ -13,6 +13,13 @@ "check": "deno check src tests", "gen-fixtures": "python3 tests/fixtures/generate.py" }, + "fmt": { + "exclude": [ + "tests/bindgen/generated/", + "tests/bindgen/fixtures/", + "tests/fixtures/" + ] + }, "publish": { "exclude": [ "tests/" diff --git a/runtime/src/cabi/async_values.ts b/runtime/src/cabi/async_values.ts index e685d68..84313df 100644 --- a/runtime/src/cabi/async_values.ts +++ b/runtime/src/cabi/async_values.ts @@ -32,9 +32,9 @@ import { ErrorContext, ReadableFutureEnd, ReadableStreamEnd, + sameElemType, type SharedBase, SharedFutureImpl, - sameElemType, SharedStreamImpl, } from "../task/streams.ts"; @@ -88,8 +88,15 @@ function liftAsyncValue( assert_(inst !== null, `${what} lift requires a component instance`); const e = inst!.handles.remove(i); trapIf(!(e instanceof EndT), `${what} lift: handle is not a ${what} end`); - const end = e as { shared: SharedBase; state: CopyState; inWaitableSet(): boolean }; - trapIf(!sameElemType(end.shared.t, elem), `${what} lift: element type mismatch`); + const end = e as { + shared: SharedBase; + state: CopyState; + inWaitableSet(): boolean; + }; + trapIf( + !sameElemType(end.shared.t, elem), + `${what} lift: element type mismatch`, + ); trapIf( end.state === CopyState.DONE, what === "future" diff --git a/runtime/src/cabi/bulk_lists.ts b/runtime/src/cabi/bulk_lists.ts index 889bcb7..07e8905 100644 --- a/runtime/src/cabi/bulk_lists.ts +++ b/runtime/src/cabi/bulk_lists.ts @@ -88,9 +88,11 @@ const FLOAT_CTORS: Record< f64: Float64Array, }; - function viewOf< - C extends { new (b: ArrayBufferLike, o: number, n: number): InstanceType; readonly BYTES_PER_ELEMENT: number }, + C extends { + new (b: ArrayBufferLike, o: number, n: number): InstanceType; + readonly BYTES_PER_ELEMENT: number; + }, >(ctor: C, mem: MemInst, ptr: number, length: number): InstanceType | null { if (!PLATFORM_LITTLE_ENDIAN) return null; const byteOffset = mem.bytes.byteOffset + ptr; diff --git a/runtime/src/cabi/handles.ts b/runtime/src/cabi/handles.ts index 8e909d7..ba6d677 100644 --- a/runtime/src/cabi/handles.ts +++ b/runtime/src/cabi/handles.ts @@ -14,10 +14,10 @@ import { assert_, Trap, trap, trapIf } from "./trap.ts"; import { + entryRefusal, NeedsJspi, notifyInstancePoisoned, PendingCapability, - entryRefusal, } from "../task/scheduler.ts"; import { COMPONENT_INSTANCE } from "./context.ts"; import type { diff --git a/runtime/src/cabi/lift.ts b/runtime/src/cabi/lift.ts index 15089fa..e057470 100644 --- a/runtime/src/cabi/lift.ts +++ b/runtime/src/cabi/lift.ts @@ -26,11 +26,7 @@ import { type FieldType, type ValType, } from "./types.ts"; -import { - liftErrorContext, - liftFuture, - liftStream, -} from "./async_values.ts"; +import { liftErrorContext, liftFuture, liftStream } from "./async_values.ts"; /** Anything lift can pull core values from (CoreValueIter or the variant * coercion iterator). */ diff --git a/runtime/src/cabi/load.ts b/runtime/src/cabi/load.ts index 223307d..4631ff8 100644 --- a/runtime/src/cabi/load.ts +++ b/runtime/src/cabi/load.ts @@ -14,11 +14,7 @@ import { type FieldType, type ValType, } from "./types.ts"; -import { - liftErrorContext, - liftFuture, - liftStream, -} from "./async_values.ts"; +import { liftErrorContext, liftFuture, liftStream } from "./async_values.ts"; export const MAX_LIST_BYTE_LENGTH = (1 << 28) - 1; diff --git a/runtime/src/cabi/lower.ts b/runtime/src/cabi/lower.ts index 7373854..ddcb257 100644 --- a/runtime/src/cabi/lower.ts +++ b/runtime/src/cabi/lower.ts @@ -22,11 +22,7 @@ import { type ValType, type VariantValue, } from "./types.ts"; -import { - lowerErrorContext, - lowerFuture, - lowerStream, -} from "./async_values.ts"; +import { lowerErrorContext, lowerFuture, lowerStream } from "./async_values.ts"; export function lowerFlat( cx: LiftLowerContext, diff --git a/runtime/src/cabi/memory.ts b/runtime/src/cabi/memory.ts index 68cb79d..e72c56a 100644 --- a/runtime/src/cabi/memory.ts +++ b/runtime/src/cabi/memory.ts @@ -134,12 +134,14 @@ export function loadPtr(mem: MemInst, ptr: number): number | bigint { // bulk_lists.ts intentionally wraps instead — see that file's header for why. function bigintFitsIn64(v: bigint, signed: boolean): boolean { - return signed - ? v >= -(2n ** 63n) && v < 2n ** 63n - : v >= 0n && v < 2n ** 64n; + return signed ? v >= -(2n ** 63n) && v < 2n ** 63n : v >= 0n && v < 2n ** 64n; } -function numberFitsInWidth(v: number, nbytes: 1 | 2 | 4, signed: boolean): boolean { +function numberFitsInWidth( + v: number, + nbytes: 1 | 2 | 4, + signed: boolean, +): boolean { const bits = nbytes * 8; if (signed) { const min = -(2 ** (bits - 1)); diff --git a/runtime/src/cabi/store.ts b/runtime/src/cabi/store.ts index 76d9f57..b95eaf3 100644 --- a/runtime/src/cabi/store.ts +++ b/runtime/src/cabi/store.ts @@ -22,11 +22,7 @@ import { type ValType, type VariantValue, } from "./types.ts"; -import { - lowerErrorContext, - lowerFuture, - lowerStream, -} from "./async_values.ts"; +import { lowerErrorContext, lowerFuture, lowerStream } from "./async_values.ts"; export function store( cx: LiftLowerContext, diff --git a/runtime/src/cabi/strings.ts b/runtime/src/cabi/strings.ts index 852f6f5..c684aeb 100644 --- a/runtime/src/cabi/strings.ts +++ b/runtime/src/cabi/strings.ts @@ -328,21 +328,21 @@ function storeStringToUtf8( assert_(worstCaseSize <= REALLOC_I32_MAX); ptr = cx.reallocate(ptr, srcCodeUnits, 1, worstCaseSize); trapIfRangeExceedsMemory( - mem, - ptr, - worstCaseSize, - REALLOC_OOB, - ); + mem, + ptr, + worstCaseSize, + REALLOC_OOB, + ); const encoded = utf8Encoder.encode(src); // USVString: replaces lone surrogates writeBytes(mem, ptr + i, encoded.subarray(i)); if (worstCaseSize > encoded.length) { ptr = cx.reallocate(ptr, worstCaseSize, 1, encoded.length); trapIfRangeExceedsMemory( - mem, - ptr, - encoded.length, - REALLOC_OOB, - ); + mem, + ptr, + encoded.length, + REALLOC_OOB, + ); } return [ptr, BigInt(encoded.length)]; } @@ -380,11 +380,11 @@ function storeStringToLatin1OrUtf16( ptr = cx.reallocate(ptr, srcCodeUnits, 2, worstCaseSize); trapIf(ptr !== alignTo(ptr, 2), REALLOC_MISALIGNED); trapIfRangeExceedsMemory( - mem, - ptr, - worstCaseSize, - REALLOC_OOB, - ); + mem, + ptr, + worstCaseSize, + REALLOC_OOB, + ); for (let j = dstByteLength - 1; j >= 0; j--) { mem.bytes[ptr + 2 * j] = mem.bytes[ptr + j]; mem.bytes[ptr + 2 * j + 1] = 0; @@ -399,11 +399,11 @@ function storeStringToLatin1OrUtf16( ptr = cx.reallocate(ptr, worstCaseSize, 2, encoded.length); trapIf(ptr !== alignTo(ptr, 2), REALLOC_MISALIGNED); trapIfRangeExceedsMemory( - mem, - ptr, - encoded.length, - REALLOC_OOB, - ); + mem, + ptr, + encoded.length, + REALLOC_OOB, + ); } const taggedCodeUnits = BigInt(encoded.length / 2) | utf16TagBig(mem.ptrType()); @@ -414,11 +414,11 @@ function storeStringToLatin1OrUtf16( ptr = cx.reallocate(ptr, srcCodeUnits, 2, dstByteLength); trapIf(ptr !== alignTo(ptr, 2), REALLOC_MISALIGNED); trapIfRangeExceedsMemory( - mem, - ptr, - dstByteLength, - REALLOC_OOB, - ); + mem, + ptr, + dstByteLength, + REALLOC_OOB, + ); } return [ptr, BigInt(dstByteLength)]; } diff --git a/runtime/src/cabi/types.ts b/runtime/src/cabi/types.ts index 331128c..7506ac9 100644 --- a/runtime/src/cabi/types.ts +++ b/runtime/src/cabi/types.ts @@ -339,7 +339,9 @@ function despecializeUncached(t: ValType): DespecializedValType { return Object.freeze({ kind: "record" as const, fields: Object.freeze( - t.elements.map((e, i) => Object.freeze({ label: String(i), type: e })), + t.elements.map((e, i) => + Object.freeze({ label: String(i), type: e }) + ), ) as FieldType[], }); case "enum": @@ -464,7 +466,6 @@ export function containsBorrow(t: ValType | null): boolean { return contains(t, (u) => u.kind === "borrow"); } - export function contains( t: ValType | null, p: (t: DespecializedValType) => boolean, @@ -522,7 +523,8 @@ export function valTypeEqual(a: ValType, b: ValType): boolean { const bb = b as typeof a; return a.fields.length === bb.fields.length && a.fields.every((f, i) => - f.label === bb.fields[i].label && valTypeEqual(f.type, bb.fields[i].type) + f.label === bb.fields[i].label && + valTypeEqual(f.type, bb.fields[i].type) ); } case "tuple": { @@ -536,7 +538,9 @@ export function valTypeEqual(a: ValType, b: ValType): boolean { a.cases.every((c, i) => { const other = bb.cases[i]; if (c.label !== other.label) return false; - if (c.type === null || other.type === null) return c.type === other.type; + if (c.type === null || other.type === null) { + return c.type === other.type; + } return valTypeEqual(c.type, other.type); }); } @@ -594,12 +598,16 @@ export function fmtValType(t: ValType | null): string { ? `list<${fmtValType(t.element)}>` : `list<${fmtValType(t.element)}, ${t.length}>`; case "record": - return `record{${t.fields.map((f) => `${f.label}: ${fmtValType(f.type)}`).join(", ")}}`; + return `record{${ + t.fields.map((f) => `${f.label}: ${fmtValType(f.type)}`).join(", ") + }}`; case "tuple": return `tuple<${t.elements.map(fmtValType).join(", ")}>`; case "variant": return `variant{${ - t.cases.map((c) => c.type === null ? c.label : `${c.label}(${fmtValType(c.type)})`) + t.cases.map((c) => + c.type === null ? c.label : `${c.label}(${fmtValType(c.type)})` + ) .join(", ") }}`; case "enum": diff --git a/runtime/src/cache/dir.ts b/runtime/src/cache/dir.ts index be11909..0500fb2 100644 --- a/runtime/src/cache/dir.ts +++ b/runtime/src/cache/dir.ts @@ -14,7 +14,12 @@ // Deliberately does NOT store component bytes — see core.ts's // "PERSISTED-ARTIFACT-SET DECISION" docs for why that's sound, not a gap. -import type { ArtifactCache, CacheKey, CachedArtifacts, CacheMeta } from "./core.ts"; +import type { + ArtifactCache, + CachedArtifacts, + CacheKey, + CacheMeta, +} from "./core.ts"; import { CACHE_LAYOUT_VERSION, keyHex } from "./core.ts"; import type { WirePlan } from "../plan/format.ts"; import { loadPlan, PlanError } from "../plan/loader.ts"; @@ -139,7 +144,10 @@ class DirCache implements ArtifactCache { features: key.features, }; await Deno.writeTextFile(`${tmp}/meta.json`, JSON.stringify(meta)); - await Deno.writeTextFile(`${tmp}/plan.json`, JSON.stringify(artifacts.plan)); + await Deno.writeTextFile( + `${tmp}/plan.json`, + JSON.stringify(artifacts.plan), + ); for (const [file, bytes] of artifacts.adapters) { const name = safeRelName(file); await Deno.writeFile(`${tmp}/adapters/${name}`, bytes); diff --git a/runtime/src/cache/web.ts b/runtime/src/cache/web.ts index 4855b76..28a3ff1 100644 --- a/runtime/src/cache/web.ts +++ b/runtime/src/cache/web.ts @@ -10,7 +10,12 @@ // so we can't mirror dirCache's file-per-adapter layout; one blob per entry // is the natural shape here). -import type { ArtifactCache, CacheKey, CachedArtifacts, CacheMeta } from "./core.ts"; +import type { + ArtifactCache, + CachedArtifacts, + CacheKey, + CacheMeta, +} from "./core.ts"; import { CACHE_LAYOUT_VERSION, keyHex } from "./core.ts"; import type { WirePlan } from "../plan/format.ts"; import { loadPlan } from "../plan/loader.ts"; @@ -135,7 +140,11 @@ class WebCache implements ArtifactCache { for (const [file, bytes] of artifacts.adapters) { adaptersB64[file] = toBase64(bytes); } - const entry: StoredEntry = { meta, plan: artifacts.plan, adapters: adaptersB64 }; + const entry: StoredEntry = { + meta, + plan: artifacts.plan, + adapters: adaptersB64, + }; const body = JSON.stringify(entry); await cache.put( entryUrl(hex), diff --git a/runtime/src/digest/digest.ts b/runtime/src/digest/digest.ts index cf4ac39..2995523 100644 --- a/runtime/src/digest/digest.ts +++ b/runtime/src/digest/digest.ts @@ -75,7 +75,9 @@ export async function computeWorldDigest( plan: WirePlan, ): Promise { const resourceNames = buildResourceNameMap(plan); - const imports = plan.imports.map((imp) => canonImport(plan, imp, resourceNames)); + const imports = plan.imports.map((imp) => + canonImport(plan, imp, resourceNames) + ); const exports = plan.exports .map((exp) => canonExportItem(plan, exp, resourceNames)) .filter((c): c is Canon => c !== null); @@ -117,7 +119,9 @@ function buildResourceNameMap(plan: WirePlan): Map { // ABI-compatible with — worse than an unresolved-index throw. Refuse // conservatively whenever the plan declares any imported resources, full // stop, regardless of how many named (exported) resources exist. - if (plan.importedResources !== undefined && plan.importedResources.length > 0) { + if ( + plan.importedResources !== undefined && plan.importedResources.length > 0 + ) { throw new DigestError( `digest: plan declares ${plan.importedResources.length} imported ` + `resource(s); resolving which own/borrow occurrences reference an ` + @@ -191,7 +195,9 @@ function canonImport( // from a flat list. Revisit when a corpus component actually imports // something (flagged in the track report for §9's degraded-mode // question). - const name = imp.path.length > 0 ? [...imp.path, imp.name].join("/") : imp.name; + const name = imp.path.length > 0 + ? [...imp.path, imp.name].join("/") + : imp.name; if (imp.kind === "func" && imp.type !== undefined) { return { kind: "func", @@ -269,7 +275,10 @@ function canonFuncType( }; } -function canonValType(t: WireValType, resourceNames: Map): Canon { +function canonValType( + t: WireValType, + resourceNames: Map, +): Canon { switch (t.kind) { case "bool": case "s8": @@ -354,12 +363,16 @@ function canonValType(t: WireValType, resourceNames: Map): Canon case "stream": return { kind: "stream", - element: t.element === null ? null : canonValType(t.element, resourceNames), + element: t.element === null + ? null + : canonValType(t.element, resourceNames), }; case "future": return { kind: "future", - element: t.element === null ? null : canonValType(t.element, resourceNames), + element: t.element === null + ? null + : canonValType(t.element, resourceNames), }; default: { const exhaustive: never = t; diff --git a/runtime/src/embedder/errors.ts b/runtime/src/embedder/errors.ts index cad7eb1..0d632ef 100644 --- a/runtime/src/embedder/errors.ts +++ b/runtime/src/embedder/errors.ts @@ -12,17 +12,17 @@ // arrives from embedder code (issue #83). export { + ComponentException, DroppedError, InvalidHandleError, + isComponentException, isDroppedError, isInvalidHandleError, isPeerTrappedError, isStreamProducerError, isTrap, - isComponentException, PeerTrappedError, Trap, - ComponentException, } from "@polyengine/protocol"; /** diff --git a/runtime/src/embedder/imports.ts b/runtime/src/embedder/imports.ts index 840c7a3..3baf8db 100644 --- a/runtime/src/embedder/imports.ts +++ b/runtime/src/embedder/imports.ts @@ -11,7 +11,12 @@ import type { WirePlan } from "../plan/format.ts"; import { loadPlan } from "../plan/loader.ts"; import type { LoadedPlan } from "../plan/loader.ts"; import type { ValType } from "../cabi/types.ts"; -import { camelCase, type LeafName, parseLeafName, pascalCase } from "./casing.ts"; +import { + camelCase, + type LeafName, + parseLeafName, + pascalCase, +} from "./casing.ts"; /** Function type summary of an import leaf (names are docs-only; §"Functions"). */ export interface FuncSummary { diff --git a/runtime/src/embedder/instantiate.ts b/runtime/src/embedder/instantiate.ts index a729cb6..cca5716 100644 --- a/runtime/src/embedder/instantiate.ts +++ b/runtime/src/embedder/instantiate.ts @@ -18,10 +18,10 @@ import type { ComponentValue, VariantValue } from "../cabi/types.ts"; import { Trap } from "../cabi/trap.ts"; import { type ComponentHandle, - SYNC_ENTRY, type HostImports, hostResourceType, instantiateComponent, + SYNC_ENTRY, } from "../exec/mod.ts"; import { camelCase, parseLeafName, pascalCase } from "./casing.ts"; import { @@ -33,8 +33,8 @@ import { suspending, } from "../jspi/suspending.ts"; import { Translator } from "../shim/mod.ts"; -import { copyCensus, isTrap, isComponentException } from "@polyengine/protocol"; -import { NameCollisionError, ComponentException } from "./errors.ts"; +import { copyCensus, isComponentException, isTrap } from "@polyengine/protocol"; +import { ComponentException, NameCollisionError } from "./errors.ts"; import { type ImportLeaf, requiredImports } from "./imports.ts"; import { hostDtorCall } from "../exec/boundary.ts"; import { diff --git a/runtime/src/embedder/mod.ts b/runtime/src/embedder/mod.ts index 488b296..7f947f6 100644 --- a/runtime/src/embedder/mod.ts +++ b/runtime/src/embedder/mod.ts @@ -39,14 +39,19 @@ export { type ComponentArtifacts, type EmbedderInstance, type EmbedderOptions, - type InstantiateSource, - type UntranslatedArtifacts, instantiate, instantiateEmbedder, + type InstantiateSource, resolveArtifacts, + type UntranslatedArtifacts, } from "./instantiate.ts"; -export { type FuncSummary, type ImportLeaf, type PlanLike, requiredImports } from "./imports.ts"; +export { + type FuncSummary, + type ImportLeaf, + type PlanLike, + requiredImports, +} from "./imports.ts"; // `NameCollisionError` is the one error class that stays here: it's raised // while building an instantiation facade, before any handle/value exists — @@ -62,9 +67,15 @@ export { type ElemCodec } from "./streams.ts"; // concrete `Stream`/`StreamWriter` classes are no longer exported. Handle // TYPES are spelled against `@polyengine/protocol`'s structural interfaces. import { Stream as InternalStream } from "./streams.ts"; -import type { Stream as ProtocolStream, StreamWriter as ProtocolStreamWriter } from "@polyengine/protocol"; +import type { + Stream as ProtocolStream, + StreamWriter as ProtocolStreamWriter, +} from "@polyengine/protocol"; -export function createStream(): { stream: ProtocolStream; writer: ProtocolStreamWriter } { +export function createStream(): { + stream: ProtocolStream; + writer: ProtocolStreamWriter; +} { return InternalStream.create(); } diff --git a/runtime/src/embedder/resources.ts b/runtime/src/embedder/resources.ts index 8f42428..e53d398 100644 --- a/runtime/src/embedder/resources.ts +++ b/runtime/src/embedder/resources.ts @@ -209,11 +209,13 @@ function requireLive(w: object, what: string): WrapperState { const s = wrapperState(w); if (s === undefined) { if (isForeignWrapper(w)) { - throw new InvalidHandleError(`${what}: ${describeCrossCopy( - "this resource handle", - "Resource wrappers hold a rep in the minting copy's tables; there " + - "is no by-value form — call through the copy that created it.", - )}`); + throw new InvalidHandleError(`${what}: ${ + describeCrossCopy( + "this resource handle", + "Resource wrappers hold a rep in the minting copy's tables; there " + + "is no by-value form — call through the copy that created it.", + ) + }`); } throw new InvalidHandleError(`${what}: not a resource handle`); } @@ -435,9 +437,7 @@ export function buildGuestResourceClass( // callable bare. markSyncCallable( methodFn, - payload.kind === "free" - ? { kind: "method", fn: payload.fn } - : payload, // kind "async": pass the brand through unchanged + payload.kind === "free" ? { kind: "method", fn: payload.fn } : payload, // kind "async": pass the brand through unchanged ); } Object.defineProperty(cls.prototype, js, { diff --git a/runtime/src/embedder/streams.ts b/runtime/src/embedder/streams.ts index f1eaf0e..e904bc3 100644 --- a/runtime/src/embedder/streams.ts +++ b/runtime/src/embedder/streams.ts @@ -33,16 +33,16 @@ import { type DirectDestination, type DirectSource, type DirectVerdict, - type ErrorContext as ProtocolErrorContext, ERROR_CONTEXT, - type Future as ProtocolFuture, + type ErrorContext as ProtocolErrorContext, FUTURE, + type Future as ProtocolFuture, hasBrand, isStreamProducerError, STREAM, type Stream as ProtocolStream, - StreamProducerError, STREAM_WRITER, + StreamProducerError, type StreamWriter as ProtocolStreamWriter, } from "@polyengine/protocol"; import { describeCrossCopy } from "./copy.ts"; @@ -165,7 +165,11 @@ export function isU8Element(element: ValType | null): boolean { // (§"The host-ABI surface and its version"); `exec/host_streams.ts` keeps its // own structurally-identical copies for the low-level seam, so both layers // agree without either importing the other. -export type { DirectDestination, DirectSource, DirectVerdict } from "@polyengine/protocol"; +export type { + DirectDestination, + DirectSource, + DirectVerdict, +} from "@polyengine/protocol"; /** * direct-access byte edge (#128): the direct-access byte edges are `stream` only. A diff --git a/runtime/src/embedder/sync.ts b/runtime/src/embedder/sync.ts index f9bdfd8..deb6ac5 100644 --- a/runtime/src/embedder/sync.ts +++ b/runtime/src/embedder/sync.ts @@ -255,10 +255,8 @@ function recordView(rec: object): unknown { * instead of leaving the function itself alone. */ export type Sync = F extends (...a: infer A) => Promise ? (...a: A) => R - : F extends (...a: never[]) => unknown - ? F // non-Promise functions (e.g. `drop(): void`) pass through unchanged - : F extends object - ? { [K in keyof F]: Sync } // interfaces, class instances, records + : F extends (...a: never[]) => unknown ? F // non-Promise functions (e.g. `drop(): void`) pass through unchanged + : F extends object ? { [K in keyof F]: Sync } // interfaces, class instances, records : F; /** diff --git a/runtime/src/embedder/values.ts b/runtime/src/embedder/values.ts index 61cc037..42aa9c9 100644 --- a/runtime/src/embedder/values.ts +++ b/runtime/src/embedder/values.ts @@ -341,7 +341,9 @@ export function toHost( // kind is "err" (cabi/types.ts `despecialize`). const kind = label === "error" ? "err" : "ok"; const ct = label === "error" ? t.error : t.ok; - return ct === null ? { kind } : { kind, value: toHost(payload, ct, o, scope) }; + return ct === null + ? { kind } + : { kind, value: toHost(payload, ct, o, scope) }; } case "flags": { checkNoCollisions(t, t.labels, `${o.where}: flags`); @@ -452,7 +454,9 @@ export function fromHost( if (v instanceof ErrorContext) { return v.internal as unknown as ComponentValue; } - if (v instanceof InternalErrorContext) return v as unknown as ComponentValue; + if (v instanceof InternalErrorContext) { + return v as unknown as ComponentValue; + } // realm boundary (contracts/embedder-api.md §"Error-context is message-valued"; // issue #131; definitions.py — an error-context's state is exactly // its debug message): a branded carrier of a string `message`, from @@ -536,7 +540,9 @@ export function fromHost( } if (c.type === null) return { kind, value: null }; if (!has) { - throw new TypeError(`${o.where}: variant case '${kind}' needs a 'value'`); + throw new TypeError( + `${o.where}: variant case '${kind}' needs a 'value'`, + ); } return { kind, value: fromHost(value, c.type, o) }; } diff --git a/runtime/src/embedder/version.ts b/runtime/src/embedder/version.ts index 991b55c..e76f6b6 100644 --- a/runtime/src/embedder/version.ts +++ b/runtime/src/embedder/version.ts @@ -269,7 +269,9 @@ export class ImportResolver { `'${p.base}' is the unversioned key '${un}'. Version-agnostic ` + `folding is banned (contracts/embedder-api.md §"Version canonicalization"): ` + `register '${p.base}@${p.version}' or the compatibility-track ` + - `key '${trackKeyOf(p.base, p.semver) ?? p.base + "@" + p.version}'.`, + `key '${ + trackKeyOf(p.base, p.semver) ?? p.base + "@" + p.version + }'.`, ); } return undefined; diff --git a/runtime/src/exec/boundary.ts b/runtime/src/exec/boundary.ts index 479fa43..1b4f3e7 100644 --- a/runtime/src/exec/boundary.ts +++ b/runtime/src/exec/boundary.ts @@ -5,14 +5,14 @@ import { type CanonicalOptions, - coreFuncTypeEquals, - CoreValueIter, type ComponentValue, type CoreFuncType, + coreFuncTypeEquals, type CoreType, type CoreValue, - type FuncType, + CoreValueIter, flattenFunctype, + type FuncType, liftFlatValues, LiftLowerContext, lowerFlatValues, @@ -25,34 +25,34 @@ import { trap, trapIf, } from "../cabi/mod.ts"; -import { AssertionError, assert_ } from "../cabi/trap.ts"; +import { assert_, AssertionError } from "../cabi/trap.ts"; import { + addInstancePoisonedListener, type BlockRequest, type Cancelled, ComponentInstanceState, driveSyncLift, + entryRefusal, EventCode, - withActivation, - addInstancePoisonedListener, + type EventTuple, hasRealHostCall, isInstancePoisoned, - type EventTuple, NeedsJspi, needsJspi, + notifyInstancePoisoned, packSubtaskResult, PendingCapability, - notifyInstancePoisoned, realHostCalls, Store, storeQuiescent, Subtask, - SyncEntryBusy, - WaitableSet, SubtaskState, + SyncEntryBusy, Task, type TaskOptions, Thread, - entryRefusal, + WaitableSet, + withActivation, } from "../task/mod.ts"; import { currentTask } from "../task/scheduler.ts"; import { PlanError } from "../plan/loader.ts"; @@ -376,7 +376,6 @@ function isPromiseLike(v: unknown): v is PromiseLike { ); } - // --------------------------------------------------------------------------- // Handshake probe // --------------------------------------------------------------------------- @@ -406,14 +405,21 @@ function describeWaiter(t: unknown): string { const kind = w?.constructor?.name ?? "?"; let verdict = "?"; try { - verdict = w.ready?.() ? "READY" : (w.readyFunc === null ? "explicit" : "not-ready"); + verdict = w.ready?.() + ? "READY" + : (w.readyFunc === null ? "explicit" : "not-ready"); } catch (e) { verdict = `threw:${e}`; } return `${kind}[${verdict}]`; } -function traceDrive(loop: string, store: Store, done: () => boolean, branch: string): void { +function traceDrive( + loop: string, + store: Store, + done: () => boolean, + branch: string, +): void { if (!DRIVE_TRACE) return; let doneVerdict = "?"; try { @@ -423,7 +429,10 @@ function traceDrive(loop: string, store: Store, done: () => boolean, branch: str } const waiters = store.waiting.map(describeWaiter).join(","); const awaiters = [...store.awaiting].map((t) => { - const a = t as { constructor?: { name?: string }; task?: { label?: string } }; + const a = t as { + constructor?: { name?: string }; + task?: { label?: string }; + }; return `${a?.constructor?.name ?? "?"}`; }).join(","); console.error( @@ -569,7 +578,10 @@ function driveLoop( /** A settled parked-thread promise, tagged with the thread that owns it. */ type AwaitWinner = { - t: { awaiting: Promise | null; resumeWith(v: unknown, f?: { error: unknown }): void }; + t: { + awaiting: Promise | null; + resumeWith(v: unknown, f?: { error: unknown }): void; + }; /** * The promise this tag was minted from — i.e. what `t.awaiting` held at * `tagAwait` time. Carried so a resumption site can check that the thread is @@ -731,7 +743,10 @@ export function whenStoreDriverIdle(store: Store): Promise { // drops the speculative entry on its way out of the race, and re-evaluates // `done()` — which is exactly the stand-down the pumps were always supposed to // perform, now prompt instead of "whenever the host happens to answer". -const driverArrivals = new WeakMap; r: () => void }>(); +const driverArrivals = new WeakMap< + Store, + { p: Promise; r: () => void } +>(); /** A one-shot that resolves (to `null`, the race's "nothing settled" value) * when another driver starts on `store`. */ @@ -796,7 +811,10 @@ function fireDriverArrival(store: Store): void { // two mean different things — "another loop is driving this store, stand // down" versus "your snapshot is stale, re-take it" — and only the first is // what `fireDriverArrival`'s callers and doc comment assert. -const hostCallArrivals = new WeakMap; r: () => void }>(); +const hostCallArrivals = new WeakMap< + Store, + { p: Promise; r: () => void } +>(); /** A one-shot that resolves (to `null`, the race's "nothing settled" value) * when a new host call is registered on `store`. */ @@ -833,7 +851,10 @@ function fireHostCallArrival(store: Store): void { * act", not "the host owes an event" — the same distinction `hasRealHostCall` * draws.) */ -export function registerHostCall(store: Store, promise: Promise): void { +export function registerHostCall( + store: Store, + promise: Promise, +): void { store.pendingHostCalls.add(promise); fireHostCallArrival(store); } @@ -890,7 +911,10 @@ export function registerHostCall(store: Store, promise: Promise): void // the keeper wakes, re-snapshots, and re-parks. const settlementPumps = new WeakSet(); -const settlementNudges = new WeakMap; r: () => void }>(); +const settlementNudges = new WeakMap< + Store, + { p: Promise; r: () => void } +>(); function armSettlementNudge(store: Store): Promise { let n = settlementNudges.get(store); @@ -1030,415 +1054,422 @@ async function driveAsync( // stands down within a microtask. if (depth > 1) fireDriverArrival(store); try { - let claimHops = 0; - for (;;) { - traceDrive("driveAsync", store, done, "top"); - // FIRST: service every settled-but-unserviced activation tail, in settle - // order (`Store.settled` — armed eagerly at park time). A settled - // `awaitValue` is the rest of an activation that already finished its - // wasm; the reference runs that bookkeeping atomically inside - // `Thread.resume`, so nothing may be scheduled past it (`Store.tick` - // refuses while the queue is non-empty). Servicing after ticking let a - // freshly-resumed caller race into an entry gate while a finished - // callee's body had yet to release the exclusive slot — cancellable.wast - // then reported STARTING for an entry the reference admits. - store.serviceSettled(); - if (store.hostFailure !== undefined) throw takeHostFailure(store); - // A pending resumption of THIS store is an engine-driven resumption in - // flight: its activation has not yet parked again or finished. It will - // die on its own — parking consumes it (`blockCurrentActivation`), - // finishing releases it (`Store.noteAwaiting`'s settle continuation) — so - // yield microtasks until it does. The driver must NOT blanket-clear here: - // an entry may have been taken by a guest built-in settling another - // activation's suspension (`subtask.cancel` delivering a cancellation), - // and clearing it before that activation runs re-opens the - // mis-attribution window the entry exists to close. - // - // PER-STORE (issue #210): read only THIS store's entries. Activations - // never cross stores, so another store's pending resumption is none of - // this loop's business — and a gate shared across stores would spin an - // idle store's driver here, to its death at the hop bound below in - // ~311ms, merely because ANOTHER store's guest was dwelling on a slow - // host import. - if (store.hasPendingResumptions()) { - traceDrive("driveAsync", store, done, "yield-pending"); - // Bounded: a pending entry that never dies is an internal bug (every - // path out of a resumed activation releases it — park, finish, trap), - // and a pure-microtask wait would otherwise starve the event loop and - // every stall timer with it. Interleave macrotask hops so timers stay - // alive, and fail loudly rather than spin forever. Scoped per store, - // this is again the internal-bug detector it was meant to be. - claimHops++; - assert_( - claimHops < 10_000, - "driveAsync: a resumed-activation claim was never released " + - "(the activation neither parked, finished, nor trapped)", - ); - if (claimHops % 100 === 0) { - await new Promise((r) => setTimeout(r, 0)); - } else { - await Promise.resolve(); + let claimHops = 0; + for (;;) { + traceDrive("driveAsync", store, done, "top"); + // FIRST: service every settled-but-unserviced activation tail, in settle + // order (`Store.settled` — armed eagerly at park time). A settled + // `awaitValue` is the rest of an activation that already finished its + // wasm; the reference runs that bookkeeping atomically inside + // `Thread.resume`, so nothing may be scheduled past it (`Store.tick` + // refuses while the queue is non-empty). Servicing after ticking let a + // freshly-resumed caller race into an entry gate while a finished + // callee's body had yet to release the exclusive slot — cancellable.wast + // then reported STARTING for an entry the reference admits. + store.serviceSettled(); + if (store.hostFailure !== undefined) throw takeHostFailure(store); + // A pending resumption of THIS store is an engine-driven resumption in + // flight: its activation has not yet parked again or finished. It will + // die on its own — parking consumes it (`blockCurrentActivation`), + // finishing releases it (`Store.noteAwaiting`'s settle continuation) — so + // yield microtasks until it does. The driver must NOT blanket-clear here: + // an entry may have been taken by a guest built-in settling another + // activation's suspension (`subtask.cancel` delivering a cancellation), + // and clearing it before that activation runs re-opens the + // mis-attribution window the entry exists to close. + // + // PER-STORE (issue #210): read only THIS store's entries. Activations + // never cross stores, so another store's pending resumption is none of + // this loop's business — and a gate shared across stores would spin an + // idle store's driver here, to its death at the hop bound below in + // ~311ms, merely because ANOTHER store's guest was dwelling on a slow + // host import. + if (store.hasPendingResumptions()) { + traceDrive("driveAsync", store, done, "yield-pending"); + // Bounded: a pending entry that never dies is an internal bug (every + // path out of a resumed activation releases it — park, finish, trap), + // and a pure-microtask wait would otherwise starve the event loop and + // every stall timer with it. Interleave macrotask hops so timers stay + // alive, and fail loudly rather than spin forever. Scoped per store, + // this is again the internal-bug detector it was meant to be. + claimHops++; + assert_( + claimHops < 10_000, + "driveAsync: a resumed-activation claim was never released " + + "(the activation neither parked, finished, nor trapped)", + ); + if (claimHops % 100 === 0) { + await new Promise((r) => setTimeout(r, 0)); + } else { + await Promise.resolve(); + } + continue; + } + claimHops = 0; + while (store.tick()) { + if (store.hostFailure !== undefined) throw takeHostFailure(store); + // FAIRNESS between tick-able threads and promise-parked ones. A thread + // that is READY again on every resume (the callback-ABI YIELD spin) + // would otherwise monopolize this drain while a parked thread's + // settled promise waits (the starvation that hung + // drop-subtask.wast:139), and the engine's own continuations (jspi + // pin (j)) only ever land on microtask turns. One hop per tick; bail + // to the top the moment an activation tail lands. + if (store.awaiting.size > 0) { + await Promise.resolve(); + if (store.hasServiceableSettled()) break; + } } - continue; - } - claimHops = 0; - while (store.tick()) { if (store.hostFailure !== undefined) throw takeHostFailure(store); - // FAIRNESS between tick-able threads and promise-parked ones. A thread - // that is READY again on every resume (the callback-ABI YIELD spin) - // would otherwise monopolize this drain while a parked thread's - // settled promise waits (the starvation that hung - // drop-subtask.wast:139), and the engine's own continuations (jspi - // pin (j)) only ever land on microtask turns. One hop per tick; bail - // to the top the moment an activation tail lands. - if (store.awaiting.size > 0) { - await Promise.resolve(); - if (store.hasServiceableSettled()) break; + if (done()) { + traceDrive("driveAsync", store, done, "EXIT-done"); + return "done"; } - } - if (store.hostFailure !== undefined) throw takeHostFailure(store); - if (done()) { - traceDrive("driveAsync", store, done, "EXIT-done"); - return "done"; - } - // Only a SERVICEABLE tail is a reason to loop again: a queue holding - // only tails DEFERRED on a non-enterable instance (issue #156) would - // spin this loop hot — nothing in the cycle awaits. - if (store.hasServiceableSettled() || store.hasPendingResumptions()) { - continue; - } - // Service promise-parked threads (jspi). - // - // This must NOT block on one chosen thread's promise. A thread parked on a - // promising-wrapped nested activation only settles once that activation's - // own suspension points have been resumed -- and resuming those is - // `Store.tick`'s job, i.e. *this loop's* job. Awaiting a single promise - // therefore stops the scheduler while waiting for something that needs the - // scheduler: a pure-microtask stall with no trap and no rejection. - // Observed on `async/async-calls-sync.wast` the moment site 1 became the - // first lit suspension site: turn N serviced a promise that - // never settled while three other parked threads and three ready-able - // suspension points went unexamined. - // - // So: race every outstanding promise (parked threads AND host calls) and - // service whichever settles first, re-ticking each turn. The claim is - // taken in the tagged continuation -- as close to settlement as we can get - // -- so pin (i)'s window (engine-driven wasm resumption running built-ins - // before our continuation) is still covered for the thread that actually - // resumed, without falsely claiming the ambient for threads that did not. - if (store.awaiting.size > 0) { - // Is this actually progress, or a deadlock wearing its clothes? + // Only a SERVICEABLE tail is a reason to loop again: a queue holding + // only tails DEFERRED on a non-enterable instance (issue #156) would + // spin this loop hot — nothing in the cycle awaits. + if (store.hasServiceableSettled() || store.hasPendingResumptions()) { + continue; + } + // Service promise-parked threads (jspi). + // + // This must NOT block on one chosen thread's promise. A thread parked on a + // promising-wrapped nested activation only settles once that activation's + // own suspension points have been resumed -- and resuming those is + // `Store.tick`'s job, i.e. *this loop's* job. Awaiting a single promise + // therefore stops the scheduler while waiting for something that needs the + // scheduler: a pure-microtask stall with no trap and no rejection. + // Observed on `async/async-calls-sync.wast` the moment site 1 became the + // first lit suspension site: turn N serviced a promise that + // never settled while three other parked threads and three ready-able + // suspension points went unexamined. // - // Everything in `store.awaiting` is an INTERNAL promise: a - // promising-wrapped wasm activation. Such a promise settles either on - // its own (the activation ran to completion -- which happens within one - // macrotask turn, since the work is already done and only the microtask - // hop remains) or because WE resume a suspension point it is waiting - // behind. If no thread is ready, no host call is outstanding, and a full - // macrotask turn passes with nothing settling, then nobody can move: the - // awaited promises need us and we need them. That is the deadlock trap - // (definitions.py `canon_lift`'s empty-candidate-set `trap_if`), and - // without this check it presents as a silent stall instead -- which is - // exactly what `tests/jspi/deadlock_test.ts` caught the moment site 2 - // was lit. - if (store.pendingHostCalls.size === 0 && !store.hasPendingResumptions()) { - traceDrive("driveAsync", store, done, "deadlock-probe"); - // Exclude threads whose settle is already QUEUED in `store.settled` - // (issue #156): their promise has settled, so racing them wins - // instantly off the memoized `tagAwait` tag, forever, in an unbounded - // microtask chain — the tail is `serviceSettled`'s to run. + // So: race every outstanding promise (parked threads AND host calls) and + // service whichever settles first, re-ticking each turn. The claim is + // taken in the tagged continuation -- as close to settlement as we can get + // -- so pin (i)'s window (engine-driven wasm resumption running built-ins + // before our continuation) is still covered for the thread that actually + // resumed, without falsely claiming the ambient for threads that did not. + if (store.awaiting.size > 0) { + // Is this actually progress, or a deadlock wearing its clothes? + // + // Everything in `store.awaiting` is an INTERNAL promise: a + // promising-wrapped wasm activation. Such a promise settles either on + // its own (the activation ran to completion -- which happens within one + // macrotask turn, since the work is already done and only the microtask + // hop remains) or because WE resume a suspension point it is waiting + // behind. If no thread is ready, no host call is outstanding, and a full + // macrotask turn passes with nothing settling, then nobody can move: the + // awaited promises need us and we need them. That is the deadlock trap + // (definitions.py `canon_lift`'s empty-candidate-set `trap_if`), and + // without this check it presents as a silent stall instead -- which is + // exactly what `tests/jspi/deadlock_test.ts` caught the moment site 2 + // was lit. + if ( + store.pendingHostCalls.size === 0 && !store.hasPendingResumptions() + ) { + traceDrive("driveAsync", store, done, "deadlock-probe"); + // Exclude threads whose settle is already QUEUED in `store.settled` + // (issue #156): their promise has settled, so racing them wins + // instantly off the memoized `tagAwait` tag, forever, in an unbounded + // microtask chain — the tail is `serviceSettled`'s to run. + const queued = new Set(store.settled.map((s) => s.t)); + const parked = ([...store.awaiting] as AwaitWinner["t"][]).filter( + (t) => !queued.has(t), + ); + const progressed = await Promise.race([ + ...parked.map((t) => tagAwait(t).then(() => true)), + new Promise((r) => setTimeout(() => r(false), 0)), + ]); + traceDrive( + "driveAsync", + store, + done, + `deadlock-probe:progressed=${progressed}`, + ); + if (!progressed) { + // The race covered a SNAPSHOT of the awaiting set. A thread that + // parked during the macrotask turn (a promising callee's body + // yielding its awaitValue mid-hop — jspi pin (j) makes this + // routine) was not raced, and its promise may already be settled; + // trapping now would declare a deadlock one iteration before the + // loop would have serviced it. Membership change ⇒ re-probe. + // + // `fresh` gets the SAME queued-entry filter `parked` got (issue + // #156), against a RECOMPUTED queued set — the settled queue can + // change across the probe's await. Comparing a filtered snapshot + // against an unfiltered one would read "changed" on every turn in + // the all-deferred wedge state, so the verdict below could never + // be reached and the wedge would present as a silent + // macrotask-paced busy idle instead of a trap. + const freshQueued = new Set(store.settled.map((s) => s.t)); + const fresh = ([...store.awaiting] as AwaitWinner["t"][]).filter( + (t) => !freshQueued.has(t), + ); + const changed = fresh.length !== parked.length || + fresh.some((t, i) => t !== parked[i]); + if (changed) continue; + // The probe's precondition can also expire WITHOUT the awaiting + // set changing: the same activation resumes off an engine + // continuation chunk during the probe's macrotask turn (jspi + // pin (j) — a sync-completing Suspending import still defers its + // continuation), runs, and re-parks through the suspending mark arm, which + // registers a fresh `pendingHostCalls` entry. The activation + // promise never settled and `awaiting` membership is unchanged, + // but the park is externally wakeable now — the verdict's own + // precondition (`pendingHostCalls.size === 0`) no longer holds. + // Observed on wasi-shims' stream/future round-trip poll (sync fast path): probe sampled + // hostCalls=0 between a settled park and the next one, then + // trapped a live workload with hostCalls=1. Re-check ⇒ re-probe. + // Likewise a SERVICEABLE settled entry (issue #156): dispatching + // it is progress, so this is not a deadlock verdict — re-probe. + // A deferred-only queue deliberately does NOT re-probe: nothing + // can dispatch it while the lock is held, and if no host call is + // outstanding nothing will ever release that lock, so it falls + // THROUGH to the verdict below — the same loud-wedge treatment the + // servicing race's own all-deferred fallthrough gets. Per the #156 + // analysis that state is unreachable (a lock spanning this loop's + // await always has a `pendingHostCalls` entry, which fails this + // probe's precondition); keeping it loud is what makes it an + // internal-wedge detector rather than dead code. + if ( + store.pendingHostCalls.size > 0 || + store.hasPendingResumptions() || + store.hasServiceableSettled() + ) { + continue; + } + if (store.readyCandidates().length === 0) { + if (idle === "exit") { + traceDrive("driveAsync", store, done, "EXIT-idle"); + return "idle"; + } + trapIf( + true, + `wasm trap: deadlock detected: event loop cannot make ` + + `further progress (${what}: every suspended activation is ` + + `waiting on a suspension only this scheduler could resume, ` + + `and none is ready)`, + ); + } + // No promise settled, but a thread became READY while we waited -- + // typically a suspension point whose `readyFunc` turned true because + // another activation ran during the macrotask turn. The way forward + // is `Store.tick`, not a promise: go back to the top and resume it. + // Falling through to the servicing block instead would await + // promises that nothing will settle while a runnable thread sits + // there -- the `async/sync-barges-in.wast` stall exactly. + continue; + } + // Progress IS possible: fall through to the normal servicing below, + // which resumes the settled thread. Returning to the top instead would + // spin -- the memoized tag is already settled, so the race would win + // instantly, forever, without anyone being resumed. + } + // Re-check membership: the deadlock probe above AWAITS, and everything + // below reads `[...store.awaiting][0]` as if the set were still + // non-empty. A thread resumed during the probe (its settle continuation + // runs `resumeWith`, which deletes it) can empty the set, and the + // snapshot's `parked[0]` is then `undefined` — the exact check-then-act + // shape that made the host pump's copy of this loop throw + // `TypeError: ... (reading 'awaiting')` into `store.hostFailure`, where + // it poisoned a later unrelated call via check-then-act on `store.hostFailure`. Nothing to + // service ⇒ go back to the top and re-evaluate `done`. + // Same re-check for the settled queue, and for the same reason: the + // probe's macrotask turn can land a fresh, SERVICEABLE activation tail + // (that is exactly what "progress IS possible" above usually means). + // The queue owns those threads — the race below deliberately excludes + // them (issue #156) — so the way forward is the top of the loop, where + // `serviceSettled` dispatches them. Without this, filtering the + // just-settled thread out of the race left the loop awaiting promises + // that only its dispatch could settle (observed: tests/jspi/ + // handshake_test.ts stalled, then tripped the claim assert). + if (store.awaiting.size === 0 || store.hasServiceableSettled()) { + continue; + } + // Claim the ambient for ONE parked thread and await its promise -- as + // before, so pin (i)'s window is covered exactly as it was -- but race + // that promise against every other outstanding promise so this loop can + // never be held hostage by it. The claimed thread's promise may only be + // settleable by further scheduler progress (a promising-wrapped nested + // activation whose own suspension points this loop must still resume); + // blocking on it alone is the pure-microtask stall described above. + // Same exclusion as the probe (issue #156): a thread whose tail is + // already queued in `store.settled` must not be raced — its tag is + // settled, so it re-wins instantly and livelocks the event loop, + // starving the very host-call settle that would release the lock. const queued = new Set(store.settled.map((s) => s.t)); const parked = ([...store.awaiting] as AwaitWinner["t"][]).filter( (t) => !queued.has(t), ); - const progressed = await Promise.race([ - ...parked.map((t) => tagAwait(t).then(() => true)), - new Promise((r) => setTimeout(() => r(false), 0)), - ]); - traceDrive( - "driveAsync", - store, - done, - `deadlock-probe:progressed=${progressed}`, - ); - if (!progressed) { - // The race covered a SNAPSHOT of the awaiting set. A thread that - // parked during the macrotask turn (a promising callee's body - // yielding its awaitValue mid-hop — jspi pin (j) makes this - // routine) was not raced, and its promise may already be settled; - // trapping now would declare a deadlock one iteration before the - // loop would have serviced it. Membership change ⇒ re-probe. - // - // `fresh` gets the SAME queued-entry filter `parked` got (issue - // #156), against a RECOMPUTED queued set — the settled queue can - // change across the probe's await. Comparing a filtered snapshot - // against an unfiltered one would read "changed" on every turn in - // the all-deferred wedge state, so the verdict below could never - // be reached and the wedge would present as a silent - // macrotask-paced busy idle instead of a trap. - const freshQueued = new Set(store.settled.map((s) => s.t)); - const fresh = ([...store.awaiting] as AwaitWinner["t"][]).filter( - (t) => !freshQueued.has(t), - ); - const changed = fresh.length !== parked.length || - fresh.some((t, i) => t !== parked[i]); - if (changed) continue; - // The probe's precondition can also expire WITHOUT the awaiting - // set changing: the same activation resumes off an engine - // continuation chunk during the probe's macrotask turn (jspi - // pin (j) — a sync-completing Suspending import still defers its - // continuation), runs, and re-parks through the suspending mark arm, which - // registers a fresh `pendingHostCalls` entry. The activation - // promise never settled and `awaiting` membership is unchanged, - // but the park is externally wakeable now — the verdict's own - // precondition (`pendingHostCalls.size === 0`) no longer holds. - // Observed on wasi-shims' stream/future round-trip poll (sync fast path): probe sampled - // hostCalls=0 between a settled park and the next one, then - // trapped a live workload with hostCalls=1. Re-check ⇒ re-probe. - // Likewise a SERVICEABLE settled entry (issue #156): dispatching - // it is progress, so this is not a deadlock verdict — re-probe. - // A deferred-only queue deliberately does NOT re-probe: nothing - // can dispatch it while the lock is held, and if no host call is - // outstanding nothing will ever release that lock, so it falls - // THROUGH to the verdict below — the same loud-wedge treatment the - // servicing race's own all-deferred fallthrough gets. Per the #156 - // analysis that state is unreachable (a lock spanning this loop's - // await always has a `pendingHostCalls` entry, which fails this - // probe's precondition); keeping it loud is what makes it an - // internal-wedge detector rather than dead code. - if ( - store.pendingHostCalls.size > 0 || store.hasPendingResumptions() || - store.hasServiceableSettled() - ) { + if (parked.length === 0) { + // UNREACHABLE BY CONSTRUCTION. `parked` is `store.awaiting` minus + // the threads whose tails are already queued in `store.settled`, and + // we only get here with `awaiting` non-empty and + // `hasServiceableSettled()` false — which means the settled queue is + // EMPTY, so nothing was excluded. Retained as a wedge detector, not + // as expected behavior. + if (store.pendingHostCalls.size > 0) { + await Promise.race([ + ...store.pendingHostCalls, + armDriverArrival(store), + armHostCallArrival(store), + ]).catch(() => {}); continue; } - if (store.readyCandidates().length === 0) { - if (idle === "exit") { - traceDrive("driveAsync", store, done, "EXIT-idle"); - return "idle"; - } - trapIf( - true, - `wasm trap: deadlock detected: event loop cannot make ` + - `further progress (${what}: every suspended activation is ` + - `waiting on a suspension only this scheduler could resume, ` + - `and none is ready)`, - ); - } - // No promise settled, but a thread became READY while we waited -- - // typically a suspension point whose `readyFunc` turned true because - // another activation ran during the macrotask turn. The way forward - // is `Store.tick`, not a promise: go back to the top and resume it. - // Falling through to the servicing block instead would await - // promises that nothing will settle while a runnable thread sits - // there -- the `async/sync-barges-in.wast` stall exactly. - continue; + // Per the issue #156 analysis this is unreachable (a spanning lock + // always has a `pendingHostCalls` entry; a synchronous lock cannot + // span this loop's await). An internal-wedge detector, not expected + // behavior. + traceDrive("driveAsync", store, done, "DEADLOCK-TRAP-deferred"); + trapIf( + true, + `wasm trap: deadlock detected: event loop cannot make further ` + + `progress (${what}: every settled activation tail is deferred ` + + `on a non-enterable instance and no host call is outstanding)`, + ); } - // Progress IS possible: fall through to the normal servicing below, - // which resumes the settled thread. Returning to the top instead would - // spin -- the memoized tag is already settled, so the race would win - // instantly, forever, without anyone being resumed. - } - // Re-check membership: the deadlock probe above AWAITS, and everything - // below reads `[...store.awaiting][0]` as if the set were still - // non-empty. A thread resumed during the probe (its settle continuation - // runs `resumeWith`, which deletes it) can empty the set, and the - // snapshot's `parked[0]` is then `undefined` — the exact check-then-act - // shape that made the host pump's copy of this loop throw - // `TypeError: ... (reading 'awaiting')` into `store.hostFailure`, where - // it poisoned a later unrelated call via check-then-act on `store.hostFailure`. Nothing to - // service ⇒ go back to the top and re-evaluate `done`. - // Same re-check for the settled queue, and for the same reason: the - // probe's macrotask turn can land a fresh, SERVICEABLE activation tail - // (that is exactly what "progress IS possible" above usually means). - // The queue owns those threads — the race below deliberately excludes - // them (issue #156) — so the way forward is the top of the loop, where - // `serviceSettled` dispatches them. Without this, filtering the - // just-settled thread out of the race left the loop awaiting promises - // that only its dispatch could settle (observed: tests/jspi/ - // handshake_test.ts stalled, then tripped the claim assert). - if (store.awaiting.size === 0 || store.hasServiceableSettled()) continue; - // Claim the ambient for ONE parked thread and await its promise -- as - // before, so pin (i)'s window is covered exactly as it was -- but race - // that promise against every other outstanding promise so this loop can - // never be held hostage by it. The claimed thread's promise may only be - // settleable by further scheduler progress (a promising-wrapped nested - // activation whose own suspension points this loop must still resume); - // blocking on it alone is the pure-microtask stall described above. - // Same exclusion as the probe (issue #156): a thread whose tail is - // already queued in `store.settled` must not be raced — its tag is - // settled, so it re-wins instantly and livelocks the event loop, - // starving the very host-call settle that would release the lock. - const queued = new Set(store.settled.map((s) => s.t)); - const parked = ([...store.awaiting] as AwaitWinner["t"][]).filter( - (t) => !queued.has(t), - ); - if (parked.length === 0) { - // UNREACHABLE BY CONSTRUCTION. `parked` is `store.awaiting` minus - // the threads whose tails are already queued in `store.settled`, and - // we only get here with `awaiting` non-empty and - // `hasServiceableSettled()` false — which means the settled queue is - // EMPTY, so nothing was excluded. Retained as a wedge detector, not - // as expected behavior. - if (store.pendingHostCalls.size > 0) { - await Promise.race([ - ...store.pendingHostCalls, + const chosen = parked[0]; + const chosenTag = tagAwait(chosen); + const others: Promise[] = parked.slice(1).map( + tagAwait, + ); + for (const h of store.pendingHostCalls) { + others.push(h.then(() => null, () => null)); + } + // A SPECULATIVE entry: the chosen thread is a promising-wrapped + // activation, and the engine may run its wasm during this await (pin + // (i)). It is dropped on the way out — if the activation is genuinely + // mid-resumption its own exact entry (minted by + // `SuspensionPoint.resume`) is what carries it, and dropping an entry + // that names a thread already gone from the set is a no-op. + // + // ONLY ITS OWN ENTRY (issue #158): the `finally` must drop the entry + // THIS loop added and nothing else. A guest-synchronous delivery during + // the await takes a fresh entry of its own, and clearing that one here + // would re-open early the very window it exists to close — which is why + // the gate is a set of entries rather than a single slot. + // + // SOLE DRIVER ONLY, AND ONLY UNTIL ONE ARRIVES (issue #239). The entry + // is a claim over a window this loop cannot bound: the race settles when + // the HOST answers, which may be never. As a store-wide scheduling gate + // (`Store.tick` refuses; every driver yields at its top) that is a wedge + // the moment a second driver exists — it spins at the top of its own + // loop and dies at the 10,000-hop assert in ~311ms, an internal-bug + // detector firing on a perfectly ordinary suspended guest. Two concurrent + // export calls with one slow suspending import were enough; the reported + // shape was a detached guest task cancelling an in-flight import, which + // parks mid-frame with no export call outstanding and leaves the + // settlement pump holding this entry. + // + // What the entry protects — "the engine may run `chosen`'s wasm during + // this await" — it protects by refusing OTHER `Store.tick` callers, and + // this loop is not one of them while it awaits. The tick callers that + // can reach a store mid-race are another `driveAsync` loop and + // `HostActivity.pump`'s synchronous drain (exec/host_streams.ts) — the + // latter is not gated by driver depth, so scoping the entry to "sole + // driver" does hand it a window an unscoped entry would close at + // depth >= 2. + // What holds regardless is the invariant the `driverDepth` note names: + // a genuine resumption is preceded by `SuspensionPoint.resume`'s OWN + // entry (jspi/bridge.ts, minted before the settle), and every + // resumption site here re-checks membership and promise identity + // synchronously — mechanisms (a) and (b), which is where that note + // already puts the weight. + // ONLY IF WE ADDED IT (issue #158, same rule as the `finally` below): + // `pendingResumptions` is a Set by identity, so a genuine entry for + // `chosen` minted meanwhile — or already held — collapses with ours, + // and removing "ours" would drop the genuine one. + const sole = storeDriverDepth(store) === 1; + const added = sole && !store.pendingResumptions.has(chosen); + if (added) store.addPendingResumption(chosen); + let winner: AwaitWinner | null; + try { + // `armDriverArrival` rides the race for every driver, not just the one + // holding the entry: waking on a new arrival is also how a fallback + // pump reaches its next `done()` — i.e. its stand-down — promptly. + // `armHostCallArrival` rides for the sibling reason: the tags below + // are a snapshot of what was parked when we entered the race, so a + // host call registered after that under no new driver (a sync `drive` + // export, `HostActivity.pump()`'s sync drain — neither fires a driver + // arrival) can ready a thread with no racer watching for it. See the + // host-call arrival note above. + winner = await Promise.race([ + chosenTag, + ...others, armDriverArrival(store), armHostCallArrival(store), - ]).catch(() => {}); - continue; + ]); + } finally { + if (added) store.removePendingResumption(chosen); + } + // Resume whichever thread actually settled -- not necessarily the one we + // claimed. Resuming only the claimed thread would spin: its promise may + // never settle, the same thread would be chosen again next turn, and the + // already-settled tags would win the race instantly forever (observed as + // an OOM, not a hang). Our own entry is dropped above before any resumption, + // exactly as on the original single-promise path, so this does not widen + // the ambient window; it only ensures the loop always makes progress. + // Membership is not enough: the corner it misses is a thread the OTHER + // overlapping loop resumed via `tick`, which then re-parked on a NEW + // promise, after which its OLD promise settles late — membership is + // true again but the tag's value belongs to a settlement this thread + // has already consumed. Compare promise identity too. + // ONE SETTLEMENT, ONE DELIVERY (definitions.py `Thread.resume` is + // atomic). `noteAwaiting` records settlements EAGERLY, so this + // promise's `store.settled` entry is already queued; left there, a + // body that re-parks SYNCHRONOUSLY inside `resumeWith` gets the OLD + // value delivered against its NEW park by the next `serviceSettled`. + if ( + winner !== null && store.awaiting.has(winner.t) && + winner.t.awaiting === winner.p + ) { + for (let i = store.settled.length - 1; i >= 0; i--) { + if (store.settled[i].t === winner.t) store.settled.splice(i, 1); + } + winner.t.resumeWith(winner.value, winner.failure); + } + continue; + } + if (store.pendingHostCalls.size === 0) { + if (idle === "exit") { + traceDrive("driveAsync", store, done, "EXIT-idle"); + return "idle"; } - // Per the issue #156 analysis this is unreachable (a spanning lock - // always has a `pendingHostCalls` entry; a synchronous lock cannot - // span this loop's await). An internal-wedge detector, not expected - // behavior. - traceDrive("driveAsync", store, done, "DEADLOCK-TRAP-deferred"); + traceDrive("driveAsync", store, done, "DEADLOCK-TRAP"); trapIf( true, `wasm trap: deadlock detected: event loop cannot make further ` + - `progress (${what}: every settled activation tail is deferred ` + - `on a non-enterable instance and no host call is outstanding)`, + `progress (${what}: no thread is ready and no host call is ` + + `outstanding)`, ); } - const chosen = parked[0]; - const chosenTag = tagAwait(chosen); - const others: Promise[] = parked.slice(1).map(tagAwait); - for (const h of store.pendingHostCalls) { - others.push(h.then(() => null, () => null)); - } - // A SPECULATIVE entry: the chosen thread is a promising-wrapped - // activation, and the engine may run its wasm during this await (pin - // (i)). It is dropped on the way out — if the activation is genuinely - // mid-resumption its own exact entry (minted by - // `SuspensionPoint.resume`) is what carries it, and dropping an entry - // that names a thread already gone from the set is a no-op. - // - // ONLY ITS OWN ENTRY (issue #158): the `finally` must drop the entry - // THIS loop added and nothing else. A guest-synchronous delivery during - // the await takes a fresh entry of its own, and clearing that one here - // would re-open early the very window it exists to close — which is why - // the gate is a set of entries rather than a single slot. - // - // SOLE DRIVER ONLY, AND ONLY UNTIL ONE ARRIVES (issue #239). The entry - // is a claim over a window this loop cannot bound: the race settles when - // the HOST answers, which may be never. As a store-wide scheduling gate - // (`Store.tick` refuses; every driver yields at its top) that is a wedge - // the moment a second driver exists — it spins at the top of its own - // loop and dies at the 10,000-hop assert in ~311ms, an internal-bug - // detector firing on a perfectly ordinary suspended guest. Two concurrent - // export calls with one slow suspending import were enough; the reported - // shape was a detached guest task cancelling an in-flight import, which - // parks mid-frame with no export call outstanding and leaves the - // settlement pump holding this entry. + traceDrive("driveAsync", store, done, "await-race"); + // Settlement order among several outstanding host calls is the host's, + // not ours — this is genuine, unavoidable nondeterminism at the boundary + // (the reference has the same freedom in `Store.tick`). Everything + // *inside* the component stays deterministic per scheduler.ts. // - // What the entry protects — "the engine may run `chosen`'s wasm during - // this await" — it protects by refusing OTHER `Store.tick` callers, and - // this loop is not one of them while it awaits. The tick callers that - // can reach a store mid-race are another `driveAsync` loop and - // `HostActivity.pump`'s synchronous drain (exec/host_streams.ts) — the - // latter is not gated by driver depth, so scoping the entry to "sole - // driver" does hand it a window an unscoped entry would close at - // depth >= 2. - // What holds regardless is the invariant the `driverDepth` note names: - // a genuine resumption is preceded by `SuspensionPoint.resume`'s OWN - // entry (jspi/bridge.ts, minted before the settle), and every - // resumption site here re-checks membership and promise identity - // synchronously — mechanisms (a) and (b), which is where that note - // already puts the weight. - // ONLY IF WE ADDED IT (issue #158, same rule as the `finally` below): - // `pendingResumptions` is a Set by identity, so a genuine entry for - // `chosen` minted meanwhile — or already held — collapses with ours, - // and removing "ours" would drop the genuine one. - const sole = storeDriverDepth(store) === 1; - const added = sole && !store.pendingResumptions.has(chosen); - if (added) store.addPendingResumption(chosen); - let winner: AwaitWinner | null; - try { - // `armDriverArrival` rides the race for every driver, not just the one - // holding the entry: waking on a new arrival is also how a fallback - // pump reaches its next `done()` — i.e. its stand-down — promptly. - // `armHostCallArrival` rides for the sibling reason: the tags below - // are a snapshot of what was parked when we entered the race, so a - // host call registered after that under no new driver (a sync `drive` - // export, `HostActivity.pump()`'s sync drain — neither fires a driver - // arrival) can ready a thread with no racer watching for it. See the - // host-call arrival note above. - winner = await Promise.race([ - chosenTag, - ...others, - armDriverArrival(store), - armHostCallArrival(store), - ]); - } finally { - if (added) store.removePendingResumption(chosen); - } - // Resume whichever thread actually settled -- not necessarily the one we - // claimed. Resuming only the claimed thread would spin: its promise may - // never settle, the same thread would be chosen again next turn, and the - // already-settled tags would win the race instantly forever (observed as - // an OOM, not a hang). Our own entry is dropped above before any resumption, - // exactly as on the original single-promise path, so this does not widen - // the ambient window; it only ensures the loop always makes progress. - // Membership is not enough: the corner it misses is a thread the OTHER - // overlapping loop resumed via `tick`, which then re-parked on a NEW - // promise, after which its OLD promise settles late — membership is - // true again but the tag's value belongs to a settlement this thread - // has already consumed. Compare promise identity too. - // ONE SETTLEMENT, ONE DELIVERY (definitions.py `Thread.resume` is - // atomic). `noteAwaiting` records settlements EAGERLY, so this - // promise's `store.settled` entry is already queued; left there, a - // body that re-parks SYNCHRONOUSLY inside `resumeWith` gets the OLD - // value delivered against its NEW park by the next `serviceSettled`. - if ( - winner !== null && store.awaiting.has(winner.t) && - winner.t.awaiting === winner.p - ) { - for (let i = store.settled.length - 1; i >= 0; i--) { - if (store.settled[i].t === winner.t) store.settled.splice(i, 1); - } - winner.t.resumeWith(winner.value, winner.failure); - } - continue; + // The driver-arrival one-shot rides here too. This is the routine park of + // a quiet guest with a real host call outstanding — no speculative entry + // is held, so there is no wedge to break, but a fallback pump parked here + // would otherwise not reach its `done()` (i.e. its stand-down) until the + // HOST answered, leaving two loops interleaving `serviceSettled`/`tick` + // for that whole window. That interleaving is what the `driverDepth` note + // above calls out as bad for throughput and blame. + await Promise.race([ + ...store.pendingHostCalls, + armDriverArrival(store), + // ... and the host-call-arrival one-shot, because the spread above is a + // SNAPSHOT: a host call registered while we are parked here under no + // new driver (a sync `drive` export, `HostActivity.pump()`'s sync drain + // — neither fires a driver arrival) would otherwise be watched by + // nobody at all (the settlement pump stands down while we, the parked + // driver, keep `storeDriverDepth` positive). See the host-call arrival + // note above. + armHostCallArrival(store), + ]).catch(() => {}); } - if (store.pendingHostCalls.size === 0) { - if (idle === "exit") { - traceDrive("driveAsync", store, done, "EXIT-idle"); - return "idle"; - } - traceDrive("driveAsync", store, done, "DEADLOCK-TRAP"); - trapIf( - true, - `wasm trap: deadlock detected: event loop cannot make further ` + - `progress (${what}: no thread is ready and no host call is ` + - `outstanding)`, - ); - } - traceDrive("driveAsync", store, done, "await-race"); - // Settlement order among several outstanding host calls is the host's, - // not ours — this is genuine, unavoidable nondeterminism at the boundary - // (the reference has the same freedom in `Store.tick`). Everything - // *inside* the component stays deterministic per scheduler.ts. - // - // The driver-arrival one-shot rides here too. This is the routine park of - // a quiet guest with a real host call outstanding — no speculative entry - // is held, so there is no wedge to break, but a fallback pump parked here - // would otherwise not reach its `done()` (i.e. its stand-down) until the - // HOST answered, leaving two loops interleaving `serviceSettled`/`tick` - // for that whole window. That interleaving is what the `driverDepth` note - // above calls out as bad for throughput and blame. - await Promise.race([ - ...store.pendingHostCalls, - armDriverArrival(store), - // ... and the host-call-arrival one-shot, because the spread above is a - // SNAPSHOT: a host call registered while we are parked here under no - // new driver (a sync `drive` export, `HostActivity.pump()`'s sync drain - // — neither fires a driver arrival) would otherwise be watched by - // nobody at all (the settlement pump stands down while we, the parked - // driver, keep `storeDriverDepth` positive). See the host-call arrival - // note above. - armHostCallArrival(store), - ]).catch(() => {}); - } } finally { const left = storeDriverDepth(store) - 1; driverDepth.set(store, left); @@ -1527,7 +1558,7 @@ const pendingLifts = new WeakMap void>>(); function registerPendingLift(inst: object, reject: (c: unknown) => void): void { let s = pendingLifts.get(inst); - if (s === undefined) pendingLifts.set(inst, (s = new Set())); + if (s === undefined) pendingLifts.set(inst, s = new Set()); s.add(reject); } @@ -1637,8 +1668,9 @@ export function createLiftedFunction(input: { // `Suspending`-wrapped. const enteredCore = enterWasm(core, mode); // See the comment at the `drive` call in `invokeNow` and `IdlePolicy`. - const idlePolicy: IdlePolicy = - ft.async === true && input.trapOnIdle !== true ? "exit" : "trap"; + const idlePolicy: IdlePolicy = ft.async === true && input.trapOnIdle !== true + ? "exit" + : "trap"; const taskOpts: TaskOptions = { async_: opts.async, callback: opts.callback !== null, @@ -2167,10 +2199,12 @@ async function awaitHopQuiescence(store: Store, inst: unknown): Promise { const hops = entryHopThreads(store, inst); if (hops.length === 0) return; await Promise.race( - hops.map((t) => (t.awaiting ?? Promise.resolve()).then( - () => undefined, - () => undefined, - )), + hops.map((t) => + (t.awaiting ?? Promise.resolve()).then( + () => undefined, + () => undefined, + ) + ), ); store.serviceSettled(); } @@ -2364,7 +2398,9 @@ export function* awaitCore( ), }; if (settled === undefined) return []; - return Array.isArray(settled) ? settled as CoreValue[] : [settled as CoreValue]; + return Array.isArray(settled) + ? settled as CoreValue[] + : [settled as CoreValue]; } return raw; } @@ -2713,18 +2749,18 @@ export function createLoweredImport(input: { // guest code in an unattributed chunk — the issue-#24 class the // attribution sentinels exist to prevent. let outcome: { value: unknown } | { error: unknown } | undefined; - // The async arm runs `onResolve` — result lowering, including possible - // realloc re-entry into the guest — in this bare promise continuation, - // where the sync arm above defers all CABI work to `produce` (the - // issue-#24 attribution note). The asymmetry is deliberate (#93): here - // no wasm frame is suspended mid-call — the guest returned BLOCKED and - // is between activations, which is exactly when the reference's - // `on_resolve` runs (the callee's turn), so there is no activation for - // the sentinels to attribute this chunk to. Lowering failures are host - // failures, not guest traps: they land on `store.hostFailure` and the - // driving loop raises them site-named (pinned by - // tests/async_lower_onresolve_failure_test.ts). - const promise = Promise.resolve(raw).then( + // The async arm runs `onResolve` — result lowering, including possible + // realloc re-entry into the guest — in this bare promise continuation, + // where the sync arm above defers all CABI work to `produce` (the + // issue-#24 attribution note). The asymmetry is deliberate (#93): here + // no wasm frame is suspended mid-call — the guest returned BLOCKED and + // is between activations, which is exactly when the reference's + // `on_resolve` runs (the callee's turn), so there is no activation for + // the sentinels to attribute this chunk to. Lowering failures are host + // failures, not guest traps: they land on `store.hostFailure` and the + // driving loop raises them site-named (pinned by + // tests/async_lower_onresolve_failure_test.ts). + const promise = Promise.resolve(raw).then( (v) => { store.pendingHostCalls.delete(promise); outcome = { value: v }; @@ -2916,7 +2952,6 @@ export function createLoweredImport(input: { }; } - /** * The callback-ABI dispatch loop of `canon_lift` (definitions.py lines * 2183-2214), factored out so both entry points share one implementation: diff --git a/runtime/src/exec/executor.ts b/runtime/src/exec/executor.ts index bf67363..51c32ba 100644 --- a/runtime/src/exec/executor.ts +++ b/runtime/src/exec/executor.ts @@ -15,22 +15,18 @@ import { ComponentInstanceState, Store } from "../task/mod.ts"; import { anySuspendingImport, assertModeConsistent, - type SuspendingImport, chooseMode, isAbortable, isDeferCancel, isSuspending, planNeedsSuspension, + type SuspendingImport, suspendingImport, + type SuspensionMode, trampolineCanBlock, trampolineNeedsSuspension, - type SuspensionMode, } from "../jspi/mod.ts"; -import { - loadPlan, - PlanError, - resourceIndexOfDefined, -} from "../plan/loader.ts"; +import { loadPlan, PlanError, resourceIndexOfDefined } from "../plan/loader.ts"; import { PendingCapability } from "../task/mod.ts"; import type { WireCanonicalOptions, @@ -42,7 +38,6 @@ import type { } from "../plan/format.ts"; import type { LoadedPlan, LoadedType } from "../plan/loader.ts"; import { - SYNC_ENTRY, type CoreFn, createDtorEntry, createLiftedFunction, @@ -51,13 +46,14 @@ import { LiveMemory, newStats, type ResolvedOptions, + SYNC_ENTRY, } from "./boundary.ts"; import { createTrampoline, createUnsafeIntrinsic, - type PreparedCall, - type HostTrapState, type FactStartScope, + type HostTrapState, + type PreparedCall, type SyncCallScope, TranscodeMemory, } from "../intrinsics/mod.ts"; @@ -668,9 +664,9 @@ class Executor { ); } seenAt.set(key, { index: i, value }); - (importObject[imp.module] ??= - {} as WebAssembly.ModuleImports)[imp.name] = - value as WebAssembly.ImportValue; + (importObject[imp.module] ??= {} as WebAssembly.ModuleImports)[ + imp.name + ] = value as WebAssembly.ImportValue; }); // Scoped strictly to the import list above: a CoreDef resolved by // any other initializer (extract-*, resource dtors) names no core @@ -938,22 +934,22 @@ class Executor { (value as unknown as Record)[ SYNC_ENTRY ] = createLiftedFunction({ - name: `${path} (sync entry)`, - ft, - opts, - core, - stats: this.stats, - suspensionMode: "plain", - trapState: this.trapState, - syncCallStack: this.syncCallStack, - allInstances: () => this.componentInstances.values(), - // sync() arm 2: a synchronous caller cannot be deferred by the - // hop-quiescence gate, so it refuses (SyncEntryBusy) instead. - // This deliberately changes constructor behaviour: the - // constructor sync entry previously bypassed the gate - // entirely, a latent lift-corruption window. - refuseOnEntryHops: true, - }); + name: `${path} (sync entry)`, + ft, + opts, + core, + stats: this.stats, + suspensionMode: "plain", + trapState: this.trapState, + syncCallStack: this.syncCallStack, + allInstances: () => this.componentInstances.values(), + // sync() arm 2: a synchronous caller cannot be deferred by the + // hop-quiescence gate, so it refuses (SyncEntryBusy) instead. + // This deliberately changes constructor behaviour: the + // constructor sync entry previously bypassed the gate + // entirely, a latent lift-corruption window. + refuseOnEntryHops: true, + }); } return { kind: "value", value }; } @@ -1371,9 +1367,9 @@ class Executor { for (const segment of path) { if (value === null || typeof value !== "object") { throw new PlanError( - `host import '${label}': '${ - [name, ...walked].join("/") - }' is ${describe(value)}, expected an object to read ` + + `host import '${label}': '${[name, ...walked].join("/")}' is ${ + describe(value) + }, expected an object to read ` + `'${segment}' from`, ); } diff --git a/runtime/src/exec/host_streams.ts b/runtime/src/exec/host_streams.ts index 7878107..c296063 100644 --- a/runtime/src/exec/host_streams.ts +++ b/runtime/src/exec/host_streams.ts @@ -698,11 +698,15 @@ class DirectSession implements DirectBuffer { } read(_n: number): PayloadChunk { - throw new Error("internal: a direct session must go through the direct-access byte edge seam"); + throw new Error( + "internal: a direct session must go through the direct-access byte edge seam", + ); } write(_vs: PayloadChunk): void { - throw new Error("internal: a direct session must go through the direct-access byte edge seam"); + throw new Error( + "internal: a direct session must go through the direct-access byte edge seam", + ); } // --- the direct protocol --- @@ -735,7 +739,9 @@ class DirectSession implements DirectBuffer { this.#fail( new TypeError( `a direct-access callback must return "more" or "done", got ` + - `${JSON.stringify(verdict)} (embedder-api.md §"Streams and futures" ("Direct-access byte edges"))`, + `${ + JSON.stringify(verdict) + } (embedder-api.md §"Streams and futures" ("Direct-access byte edges"))`, ), ); return "failed"; diff --git a/runtime/src/intrinsics/async_builtins.ts b/runtime/src/intrinsics/async_builtins.ts index 71d25af..89c7dea 100644 --- a/runtime/src/intrinsics/async_builtins.ts +++ b/runtime/src/intrinsics/async_builtins.ts @@ -48,8 +48,8 @@ import type { Cancelled } from "../task/mod.ts"; import { assert_, trap, trapIf } from "../cabi/trap.ts"; import { CoreValueIter, - LiftLowerContext, liftFlatValues, + LiftLowerContext, MAX_FLAT_PARAMS, store as storeValue, } from "../cabi/mod.ts"; @@ -287,7 +287,10 @@ export function createWaitableSetWait( // signed (F3, R2). Normalize at the entry boundary. si = (si ?? 0) >>> 0; ptr = (ptr ?? 0) >>> 0; - trapIf(!inst.mayLeave, "waitable-set.wait: cannot leave component instance"); + trapIf( + !inst.mayLeave, + "waitable-set.wait: cannot leave component instance", + ); const wset = requireWaitableSet(inst, si, "waitable-set.wait"); const task = currentTask() as Task; let event: EventTuple; @@ -363,7 +366,10 @@ export function createWaitableSetPoll( return (si?: number, ptr?: number) => { si = (si ?? 0) >>> 0; ptr = (ptr ?? 0) >>> 0; - trapIf(!inst.mayLeave, "waitable-set.poll: cannot leave component instance"); + trapIf( + !inst.mayLeave, + "waitable-set.poll: cannot leave component instance", + ); const wset = requireWaitableSet(inst, si, "waitable-set.poll"); const event = wset.poll(currentTask(), cancellable); return unpackEvent(opts, inst, ptr, event); @@ -375,7 +381,10 @@ export function createWaitableSetDrop(inst: ComponentInstanceState): CoreFn { return (i?: number) => { // Guest-supplied index is u32; core wasm delivers i32 args signed (F3, R2). i = (i ?? 0) >>> 0; - trapIf(!inst.mayLeave, "waitable-set.drop: cannot leave component instance"); + trapIf( + !inst.mayLeave, + "waitable-set.drop: cannot leave component instance", + ); const wset = inst.handles.remove(i); trapIf( !(wset instanceof WaitableSet), diff --git a/runtime/src/intrinsics/context.ts b/runtime/src/intrinsics/context.ts index 4a11192..3e93603 100644 --- a/runtime/src/intrinsics/context.ts +++ b/runtime/src/intrinsics/context.ts @@ -57,11 +57,13 @@ export function ctxThreadId(t: unknown): string { } function trace(msg: string, thread: unknown): void { const a = ambientDebug(); - console.error(`[ctx] ${ctxThreadId(thread)} ${msg} storage=${ - JSON.stringify((thread as CurrentThreadLike).storage) - } | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${ - a.claims.map(ctxThreadId).join(",") - }]`); + console.error( + `[ctx] ${ctxThreadId(thread)} ${msg} storage=${ + JSON.stringify((thread as CurrentThreadLike).storage) + } | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${ + a.claims.map(ctxThreadId).join(",") + }]`, + ); } /** definitions.py `canon_context_set` (line 2358). */ diff --git a/runtime/src/intrinsics/fact_calls.ts b/runtime/src/intrinsics/fact_calls.ts index 2b883c1..5f1fd18 100644 --- a/runtime/src/intrinsics/fact_calls.ts +++ b/runtime/src/intrinsics/fact_calls.ts @@ -74,9 +74,10 @@ import { type BlockRequest, type Cancelled, ComponentInstanceState, - NeedsJspi, currentTask, + entryRefusal, maybeCurrentTask, + NeedsJspi, needsJspi, notifyInstancePoisoned, packSubtaskResult, @@ -86,7 +87,6 @@ import { Task, type TaskOptions, Thread, - entryRefusal, } from "../task/mod.ts"; import { blockCurrentActivation, enterWasm } from "../jspi/mod.ts"; import { @@ -250,8 +250,16 @@ export function createPrepareCall( args.length >= PREPARE_FIXED, `prepare-call: expected at least ${PREPARE_FIXED} arguments`, ); - const [start, return_, callerI, calleeI, taskReturnType, calleeAsync, enc, rc_] = - args; + const [ + start, + return_, + callerI, + calleeI, + taskReturnType, + calleeAsync, + enc, + rc_, + ] = args; assert_( typeof start === "function" && typeof return_ === "function", "prepare-call: start/return must be funcrefs", @@ -359,8 +367,13 @@ function mkCalleeTask(input: { * `deliverResolve` releases them); sync-start-call passes a scope it * releases when the blocked caller frame gets its results. */ - lenderScope: { addLender(h: import("../cabi/handles.ts").ResourceHandle): void }; -}): { task: Task; body: (t: Thread) => Generator } { + lenderScope: { + addLender(h: import("../cabi/handles.ts").ResourceHandle): void; + }; +}): { + task: Task; + body: (t: Thread) => Generator; +} { const { prepared, callee, callback, postReturn, ctx, calleeUsesAsyncAbi } = input; // CONTRACT: default to `plain` when the context predates this field. Only diff --git a/runtime/src/intrinsics/mod.ts b/runtime/src/intrinsics/mod.ts index 58a25f9..e696986 100644 --- a/runtime/src/intrinsics/mod.ts +++ b/runtime/src/intrinsics/mod.ts @@ -25,7 +25,12 @@ import { trapIf } from "../cabi/trap.ts"; import { assert_ } from "../cabi/trap.ts"; import type { ResourceTypeInfo } from "../cabi/types.ts"; import type { ComponentInstanceState } from "../task/mod.ts"; -import { dbgId, entryRefusal, maybeCurrentThread, maybeCurrentTask } from "../task/mod.ts"; +import { + dbgId, + entryRefusal, + maybeCurrentTask, + maybeCurrentThread, +} from "../task/mod.ts"; import type { WireTrampoline } from "../plan/format.ts"; import type { CoreFn, ExecutionStats } from "../exec/boundary.ts"; import { UnsupportedFeatureError } from "./errors.ts"; @@ -52,15 +57,18 @@ import { type PreparedCall, } from "./fact_calls.ts"; import { + type AsyncTransferContext, createErrorContextDebugMessage, createErrorContextDrop, createErrorContextNew, + createErrorContextTransfer, createFutureCancelRead, createFutureCancelWrite, createFutureDropReadable, createFutureDropWritable, createFutureNew, createFutureRead, + createFutureTransfer, createFutureWrite, createStreamCancelRead, createStreamCancelWrite, @@ -68,18 +76,15 @@ import { createStreamDropWritable, createStreamNew, createStreamRead, + createStreamTransfer, createStreamWrite, type StreamTrampolineContext, - type AsyncTransferContext, - createStreamTransfer, - createFutureTransfer, - createErrorContextTransfer, } from "./stream_builtins.ts"; import { createTranscoder, + TRANSCODE_OPS, type TranscodeMemory, type TranscodeOp, - TRANSCODE_OPS, } from "./transcode.ts"; export * from "./transcode.ts"; @@ -149,7 +154,10 @@ const TRAP_UNCAUGHT_EXCEPTION = 49; export { UnsupportedFeatureError } from "./errors.ts"; /** Capability at which each trampoline kind stops instantiate-failing. */ -const TRAMPOLINE_CAPABILITY: Record = { +const TRAMPOLINE_CAPABILITY: Record< + string, + "core" | "resources" | "task-core" +> = { "lower-import": "core", "trap": "core", "enter-sync-call": "core", @@ -299,7 +307,9 @@ export interface TrampolineContext { * `prepare-call` passes as `task_return_type`, or `null` when the plan * carries no mapping for it (see `LoadedPlan.resultTupleTypes`). */ - resultTypesForTuple(tupleIndex: number): import("../cabi/types.ts").ValType[] | null; + resultTypesForTuple( + tupleIndex: number, + ): import("../cabi/types.ts").ValType[] | null; /** Build the lowered-import body for `lowered` (LoweredIndex). */ loweredImport(decl: { lowered: number; @@ -390,7 +400,9 @@ function syncScopes(ctx: TrampolineContext, site = "?"): any[] { const scopes = thread?.syncCallStack ?? ctx.syncCallStack; if (SCOPE_TRACE) { console.error( - `[scope] ${site} act=${thread ? dbgId(thread) : "NONE(->ctx fallback)"} ` + + `[scope] ${site} act=${ + thread ? dbgId(thread) : "NONE(->ctx fallback)" + } ` + `depth=${scopes.length}`, ); } @@ -661,7 +673,10 @@ function createTrampolineBody( ); case "async-start-call": return createAsyncStartCall( - decl as unknown as { callback: number | null; postReturn: number | null }, + decl as unknown as { + callback: number | null; + postReturn: number | null; + }, ctx as unknown as FactCallContext, ); @@ -679,33 +694,89 @@ function createTrampolineBody( declaredInstance(decl, ctx), ); case "stream-read": - return createStreamRead(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createStreamRead( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "stream-write": - return createStreamWrite(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createStreamWrite( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "future-read": - return createFutureRead(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createFutureRead( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "future-write": - return createFutureWrite(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createFutureWrite( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "stream-cancel-read": - return createStreamCancelRead(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createStreamCancelRead( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "stream-cancel-write": - return createStreamCancelWrite(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createStreamCancelWrite( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "future-cancel-read": - return createFutureCancelRead(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createFutureCancelRead( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "future-cancel-write": - return createFutureCancelWrite(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createFutureCancelWrite( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "stream-drop-readable": - return createStreamDropReadable(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createStreamDropReadable( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "stream-drop-writable": - return createStreamDropWritable(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createStreamDropWritable( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "future-drop-readable": - return createFutureDropReadable(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createFutureDropReadable( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "future-drop-writable": - return createFutureDropWritable(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createFutureDropWritable( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "error-context-new": - return createErrorContextNew(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createErrorContextNew( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "error-context-debug-message": - return createErrorContextDebugMessage(decl as never, sctx(ctx), declaredInstance(decl, ctx)); + return createErrorContextDebugMessage( + decl as never, + sctx(ctx), + declaredInstance(decl, ctx), + ); case "error-context-drop": return createErrorContextDrop(declaredInstance(decl, ctx)); case "stream-transfer": @@ -740,7 +811,6 @@ function createTrampolineBody( } } - // --------------------------------------------------------------------------- // Resource transfer (FACT `resource.transfer-own` / `transfer-borrow`) // --------------------------------------------------------------------------- diff --git a/runtime/src/intrinsics/stream_builtins.ts b/runtime/src/intrinsics/stream_builtins.ts index d3a22e9..e89d8ca 100644 --- a/runtime/src/intrinsics/stream_builtins.ts +++ b/runtime/src/intrinsics/stream_builtins.ts @@ -43,13 +43,17 @@ import { needsJspi, ReadableFutureEnd, ReadableStreamEnd, + sameElemType as sameElem, SharedFutureImpl, SharedStreamImpl, - sameElemType as sameElem, WritableFutureEnd, WritableStreamEnd, } from "../task/mod.ts"; -import { cabiOptions, type CoreFn, type ResolvedOptions } from "../exec/boundary.ts"; +import { + cabiOptions, + type CoreFn, + type ResolvedOptions, +} from "../exec/boundary.ts"; import { BLOCKED } from "./async_builtins.ts"; /** @@ -186,9 +190,7 @@ function streamCopy(input: { ): EventTuple => { reclaim(); assert_(end.copying(), "stream event on a non-copying end"); - end.state = result === CopyResult.DROPPED - ? CopyState.DONE - : CopyState.IDLE; + end.state = result === CopyResult.DROPPED ? CopyState.DONE : CopyState.IDLE; assert_( buffer.progress <= BUFFER_MAX_LENGTH, "stream progress out of packing range", @@ -527,7 +529,10 @@ export function createErrorContextNew( return (ptr?: number, taggedCodeUnits?: number) => { ptr = (ptr ?? 0) >>> 0; taggedCodeUnits = (taggedCodeUnits ?? 0) >>> 0; - trapIf(!inst.mayLeave, "error-context.new: cannot leave component instance"); + trapIf( + !inst.mayLeave, + "error-context.new: cannot leave component instance", + ); const cx = new LiftLowerContext(cabiOptions(opts), inst, null); const s = loadStringFromRange(cx, ptr, taggedCodeUnits); return inst.handles.add(new ErrorContext(s)); @@ -845,7 +850,10 @@ function transferAsyncEnd(input: { trapIf(!(e instanceof EndT), `${what}: handle is not a readable ${what} end`); const end = e as CopyEnd; trapIf(!sameElem(end.shared.t, srcElem), `${what}: source element mismatch`); - trapIf(!sameElem(end.shared.t, dstElem), `${what}: destination element mismatch`); + trapIf( + !sameElem(end.shared.t, dstElem), + `${what}: destination element mismatch`, + ); // definitions.py `lift_async_value`: an end that is mid-copy or parked in a // waitable set cannot be handed on. The messages match the suite's // `assert_trap` text. diff --git a/runtime/src/jspi/bridge.ts b/runtime/src/jspi/bridge.ts index 03b3b6e..0578548 100644 --- a/runtime/src/jspi/bridge.ts +++ b/runtime/src/jspi/bridge.ts @@ -42,13 +42,18 @@ // results to become Promises where cabi needs a number synchronously. import { assert_ } from "../cabi/trap.ts"; -import { type SuspendingImport, isSupported, makePromising, makeSuspending } from "./mechanics.ts"; import { - withActivation, + isSupported, + makePromising, + makeSuspending, + type SuspendingImport, +} from "./mechanics.ts"; +import { claimActivationAmbient, dbgId, maybeCurrentThread, releaseActivationAmbient, + withActivation, } from "../task/mod.ts"; import type { Cancelled, SchedulableThread, Store } from "../task/mod.ts"; @@ -201,7 +206,9 @@ export function enterWasm unknown>( ): T { if (mode === "plain") return fn; assert_(isSupported(), "jspi mode selected on an engine without JSPI"); - return makePromising(fn as unknown as (...a: unknown[]) => unknown) as unknown as T; + return makePromising( + fn as unknown as (...a: unknown[]) => unknown, + ) as unknown as T; } /** @@ -283,7 +290,10 @@ const SENTINEL_TICK = Promise.resolve(); /** Wrap a suspending import's thenable so the eventual resumption chunk is * preceded contiguously by its attribution sentinel. */ -function attributeContinuation(owner: unknown, r: PromiseLike): Promise { +function attributeContinuation( + owner: unknown, + r: PromiseLike, +): Promise { return Promise.resolve(r).then( (v) => { sentinelFor(owner); @@ -306,7 +316,8 @@ export function suspendingImport unknown>( // The activation calling us — read while its bracket (or its hop claim) // is still the ambient. const owner = maybeCurrentThread() ?? null; - const invoke = () => (fn as unknown as (...a: unknown[]) => unknown)(...args); + const invoke = () => + (fn as unknown as (...a: unknown[]) => unknown)(...args); let r: unknown; try { // Bracket our own JS frame with the caller. Without this, a built-in @@ -336,7 +347,9 @@ export function suspendingImport unknown>( return r; } if (SP_TRACE) { - console.error(`[sp] hop-suspend owner=${dbgId(owner)} promise=${dbgId(r)}`); + console.error( + `[sp] hop-suspend owner=${dbgId(owner)} promise=${dbgId(r)}`, + ); } return attributeContinuation(owner, r as PromiseLike); }; @@ -468,7 +481,11 @@ export class SuspensionPoint implements SchedulableThread { this.#store = store; this.owner = owner ?? maybeCurrentThread() ?? task?.implicitThread ?? null; if (SP_TRACE) { - console.error(`[sp] mint ${dbgId(this)} owner=${dbgId(this.owner)} task=${dbgId(this.task)}\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`); + console.error( + `[sp] mint ${dbgId(this)} owner=${dbgId(this.owner)} task=${ + dbgId(this.task) + }\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`, + ); } this.promise = new Promise((res, rej) => { this.#settle = res; @@ -518,7 +535,11 @@ export class SuspensionPoint implements SchedulableThread { "cancelled resume of a non-cancellable suspension point", ); if (SP_TRACE) { - console.error(`[sp] resume ${dbgId(this)} owner=${dbgId(this.owner)}\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`); + console.error( + `[sp] resume ${dbgId(this)} owner=${dbgId(this.owner)}\n${ + (new Error().stack ?? "").split("\n").slice(2, 5).join("\n") + }`, + ); } this.#done = true; // AFTER the block (definitions.py `Thread.wait_until` line 372, ported to @@ -572,7 +593,9 @@ export class SuspensionPoint implements SchedulableThread { // gone with the single-slot claim (#158 mechanism B), but the entry must // still be retired here or the store stays gated on a finished window. this.#store.consumePendingIfRunning(); - if (maybeCurrentThread() === undefined) claimActivationAmbient(this.owner); + if (maybeCurrentThread() === undefined) { + claimActivationAmbient(this.owner); + } this.#store.addPendingResumption(this.task?.implicitThread ?? null); this.#fail(e); return; diff --git a/runtime/src/jspi/mechanics.ts b/runtime/src/jspi/mechanics.ts index 2fff309..15a7331 100644 --- a/runtime/src/jspi/mechanics.ts +++ b/runtime/src/jspi/mechanics.ts @@ -45,7 +45,9 @@ import { jspiApi } from "./types.ts"; * exactly a `WebAssembly.Suspending`, usable anywhere an import value is * expected. */ -export type SuspendingImport = { readonly __polyengineSuspending: unique symbol }; +export type SuspendingImport = { + readonly __polyengineSuspending: unique symbol; +}; /** True if the current engine implements `WebAssembly.promising` and * `WebAssembly.Suspending`. Both are phase-4 API surface (docs/architecture.md §3): no diff --git a/runtime/src/plan/loader.ts b/runtime/src/plan/loader.ts index 13a4701..8065ece 100644 --- a/runtime/src/plan/loader.ts +++ b/runtime/src/plan/loader.ts @@ -197,7 +197,9 @@ export function loadPlan(wire: WirePlan): LoadedPlan { wire.initializers.forEach((init, i) => validateInitializer(init, `initializers[${i}]`) ); - wire.trampolines.forEach((t, i) => validateTrampoline(t, `trampolines[${i}]`)); + wire.trampolines.forEach((t, i) => + validateTrampoline(t, `trampolines[${i}]`) + ); wire.canonicalOptions.forEach((o, i) => validateCanonicalOptions(o, `canonicalOptions[${i}]`) ); @@ -285,7 +287,10 @@ export function loadPlan(wire: WirePlan): LoadedPlan { } resultTupleTypes.set(decl.results, decl.resultType); } - const elems = (ts: { element: WireValType | null }[] | undefined, what: string) => + const elems = ( + ts: { element: WireValType | null }[] | undefined, + what: string, + ) => (ts ?? []).map((t, i) => t.element === null ? null @@ -383,7 +388,11 @@ function expect( if (!cond) throw new PlanError(`${where}: ${what}`); } -function expectNumber(o: Record, field: string, where: string) { +function expectNumber( + o: Record, + field: string, + where: string, +) { expect( typeof o[field] === "number", where, @@ -403,7 +412,11 @@ function expectNumberOrNull( ); } -function expectString(o: Record, field: string, where: string) { +function expectString( + o: Record, + field: string, + where: string, +) { expect( typeof o[field] === "string", where, @@ -429,7 +442,11 @@ function expectNonNegativeInt( ); } -function expectBoolean(o: Record, field: string, where: string) { +function expectBoolean( + o: Record, + field: string, + where: string, +) { expect( typeof o[field] === "boolean", where, @@ -475,7 +492,9 @@ function validateCoreDef(def: unknown, where: string): void { expectString(d, "intrinsic", where); return; default: - throw new PlanError(`${where}: unknown CoreDef kind ${describeValue(d.kind)}`); + throw new PlanError( + `${where}: unknown CoreDef kind ${describeValue(d.kind)}`, + ); } } @@ -503,7 +522,11 @@ function validateCoreExport(exp: unknown, where: string): void { } function validateInitializer(init: unknown, where: string): void { - expect(isRecord(init), where, `must be an object, got ${describeValue(init)}`); + expect( + isRecord(init), + where, + `must be an object, got ${describeValue(init)}`, + ); const i = init as Record; expectString(i, "op", where); switch (i.op) { @@ -549,7 +572,9 @@ function validateInitializer(init: unknown, where: string): void { expectNumber(i, "instance", where); return; default: - throw new PlanError(`${where}: unknown initializer op ${describeValue(i.op)}`); + throw new PlanError( + `${where}: unknown initializer op ${describeValue(i.op)}`, + ); } } @@ -617,7 +642,12 @@ function validateCanonicalOptions(o: unknown, where: string): void { const ct = co.coreType as Record; expectArray(ct, "params", `${where}.coreType`); expectArray(ct, "results", `${where}.coreType`); - for (const [field, lanes] of [["params", ct.params], ["results", ct.results]] as const) { + for ( + const [field, lanes] of [["params", ct.params], [ + "results", + ct.results, + ]] as const + ) { (lanes as unknown[]).forEach((lane, idx) => { expect( typeof lane === "string" && CORE_TYPE_LANES.has(lane), @@ -652,12 +682,18 @@ function validateModule(m: unknown, where: string): void { ); return; default: - throw new PlanError(`${where}: unknown module kind ${describeValue(mm.kind)}`); + throw new PlanError( + `${where}: unknown module kind ${describeValue(mm.kind)}`, + ); } } function validateIntrinsicEntry(entry: unknown, where: string): void { - expect(isRecord(entry), where, `must be an object, got ${describeValue(entry)}`); + expect( + isRecord(entry), + where, + `must be an object, got ${describeValue(entry)}`, + ); const e = entry as Record; expectString(e, "module", where); expectString(e, "name", where); @@ -719,7 +755,9 @@ function validateExport(exp: unknown, where: string): void { expectNumber(e, "module", where); return; default: - throw new PlanError(`${where}: unknown export kind ${describeValue(e.kind)}`); + throw new PlanError( + `${where}: unknown export kind ${describeValue(e.kind)}`, + ); } } @@ -735,7 +773,9 @@ function validateTypeExport(t: unknown, where: string): void { expectNumber(tt, "type", where); return; default: - throw new PlanError(`${where}: unknown type-export kind ${describeValue(tt.kind)}`); + throw new PlanError( + `${where}: unknown type-export kind ${describeValue(tt.kind)}`, + ); } } diff --git a/runtime/src/shim/translator.ts b/runtime/src/shim/translator.ts index ca4d1ed..6aad7f7 100644 --- a/runtime/src/shim/translator.ts +++ b/runtime/src/shim/translator.ts @@ -63,7 +63,10 @@ export class Translator { module = source; } else { module = await WebAssembly.compile(source.slice().buffer as ArrayBuffer); - const digest = await crypto.subtle.digest("SHA-256", source.slice().buffer as ArrayBuffer); + const digest = await crypto.subtle.digest( + "SHA-256", + source.slice().buffer as ArrayBuffer, + ); buildHash = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0") ).join(""); diff --git a/runtime/src/task/mod.ts b/runtime/src/task/mod.ts index 60957f6..f29479d 100644 --- a/runtime/src/task/mod.ts +++ b/runtime/src/task/mod.ts @@ -21,12 +21,12 @@ import { type Cancelled, CANCELLED_TRUE, chooseCandidate, - isInstancePoisoned, - Store, dbgId, + isInstancePoisoned, NeedsJspi, notifyInstancePoisoned, PendingCapability, + Store, } from "./scheduler.ts"; import { Thread } from "./thread.ts"; import { Waitable, WaitableSet } from "./waitable.ts"; @@ -91,7 +91,6 @@ export class ComponentInstanceState implements ComponentInstanceLike { set mayLeave(v: boolean) { this.flags.value = v ? 1 : 0; } - } /** definitions.py `Task.State` (line 445). */ diff --git a/runtime/src/task/thread.ts b/runtime/src/task/thread.ts index 81b520e..80eefe9 100644 --- a/runtime/src/task/thread.ts +++ b/runtime/src/task/thread.ts @@ -22,9 +22,9 @@ import { type Cancelled, CANCELLED_FALSE, CANCELLED_TRUE, + isInstancePoisoned, NeedsJspi, notifyInstancePoisoned, - isInstancePoisoned, PendingCapability, popCurrentThread, pushCurrentThread, @@ -144,7 +144,10 @@ export class Thread implements SchedulableThread { /** Resume a promise-parked thread with the settled result. */ resumeWith(value: unknown, failure?: { error: unknown }): void { - assert_(this.awaiting !== null, "resumeWith on a thread that is not awaiting"); + assert_( + this.awaiting !== null, + "resumeWith on a thread that is not awaiting", + ); this.awaiting = null; this.#store.awaiting.delete(this); this.#state = "suspended"; @@ -287,7 +290,9 @@ export class Thread implements SchedulableThread { } /** definitions.py `Thread.suspend` (line 390). */ - *suspend(cancellable: boolean): Generator { + *suspend( + cancellable: boolean, + ): Generator { assert_(this.running(), "suspend on a non-running thread"); if (this.task.deliverPendingCancel(cancellable)) return CANCELLED_TRUE; const cancelled = yield { readyFunc: null, cancellable }; diff --git a/runtime/src/task/waitable.ts b/runtime/src/task/waitable.ts index 6309992..29a16fb 100644 --- a/runtime/src/task/waitable.ts +++ b/runtime/src/task/waitable.ts @@ -20,7 +20,6 @@ export enum EventCode { /** definitions.py `EventTuple` = `(EventCode, int, int)`. */ export type EventTuple = [code: EventCode, p1: number, p2: number]; - /** * definitions.py `class Waitable` (line 767). * @@ -85,7 +84,10 @@ export class Waitable { /** definitions.py `Waitable.drop` (line 805). */ drop(): void { - assert_(!this.hasPendingEvent(), "dropping a waitable with a pending event"); + assert_( + !this.hasPendingEvent(), + "dropping a waitable with a pending event", + ); assert_(!this.hasSyncWaiter, "dropping a waitable with a sync waiter"); this.join(null); } diff --git a/runtime/tests/async_builtins_test.ts b/runtime/tests/async_builtins_test.ts index 7961e16..9f42047 100644 --- a/runtime/tests/async_builtins_test.ts +++ b/runtime/tests/async_builtins_test.ts @@ -349,9 +349,17 @@ Deno.test("stream.cancel-write supersedes an undelivered COMPLETED", () => { // deno-lint-ignore no-explicit-any const newStream = createStreamNew({ streamTable: 0 }, ctx as any, inst); // deno-lint-ignore no-explicit-any - const write = createStreamWrite({ streamTable: 0, options: 0 }, ctx as any, inst); + const write = createStreamWrite( + { streamTable: 0, options: 0 }, + ctx as any, + inst, + ); // deno-lint-ignore no-explicit-any - const read = createStreamRead({ streamTable: 0, options: 0 }, ctx as any, inst); + const read = createStreamRead( + { streamTable: 0, options: 0 }, + ctx as any, + inst, + ); const cancelWrite = createStreamCancelWrite( { streamTable: 0, async: true }, // deno-lint-ignore no-explicit-any diff --git a/runtime/tests/async_lower_onresolve_failure_test.ts b/runtime/tests/async_lower_onresolve_failure_test.ts index 568bcf0..fb25fb2 100644 --- a/runtime/tests/async_lower_onresolve_failure_test.ts +++ b/runtime/tests/async_lower_onresolve_failure_test.ts @@ -23,8 +23,8 @@ import { } from "../src/exec/boundary.ts"; import { ComponentInstanceState, - pushCurrentThread, popCurrentThread, + pushCurrentThread, Store, Task, type TaskOptions, @@ -120,7 +120,9 @@ Deno.test( // uncaught exception, and not delivered to the guest through the // subtask's SUBTASK event. assertEq(store.hostFailure !== undefined, true); - const msg = String((store.hostFailure as { message?: string })?.message ?? store.hostFailure); + const msg = String( + (store.hostFailure as { message?: string })?.message ?? store.hostFailure, + ); assertEq(msg.includes("realloc required but not provided"), true); // Consumed: the subtask never resolved, and the driving loop is the one // responsible for rethrowing `store.hostFailure` — pinning that plumbing diff --git a/runtime/tests/async_lower_test.ts b/runtime/tests/async_lower_test.ts index a1d74ce..120075d 100644 --- a/runtime/tests/async_lower_test.ts +++ b/runtime/tests/async_lower_test.ts @@ -22,8 +22,8 @@ import { ComponentInstanceState, EventCode, NeedsJspi, - pushCurrentThread, popCurrentThread, + pushCurrentThread, Store, Subtask, SubtaskState, @@ -34,7 +34,10 @@ import { WaitableSet, } from "../src/task/mod.ts"; import type { FuncType } from "../src/cabi/types.ts"; -import { BLOCKED, createSubtaskCancel } from "../src/intrinsics/async_builtins.ts"; +import { + BLOCKED, + createSubtaskCancel, +} from "../src/intrinsics/async_builtins.ts"; // cancellation discard: the cancel-discard opt-out, read off the host function exactly as // `executor.ts buildLoweredImport` reads it from the embedder's imports record. import { deferCancel, isDeferCancel } from "../src/jspi/suspending.ts"; @@ -248,12 +251,18 @@ Deno.test("sync lower of a Promise-returning host import needs JSPI", () => { mode: "plain", suspendable: false, }); - const task = new Task(syncFt, { - async_: false, - callback: false, - stringEncoding: "utf8", - memory: null, - }, inst, () => [], () => {}); + const task = new Task( + syncFt, + { + async_: false, + callback: false, + stringEncoding: "utf8", + memory: null, + }, + inst, + () => [], + () => {}, + ); const thread = new Thread(task, (function* () {})()); pushCurrentThread(thread); let raised: unknown; diff --git a/runtime/tests/bindgen/instantiate_test.ts b/runtime/tests/bindgen/instantiate_test.ts index c976fbd..b1fd64d 100644 --- a/runtime/tests/bindgen/instantiate_test.ts +++ b/runtime/tests/bindgen/instantiate_test.ts @@ -59,11 +59,20 @@ Deno.test("generated instantiate: matching component verifies and binds", async const instance = await hello.instantiate(artifacts(helloWasm)); // Typed exports work through the wrapper's return value... - assertEq(await instance.exports.greet("component model"), "Hello, component model!"); + assertEq( + await instance.exports.greet("component model"), + "Hello, component model!", + ); // ...and the embedder-conventions instance shape is preserved // (contracts/embedder-api.md: `{ exports, handle, imports }`). - assert(instance.handle !== undefined, "wrapper must expose the runtime handle"); - assert(Array.isArray(instance.imports), "wrapper must expose the import leaves"); + assert( + instance.handle !== undefined, + "wrapper must expose the runtime handle", + ); + assert( + Array.isArray(instance.imports), + "wrapper must expose the import leaves", + ); }); Deno.test("generated instantiate: also accepts an untranslated source", async () => { @@ -108,7 +117,11 @@ Deno.test("generated bind(): unchecked cast, no verification", () => { // `bind` stays a pure cast (identity on `.exports`) — it must NOT verify, // even when handed an instance whose plan could never match this world. const exports = { greet: () => "not really" }; - const fake = { exports, handle: null, imports: [] } as unknown as EmbedderInstance; + const fake = { + exports, + handle: null, + imports: [], + } as unknown as EmbedderInstance; const bound = hello.bind(fake); assert( bound === (exports as unknown as hello.HelloExports), diff --git a/runtime/tests/bindgen/usage/values_usage.ts b/runtime/tests/bindgen/usage/values_usage.ts index c53a523..821f022 100644 --- a/runtime/tests/bindgen/usage/values_usage.ts +++ b/runtime/tests/bindgen/usage/values_usage.ts @@ -103,7 +103,9 @@ export function useValues(instance: EmbedderInstance) { ValuesExports["echoOptionNested"], ( v: { kind: "some"; value: number } | { kind: "none" } | undefined, - ) => Promise<{ kind: "some"; value: number } | { kind: "none" } | undefined> + ) => Promise< + { kind: "some"; value: number } | { kind: "none" } | undefined + > > >; const none: ReturnType extends diff --git a/runtime/tests/boundary_trap_test.ts b/runtime/tests/boundary_trap_test.ts index 96f81e0..c5f2f88 100644 --- a/runtime/tests/boundary_trap_test.ts +++ b/runtime/tests/boundary_trap_test.ts @@ -12,16 +12,42 @@ import { callCore } from "../src/exec/boundary.ts"; /** A real `WebAssembly.Module` whose sole export unconditionally traps. */ function unreachableCoreFn(): (...args: unknown[]) => unknown { const wat = new Uint8Array([ - 0x00, 0x61, 0x73, 0x6d, // \0asm - 0x01, 0x00, 0x00, 0x00, // version 1 + 0x00, + 0x61, + 0x73, + 0x6d, // \0asm + 0x01, + 0x00, + 0x00, + 0x00, // version 1 // type section: () -> () - 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, + 0x01, + 0x04, + 0x01, + 0x60, + 0x00, + 0x00, // function section: 1 function of type 0 - 0x03, 0x02, 0x01, 0x00, + 0x03, + 0x02, + 0x01, + 0x00, // export section: export "f" as function 0 - 0x07, 0x05, 0x01, 0x01, 0x66, 0x00, 0x00, + 0x07, + 0x05, + 0x01, + 0x01, + 0x66, + 0x00, + 0x00, // code section: body = unreachable; end - 0x0a, 0x05, 0x01, 0x03, 0x00, 0x00, 0x0b, + 0x0a, + 0x05, + 0x01, + 0x03, + 0x00, + 0x00, + 0x0b, ]); const mod = new WebAssembly.Module(wat); const inst = new WebAssembly.Instance(mod, {}); diff --git a/runtime/tests/bulk_list_test.ts b/runtime/tests/bulk_list_test.ts index 7ac22f2..933cce1 100644 --- a/runtime/tests/bulk_list_test.ts +++ b/runtime/tests/bulk_list_test.ts @@ -93,7 +93,12 @@ Deno.test("bulk lists: 64-bit kinds are bigint-shaped and wrap mod 2^64", () => Deno.test("bulk lists: bool normalizes by truthiness, lifts nonzero as true", () => { // store(): `Number(Boolean(v))` accepted ANY value — pin that. const { bytes, lifted } = storeAndReadBack( - [true, false, 2 as unknown as ComponentValue, "" as unknown as ComponentValue], + [ + true, + false, + 2 as unknown as ComponentValue, + "" as unknown as ComponentValue, + ], listOf("bool"), ); assertEq([...bytes], [1, 0, 1, 0], "stored bytes normalized"); @@ -113,7 +118,10 @@ Deno.test("bulk lists: bool normalizes by truthiness, lifts nonzero as true", () Deno.test("bulk lists: floats round-trip, f32 narrows like setFloat32", () => { const f64 = storeAndReadBack([0.5, -0, 1e308, 5e-324], listOf("f64")); assertEq(f64.lifted, [0.5, -0, 1e308, 5e-324], "f64 exact"); - const f32 = storeAndReadBack([0.5, 1.1, -3.4028234663852886e38], listOf("f32")); + const f32 = storeAndReadBack( + [0.5, 1.1, -3.4028234663852886e38], + listOf("f32"), + ); assertEq( f32.lifted, [0.5, Math.fround(1.1), -3.4028234663852886e38], diff --git a/runtime/tests/cache_test.ts b/runtime/tests/cache_test.ts index 013758e..bc2284a 100644 --- a/runtime/tests/cache_test.ts +++ b/runtime/tests/cache_test.ts @@ -107,7 +107,11 @@ async function roundTrip(cache: ArtifactCache, label: string) { adapters: second.adapters, }); const greet = component.exports.greet as (name: string) => string; - assertEq(greet("cache"), "Hello, cache!", `${label}: cached artifacts still work`); + assertEq( + greet("cache"), + "Hello, cache!", + `${label}: cached artifacts still work`, + ); } Deno.test("dirCache: round-trip, cache hit skips the translator entirely", async () => { @@ -317,7 +321,10 @@ class FaultyCache implements ArtifactCache { failGet: boolean; failPut: boolean; - constructor(inner: ArtifactCache, opts: { failGet?: boolean; failPut?: boolean } = {}) { + constructor( + inner: ArtifactCache, + opts: { failGet?: boolean; failPut?: boolean } = {}, + ) { this.#inner = inner; this.failGet = opts.failGet ?? false; this.failPut = opts.failPut ?? false; @@ -355,7 +362,10 @@ Deno.test("translateCached: cache.get throwing reads as a miss, translation stil assertEq(result.fromCache, false, "get failure -> fresh translate"); assertEq(spy.translateCalls, 1, "exactly one translation"); assertEq(reportedOp, "get"); - assert(reportedErr instanceof Error, "onCacheError received the original error"); + assert( + reportedErr instanceof Error, + "onCacheError received the original error", + ); }); Deno.test("translateCached: cache.put throwing is swallowed, fresh result still returned", async () => { @@ -375,7 +385,10 @@ Deno.test("translateCached: cache.put throwing is swallowed, fresh result still assertEq(result.fromCache, false); assertEq(spy.translateCalls, 1, "exactly one translation"); assertEq(reportedOp, "put"); - assert(reportedErr instanceof Error, "onCacheError received the original error"); + assert( + reportedErr instanceof Error, + "onCacheError received the original error", + ); // The result artifacts themselves are still usable, despite the failed store. const component = await instantiateComponent({ plan: result.plan, @@ -389,7 +402,10 @@ Deno.test("translateCached: cache.put throwing is swallowed, fresh result still Deno.test("translateCached: onCacheError itself throwing does not fail the translation", async () => { const translator = await Translator.create(shimWasm); const spy = new SpyTranslator(translator); - const cache = new FaultyCache(dirCache(await tmpDir()), { failGet: true, failPut: true }); + const cache = new FaultyCache(dirCache(await tmpDir()), { + failGet: true, + failPut: true, + }); const result = await translateCached(spy, helloWasm, cache, { onCacheError: () => { @@ -430,7 +446,12 @@ Deno.test("dirCache: put against an ENOTDIR root still throws (backend stays hon let threw = false; try { - await cache.put(key, { plan: (await translateCached(translator, helloWasm, dirCache(await tmpDir()))).plan, adapters: new Map() }); + await cache.put(key, { + plan: + (await translateCached(translator, helloWasm, dirCache(await tmpDir()))) + .plan, + adapters: new Map(), + }); } catch { threw = true; } @@ -484,7 +505,11 @@ Deno.test("dirCache: read-only cache root (deployment recipe) still serves a war const spy = new SpyTranslator(translator); const result = await translateCached(spy, helloWasm, cache); - assertEq(result.fromCache, true, "warm hit must still work with a read-only root"); + assertEq( + result.fromCache, + true, + "warm hit must still work with a read-only root", + ); assertEq(spy.translateCalls, 0); const direct = await cache.get(key); diff --git a/runtime/tests/cancel_bracket_race_test.ts b/runtime/tests/cancel_bracket_race_test.ts index 0bccea8..4f96999 100644 --- a/runtime/tests/cancel_bracket_race_test.ts +++ b/runtime/tests/cancel_bracket_race_test.ts @@ -16,7 +16,11 @@ import { assertEq } from "./support/asserts.ts"; import { createWaitableSetWait } from "../src/intrinsics/async_builtins.ts"; -import { createLiftedFunction, newStats, type ResolvedOptions } from "../src/exec/boundary.ts"; +import { + createLiftedFunction, + newStats, + type ResolvedOptions, +} from "../src/exec/boundary.ts"; import { entryRefusal } from "../src/task/scheduler.ts"; import { ComponentInstanceState, diff --git a/runtime/tests/conventions/error_context_test.ts b/runtime/tests/conventions/error_context_test.ts index 1d0dd21..371be9f 100644 --- a/runtime/tests/conventions/error_context_test.ts +++ b/runtime/tests/conventions/error_context_test.ts @@ -24,7 +24,8 @@ const FIXTURE = local("error-context-relay"); const ready = await haveFixture(FIXTURE); Deno.test({ - name: "conventions/g: a lifted error-context is branded and carries its message", + name: + "conventions/g: a lifted error-context is branded and carries its message", ignore: !ready, fn: async () => { await transcript("g-error-context-lift", async (t) => { @@ -98,7 +99,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/g: isErrorContext accepts a hand-rolled carrier, rejects a husk", + name: + "conventions/g: isErrorContext accepts a hand-rolled carrier, rejects a husk", fn: async () => { await transcript("g-error-context-predicate", async (t) => { // The vocabulary claim on its own: recognition is brand + string diff --git a/runtime/tests/conventions/errors_test.ts b/runtime/tests/conventions/errors_test.ts index d62a57f..ffb137f 100644 --- a/runtime/tests/conventions/errors_test.ts +++ b/runtime/tests/conventions/errors_test.ts @@ -28,7 +28,8 @@ const HOST_PAYLOAD = "runtime/tests/embedder/host-result-payload.wasm"; const valuesReady = await haveFixture(guest("values")); Deno.test({ - name: "conventions/e: a guest err-result lifts as ComponentException(payload)", + name: + "conventions/e: a guest err-result lifts as ComponentException(payload)", ignore: !valuesReady, fn: async () => { await transcript("e-guest-err-lifts", async (t) => { @@ -36,7 +37,10 @@ Deno.test({ // `echo-result: func(v: result) -> result`. // As a VALUE (parameter position) a result is plain `{kind, value}` // data that never throws; in RESULT position the same value throws. - await t.attempt("ok", () => c.exports.echoResult({ kind: "ok", value: 5 })); + await t.attempt( + "ok", + () => c.exports.echoResult({ kind: "ok", value: 5 }), + ); await t.attempt( "err", () => c.exports.echoResult({ kind: "err", value: "boom" }), @@ -48,7 +52,8 @@ Deno.test({ const emptyReady = await haveFixture(HOST_RESULT); Deno.test({ - name: "conventions/e: host ComponentException -> guest err (payloadless side)", + name: + "conventions/e: host ComponentException -> guest err (payloadless side)", ignore: !emptyReady, fn: async () => { await transcript("e-host-throw-empty", async (t) => { @@ -99,7 +104,8 @@ Deno.test({ const payloadReady = await haveFixture(HOST_PAYLOAD); Deno.test({ - name: "conventions/e: host ComponentException payload lowers into the err case", + name: + "conventions/e: host ComponentException payload lowers into the err case", ignore: !payloadReady, fn: async () => { await transcript("e-host-throw-payload", async (t) => { @@ -134,7 +140,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/e: predicates recognize a hand-rolled exception, either copy", + name: + "conventions/e: predicates recognize a hand-rolled exception, either copy", fn: async () => { await transcript("e-brand-recognition", async (t) => { // No engine involved: the vocabulary claim itself. A hand-rolled brand @@ -157,7 +164,8 @@ Deno.test({ const passReady = await haveFixture(guest("stream-pass")); Deno.test({ - name: "conventions/e: a peer TRAP surfaces as PeerTrappedError, not clean EOS", + name: + "conventions/e: a peer TRAP surfaces as PeerTrappedError, not clean EOS", ignore: !passReady, fn: async () => { await transcript("e-peer-trapped", async (t) => { diff --git a/runtime/tests/conventions/imports_shape_test.ts b/runtime/tests/conventions/imports_shape_test.ts index 4ebb603..7a2f5ff 100644 --- a/runtime/tests/conventions/imports_shape_test.ts +++ b/runtime/tests/conventions/imports_shape_test.ts @@ -74,7 +74,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/a: suspending mark — an interface member's receiver is its provider", + name: + "conventions/a: suspending mark — an interface member's receiver is its provider", ignore: !importsReady, fn: async () => { await transcript("a-interface-receiver", async (t) => { diff --git a/runtime/tests/conventions/lifting_test.ts b/runtime/tests/conventions/lifting_test.ts index 52f3404..c315dc2 100644 --- a/runtime/tests/conventions/lifting_test.ts +++ b/runtime/tests/conventions/lifting_test.ts @@ -39,7 +39,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/c: a lifted stream chunks as Uint8Array — async iteration", + name: + "conventions/c: a lifted stream chunks as Uint8Array — async iteration", ignore: !passReady, fn: async () => { await transcript("c-lift-stream-u8-iterate", async (t) => { @@ -126,7 +127,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/c: awaiting a DROPPED-without-value future rejects DroppedError", + name: + "conventions/c: awaiting a DROPPED-without-value future rejects DroppedError", ignore: !futureUserReady, fn: async () => { await transcript("c-lift-future-dropped", async (t) => { diff --git a/runtime/tests/conventions/lowering_test.ts b/runtime/tests/conventions/lowering_test.ts index 1c978aa..819d92e 100644 --- a/runtime/tests/conventions/lowering_test.ts +++ b/runtime/tests/conventions/lowering_test.ts @@ -130,7 +130,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/b: a Stream handle lowered into an IMPORT reaches the host", + name: + "conventions/b: a Stream handle lowered into an IMPORT reaches the host", ignore: !passReady, fn: async () => { await transcript("b-stream-handle-import-position", async (t) => { @@ -160,7 +161,8 @@ Deno.test({ const futureImportReady = await haveFixture(guest("future-import")); Deno.test({ - name: "conventions/b: future import result — an import whose result is future returns the source", + name: + "conventions/b: future import result — an import whose result is future returns the source", ignore: !futureImportReady, fn: async () => { await transcript("b-a12-future-result-import", async (t) => { diff --git a/runtime/tests/conventions/resources_test.ts b/runtime/tests/conventions/resources_test.ts index 9f3527a..4a72682 100644 --- a/runtime/tests/conventions/resources_test.ts +++ b/runtime/tests/conventions/resources_test.ts @@ -17,7 +17,8 @@ import { Cell, Gauge, MathProvider } from "./probe.ts"; const resReady = await haveFixture(testdata("imported-resource")); Deno.test({ - name: "conventions/d: a plain class IS the resource; own out, borrow in, dtor on drop", + name: + "conventions/d: a plain class IS the resource; own out, borrow in, dtor on drop", ignore: !resReady, fn: async () => { await transcript("d-host-resource-plain-class", async (t) => { @@ -46,8 +47,10 @@ Deno.test({ t.note("effects", { events, disposed: Cell.disposed }); // `make-and-keep` leaves the handle ALIVE in the guest: no dispose yet. - const h = await t.attempt("make-and-keep", () => - c.exports.makeAndKeep(9)) as number; + const h = await t.attempt( + "make-and-keep", + () => c.exports.makeAndKeep(9), + ) as number; t.note("before-guest-drop", { disposed: Cell.disposed }); // …and `drop-handle` runs the destructor, right there. await t.attempt("drop-handle", () => c.exports.dropHandle(h)); @@ -87,7 +90,8 @@ Deno.test({ const importsReady = await haveFixture(testdata("imports")); Deno.test({ - name: "conventions/d: suspending mark — a class instance is a legal interface provider", + name: + "conventions/d: suspending mark — a class instance is a legal interface provider", ignore: !importsReady, fn: async () => { await transcript("d-interface-provider-class", async (t) => { diff --git a/runtime/tests/conventions/suspending_test.ts b/runtime/tests/conventions/suspending_test.ts index 206de52..4c08b5c 100644 --- a/runtime/tests/conventions/suspending_test.ts +++ b/runtime/tests/conventions/suspending_test.ts @@ -12,9 +12,14 @@ // Promise is refused, naming `suspending()`. Silent degradation is what the // declaration exists to prevent. -import { haveFixture, instantiateFixture, jspiSupported, testdata } from "./harness.ts"; +import { + haveFixture, + instantiateFixture, + jspiSupported, + testdata, +} from "./harness.ts"; import { transcript } from "./support.ts"; -import { Gauge, SuspendingGauge, suspending } from "./probe.ts"; +import { Gauge, suspending, SuspendingGauge } from "./probe.ts"; import { handRolledSuspending } from "./probe_zero_import.ts"; /** @@ -26,10 +31,12 @@ function later(value: T): Promise { return new Promise((r) => setTimeout(() => r(value), 0)); } -const importsReady = (await haveFixture(testdata("imports"))) && jspiSupported(); +const importsReady = (await haveFixture(testdata("imports"))) && + jspiSupported(); Deno.test({ - name: "conventions/f: a MARKED sync-typed import parks and resumes with the value", + name: + "conventions/f: a MARKED sync-typed import parks and resumes with the value", ignore: !importsReady, fn: async () => { await transcript("f-suspending-plain-import", async (t) => { @@ -88,7 +95,8 @@ Deno.test({ }); Deno.test({ - name: "conventions/f: an explicit jspi:false refuses a MARKED import's Promise", + name: + "conventions/f: an explicit jspi:false refuses a MARKED import's Promise", ignore: !importsReady, fn: async () => { await transcript("f-suspending-jspi-false", async (t) => { @@ -111,7 +119,8 @@ const GAUGE = "runtime/tests/embedder/suspending-method.wasm"; const gaugeReady = (await haveFixture(GAUGE)) && jspiSupported(); Deno.test({ - name: "conventions/f: suspending mark — a mark on the class PROTOTYPE relays to instances", + name: + "conventions/f: suspending mark — a mark on the class PROTOTYPE relays to instances", ignore: !gaugeReady, fn: async () => { await transcript("f-suspending-prototype-relay", async (t) => { diff --git a/runtime/tests/cross_store_driver_test.ts b/runtime/tests/cross_store_driver_test.ts index 8c16021..91c389c 100644 --- a/runtime/tests/cross_store_driver_test.ts +++ b/runtime/tests/cross_store_driver_test.ts @@ -30,8 +30,7 @@ function assert(cond: boolean, msg: string): asserts cond { } function fakeInst() { - return { - }; + return {}; } /** A thread parked on an awaitValue promise, as a promising-wrapped guest diff --git a/runtime/tests/digest_test.ts b/runtime/tests/digest_test.ts index 1eff978..5baaa4d 100644 --- a/runtime/tests/digest_test.ts +++ b/runtime/tests/digest_test.ts @@ -75,7 +75,9 @@ async function expectDigestError( } if (!e.message.includes(messageIncludes)) { throw new Error( - `expected DigestError message to include ${JSON.stringify(messageIncludes)}, got: ${e.message}`, + `expected DigestError message to include ${ + JSON.stringify(messageIncludes) + }, got: ${e.message}`, ); } } @@ -95,9 +97,12 @@ async function readEnvelope(name: string) { // Rust side — the two implementations were developed against the same // fixture corpus and must never be "fixed" independently of one another). const EXPECTED = { - hello: "sha256:04ae5eb2633ff22f5af8c5e9234c18d089e80a99e04b0946929f0a2e3f5ad7c9", - values: "sha256:e0791536cb4b9731057b82831150611eed64f22d665130a02f247d3227e2e4a7", - resources: "sha256:d72d1754bca4332fb3a5e21526872d28c0914f54253979e3bc8ab8e1e083b4d4", + hello: + "sha256:04ae5eb2633ff22f5af8c5e9234c18d089e80a99e04b0946929f0a2e3f5ad7c9", + values: + "sha256:e0791536cb4b9731057b82831150611eed64f22d665130a02f247d3227e2e4a7", + resources: + "sha256:d72d1754bca4332fb3a5e21526872d28c0914f54253979e3bc8ab8e1e083b4d4", }; for (const world of ["hello", "values", "resources"] as const) { diff --git a/runtime/tests/driver_poisoned_probe_test.ts b/runtime/tests/driver_poisoned_probe_test.ts index 3614bcd..52776ce 100644 --- a/runtime/tests/driver_poisoned_probe_test.ts +++ b/runtime/tests/driver_poisoned_probe_test.ts @@ -57,7 +57,11 @@ Deno.test({ store.startWaiting(sp as any); let finished = false; - const driving = driveStoreAsync(store, () => finished, "export 'abandoned'"); + const driving = driveStoreAsync( + store, + () => finished, + "export 'abandoned'", + ); let outcome: { ok: true } | { err: unknown } | undefined; driving.then(() => (outcome = { ok: true }), (e) => (outcome = { err: e })); diff --git a/runtime/tests/dtor_normalization_test.ts b/runtime/tests/dtor_normalization_test.ts index 207f690..2f73a20 100644 --- a/runtime/tests/dtor_normalization_test.ts +++ b/runtime/tests/dtor_normalization_test.ts @@ -90,9 +90,12 @@ Deno.test("#160/#173: a dtor may run while its own instance is LIVE", async () = // A SECOND, synchronous dtor of the same instance, entered while the first // is still in flight: it simply runs (CM#705). let ranNested = 0; - const quick = new ResourceTypeInfo(impl, (() => { - ranNested += 1; - }) as unknown as (rep: number) => void); + const quick = new ResourceTypeInfo( + impl, + (() => { + ranNested += 1; + }) as unknown as (rep: number) => void, + ); hostDtorCall(quick, 6); assertEq(ranNested, 1, "the nested dtor ran; nothing was refused"); diff --git a/runtime/tests/embedder/cancel_import_test.ts b/runtime/tests/embedder/cancel_import_test.ts index eca8c9f..8c7e99b 100644 --- a/runtime/tests/embedder/cancel_import_test.ts +++ b/runtime/tests/embedder/cancel_import_test.ts @@ -41,7 +41,9 @@ async function instantiateGuest() { return await instantiateFixture(guest("cancel-import"), { sleep: (ms: bigint) => delay(Number(ms)), block: (_ms: bigint) => { - throw new Error("cancel-import cancellation discard tests never call `block`"); + throw new Error( + "cancel-import cancellation discard tests never call `block`", + ); }, "sleep-defer": deferCancel((ms: bigint) => delay(Number(ms))), timers: { diff --git a/runtime/tests/embedder/cross_copy_test.ts b/runtime/tests/embedder/cross_copy_test.ts index fc67c0c..0b8c000 100644 --- a/runtime/tests/embedder/cross_copy_test.ts +++ b/runtime/tests/embedder/cross_copy_test.ts @@ -20,8 +20,15 @@ import { runtimeCopies, } from "@polyengine/protocol"; import { COPY_URL, RUNTIME_VERSION } from "../../src/embedder/mod.ts"; -import { lowerFutureSource, lowerStreamSource } from "../../src/embedder/streams.ts"; -import { initWrapper, takeRep, wrapperState } from "../../src/embedder/resources.ts"; +import { + lowerFutureSource, + lowerStreamSource, +} from "../../src/embedder/streams.ts"; +import { + initWrapper, + takeRep, + wrapperState, +} from "../../src/embedder/resources.ts"; import { GuestResource } from "../../src/embedder/mod.ts"; import { fromHost } from "../../src/embedder/values.ts"; @@ -32,7 +39,10 @@ const CODEC = { }; /** A value carrying a brand but minted by nobody this copy knows. */ -function foreign(brandKey: string, props: Record = {}): object { +function foreign( + brandKey: string, + props: Record = {}, +): object { class Foreign {} Object.defineProperty(Foreign.prototype, Symbol.for(brandKey), { value: true, @@ -68,7 +78,9 @@ Deno.test("module identity: a foreign Stream is refused at lowering, not pumped // The silent path module identity bans: without the brand check this object would fall // through to producer adaptation. const src = foreign("polyengine.stream/1", { - [Symbol.asyncIterator]: () => ({ next: () => Promise.resolve({ done: true }) }), + [Symbol.asyncIterator]: () => ({ + next: () => Promise.resolve({ done: true }), + }), }); const e = caught(() => lowerStreamSource(src as never, CODEC as never)); assertTrue(e instanceof TypeError, `TypeError, got ${e}`); @@ -86,7 +98,10 @@ Deno.test("module identity: a foreign Future is refused, not silently adopted as assertTrue(e instanceof TypeError, `TypeError, got ${e}`); const m = String((e as Error).message); assertTrue(m.includes("DIFFERENT polyengine runtime copy"), m); - assertTrue(m.includes("Promise.resolve(f)"), "names the by-value remediation"); + assertTrue( + m.includes("Promise.resolve(f)"), + "names the by-value remediation", + ); }); Deno.test("module identity: a foreign error-context is named cross-copy, not 'expected an ErrorContext' (realm boundary: only without a string message)", () => { @@ -97,7 +112,11 @@ Deno.test("module identity: a foreign error-context is named cross-copy, not 'ex // which is a genuinely foreign stateful handle, not a message carrier. const v = foreign("polyengine.errorContext/1", { message: 42 }); const e = caught(() => - fromHost(v, { kind: "error-context" } as never, { where: "export 'f'" } as never) + fromHost( + v, + { kind: "error-context" } as never, + { where: "export 'f'" } as never, + ) ); const m = String((e as Error).message); assertTrue(m.includes("DIFFERENT polyengine runtime copy"), m); @@ -106,7 +125,11 @@ Deno.test("module identity: a foreign error-context is named cross-copy, not 'ex Deno.test("module identity: an unbranded value at a handle site keeps its original diagnosis", () => { const e = caught(() => - fromHost({}, { kind: "error-context" } as never, { where: "export 'f'" } as never) + fromHost( + {}, + { kind: "error-context" } as never, + { where: "export 'f'" } as never, + ) ); assertTrue(String((e as Error).message).includes("expected an ErrorContext")); }); @@ -115,7 +138,9 @@ Deno.test("module identity: a foreign resource wrapper is named cross-copy, not // Same brand KEY, a foreign copy's state object (whose SHAPE we must never // read — the module identity table pins only the key). const w = new GuestResource(); - (w as unknown as Record)[Symbol.for("polyengine.resourceState/1")] = { + (w as unknown as Record)[ + Symbol.for("polyengine.resourceState/1") + ] = { copyUrl: "file:///some/other/copy/mod.ts", rep: 7, valid: true, @@ -161,12 +186,17 @@ Deno.test("module identity: the census is empty for a single-copy graph and name try { const c = copyCensus(); assertTrue(c.startsWith("2 polyengine copies loaded: "), c); - assertTrue(c.includes(COPY_URL) && c.includes("file:///fake/second/copy.mjs"), c); + assertTrue( + c.includes(COPY_URL) && c.includes("file:///fake/second/copy.mjs"), + c, + ); // And the cross-copy messages pick it up. const e = caught(() => lowerStreamSource(foreign("polyengine.stream/1") as never, CODEC as never) ); - assertTrue(String((e as Error).message).includes("file:///fake/second/copy.mjs")); + assertTrue( + String((e as Error).message).includes("file:///fake/second/copy.mjs"), + ); } finally { // Leave the census clean for the rest of the suite. const copies = (globalThis as unknown as Record)[ diff --git a/runtime/tests/embedder/direct_streams_test.ts b/runtime/tests/embedder/direct_streams_test.ts index 37793bd..e34776a 100644 --- a/runtime/tests/embedder/direct_streams_test.ts +++ b/runtime/tests/embedder/direct_streams_test.ts @@ -33,7 +33,8 @@ function assert(cond: boolean, msg: string): asserts cond { } Deno.test({ - name: "direct-access byte edge e2e: StreamWriter.writeDirect feeds a real guest", + name: + "direct-access byte edge e2e: StreamWriter.writeDirect feeds a real guest", ignore: !ready, fn: async () => { // `take: async func(input: stream, count: u32) -> u64` reads `count` @@ -130,7 +131,8 @@ Deno.test({ }); Deno.test({ - name: "direct-access byte edge: a non-u8 element type is refused on both direct forms", + name: + "direct-access byte edge: a non-u8 element type is refused on both direct forms", ignore: !ready, fn: async () => { // `pass-through-text` is `stream`: the writer parks until that diff --git a/runtime/tests/embedder/future_result_test.ts b/runtime/tests/embedder/future_result_test.ts index 1f646cd..989b5a5 100644 --- a/runtime/tests/embedder/future_result_test.ts +++ b/runtime/tests/embedder/future_result_test.ts @@ -70,7 +70,8 @@ Deno.test({ }); Deno.test({ - name: "futures: the tcp-receive shape — tuple, future> from one sync import", + name: + "futures: the tcp-receive shape — tuple, future> from one sync import", ignore: !have, async fn() { const c = await instantiateFixture(FIXTURE, { @@ -99,7 +100,8 @@ Deno.test({ }); Deno.test({ - name: "futures: a rejecting future-source Promise is a producer failure, not an err value", + name: + "futures: a rejecting future-source Promise is a producer failure, not an err value", ignore: !have, async fn() { const c = await instantiateFixture(FIXTURE, { diff --git a/runtime/tests/embedder/host_imports_test.ts b/runtime/tests/embedder/host_imports_test.ts index 80f0ef0..2225b05 100644 --- a/runtime/tests/embedder/host_imports_test.ts +++ b/runtime/tests/embedder/host_imports_test.ts @@ -8,7 +8,12 @@ // the host has to keep its own identity table — the friction this layer deletes. import { assertEq } from "../support/asserts.ts"; -import { caught, haveFixture, instantiateFixture, testdata } from "./support.ts"; +import { + caught, + haveFixture, + instantiateFixture, + testdata, +} from "./support.ts"; import { ComponentException, Trap } from "@polyengine/protocol"; import { INTERNAL_HOST_REGISTRIES } from "../../src/embedder/instantiate.ts"; @@ -196,7 +201,11 @@ Deno.test({ `the trap must name the import leaf: ${e}`, ); assertEq(String(e).includes("TypeError"), true, `${e}`); - assertEq(String(e).includes("ComponentException"), true, "…and say how to signal err"); + assertEq( + String(e).includes("ComponentException"), + true, + "…and say how to signal err", + ); }, }); @@ -279,7 +288,8 @@ Deno.test({ }); Deno.test({ - name: "error model: ComponentException's PAYLOAD reaches the guest's err case", + name: + "error model: ComponentException's PAYLOAD reaches the guest's err case", ignore: !payloadReady, fn: async () => { // The whole branded-throw path end to end: `throw new ComponentException("boom")` @@ -307,7 +317,8 @@ Deno.test({ }); Deno.test({ - name: "error model: an unbranded throw from a FALLIBLE import is still a trap", + name: + "error model: an unbranded throw from a FALLIBLE import is still a trap", ignore: !payloadReady, fn: async () => { // Having an err side does not make a stray platform error into one. @@ -324,10 +335,13 @@ Deno.test({ // B2: a borrow of a never-registered host instance is CALL-SCOPED // --------------------------------------------------------------------------- -const borrowReady = await haveFixture("runtime/tests/embedder/host-borrow.wasm"); +const borrowReady = await haveFixture( + "runtime/tests/embedder/host-borrow.wasm", +); Deno.test({ - name: "host resources: a borrow-allocated rep is released when the call returns", + name: + "host resources: a borrow-allocated rep is released when the call returns", ignore: !borrowReady, fn: async () => { // contracts/embedder-api.md 2x4 table, bottom-right: "a @@ -339,9 +353,12 @@ Deno.test({ "runtime/tests/embedder/host-borrow.wasm", { "host:api/res": { R: Cell, value: (r: Cell) => r.v } }, ); - const registries = (c as unknown as Record>)[INTERNAL_HOST_REGISTRIES]; + const registries = (c as unknown as Record< + symbol, + Map + >)[INTERNAL_HOST_REGISTRIES]; const registry = registries.get(0)!; assertEq(registry.liveCount, 0, "nothing registered yet"); @@ -361,7 +378,8 @@ Deno.test({ }); Deno.test({ - name: "host resources: an own-registered instance survives the call, as owned", + name: + "host resources: an own-registered instance survives the call, as owned", ignore: !borrowReady, fn: async () => { // The other half of the rule: only a rep minted *for* the borrow is @@ -370,11 +388,14 @@ Deno.test({ "runtime/tests/embedder/host-borrow.wasm", { "host:api/res": { R: Cell, value: (r: Cell) => r.v } }, ); - const registries = (c as unknown as Record>)[INTERNAL_HOST_REGISTRIES]; + const registries = (c as unknown as Record< + symbol, + Map + >)[INTERNAL_HOST_REGISTRIES]; const registry = registries.get(0)!; const cell = new Cell(5); const rep = registry.repFor(cell); // as if the guest had been given an own @@ -390,7 +411,8 @@ Deno.test({ }); Deno.test({ - name: "host resources: a resource import with no importedResources table is loud", + name: + "host resources: a resource import with no importedResources table is loud", ignore: !borrowReady, fn: async () => { const { artifactsOf } = await import("./support.ts"); diff --git a/runtime/tests/embedder/long_poll_test.ts b/runtime/tests/embedder/long_poll_test.ts index df5ea47..d1eb503 100644 --- a/runtime/tests/embedder/long_poll_test.ts +++ b/runtime/tests/embedder/long_poll_test.ts @@ -37,7 +37,8 @@ for (const jspi of [false, true]) { const tag = jspi ? "jspi" : "plain"; Deno.test({ - name: `long-poll (${tag}): next() stays pending until a later push() readies it`, + name: + `long-poll (${tag}): next() stays pending until a later push() readies it`, ignore: !ready, fn: async () => { const c = await instantiateFixture(FIXTURE, {}, { jspi }); @@ -70,7 +71,8 @@ for (const jspi of [false, true]) { }); Deno.test({ - name: `long-poll (${tag}): a trap while completing next() rejects the pending Promise`, + name: + `long-poll (${tag}): a trap while completing next() rejects the pending Promise`, ignore: !ready, fn: async () => { const c = await instantiateFixture(FIXTURE, {}, { jspi }); @@ -126,9 +128,7 @@ Deno.test({ adapters, trapOnIdle: true, }); - const err = await caught(() => - (handle.exports["next"] as () => unknown)() - ); + const err = await caught(() => (handle.exports["next"] as () => unknown)()); assertEq( String(err).includes("deadlock detected"), true, diff --git a/runtime/tests/embedder/passthrough_test.ts b/runtime/tests/embedder/passthrough_test.ts index 6024173..9e5287d 100644 --- a/runtime/tests/embedder/passthrough_test.ts +++ b/runtime/tests/embedder/passthrough_test.ts @@ -10,7 +10,12 @@ // `pass-through-text` does the same for stream. import { assertEq } from "../support/asserts.ts"; -import { artifactsOf, guest, haveFixture, instantiateFixture } from "./support.ts"; +import { + artifactsOf, + guest, + haveFixture, + instantiateFixture, +} from "./support.ts"; import type { ComponentValue, ValType } from "../../src/cabi/types.ts"; import { SharedFutureImpl } from "../../src/task/mod.ts"; import { Future, Stream } from "../../src/embedder/streams.ts"; @@ -25,7 +30,8 @@ const FIXTURE = guest("stream-pass"); const ready = await haveFixture(FIXTURE); Deno.test({ - name: "pass-through: result position — data flows host->host after the round trip", + name: + "pass-through: result position — data flows host->host after the round trip", ignore: !ready, fn: async () => { const c = await instantiateFixture(FIXTURE, { sink: () => 0n }); @@ -143,12 +149,17 @@ Deno.test({ await (h.exports["forward"] as (...a: unknown[]) => Promise)( hs2.value, ); - assertEq(received === hs2.value, true, "import position: same shared object"); + assertEq( + received === hs2.value, + true, + "import position: same shared object", + ); }, }); Deno.test({ - name: "guest-side partial take: a bounded reader drains part of a big typed offer", + name: + "guest-side partial take: a bounded reader drains part of a big typed offer", ignore: !ready, fn: async () => { // #63 review F3 / #67 checklist: the host offers far more than the guest @@ -184,7 +195,8 @@ Deno.test({ }); Deno.test({ - name: "host<->host u8: partial reads drain a typed write; re-offers stay typed", + name: + "host<->host u8: partial reads drain a typed write; re-offers stay typed", ignore: false, fn: async () => { // Exercises both halves of the typed-chunk write path (review F3): @@ -216,7 +228,8 @@ Deno.test({ }); Deno.test({ - name: "host<->host u8: a raw plain-array writer still reads back as Uint8Array", + name: + "host<->host u8: a raw plain-array writer still reads back as Uint8Array", ignore: false, fn: async () => { // Raw-layer writers may feed number[]; HostBuffer.taken() packs them so @@ -257,7 +270,8 @@ async function assertTransferRefusal( } Deno.test({ - name: "deadlock-verdict suppression: reading a Stream handle already passed to a guest is refused", + name: + "deadlock-verdict suppression: reading a Stream handle already passed to a guest is refused", ignore: !ready, fn: async () => { // The guest owns the readable end after the transfer (definitions.py @@ -310,7 +324,8 @@ Deno.test({ }); Deno.test({ - name: "deadlock-verdict suppression: awaiting a Future handle already passed to a guest is refused", + name: + "deadlock-verdict suppression: awaiting a Future handle already passed to a guest is refused", ignore: false, fn: async () => { // The `Stream` mirror, at the handle layer (no fixture needed): once @@ -331,7 +346,8 @@ Deno.test({ }); Deno.test({ - name: "deadlock-verdict suppression: a Future read memoized BEFORE the transfer still resolves", + name: + "deadlock-verdict suppression: a Future read memoized BEFORE the transfer still resolves", ignore: false, fn: async () => { // The read genuinely happened while the host owned the end; only reads diff --git a/runtime/tests/embedder/platform_class_test.ts b/runtime/tests/embedder/platform_class_test.ts index 31bcbe7..e7a6dd5 100644 --- a/runtime/tests/embedder/platform_class_test.ts +++ b/runtime/tests/embedder/platform_class_test.ts @@ -41,7 +41,8 @@ const WEB = { }; Deno.test({ - name: "platform class: happy path — native URLSearchParams/TextDecoder with no wrapper", + name: + "platform class: happy path — native URLSearchParams/TextDecoder with no wrapper", ignore: !ready, fn: async () => { const c = await instantiateFixture(FIXTURE, WEB); @@ -64,7 +65,8 @@ Deno.test({ }); Deno.test({ - name: "platform class: option-limit — a missing key surfaces null, which fails (not none)", + name: + "platform class: option-limit — a missing key surfaces null, which fails (not none)", ignore: !ready, fn: async () => { // `URLSearchParams.prototype.get` returns `null` for a missing key. The @@ -92,7 +94,8 @@ Deno.test({ }); Deno.test({ - name: "platform class: getter limit — `size` is a property, not a method, and traps", + name: + "platform class: getter limit — `size` is a property, not a method, and traps", ignore: !ready, fn: async () => { // `URLSearchParams.prototype.size` is an accessor (getter), so @@ -110,7 +113,8 @@ Deno.test({ }); Deno.test({ - name: "platform class: a native platform exception traps, even from a result-typed import", + name: + "platform class: a native platform exception traps, even from a result-typed import", ignore: !ready, fn: async () => { // `fatal: true` + invalid UTF-8 makes native TextDecoder.prototype.decode @@ -123,7 +127,11 @@ Deno.test({ const e = await caught(() => c.exports.probeDecode(true, new Uint8Array([0xff])) ); - assertEq(isTrap(e), true, `expected a Trap (native throw is unbranded), got ${e}`); + assertEq( + isTrap(e), + true, + `expected a Trap (native throw is unbranded), got ${e}`, + ); assertEq( e instanceof ComponentException, false, @@ -133,7 +141,8 @@ Deno.test({ }); Deno.test({ - name: "platform class: the one-line wrapper recipe turns a native throw into a WIT err", + name: + "platform class: the one-line wrapper recipe turns a native throw into a WIT err", ignore: !ready, fn: async () => { // Contrast pin for the previous test: wrapping just the fallible method @@ -158,7 +167,11 @@ Deno.test({ const e = await caught(() => c.exports.probeDecode(true, new Uint8Array([0xff])) ); - assertEq(e instanceof ComponentException, true, `expected ComponentException, got ${e}`); + assertEq( + e instanceof ComponentException, + true, + `expected ComponentException, got ${e}`, + ); const payload = (e as ComponentException).payload; assertEq( typeof payload === "string" && payload.length > 0, diff --git a/runtime/tests/embedder/resource_stream_test.ts b/runtime/tests/embedder/resource_stream_test.ts index 6338279..618c082 100644 --- a/runtime/tests/embedder/resource_stream_test.ts +++ b/runtime/tests/embedder/resource_stream_test.ts @@ -48,7 +48,8 @@ function ticketSource(count: number): AsyncIterable { } Deno.test({ - name: "resource streams: own elements arrive live; each guest drop runs the dtor", + name: + "resource streams: own elements arrive live; each guest drop runs the dtor", ignore: !have, async fn() { reset(); @@ -68,7 +69,8 @@ Deno.test({ }); Deno.test({ - name: "resource streams: un-taken elements are released when the reader drops", + name: + "resource streams: un-taken elements are released when the reader drops", ignore: !have, async fn() { reset(); diff --git a/runtime/tests/embedder/resources_test.ts b/runtime/tests/embedder/resources_test.ts index 0bcd5f9..e4c0a36 100644 --- a/runtime/tests/embedder/resources_test.ts +++ b/runtime/tests/embedder/resources_test.ts @@ -20,7 +20,8 @@ async function counters(): Promise { } Deno.test({ - name: "resources: the interface exposes a PascalCase class and camelCase funcs", + name: + "resources: the interface exposes a PascalCase class and camelCase funcs", ignore: !ready, fn: async () => { const c = await counters(); diff --git a/runtime/tests/embedder/start_imports_test.ts b/runtime/tests/embedder/start_imports_test.ts index d301e75..34cecda 100644 --- a/runtime/tests/embedder/start_imports_test.ts +++ b/runtime/tests/embedder/start_imports_test.ts @@ -104,6 +104,9 @@ Deno.test({ const { requiredImports } = await import("../../src/embedder/mod.ts"); const leaves = requiredImports(await artifactsOf(FIXTURE)); assertEq(leaves.map((l) => l.jsName).sort(), ["note", "tick"]); - assertEq(leaves.find((l) => l.jsName === "tick")?.type?.results[0].kind, "u64"); + assertEq( + leaves.find((l) => l.jsName === "tick")?.type?.results[0].kind, + "u64", + ); }, }); diff --git a/runtime/tests/embedder/streams_test.ts b/runtime/tests/embedder/streams_test.ts index 91d2d7b..171f1a1 100644 --- a/runtime/tests/embedder/streams_test.ts +++ b/runtime/tests/embedder/streams_test.ts @@ -4,12 +4,12 @@ import { assertEq } from "../support/asserts.ts"; import { caught, guest, haveFixture, instantiateFixture } from "./support.ts"; import { DroppedError, StreamProducerError } from "@polyengine/protocol"; -import { - Future, - Stream, -} from "../../src/embedder/streams.ts"; +import { Future, Stream } from "../../src/embedder/streams.ts"; import { hostStream, hostStreamFor } from "../../src/exec/mod.ts"; -import { LiftLowerContext, mkCanonicalOptions } from "../../src/cabi/context.ts"; +import { + LiftLowerContext, + mkCanonicalOptions, +} from "../../src/cabi/context.ts"; import { Table } from "../../src/cabi/handles.ts"; import { liftStream } from "../../src/cabi/async_values.ts"; import { ReadableStreamEnd, SharedStreamImpl } from "../../src/task/mod.ts"; @@ -104,7 +104,11 @@ Deno.test({ const c = await instantiateFixture(guest("stream-echo")); const { stream: input, writer } = Stream.create(); const out = await c.exports.echoDoubled(input); - assertEq(out instanceof Stream, true, "lifted stream is a Stream handle"); + assertEq( + out instanceof Stream, + true, + "lifted stream is a Stream handle", + ); const feed = (async () => { await writer.writeAll([1, 2, 3]); @@ -165,7 +169,11 @@ Deno.test({ await new Promise((r) => setTimeout(r, 0)); f.drop(); const e = await caught(() => Promise.resolve(f)); - assertEq(e instanceof DroppedError, true, `expected DroppedError, got ${e}`); + assertEq( + e instanceof DroppedError, + true, + `expected DroppedError, got ${e}`, + ); assertEq(String(e).includes("dropped"), true, `${e}`); }, }); @@ -326,7 +334,8 @@ Deno.test({ }); Deno.test({ - name: "futures: a rejecting Promise producer reports its cause, not a bare drop", + name: + "futures: a rejecting Promise producer reports its cause, not a bare drop", ignore: !ready, fn: async () => { // `future` has no error channel of its own, so the guest can only ever diff --git a/runtime/tests/embedder/suspending_imports_test.ts b/runtime/tests/embedder/suspending_imports_test.ts index a134dcf..5c56a8e 100644 --- a/runtime/tests/embedder/suspending_imports_test.ts +++ b/runtime/tests/embedder/suspending_imports_test.ts @@ -28,8 +28,8 @@ import { import { suspending } from "@polyengine/protocol"; import { anySuspendingImport, - isSuspending, isSupported, + isSuspending, } from "../../src/jspi/mod.ts"; const ready = (await haveFixture(testdata("imports"))) && isSupported(); @@ -41,7 +41,8 @@ function later(value: T): Promise { } Deno.test({ - name: "suspending(): a marked sync-typed import parks the frame and resumes with the value", + name: + "suspending(): a marked sync-typed import parks the frame and resumes with the value", ignore: !ready, fn: async () => { const logged: number[] = []; @@ -61,7 +62,8 @@ Deno.test({ }); Deno.test({ - name: "suspending(): resume-time result lowering drives guest realloc (string result)", + name: + "suspending(): resume-time result lowering drives guest realloc (string result)", ignore: !ready, fn: async () => { // greet: string -> string. Lowering the settled result re-enters the @@ -80,7 +82,8 @@ Deno.test({ }); Deno.test({ - name: "suspending(): a marked import returning synchronously stays on the value path", + name: + "suspending(): a marked import returning synchronously stays on the value path", ignore: !ready, fn: async () => { // Marking declares that the import MAY park, not that it must: a plain @@ -98,7 +101,8 @@ Deno.test({ }); Deno.test({ - name: "unmarked sync import returning a Promise still refuses, naming suspending()", + name: + "unmarked sync import returning a Promise still refuses, naming suspending()", ignore: !ready, fn: async () => { // Fail-on-pre-fix shape, upgraded message: without the marker there is @@ -122,7 +126,8 @@ Deno.test({ }); Deno.test({ - name: "explicit jspi:false forces plain mode; a marked import's Promise still refuses", + name: + "explicit jspi:false forces plain mode; a marked import's Promise still refuses", ignore: !ready, fn: async () => { // The embedder's explicit override outranks marker evidence (chooseMode: @@ -144,7 +149,8 @@ Deno.test({ }); Deno.test({ - name: "suspending(): a rejected host promise surfaces as the export call's failure", + name: + "suspending(): a rejected host promise surfaces as the export call's failure", ignore: !ready, fn: async () => { // A rejection at resume time routes through the suspension point's fail @@ -176,7 +182,8 @@ const fallibleReady = isSupported(); Deno.test({ - name: "suspending(): a ComponentException rejection over a park becomes the guest's err case, not a trap", + name: + "suspending(): a ComponentException rejection over a park becomes the guest's err case, not a trap", ignore: !fallibleReady, fn: async () => { // The branded-throw contract survives the suspension: #wrapImportFn @@ -208,7 +215,8 @@ const startReady = (await haveFixture(testdata("imports"))) && isSupported(); Deno.test({ - name: "suspending(): a marked import reached from a start function traps (pin (c)), even returning synchronously", + name: + "suspending(): a marked import reached from a start function traps (pin (c)), even returning synchronously", ignore: !startReady, fn: async () => { // THE documented cost of marking (suspending.ts doc): a Suspending @@ -258,7 +266,8 @@ Deno.test("suspending(): marker mechanics (brand, identity, record scan)", () => // --------------------------------------------------------------------------- Deno.test({ - name: "suspending mark: @suspending on a provider-class method parks, with `this` bound to the provider", + name: + "suspending mark: @suspending on a provider-class method parks, with `this` bound to the provider", ignore: !ready, fn: async () => { // Two pins in one: the stage-3 decorator marks the prototype method the @@ -288,7 +297,8 @@ Deno.test({ }); Deno.test({ - name: "suspending mark: receiver binding alone — an unmarked stateful class provider works synchronously", + name: + "suspending mark: receiver binding alone — an unmarked stateful class provider works synchronously", ignore: !ready, fn: async () => { // The receiver fix is independent of parking: no marks, no Promises, @@ -315,7 +325,8 @@ const methodReady = null && isSupported(); Deno.test({ - name: "suspending mark: @suspending on a host-resource METHOD parks the frame (the pollable.block shape)", + name: + "suspending mark: @suspending on a host-resource METHOD parks the frame (the pollable.block shape)", ignore: !methodReady, fn: async () => { // The load-bearing scope extension: `[method]gauge.read` is the same @@ -349,10 +360,13 @@ Deno.test({ Deno.test("suspending mark: the decorator refuses non-method positions at class-definition time", () => { let raised: unknown; try { + // Untyped alias: the decorator's own type refuses accessor positions, + // and the runtime refusal under test is the one that fires anyway. + // deno-lint-ignore no-explicit-any + const untypedSuspending = suspending as any; // deno-lint-ignore no-unused-vars class Bad { - // deno-lint-ignore no-explicit-any - @(suspending as any) + @untypedSuspending get x(): number { return 1; } diff --git a/runtime/tests/embedder/sync_adapter_test.ts b/runtime/tests/embedder/sync_adapter_test.ts index 7f5837b..5226d2b 100644 --- a/runtime/tests/embedder/sync_adapter_test.ts +++ b/runtime/tests/embedder/sync_adapter_test.ts @@ -64,7 +64,11 @@ Deno.test({ false, "no microtask elapsed between the call and the throw", ); - assertEq(caughtErr instanceof ComponentException, true, `got: ${caughtErr}`); + assertEq( + caughtErr instanceof ComponentException, + true, + `got: ${caughtErr}`, + ); assertEq((caughtErr as ComponentException).payload, "boom"); // Drain the queued microtask so it doesn't leak into a later test. await new Promise((r) => queueMicrotask(() => r(undefined))); diff --git a/runtime/tests/embedder/trap_retire_test.ts b/runtime/tests/embedder/trap_retire_test.ts index aeeccc4..c7e03e5 100644 --- a/runtime/tests/embedder/trap_retire_test.ts +++ b/runtime/tests/embedder/trap_retire_test.ts @@ -24,7 +24,8 @@ const FIXTURE = guest("stream-pass"); const ready = await haveFixture(FIXTURE); Deno.test({ - name: "trap retire: a write parked on a trapped consumer rejects with PeerTrappedError", + name: + "trap retire: a write parked on a trapped consumer rejects with PeerTrappedError", ignore: !ready, fn: async () => { const c = await instantiateFixture(FIXTURE, { sink: () => 0n }); @@ -62,7 +63,8 @@ Deno.test({ }); Deno.test({ - name: "trap retire: reads from a stream whose writer trapped reject, data first", + name: + "trap retire: reads from a stream whose writer trapped reject, data first", ignore: !ready, fn: async () => { // `open-then-trap` writes n bytes from a background task, then traps: @@ -102,7 +104,8 @@ Deno.test({ }); Deno.test({ - name: "trap retire: a future whose writer trapped rejects PeerTrappedError, not DroppedError", + name: + "trap retire: a future whose writer trapped rejects PeerTrappedError, not DroppedError", ignore: !ready, fn: async () => { // The guest parks on the gate stream, so the call resolves and the host @@ -125,7 +128,8 @@ Deno.test({ }); Deno.test({ - name: "trap retire: a trapping import drops its lifted stream args (E2 shape)", + name: + "trap retire: a trapping import drops its lifted stream args (E2 shape)", ignore: !ready, fn: async () => { // The guest hands the stream to `sink`, which throws unbranded (a host @@ -151,7 +155,8 @@ Deno.test({ }); Deno.test({ - name: "clean paths stay unbranded: writer close is end-of-stream, not an error", + name: + "clean paths stay unbranded: writer close is end-of-stream, not an error", ignore: !ready, fn: async () => { const c = await instantiateFixture(FIXTURE, { sink: () => 0n }); @@ -175,7 +180,8 @@ Deno.test({ // --------------------------------------------------------------------------- Deno.test({ - name: "host ends: a second same-direction op throws instead of self-rendezvousing", + name: + "host ends: a second same-direction op throws instead of self-rendezvousing", ignore: false, fn: async () => { const hs = hostStream({ kind: "u8" }); @@ -195,7 +201,11 @@ Deno.test({ // Reading while a write is parked stays legal (different ends): it is // the rendezvous itself. - assertEq([...(await hs.readable.read(8)) as unknown as Uint8Array], [1, 2, 3]); + assertEq([...(await hs.readable.read(8)) as unknown as Uint8Array], [ + 1, + 2, + 3, + ]); assertEq(await w1, 3); const r1 = hs.readable.read(8); // parks (no writer) diff --git a/runtime/tests/embedder/untranslated_artifacts_test.ts b/runtime/tests/embedder/untranslated_artifacts_test.ts index c64efae..68faf34 100644 --- a/runtime/tests/embedder/untranslated_artifacts_test.ts +++ b/runtime/tests/embedder/untranslated_artifacts_test.ts @@ -23,17 +23,22 @@ const IMPORTS = { }; Deno.test({ - name: "module wiring: instantiate({ componentBytes, translator: bytes }) translates internally", + name: + "module wiring: instantiate({ componentBytes, translator: bytes }) translates internally", ignore: !ready, fn: async () => { const componentBytes = (await readArtifact(testdata("imports")))!; - const c = await instantiate({ componentBytes, translator: shimWasm! }, IMPORTS); + const c = await instantiate( + { componentBytes, translator: shimWasm! }, + IMPORTS, + ); assertEq(await c.exports.run(2, 40), 42); }, }); Deno.test({ - name: "module wiring: a shared Translator instance serves several instantiations", + name: + "module wiring: a shared Translator instance serves several instantiations", ignore: !ready, fn: async () => { const translator = await Translator.create(shimWasm!); diff --git a/runtime/tests/embedder/values_test.ts b/runtime/tests/embedder/values_test.ts index 2bb3e86..9ec3c54 100644 --- a/runtime/tests/embedder/values_test.ts +++ b/runtime/tests/embedder/values_test.ts @@ -27,7 +27,11 @@ Deno.test({ assertEq(names.includes("echo-bool"), false, "no kebab spellings survive"); // Even a sync WIT function returns a Promise: one calling convention. const p = v.echoBool(true); - assertEq(p instanceof Promise, true, "exports are uniformly Promise-shaped"); + assertEq( + p instanceof Promise, + true, + "exports are uniformly Promise-shaped", + ); assertEq(await p, true); }, }); @@ -59,7 +63,9 @@ Deno.test({ true, ); assertEq( - String(await caught(() => v.echoChar("ab"))).includes("single-code-point"), + String(await caught(() => v.echoChar("ab"))).includes( + "single-code-point", + ), true, ); }, @@ -163,7 +169,8 @@ Deno.test({ }); Deno.test({ - name: "values: result in FUNCTION-RESULT position resolves T / rejects ComponentException", + name: + "values: result in FUNCTION-RESULT position resolves T / rejects ComponentException", ignore: !ready, fn: async () => { // `echo-result: func(v: result) -> result`: the @@ -172,7 +179,11 @@ Deno.test({ assertEq(await v.echoResult({ kind: "ok", value: 42 }), 42); const e = await caught(() => v.echoResult({ kind: "err", value: "boom" })); - assertEq(e instanceof ComponentException, true, `expected ComponentException, got ${e}`); + assertEq( + e instanceof ComponentException, + true, + `expected ComponentException, got ${e}`, + ); assertEq((e as ComponentException).payload, "boom"); assertEq((e as ComponentException).name, "ComponentException"); }, @@ -267,7 +278,9 @@ Deno.test({ { label: "note", type: { kind: "option", type: { kind: "string" } } }, ], } as unknown as Parameters[1]; - const o = { where: "export 'f'" } as unknown as Parameters[2]; + const o = { where: "export 'f'" } as unknown as Parameters< + typeof toHost + >[2]; // "fields of option type are optional properties": some -> present and // UNWRAPPED (not the `{kind, value}` box), none -> the property is absent, diff --git a/runtime/tests/embedder/version_test.ts b/runtime/tests/embedder/version_test.ts index 07fc319..93efd8f 100644 --- a/runtime/tests/embedder/version_test.ts +++ b/runtime/tests/embedder/version_test.ts @@ -12,7 +12,11 @@ import { ImportResolver, trackKey, } from "../../src/embedder/version.ts"; -import { camelCase, parseLeafName, pascalCase } from "../../src/embedder/casing.ts"; +import { + camelCase, + parseLeafName, + pascalCase, +} from "../../src/embedder/casing.ts"; import { NameCollisionError } from "../../src/embedder/errors.ts"; import { checkNoCollisions } from "../../src/embedder/values.ts"; @@ -32,7 +36,11 @@ Deno.test("track key: major 0 tracks the minor", () => { Deno.test("track key: 0.0.z and prereleases belong to no track", () => { assertEq(trackKey(`${P}@0.0.1`), null, "patch-only: compatible with nothing"); - assertEq(trackKey(`${P}@0.2.0-rc-2023-10-18`), null, "prerelease is exact-only"); + assertEq( + trackKey(`${P}@0.2.0-rc-2023-10-18`), + null, + "prerelease is exact-only", + ); assertEq(trackKey(P), null, "unversioned ids have no track"); }); @@ -77,7 +85,11 @@ Deno.test("max-wins: the highest full version claims the track", () => { // Registration order must not matter: 0.2.12 > 0.2.9 > 0.2.6 numerically, // not lexically (a string compare would pick "0.2.9"). assertEq(r.resolve(`${P}@0.2.4`)?.value === hi, true); - assertEq(r.resolve(`${P}@0.2.6`)?.value === lo, true, "exact beats the track"); + assertEq( + r.resolve(`${P}@0.2.6`)?.value === lo, + true, + "exact beats the track", + ); void mid; }); @@ -180,7 +192,10 @@ Deno.test("casing: later fragments capitalize, remainders are preserved", () => }); Deno.test("mangled leaf names decode to resource membership", () => { - assertEq(parseLeafName("make-counter"), { form: "plain", name: "make-counter" }); + assertEq(parseLeafName("make-counter"), { + form: "plain", + name: "make-counter", + }); assertEq(parseLeafName("[constructor]counter"), { form: "constructor", resource: "counter", diff --git a/runtime/tests/enter_sync_call_reentrance_test.ts b/runtime/tests/enter_sync_call_reentrance_test.ts index 7bd2d4e..2473c63 100644 --- a/runtime/tests/enter_sync_call_reentrance_test.ts +++ b/runtime/tests/enter_sync_call_reentrance_test.ts @@ -47,8 +47,14 @@ function fixture() { stats: newStats(), trapState: { pending: undefined }, } as unknown as TrampolineContext; - const enter = createTrampoline({ kind: "enter-sync-call", index: 0 } as never, ctx); - const exit = createTrampoline({ kind: "exit-sync-call", index: 0 } as never, ctx); + const enter = createTrampoline( + { kind: "enter-sync-call", index: 0 } as never, + ctx, + ); + const exit = createTrampoline( + { kind: "exit-sync-call", index: 0 } as never, + ctx, + ); const inst = (i: number) => (ctx as TrampolineContext).componentInstance(i); return { ctx, enter, exit, inst, syncCallStack }; } diff --git a/runtime/tests/executor_duplicate_import_test.ts b/runtime/tests/executor_duplicate_import_test.ts index 17874ad..35b00cb 100644 --- a/runtime/tests/executor_duplicate_import_test.ts +++ b/runtime/tests/executor_duplicate_import_test.ts @@ -29,17 +29,48 @@ import type { WirePlan } from "../src/plan/format.ts"; * either slot — see task/mod.ts). */ const DUP_IMPORT_MODULE = new Uint8Array([ - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // \0asm, version 1 + 0x00, + 0x61, + 0x73, + 0x6d, + 0x01, + 0x00, + 0x00, + 0x00, // \0asm, version 1 // import section: 2 entries, each "env"."g" (mut i32 global) - 0x02, 0x13, 0x02, - 0x03, 0x65, 0x6e, 0x76, 0x01, 0x67, 0x03, 0x7f, 0x01, - 0x03, 0x65, 0x6e, 0x76, 0x01, 0x67, 0x03, 0x7f, 0x01, + 0x02, + 0x13, + 0x02, + 0x03, + 0x65, + 0x6e, + 0x76, + 0x01, + 0x67, + 0x03, + 0x7f, + 0x01, + 0x03, + 0x65, + 0x6e, + 0x76, + 0x01, + 0x67, + 0x03, + 0x7f, + 0x01, ]); -function planFor(args: WirePlan["initializers"][0] & { op: "instantiate-module" }): WirePlan { +function planFor( + args: WirePlan["initializers"][0] & { op: "instantiate-module" }, +): WirePlan { return { formatVersion: SUPPORTED_FORMAT_VERSION, - producer: { shimVersion: "test", wasmtimeEnviron: "49.0.0-dev+4675ee1", features: [] }, + producer: { + shimVersion: "test", + wasmtimeEnviron: "49.0.0-dev+4675ee1", + features: [], + }, component: { sha256: "0".repeat(64), len: DUP_IMPORT_MODULE.length }, modules: [{ kind: "embedded", offset: 0, len: DUP_IMPORT_MODULE.length }], initializers: [args], @@ -107,6 +138,9 @@ Deno.test("executor: duplicate (module,field) core imports resolving to DIFFEREN const msg = String(caught); assertEq(msg.includes("env"), true, `message names the module: ${msg}`); assertEq(msg.includes("g"), true, `message names the field: ${msg}`); - assertEq(msg.includes("0") && msg.includes("1"), true, - `message names the conflicting arg indices: ${msg}`); + assertEq( + msg.includes("0") && msg.includes("1"), + true, + `message names the conflicting arg indices: ${msg}`, + ); }); diff --git a/runtime/tests/fact_call_test.ts b/runtime/tests/fact_call_test.ts index 0643d88..a1565ce 100644 --- a/runtime/tests/fact_call_test.ts +++ b/runtime/tests/fact_call_test.ts @@ -97,7 +97,10 @@ function runPrepared(input: { // deno-lint-ignore no-explicit-any const prep = createPrepareCall({ memory: null }, ctx as any); // deno-lint-ignore no-explicit-any - const startCall = createAsyncStartCall({ callback: null, postReturn: null }, ctx as any); + const startCall = createAsyncStartCall( + { callback: null, postReturn: null }, + ctx as any, + ); prep( start, diff --git a/runtime/tests/host_import_cancel_test.ts b/runtime/tests/host_import_cancel_test.ts index d98c297..b275e97 100644 --- a/runtime/tests/host_import_cancel_test.ts +++ b/runtime/tests/host_import_cancel_test.ts @@ -47,7 +47,10 @@ import { unpackSubtaskResult, } from "../src/task/mod.ts"; import type { FuncType } from "../src/cabi/types.ts"; -import { BLOCKED, createSubtaskCancel } from "../src/intrinsics/async_builtins.ts"; +import { + BLOCKED, + createSubtaskCancel, +} from "../src/intrinsics/async_builtins.ts"; import { abortable, deferCancel, @@ -213,7 +216,8 @@ Deno.test("cancellation discard: the SYNC cancel form under jspi also answers sy const rc = f.asGuest(() => cancel(subtaski)); assert(typeof rc === "number", `expected a number, got ${typeof rc}`); assert( - !(rc !== null && typeof (rc as unknown as PromiseLike) === "object"), + !(rc !== null && + typeof (rc as unknown as PromiseLike) === "object"), "the sync form did not park", ); assertEq(rc, SubtaskState.CANCELLED_BEFORE_RETURNED); @@ -308,7 +312,9 @@ Deno.test("cancellation discard: deferCancel() keeps run-to-completion — BLOCK const f = mkFixture(deferCancel(() => d.promise)); const { subtaski, subtask } = inFlight(f); - const rc = f.asGuest(() => createSubtaskCancel({ async: true }, f.inst)(subtaski)); + const rc = f.asGuest(() => + createSubtaskCancel({ async: true }, f.inst)(subtaski) + ); assertEq(rc, BLOCKED); assertEq(subtask.resolved(), false); assertEq(subtask.state, SubtaskState.STARTED); diff --git a/runtime/tests/host_pump_test.ts b/runtime/tests/host_pump_test.ts index bf08549..772e404 100644 --- a/runtime/tests/host_pump_test.ts +++ b/runtime/tests/host_pump_test.ts @@ -42,8 +42,7 @@ function assert(cond: boolean, msg: string): asserts cond { /** The slice of `ComponentInstance` that `Store.tick` touches. */ function fakeInst() { - return { - }; + return {}; } /** @@ -253,7 +252,10 @@ Deno.test({ (reclaim) => reclaim(), () => {}, ); - assertEq((await withTimeout(reading, `raced read hops=${hops}`)).length, 3); + assertEq( + (await withTimeout(reading, `raced read hops=${hops}`)).length, + 3, + ); host.readable.drop(); await new Promise((r) => setTimeout(r, 2)); assert( diff --git a/runtime/tests/host_pump_trap_test.ts b/runtime/tests/host_pump_trap_test.ts index 6edfc12..b3ecc09 100644 --- a/runtime/tests/host_pump_trap_test.ts +++ b/runtime/tests/host_pump_trap_test.ts @@ -16,11 +16,7 @@ import { assertEq } from "./support/asserts.ts"; import { hostStreamFor } from "../src/exec/mod.ts"; -import { - ReadableStreamEnd, - SharedStreamImpl, - Store, -} from "../src/task/mod.ts"; +import { ReadableStreamEnd, SharedStreamImpl, Store } from "../src/task/mod.ts"; import { Trap } from "../src/cabi/trap.ts"; import type { ComponentValue, ValType } from "../src/cabi/types.ts"; @@ -98,9 +94,11 @@ Deno.test({ // A ready thread of an unrelated instance (no stream ends) that traps the // moment the host op's `pump()` ticks the store. const trap = new Trap("boom"); - store.startWaiting(new FakeThread(() => { - throw trap; - }, fakeInst())); + store.startWaiting( + new FakeThread(() => { + throw trap; + }, fakeInst()), + ); const first = host.writable.write([1]); let firstErr: unknown = undefined; @@ -143,9 +141,11 @@ Deno.test({ const { host } = hostEndOn(store, U8); const trap = new Trap("boom"); - store.startWaiting(new FakeThread(() => { - throw trap; - }, fakeInst())); + store.startWaiting( + new FakeThread(() => { + throw trap; + }, fakeInst()), + ); const first = host.readable.read(8); let settled = false; @@ -192,9 +192,11 @@ Deno.test({ // `pump()` — so the executor's throw hits an already-settled promise. const guestEnd = new ReadableStreamEnd(shared); const trap = new Trap("boom"); - store.startWaiting(new FakeThread(() => { - throw trap; - }, fakeInst([guestEnd]))); + store.startWaiting( + new FakeThread(() => { + throw trap; + }, fakeInst([guestEnd])), + ); const p = host.writable.write([1]); let rejected: unknown = undefined; diff --git a/runtime/tests/integration/e2e_imports_test.ts b/runtime/tests/integration/e2e_imports_test.ts index 4a7304b..36163ae 100644 --- a/runtime/tests/integration/e2e_imports_test.ts +++ b/runtime/tests/integration/e2e_imports_test.ts @@ -11,10 +11,7 @@ import { assertEq } from "../support/asserts.ts"; import { Translator } from "../../src/shim/mod.ts"; -import { - hostResourceType, - instantiateComponent, -} from "../../src/exec/mod.ts"; +import { hostResourceType, instantiateComponent } from "../../src/exec/mod.ts"; import { PlanError } from "../../src/plan/mod.ts"; import { ResourceHandle } from "../../src/cabi/mod.ts"; import { SyncCallScope } from "../../src/intrinsics/mod.ts"; @@ -221,7 +218,6 @@ Deno.test({ }, }); - // --------------------------------------------------------------------------- // Re-lending a borrow across three components // --------------------------------------------------------------------------- @@ -345,7 +341,6 @@ Deno.test({ }, }); - // --------------------------------------------------------------------------- // Cross-encoding strings (FACT Transcoder trampoline) // --------------------------------------------------------------------------- @@ -383,20 +378,23 @@ Deno.test({ const logged: number[] = []; let depth = 0; let inner: number | undefined; - const c: { exports: Record } = await instantiate("imports", { - "log": (x: unknown) => { - logged.push(x as number); - if (depth === 0) { - depth = 1; - // Re-entry into the live instance, host-mediated. - inner = fn(c, "run")(10, 20) as number; - } - }, - "host:api/math": { - add: (a: unknown, b: unknown) => (a as number) + (b as number), - greet: (who: unknown) => `Hello, ${who as string}!`, + const c: { exports: Record } = await instantiate( + "imports", + { + "log": (x: unknown) => { + logged.push(x as number); + if (depth === 0) { + depth = 1; + // Re-entry into the live instance, host-mediated. + inner = fn(c, "run")(10, 20) as number; + } + }, + "host:api/math": { + add: (a: unknown, b: unknown) => (a as number) + (b as number), + greet: (who: unknown) => `Hello, ${who as string}!`, + }, }, - }); + ); const outer = fn(c, "run")(1, 2) as number; assertEq(outer, 3, "the outer call completed"); diff --git a/runtime/tests/integration/e2e_resources_test.ts b/runtime/tests/integration/e2e_resources_test.ts index 36d10ea..8e5c762 100644 --- a/runtime/tests/integration/e2e_resources_test.ts +++ b/runtime/tests/integration/e2e_resources_test.ts @@ -35,8 +35,10 @@ Deno.test("resources: instantiate + counter lifecycle + dtor observation", async adapters, }); - const counters = component.exports["polyengine:resources/counters"] as - Record unknown>; + const counters = component.exports["polyengine:resources/counters"] as Record< + string, + (...args: unknown[]) => unknown + >; assertEq(typeof counters, "object"); const names = Object.keys(counters).sort(); assertEq(names.includes("make-counter"), true, `exports: ${names}`); diff --git a/runtime/tests/integration/e2e_streams_test.ts b/runtime/tests/integration/e2e_streams_test.ts index 65b835a..0fe1397 100644 --- a/runtime/tests/integration/e2e_streams_test.ts +++ b/runtime/tests/integration/e2e_streams_test.ts @@ -58,7 +58,10 @@ Deno.test({ const pending = (c.exports["sum-stream"] as (v: unknown) => unknown)( s.value, ); - assert(pending instanceof Promise, "a parked async export returns a Promise"); + assert( + pending instanceof Promise, + "a parked async export returns a Promise", + ); assertEq(await s.writable.writeAll([1, 2, 3, 4]), 4); s.writable.drop(); // end-of-stream: the guest's read loop terminates assertEq(await pending, 10n); @@ -71,10 +74,11 @@ Deno.test({ fn: async () => { const c = await instantiate("async-probe"); const f = hostFuture(U32); - const pending = (c.exports["future-add"] as (a: unknown, b: number) => unknown)( - f.value, - 5, - ); + const pending = + (c.exports["future-add"] as (a: unknown, b: number) => unknown)( + f.value, + 5, + ); await f.write(37); assertEq(await pending, 42); }, @@ -89,9 +93,10 @@ Deno.test({ // must be able to read the output while still feeding the input. const c = await instantiate("stream-echo"); const input = hostStream(U32); - const returned = await (c.exports["echo-doubled"] as (v: unknown) => unknown)( - input.value, - ); + const returned = + await (c.exports["echo-doubled"] as (v: unknown) => unknown)( + input.value, + ); const output = hostStreamFor(returned as never); const feed = (async () => { await input.writable.writeAll([1, 2, 3]); @@ -122,9 +127,10 @@ Deno.test({ // Guest as producer: `make-future` hands back a future it resolves later. const c2 = await instantiate("future-user"); - const returned = await (c2.exports["make-future"] as (x: number) => unknown)( - 7, - ); + const returned = + await (c2.exports["make-future"] as (x: number) => unknown)( + 7, + ); assertEq(await hostFutureFor(returned as never).read(), 8); }, }); diff --git a/runtime/tests/integration/e2e_suite_test.ts b/runtime/tests/integration/e2e_suite_test.ts index bb8e695..17b44d2 100644 --- a/runtime/tests/integration/e2e_suite_test.ts +++ b/runtime/tests/integration/e2e_suite_test.ts @@ -459,7 +459,9 @@ Deno.test({ const base = new URL(`harness/generated/${dir}/`, root); for (const [file, commands] of await commandsOf(dir)) { for (const cmd of commands) { - if (cmd.type !== "assert_invalid" && cmd.type !== "assert_malformed") { + if ( + cmd.type !== "assert_invalid" && cmd.type !== "assert_malformed" + ) { continue; } if (cmd.module_type !== "binary") continue; diff --git a/runtime/tests/integration/e2e_values_test.ts b/runtime/tests/integration/e2e_values_test.ts index 9e75935..302c85b 100644 --- a/runtime/tests/integration/e2e_values_test.ts +++ b/runtime/tests/integration/e2e_values_test.ts @@ -44,9 +44,11 @@ type EchoFn = (v: ComponentValue) => ComponentValue; const echo = (name: string): EchoFn => { const fn = component.exports[name] as EchoFn | undefined; if (typeof fn !== "function") { - throw new Error(`export ${name} missing; have: ${ - Object.keys(component.exports).join(", ") - }`); + throw new Error( + `export ${name} missing; have: ${ + Object.keys(component.exports).join(", ") + }`, + ); } return fn; }; diff --git a/runtime/tests/jspi/asserts.ts b/runtime/tests/jspi/asserts.ts index e908c65..1b2352a 100644 --- a/runtime/tests/jspi/asserts.ts +++ b/runtime/tests/jspi/asserts.ts @@ -2,7 +2,11 @@ // network fetch — matches runtime/tests/support/asserts.ts style but scoped // here since the jspi tests are structurally standalone). -export function assertEquals(actual: unknown, expected: unknown, msg?: string): void { +export function assertEquals( + actual: unknown, + expected: unknown, + msg?: string, +): void { const ok = actual === expected || (typeof actual === "number" && typeof expected === "number" && Number.isNaN(actual) && Number.isNaN(expected)); diff --git a/runtime/tests/jspi/bridge_test.ts b/runtime/tests/jspi/bridge_test.ts index 38f13f2..7874635 100644 --- a/runtime/tests/jspi/bridge_test.ts +++ b/runtime/tests/jspi/bridge_test.ts @@ -13,8 +13,8 @@ import { chooseMode, enterWasm, planNeedsSuspension, - SuspensionPoint, suspendingImport, + SuspensionPoint, } from "../../src/jspi/mod.ts"; import { isSupported } from "../../src/jspi/mechanics.ts"; import { entryRefusal } from "../../src/task/scheduler.ts"; @@ -199,7 +199,10 @@ Deno.test("bridge: the invariant is checked, not hoped for", () => { } catch { threw = true; } - assert(threw, `mixture (${mode}, entries=${e}, imports=${i}) must be rejected`); + assert( + threw, + `mixture (${mode}, entries=${e}, imports=${i}) must be rejected`, + ); } }); @@ -272,7 +275,9 @@ Deno.test("bridge: planNeedsSuspension recognises both sources of blocking", () `${kind} (async form)`, ); } - for (const kind of ["stream-read", "stream-write", "future-read", "future-write"]) { + for ( + const kind of ["stream-read", "stream-write", "future-read", "future-write"] + ) { assertEq( planNeedsSuspension({ canonicalOptions: [{ async: true, callback: 0 }], @@ -312,7 +317,10 @@ Deno.test("plain mode: lifted exports still return values, not Promises", async let shim: Uint8Array, guest: Uint8Array; try { shim = await Deno.readFile( - new URL("target/wasm32-unknown-unknown/release/translator_shim.wasm", root), + new URL( + "target/wasm32-unknown-unknown/release/translator_shim.wasm", + root, + ), ); guest = await Deno.readFile( new URL("examples/guests/build/hello.component.wasm", root), diff --git a/runtime/tests/jspi/chunk_attribution_test.ts b/runtime/tests/jspi/chunk_attribution_test.ts index c529cea..4b6f53c 100644 --- a/runtime/tests/jspi/chunk_attribution_test.ts +++ b/runtime/tests/jspi/chunk_attribution_test.ts @@ -26,7 +26,9 @@ Deno.test("issue #24: interleaved continuation chunks each read their own ambien const ownerB = { name: "B", storage: [0, 0] }; const seen: Array<[string, unknown]> = []; - function mkBlock(label: string): { resolveFirst: () => void; block: unknown } { + function mkBlock( + label: string, + ): { resolveFirst: () => void; block: unknown } { let calls = 0; let resolveFirst!: () => void; const gate = new Promise((res) => { diff --git a/runtime/tests/jspi/concurrent_activations_test.ts b/runtime/tests/jspi/concurrent_activations_test.ts index 2d656c6..e22d950 100644 --- a/runtime/tests/jspi/concurrent_activations_test.ts +++ b/runtime/tests/jspi/concurrent_activations_test.ts @@ -25,7 +25,11 @@ Deno.test("two concurrent suspended activations of the same instance are legal a const p2 = runPromising(2); const p3 = runPromising(3); - assertEquals(pending.length, 3, "OBSERVED: engine permits N concurrent suspensions on one instance"); + assertEquals( + pending.length, + 3, + "OBSERVED: engine permits N concurrent suspensions on one instance", + ); // Resolve out of order to rule out any hidden FIFO/queueing assumption. const byX = new Map(pending.map((p) => [p.x, p])); diff --git a/runtime/tests/jspi/cross_abi_differential_test.ts b/runtime/tests/jspi/cross_abi_differential_test.ts index b942f28..c3aa4f0 100644 --- a/runtime/tests/jspi/cross_abi_differential_test.ts +++ b/runtime/tests/jspi/cross_abi_differential_test.ts @@ -97,7 +97,9 @@ Deno.test({ jspi, }); try { - return `ok ${JSON.stringify(await (handle.exports[field] as () => unknown)())}`; + return `ok ${ + JSON.stringify(await (handle.exports[field] as () => unknown)()) + }`; } catch (e) { // Compare the failure TEXT too: "both threw" is not agreement if they // threw for different reasons. diff --git a/runtime/tests/jspi/deadlock_test.ts b/runtime/tests/jspi/deadlock_test.ts index b578a2a..c1c131d 100644 --- a/runtime/tests/jspi/deadlock_test.ts +++ b/runtime/tests/jspi/deadlock_test.ts @@ -56,7 +56,8 @@ async function run(jspi: boolean): Promise { } Deno.test({ - name: "deadlock: an unprogressable sync-lowered call traps, and does not hang", + name: + "deadlock: an unprogressable sync-lowered call traps, and does not hang", ignore: !ready, fn: async () => { // A stall would fail this test by timing out / "promise never resolved" diff --git a/runtime/tests/jspi/fact_callback_suspend_test.ts b/runtime/tests/jspi/fact_callback_suspend_test.ts index a0e996c..a7d9ffc 100644 --- a/runtime/tests/jspi/fact_callback_suspend_test.ts +++ b/runtime/tests/jspi/fact_callback_suspend_test.ts @@ -57,8 +57,7 @@ const componentWasm = await Deno.readFile( ); Deno.test({ - name: - "fact callee: a callback re-entry that blocks synchronously suspends " + + name: "fact callee: a callback re-entry that blocks synchronously suspends " + "(promising-wrapped), it does not raise SuspendError", ignore: shimWasm === null, fn: async () => { diff --git a/runtime/tests/jspi/handshake_test.ts b/runtime/tests/jspi/handshake_test.ts index cb20733..150cb0a 100644 --- a/runtime/tests/jspi/handshake_test.ts +++ b/runtime/tests/jspi/handshake_test.ts @@ -91,22 +91,34 @@ async function instantiate() { // fails such a test with "Promise resolution is still pending", so awaiting the // call IS the assertion; the returned value pins that it resolved correctly // rather than merely resolving. -Deno.test({ name: "jspi handshake: a parked caller is resumed by the scheduler (run1)", ignore: !ready, fn: async () => { - const handle = await instantiate(); - const run1 = handle.exports.run1 as () => Promise | unknown; - assertEquals(await run1(), 42); -} }); +Deno.test({ + name: "jspi handshake: a parked caller is resumed by the scheduler (run1)", + ignore: !ready, + fn: async () => { + const handle = await instantiate(); + const run1 = handle.exports.run1 as () => Promise | unknown; + assertEquals(await run1(), 42); + }, +}); -Deno.test({ name: "jspi handshake: a parked caller is resumed by the scheduler (run2)", ignore: !ready, fn: async () => { - const handle = await instantiate(); - const run2 = handle.exports.run2 as () => Promise | unknown; - assertEquals(await run2(), 42); -} }); +Deno.test({ + name: "jspi handshake: a parked caller is resumed by the scheduler (run2)", + ignore: !ready, + fn: async () => { + const handle = await instantiate(); + const run2 = handle.exports.run2 as () => Promise | unknown; + assertEquals(await run2(), 42); + }, +}); // Both exports on ONE instance: the second call must not inherit a wedged // scheduler (a stale ambient claim or a memoized await tag) from the first. -Deno.test({ name: "jspi handshake: consecutive parked calls on one instance", ignore: !ready, fn: async () => { - const handle = await instantiate(); - assertEquals(await (handle.exports.run1 as () => unknown)(), 42); - assertEquals(await (handle.exports.run2 as () => unknown)(), 42); -} }); +Deno.test({ + name: "jspi handshake: consecutive parked calls on one instance", + ignore: !ready, + fn: async () => { + const handle = await instantiate(); + assertEquals(await (handle.exports.run1 as () => unknown)(), 42); + assertEquals(await (handle.exports.run2 as () => unknown)(), 42); + }, +}); diff --git a/runtime/tests/jspi/hop_atomicity_test.ts b/runtime/tests/jspi/hop_atomicity_test.ts index 06528f9..939faf7 100644 --- a/runtime/tests/jspi/hop_atomicity_test.ts +++ b/runtime/tests/jspi/hop_atomicity_test.ts @@ -69,7 +69,10 @@ const componentWasm = await Deno.readFile( /** The value `tick` builds on every call (fixture layout: two inner lists). */ function assertTickValue(actual: unknown, where: string): void { - assert(Array.isArray(actual), `${where}: expected an array, got ${Deno.inspect(actual)}`); + assert( + Array.isArray(actual), + `${where}: expected an array, got ${Deno.inspect(actual)}`, + ); const outer = actual as unknown[]; assertEquals(outer.length, 2, `${where}: outer list length`); // contracts/embedder-api.md / docs/architecture.md §7: `list` lifts as a @@ -79,7 +82,9 @@ function assertTickValue(actual: unknown, where: string): void { const inner = outer[i]; assert( inner instanceof Uint8Array, - `${where}: inner[${i}] should lift as Uint8Array, got ${Deno.inspect(inner)}`, + `${where}: inner[${i}] should lift as Uint8Array, got ${ + Deno.inspect(inner) + }`, ); assertEquals( Array.from(inner).join(","), diff --git a/runtime/tests/jspi/reentry_test.ts b/runtime/tests/jspi/reentry_test.ts index 3aecd2c..8ac253f 100644 --- a/runtime/tests/jspi/reentry_test.ts +++ b/runtime/tests/jspi/reentry_test.ts @@ -18,7 +18,9 @@ Deno.test("reentry: calling another export of the same instance while one export }); const exp = await instantiateActivation({ - block: new WebAssembly.Suspending((x: number) => blocked.then((v) => v + x)), + block: new WebAssembly.Suspending((x: number) => + blocked.then((v) => v + x) + ), }); const runPromising = WebAssembly.promising(exp.run); @@ -29,7 +31,11 @@ Deno.test("reentry: calling another export of the same instance while one export // engine, this would trap; docs/architecture.md §6 says it is NOT — the CM-level check // is the scheduler's job (out of scope for this mechanics-only module). const otherResult = exp.other(1); - assertEquals(otherResult, 1001, "OBSERVED: reentry succeeds, no engine-level trap"); + assertEquals( + otherResult, + 1001, + "OBSERVED: reentry succeeds, no engine-level trap", + ); resolveBlock!(100); const result = await suspendedCall; diff --git a/runtime/tests/jspi/suspend_resume_test.ts b/runtime/tests/jspi/suspend_resume_test.ts index f34eb0e..7c16842 100644 --- a/runtime/tests/jspi/suspend_resume_test.ts +++ b/runtime/tests/jspi/suspend_resume_test.ts @@ -18,7 +18,9 @@ Deno.test("promising export suspends on a Suspending import and resumes with its const exp = await instantiateActivation({ // activation.wat's `run` computes block(x) + 1. - block: new WebAssembly.Suspending((x: number) => blocked.then((v) => v + x)), + block: new WebAssembly.Suspending((x: number) => + blocked.then((v) => v + x) + ), }); const runPromising = WebAssembly.promising(exp.run); diff --git a/runtime/tests/jspi/sync_entry_test.ts b/runtime/tests/jspi/sync_entry_test.ts index ca261e4..d7e3d73 100644 --- a/runtime/tests/jspi/sync_entry_test.ts +++ b/runtime/tests/jspi/sync_entry_test.ts @@ -159,8 +159,7 @@ Deno.test({ }); Deno.test({ - name: - "sync() arm 2: a SYNC_ENTRY call during a hop window refuses with " + + name: "sync() arm 2: a SYNC_ENTRY call during a hop window refuses with " + "SyncEntryBusy, non-poisoningly", ignore: shimWasm === null || !isSupported(), fn: async () => { @@ -199,7 +198,10 @@ Deno.test({ // Non-poisoning, both surfaces: the instance is enterable again once the // hop has settled. - assertTickValue(await (tick() as Promise), "default entry after refusal"); + assertTickValue( + await (tick() as Promise), + "default entry after refusal", + ); assertEquals(clobberSync(), 1, "sync entry after refusal"); assertTickValue(tickSync(), "sync entry after refusal (tick)"); }, diff --git a/runtime/tests/layout_cache_test.ts b/runtime/tests/layout_cache_test.ts index b08ff4e..2b4275b 100644 --- a/runtime/tests/layout_cache_test.ts +++ b/runtime/tests/layout_cache_test.ts @@ -122,8 +122,7 @@ Deno.test("a failing layout is not cached as a success", () => { // comparing against `alignment()`/`elemSize()` would assert nothing, // since those now just read the node back. Deno.test("layoutOf: non-compound sizes match the spec table", () => { - const labels = (n: number) => - Array.from({ length: n }, (_, i) => `flag${i}`); + const labels = (n: number) => Array.from({ length: n }, (_, i) => `flag${i}`); // [type, i32 align, i32 size, i64 align, i64 size] const table: [ValType, number, number, number, number][] = [ diff --git a/runtime/tests/lift_done_verdict_test.ts b/runtime/tests/lift_done_verdict_test.ts index cab8b7d..a893736 100644 --- a/runtime/tests/lift_done_verdict_test.ts +++ b/runtime/tests/lift_done_verdict_test.ts @@ -42,7 +42,11 @@ // the driver (`DriveExit`) it resolves. import { assertEq } from "./support/asserts.ts"; -import { createLiftedFunction, newStats, type ResolvedOptions } from "../src/exec/mod.ts"; +import { + createLiftedFunction, + newStats, + type ResolvedOptions, +} from "../src/exec/mod.ts"; import { ComponentInstanceState, currentThread, diff --git a/runtime/tests/park_state_settle_test.ts b/runtime/tests/park_state_settle_test.ts index e213d90..48a948d 100644 --- a/runtime/tests/park_state_settle_test.ts +++ b/runtime/tests/park_state_settle_test.ts @@ -102,7 +102,8 @@ function mkWorld() { thread, point(): SuspensionPoint | undefined { return store.waiting.find( - (w) => typeof (w as { resume?: unknown }).resume === "function" && + (w) => + typeof (w as { resume?: unknown }).resume === "function" && typeof (w as { abandon?: unknown }).abandon === "function", ) as SuspensionPoint | undefined; }, @@ -148,7 +149,11 @@ function parkOnWait(cancellable: boolean) { const wset = new WaitableSet(); const seti = w.inst.handles.add(wset); const o = opts(w.inst, { cancellable }); - const ctx = { componentInstance: () => w.inst, options: () => o, resultTypes: () => [] }; + const ctx = { + componentInstance: () => w.inst, + options: () => o, + resultTypes: () => [], + }; const wait = createWaitableSetWait({ options: 0 }, ctx, w.inst, "jspi"); const parked = w.run(() => wait(seti, 0)); assert(parked instanceof Promise, "the wait parked"); @@ -223,7 +228,11 @@ Deno.test("#106 SITE 4: abandon clears hasSyncWaiter; cancel-copy and drop stay // The observables the flag exists for: a concurrent cancel-copy trapped // "sync waiter" while set (cancelCopy), and Waitable.drop asserts // `!hasSyncWaiter`. Both must be legal again once no waiter exists. - const cancel = createStreamCancelRead({ streamTable: 0, async: false }, ctx, w.inst); + const cancel = createStreamCancelRead( + { streamTable: 0, async: false }, + ctx, + w.inst, + ); w.run(() => cancel(ri)); assertEq(await settled, "rejected: store teardown"); }); @@ -239,7 +248,10 @@ Deno.test("#106 SITE 5: abandon clears the subtask's hasSyncWaiter", async () => const sti = w.inst.handles.add(st); const cancel = createSubtaskCancel({ async: false }, w.inst, "jspi"); const parked = w.run(() => cancel(sti)); - assert(parked instanceof Promise, "sync subtask.cancel parked (unresolved callee)"); + assert( + parked instanceof Promise, + "sync subtask.cancel parked (unresolved callee)", + ); assertEq(st.hasSyncWaiter, true); const settled = rejected(parked as Promise); const point = w.point(); diff --git a/runtime/tests/plan_loader_test.ts b/runtime/tests/plan_loader_test.ts index a899200..4848c4a 100644 --- a/runtime/tests/plan_loader_test.ts +++ b/runtime/tests/plan_loader_test.ts @@ -5,12 +5,12 @@ import { assertEq } from "./support/asserts.ts"; import { loadEnvelope, - resourceIndexOfDefined, - TranslateError, loadPlan, loadValType, PlanError, + resourceIndexOfDefined, SUPPORTED_FORMAT_VERSION, + TranslateError, } from "../src/plan/mod.ts"; import type { WirePlan, WireValType } from "../src/plan/mod.ts"; import { ResourceTypeInfo } from "../src/cabi/mod.ts"; @@ -29,7 +29,11 @@ function assertPlanError(fn: () => unknown, includes: string) { function minimalPlan(overrides: Partial = {}): WirePlan { return { formatVersion: SUPPORTED_FORMAT_VERSION, - producer: { shimVersion: "0", wasmtimeEnviron: "49.0.0-dev+4675ee1", features: [] }, + producer: { + shimVersion: "0", + wasmtimeEnviron: "49.0.0-dev+4675ee1", + features: [], + }, component: { sha256: "0".repeat(64), len: 0 }, modules: [], initializers: [], @@ -55,7 +59,8 @@ Deno.test("loader: formatVersion is validated and fails fast", () => { "formatVersion 0", ); assertPlanError( - () => loadPlan(minimalPlan({ formatVersion: SUPPORTED_FORMAT_VERSION + 1 })), + () => + loadPlan(minimalPlan({ formatVersion: SUPPORTED_FORMAT_VERSION + 1 })), `formatVersion ${SUPPORTED_FORMAT_VERSION + 1}`, ); }); @@ -145,7 +150,10 @@ Deno.test("loader: nested structural types convert recursively", () => { kind: "list", element: { kind: "record", - fields: [{ label: "x", type: { kind: "option", type: { kind: "f64" } } }], + fields: [{ + label: "x", + type: { kind: "option", type: { kind: "f64" } }, + }], }, }, }, @@ -172,7 +180,6 @@ Deno.test("loader: nested structural types convert recursively", () => { }); }); - // --- structured translation verdicts (contracts v0.2 proposal) ------------- Deno.test("loader: envelope errorDetail becomes a TranslateError with phase", () => { @@ -488,7 +495,9 @@ Deno.test("loader: malformed export entry (missing required field per kind) is a assertPlanError( () => loadPlan(minimalPlan({ - exports: [{ kind: "type", name: "t", type: { kind: "resource" } } as never], + exports: [ + { kind: "type", name: "t", type: { kind: "resource" } } as never, + ], })), ".resource must be a number", ); @@ -542,7 +551,10 @@ Deno.test("loader: tampered cache scenario — negative modules[0].offset is ref const wire = minimalPlan({ modules: [{ kind: "embedded", offset: -100, len: 92 }], }); - assertPlanError(() => loadPlan(wire), ".offset must be a non-negative safe integer"); + assertPlanError( + () => loadPlan(wire), + ".offset must be a non-negative safe integer", + ); }); Deno.test("loader: well-formed modules/exports/imports load unaffected", () => { diff --git a/runtime/tests/plan_v3_test.ts b/runtime/tests/plan_v3_test.ts index f77466c..a9c94b7 100644 --- a/runtime/tests/plan_v3_test.ts +++ b/runtime/tests/plan_v3_test.ts @@ -17,15 +17,22 @@ import { assertEq } from "./support/asserts.ts"; import { Table } from "../src/cabi/handles.ts"; -import { loadPlan, PlanError, SUPPORTED_FORMAT_VERSION } from "../src/plan/mod.ts"; +import { + loadPlan, + PlanError, + SUPPORTED_FORMAT_VERSION, +} from "../src/plan/mod.ts"; import type { WirePlan } from "../src/plan/format.ts"; -import { createTrampoline, type TrampolineContext } from "../src/intrinsics/mod.ts"; +import { + createTrampoline, + type TrampolineContext, +} from "../src/intrinsics/mod.ts"; import { createTaskReturn } from "../src/intrinsics/async_builtins.ts"; import { ComponentInstanceState, ErrorContext, - pushCurrentThread, popCurrentThread, + pushCurrentThread, Store, Task, Thread, @@ -35,7 +42,11 @@ import type { ValType } from "../src/cabi/types.ts"; function minimalPlan(overrides: Partial = {}): WirePlan { return { formatVersion: SUPPORTED_FORMAT_VERSION, - producer: { shimVersion: "0", wasmtimeEnviron: "49.0.0-dev+4675ee1", features: [] }, + producer: { + shimVersion: "0", + wasmtimeEnviron: "49.0.0-dev+4675ee1", + features: [], + }, component: { sha256: "0".repeat(64), len: 0 }, modules: [], initializers: [], @@ -57,9 +68,15 @@ function expectPlanError(fn: () => unknown, includes: string): void { try { fn(); } catch (e) { - if (!(e instanceof PlanError)) throw new Error(`expected PlanError, got ${e}`); + if (!(e instanceof PlanError)) { + throw new Error(`expected PlanError, got ${e}`); + } if (!e.message.includes(includes)) { - throw new Error(`expected message to include ${JSON.stringify(includes)}, got: ${e.message}`); + throw new Error( + `expected message to include ${ + JSON.stringify(includes) + }, got: ${e.message}`, + ); } return; } @@ -113,8 +130,16 @@ Deno.test("v3: task-return decls require resultType and build the tuple map", () types: [tupleType as never], trampolines: [decl as never], })); - assertEq(loaded.resultTupleTypes.get(7), 0, "raw TypeTupleIndex -> plan.types"); - assertEq(loaded.resultTupleTypes.get(0), undefined, "no aliasing of the two spaces"); + assertEq( + loaded.resultTupleTypes.get(7), + 0, + "raw TypeTupleIndex -> plan.types", + ); + assertEq( + loaded.resultTupleTypes.get(0), + undefined, + "no aliasing of the two spaces", + ); const missing = { ...decl } as Record; delete missing.resultType; @@ -138,7 +163,10 @@ Deno.test("v3: task-return decls require resultType and build the tuple map", () () => loadPlan(minimalPlan({ types: [tupleType as never, tupleType as never], - trampolines: [decl as never, { ...decl, index: 1, resultType: 1 } as never], + trampolines: [ + decl as never, + { ...decl, index: 1, resultType: 1 } as never, + ], })), "maps to both type 0 and type 1", ); @@ -163,15 +191,24 @@ Deno.test("v3: error-context transfer uses the error-context table space", () => errorContextTableInstance: (i: number) => (i === 0 ? ecSrc : ecDst), } as unknown as TrampolineContext; - const transfer = createTrampoline({ kind: "error-context-transfer", index: 0 } as never, ctx); + const transfer = createTrampoline( + { kind: "error-context-transfer", index: 0 } as never, + ctx, + ); const e = new ErrorContext("boom"); const handle = ecSrc.handles.add(e); const out = transfer(handle, 0, 1) as number; - assertEq(ecDst.handles.get(out) === e, true, "landed in the error-context table's instance"); + assertEq( + ecDst.handles.get(out) === e, + true, + "landed in the error-context table's instance", + ); }); Deno.test("v3: an out-of-range error-context table is a loud PlanError", () => { - const loaded = loadPlan(minimalPlan({ errorContextTables: [{ instance: 0 }] })); + const loaded = loadPlan( + minimalPlan({ errorContextTables: [{ instance: 0 }] }), + ); // The executor's accessor shape, exercised directly: absence must fail // loudly rather than defaulting to table 0 (the `?? 0` this replaced). const accessor = (i: number) => { @@ -192,14 +229,21 @@ Deno.test("v3: error-context transfer refuses a missing table argument", () => { const ctx = { errorContextTableInstance: () => inst, } as unknown as TrampolineContext; - const transfer = createTrampoline({ kind: "error-context-transfer", index: 0 } as never, ctx); + const transfer = createTrampoline( + { kind: "error-context-transfer", index: 0 } as never, + ctx, + ); let refused = false; try { (transfer as (...a: unknown[]) => unknown)(0); } catch { refused = true; } - assertEq(refused, true, "no silent `?? 0` default for a missing table argument"); + assertEq( + refused, + true, + "no silent `?? 0` default for a missing table argument", + ); }); // --------------------------------------------------------------------------- diff --git a/runtime/tests/platform_purity_test.ts b/runtime/tests/platform_purity_test.ts index ced372e..d7ebbfc 100644 --- a/runtime/tests/platform_purity_test.ts +++ b/runtime/tests/platform_purity_test.ts @@ -24,7 +24,9 @@ Deno.test("runtime/src imports no node: specifiers (platform purity)", async () const text = await Deno.readTextFile(file); // Import/export specifiers only — comments mentioning node: APIs are // fine (several document the M3A-1 history deliberately). - for (const m of text.matchAll(/(?:from|import)\s*\(?\s*["'](node:[^"']+)["']/g)) { + for ( + const m of text.matchAll(/(?:from|import)\s*\(?\s*["'](node:[^"']+)["']/g) + ) { offenders.push(`${file.pathname}: ${m[1]}`); } } diff --git a/runtime/tests/resource_lender_park_settle_test.ts b/runtime/tests/resource_lender_park_settle_test.ts index f3bc3e6..64b7379 100644 --- a/runtime/tests/resource_lender_park_settle_test.ts +++ b/runtime/tests/resource_lender_park_settle_test.ts @@ -37,7 +37,11 @@ import { } from "../src/intrinsics/fact_calls.ts"; import type { FactStartScope } from "../src/intrinsics/mod.ts"; import { newStats } from "../src/exec/boundary.ts"; -import { ComponentInstanceState, Store, withActivation } from "../src/task/mod.ts"; +import { + ComponentInstanceState, + Store, + withActivation, +} from "../src/task/mod.ts"; import type { SuspensionPoint } from "../src/jspi/mod.ts"; import { canonResourceDrop, @@ -109,7 +113,8 @@ function mkHarness(): Harness { handleIndex, point() { return store.waiting.find( - (w) => typeof (w as { resume?: unknown }).resume === "function" && + (w) => + typeof (w as { resume?: unknown }).resume === "function" && typeof (w as { abandon?: unknown }).abandon === "function", ) as SuspensionPoint | undefined; }, @@ -130,7 +135,10 @@ function mkHarness(): Harness { // deno-lint-ignore no-explicit-any ? createSyncStartCall({ callback: null }, ctx as any) // deno-lint-ignore no-explicit-any - : createAsyncStartCall({ callback: null, postReturn: null }, ctx as any); + : createAsyncStartCall( + { callback: null, postReturn: null }, + ctx as any, + ); prep( start, @@ -142,10 +150,13 @@ function mkHarness(): Harness { 0, PREPARE_ASYNC_NO_RESULT, ); - return withActivation(callerAmbient, () => - kind === "sync" - ? startCall(calleeBody, 0) - : startCall(calleeBody, 0, 0, START_FLAG_ASYNC_CALLEE)); + return withActivation( + callerAmbient, + () => + kind === "sync" + ? startCall(calleeBody, 0) + : startCall(calleeBody, 0, 0, START_FLAG_ASYNC_CALLEE), + ); }, }; } @@ -156,7 +167,8 @@ function mkHarness(): Harness { * (a callee that merely returned without resolving would trap "task finished * all threads without resolving" instead). */ -const neverResolves = (() => new Promise(() => {})) as unknown as () => CoreValue; +const neverResolves = (() => new Promise(() => {})) as unknown as () => + CoreValue; Deno.test("#102: sync-start-call park releases lenders when abandoned (no produce)", async () => { const h = mkHarness(); @@ -213,7 +225,10 @@ Deno.test("#102: sync-start-call park releases lenders when produce throws", asy const h = mkHarness(); const parked = h.run("sync", neverResolves); assert(parked instanceof Promise, "the caller's activation parked"); - const settled = parked.then(() => "resolved", (e) => `rejected: ${(e as Error).message}`); + const settled = parked.then( + () => "resolved", + (e) => `rejected: ${(e as Error).message}`, + ); const point = h.point(); assert(point !== undefined, "the suspension point is registered as waiting"); @@ -237,7 +252,10 @@ Deno.test("#102: a cancelled resume cannot reach these non-cancellable parks", a const h = mkHarness(); const parked = h.run("sync", neverResolves); assert(parked instanceof Promise, "the caller's activation parked"); - const settled = parked.then(() => "resolved", (e) => `rejected: ${(e as Error).message}`); + const settled = parked.then( + () => "resolved", + (e) => `rejected: ${(e as Error).message}`, + ); const point = h.point(); assert(point !== undefined, "the suspension point is registered as waiting"); @@ -272,7 +290,10 @@ Deno.test("#102: async-start-call determinacy park releases subtask lenders when // eager path is `resource_lender_unwind_test.ts`'s territory). return; } - const settled = parked.then(() => "resolved", (e) => `rejected: ${(e as Error).message}`); + const settled = parked.then( + () => "resolved", + (e) => `rejected: ${(e as Error).message}`, + ); const point = h.point(); assert(point !== undefined, "the caller parked on the determinacy wait"); assertEq(h.handle.numLends, 1); diff --git a/runtime/tests/resource_lifetime_test.ts b/runtime/tests/resource_lifetime_test.ts index aa90c25..e811965 100644 --- a/runtime/tests/resource_lifetime_test.ts +++ b/runtime/tests/resource_lifetime_test.ts @@ -12,7 +12,11 @@ import { canonResourceNew, ResourceTypeInfo, } from "../src/cabi/mod.ts"; -import { ComponentInstanceState, Store, storeQuiescent } from "../src/task/mod.ts"; +import { + ComponentInstanceState, + Store, + storeQuiescent, +} from "../src/task/mod.ts"; import { driveStoreAsync, hostDtorCall } from "../src/exec/boundary.ts"; import { isInstancePoisoned, diff --git a/runtime/tests/same_store_driver_test.ts b/runtime/tests/same_store_driver_test.ts index d724ae8..22e4c48 100644 --- a/runtime/tests/same_store_driver_test.ts +++ b/runtime/tests/same_store_driver_test.ts @@ -44,8 +44,7 @@ function assert(cond: boolean, msg: string): asserts cond { } function fakeInst() { - return { - }; + return {}; } /** A thread parked on an awaitValue promise, as a promising-wrapped guest @@ -133,7 +132,10 @@ Deno.test("a second driver on the same store is not wedged by the incumbent's sp // on purpose — the property is "promptly, not gated on A's host" — and it // is the throw above that carries the regression; this only pins that B // cannot instead be made to dwell for the host's own duration. - assert(elapsed < 2000, `B's driver returned in ${elapsed}ms, expected < 2s`); + assert( + elapsed < 2000, + `B's driver returned in ${elapsed}ms, expected < 2s`, + ); } finally { aDone = true; settleThread(0); diff --git a/runtime/tests/sched_seed_guard_test.ts b/runtime/tests/sched_seed_guard_test.ts index fa00b90..f565555 100644 --- a/runtime/tests/sched_seed_guard_test.ts +++ b/runtime/tests/sched_seed_guard_test.ts @@ -33,8 +33,8 @@ Deno.test("POLYENGINE_SCHED_SEED is readable and engaged by the scheduler", () = raw = Deno.env.get("POLYENGINE_SCHED_SEED"); } catch (err) { throw new Error( - "Deno.env.get(\"POLYENGINE_SCHED_SEED\") threw a permission error " + - "(NotCapable). This means the runtime \"test\" task in " + + 'Deno.env.get("POLYENGINE_SCHED_SEED") threw a permission error ' + + '(NotCapable). This means the runtime "test" task in ' + "runtime/deno.json lost --allow-env=POLYENGINE_SCHED_SEED. " + "readSeed() (src/task/scheduler.ts) deliberately swallows this " + "error and falls back to FIFO in production — which means every " + diff --git a/runtime/tests/settlement_pump_test.ts b/runtime/tests/settlement_pump_test.ts index 4b1b731..e328a39 100644 --- a/runtime/tests/settlement_pump_test.ts +++ b/runtime/tests/settlement_pump_test.ts @@ -30,8 +30,7 @@ function assert(cond: boolean, msg: string): asserts cond { /** The slice of `ComponentInstance` that `Store.tick` touches. */ function fakeInst() { - return { - }; + return {}; } /** A stand-in guest thread: `Store.tick` resumes it whenever `ready()`. */ @@ -62,7 +61,11 @@ class FakeThread { * (boundary.ts: delete from `pendingHostCalls`, deliver, and here "deliver" * readies the guest — never a tick). */ -function hostImport(store: Store, settle: Promise, onSettle: () => void): void { +function hostImport( + store: Store, + settle: Promise, + onSettle: () => void, +): void { const p: Promise = settle.then(() => { store.pendingHostCalls.delete(p); onSettle(); @@ -110,7 +113,8 @@ Deno.test({ }); Deno.test({ - name: "T-2: a self-re-arming host call sustains progress (keep-alive ticker shape)", + name: + "T-2: a self-re-arming host call sustains progress (keep-alive ticker shape)", fn: async () => { const store = new Store(); const ROUNDS = 5; @@ -126,7 +130,11 @@ Deno.test({ done(); return; } - hostImport(store, new Promise((r) => setTimeout(r, 1)), () => guest.wake()); + hostImport( + store, + new Promise((r) => setTimeout(r, 1)), + () => guest.wake(), + ); }); store.startWaiting(guest); @@ -141,7 +149,8 @@ Deno.test({ }); Deno.test({ - name: "T-3: activity arms alone never arm the pump — no ticks, no trap, no spin", + name: + "T-3: activity arms alone never arm the pump — no ticks, no trap, no spin", fn: async () => { const store = new Store(); @@ -176,7 +185,9 @@ Deno.test({ // Modelled on the async arm's rejection continuation (boundary.ts): the // site parks the failure; the pump must neither swallow nor spin on it. - const p: Promise = new Promise((_, rj) => setTimeout(() => rj(boom), 5)) + const p: Promise = new Promise((_, rj) => + setTimeout(() => rj(boom), 5) + ) .then(undefined, (e) => { store.pendingHostCalls.delete(p); store.hostFailure = e; diff --git a/runtime/tests/store_int_range_test.ts b/runtime/tests/store_int_range_test.ts index b32ffca..e8cc54b 100644 --- a/runtime/tests/store_int_range_test.ts +++ b/runtime/tests/store_int_range_test.ts @@ -26,7 +26,11 @@ function assertRangeError(fn: () => void, msg: string): void { } catch (e) { err = e; } - assertEq(err instanceof AssertionError, true, `${msg}: expected AssertionError, got ${err}`); + assertEq( + err instanceof AssertionError, + true, + `${msg}: expected AssertionError, got ${err}`, + ); assertEq( String((err as Error)?.message ?? "").includes("out of range"), true, @@ -89,14 +93,20 @@ Deno.test("storeInt: out-of-range s16 raises the host-precondition error", () => Deno.test("storeInt: out-of-range u32 raises the host-precondition error", () => { const mem = freshMem(); - assertRangeError(() => storeInt(mem, 0x1_0000_0000, 0, 4, false), "u32 over max"); + assertRangeError( + () => storeInt(mem, 0x1_0000_0000, 0, 4, false), + "u32 over max", + ); assertRangeError(() => storeInt(mem, -1, 0, 4, false), "u32 under min"); }); Deno.test("storeInt: out-of-range s64 (bigint) raises the host-precondition error", () => { const mem = freshMem(); assertRangeError(() => storeInt(mem, 1n << 63n, 0, 8, true), "s64 over max"); - assertRangeError(() => storeInt(mem, -(1n << 63n) - 1n, 0, 8, true), "s64 under min"); + assertRangeError( + () => storeInt(mem, -(1n << 63n) - 1n, 0, 8, true), + "s64 under min", + ); }); Deno.test("storeInt: out-of-range u64 (bigint) raises the host-precondition error", () => { diff --git a/runtime/tests/store_list_test.ts b/runtime/tests/store_list_test.ts index f63ba8c..5d0e872 100644 --- a/runtime/tests/store_list_test.ts +++ b/runtime/tests/store_list_test.ts @@ -21,7 +21,10 @@ const listU32: ValType = { kind: "list", element: { kind: "u32" } }; function cxWithHeap(size: number) { const heap = new Heap(size); - return { heap, cx: mkCx(new MemInst(heap.memory, "i32"), "utf8", heap.realloc) }; + return { + heap, + cx: mkCx(new MemInst(heap.memory, "i32"), "utf8", heap.realloc), + }; } /** Store a list value at a fresh spot, return [begin, length] read back. */ diff --git a/runtime/tests/streams_teardown_test.ts b/runtime/tests/streams_teardown_test.ts index 01e1e68..efde958 100644 --- a/runtime/tests/streams_teardown_test.ts +++ b/runtime/tests/streams_teardown_test.ts @@ -293,7 +293,11 @@ Deno.test("#84(b): a JSPI-blocked future reader's suspension rejects with the tr // frame via `blockCurrentActivation`, which hands back a Promise. const syncOpts = { ...f.ctx.options(), async: false }; const syncCtx = { ...f.ctx, options: () => syncOpts }; - const read = createFutureRead({ futureTable: 0, options: 0 }, syncCtx, f.reader); + const read = createFutureRead( + { futureTable: 0, options: 0 }, + syncCtx, + f.reader, + ); const parked = f.run(() => read(f.ri, 0)) as unknown as Promise; assertEq(f.readEnd.state, CopyState.COPYING); assertEq(f.readEnd.hasSyncWaiter, true); @@ -411,7 +415,11 @@ Deno.test("#90: dropping a lowered, never-written host future traps its parked r const ri = lowerInto(host, reader); const readEnd = reader.handles.get(ri) as ReadableFutureEnd; const opts = { ...f.ctx.options(), instance: reader }; - const ctx = { ...f.ctx, componentInstance: () => reader, options: () => opts }; + const ctx = { + ...f.ctx, + componentInstance: () => reader, + options: () => opts, + }; const read = createFutureRead({ futureTable: 0, options: 0 }, ctx, reader); const task = new Task(ASYNC_FT, CALLBACK_OPTS, reader, () => [], () => {}); const thread = new Thread(task, (function* () {})()); @@ -471,8 +479,11 @@ Deno.test("#90: write-then-drop is unchanged", async () => { // The guest reader takes the value. const shared = host.value as unknown as SharedFutureImpl; let taken: CopyResult | null = null; - shared.read({ guest: 1 }, new HostBuffer(null, null, 1) as never, (r) => - taken = r); + shared.read( + { guest: 1 }, + new HostBuffer(null, null, 1) as never, + (r) => taken = r, + ); await w; assertEq(taken, CopyResult.COMPLETED); host.drop(); diff --git a/runtime/tests/task_test.ts b/runtime/tests/task_test.ts index 348a2f6..35213cc 100644 --- a/runtime/tests/task_test.ts +++ b/runtime/tests/task_test.ts @@ -16,12 +16,12 @@ import { driveSyncLift, entryRefusal, EventCode, - packSubtaskResult, - schedulerPolicy, - schedulerSeedForTesting, isInstancePoisoned, notifyInstancePoisoned, + packSubtaskResult, PendingCapability, + schedulerPolicy, + schedulerSeedForTesting, Store, Subtask, SubtaskState, @@ -75,7 +75,6 @@ const STACKFUL_OPTS: TaskOptions = { memory: null, }; - /** * Create a thread whose body needs a reference to the thread itself (every * `canon_lift` body does: `enter_implicit_thread`, `wait_until` and @@ -410,7 +409,11 @@ Deno.test("cm705: stale settled entries are removed", async () => { // Simulate the elsewhere-resumption. store.awaiting.delete(bThread); void a; - assertEq(store.serviceSettled(), false, "removing a stale entry is not progress"); + assertEq( + store.serviceSettled(), + false, + "removing a stale entry is not progress", + ); assertEq(store.settled.length, 0, "but it is removed"); }); @@ -534,7 +537,10 @@ Deno.test("exclusive thread: a callback task takes and releases it", () => { // Identity, not deep equality: Thread objects are cyclic (thread -> task // -> inst -> threads -> thread), which a structural comparison cannot // walk. - assert(inst.exclusiveThread === thread, "the task holds the exclusive thread"); + assert( + inst.exclusiveThread === thread, + "the task holds the exclusive thread", + ); task.start(); task.return_([]); task.exitImplicitThread(thread); diff --git a/runtime/tests/tls_smoke_pins_test.ts b/runtime/tests/tls_smoke_pins_test.ts index 0259c6f..d213b13 100644 --- a/runtime/tests/tls_smoke_pins_test.ts +++ b/runtime/tests/tls_smoke_pins_test.ts @@ -22,8 +22,8 @@ import { fmtValType, ResourceTypeInfo, Table, - valTypeEqual, type ValType, + valTypeEqual, } from "../src/cabi/mod.ts"; import { sameElemType } from "../src/task/streams.ts"; import { loadPlan } from "../src/plan/mod.ts"; @@ -103,7 +103,11 @@ Deno.test("pin: sameElemType survives resource-bearing (cyclic) element types", kind: "future", element: { kind: "result", ok: null, error: { kind: "own", rt: otherRt } }, }; - assertEq(sameElemType(t, different), false, "distinct rt -> unequal, no throw"); + assertEq( + sameElemType(t, different), + false, + "distinct rt -> unequal, no throw", + ); }); Deno.test("pin: fmtValType is cycle-safe and structural", () => { @@ -182,7 +186,11 @@ Deno.test("pin: transfer-borrow works inside a FACT [async-start] window", () => const dst = dstInst.handles.get(out) as ResourceHandle; assertEq(dst.own, false); assertEq(dst.rep, 17); - assertEq(dst.borrowScope === taskScope, true, "drop decrements the callee task"); + assertEq( + dst.borrowScope === taskScope, + true, + "drop decrements the callee task", + ); lenders.releaseLenders(); assertEq(src.numLends, 0, "deliver-resolve releases the lender"); diff --git a/runtime/tests/transcode_high_ptr_test.ts b/runtime/tests/transcode_high_ptr_test.ts index 77fde3a..8511247 100644 --- a/runtime/tests/transcode_high_ptr_test.ts +++ b/runtime/tests/transcode_high_ptr_test.ts @@ -13,10 +13,7 @@ // libcalls.rs takes unsigned pointers. import { assertEq } from "./support/asserts.ts"; -import { - createTranscoder, - TranscodeMemory, -} from "../src/intrinsics/mod.ts"; +import { createTranscoder, TranscodeMemory } from "../src/intrinsics/mod.ts"; // ~2.4 GiB: large enough that an address >= 2^31 is in bounds. V8 reserves // wasm memory lazily on 64-bit hosts; if this allocation fails here, the @@ -101,6 +98,10 @@ Deno.test({ // property write (or, for other ops, `.set()` throws a non-Trap // RangeError) — the correct location is left untouched. const got = [...new Uint8Array(memory.buffer).subarray(dstP, dstP + 4)]; - assertEq(got, [0x41, 0x00, 0xff, 0x00], "expected inflated utf16 at unsigned dst"); + assertEq( + got, + [0x41, 0x00, 0xff, 0x00], + "expected inflated utf16 at unsigned dst", + ); }, }); diff --git a/runtime/tests/translator_from_exports_test.ts b/runtime/tests/translator_from_exports_test.ts index b4d91a1..02810d0 100644 --- a/runtime/tests/translator_from_exports_test.ts +++ b/runtime/tests/translator_from_exports_test.ts @@ -30,7 +30,8 @@ const trivial = await maybeRead(trivialUrl); const ready = shimBytes !== null && trivial !== null; Deno.test({ - name: "fromExports: a native wasm-module import translates identically to create(bytes)", + name: + "fromExports: a native wasm-module import translates identically to create(bytes)", ignore: !ready, fn: async () => { // Deno's ESM wasm integration: dynamic import instantiates the