diff --git a/AGENTS.md b/AGENTS.md index 28e7982..1082264 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,24 +28,8 @@ and each CI job runs exactly one `gha::` recipe (`just ci` = exactly CI; affect; the full pass before commit is: ```sh -just gates # everything below, in this order -``` - -```sh -just build test-rust # cargo build --workspace; translator-shim/bindgen/testgen tests -just test-runtime # runtime check + tests (deps: shim, fixtures, corpus) -just test-protocol -just test-wasi test-ct-runner -just test-sockets-node # sockets fragment's node backend on pinned Node -just test-bundle # embedder-bundle release asset -just publish-check # deno publish --dry-run: the JSR publish checks, no upload -just examples test-translate # embedder examples; build-time translation CLI -just conformance # official CM suite, Deno lane -just sched-seeds # seeded-shuffle reruns: POLYENGINE_SCHED_SEED=1, =4242 (FIFO when unset) -just shells # pinned engine/runtime lanes: sm + node everywhere, jsc on x64, bun findings-only -just browsers # chromium + firefox lanes incl. worker/shared-worker realm rows (`just browsers-install` once) -just smoke-tls # polymorph-tls suite (issue #18) -just smoke-c0 # consumer smoke legs +just gates # everything below, in this order; see the justfile (or + # `just --list`) for the recipe list and per-gate one-liners ``` Conformance discipline: the harness fails loudly on unexpected failures *and* diff --git a/Cargo.lock b/Cargo.lock index 8bb0e95..3c16f52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "wit-bindgen-core", "wit-parser", ] @@ -595,12 +594,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8parse" version = "0.2.2" @@ -633,17 +626,6 @@ dependencies = [ "wasmparser 0.255.0", ] -[[package]] -name = "wasmparser" -version = "0.251.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" -dependencies = [ - "bitflags", - "indexmap", - "semver", -] - [[package]] name = "wasmparser" version = "0.252.0" @@ -774,22 +756,11 @@ dependencies = [ "windows-link", ] -[[package]] -name = "wit-bindgen-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4738d1c9a78e97bc7f664bfafd5d8e67d7bb26faa5c41e6d628e8bbdad3ec351" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - [[package]] name = "wit-parser" -version = "0.251.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e960732e824fab95099971a09e638979347c94ca48568d3c854c945729196947" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -800,8 +771,8 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "unicode-xid", - "wasmparser 0.251.0", + "unicode-ident", + "wasmparser 0.252.0", ] [[package]] diff --git a/contracts/digest.md b/contracts/digest.md index d3dc76f..d30cb60 100644 --- a/contracts/digest.md +++ b/contracts/digest.md @@ -2,7 +2,7 @@ The digest is the skew-protection handshake of docs/architecture.md §9: bindings generated from WIT embed an expected digest; the generated typed `instantiate` recomputes -it from the loaded plan and fails fast with a structural diff on mismatch, +it from the loaded plan and fails fast on mismatch, before any guest code runs (contracts/embedder-api.md §"Module wiring and instantiation" — the runtime's untyped `instantiate` names no world, so it does not verify). A digest match must imply ABI-shape compatibility for diff --git a/crates/bindgen/Cargo.toml b/crates/bindgen/Cargo.toml index 74d5520..733991e 100644 --- a/crates/bindgen/Cargo.toml +++ b/crates/bindgen/Cargo.toml @@ -13,30 +13,11 @@ name = "bindgen" path = "src/lib.rs" [dependencies] -# Version pinning (docs/architecture.md §9 pins wit-parser to the wasmtime-environ -# cadence): wasmtime-environ =47.0.3 (crates/translator-shim) pins -# wasmparser 0.252.0 exactly. wit-parser and wasmparser are released in -# lockstep as part of the wasm-tools workspace (same version number across -# the whole workspace's crates), so wit-parser's version number IS the -# wasm-tools release train version. There is no wit-parser 0.252.0 release -# whose *wit-bindgen-core* counterpart also exists (wit-bindgen has its own, -# slower release cadence and vendors a specific wasm-tools/wit-parser -# version per release — see wit-bindgen-core's own Cargo.toml pins). -# Surveyed via `cargo add --dry-run` against the crates.io index: -# wit-bindgen-core 0.55.0 -> wit-parser 0.246.2 (wasmparser 0.246.2) -# wit-bindgen-core 0.58.0 -> wit-parser 0.251.0 (wasmparser 0.251.0) -# wit-bindgen-core 0.60.0 -> wit-parser 0.254.0 (wasmparser 0.254.0) -# 0.251.0 is one release behind wasmtime-environ's 0.252.0 pin (0.254.0 -# would be two releases ahead). Picked the nearest-and-not-newer pair -# (0.58.0 / 0.251.0): WIT feature-resolution semantics for a slightly older -# wasm-tools release are a subset of a newer one, so parsing our fixture -# corpus (no bleeding-edge WIT features) is unaffected; being *behind* -# wasmtime-environ is the conservative direction (never claims to resolve a -# feature wasmtime's frontend doesn't also know about). Bump both together -# when translator-shim's wasmtime-environ pin moves and a matching -# wit-bindgen-core release exists. -wit-parser = { version = "=0.251.0", features = ["serde"] } -wit-bindgen-core = "=0.58.0" +# wit-parser is pinned to match crates/translator-shim's wasmparser pin +# exactly (docs/architecture.md §9): both ride the wasm-tools workspace's +# lockstep release train, so a single version number covers both crates. +# Bump this pin together with translator-shim's wasmtime-environ pin. +wit-parser = { version = "=0.252.0", features = ["serde"] } anyhow = "1" sha2 = { version = "0.10", default-features = false, features = ["std"] } serde = { version = "1", features = ["derive"] } diff --git a/crates/bindgen/src/codegen.rs b/crates/bindgen/src/codegen.rs index 34f2ea0..1c5a462 100644 --- a/crates/bindgen/src/codegen.rs +++ b/crates/bindgen/src/codegen.rs @@ -7,22 +7,24 @@ //! behavior is emitted or assumed to exist yet (the runtime's own facade is //! a separate crate) — every generated file must `deno check` standalone. //! -//! Built on `wit_bindgen_core::Source`/`Files` for text accumulation, still -//! a hand-written generator, not a `WorldGenerator` trait implementation, -//! not a `WorldGenerator` trait implementation (CONTRACT: docs/architecture.md §9 doesn't -//! mandate the trait specifically, only "built on wit-bindgen-core"). +//! Built on a local `Source` buffer (`crate::source`) matching +//! `wit_bindgen_core::Source`'s brace-tracking auto-indent behavior — still +//! a hand-written generator, not a `WorldGenerator` trait implementation +//! (CONTRACT: docs/architecture.md §9 doesn't mandate the trait +//! specifically, only "built on wit-bindgen-core"; the vocabulary carries +//! over even though the dependency itself does not). use std::collections::BTreeSet; use std::fmt::Write as _; use anyhow::{bail, Context, Result}; -use wit_bindgen_core::Source; use wit_parser::{ Function, FunctionKind, Handle, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey, }; use crate::digest; +use crate::source::Source; /// Default import base for generated bindings: the versioned JSR specifier /// for `@polyengine/runtime`, with the version derived at build time from @@ -271,7 +273,7 @@ pub fn generate( for (key, item) in w.imports.iter().chain(w.exports.iter()) { collect_and_emit_types(resolve, key, item, &mut emitted, &mut type_decls)?; } - src.push_str(&type_decls); + src.push_str(type_decls.as_str()); // ---- Resource class declarations, in first-encountered order. for rid in &resource_order { @@ -333,10 +335,11 @@ pub fn generate( * Verifies the loaded plan's world digest against `WORLD_DIGEST`\n\ * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md\n\ * §\"Module wiring and instantiation\"), throwing\n\ - * `WorldDigestMismatchError` with the structural diff on skew — no\n\ - * guest code has run when that throws. Accepts the same sources as\n\ - * the runtime `instantiate` (pre-translated artifacts, an envelope\n\ - * via `artifactsFromEnvelope`, or component bytes plus a translator).\n\ + * `WorldDigestMismatchError` — fails fast on skew, no\n\ + * structural diff — no guest code has run when that throws. Accepts\n\ + * the same sources as the runtime `instantiate` (pre-translated\n\ + * artifacts, an envelope via `artifactsFromEnvelope`, or component\n\ + * bytes plus a translator).\n\ *\n\ * Use `bind` instead only when the plan was verified already. */\n\ export async function instantiate(\n\ diff --git a/crates/bindgen/src/lib.rs b/crates/bindgen/src/lib.rs index d823ced..879b381 100644 --- a/crates/bindgen/src/lib.rs +++ b/crates/bindgen/src/lib.rs @@ -1,2 +1,3 @@ pub mod codegen; pub mod digest; +mod source; diff --git a/crates/bindgen/src/main.rs b/crates/bindgen/src/main.rs index 80fd2ed..b75da4e 100644 --- a/crates/bindgen/src/main.rs +++ b/crates/bindgen/src/main.rs @@ -56,41 +56,6 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Generate the typed TS facade for a world. - Generate { - wit_path: PathBuf, - #[arg(long)] - world: Option, - #[arg(long)] - out: PathBuf, - - /// Import base the generated bindings resolve the runtime through. - /// - /// Resolution rule, by what the base addresses: a path or URL - /// specifier addresses a *file* and yields `{base}/{module}/mod.ts`; a - /// bare or registry specifier addresses an entry in a package's - /// `exports` map — runtime/deno.json declares `./plan` / `./digest` / - /// `./embedder` — and yields `{base}/{module}`. - /// - /// Concretely, file-addressed when the base starts with `.`, `/`, - /// `file:`, `http://` or `https://`; export-addressed otherwise. An - /// unrecognized scheme falls back to export-addressed, so a future - /// registry scheme works by default while anything file-like must be - /// spelled as a path, a `file:` URL, or an `http(s)` URL. - /// - /// Useful non-default values: `../../../src` (in-repo fixture - /// regeneration), `@polyengine/runtime` (consumers using an import map - /// or npm rather than a `jsr:` specifier). - /// - /// The default's version is derived from runtime/deno.json at build - /// time. Caveat: this repo's manifests always carry the NEXT release, - /// so on a development checkout between releases the default pins a - /// version that is not published yet (semver ranges never resolve to - /// the `-pre.g` prereleases) — bindings from a dev checkout - /// belong to that unreleased line. - #[arg(long, value_name = "PREFIX", default_value = bindgen::codegen::DEFAULT_IMPORT_BASE)] - import_base: String, - }, /// Print only the canonical digest (debugging / cross-language tests). Digest { wit_path: PathBuf, @@ -105,12 +70,6 @@ enum Command { fn main() -> Result<()> { let cli = Cli::parse(); match cli.cmd { - Some(Command::Generate { - wit_path, - world, - out, - import_base, - }) => generate(&wit_path, world.as_deref(), &out, &import_base), Some(Command::Digest { wit_path, world, diff --git a/crates/bindgen/src/source.rs b/crates/bindgen/src/source.rs new file mode 100644 index 0000000..9288dd1 --- /dev/null +++ b/crates/bindgen/src/source.rs @@ -0,0 +1,86 @@ +//! A minimal local replacement for `wit_bindgen_core::Source` (issue: drop +//! the `wit-bindgen-core` dependency, whose only use in this crate was this +//! type as a string-accumulation buffer). Reproduces its brace-tracking +//! auto-indent behavior exactly (verified byte-for-byte by +//! `crates/bindgen/tests/codegen_snapshot.rs`): each `push_str` line gets the +//! current indent prepended, a line ending in `{` bumps the indent for +//! subsequent lines, a line starting with `}` drops the indent (and trims a +//! trailing two-space indent already written for that line), and text +//! recognized as a line comment (`//`) suspends brace tracking until the next +//! newline — `codegen.rs` uses `\x20` escapes on intentional `{`/`}` +//! characters that are not real braces to defeat this exact tracking. +//! +//! Source: wit-bindgen-core 0.58.0 `src/source.rs` (`push_str`), reproduced +//! under upstream's license (this crate is Apache-2.0; wit-bindgen-core is +//! Apache-2.0 WITH LLVM-exception). + +use std::fmt; + +#[derive(Default)] +pub struct Source { + s: String, + indent: usize, + in_line_comment: bool, + continuing_line: bool, +} + +impl Source { + pub fn push_str(&mut self, src: &str) { + let lines = src.lines().collect::>(); + for (i, line) in lines.iter().enumerate() { + if !self.continuing_line { + if !line.is_empty() { + for _ in 0..self.indent { + self.s.push_str(" "); + } + } + self.continuing_line = true; + } + + let trimmed = line.trim(); + if trimmed.starts_with("//") { + self.in_line_comment = true; + } + + if !self.in_line_comment { + if trimmed.starts_with('}') && self.s.ends_with(" ") { + self.s.pop(); + self.s.pop(); + } + } + self.s.push_str(if lines.len() == 1 { + line + } else { + line.trim_start() + }); + if !self.in_line_comment { + if trimmed.ends_with('{') { + self.indent += 1; + } + if trimmed.starts_with('}') { + self.indent = self.indent.saturating_sub(1); + } + } + if i != lines.len() - 1 || src.ends_with('\n') { + self.newline(); + } + } + } + + fn newline(&mut self) { + self.in_line_comment = false; + self.continuing_line = false; + self.s.push('\n'); + } + + pub fn as_str(&self) -> &str { + &self.s + } +} + +impl fmt::Write for Source { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.push_str(s); + Ok(()) + } +} diff --git a/crates/bindgen/tests/codegen_snapshot.rs b/crates/bindgen/tests/codegen_snapshot.rs index cc8efca..2838e25 100644 --- a/crates/bindgen/tests/codegen_snapshot.rs +++ b/crates/bindgen/tests/codegen_snapshot.rs @@ -7,7 +7,7 @@ //! Regenerate after an intentional codegen change: //! ```text //! for w in hello values resources async-probe stream-echo future-user; do -//! cargo run -p bindgen -- generate examples/guests/$w/wit --world $w \ +//! cargo run -p bindgen -- examples/guests/$w/wit --world $w \ //! --out runtime/tests/bindgen/generated/$w.ts --import-base ../../../src //! done //! ``` diff --git a/ct-runner/deno.json b/ct-runner/deno.json index ad91e59..05b4699 100644 --- a/ct-runner/deno.json +++ b/ct-runner/deno.json @@ -1,10 +1,8 @@ { "name": "@polyengine/ct-runner", - "version": "0.5.2", + "version": "0.6.0", "exports": { - ".": "./src/mod.ts", - "./imports": "./src/import-analysis.ts", - "./run": "./src/run-suite.ts" + ".": "./src/mod.ts" }, "tasks": { "test": "deno test --allow-read=..,/tmp --allow-write=/tmp --allow-run", diff --git a/ct-runner/src/mod.ts b/ct-runner/src/mod.ts index 2ae5f06..5c375ef 100644 --- a/ct-runner/src/mod.ts +++ b/ct-runner/src/mod.ts @@ -11,7 +11,6 @@ export { type RunCounts, runSuite, type RunSuiteOptions, - TESTS_INTERFACE, } from "./run-suite.ts"; export { @@ -20,16 +19,3 @@ export { MissingImportsError, requireImportsResolved, } from "./import-analysis.ts"; - -export { Context, TEST_CONTEXT_INTERFACE, testContextImportRecord } from "./context.ts"; - -export { - applies, - collectTagsSections, - firstExcluding, - loadTagsInventory, - parseTagsRecords, - TAGS_SECTION, - type TagsInventory, - tagsOf, -} from "./tags.ts"; diff --git a/ct-runner/src/run-suite.ts b/ct-runner/src/run-suite.ts index a8aca32..ec52f6e 100644 --- a/ct-runner/src/run-suite.ts +++ b/ct-runner/src/run-suite.ts @@ -443,5 +443,3 @@ async function findByName(list: any[], name: string, hint?: number): Promise, list\, tuple | ~24 KB | | `resources.component.wasm` | [`guests/resources/wit/world.wit`](guests/resources/wit/world.wit) | interface `counters`: `counter` resource (constructor, `increment`, `get`, static `merge`) + free funcs over own/borrow handles (`make-counter`, `sum-both`, `bump`, `consume`) + `live-counters` (observes destructor runs) | ~24 KB | | `async-probe.component.wasm` | [`guests/async-probe/wit/world.wit`](guests/async-probe/wit/world.wit) | CM 0.3 async: `wait-then-double: async func` (yields once), `sum-stream: async func(stream)`, `future-add: async func(future, u32)` | ~57 KB | -| `yield-only.component.wasm` | [`guests/yield-only/wit/world.wit`](guests/yield-only/wit/world.wit) | Pure callback-ABI exerciser: `yield-n-times: async func(count: u32) -> u32` (no I/O) | ~40 KB | | `context-user.component.wasm` | [`guests/context-user/wit/world.wit`](guests/context-user/wit/world.wit) | Context-local-storage (slot 0) via interleaved concurrent activations: `interleave: async func(count: u32) -> u32` (spawns `count` locally-concurrent tasks, each yielding a different number of times) | ~48 KB | -| `backpressure-probe.component.wasm` | [`guests/backpressure-probe/wit/world.wit`](guests/backpressure-probe/wit/world.wit) | `toggle-around-yield: async func(x: u32) -> u32` (asserts backpressure, yields, clears backpressure) | ~40 KB | | `stream-echo.component.wasm` | [`guests/stream-echo/wit/world.wit`](guests/stream-echo/wit/world.wit) | `echo-doubled: async func(input: stream) -> stream` — consumes AND produces a stream in one export | ~60 KB | | `future-user.component.wasm` | [`guests/future-user/wit/world.wit`](guests/future-user/wit/world.wit) | `double-future: async func(f: future) -> u32` (awaits an imported future); `make-future: async func(x: u32) -> future` (resolves an exported one) | ~64 KB | | `future-import.component.wasm` | [`guests/future-import/wit/world.wit`](guests/future-import/wit/world.wit) | Host imports with future-bearing results (contracts/embedder-api.md §"Streams and futures"; the `wasi:sockets@0.3` TCP shapes reduced to `u32`): `next-value: func() -> future`, `send-sink: func(stream) -> future`, `recv-pair: func() -> tuple, future>`, driven by `run-next`/`run-send`/`run-recv` exports (`run-send` writes the stream only after the sync import returns — the livelock probe) | ~64 KB | @@ -125,19 +123,20 @@ here.** Details: ## Async corpus expansion: demand-side inventory -Five targeted guests (`yield-only`, `context-user`, `backpressure-probe`, -`stream-echo`, `future-user`) were added to give the task-core/scheduler and -streams phases concrete, minimal fixtures per canonical built-in. Canonical -imports per guest (`wasm-tools print *.component.wasm | grep -oE -'\[[a-z0-9_-]+\]' | sort -u`): +Guests were added to give the task-core/scheduler and streams phases +concrete, minimal fixtures per canonical built-in. Canonical imports per +guest (`wasm-tools print *.component.wasm | grep -oE '\[[a-z0-9_-]+\]' | +sort -u`). The common base set — `async-lift`, `callback`, +`context.{get,set}`(slot 0), `task.{cancel,return}`, +`waitable-set.{new,poll,drop}`, `waitable.join`, and **no `canon yield`** +(`wit_bindgen::yield_async()` is implemented via the callback return-code +protocol, not the `yield` builtin) — is shared by every guest below: -| Guest | Canonical built-ins imported | +| Guest | Canonical built-ins imported beyond the base set | |---|---| -| `yield-only` | `async-lift`, `callback`, `context.{get,set}`(slot 0), `task.{cancel,return}`, `waitable-set.{new,poll,drop}`, `waitable.join`. **No `canon yield`** — confirms (again, isolated from the async-probe guest's other machinery) that `wit_bindgen::yield_async()` is implemented via the callback return-code protocol, not the `yield` builtin. | -| `context-user` | Same set as `yield-only` — `spawn_local`'s locally-concurrent tasks are still driven by the one export's callback-ABI event loop; no additional canonical built-ins are needed to interleave them. This means context-slot isolation across interleaved activations is entirely a **guest-side** (wit-bindgen runtime) concern from the host's point of view — the host only ever sees one `context.get`/`context.set` pair per callback invocation, exactly as for a single non-interleaved task. | -| `backpressure-probe` | `yield-only`'s set **plus** `backpressure.inc`, `backpressure.dec` — the only guest in the corpus that exercises these. | -| `stream-echo` | `yield-only`'s set **plus** `async-lower` and the full `stream.*` suite: `stream.new`, `stream.read`, `stream.write`, `stream.cancel-read`, `stream.cancel-write`, `stream.drop-readable`, `stream.drop-writable`. `async-lower` appears here (and in `future-user`) but not in the pure-yield/backpressure guests — worth the streams phase confirming why (candidate explanation: the generated stream-forwarding task itself contains an async call shape lowered via `canon lower ... async`, from `spawn_local`'s internal task machinery, but this needs the streams-phase owner to confirm against `definitions.py`, not asserted here). | -| `future-user` | `yield-only`'s set **plus** `async-lower` and the full `future.*` suite: `future.new`, `future.read`, `future.write`, `future.cancel-read`, `future.cancel-write`, `future.drop-readable`, `future.drop-writable`. | +| `context-user` | None — `spawn_local`'s locally-concurrent tasks are still driven by the one export's callback-ABI event loop; no additional canonical built-ins are needed to interleave them. This means context-slot isolation across interleaved activations is entirely a **guest-side** (wit-bindgen runtime) concern from the host's point of view — the host only ever sees one `context.get`/`context.set` pair per callback invocation, exactly as for a single non-interleaved task. | +| `stream-echo` | `async-lower` and the full `stream.*` suite: `stream.new`, `stream.read`, `stream.write`, `stream.cancel-read`, `stream.cancel-write`, `stream.drop-readable`, `stream.drop-writable`. `async-lower` appears here (and in `future-user`) but not in the pure-yield guests — worth the streams phase confirming why (candidate explanation: the generated stream-forwarding task itself contains an async call shape lowered via `canon lower ... async`, from `spawn_local`'s internal task machinery, but this needs the streams-phase owner to confirm against `definitions.py`, not asserted here). | +| `future-user` | `async-lower` and the full `future.*` suite: `future.new`, `future.read`, `future.write`, `future.cancel-read`, `future.cancel-write`, `future.drop-readable`, `future.drop-writable`. | **Stream-producer viability (wit-bindgen 0.60.0, stable Rust 1.96):** `wit_stream::new()` (the per-world generated wrapper around @@ -153,13 +152,13 @@ answers the task brief's open question: **producing a stream from a wit-bindgen 0.60 Rust guest needs no unstable feature, only the already-established `async-spawn` pattern used for `future-user`.** -All five guests build with `cargo build --release --target +All guests build with `cargo build --release --target wasm32-unknown-unknown` on stable Rust 1.96, validate with `wasm-tools validate --features component-model,cm-async` (wasm-tools 1.247), and -round-trip their worlds via `wasm-tools component wit`. `yield-only`, -`context-user`, and `backpressure-probe` are also smoke-run in `build.sh` via -`wasmtime run --invoke`; `stream-echo`/`future-user` share the CLI limitation -noted above (WAVE has no stream/future literals) and await the host harness. +round-trip their worlds via `wasm-tools component wit`. `context-user` is +also smoke-run in `build.sh` via `wasmtime run --invoke`; `stream-echo`/ +`future-user` share the CLI limitation noted above (WAVE has no +stream/future literals) and await the host harness. ## Notes for the host implementation diff --git a/examples/build.sh b/examples/build.sh index b4ffa47..904c9cd 100755 --- a/examples/build.sh +++ b/examples/build.sh @@ -20,7 +20,7 @@ TARGET=wasm32-unknown-unknown BUILD_DIR=guests/build export CARGO_TARGET_DIR="$PWD/guests/target" -GUESTS="hello values resources async-probe yield-only context-user backpressure-probe stream-echo stream-pass future-user future-import resource-stream tcp-echo http-fetch test-suite fs-probe net-probe cancel-import" +GUESTS="hello values resources async-probe context-user stream-echo stream-pass future-user future-import resource-stream tcp-echo http-fetch test-suite fs-probe net-probe cancel-import" # Most guests are pure computational reactors on wasm32-unknown-unknown; # fs-probe and net-probe build for wasm32-wasip2 ON PURPOSE — std::fs / @@ -37,7 +37,7 @@ target_for() { # CM 0.3 async guests additionally need the cm-async feature). features_for() { case "$1" in - async-probe|yield-only|context-user|backpressure-probe|stream-echo|stream-pass|future-user|future-import|resource-stream|tcp-echo|http-fetch|test-suite|cancel-import) + async-probe|context-user|stream-echo|stream-pass|future-user|future-import|resource-stream|tcp-echo|http-fetch|test-suite|cancel-import) echo "component-model,cm-async" ;; *) echo "component-model" ;; esac @@ -79,9 +79,7 @@ if command -v wasmtime >/dev/null 2>&1; then # Component Model 0.3 async export (callback ABI): runs on wasmtime 47 # with default flags; exercises yield suspension + task.return. check '42' async-probe.component.wasm 'wait-then-double(21)' - check '3' yield-only.component.wasm 'yield-n-times(3)' check '6' context-user.component.wasm 'interleave(4)' - check '5' backpressure-probe.component.wasm 'toggle-around-yield(5)' echo "smoke run OK" else echo "(wasmtime not found; skipping smoke run)" diff --git a/examples/guests/backpressure-probe/Cargo.lock b/examples/guests/backpressure-probe/Cargo.lock deleted file mode 100644 index b3f2f51..0000000 --- a/examples/guests/backpressure-probe/Cargo.lock +++ /dev/null @@ -1,337 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "guest-backpressure-probe" -version = "0.1.0" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "macro-string" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "wasm-encoder" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" -dependencies = [ - "bitflags", - "hashbrown", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a301904d6657d6364c758d869e5389d05d393b16d5b65db60b4f03cbe71bb80d" -dependencies = [ - "bitflags", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48521bb96e56cbb9e031ad306c80dc20e89e69e49ada02781ee06f60fbcb545f" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df3fe9b9a0066f82fd0c2f07f79d0ffc0f85c53fa5f998516d8722712479042" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.119", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d689c4bc9d6af067c651cfba444766343c9a4bdef15fad1e2efab95c0c277af0" -dependencies = [ - "anyhow", - "macro-string", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.119", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" -dependencies = [ - "anyhow", - "hashbrown", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-ident", - "wasmparser", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/guests/backpressure-probe/Cargo.toml b/examples/guests/backpressure-probe/Cargo.toml deleted file mode 100644 index 699b2f0..0000000 --- a/examples/guests/backpressure-probe/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "guest-backpressure-probe" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wit-bindgen = "=0.60.0" - -[profile.release] -opt-level = "s" -lto = true -codegen-units = 1 -panic = "abort" -strip = "debuginfo" - -# Opt out of the repository's root cargo workspace. -[workspace] diff --git a/examples/guests/backpressure-probe/src/lib.rs b/examples/guests/backpressure-probe/src/lib.rs deleted file mode 100644 index 0b5f3b9..0000000 --- a/examples/guests/backpressure-probe/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! `backpressure-probe` guest: toggles `backpressure.inc`/`backpressure.dec` -//! around a genuine suspension point. - -wit_bindgen::generate!({ - world: "backpressure-probe", - async: true, -}); - -struct Component; - -impl Guest for Component { - async fn toggle_around_yield(x: u32) -> u32 { - wit_bindgen::backpressure_inc(); - wit_bindgen::yield_async().await; - wit_bindgen::backpressure_dec(); - x - } -} - -export!(Component); diff --git a/examples/guests/backpressure-probe/wit/world.wit b/examples/guests/backpressure-probe/wit/world.wit deleted file mode 100644 index 1b916bd..0000000 --- a/examples/guests/backpressure-probe/wit/world.wit +++ /dev/null @@ -1,12 +0,0 @@ -package polyengine:backpressure-probe; - -/// Exercises the canonical `backpressure.inc`/`backpressure.dec` built-ins -/// (docs/architecture.md §6: backpressure is a direct port of the reference structures) -/// around a suspension point, so the host can observe backpressure state -/// toggling independently of task resolution. -world backpressure-probe { - /// Sets backpressure, yields once (a real suspension point while - /// backpressure is asserted), clears backpressure, then returns `x` - /// unchanged. - export toggle-around-yield: async func(x: u32) -> u32; -} diff --git a/examples/guests/yield-only/Cargo.lock b/examples/guests/yield-only/Cargo.lock deleted file mode 100644 index c4418bd..0000000 --- a/examples/guests/yield-only/Cargo.lock +++ /dev/null @@ -1,337 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "guest-yield-only" -version = "0.1.0" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "macro-string" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "wasm-encoder" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" -dependencies = [ - "bitflags", - "hashbrown", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a301904d6657d6364c758d869e5389d05d393b16d5b65db60b4f03cbe71bb80d" -dependencies = [ - "bitflags", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48521bb96e56cbb9e031ad306c80dc20e89e69e49ada02781ee06f60fbcb545f" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df3fe9b9a0066f82fd0c2f07f79d0ffc0f85c53fa5f998516d8722712479042" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.119", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d689c4bc9d6af067c651cfba444766343c9a4bdef15fad1e2efab95c0c277af0" -dependencies = [ - "anyhow", - "macro-string", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.119", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" -dependencies = [ - "anyhow", - "hashbrown", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-ident", - "wasmparser", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/guests/yield-only/Cargo.toml b/examples/guests/yield-only/Cargo.toml deleted file mode 100644 index f0ad5bf..0000000 --- a/examples/guests/yield-only/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "guest-yield-only" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wit-bindgen = "=0.60.0" - -[profile.release] -opt-level = "s" -lto = true -codegen-units = 1 -panic = "abort" -strip = "debuginfo" - -# Opt out of the repository's root cargo workspace. -[workspace] diff --git a/examples/guests/yield-only/src/lib.rs b/examples/guests/yield-only/src/lib.rs deleted file mode 100644 index 9c32ac4..0000000 --- a/examples/guests/yield-only/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! `yield-only` guest: pure callback-ABI exerciser. -//! -//! Isolates the suspend/resume protocol (canonical `yield` builtin) from -//! everything else in the async surface (context slots, backpressure, -//! streams/futures) — see `context-user`/`backpressure-probe`/`stream-echo`/ -//! `future-user` for those. - -wit_bindgen::generate!({ - world: "yield-only", - async: true, -}); - -struct Component; - -impl Guest for Component { - async fn yield_n_times(count: u32) -> u32 { - for _ in 0..count { - wit_bindgen::yield_async().await; - } - count - } -} - -export!(Component); diff --git a/examples/guests/yield-only/wit/world.wit b/examples/guests/yield-only/wit/world.wit deleted file mode 100644 index 06dbb4e..0000000 --- a/examples/guests/yield-only/wit/world.wit +++ /dev/null @@ -1,11 +0,0 @@ -package polyengine:yield-only; - -/// Pure callback-ABI exerciser: an export that suspends N times via the -/// canonical `yield` builtin (wit_bindgen's `yield_async`) then returns. -/// No I/O, no context/backpressure use — isolates the callback ABI's -/// suspend/resume protocol from everything else in the async surface. -world yield-only { - /// Yields `count` times (canonical `yield` builtin, once per iteration), - /// then returns `count`. - export yield-n-times: async func(count: u32) -> u32; -} diff --git a/harness/deno.json b/harness/deno.json index 223f3b4..7a94a09 100644 --- a/harness/deno.json +++ b/harness/deno.json @@ -1,21 +1,8 @@ { - "exports": { - "./executor": "./src/executor.ts", - "./runner": "./src/runner.ts", - "./runtime-executor": "./src/runtime-executor.ts", - "./schema": "./src/schema.ts", - "./summary": "./src/summary.ts", - "./value-mapping": "./src/value-mapping.ts", - "./xfail": "./src/xfail.ts" - }, "tasks": { "gen": "cargo run -q -p testgen --manifest-path ../Cargo.toml", "shim-check": "test -f ../target/wasm32-unknown-unknown/release/translator_shim.wasm || cargo build -p translator-shim --manifest-path ../Cargo.toml --target wasm32-unknown-unknown --release", "test": "deno test --allow-read=.. --allow-env=CONFORMANCE_EXECUTOR,POLYENGINE_SCHED_SEED tests/", - "conformance": "deno task gen && deno task shim-check && deno task test", - "browser:bundle": "deno run -A ../tools/browser/bundle.ts", - "browser:chromium": "deno task gen && deno task shim-check && deno run -A ../tools/browser/run-lane.ts chromium", - "browser:firefox": "deno task gen && deno task shim-check && deno run -A ../tools/browser/run-lane.ts firefox", - "browser:webkit": "deno task gen && deno task shim-check && deno run -A ../tools/browser/run-lane.ts webkit" + "conformance": "deno task gen && deno task shim-check && deno task test" } } diff --git a/harness/src/xfail.ts b/harness/src/xfail.ts index b887bf0..7b8b6d7 100644 --- a/harness/src/xfail.ts +++ b/harness/src/xfail.ts @@ -395,10 +395,6 @@ export const XFAIL: XfailEntry[] = [ // fidelity, instance poisoning, instantiation-time task context, and one // shim decoder gap). // ===================================================================== - // --- async/async-calls-sync.json: GREEN under jspi auto-detection - // (the jspi flip); entries pruned. --- - // --- async/big-interleaving-test.json: GREEN under jspi auto-detection - // (the jspi flip); entry pruned. --- // --- async/builtin-trap-poisons-instance.json: root cause: STREAMS --- // --- async/cancel-and-exclusive-lock.json: CM#707 "always deliver // cancellation as soon as possible" (third_party/component-model commit @@ -425,34 +421,11 @@ export const XFAIL: XfailEntry[] = [ "finds no ready thread; cm707-cancel, https://github.com/polymorph-components/polyengine/issues/250", }, // --- async/cancel-stream.json: root cause: STREAMS --- - // --- async/cancel-subtask.json: GREEN under jspi auto-detection - // (the jspi flip); entry pruned. --- - // --- async/cancellable.json: GREEN under jspi auto-detection (the jspi flip: - // request_cancellation now finds cancellable SuspensionPoints, and the - // async subtask.cancel waits for callee determinacy); entry pruned. Still - // GREEN after the CM#705 pin advance (polyengine#173): the dispatch - // predicted a single failing final assert here (cm707-cancel class, - // https://github.com/polymorph-components/polyengine/issues/250) but both of this file's commands pass — - // DEVIATION FROM PREDICTION, no entry added. --- // --- async/closed-stream.json: root cause: STREAMS --- // --- async/cross-abi-calls.json: root cause: FACT-ASYNC --- // --- async/cross-task-future.json: root cause: STREAMS --- - // --- async/deadlock.json: GREEN under jspi auto-detection (the jspi flip: the - // driver's deadlock verdict now fires with wasmtime's trap text); entry - // pruned. --- - // --- async/dont-block-start.json: GREEN under jspi auto-detection (the - // jspi flip: a start-function SuspendError maps to "cannot block a synchronous - // task before returning"); entry pruned. --- // --- async/drop-cross-task-borrow.json: root cause: FACT-ASYNC --- - // lines 305/307 GREEN after the #18 tls-smoke fixes (FACT [async-start] - // borrow window + ResourceTypeInfo unification); line 309 GREEN after the - // #13 wording fix (task-exit borrow check words as wasmtime's "borrow - // handles still remain at the end of the call"); entries pruned. // --- async/drop-stream.json: root cause: STREAMS --- - // line 158 GREEN after the #13 wording fix (busy readable-end drop words - // as a removal, matching wasmtime); entry pruned. - // --- async/drop-subtask.json: GREEN under jspi auto-detection (the jspi flip); - // entry pruned. --- // --- async/drop-waitable-set.json: root cause: FACT-ASYNC --- // --- async/during-sync-call-*.json + during-sync-scheduling-candidates.json: // all pin 🧵 sync-call-blocking semantics and are built largely from thread @@ -816,14 +789,7 @@ export const XFAIL: XfailEntry[] = [ line: 215, reason: "same line-162 cascade as line 214", }, - // --- async/empty-wait.json: GREEN under jspi auto-detection (the jspi flip); - // entry pruned. --- // --- async/futures-must-write.json: root cause: STREAMS --- - // --- async/partial-stream-copies.json: GREEN under jspi auto-detection - // (the jspi flip); entry pruned. --- - // --- async/passing-resources.json: lines 175/176 GREEN after the #18 - // tls-smoke fixes (cycle-safe structural ValType equality + token - // unification); entries pruned. --- // --- async/reentrance.json: BRAND NEW file (test/async/reentrance.wast is // 100% new content added by CM#705's "remove the may_enter flag/trap", // polyengine#173). CORRECTED CLASSIFICATION (revision round; verified by @@ -1041,37 +1007,30 @@ export const XFAIL: XfailEntry[] = [ "trap by firing first on the reentrant call; fact-reentrance-47, " + "https://github.com/polymorph-components/polyengine/issues/248 (pending-capability: wasmtime-environ bump)", }, - // --- async/sync-barges-in.json: GREEN under jspi auto-detection - // (the jspi flip); entry pruned. --- - // --- async/sync-streams.json: mostly GREEN (see the #43 note below for - // the entry-gate/drain-policy history), but CM#705 (polyengine#173) FLIPPED - // three expected values in test/async/sync-streams.wast (STARTING vs - // STARTED at the sync-lowered `set` call, and a COMPLETED<->DROPPED - // completion-code swap on the paired stream.read/write) to track the new - // spec's blocking semantics now that may_enter no longer exists. polyengine - // still returns the pre-CM#705 codes, so the file's single all-in-one - // assert_return now hits a guest `unreachable`. Classed `cm705-sync-sched` - // (https://github.com/polymorph-components/polyengine/issues/249) exactly as the dispatch predicted. Since #43 - // polyengine implements wasmtime's model: the entry gate is HELD for the - // whole core invocation (a resolved producer blocked mid-sync-write keeps - // gating), and the async-lowered call's initial status is decided only - // after the callee instance's runnable work has been drained to - // quiescence — by which time the producer has exited and the next task - // reports STARTED. Adjudicated 2026-08-10 (issue #43): the test's hard - // STARTED assertion is schedule-dependent — an upstream test defect - // overfitting wasmtime's deferred-entry policy (pristine definitions.py - // answers STARTING) — and polyengine's drain policy satisfies it as - // written under any seed. The former release-at-BLOCK divergence is gone. - // (Before the jspi flip this file was xfailed outright.) --- + // --- async/sync-streams.json: test/async/sync-streams.wast expects three + // values polyengine does not produce (STARTING vs STARTED at the + // sync-lowered `set` call, and a COMPLETED<->DROPPED completion-code swap + // on the paired stream.read/write) under CM#705's blocking semantics, so + // the file's single all-in-one assert_return hits a guest `unreachable`. + // Classed `cm705-sync-sched` + // (https://github.com/polymorph-components/polyengine/issues/249). + // + // For the rest of the file polyengine implements wasmtime's model (#43): + // the async-lowered call's initial status is decided only after the callee + // instance's runnable work has been drained to quiescence — by which time + // the producer has exited and the next task reports STARTED. Adjudicated + // 2026-08-10 (issue #43): the test's hard STARTED assertion is + // schedule-dependent — an upstream test defect overfitting wasmtime's + // deferred-entry policy (pristine definitions.py answers STARTING) — and + // polyengine's drain policy satisfies it as written under any seed. --- { file: "async/sync-streams.json", line: 208, reason: - "expected return, got trap: guest trapped: unreachable — CM#705 " + - "(polyengine#173) flipped this file's expected STARTING/STARTED and " + - "COMPLETED/DROPPED codes to match the post-may_enter blocking " + - "semantics; polyengine still returns the pre-CM#705 codes, so the " + - "guest's own assertion traps; cm705-sync-sched, " + + "expected return, got trap: guest trapped: unreachable — this file's " + + "expected STARTING/STARTED and COMPLETED/DROPPED codes track CM#705's " + + "blocking semantics, which polyengine's sync scheduling does not yet " + + "produce, so the guest's own assertion traps; cm705-sync-sched, " + "https://github.com/polymorph-components/polyengine/issues/249", }, // --- async/trap-if-block-and-sync.json: cm705-gate-removal? No — the @@ -1083,10 +1042,8 @@ export const XFAIL: XfailEntry[] = [ // misparses, same mechanism as binary.json:974/1206. Every later // "(component instance $i $Tester)" + assert command cascades off the one // failed component-definition command at line 5; CM#705 grew the file from - // 17 to 18 exported tests (test/async/trap-if-block-and-sync.wast +82/-‑, - // adding trap-if-sync-cancel plus the four sync-stream/-future rows), - // stretching the cascade from lines 273-311 (was 286-331 pre-pin-advance; - // lines 312-331 no longer exist and their entries are deleted as stale). --- + // 17 to 18 exported tests (trap-if-sync-cancel plus the four + // sync-stream/-future rows), so the cascade spans lines 273-311. --- { file: "async/trap-if-block-and-sync.json", line: 5, @@ -1097,11 +1054,8 @@ export const XFAIL: XfailEntry[] = [ "the version wasmtime-environ 47.0.3 links against. The 0.253-0.255 " + "window re-aritied the thread built-in opcodes, so 0.252 misparses the " + "$Tester canonical section and rejects a 🧵 thread-built-in-derived " + - "leading byte (observed post-CM#705-pin-advance, polyengine#173: " + - "\"invalid leading byte (0x28) for canonical function lift (at offset " + - "0xb66)\" — offset moved from 0xc16 pre-advance because CM#705 added " + - "the trap-if-sync-cancel/sync-stream/sync-future exports ahead of it in " + - "the same canonical section; same decoder-level failure, not a plan.rs " + + "leading byte (\"invalid leading byte (0x28) for canonical function " + + "lift (at offset 0xb66)\" — a decoder-level failure, not a plan.rs " + "mapping bug). Lifted by a wasmtime-environ whose wasmparser is >= the " + "0.255 line; downgrading testgen to `wast` 252 is NOT a fix (verified: " + "it fails to parse 44 of the 59 suite files, which use the newer " + diff --git a/protocol/deno.json b/protocol/deno.json index 2619123..bb00f27 100644 --- a/protocol/deno.json +++ b/protocol/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/protocol", - "version": "0.2.4", + "version": "0.3.0", "exports": { ".": "./src/mod.ts" }, diff --git a/protocol/src/abortable.ts b/protocol/src/abortable.ts index 76980b4..4d72db8 100644 --- a/protocol/src/abortable.ts +++ b/protocol/src/abortable.ts @@ -56,7 +56,8 @@ // Layering: dependency-free apart from ./brands.ts (the protocol package as a // whole imports nothing). -import { ABORTABLE, defineBrand, hasBrand } from "./brands.ts"; +import { ABORTABLE, hasBrand } from "./brands.ts"; +import { makeMark } from "./mark_decorator.ts"; /** * Declare that this host import takes a per-call `AbortSignal`, appended after @@ -105,46 +106,11 @@ import { ABORTABLE, defineBrand, hasBrand } from "./brands.ts"; * type: the conventions facade is untyped at runtime, and bindgen owns the * compile-time shape of a marked import. */ -export function abortable( +export const abortable: ( fn: F, context?: unknown, legacyDescriptor?: unknown, -): F { - // TypeScript-legacy method decorator convention: (prototype, key, - // descriptor). Detectable because stage-3 contexts are objects with a - // string `kind`, never string/symbol property keys. - if ( - typeof context === "string" || typeof context === "symbol" || - legacyDescriptor !== undefined - ) { - throw new TypeError( - "abortable: legacy (experimentalDecorators) method decoration is not " + - "supported — the decorator would receive the prototype, not the " + - "method. Compile with stage-3 decorators (the default), or use the " + - "call form: `f: abortable(fn)`.", - ); - } - if (context !== undefined) { - const kind = (context as { kind?: unknown }).kind; - if (kind !== "method") { - throw new TypeError( - `abortable: cannot decorate a ${String(kind)} — only methods ` + - `(instance or static) can be marked abortable. Constructors are ` + - `synchronous by contract; for record-literal imports use the call ` + - `form: \`f: abortable(fn)\`.`, - ); - } - } - if (typeof fn !== "function") { - throw new TypeError( - `abortable: expected a function, got ${typeof fn}`, - ); - } - // Non-enumerable (`defineBrand`): the mark must not show up in value - // walks of an imports record, and re-marking the same function is a no-op. - defineBrand(fn as unknown as object, ABORTABLE); - return fn; -} +) => F = makeMark("abortable", "abortable", ABORTABLE); /** Brand check (executor-side, read per-declaration at lowering time). */ export function isAbortable(value: unknown): boolean { diff --git a/protocol/src/defer_cancel.ts b/protocol/src/defer_cancel.ts index ec24578..e215e2e 100644 --- a/protocol/src/defer_cancel.ts +++ b/protocol/src/defer_cancel.ts @@ -40,7 +40,8 @@ // Layering: dependency-free apart from ./brands.ts (the protocol package as a // whole imports nothing). -import { DEFER_CANCEL, defineBrand, hasBrand } from "./brands.ts"; +import { DEFER_CANCEL, hasBrand } from "./brands.ts"; +import { makeMark } from "./mark_decorator.ts"; /** * Declare that this host import must run to completion: a guest cancellation @@ -79,46 +80,11 @@ import { DEFER_CANCEL, defineBrand, hasBrand } from "./brands.ts"; * same function, typed for insertion into an imports record or for method * replacement. */ -export function deferCancel( +export const deferCancel: ( fn: F, context?: unknown, legacyDescriptor?: unknown, -): F { - // TypeScript-legacy method decorator convention: (prototype, key, - // descriptor). Detectable because stage-3 contexts are objects with a - // string `kind`, never string/symbol property keys. - if ( - typeof context === "string" || typeof context === "symbol" || - legacyDescriptor !== undefined - ) { - throw new TypeError( - "deferCancel: legacy (experimentalDecorators) method decoration is not " + - "supported — the decorator would receive the prototype, not the " + - "method. Compile with stage-3 decorators (the default), or use the " + - "call form: `f: deferCancel(fn)`.", - ); - } - if (context !== undefined) { - const kind = (context as { kind?: unknown }).kind; - if (kind !== "method") { - throw new TypeError( - `deferCancel: cannot decorate a ${String(kind)} — only methods ` + - `(instance or static) can be marked cancel-deferring. Constructors ` + - `are synchronous by contract; for record-literal imports use the ` + - `call form: \`f: deferCancel(fn)\`.`, - ); - } - } - if (typeof fn !== "function") { - throw new TypeError( - `deferCancel: expected a function, got ${typeof fn}`, - ); - } - // Non-enumerable (`defineBrand`): the mark must not show up in value - // walks of an imports record, and re-marking the same function is a no-op. - defineBrand(fn as unknown as object, DEFER_CANCEL); - return fn; -} +) => F = makeMark("deferCancel", "cancel-deferring", DEFER_CANCEL); /** Brand check (executor-side, read per-declaration at lowering time). */ export function isDeferCancel(value: unknown): boolean { diff --git a/protocol/src/mark_decorator.ts b/protocol/src/mark_decorator.ts new file mode 100644 index 0000000..3e7dcd5 --- /dev/null +++ b/protocol/src/mark_decorator.ts @@ -0,0 +1,66 @@ +// Module-private factory shared by suspending(), deferCancel(), and +// abortable() (contracts/embedder-api.md §"Functions and async"). The three +// marks differ only in name, brand symbol, and one describing adjective; this +// helper carries the byte-identical validation body so the three public +// functions stay in lockstep without copy-paste. Not exported from mod.ts — +// intra-package only. + +import { defineBrand, hasBrand } from "./brands.ts"; + +/** + * Build a mark function `(fn, context?, legacyDescriptor?) => fn` that + * validates the stage-3-decorator / direct-call calling convention, throws + * the shared error messages (parameterized by `name` and `adjective`), and + * brands `fn` with `brand` in place. + */ +export function makeMark( + name: string, + adjective: string, + brand: symbol, +): ( + fn: F, + context?: unknown, + legacyDescriptor?: unknown, +) => F { + return function mark( + fn: F, + context?: unknown, + legacyDescriptor?: unknown, + ): F { + // TypeScript-legacy method decorator convention: (prototype, key, + // descriptor). Detectable because stage-3 contexts are objects with a + // string `kind`, never string/symbol property keys. + if ( + typeof context === "string" || typeof context === "symbol" || + legacyDescriptor !== undefined + ) { + throw new TypeError( + `${name}: legacy (experimentalDecorators) method decoration is not ` + + `supported — the decorator would receive the prototype, not the ` + + `method. Compile with stage-3 decorators (the default), or use the ` + + `call form: \`f: ${name}(fn)\`.`, + ); + } + if (context !== undefined) { + const kind = (context as { kind?: unknown }).kind; + if (kind !== "method") { + throw new TypeError( + `${name}: cannot decorate a ${String(kind)} — only methods ` + + `(instance or static) can be marked ${adjective}. Constructors are ` + + `synchronous by contract; for record-literal imports use the call ` + + `form: \`f: ${name}(fn)\`.`, + ); + } + } + if (typeof fn !== "function") { + throw new TypeError( + `${name}: expected a function, got ${typeof fn}`, + ); + } + // Non-enumerable (`defineBrand`): the mark must not show up in value + // walks of an imports record, and re-marking the same function is a + // no-op. + defineBrand(fn as unknown as object, brand); + return fn; + }; +} diff --git a/protocol/src/mod.ts b/protocol/src/mod.ts index 793e6f7..11ab661 100644 --- a/protocol/src/mod.ts +++ b/protocol/src/mod.ts @@ -29,7 +29,6 @@ export { PROTOCOL_GENERATION, REALM_LOCAL, RESOURCE_STATE, - RUNTIME_COPIES, STREAM, STREAM_PRODUCER, STREAM_WRITER, diff --git a/protocol/src/suspending.ts b/protocol/src/suspending.ts index 8164f8b..2bb73dd 100644 --- a/protocol/src/suspending.ts +++ b/protocol/src/suspending.ts @@ -26,7 +26,8 @@ // Layering: dependency-free apart from ./brands.ts (the protocol package as a // whole imports nothing). -import { defineBrand, hasBrand, SUSPENDING } from "./brands.ts"; +import { hasBrand, SUSPENDING } from "./brands.ts"; +import { makeMark } from "./mark_decorator.ts"; interface Suspendable { [SUSPENDING]?: true; @@ -67,46 +68,11 @@ interface Suspendable { * same function, typed for insertion into an imports record or for method * replacement. */ -export function suspending( +export const suspending: ( fn: F, context?: unknown, legacyDescriptor?: unknown, -): F { - // TypeScript-legacy method decorator convention: (prototype, key, - // descriptor). Detectable because stage-3 contexts are objects with a - // string `kind`, never string/symbol property keys. - if ( - typeof context === "string" || typeof context === "symbol" || - legacyDescriptor !== undefined - ) { - throw new TypeError( - "suspending: legacy (experimentalDecorators) method decoration is not " + - "supported — the decorator would receive the prototype, not the " + - "method. Compile with stage-3 decorators (the default), or use the " + - "call form: `f: suspending(fn)`.", - ); - } - if (context !== undefined) { - const kind = (context as { kind?: unknown }).kind; - if (kind !== "method") { - throw new TypeError( - `suspending: cannot decorate a ${String(kind)} — only methods ` + - `(instance or static) can be marked suspendable. Constructors are ` + - `synchronous by contract; for record-literal imports use the call ` + - `form: \`f: suspending(fn)\`.`, - ); - } - } - if (typeof fn !== "function") { - throw new TypeError( - `suspending: expected a function, got ${typeof fn}`, - ); - } - // Non-enumerable (`defineBrand`): the mark must not show up in value - // walks of an imports record, and re-marking the same function is a no-op. - defineBrand(fn as unknown as object, SUSPENDING); - return fn; -} +) => F = makeMark("suspending", "suspendable", SUSPENDING); /** Brand check (executor-side). */ export function isSuspending(value: unknown): boolean { diff --git a/protocol/tests/registry_test.ts b/protocol/tests/registry_test.ts index 5993b94..8c3a96c 100644 --- a/protocol/tests/registry_test.ts +++ b/protocol/tests/registry_test.ts @@ -10,9 +10,11 @@ import { copyCensus, PROTOCOL_GENERATION, registerRuntimeCopy, - RUNTIME_COPIES, runtimeCopies, } from "../src/mod.ts"; +// RUNTIME_COPIES is @internal (not part of the public surface); this test +// reaches into the package's own module for the global-slot key. +import { RUNTIME_COPIES } from "../src/brands.ts"; function reset(): void { // deno-lint-ignore no-explicit-any diff --git a/runtime/deno.json b/runtime/deno.json index ea96a5c..67df299 100644 --- a/runtime/deno.json +++ b/runtime/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/runtime", - "version": "0.5.2", + "version": "0.6.0", "exports": { "./cache": "./src/cache/mod.ts", "./plan": "./src/plan/mod.ts", diff --git a/runtime/src/cabi/bulk_lists.ts b/runtime/src/cabi/bulk_lists.ts index 4c074ef..889bcb7 100644 --- a/runtime/src/cabi/bulk_lists.ts +++ b/runtime/src/cabi/bulk_lists.ts @@ -88,11 +88,6 @@ const FLOAT_CTORS: Record< f64: Float64Array, }; -/** Kinds these helpers handle (besides them, u8 has its own path). */ -export function isBulkListKind(kind: string): boolean { - return kind === "bool" || kind in INT_CTORS || kind in BIG_CTORS || - kind in FLOAT_CTORS; -} function viewOf< C extends { new (b: ArrayBufferLike, o: number, n: number): InstanceType; readonly BYTES_PER_ELEMENT: number }, diff --git a/runtime/src/cabi/context.ts b/runtime/src/cabi/context.ts index ec10501..9dad3ae 100644 --- a/runtime/src/cabi/context.ts +++ b/runtime/src/cabi/context.ts @@ -68,12 +68,8 @@ export interface ComponentInstanceLike { * instance at all, and test harnesses supply bare `{handles, mayLeave}` * doubles. cabi must not depend on task/, so the symbol lives here and * `ComponentInstanceState` declares it; cabi/handles.ts `isComponentInstance` - * is the only reader. - * - * It replaced a structural match on the pre-CM#705 reentrance methods - * (`may_enter_from`/`enter_from`/`leave_to`), which polyengine#173 deleted - * along with the rest of the transient reentrance model. `ComponentInstanceLike` - * stays deliberately structural: the brand is NOT part of it. + * is the only reader. `ComponentInstanceLike` stays deliberately structural: + * the brand is NOT part of it. */ export const COMPONENT_INSTANCE: unique symbol = Symbol( "polyengine.ComponentInstance", @@ -106,9 +102,8 @@ export class LiftLowerContext { * with `may_leave` cleared, so a realloc that lowers an import traps * (`canon_lower`'s `trap_if(not ...may_leave)`, implemented here by * exec/boundary.ts `createLoweredImport`). That bracket is implemented - * below. What remains deferred is only the reference's routing of the call - * through `canon_lift`; upstream component-model PR #705 removes that - * routing, leaving this bracket as the whole story. polyengine issue #147. + * below, and it is the whole story: the pinned reference does not route + * the call through `canon_lift` (CM#705). polyengine issue #147. */ reallocate( old: number, diff --git a/runtime/src/cabi/flatten.ts b/runtime/src/cabi/flatten.ts index ae8d6be..9efdb58 100644 --- a/runtime/src/cabi/flatten.ts +++ b/runtime/src/cabi/flatten.ts @@ -16,15 +16,7 @@ import { requireMemory } from "./context.ts"; export const MAX_FLAT_PARAMS = 16; export const MAX_FLAT_ASYNC_PARAMS = 4; -// Mutable to mirror run_tests.py toggling definitions.MAX_FLAT_RESULTS. -export let MAX_FLAT_RESULTS = 1; -// Test-only mirror of run_tests.py's constant toggling; not for production -// use (naming convention: see `schedulerSeedForTesting`). -export function setMaxFlatResultsForTesting(n: number): number { - const prev = MAX_FLAT_RESULTS; - MAX_FLAT_RESULTS = n; - return prev; -} +export const MAX_FLAT_RESULTS = 1; export type FlattenContext = "lift" | "lower"; diff --git a/runtime/src/cabi/handles.ts b/runtime/src/cabi/handles.ts index f173538..1051ea3 100644 --- a/runtime/src/cabi/handles.ts +++ b/runtime/src/cabi/handles.ts @@ -8,7 +8,7 @@ // current_instance() from the running thread; // - canon_resource_drop routes the dtor through `callDtorGated` below, // which reconstructs the reference's store.lift/store.lower bracket -// (may_enter gating + trap poisoning) around the destructor call (#85). +// (entry refusal + trap poisoning) around the destructor call (#85). // Host-initiated drops do NOT come here: they run the dtor through the // real lift harness (`hostDtorCall`, exec/boundary.ts) — see #160. @@ -183,10 +183,6 @@ interface RealComponentInstance { * `ComponentInstanceLike`, which those doubles satisfy: it reads the * `COMPONENT_INSTANCE` brand, declared on `ComponentInstanceState` and * defined in ./context.ts so that cabi does not have to import task/. - * - * (It replaced a structural match on `may_enter_from`/`enter_from`/`leave_to`, - * the reentrance methods CM#705 and polyengine#173 deleted. Same population, - * by construction: `ComponentInstanceState` was their only implementor.) */ function isComponentInstance(x: unknown): RealComponentInstance | null { if (x === null || typeof x !== "object") return null; @@ -211,13 +207,10 @@ function isThenable(v: unknown): v is PromiseLike { * caller([h.rep]) * ``` * - * Post-CM#705 that lift carries NO gate: dropping a handle whose implementing - * instance is mid-execution is VALID, including the dtor-less case. The - * pre-#705 `may_enter_from`/`enter_from`/`leave_to` bracket (and with it the - * "same-instance exemption" that fell out of an empty entering set) is gone - * from the reference and gone from here. + * That lift carries NO gate (CM#705): dropping a handle whose implementing + * instance is mid-execution is VALID, including the dtor-less case. * - * What remains is polyengine's per-instance poisoning divergence, and it + * What this adds is polyengine's per-instance poisoning divergence, and it * applies to `rt.impl`, not to the dropping instance: a trap out of the dtor * buries the implementing instance (refusal names the original trap, * polyengine#145; its live stream/future ends are retired, #66). The @@ -232,10 +225,8 @@ function isThenable(v: unknown): v is PromiseLike { * SCOPE (#160): this is the **guest-initiated** path only. A guest-initiated * drop must complete synchronously (the reference lifts the dtor with * `async_ = False`), so a thenable here is a trap. The host-initiated path - * used to share this function with an `allowAsync` flag that held the entry - * bracket across the dtor's promise; it now goes through the full lift - * harness instead (`hostDtorCall` in exec/boundary.ts), which is what - * definitions.py actually does and what unwedges #160. + * goes through the full lift harness instead (`hostDtorCall` in + * exec/boundary.ts), which is what definitions.py actually does. */ export function callDtorGated( rt: ResourceTypeInfo, @@ -266,9 +257,8 @@ export function callDtorGated( // A poisoned target's refusal names the original trap (polyengine#145). // `callerInst` can legitimately BE `impl` here (a guest dropping its own - // resource): `entryRefusal`'s vacuous-pass guard keeps that entry allowed - // even against a marked instance, matching the pre-CM#705 reference's - // vacuous pass on an empty entering set. + // resource): `entryRefusal`'s self-call guard keeps that entry allowed + // even against a marked instance. { const refusal = entryRefusal( impl, diff --git a/runtime/src/cabi/types.ts b/runtime/src/cabi/types.ts index 9e40932..148705a 100644 --- a/runtime/src/cabi/types.ts +++ b/runtime/src/cabi/types.ts @@ -320,9 +320,6 @@ export function containsBorrow(t: ValType | null): boolean { return contains(t, (u) => u.kind === "borrow"); } -export function containsAsyncValue(t: ValType | null): boolean { - return contains(t, (u) => u.kind === "stream" || u.kind === "future"); -} export function contains( t: ValType | null, diff --git a/runtime/src/cache/core.ts b/runtime/src/cache/core.ts index b40f4c1..977210a 100644 --- a/runtime/src/cache/core.ts +++ b/runtime/src/cache/core.ts @@ -51,7 +51,7 @@ // this layer never sees. import type { WirePlan } from "../plan/format.ts"; -import { loadEnvelope, PlanError, TranslateError } from "../plan/loader.ts"; +import { loadEnvelope, PlanError } from "../plan/loader.ts"; /** * The minimal surface `translateCached`/`keyFor` need from a translator. @@ -246,14 +246,7 @@ export async function translateCached( // verdict — must propagate uncached, per TranslateError's docs: a // validation verdict is a judgment about the *input component*, not // something to cache-and-replay) and gives us the split plan/adapters. - let wire: WirePlan; - let adapters: Map; - try { - ({ wire, adapters } = loadEnvelope(first)); - } catch (e) { - if (e instanceof TranslateError) throw e; - throw e; - } + const { wire, adapters } = loadEnvelope(first); // A `put` failure (issue #196) is swallowed: the translation already // succeeded and was already validated above by `loadEnvelope` — failing diff --git a/runtime/src/digest/verify.ts b/runtime/src/digest/verify.ts index f741214..b763744 100644 --- a/runtime/src/digest/verify.ts +++ b/runtime/src/digest/verify.ts @@ -1,7 +1,6 @@ -// Runtime handshake (docs/architecture.md §9): verify a loaded plan's world against the -// digest embedded by generated bindgen code, producing a rich mismatch -// report that names the first divergent import/export/type path rather -// than just "digests differ". +// Runtime handshake (docs/architecture.md §9): verify a loaded plan's world +// against the digest embedded by generated bindgen code, failing fast with +// the expected/actual digests on mismatch (contracts/digest.md). import type { WirePlan } from "../plan/format.ts"; import { computeWorldDigest } from "./digest.ts"; @@ -10,18 +9,12 @@ import { computeWorldDigest } from "./digest.ts"; export interface DigestMismatch { expected: string; actual: string; - /** Human-readable description of the first structural divergence found, - * or `null` if a divergence exists but no finer-grained cause could be - * isolated (e.g. the two canonical JSON trees have the same top-level - * shape but the digest still differs — should not happen in practice - * since the digest is a pure hash of that JSON, but guarded anyway). */ - firstDivergence: string | null; } /** * Thrown by generated `instantiate` wrappers when the loaded plan's world * digest does not match the constant bindgen embedded at generation time - * (contracts/digest.md: "fails fast with a structural diff on mismatch"). + * (contracts/digest.md: "fails fast on mismatch"). * Raised BEFORE the component is instantiated, so no guest code has run * when a caller catches this. * @@ -33,16 +26,13 @@ export class WorldDigestMismatchError extends Error { override readonly name = "WorldDigestMismatchError"; /** The world these bindings were generated from. */ readonly world: string; - /** The full mismatch report (expected/actual digest, first divergence). */ + /** The full mismatch report (expected/actual digest). */ readonly mismatch: DigestMismatch; constructor(world: string, mismatch: DigestMismatch) { super( `world digest mismatch for \`${world}\`: bindings expect ` + `${mismatch.expected}, loaded plan computes ${mismatch.actual}` + - (mismatch.firstDivergence - ? ` (first divergence: ${mismatch.firstDivergence})` - : "") + " — regenerate the bindings from the component's WIT", ); this.world = world; @@ -62,8 +52,7 @@ export class WorldDigestMismatchError extends Error { /** * Verify `plan`'s computed world digest against `expectedDigest` (the * constant bindgen embedded at generation time). Returns `null` on match, - * or a `DigestMismatch` report naming the first divergent path on - * mismatch. + * or a `DigestMismatch` report on mismatch. * @internal */ export async function verifyWorldDigest( @@ -75,93 +64,5 @@ export async function verifyWorldDigest( return { expected: expectedDigest, actual: actual.digest, - firstDivergence: null, // filled in by compareAgainstExpectedJson if available }; } - -/** - * `diffWorldDigest` — richer variant for tests/tooling: compare against - * another plan's (or a WIT-derived) canonical JSON directly, walking both - * trees in parallel to name the first divergent import/export/type path. - * `expectedCanonicalJson` is normally produced by `crates/bindgen`'s - * `digest --json` output, or by `computeWorldDigest` on a reference plan. - * @internal - */ -export async function diffWorldDigest( - plan: WirePlan, - expectedCanonicalJson: string, -): Promise { - const actual = await computeWorldDigest(plan); - const expected = JSON.parse(expectedCanonicalJson); - const expectedDigestBytes = await crypto.subtle.digest( - "SHA-256", - new TextEncoder().encode(expectedCanonicalJson), - ); - const expectedDigest = "sha256:" + - Array.from(new Uint8Array(expectedDigestBytes)).map((b) => - b.toString(16).padStart(2, "0") - ).join(""); - if (actual.digest === expectedDigest) return null; - const actualParsed = JSON.parse(actual.canonicalJson); - return { - expected: expectedDigest, - actual: actual.digest, - firstDivergence: firstDivergentPath(expected, actualParsed, "$"), - }; -} - -/** - * Walk two canonical world trees in parallel (both already sorted by name - * at every `imports`/`exports`/`items` level — see digest.ts), returning a - * human-readable path to the first field that differs, or `null` if the - * trees are structurally identical (shouldn't happen if the digests - * differ, but the digest is a hash — collisions or caller error are - * possible, so this is a defensive `null`, not a promise of "same"). - */ -function firstDivergentPath( - expected: unknown, - actual: unknown, - path: string, -): string | null { - if (Array.isArray(expected) && Array.isArray(actual)) { - if (expected.length !== actual.length) { - return `${path}: length ${expected.length} (expected) vs ${actual.length} (actual)`; - } - for (let i = 0; i < expected.length; i++) { - const label = itemLabel(expected[i]) ?? `[${i}]`; - const d = firstDivergentPath(expected[i], actual[i], `${path}.${label}`); - if (d) return d; - } - return null; - } - if ( - expected !== null && actual !== null && - typeof expected === "object" && typeof actual === "object" && - !Array.isArray(expected) && !Array.isArray(actual) - ) { - const e = expected as Record; - const a = actual as Record; - const keys = new Set([...Object.keys(e), ...Object.keys(a)]); - for (const k of [...keys].sort()) { - if (!(k in a)) return `${path}.${k}: present (expected) but missing (actual)`; - if (!(k in e)) return `${path}.${k}: missing (expected) but present (actual)`; - const d = firstDivergentPath(e[k], a[k], `${path}.${k}`); - if (d) return d; - } - return null; - } - if (expected !== actual) { - return `${path}: ${JSON.stringify(expected)} (expected) vs ${ - JSON.stringify(actual) - } (actual)`; - } - return null; -} - -function itemLabel(v: unknown): string | undefined { - if (v !== null && typeof v === "object" && "name" in v) { - const name = (v as Record).name; - if (typeof name === "string") return JSON.stringify(name); - } - return undefined; -} diff --git a/runtime/src/embedder/copy.ts b/runtime/src/embedder/copy.ts index 6f4af1a..0ec7f2a 100644 --- a/runtime/src/embedder/copy.ts +++ b/runtime/src/embedder/copy.ts @@ -32,7 +32,7 @@ export const COPY_URL: string = import.meta.url; * @internal — copy-identity constant for the module identity multi-copy diagnostics; not * host-facing. */ -export const RUNTIME_VERSION = "0.5.2"; +export const RUNTIME_VERSION = "0.6.0"; /** * Compose a cross-copy diagnostic: what was foreign, which copy is speaking, diff --git a/runtime/src/embedder/mod.ts b/runtime/src/embedder/mod.ts index 70d1c7e..488b296 100644 --- a/runtime/src/embedder/mod.ts +++ b/runtime/src/embedder/mod.ts @@ -70,19 +70,12 @@ export function createStream(): { stream: ProtocolStream; writer: Protocol export { GuestResource, HostResourceRegistry } from "./resources.ts"; -export { camelCase, type LeafName, parseLeafName, pascalCase } from "./casing.ts"; +export { camelCase, pascalCase } from "./casing.ts"; export { - asTrackKeySpelling, - compareSemver, ImportRegistrationError, ImportResolutionError, ImportResolver, - type ParsedId, - parseInterfaceId, - parseSemver, - type Semver, - trackKey, } from "./version.ts"; export { diff --git a/runtime/src/embedder/sync.ts b/runtime/src/embedder/sync.ts index 89651cf..f9bdfd8 100644 --- a/runtime/src/embedder/sync.ts +++ b/runtime/src/embedder/sync.ts @@ -224,17 +224,6 @@ function recordView(rec: object): unknown { for (const key of Object.keys(rec)) { const d = Object.getOwnPropertyDescriptor(rec, key); if (d === undefined) continue; - if (!("value" in d)) { - // An accessor-backed own member (not expected on a runtime-built - // exports record today, but nothing here assumes data properties - // only): forward reads/writes to the underlying record unmapped. - Object.defineProperty(view, key, { - enumerable: true, - configurable: true, - get: () => (rec as Record)[key], - }); - continue; - } const value = d.value; // Lazy: `mapMember` runs (and can throw, for an async member) only // when the caller actually reads this key — see the CONTRACT note on diff --git a/runtime/src/exec/boundary.ts b/runtime/src/exec/boundary.ts index 0439a27..6282d3d 100644 --- a/runtime/src/exec/boundary.ts +++ b/runtime/src/exec/boundary.ts @@ -863,11 +863,12 @@ async function driveAsync( // and clearing it before that activation runs re-opens the // mis-attribution window the entry exists to close. // - // PER-STORE (issue #210): this gate used to read a module-global slot, so - // an idle store's driver spun here — and died at the hop bound below in - // ~311ms — merely because ANOTHER store's guest was dwelling on a slow - // host import. Activations never cross stores; another store's pending - // resumption is none of this loop's business. + // 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 @@ -1078,18 +1079,12 @@ async function driveAsync( (t) => !queued.has(t), ); if (parked.length === 0) { - // UNREACHABLE BY CONSTRUCTION since polyengine#173 deleted the - // reentrance model. `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 now means the settled queue is EMPTY, so nothing was - // excluded. (Pre-CM#705 a queue of reentrance-deferred tails answered - // false while still excluding every parked thread; issue #156. The - // way out was the lock holder finishing, and the only await-spanning - // host-entry lock was the async-dtor bracket, which registers in - // `pendingHostCalls` — hence the park below, plus the driver-arrival - // one-shot every park in this loop races.) Retained as a wedge - // detector, not as expected behavior. + // 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, @@ -1122,11 +1117,11 @@ async function driveAsync( // `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` used to blanket-clear - // the single global slot, so a guest-synchronous delivery during the - // await — which takes a fresh entry of its own — had that entry - // clobbered early, re-opening the window it exists to close. With a set - // we can name exactly what we added. + // 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 @@ -1146,7 +1141,8 @@ async function driveAsync( // 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 the entry used to close at depth >= 2. + // 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 @@ -1395,9 +1391,9 @@ export function createLiftedFunction(input: { // Depth of the sync-call scope stack on entry; see the `finally` below. const syncCallDepth = syncCallStack?.length ?? 0; - // Reference `Store.lift` (@ 2f13265) runs `canon_lift` with NO gate: the - // transient reentrance check went away with CM#705, so host entry into a - // live instance is valid. What survives is polyengine's per-instance + // Reference `Store.lift` (@ 2f13265) runs `canon_lift` with NO gate + // (CM#705), so host entry into a live instance is valid. What this adds + // is polyengine's per-instance // poisoning divergence — a poisoned instance is a corpse, and its refusal // names the original trap (polyengine#145 ask 1). { @@ -1512,14 +1508,9 @@ export function createLiftedFunction(input: { * call reports `cannot enter component instance` with the recorded cause * appended (polyengine#145 ask 1). * `test/async/builtin-trap-poisons-instance.wast` asserts exactly this, - * twice; the marker (`notifyInstancePoisoned`) is the whole mechanism - * since #251's re-key, and since CM#705 removed `may_enter` it is also - * the only one there could be. + * twice; the marker (`notifyInstancePoisoned`) is the whole mechanism. * - * Only `inst` is affected; sibling instances stay usable. (Historically - * this walked the entry's `entering_set` and had to hand-release the - * synthetic per-instantiation root to avoid store-wide poisoning; with - * the gate gone there is no set and no root to release.) + * Only `inst` is affected; sibling instances stay usable. * * Poisoned instances can never rendezvous again, so their handle tables' * live stream/future ends are retired here (#66): parked host operations @@ -1793,30 +1784,13 @@ function dtorOptions(instance: ComponentInstanceState): ResolvedOptions { * callee = inst.store.lift(dtor, ft, opts, rt.impl) * ``` * - * Before #160 the host-initiated path (embedder `drop()`, the GC backstop, - * `dropOwn`) hand-rolled the bracket in cabi/handles.ts `callDtorGated`: a - * bare call to the dtor with the pre-CM#705 host-entry bracket HELD across - * the returned promise. Three defects followed from having no Task/Thread behind the - * activation: - * - * - **#160 itself**: the held bracket left the impl instance non-enterable, - * so `Store.tick`'s enterability filter (#155) could never resume a - * suspension point belonging to the dtor's own activation. The completion - * promise sat in `pendingHostCalls` looking like external work, and every - * driver parked on it forever. - * - it was the runtime's only host-entry bracket spanning an await — - * the macro-scale reachability window of the #156 class, through which a - * sibling instance looked non-enterable (the shared per-instantiation - * root of the since-deleted reentrance model, polyengine#173). - * - built-ins reached inside the dtor had no ambient task (`currentTask()` - * → `PendingCapability`, or a foreign-task misattribution, the #24 class). - * - * Under the lift harness all three go away structurally: the activation has a - * real `Task` + implicit `Thread`, and settled tails flow through - * `serviceSettled` like any other lifted sync call. (CM#705 has since removed - * the transient gate entirely, so the first two defects could no longer arise - * at all; the history is kept because the Task/Thread shape it forced is - * still what makes built-ins inside a dtor well-attributed.) + * The host-initiated paths (embedder `drop()`, the GC backstop, `dropOwn`) + * route through this harness rather than calling the dtor bare, because the + * activation then has a real `Task` + implicit `Thread`: built-ins reached + * inside the dtor are well-attributed (a bare call leaves `currentTask()` + * with no ambient task — `PendingCapability`, or a foreign-task + * misattribution, the #24 class), and settled tails flow through + * `serviceSettled` like any other lifted sync call. * * The returned function takes the rep and returns either `undefined` (the * activation completed synchronously — the overwhelmingly common case) or a diff --git a/runtime/src/intrinsics/fact_calls.ts b/runtime/src/intrinsics/fact_calls.ts index e45fa9e..2b883c1 100644 --- a/runtime/src/intrinsics/fact_calls.ts +++ b/runtime/src/intrinsics/fact_calls.ts @@ -61,9 +61,9 @@ // `trampoline.rs:116-127` emits an unconditional // `trap(Trap::CannotEnterComponent)` when the lower and lift instances are // the same or are ancestors of one another. The runtime-side counterpart -// — "is the callee instance currently executing" — is GONE as of CM#705 -// (definitions.py @ 2f13265 has no `may_enter`), so the only refusal left -// at these call sites is polyengine's per-instance poisoned-corpse check +// — "is the callee instance currently executing" — does not exist at the +// pinned reference (definitions.py @ 2f13265 has no `may_enter`, CM#705), +// so the only refusal at these sites is polyengine's poisoned-corpse check // (`entryRefusal`). The flat-instance-tree gap recorded in task/mod.ts is // doubly not load-bearing here. diff --git a/runtime/src/intrinsics/mod.ts b/runtime/src/intrinsics/mod.ts index eb4d3c2..add986a 100644 --- a/runtime/src/intrinsics/mod.ts +++ b/runtime/src/intrinsics/mod.ts @@ -25,7 +25,7 @@ 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 { entryRefusal, maybeCurrentThread, maybeCurrentTask, PendingCapability } from "../task/mod.ts"; +import { dbgId, entryRefusal, maybeCurrentThread, maybeCurrentTask } from "../task/mod.ts"; import type { WireTrampoline } from "../plan/format.ts"; import type { CoreFn, ExecutionStats } from "../exec/boundary.ts"; import { UnsupportedFeatureError } from "./errors.ts"; @@ -344,19 +344,6 @@ export function createTrampoline( }; } -/** - * A trampoline that instantiates fine but fails at its first call, naming the - * phase that will implement it. See the CONTRACT note at the stream/future - * cases for why these are not instantiate-time failures. - */ -function deferredCapability(kind: string, capability: string): CoreFn { - return () => { - throw new PendingCapability( - `built-in '${kind}' is not implemented yet: ${capability}`, - ); - }; -} - /** * The component instance a trampoline is declared in (wasmtime names it in * every instance-scoped `Trampoline` variant). This is the static answer to @@ -396,18 +383,6 @@ const SCOPE_TRACE = (() => { } })(); -const taskIds = new WeakMap(); -let nextTaskId = 1; -function taskId(t: unknown): string { - if (t === undefined || t === null) return "NONE(->ctx fallback)"; - let id = taskIds.get(t as object); - if (id === undefined) { - id = nextTaskId++; - taskIds.set(t as object, id); - } - return `T${id}`; -} - function syncScopes(ctx: TrampolineContext, site = "?"): any[] { const thread = maybeCurrentThread() as | { syncCallStack: any[] } @@ -415,7 +390,8 @@ function syncScopes(ctx: TrampolineContext, site = "?"): any[] { const scopes = thread?.syncCallStack ?? ctx.syncCallStack; if (SCOPE_TRACE) { console.error( - `[scope] ${site} act=${taskId(thread)} depth=${scopes.length}`, + `[scope] ${site} act=${thread ? dbgId(thread) : "NONE(->ctx fallback)"} ` + + `depth=${scopes.length}`, ); } return scopes; @@ -472,9 +448,9 @@ function createTrampolineBody( ) => { // ENTRY REFUSAL at the fused sync-call boundary. // - // The reference's reentrance gate is GONE (CM#705; definitions.py @ + // The reference has no reentrance gate (CM#705; definitions.py @ // 2f13265 has no `may_enter`/`entering_set`/`enter_from`): a - // guest->guest call through `Store.lift` now runs `canon_lift` + // guest->guest call through `Store.lift` runs `canon_lift` // unconditionally, and host-mediated reentrance — host -> A.f -> C.g // -> host import -> host invokes C.g — is simply valid. wasmtime's // fused adapters agreed all along: `enter_guest_sync_call` @@ -484,12 +460,7 @@ function createTrampolineBody( // `test/async/trap-on-reenter.wast` cases 2 and 3 pin — a translation // -time trap, not this site). // - // polyengine#165 recorded the omitted enter/leave bracket as a named - // divergence "pending the pin advance". The pin advance happened - // (polyengine#173): the divergence is CLOSED, because there is no - // bracket left to omit. - // - // What survives here is polyengine's per-instance poisoning: a + // What this site does check is polyengine's per-instance poisoning: a // callee that trapped is a corpse and may never be entered again, // and the refusal names the original trap (polyengine#145). That is // the whole content of this check. diff --git a/runtime/src/task/mod.ts b/runtime/src/task/mod.ts index 99b7feb..757b153 100644 --- a/runtime/src/task/mod.ts +++ b/runtime/src/task/mod.ts @@ -50,11 +50,11 @@ export type HandleTableEntry = unknown; * the may_leave boolean (wasmtime 47 FACT treats the whole flags global as * may_leave; there is no bitmask). Initial value 1 (true). * - * There is no `may_enter` counterpart and no instance tree: upstream - * component-model PR #705 (definitions.py @ 2f13265) deleted `may_enter`, - * `parent`, `entering_set`, `enter_from` and `leave_to` outright, so nothing - * gates entry into a live instance. polyengine#173 followed. What polyengine - * keeps beyond the reference is per-instance POISONING — a named divergence + * There is no `may_enter` counterpart and no instance tree: at the pinned + * reference (definitions.py @ 2f13265, CM#705) there is no `may_enter`, + * `parent`, `entering_set`, `enter_from` or `leave_to`, so nothing gates + * entry into a live instance. What polyengine adds beyond the reference is + * per-instance POISONING — a named divergence * living entirely in ./scheduler.ts (`isInstancePoisoned`, `entryRefusal`), * not in any state on this class. * @@ -316,8 +316,8 @@ export class Task { * picked up at the next cancellable block point (`deliverPendingCancel`). * * `caller` is retained for the call-site shape (fact_calls.ts's - * `subtask.onCancel`) and for diagnostics; the reentrance condition it used - * to feed went away with CM#705 (polyengine#173). + * `subtask.onCancel`) and for diagnostics; no condition here consults it + * (CM#705: entry into a live instance is ungated). */ requestCancellation(caller: ComponentInstanceState | null): void { void caller; @@ -379,8 +379,7 @@ export class Task { // threads never resume, so the request parks as pending-cancel forever — // which is the honest state, since a corpse can never reach a cancellable // suspension to deliver at. The reference never faces this because a trap - // there kills the whole store. The marker is the authoritative input - // (polyengine#173, #251's re-key). + // there kills the whole store. The marker is the authoritative input. if (candidates.length > 0 && !isInstancePoisoned(this.inst)) { this.state = "cancel-delivered"; try { diff --git a/runtime/src/task/scheduler.ts b/runtime/src/task/scheduler.ts index bf3a269..5b4126e 100644 --- a/runtime/src/task/scheduler.ts +++ b/runtime/src/task/scheduler.ts @@ -204,9 +204,9 @@ export function instancePoisonCause(inst: object): unknown { /** * Append the recorded poison cause to an entry-refusal trap message - * (polyengine#145 ask 1). Since the transient gate went away with CM#705, - * "cannot enter component instance" has exactly one cause left — a - * permanently poisoned instance, the corpse of an earlier trap — and this + * (polyengine#145 ask 1). "cannot enter component instance" has exactly one + * cause — a permanently poisoned instance, the corpse of an earlier trap — + * and this * suffix names the trap that made it one. The call is kept unconditional at * the refusal sites (returning `base` unchanged for an unmarked instance) so * the message construction stays in one place; the suffix is @@ -224,27 +224,22 @@ export function withPoisonCause(inst: object, base: string): string { * now, and if not, what does the refusal trap say? Returns `null` when entry * is allowed, otherwise the exact trap message for `base`. * - * POISONING IS THE WHOLE MECHANISM (polyengine#173, CM#705 adoption). The - * transient reentrance gate is GONE: at the pinned reference - * (definitions.py @ 2f13265) `may_enter`, `entering_set`, `enter_from`, - * `leave_to` and `ComponentInstance.parent` no longer exist — `Store.lift` - * runs `canon_lift` with no gate at all, so host-mediated reentrance into a - * live instance is simply VALID. The clause that consulted the transient - * gate was deleted with the pin advance and the model itself with - * polyengine#173; #251's re-key onto the marker is what made those deletions - * pure subtractions (the marker never depended on `may_enter`). + * POISONING IS THE WHOLE MECHANISM (CM#705). There is no transient + * reentrance gate: at the pinned reference (definitions.py @ 2f13265) + * `may_enter`, `entering_set`, `enter_from`, `leave_to` and + * `ComponentInstance.parent` do not exist — `Store.lift` runs `canon_lift` + * with no gate at all, so host-mediated reentrance into a live instance is + * simply VALID. * - * What survives is polyengine's NAMED DIVERGENCE: per-instance poisoning. A + * Against that, per-instance poisoning is polyengine's NAMED DIVERGENCE. A * trapped instance is a corpse — entry is refused permanently, with the * recorded cause appended (polyengine#145 ask 1) — where wasmtime instead * kills the whole store. The reference never faces the question because a * trap there is the end of the world. * - * The `caller !== callee` guard preserves the reference's vacuous pass on an - * EMPTY entering set (the pre-#705 `entering_set` was - * `self_and_ancestors() - caller.self_and_ancestors()`, empty when caller is - * callee). A dtor invoked from inside its own instance (cabi/handles.ts) is - * the live case: it must not be refused by its own instance's marker. + * The `caller !== callee` guard keeps a self-call out of the refusal: a dtor + * invoked from inside its own instance (cabi/handles.ts) is the live case — + * it must not be refused by its own instance's marker. */ export function entryRefusal( callee: object, @@ -477,9 +472,9 @@ function activationOf(): any { * The opposite shape — A settles B's suspension so B runs AFTER A — is * deliberately NOT represented here: `SuspensionPoint.resume` pushes only when * nothing is currently running, so B never shadows A. B is picked up by its - * own first `Suspending` call. (Until 2026-08-22 a third ambient tier — the - * driver's `resumingThread` slot — also named B here; it was retired with the - * slot, see `resolveAmbient` and `Store.pendingResumptions`.) + * own first `Suspending` call. A driver's settle-time claim would name B + * here, which is exactly why it is not an ambient tier — see `resolveAmbient` + * and `Store.pendingResumptions`. * * An activation leaves this stack when it parks again * (`blockCurrentActivation`) or finishes (its `awaitValue` promise settles — @@ -570,14 +565,11 @@ export function releaseActivationAmbient(t: any): void { // The resumed-but-not-yet-run gate (a SEPARATE concern from the ambient above) // --------------------------------------------------------------------------- // -// This used to be a module-global single slot, `resumingThread`, doing two -// jobs: (1) the DRIVER's scheduling gate ("a suspension was settled and its -// activation has not run yet — do not schedule anything else"), and (2) tier 3 -// of ambient resolution. Job (2) was retired on 2026-08-22 (issue #158) after -// measurement showed it never decided a read; job (1) is real, but it is -// per-Store SET semantics, not a global identity slot — see -// `Store.pendingResumptions` below, and `resolveAmbient` for the retirement -// evidence. +// The gate answers one question for the DRIVER: "was a suspension settled +// whose activation has not run yet — must I refrain from scheduling anything +// else?" That is per-Store SET semantics, not a global identity slot, and it +// is not an input to ambient resolution: see `Store.pendingResumptions` +// below. const AMBIENT_TRACE = (() => { try { @@ -621,48 +613,38 @@ export function ambientResidue(): { stack: number; claim: boolean } { * is running outside our frames (a `Suspending` hop or a resumption). * LIFO, because activations nest: an outer activation's built-in can * synchronously enter an inner one's wasm. - * (There is no tier 3. A third tier — `resumingThread`, the driver's - * settle-time claim — existed from M3A-1 until 2026-08-22 and was - * RETIRED, see below.) * - * Tier 2 replaced an async-context store (M3A-1). The store held - * precisely "the innermost wasm activation currently executing, across the - * engine's hops and resumptions", because it was written by `withActivation` - * around the wasm entry and the engine restored it on every continuation it - * had captured inside that extent. Tiers 1+2 now state that directly. The - * equivalence is not asserted from the armchair: it was established - * differentially, by running the whole conformance corpus with both the store - * and this queue live and comparing them at every read (zero disagreements - * over 1395 commands), and the corpus pins the result. + * What tiers 1+2 state directly is "the innermost wasm activation currently + * executing, across the engine's hops and resumptions" -- the same quantity + * an async-context store written around the wasm entry would carry, without + * depending on the engine to restore it on every continuation captured inside + * that extent (M3A-1). The equivalence is not asserted from the armchair: it + * was established differentially, against such a store, over the whole + * conformance corpus, comparing at every read (zero disagreements over 1395 + * commands), and the corpus pins the result. * - * Having TWO readers with different orders is not a hypothetical hazard: for - * two rounds `currentThread` used store-first while `maybeCurrentThread` still - * used slot-first, and since the FACT bracket sites read the latter, the - * bracket was attributed to the driver's claim instead of its own activation - * (`exit-sync-call with an empty sync-call stack`). Fixing the precedence in - * one reader measured as "no change" because the failing sites used the other. - * Do not add a third reader; extend this one. (`activationOf` above is not a + * Having TWO readers with different precedence orders is not a hypothetical + * hazard: they disagree silently at exactly the sites that matter -- the FACT + * bracket sites read `maybeCurrentThread`, so a divergent order there + * attributes the bracket to the driver's claim instead of its own activation + * (`exit-sync-call with an empty sync-call stack`), and a precedence fix + * applied to the other reader measures as "no change" because the failing + * sites never call it. Do not add a third reader; extend this one. (`activationOf` above is not a * second reader -- it answers a different question, "whose wasm frame are we * running on behalf of", and is used only by * `Store.consumePendingIfRunning`.) * - * TIER 3 RETIRED, 2026-08-22 (issue #158). The bottom tier used to be - * `resumingThread`, the driver's settle-time claim -- a last resort that named - * whichever activation was settled or claimed across an await, right for that - * one and wrong for every other in-flight activation. It was removed on the - * strength of a re-run of the M3A-1 differential methodology: an instrumented - * build counted every read where tiers 1-2 were empty and the slot was live, - * and measured ZERO deciding reads across the conformance corpus (FIFO, - * 1257/0), both seeded shuffles (`POLYENGINE_SCHED_SEED` 1 and 4242), - * test-runtime (all jspi pins), and the smoke-tls three-async-component #24 - * corpus. A removal build then ran green on every engine lane we have: - * test-runtime, test-protocol, conformance (1257/0, no expectation changes), - * sched-seeds, the shells (sm + node + jsc + bun, all "OK, matches - * expectation"), the browsers (chromium + firefox), smoke-tls and smoke-c0. - * The reading: post-#24 the sentinel discipline (tier 2's claim/release edges) - * always answers first, so the slot's attribution role was vestigial. Its - * other, live role -- the scheduling gate -- survives as the per-Store - * `Store.pendingResumptions` set. + * TWO TIERS ARE ENOUGH, and specifically a driver's settle-time claim is NOT + * a third: such a claim names whichever activation was settled or claimed + * across an await -- right for that one and wrong for every other in-flight + * activation. It is not needed, because the sentinel discipline (tier 2's + * claim/release edges, #24) always answers first: instrumented reads where + * tiers 1-2 were empty and a settle-time claim was live decided NOTHING + * across the conformance corpus, both seeded shuffles + * (`POLYENGINE_SCHED_SEED` 1 and 4242), test-runtime and the smoke-tls + * three-async-component #24 corpus. A driver's settle-time claim is a + * SCHEDULING gate only, and lives as the per-Store `Store.pendingResumptions` + * set (issue #158). */ function resolveAmbient(): CurrentThreadLike | undefined { return threadStack[threadStack.length - 1] ?? @@ -855,22 +837,18 @@ export class Store { * wedges the loops, because an activation that merely hopped legitimately * holds an ambient while the scheduler is free to proceed. * - * PER-STORE and MULTI-ENTRY since 2026-08-22 (issues #158 mechanism B, - * #210). It was one module-global slot with a one-claimant assert, which - * (a) could not represent two legitimately-pending engine resumptions — a - * running activation X delivering a resume to Z while Y's resumption was - * still pending crashed on the assert — and (b) made every driver on every - * store yield while ANY store held a claim, so an idle store's - * `driveStoreAsync` died at the 10,000-hop assert (~311ms) while another - * store merely dwelt on a slow host import. The assert's invariant was - * tier-3 attribution unambiguity, which no longer exists (see - * `resolveAmbient`), so it is gone with the slot; the entries and their - * release edges are otherwise unchanged, per entry. + * PER-STORE and MULTI-ENTRY (issues #158 mechanism B, #210), both load + * bearing. MULTI-ENTRY because two engine resumptions can legitimately be + * pending at once: a running activation X may deliver a resume to Z while + * Y's resumption is still outstanding, and a one-claimant gate cannot + * represent that. PER-STORE because a claim held store-wide makes every + * driver on EVERY store yield: an idle store's `driveStoreAsync` dies at + * the 10,000-hop assert (~311ms) while another store merely dwells on a + * slow host import. * * Cross-store de-serialization is safe by disjointness: an activation - * belongs to exactly one store. Same-store it is strictly more conservative - * than the old slot — the gate keeps refusing until EVERY pending entry has - * died, rather than crashing on the second. + * belongs to exactly one store. Same-store the set is conservative — the + * gate keeps refusing until EVERY pending entry has died. * * Release edges, per entry: the activation PARKS again * (`blockCurrentActivation` -> `consumePendingIfRunning`), it FINISHES (its @@ -1028,10 +1006,8 @@ export class Store { * throw (trap unwinding); callers propagate or park it exactly as they do * for `tick`. * - * Every non-stale tail is dispatched immediately, in queue order. (History, - * issue #156: tails whose instance was not host-enterable were deferred in - * place until the reentrance lock released. CM#705 / polyengine#173 deleted - * the reentrance model, so there is nothing left to defer on.) + * Every non-stale tail is dispatched immediately, in queue order: there is + * no enterability condition to defer on (CM#705). * * The ordering discipline is therefore settle order, full stop — and it is * the reason this queue exists rather than a direct resumption from the @@ -1077,9 +1053,6 @@ export class Store { * It exists to gate `tick` (and to keep the driving loops from parking) * behind unserviced tails: resuming some other thread while a settled tail * waits would expose the out-of-order state the queue is there to prevent. - * (Pre-CM#705 this had to inspect each entry, because a queue holding only - * reentrance-DEFERRED tails had to answer false — issue #156. That case is - * gone with the reentrance model, polyengine#173.) */ hasServiceableSettled(): boolean { return this.settled.length > 0; @@ -1154,9 +1127,9 @@ export class Store { } /** - * definitions.py `Store.tick` (@ 2f13265): resume one ready thread. Post - * CM#705 there is no bracket and no gate — the reference body is exactly - * "pick a ready thread, resume it". + * definitions.py `Store.tick` (@ 2f13265): resume one ready thread. There + * is no bracket and no gate — the reference body is exactly "pick a ready + * thread, resume it" (CM#705). * * Returns false when no thread was ready, so callers can distinguish * "made progress" from "stuck" without inspecting the queue themselves. @@ -1180,21 +1153,21 @@ export class Store { // Same discipline, other edge: a settled-but-unserviced activation tail // (see `settled`) is mid-"atomic resume" from the reference's point of // view; scheduling anything before servicing it acts on phantom state. - // That is settle-order discipline and has nothing to do with reentrance: - // it survives CM#705 unchanged. `hasServiceableSettled` (rather than + // That is settle-order discipline and has nothing to do with reentrance. + // `hasServiceableSettled` (rather than // "queue non-empty") only because a tail whose thread was already resumed // elsewhere must not wedge the store. if (this.hasServiceableSettled()) return false; - // Ready is sufficient — almost. The reentrance constraint that used to - // filter this set is GONE: post-CM#705 (definitions.py @ 2f13265) - // `Store.tick` resumes any ready thread with no gate and no bracket, so a - // sibling instance's thread going ready while another instance is entered - // from the host is simply resumable. + // Ready is sufficient — almost. Nothing filters this set for reentrance: + // at the pinned reference (definitions.py @ 2f13265) `Store.tick` resumes + // any ready thread with no gate and no bracket (CM#705), so a sibling + // instance's thread going ready while another instance is entered from + // the host is simply resumable. // - // What remains is polyengine's per-instance poisoning divergence: a + // What is added is polyengine's per-instance poisoning divergence: a // poisoned instance is a corpse, its threads must never resume, and the - // MARKER is the whole test (#251's re-key). `Thread.resumeWith` makes the - // same call on the tail path. + // MARKER is the whole test. `Thread.resumeWith` makes the same call on + // the tail path. const candidates = this.readyCandidates().filter((t) => !isInstancePoisoned(t.task.inst) ); diff --git a/runtime/src/task/streams.ts b/runtime/src/task/streams.ts index d8e108c..f03a67f 100644 --- a/runtime/src/task/streams.ts +++ b/runtime/src/task/streams.ts @@ -1000,18 +1000,14 @@ interface PoisonedInstanceLike { * can no longer be entered (`tick` excludes poisoned instances). Host sentinels are * not instances at all, so they are always notified. * - * #100: THE HEALTH TEST IS "POISONED", NOT NON-ENTERABILITY. Historical: the - * original test used the transient reentrance gate (`may_enter === False`) as - * a proxy for deadness. That is settled twice over — CM#705 / polyengine#173 - * deleted the whole reentrance model, so the poison marker is not merely the - * better test but the only one left — and the original argument is kept only - * because it explains what the marker is FOR: the proxy was unsound in one - * direction, and the unsoundness stranded healthy tasks. A caller instance - * stayed non-enterable for the whole duration of a cross-component (FACT) - * call into an instance that trapped, so a *different*, healthy task of that - * caller, parked on an end of a stream/future the trapping callee also held, - * was classified dead here and retired silently — stranded, the exact - * outcome #66 exists to prevent. + * #100: THE HEALTH TEST IS "POISONED", NOT "BUSY". Deadness must be judged + * by the poison marker and nothing weaker: any liveness proxy that also + * covers a merely mid-call instance is unsound in one direction and strands + * healthy tasks. Under such a proxy a caller mid cross-component (FACT) call + * into an instance that trapped would drag its *other*, healthy tasks down + * with it — one parked on an end of a stream/future the trapping callee also + * held would be classified dead here and retired silently, the exact outcome + * #66 exists to prevent. * * So the test consults the poison marker itself. It is per-instance and * recorded at the single seam every poisoning site routes through diff --git a/runtime/src/task/waitable.ts b/runtime/src/task/waitable.ts index b916b11..6309992 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]; -export const NO_EVENT: EventTuple = [EventCode.NONE, 0, 0]; /** * definitions.py `class Waitable` (line 767). diff --git a/runtime/tests/bindgen/generated/async-probe.ts b/runtime/tests/bindgen/generated/async-probe.ts index 9f193b8..db4c134 100644 --- a/runtime/tests/bindgen/generated/async-probe.ts +++ b/runtime/tests/bindgen/generated/async-probe.ts @@ -59,10 +59,11 @@ export interface AsyncProbeInstance extends Omit { * Verifies the loaded plan's world digest against `WORLD_DIGEST` * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md * §"Module wiring and instantiation"), throwing -* `WorldDigestMismatchError` with the structural diff on skew — no -* guest code has run when that throws. Accepts the same sources as -* the runtime `instantiate` (pre-translated artifacts, an envelope -* via `artifactsFromEnvelope`, or component bytes plus a translator). +* `WorldDigestMismatchError` — fails fast on skew, no +* structural diff — no guest code has run when that throws. Accepts +* the same sources as the runtime `instantiate` (pre-translated +* artifacts, an envelope via `artifactsFromEnvelope`, or component +* bytes plus a translator). * * Use `bind` instead only when the plan was verified already. */ export async function instantiate( diff --git a/runtime/tests/bindgen/generated/future-user.ts b/runtime/tests/bindgen/generated/future-user.ts index e4e3691..f11071e 100644 --- a/runtime/tests/bindgen/generated/future-user.ts +++ b/runtime/tests/bindgen/generated/future-user.ts @@ -58,10 +58,11 @@ export interface FutureUserInstance extends Omit { * Verifies the loaded plan's world digest against `WORLD_DIGEST` * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md * §"Module wiring and instantiation"), throwing -* `WorldDigestMismatchError` with the structural diff on skew — no -* guest code has run when that throws. Accepts the same sources as -* the runtime `instantiate` (pre-translated artifacts, an envelope -* via `artifactsFromEnvelope`, or component bytes plus a translator). +* `WorldDigestMismatchError` — fails fast on skew, no +* structural diff — no guest code has run when that throws. Accepts +* the same sources as the runtime `instantiate` (pre-translated +* artifacts, an envelope via `artifactsFromEnvelope`, or component +* bytes plus a translator). * * Use `bind` instead only when the plan was verified already. */ export async function instantiate( diff --git a/runtime/tests/bindgen/generated/hello.ts b/runtime/tests/bindgen/generated/hello.ts index 70569bd..ee7e6e0 100644 --- a/runtime/tests/bindgen/generated/hello.ts +++ b/runtime/tests/bindgen/generated/hello.ts @@ -57,10 +57,11 @@ export interface HelloInstance extends Omit { * Verifies the loaded plan's world digest against `WORLD_DIGEST` * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md * §"Module wiring and instantiation"), throwing -* `WorldDigestMismatchError` with the structural diff on skew — no -* guest code has run when that throws. Accepts the same sources as -* the runtime `instantiate` (pre-translated artifacts, an envelope -* via `artifactsFromEnvelope`, or component bytes plus a translator). +* `WorldDigestMismatchError` — fails fast on skew, no +* structural diff — no guest code has run when that throws. Accepts +* the same sources as the runtime `instantiate` (pre-translated +* artifacts, an envelope via `artifactsFromEnvelope`, or component +* bytes plus a translator). * * Use `bind` instead only when the plan was verified already. */ export async function instantiate( diff --git a/runtime/tests/bindgen/generated/resources.ts b/runtime/tests/bindgen/generated/resources.ts index 37f69fb..d80916d 100644 --- a/runtime/tests/bindgen/generated/resources.ts +++ b/runtime/tests/bindgen/generated/resources.ts @@ -82,10 +82,11 @@ export interface ResourcesInstance extends Omit { * Verifies the loaded plan's world digest against `WORLD_DIGEST` * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md * §"Module wiring and instantiation"), throwing -* `WorldDigestMismatchError` with the structural diff on skew — no -* guest code has run when that throws. Accepts the same sources as -* the runtime `instantiate` (pre-translated artifacts, an envelope -* via `artifactsFromEnvelope`, or component bytes plus a translator). +* `WorldDigestMismatchError` — fails fast on skew, no +* structural diff — no guest code has run when that throws. Accepts +* the same sources as the runtime `instantiate` (pre-translated +* artifacts, an envelope via `artifactsFromEnvelope`, or component +* bytes plus a translator). * * Use `bind` instead only when the plan was verified already. */ export async function instantiate( diff --git a/runtime/tests/bindgen/generated/stream-echo.ts b/runtime/tests/bindgen/generated/stream-echo.ts index 461f9b5..6bb100d 100644 --- a/runtime/tests/bindgen/generated/stream-echo.ts +++ b/runtime/tests/bindgen/generated/stream-echo.ts @@ -57,10 +57,11 @@ export interface StreamEchoInstance extends Omit { * Verifies the loaded plan's world digest against `WORLD_DIGEST` * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md * §"Module wiring and instantiation"), throwing -* `WorldDigestMismatchError` with the structural diff on skew — no -* guest code has run when that throws. Accepts the same sources as -* the runtime `instantiate` (pre-translated artifacts, an envelope -* via `artifactsFromEnvelope`, or component bytes plus a translator). +* `WorldDigestMismatchError` — fails fast on skew, no +* structural diff — no guest code has run when that throws. Accepts +* the same sources as the runtime `instantiate` (pre-translated +* artifacts, an envelope via `artifactsFromEnvelope`, or component +* bytes plus a translator). * * Use `bind` instead only when the plan was verified already. */ export async function instantiate( diff --git a/runtime/tests/bindgen/generated/values.ts b/runtime/tests/bindgen/generated/values.ts index 02b7f61..8466fe7 100644 --- a/runtime/tests/bindgen/generated/values.ts +++ b/runtime/tests/bindgen/generated/values.ts @@ -104,10 +104,11 @@ export interface ValuesInstance extends Omit { * Verifies the loaded plan's world digest against `WORLD_DIGEST` * BEFORE instantiating (contracts/digest.md; contracts/embedder-api.md * §"Module wiring and instantiation"), throwing -* `WorldDigestMismatchError` with the structural diff on skew — no -* guest code has run when that throws. Accepts the same sources as -* the runtime `instantiate` (pre-translated artifacts, an envelope -* via `artifactsFromEnvelope`, or component bytes plus a translator). +* `WorldDigestMismatchError` — fails fast on skew, no +* structural diff — no guest code has run when that throws. Accepts +* the same sources as the runtime `instantiate` (pre-translated +* artifacts, an envelope via `artifactsFromEnvelope`, or component +* bytes plus a translator). * * Use `bind` instead only when the plan was verified already. */ export async function instantiate( diff --git a/runtime/tests/bindgen/instantiate_test.ts b/runtime/tests/bindgen/instantiate_test.ts index 1e90ee0..c976fbd 100644 --- a/runtime/tests/bindgen/instantiate_test.ts +++ b/runtime/tests/bindgen/instantiate_test.ts @@ -3,7 +3,7 @@ // contracts/embedder-api.md §"Module wiring and instantiation" + // contracts/digest.md: bindings embed the expected world digest and the // digest is recomputed from the loaded plan AT INSTANTIATE TIME, failing -// fast with a structural diff on mismatch. `crates/bindgen` emits that +// fast on mismatch (no structural diff). `crates/bindgen` emits that // path as `instantiate` in every generated module (the checked-in // snapshots under ./generated); this test drives it against real // translated components. diff --git a/runtime/tests/bindgen/usage/hello_usage.ts b/runtime/tests/bindgen/usage/hello_usage.ts index 37a51b8..2e46cff 100644 --- a/runtime/tests/bindgen/usage/hello_usage.ts +++ b/runtime/tests/bindgen/usage/hello_usage.ts @@ -21,8 +21,7 @@ export async function useHello(instance: EmbedderInstance, plan: WirePlan) { const mismatch = await verify(plan); if (mismatch) { throw new Error( - `hello world digest mismatch: expected ${mismatch.expected}, got ${mismatch.actual}` + - (mismatch.firstDivergence ? ` (${mismatch.firstDivergence})` : ""), + `hello world digest mismatch: expected ${mismatch.expected}, got ${mismatch.actual}`, ); } const exports = bind(instance); diff --git a/runtime/tests/cancel_bracket_race_test.ts b/runtime/tests/cancel_bracket_race_test.ts index 33af676..0bccea8 100644 --- a/runtime/tests/cancel_bracket_race_test.ts +++ b/runtime/tests/cancel_bracket_race_test.ts @@ -7,12 +7,9 @@ // built-ins, `exit-sync-call`, etc.) runs on a LATER microtask. A concurrent // host EXPORT call can enter the same instance in between. // -// Originally the delivery was wrapped in a host-entry bracket and -// the question was whether that bracket closed too early. CM#705 -// (polyengine#173) removed the bracket — and the whole reentrance gate — -// from the reference and from this runtime, so the concurrent entry is now -// unconditionally ADMITTED by design rather than by an accident of bracket -// timing. The behavioral pin is unchanged and still worth keeping: driving a +// There is no host-entry bracket and no reentrance gate, in the reference or +// here (CM#705), so the concurrent entry is ADMITTED by design rather than by +// an accident of bracket timing. The behavioral pin: driving a // second export call through that window does not double-resume anything or // leave inconsistent final state. It is included in `just sched-seeds` so any // future schedule-order dependence here is caught. @@ -114,8 +111,8 @@ Deno.test( assertEq(task.state, "cancel-delivered"); // Drive a concurrent EXPORT call into the SAME instance through the real - // host-entry path (`createLiftedFunction`), which post-CM#705 refuses - // only a poisoned instance. If this traps or corrupts state, the + // host-entry path (`createLiftedFunction`), which refuses only a + // poisoned instance (CM#705). If this traps or corrupts state, the // divergence is no longer merely theoretical. const syncFt: FuncType = { params: [], results: [], async: false }; const exportOpts: ResolvedOptions = { diff --git a/runtime/tests/cross_store_driver_test.ts b/runtime/tests/cross_store_driver_test.ts index e9e16c0..8c16021 100644 --- a/runtime/tests/cross_store_driver_test.ts +++ b/runtime/tests/cross_store_driver_test.ts @@ -3,19 +3,19 @@ // The driver's speculative resume entry (exec/boundary.ts, `Promise.race` // over the parked threads) is held for the entire duration of a guest's wait // on a slow host import — the completely ordinary suspended-guest shape. -// While that gate lived in a module-global slot, EVERY `driveAsync` loop -// yielded at its top while ANY store held it, with a bounded hop counter: +// The gate is `Store.pendingResumptions`, PER STORE: activations never cross +// stores, so A's pending resumption is none of B's business. Were it shared, +// every `driveAsync` loop would yield at its top while ANY store held an +// entry, against a bounded hop counter: // // assert_(claimHops < 10_000, "driveAsync: a resumed-activation claim was // never released ...") // -// so an idle, completely unrelated store B's `driveStoreAsync` died at 10,000 -// hops in ~311ms while store A merely dwelt on its import — an internal -// AssertionError naming neither component (issue #210). The gate is now -// `Store.pendingResumptions`, per store: activations never cross stores, so -// A's pending resumption is none of B's business. +// so an idle, completely unrelated store B's `driveStoreAsync` would die at +// 10,000 hops in ~311ms while store A merely dwelt on its import — an +// internal AssertionError naming neither component (issue #210). // -// This test is the #210 probe INVERTED: B's driver must return promptly. The +// This test pins the requirement: B's driver must return promptly. The // control below pins that the gate still gates — A's OWN driver refuses to // tick past A's own pending entry. // diff --git a/runtime/tests/digest_test.ts b/runtime/tests/digest_test.ts index 36bfea8..1eff978 100644 --- a/runtime/tests/digest_test.ts +++ b/runtime/tests/digest_test.ts @@ -33,7 +33,6 @@ import { assertEq } from "./support/asserts.ts"; import { loadEnvelope } from "../src/plan/mod.ts"; import { computeWorldDigest, DigestError } from "../src/digest/digest.ts"; -import { diffWorldDigest } from "../src/digest/verify.ts"; import type { WireExport, WirePlan } from "../src/plan/format.ts"; /** @@ -117,67 +116,6 @@ Deno.test("digest: recomputing from the same plan is deterministic", async () => assertEq(a.canonicalJson, b.canonicalJson); }); -Deno.test("digest: mismatch report names a concrete first divergence (values WIT vs hello plan)", async () => { - const { wire: helloWire } = await readEnvelope("hello"); - // The `values` world's canonical JSON, as an independent "expected" side - // (in real bindgen-generated code this would be `WORLD_DIGEST`'s - // canonical JSON, embedded at generation time — here reconstructed from - // the values plan fixture, which is digest-equal to the values WIT by - // the test above). - const { wire: valuesWire } = await readEnvelope("values"); - const { canonicalJson: valuesCanonicalJson } = await computeWorldDigest( - valuesWire, - ); - - const mismatch = await diffWorldDigest(helloWire, valuesCanonicalJson); - if (mismatch === null) { - throw new Error("expected a digest mismatch between hello and values"); - } - assertEq(mismatch.expected, EXPECTED.values); - assertEq(mismatch.actual, EXPECTED.hello); - // Not just "digests differ": a concrete path into the export tree. - if (mismatch.firstDivergence === null) { - throw new Error("expected a concrete firstDivergence path, got null"); - } - console.log("mismatch report:", mismatch.firstDivergence); - // Concrete, not just "digests differ": names the exports list and shows - // the actual counts that diverge (hello has 1 export, values has 17). - if (!mismatch.firstDivergence.includes("exports")) { - throw new Error( - `expected firstDivergence to name the exports-list mismatch, got: ${mismatch.firstDivergence}`, - ); - } - if (!mismatch.firstDivergence.includes("17") || !mismatch.firstDivergence.includes("1 ")) { - throw new Error( - `expected firstDivergence to show the diverging counts (17 vs 1), got: ${mismatch.firstDivergence}`, - ); - } -}); - -Deno.test("digest: mismatch report names a concrete divergent export name (same export count)", async () => { - // A sharper case than the count-mismatch above: two worlds with the same - // *number* of exports, where the Nth export differs by name — the report - // must point at that specific slot, not just say lengths matched. - const { wire: helloWire } = await readEnvelope("hello"); - const decoyExpected = JSON.stringify({ - cewd: 1, - imports: [], - exports: [{ - kind: "func", - name: "not-greet", - func: { params: [{ kind: "string" }], results: [{ kind: "string" }], async: false }, - }], - }); - const mismatch = await diffWorldDigest(helloWire, decoyExpected); - if (mismatch === null) throw new Error("expected a mismatch"); - console.log("mismatch report (same count):", mismatch.firstDivergence); - if (!mismatch.firstDivergence?.includes("greet") || !mismatch.firstDivergence?.includes("name")) { - throw new Error( - `expected firstDivergence to name the diverging export name, got: ${mismatch.firstDivergence}`, - ); - } -}); - Deno.test("digest: verifyWorldDigest returns null on an exact match", async () => { const { wire } = await readEnvelope("hello"); const { verifyWorldDigest } = await import("../src/digest/verify.ts"); diff --git a/runtime/tests/dtor_normalization_test.ts b/runtime/tests/dtor_normalization_test.ts index 02ef73c..207f690 100644 --- a/runtime/tests/dtor_normalization_test.ts +++ b/runtime/tests/dtor_normalization_test.ts @@ -3,26 +3,11 @@ // Authority: definitions.py `canon_resource_drop` (line 2319) builds the dtor // into a function instance and calls it through `Store.lift` with // `CanonicalOptions(async_ = False)` / `FuncType([U32Type()], [], async_ = -// False)`. Before #160 the host-initiated path called `rt.dtor` bare while -// HOLDING a host-entry bracket across the returned promise, which produced two -// observable defects pinned below: -// -// 1. the dtor's own suspension points were unresumable — `Store.tick`'s -// enterability filter (#155) skips a thread whose instance is not -// host-enterable, and the held bracket made the impl exactly that, so -// the completion promise (parked in `pendingHostCalls`, i.e. advertised -// as *external* work) never settled and every driver waited forever; -// 2. the held bracket also locked the per-instantiation root for -// the whole activation, so a SIBLING instance of the same component -// looked non-enterable from the host — the macro-scale window of the -// #156 class. -// -// Both are structural consequences of the missing Task/Thread, and both are -// gone now that the dtor runs through `createLiftedFunction`. CM#705 -// (polyengine#173) has since removed the gate itself, so neither shape is -// even expressible any more; the pins below are restated in terms of what is -// still observable — the dtor's own task, scheduler resumability, and the -// fact that a dtor may re-enter a LIVE instance at all. +// False)`. The host-initiated path runs the dtor through +// `createLiftedFunction`, so the activation has a real Task/Thread. The pins +// below cover what that buys: the dtor's own task, scheduler resumability of +// its suspension points, its completion NOT being advertised as external +// work, and the fact that a dtor may enter a LIVE instance at all. import { ResourceTypeInfo } from "../src/cabi/mod.ts"; import { @@ -67,9 +52,8 @@ Deno.test("#160: a dtor parked on a scheduler-resumable suspension point complet hostDtorCall(rt, 77); - // The park happened, and `tick` can resume the point below. Pre-#160 the - // held bracket made the impl non-enterable and the store wedged forever; - // post-#705 nothing can make it non-enterable except poisoning. + // The park happened, and `tick` can resume the point below: nothing can + // make the impl non-enterable except poisoning (CM#705). assertEq(finished, false); assertEq(entryRefusal(impl, null, "base"), null); assertEq(store.waiting.length >= 1, true); @@ -87,9 +71,7 @@ Deno.test("#160: a dtor parked on a scheduler-resumable suspension point complet }); Deno.test("#160/#173: a dtor may run while its own instance is LIVE", async () => { - // REPLACES the "#160/#156: a sibling instance stays enterable" pin, which - // is trivial now (nothing can be non-enterable). The stronger merged - // property: `canon_resource_drop` lifts the dtor with no gate at all + // `canon_resource_drop` lifts the dtor with no gate at all // (definitions.py @ 2f13265), so a dtor whose implementing instance is in // the middle of a host-initiated activation is valid and both complete. const store = new Store(); @@ -106,8 +88,7 @@ Deno.test("#160/#173: a dtor may run while its own instance is LIVE", async () = hostDtorCall(slow, 5); // A SECOND, synchronous dtor of the same instance, entered while the first - // is still in flight. Pre-#705 this was refused ("cannot enter component - // instance"); now it simply runs. + // is still in flight: it simply runs (CM#705). let ranNested = 0; const quick = new ResourceTypeInfo(impl, (() => { ranNested += 1; diff --git a/runtime/tests/embedder/version_test.ts b/runtime/tests/embedder/version_test.ts index 3742a3d..07fc319 100644 --- a/runtime/tests/embedder/version_test.ts +++ b/runtime/tests/embedder/version_test.ts @@ -7,15 +7,13 @@ import { assertEq } from "../support/asserts.ts"; import { asTrackKeySpelling, - camelCase, ImportRegistrationError, ImportResolutionError, ImportResolver, - parseLeafName, - pascalCase, - NameCollisionError, trackKey, -} from "../../src/embedder/mod.ts"; +} from "../../src/embedder/version.ts"; +import { camelCase, parseLeafName, pascalCase } from "../../src/embedder/casing.ts"; +import { NameCollisionError } from "../../src/embedder/errors.ts"; import { checkNoCollisions } from "../../src/embedder/values.ts"; const P = "wasi:clocks/monotonic-clock"; diff --git a/runtime/tests/enter_sync_call_reentrance_test.ts b/runtime/tests/enter_sync_call_reentrance_test.ts index a77c931..7bd2d4e 100644 --- a/runtime/tests/enter_sync_call_reentrance_test.ts +++ b/runtime/tests/enter_sync_call_reentrance_test.ts @@ -1,13 +1,10 @@ -// Entry refusal on the sync fused-adapter bracket (issues #99, #173). +// Entry refusal on the sync fused-adapter bracket (issue #99). // -// INVERTED by polyengine#173. The reference chain that used to gate here — -// `canon_lower` invoking the callee `FuncInst` with `caller = -// thread.task.inst`, and `Store.lift`'s `trap_if(not -// inst.may_enter_from(caller))` over `entering_set(caller)` — is GONE: -// CM#705 (definitions.py @ 2f13265) removed `may_enter`, `entering_set`, -// `enter_from`, `leave_to` and `ComponentInstance.parent` outright, and -// `Store.lift` now runs `canon_lift` unconditionally. A sibling cycle -// A -> C -> A through the trampoline therefore does NOT trap. +// Nothing at the pinned reference gates this site: definitions.py @ 2f13265 +// has no `may_enter`, `entering_set`, `enter_from`, `leave_to` or +// `ComponentInstance.parent`, and `Store.lift` runs `canon_lift` +// unconditionally (CM#705). A sibling cycle A -> C -> A through the +// trampoline therefore does NOT trap. // // wasmtime agreed all along: `enter_guest_sync_call` // (47.0.3 `runtime/component/concurrent.rs:1723`) performs no reentrance @@ -69,8 +66,8 @@ Deno.test("enter-sync-call: an idle sibling callee is enterable", () => { }); Deno.test("enter-sync-call: a sibling cycle A -> C -> A no longer traps (CM#705)", () => { - // Was: "sibling cycle A -> C -> A traps". Host entered A; A is mid-call - // into C; C calls back into A. Post-CM#705 that is simply a valid call. + // Host entered A; A is mid-call into C; C calls back into A. That is + // simply a valid call (CM#705). const { enter, exit, syncCallStack } = fixture(); enter(A, 0, C); enter(C, 0, A); @@ -100,8 +97,8 @@ Deno.test("enter-sync-call: a POISONED callee is refused, naming the trap", () = }); Deno.test("enter-sync-call: a poisoned instance calling ITSELF passes vacuously", () => { - // `entryRefusal`'s `caller !== callee` guard: the pre-#705 entering set - // `{A} - {A}` was empty, and the vacuous pass is preserved. + // `entryRefusal`'s `caller !== callee` guard passes a self-call + // vacuously, even against a marked instance. const { enter, inst } = fixture(); notifyInstancePoisoned(inst(A), new Error("earlier boom")); enter(A, 0, A); diff --git a/runtime/tests/integration/e2e_hello_test.ts b/runtime/tests/integration/e2e_hello_test.ts index 7c5619c..16f50eb 100644 --- a/runtime/tests/integration/e2e_hello_test.ts +++ b/runtime/tests/integration/e2e_hello_test.ts @@ -64,8 +64,7 @@ Deno.test("hello: full pipeline shim -> plan -> executor -> greet()", async () = assertEq(component.stats.tasksResolved, 1); // The may_leave flag is released after the sync call resolved. (There is - // no may_enter counterpart any more: CM#705 / polyengine#173 deleted the - // transient reentrance model.) + // no may_enter counterpart: CM#705.) const inst = component.componentInstances[0]; assert(inst.mayLeave, "may_leave must be restored after call"); assertEq(inst.flags.value, 1); @@ -136,11 +135,9 @@ Deno.test("task model: reentrance is permitted; a failed call poisons", async () const greet = component.exports.greet as (name: string) => string; greet("warm-up"); - // INVERTED by polyengine#173 (CM#705). This used to simulate an - // in-progress activation of the same instance and require the next host - // entry to trap. The merged reference (definitions.py @ 2f13265) has no - // `may_enter`, no `entering_set` and no bracket in `Store.lift`, so entry - // into a live instance is valid. The live host-mediated shape is pinned + // The reference (definitions.py @ 2f13265) has no `may_enter`, no + // `entering_set` and no bracket in `Store.lift` (CM#705), so entry into a + // live instance is valid. The live host-mediated shape is pinned // end-to-end in e2e_imports_test.ts ("a host import may synchronously // re-enter its own instance"); here we only pin that nothing refuses. const inst = component.componentInstances[0]; @@ -154,7 +151,7 @@ Deno.test("task model: reentrance is permitted; a failed call poisons", async () // half-written its argument buffer — the instance is in exactly the // indeterminate state poisoning exists for. (Poisoning is polyengine's // named divergence — a per-instance corpse where wasmtime kills the whole - // store — and since CM#705 it is the ONLY reason an entry is refused.) + // store — and it is the ONLY reason an entry is refused, CM#705.) let threw = false; try { greet(123 as unknown as string); diff --git a/runtime/tests/integration/e2e_imports_test.ts b/runtime/tests/integration/e2e_imports_test.ts index 25ecf48..4a7304b 100644 --- a/runtime/tests/integration/e2e_imports_test.ts +++ b/runtime/tests/integration/e2e_imports_test.ts @@ -366,20 +366,17 @@ Deno.test({ }); // --------------------------------------------------------------------------- -// polyengine#173 (CM#705): HOST-MEDIATED REENTRANCE IS VALID +// HOST-MEDIATED REENTRANCE IS VALID (CM#705) // --------------------------------------------------------------------------- Deno.test({ name: "reentrance: a host import may synchronously re-enter its own instance", ignore: shimWasm === null, fn: async () => { - // The headline of the CM#705 adoption. Host calls `run`; `run` calls the - // host import `log`; the host handler synchronously calls `run` on the - // SAME instance again. This used to trap at the host-entry gate - // ("cannot enter component instance"): the reference's `Store.lift` - // refused a second entry while the first was live. + // Host calls `run`; `run` calls the host import `log`; the host handler + // synchronously calls `run` on the SAME instance again. // - // Merged reference (definitions.py @ 2f13265): `Store.lift` runs + // Reference (definitions.py @ 2f13265): `Store.lift` runs // `canon_lift` with no gate, `may_enter`/`entering_set`/`enter_from`/ // `leave_to` do not exist, and nesting host entries is simply legal. // Both calls complete. diff --git a/runtime/tests/integration/e2e_suite_test.ts b/runtime/tests/integration/e2e_suite_test.ts index 59ff069..0c9a015 100644 --- a/runtime/tests/integration/e2e_suite_test.ts +++ b/runtime/tests/integration/e2e_suite_test.ts @@ -169,15 +169,10 @@ Deno.test({ name: "suite resources/borrows.0: a trapped instance is poisoned", ignore: !ready, fn: async () => { - // definitions.py `Store.lift` (line 578): - // - // trap_if(not inst.may_enter_from(caller)) - // inst.enter_from(caller) - // on_cancel = canon_lift(...) # a Trap propagates out of here ... - // inst.leave_to(caller) # ... so this never runs - // - // A trap therefore leaves every instance the call entered permanently - // un-enterable. `test/async/builtin-trap-poisons-instance.wast` asserts + // A trap leaves the instance it escaped from permanently un-enterable: + // polyengine's per-instance poisoning divergence (task/scheduler.ts + // `entryRefusal`), where wasmtime kills the whole store. + // `test/async/builtin-trap-poisons-instance.wast` asserts // this directly ("cannot enter component instance" on the second invoke), // and the whole suite relies on it — which is why files that test several // traps build a fresh component instance for each one. @@ -411,8 +406,7 @@ async function commandsOf(dir: string): Promise<[string, WastCommand[]][]> { * Known acceptance gaps for the blanket verdict check below: wasmparser * 0.252 (pinned via wasmtime-environ 47.0.3, crates/translator-shim) * validates components that CM#703/#704 ("name rules") and CM#688 - * ("max-value-size") — pulled in by the CM#705 pin advance, polyengine#173 - * — newly require rejecting. Tracked https://github.com/polymorph-components/polyengine/issues/248. + * ("max-value-size") require rejecting at the current pin. Tracked https://github.com/polymorph-components/polyengine/issues/248. * * Authoritative source of truth for these rows is harness/src/xfail.ts (same * (file, line) keys): its stale-xfail detector (harness/tests/ @@ -547,11 +541,11 @@ Deno.test({ // schema notes; polyengine#13). The // export surfaces as the already-compiled `WebAssembly.Module`, and it is // the *embedded* module: instantiating it works and its export list matches -// the wast source (an empty module). Artifact index shifted 115->116 by the -// CM#705 pin advance (polyengine#173): CM#698 (dff1181, "fix some spec and -// test typos") added 12 lines earlier in binary.wast, renumbering this -// component from wast line 1421 to 1433 and its testgen-assigned positional -// artifact index from 115 to 116 (verified: +// the wast source (an empty module). The artifact index is 116, not the 115 +// its wast position might suggest: CM#698 (dff1181, "fix some spec and test +// typos") added 12 lines earlier in binary.wast, moving this component to +// wast line 1433 and its testgen-assigned positional artifact index to 116 +// (verified: // `git -C third_party/component-model show 2f13265:test/binary/binary.wast`). Deno.test({ name: "suite binary.116: a core-module export surfaces as WebAssembly.Module", diff --git a/runtime/tests/jspi/ambient_test.ts b/runtime/tests/jspi/ambient_test.ts index 08088a3..9af9902 100644 --- a/runtime/tests/jspi/ambient_test.ts +++ b/runtime/tests/jspi/ambient_test.ts @@ -72,7 +72,8 @@ Deno.test({ name: "jspi pin (i): a resumed activation runs BEFORE our own continuation", ignore: !isSupported(), fn: async () => { - // This is the ordering the `resumingThread` mechanism relies on. Settling + // This is the ordering the driver's pending-resumption gate + // (`Store.pendingResumptions`) relies on. Settling // a Suspending import's Promise hands control to wasm, and the resumed // activation runs its built-ins *before* the promising Promise settles and // before our await continuation. Combined with JS being single-threaded, diff --git a/runtime/tests/jspi/bridge_test.ts b/runtime/tests/jspi/bridge_test.ts index 2d967a1..38f13f2 100644 --- a/runtime/tests/jspi/bridge_test.ts +++ b/runtime/tests/jspi/bridge_test.ts @@ -105,14 +105,12 @@ Deno.test("bridge: a trap computed at resume time becomes a rejection", async () }); Deno.test("bridge: a suspension does not make the instance unusable", async () => { - // Was: "reentrance gates are ours and hold across a suspension". Empirical - // fact (d) is unchanged — the ENGINE freely permits reentering an instance - // while one of its activations is suspended — but the Component Model no - // longer forbids it either: CM#705 (definitions.py @ 2f13265) deleted - // `may_enter`/`enter_from`/`leave_to`, so there is no gate to hold - // (polyengine#173). What this pins now is that a suspended-and-resumed - // activation leaves the instance entirely usable: nothing is refused before, - // during or after. + // Empirical fact (d): the ENGINE freely permits reentering an instance + // while one of its activations is suspended — and the Component Model does + // not forbid it either (definitions.py @ 2f13265 has no + // `may_enter`/`enter_from`/`leave_to`, CM#705), so there is no gate to + // hold. What this pins: a suspended-and-resumed activation leaves the + // instance entirely usable — nothing is refused before, during or after. const store = new Store(); const inst = new ComponentInstanceState(0, store); let flag = false; diff --git a/runtime/tests/jspi/hop_atomicity_test.ts b/runtime/tests/jspi/hop_atomicity_test.ts index c5d054e..06528f9 100644 --- a/runtime/tests/jspi/hop_atomicity_test.ts +++ b/runtime/tests/jspi/hop_atomicity_test.ts @@ -15,9 +15,9 @@ // each inner (ptr,len), then the bytes. // // Nothing holds the instance across that window: the lift happens later -// still, in `finishHostEntry`, and post-CM#705 (polyengine#173) there is no -// reentrance gate anywhere to hold in the first place. A hop-park is a park, -// so pre-fix a SECOND host call could enter and run a full guest turn in that +// still, in `finishHostEntry`, and there is no reentrance gate anywhere to +// hold in the first place (CM#705). A hop-park is a park, so without the gate +// below a SECOND host call could enter and run a full guest turn in that // window — and if that turn mutates the memory the pending lift is about to // read, the lift reads whatever the intruder left. The hop-quiescence gate in // `exec/boundary.ts` is the only thing standing between the two. diff --git a/runtime/tests/poison_cause_test.ts b/runtime/tests/poison_cause_test.ts index 72380af..4bb23b8 100644 --- a/runtime/tests/poison_cause_test.ts +++ b/runtime/tests/poison_cause_test.ts @@ -1,10 +1,10 @@ // polyengine#145 ask 1: entry refusals on a poisoned instance name the // original trap. The scheduler records the FIRST poisoning cause per instance // (follow-on failures against a corpse are noise) and `withPoisonCause` -// appends it to the refusal message. Since CM#705 (polyengine#173) poisoning -// is the ONLY reason an entry is refused, so the helper's pass-through -// behavior on an unmarked instance is just the "nothing to say" case rather -// than a second refusal class. The e2e face is asserted in +// appends it to the refusal message. Poisoning is the ONLY reason an entry is +// refused (CM#705), so the helper's pass-through behavior on an unmarked +// instance is just the "nothing to say" case rather than a second refusal +// class. The e2e face is asserted in // integration/e2e_hello_test.ts, the trampoline face in // enter_sync_call_reentrance_test.ts. // diff --git a/runtime/tests/resource_lifetime_test.ts b/runtime/tests/resource_lifetime_test.ts index 6d175bd..aa90c25 100644 --- a/runtime/tests/resource_lifetime_test.ts +++ b/runtime/tests/resource_lifetime_test.ts @@ -1,8 +1,8 @@ // Resource destructor gating and host-side lend tracking (issues #85, #86). // // Authority: definitions.py `canon_resource_drop` (@ 2f13265) and the -// `Store.lift` it routes the dtor through — which post-CM#705 carries NO -// entry gate, so dtor reentrance into a live instance is valid — plus +// `Store.lift` it routes the dtor through — which carries NO entry gate +// (CM#705), so dtor reentrance into a live instance is valid — plus // the lend bookkeeping of `Subtask.add_lender` / `deliver_resolve` // (lines 890, 902) and the `num_lends` traps in `lift_own` / // `canon_resource_drop` (lines 1508, 2325). @@ -88,9 +88,9 @@ function mkPair(): { // --------------------------------------------------------------------------- Deno.test("#85/#173: dropping a cross-instance own while the impl is LIVE succeeds", () => { - // INVERTED by polyengine#173 (CM#705): `canon_resource_drop` lifts the dtor - // through a `Store.lift` that no longer gates, so a drop whose implementing - // instance is mid-execution is valid and the dtor simply runs. + // `canon_resource_drop` lifts the dtor through a `Store.lift` that does + // not gate (CM#705), so a drop whose implementing instance is + // mid-execution is valid and the dtor simply runs. const { caller, impl } = mkPair(); let ran = 0; const rt = new ResourceTypeInfo(impl, () => { @@ -183,15 +183,10 @@ Deno.test("#85: a guest-initiated dtor that does not finish synchronously traps" }); Deno.test("#160: a host-initiated async dtor is not external work", async () => { - // REVISED from the #85 pin "holds the gate until it settles". That - // behaviour was the bug: the held host-entry bracket made the impl - // instance non-enterable for the whole activation, so `Store.tick`'s - // enterability filter could never resume a suspension point belonging to - // the dtor itself (#160). A host-initiated dtor is a full canonical lift - // (definitions.py `canon_resource_drop`), and post-CM#705 (polyengine#173) - // there is no gate left to hold at all. What still needs pinning: the - // completion promise is NOT a `pendingHostCalls` entry (it is not external - // work), and the store drains cleanly. + // A host-initiated dtor is a full canonical lift (definitions.py + // `canon_resource_drop`), and there is no gate to hold across it (CM#705). + // What needs pinning: the completion promise is NOT a `pendingHostCalls` + // entry (it is not external work), and the store drains cleanly. const { store, impl } = mkPair(); let resolveDtor: () => void = () => {}; const rt = new ResourceTypeInfo( diff --git a/runtime/tests/resume_claim_discipline_test.ts b/runtime/tests/resume_claim_discipline_test.ts index 5766395..6a8f741 100644 --- a/runtime/tests/resume_claim_discipline_test.ts +++ b/runtime/tests/resume_claim_discipline_test.ts @@ -322,7 +322,8 @@ Deno.test("resume from a DIFFERENT running activation while a resumption is pend Deno.test("resume from a DIFFERENT running activation while a resumption is pending — same store (#158 mechanism B)", () => { // The same shape with all three activations in ONE store: the set holds both // pending entries, and the store's gate refuses to schedule while either - // lives (strictly more conservative than the old slot, which crashed). + // lives — two legitimately-pending resumptions, neither of which may be + // dropped. const store = new Store(); const x = mkWorld({ store }); const y = mkWorld({ store }); diff --git a/runtime/tests/settled_deferral_test.ts b/runtime/tests/settled_deferral_test.ts index 5ee8fd3..96da05d 100644 --- a/runtime/tests/settled_deferral_test.ts +++ b/runtime/tests/settled_deferral_test.ts @@ -1,17 +1,13 @@ -// Driver-level coverage for issue #156, INVERTED by polyengine#173 (CM#705). +// Driver-level coverage for issue #156. // -// #156's shape: instance B's thread parked on an already-settled -// `awaitValue` (tail queued in `store.settled`) while sibling instance A held -// a host entry, which under the transient reentrance model's shared -// per-instantiation root made B non-enterable — so B's tail was DEFERRED IN -// PLACE and `driveAsync` had to park (not spin) until the lock released. -// -// Deferral can no longer occur: definitions.py @ 2f13265 has no -// `may_enter`/`enter_from`/`leave_to`, polyengine#173 deleted the model here -// too, and every non-stale tail dispatches on the spot. The pin below -// is the merged behavior — the tail dispatches immediately, with an unrelated -// outstanding host call in flight, and the driver still reaches quiescence -// (the outstanding call must not be mistaken for a reason to wedge). +// The shape: instance B's thread parked on an already-settled `awaitValue` +// (tail queued in `store.settled`) while sibling instance A holds a host +// entry. Nothing defers the tail — definitions.py @ 2f13265 has no +// `may_enter`/`enter_from`/`leave_to` (CM#705), so every non-stale tail +// dispatches on the spot. The pin below: the tail dispatches immediately, +// with an unrelated outstanding host call in flight, and the driver still +// reaches quiescence (the outstanding call must not be mistaken for a reason +// to wedge). import { assertEq } from "./support/asserts.ts"; import { driveStoreAsync } from "../src/exec/boundary.ts"; @@ -71,10 +67,10 @@ Deno.test("driveAsync: a sibling's tail dispatches immediately (CM#705)", async await Promise.resolve(); assertEq(store.settled.length, 1, "B's tail is queued"); - // A is mid-host-entry. Post-CM#705 that constrains nothing about B. + // A is mid-host-entry; that constrains nothing about B (CM#705). void a; - // An outstanding host call, registered exactly as `callDtorGated` did + // An outstanding host call, registered as the dtor path registers one // (`.then` attached BEFORE insertion, so the driver's race sees an entry // that self-removes). Demonstrably a macrotask away: the driver must park, // not spin, while it is outstanding. diff --git a/runtime/tests/streams_teardown_test.ts b/runtime/tests/streams_teardown_test.ts index 937532f..01e1e68 100644 --- a/runtime/tests/streams_teardown_test.ts +++ b/runtime/tests/streams_teardown_test.ts @@ -535,14 +535,12 @@ Deno.test("#97: cancelRead resolves the read exactly like end-of-stream does", a // cross-component (FACT) call into instance B (callee). A DIFFERENT, // perfectly healthy task of A is parked on an end of a stream/future whose // peer end B holds. B traps; the poisoning walk runs over B's table and -// reaches A's parked side. The old health test (non-enterability) read A -// as a corpse — A was non-enterable merely because it was mid-call — and -// retired it in silence: stranded, the outcome #66 exists to prevent. The -// narrowed test (task/scheduler.ts's per-instance poison marker, recorded at -// the `notifyInstancePoisoned` seam) gives A the spec-shaped outcome instead: -// DROPPED for a stream, the #84 abandonment trap for an unwritten future. -// CM#705 (polyengine#173) has since deleted `may_enter` outright, so the -// marker is not merely the better predicate but the only one available. +// reaches A's parked side. A must NOT be read as a corpse merely because it +// is mid-call: any liveness proxy would retire it in silence — stranded, the +// outcome #66 exists to prevent. The health test is task/scheduler.ts's +// per-instance poison marker (recorded at the `notifyInstancePoisoned` +// seam), which gives A the spec-shaped outcome instead: DROPPED for a +// stream, the #84 abandonment trap for an unwritten future. // // Both directions are pinned here: the dead-guest discipline (a parked task of // the POISONED instance itself is still silently retired) has its own leg @@ -588,11 +586,9 @@ function inTask(inst: ComponentInstanceState, fn: () => T): T { } /** - * `caller` is mid-cross-component-call into `callee`. Post-CM#705 that state - * carries no instance-level flag at all (the pre-#705 host-entry chain, whose - * cleared `may_enter` on BOTH instances is what the old health test tripped - * over, no longer exists), so this is documentation: neither instance is - * marked, and only the marker decides. + * `caller` is mid-cross-component-call into `callee`. That state carries no + * instance-level flag at all (CM#705), so this is documentation: neither + * instance is marked, and only the marker decides. */ function enterMidFactCall( caller: ComponentInstanceState, diff --git a/runtime/tests/task_test.ts b/runtime/tests/task_test.ts index 261d3e2..348a2f6 100644 --- a/runtime/tests/task_test.ts +++ b/runtime/tests/task_test.ts @@ -195,24 +195,18 @@ Deno.test("canon_lift sync loop: traps when no thread can make progress", () => }); // --------------------------------------------------------------------------- -// Post-CM#705 entry semantics (polyengine#173) +// Entry semantics (CM#705) // --------------------------------------------------------------------------- // -// CM#705 (definitions.py @ 2f13265) removed `may_enter`, `entering_set`, -// `enter_from`, `leave_to` and `ComponentInstance.parent` outright; -// polyengine#173 removed every call to them and then the definitions -// themselves, along with the per-instantiation root the plan used to stand -// in for the instance tree. Nothing gates entry any more except -// polyengine's own per-instance poison marker (pinned further below). -// -// What follows pins the MERGED semantics: the shapes that the deleted model -// used to forbid, and that must now proceed. +// At the pinned reference (definitions.py @ 2f13265) there is no `may_enter`, +// `entering_set`, `enter_from`, `leave_to` or `ComponentInstance.parent`: +// nothing gates entry except polyengine's own per-instance poison marker +// (pinned further below). What follows pins those entry semantics. Deno.test("cm705: tick resumes a ready sibling thread during a live host entry", () => { - // INVERTED by polyengine#173 (CM#705). This shape used to be the #155 - // regression: `tick` filtered its candidates on host-enterability, and - // under that model's shared per-instantiation root a host entry into A made - // every sibling non-enterable, so B could not run until A's call returned. + // `tick` filters candidates on nothing but poisoning (CM#705), so a host + // entry into A does not hold B: B's ready thread resumes while A's call is + // still in flight. // // The merged reference (definitions.py @ 2f13265 `Store.tick`) resumes any // ready thread with no gate and no bracket, so B runs immediately. @@ -257,19 +251,12 @@ Deno.test("cm705: tick resumes a ready sibling thread during a live host entry", store.pendingHostCalls.delete(pendingImport); }); -// --- issue #156: settled activation tails, post-CM#705 --------------------- -// -// History: `Store.settled` tails are dispatched through `Thread.resumeWith`, -// which used to bracket the resumption with a host entry. Under the shared -// per-instantiation root a host entry into ANY instance locked every sibling, -// so dispatching a sibling's tail in that window tripped `resumeWith`'s -// enterability assert (and, mutating before asserting, stranded the thread -// and lost the settle); #156 deferred such tails IN PLACE. +// --- settled activation tails (issue #156) --------------------------------- // -// INVERTED by polyengine#173: there is no bracket and nothing is ever -// non-enterable, so every non-stale tail dispatches immediately. What still -// holds — and is pinned below — is the settle-order discipline (a -// serviceable tail gates `tick`) and the poisoned-tail retirement (#66). +// `Store.settled` tails are dispatched through `Thread.resumeWith` with no +// bracket and no enterability condition (CM#705), so every non-stale tail +// dispatches immediately. What is pinned below is the settle-order discipline +// (a serviceable tail gates `tick`) and the poisoned-tail retirement (#66). /** Settle a park promise and let `noteAwaiting`'s eager continuation run. */ async function queueSettledTail(settle: () => void): Promise { @@ -305,8 +292,8 @@ Deno.test("cm705: serviceSettled dispatches a sibling tail immediately", async ( await queueSettledTail(settle); assertEq(store.settled.length, 1, "the tail is queued"); - // A live host entry into the sibling used to defer this tail (#156). Post - // CM#705 nothing defers: the tail dispatches on the spot. + // A live host entry into the sibling defers nothing (CM#705): the tail + // dispatches on the spot. void a; assertEq(store.serviceSettled(), true, "dispatched, not deferred"); assertEq(order.join(","), "b tail ran"); @@ -317,8 +304,7 @@ Deno.test("cm705: serviceSettled dispatches a sibling tail immediately", async ( Deno.test("cm705: the phantom-state gate holds for a serviceable tail", async () => { // An unserviced tail refuses tick, preserving the reference's atomic-resume - // discipline. (Pre-#173 the gate relaxed for reentrance-deferred tails; - // there are none now, so the gate is simply "the queue is non-empty".) + // discipline. The gate is simply "the queue is non-empty". const store = new Store(); const a = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); @@ -366,8 +352,8 @@ Deno.test("cm705: the phantom-state gate holds for a serviceable tail", async () Deno.test("cm705: a poisoned instance's tail retires without running", async () => { // `resumeWith`'s poison early-return retires the tail: the queue drains and - // the body does NOT run. (#66 / #156; unchanged by CM#705 — a corpse's - // parked segments must never resume.) + // the body does NOT run (#66 / #156): a corpse's parked segments must + // never resume. const store = new Store(); const a = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); @@ -433,9 +419,8 @@ Deno.test("cm705: trap poisoning stays per-instance (named divergence)", () => { const a = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); // polyengine buries only the instance that trapped, where wasmtime kills - // the whole store (exec/boundary.ts `poison`). Since polyengine#173 the - // marker is the entire mechanism, so "per-instance" is a property of the - // marker alone. + // the whole store (exec/boundary.ts `poison`). The marker is the entire + // mechanism, so "per-instance" is a property of the marker alone. notifyInstancePoisoned(a, new Trap("boom")); assertEq(entryRefusal(a, null, "base") !== null, true, "A is a corpse"); assertEq(entryRefusal(b, null, "base"), null, "a sibling is still enterable"); @@ -752,10 +737,9 @@ Deno.test("cancellation: with no cancellable thread it becomes pending", () => { Deno.test("tick: a trap under tick records the poison marker", async () => { // A trap escaping `thread.resume()` under `Store.tick` poisons the - // instance. Post-CM#705 there is no bracket to break, so recording the + // instance. There is no bracket to break (CM#705), so recording the // MARKER is the entire act — and it is what `Thread.resumeWith`'s - // quiet-retire and `entryRefusal` both read - // (polyengine#145, #156, #251). + // quiet-retire and `entryRefusal` both read (polyengine#145, #156). const store = new Store(); const b = new ComponentInstanceState(0, store); @@ -862,14 +846,13 @@ Deno.test("request_cancellation: a capability signal does not poison", () => { }); // --------------------------------------------------------------------------- -// Poisoning re-key (polyengine#173) +// Poisoning is the refusal mechanism // --------------------------------------------------------------------------- // -// White-box pins on the property the re-key bought and CM#705 then made -// unavoidable: every entry-refusal DECISION reads the poison MARKER, which is -// now the only refusal mechanism there is. Nothing locks an instance any -// more, so these tests need no reentrance-state manipulation — an unmarked -// instance is always enterable, by construction. +// White-box pins: every entry-refusal DECISION reads the poison MARKER, the +// only refusal mechanism there is (CM#705). Nothing else locks an instance, +// so these tests need no reentrance-state manipulation — an unmarked instance +// is always enterable, by construction. Deno.test("re-key: entryRefusal refuses a marked instance", () => { const store = new Store(); @@ -884,8 +867,7 @@ Deno.test("re-key: entryRefusal refuses a marked instance", () => { }); Deno.test("re-key: an unmarked instance is never refused (CM#705)", () => { - // The clause that returned the bare base for a transiently-locked instance - // is DELETED with polyengine#173: entry into a live instance is valid. + // Entry into a live instance is valid (CM#705). const store = new Store(); const inst = new ComponentInstanceState(0, store); assertEq(entryRefusal(inst, null, "base"), null); @@ -894,8 +876,7 @@ Deno.test("re-key: an unmarked instance is never refused (CM#705)", () => { }); Deno.test("re-key: caller === callee passes vacuously even when marked", () => { - // The pre-#705 `entering_set` was empty for a self-call, so there was no - // instance to check; `entryRefusal` keeps that vacuous pass. The dtor path + // `entryRefusal` passes a self-call vacuously. The dtor path // (cabi/handles.ts) relies on it: a guest dropping its own resource is not // refused by its own marker. const store = new Store(); diff --git a/tools/browser/gen-overlay.ts b/tools/browser/gen-overlay.ts deleted file mode 100644 index f0c5018..0000000 --- a/tools/browser/gen-overlay.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Generates a lane-expectation overlay skeleton from a raw results JSON -// captured with `run-lane.ts --json `. -// -// deno run -A tools/browser/gen-overlay.ts /tmp/chromium.json > /tmp/o.ts -// -// It emits one `expected-fail` entry per command that failed on the browser -// lane and is NOT an xfail on the Deno lane, with a reason drawn from the -// classifier below. The output is a STARTING POINT: every reason must be read -// and, where the classifier guessed, replaced by a real triage note. Nothing -// here is applied automatically — the overlay files are checked in by hand. - -import { isXfail } from "../../harness/src/xfail.ts"; - -/** Failure-signature -> root-cause label. Order matters (first match wins). */ -const CLASSES: [RegExp, string][] = [ - // ---- engine variance (the reason the stretch lanes exist) -------------- - [ - /there can at most be one Memory section|Memory section has more than one memory/, - "ENGINE (JavaScriptCore): multi-memory is not implemented — JSC rejects the core module at compile time", - ], - [ - /expected trap "wasm trap: wasm `unreachable` instruction executed", got/, - "ENGINE: this engine words the unreachable trap differently; the suite's assert_trap text is de facto wasmtime/V8 wording (docs/architecture.md \u00a71)", - ], - [ - /exit-sync-call with an empty sync-call stack|transfer-borrow outside an enter-sync-call/, - "M3A-1 (no AsyncLocalStorage in browsers): the FACT sync-call bracket lost its activation ambient across an await", - ], - [ - /a resumed-activation claim was never released/, - "M3A-1: `consumeClaimIfRunning` never fires because the shimmed ALS reports no store outside a synchronous extent", - ], - [ - /two activations claim the resumed ambient at once/, - "M3A-1: the one-claimant assert trips because the claim could not be consumed without an ALS store", - ], - [ - /task\.return from a non-async task/, - "M3A-1: `resolveAmbient` fell through to the wrong tier and named a non-async task, so `task.return` rejected a legitimate call", - ], - [ - /instantiation-time task context/, - "M3A-1 cascade: a built-in ran with no resolvable ambient and was classified pending-capability", - ], - [ - /reentrance forbidden|no current instance|no definition named|table entry empty|wasm `unreachable`|Converting circular structure/, - "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", - ], -]; - -function reasonFor(detail: string): string { - for (const [re, why] of CLASSES) if (re.test(detail)) return why; - return "UNTRIAGED — classify this before checking the overlay in"; -} - -const path = Deno.args[0]; -if (!path) { - console.error("usage: gen-overlay.ts "); - Deno.exit(2); -} -const raw = JSON.parse(await Deno.readTextFile(path)); -const lane: string = raw.lane; - -const lines: string[] = []; -let untriaged = 0; -for (const f of raw.files) { - for (const r of f.results) { - if (r.status !== "failed") continue; - if (isXfail(f.path, r.line)) continue; - const reason = reasonFor(String(r.detail ?? "")); - if (reason.startsWith("UNTRIAGED")) untriaged++; - lines.push( - ` { file: ${JSON.stringify(f.path)}, line: ${r.line}, ` + - `kind: "expected-fail", reason: ${JSON.stringify(reason)} },`, - ); - } -} -console.error( - `[gen-overlay] ${lane}: ${lines.length} deltas, ${untriaged} untriaged`, -); -console.log(lines.join("\n")); diff --git a/tools/browser/run-lane.ts b/tools/browser/run-lane.ts index 953edd3..b887ca7 100644 --- a/tools/browser/run-lane.ts +++ b/tools/browser/run-lane.ts @@ -14,9 +14,9 @@ // // Then, from the repo root: // -// deno task -c harness/deno.json browser:chromium # required lane -// deno task -c harness/deno.json browser:firefox # findings lane -// deno task -c harness/deno.json browser:webkit # findings lane +// just browser-lane chromium # required lane +// just browser-lane firefox # findings lane +// just browser-lane webkit # findings lane // // or directly: // diff --git a/tools/npm-build/consumer/check.mjs b/tools/npm-build/consumer/check.mjs index 49f06eb..e81a8d3 100644 --- a/tools/npm-build/consumer/check.mjs +++ b/tools/npm-build/consumer/check.mjs @@ -104,7 +104,6 @@ for ( "@polyengine/runtime/cache", "@polyengine/wasi/clocks", "@polyengine/wasi/filesystem-node", - "@polyengine/ct-runner/run", ] ) { const mod = await import(subpath); diff --git a/tools/smoke-c0/deno.json b/tools/smoke-c0/deno.json index 2fdd54c..ccfc5f4 100644 --- a/tools/smoke-c0/deno.json +++ b/tools/smoke-c0/deno.json @@ -9,7 +9,6 @@ "leg1": "deno run --allow-read --allow-env=POLYMORPH_ROOT,WOSH_ROOT leg1_tdz.ts", "leg2": "deno run --allow-read --allow-env=POLYMORPH_ROOT,WOSH_ROOT leg2_exec_model.ts", "leg3": "deno run --allow-read --allow-env=POLYMORPH_ROOT,WOSH_ROOT leg3_throughput.ts", - "leg4": "deno run --allow-read --allow-env=POLYMORPH_ROOT,WOSH_ROOT leg4_websocket.ts", - "repro": "deno run --allow-read --allow-env=POLYMORPH_ROOT,WOSH_ROOT repro_stream_pump.ts" + "leg4": "deno run --allow-read --allow-env=POLYMORPH_ROOT,WOSH_ROOT leg4_websocket.ts" } } diff --git a/tools/smoke-c0/leg2_exec_model.ts b/tools/smoke-c0/leg2_exec_model.ts index 124514a..c2fbb68 100644 --- a/tools/smoke-c0/leg2_exec_model.ts +++ b/tools/smoke-c0/leg2_exec_model.ts @@ -309,7 +309,7 @@ if (pumpStarted) { // --------------------------------------------------------------------------- // Probe 4a — exported stream, read to completion. // -// FINDING R-1 (see repro_stream_pump.ts): the guest fills this stream from a +// FINDING R-1 (see runtime/tests/host_pump_test.ts): the guest fills this stream from a // DETACHED task that awaits `wait-for` between chunks. Once the export call // has returned there is no `driveAsync` loop, and nothing re-pumps the store // when a Promise-returning host import settles — so the host read stalls after diff --git a/tools/smoke-c0/repro_stream_pump.ts b/tools/smoke-c0/repro_stream_pump.ts deleted file mode 100644 index dcb845c..0000000 --- a/tools/smoke-c0/repro_stream_pump.ts +++ /dev/null @@ -1,194 +0,0 @@ -// Finding R-1 (host-pump starvation of pendingHostCalls) — minimal repro: a -// host-side read of a guest stream hangs -// when the guest's writer task is parked on a Promise-returning host import -// and no export call is in flight. -// -// deno run --allow-read repro_stream_pump.ts -// -// Shape (polymorph-iroh exec-model `open-stream`): the guest returns a -// `stream` immediately and fills it from a DETACHED task that awaits -// `wasi:clocks/monotonic-clock@0.3.0 wait-for` between chunks. The host then -// reads the stream between export calls. -// -// Observed: the first chunk arrives (it was already buffered when the export -// returned); the second read never resolves. -// -// Mechanism (runtime/src/exec/boundary.ts:1393-1416): a Promise-returning -// lowered import registers its promise in `store.pendingHostCalls` and, on -// settle, calls `onResolve` — which readies the guest thread but does NOT -// tick the store. Only `driveAsync` (runtime/src/exec/boundary.ts:614+) -// races `pendingHostCalls` and re-pumps, and `driveAsync` exists only for the -// duration of an export call. `HostBuffer.pump()` -// (runtime/src/exec/host_streams.ts:161-175) is the between-export-calls -// driver, but it only drains `store.awaiting` (the JSPI park set); it has no -// arm for `pendingHostCalls`. -// -// Control below: with ANY export call concurrently in flight, the same reads -// complete — which is the diagnosis, executable. -// -// Triage: RUNTIME BUG (not toolchain drift, not a missing shim). - -import { - ARTIFACTS, - loadTranslator, - ms, - readArtifact, - translateOnce, -} from "./common.ts"; -import { buildImports } from "./wasi_stub.ts"; -import { - hostStreamFor, - instantiateComponent, -} from "../../runtime/src/exec/mod.ts"; -import type { ComponentValue } from "../../runtime/src/cabi/types.ts"; - -const bytes = await readArtifact(ARTIFACTS.execModel); -const t = await loadTranslator(); -const { plan, adapters } = translateOnce(t, bytes) as { - plan: NonNullable["plan"]>; - adapters: Map; -}; - -const overrides: Record = { - "wasi:clocks/monotonic-clock/wait-for": (ns: unknown) => - new Promise((res) => - setTimeout(() => res(null), Math.max(0, Math.ceil(Number(ns as bigint) / 1e6))) - ), -}; - -async function newInstance() { - const { imports } = buildImports(plan, { overrides }); - const c = await instantiateComponent({ - plan, - componentBytes: bytes, - adapters, - imports, - }); - return c.exports[plan.exports[0].name] as Record; -} - -type AnyFn = (...a: unknown[]) => unknown; -const TIMEOUT_MS = 4000; -function withTimeout(p: Promise, label: string): Promise { - return Promise.race([ - p, - new Promise((_, rj) => - setTimeout(() => rj(new Error(`TIMEOUT ${TIMEOUT_MS}ms: ${label}`)), TIMEOUT_MS) - ), - ]); -} - -console.log("=== repro R-1: host stream read vs. detached writer on an async import ===\n"); - -// --- A. the bug ------------------------------------------------------------- -console.log("A. read the guest stream with NO export call in flight"); -{ - const probe = await newInstance(); - const returned = await (probe["open-stream"] as AnyFn)(5000, 1000); - const s = hostStreamFor(returned as ComponentValue); - let count = 0; - try { - for (let i = 0; i < 10; i++) { - const t0 = performance.now(); - const vs = await withTimeout(s.readable.read(4096), `read#${i}`); - console.log(` read#${i} -> ${vs.length} bytes in ${ms(performance.now() - t0)}`); - if (vs.length === 0) break; - count += vs.length; - } - console.log(` RESULT: read ${count}/5000 bytes — no hang (bug not reproduced)`); - } catch (e) { - console.log(` RESULT: HUNG after ${count}/5000 bytes — ${(e as Error).message}`); - console.log(` ^ finding R-1 reproduced`); - } -} - -// --- B. the control --------------------------------------------------------- -console.log( - "\nB. same reads, but with `stream-outcome()` concurrently in flight\n" + - " (it loops on wait-for, so a driveAsync loop exists the whole time)", -); -{ - const probe = await newInstance(); - const returned = await (probe["open-stream"] as AnyFn)(5000, 1000); - const s = hostStreamFor(returned as ComponentValue); - // Keep an export call alive; its driveAsync is the pump the host read lacks. - // Attach the rejection handler IMMEDIATELY: this call can reject before we - // await it (finding R-2), and an unhandled rejection aborts the process. - let keepAliveOutcome = "still pending"; - const keepAlive = ((probe["stream-outcome"] as AnyFn)() as Promise) - .then( - (v) => (keepAliveOutcome = `resolved ${JSON.stringify(v)}`), - (e) => (keepAliveOutcome = `REJECTED ${(e as Error).message}`), - ); - let count = 0; - try { - for (let i = 0; i < 10; i++) { - const t0 = performance.now(); - const vs = await withTimeout(s.readable.read(4096), `read#${i}`); - console.log(` read#${i} -> ${vs.length} bytes in ${ms(performance.now() - t0)}`); - if (vs.length === 0) break; - count += vs.length; - } - console.log(` RESULT: read ${count}/5000 bytes with a driver in flight`); - } catch (e) { - console.log(` RESULT: HUNG after ${count}/5000 bytes — ${(e as Error).message}`); - } - try { - await withTimeout(keepAlive, "stream-outcome"); - } catch (e) { - keepAliveOutcome = (e as Error).message; - } - console.log(` stream-outcome: ${keepAliveOutcome}`); -} - -// --- C. the discriminator --------------------------------------------------- -console.log( - "\nC. same reads, but `wait-for` returns SYNCHRONOUSLY (no Promise)\n" + - " — the guest writer then never parks on a host-import promise.", -); -{ - const { imports } = buildImports(plan, { - overrides: { "wasi:clocks/monotonic-clock/wait-for": () => null }, - }); - const c = await instantiateComponent({ - plan, - componentBytes: bytes, - adapters, - imports, - }); - const probe = c.exports[plan.exports[0].name] as Record; - const returned = await (probe["open-stream"] as AnyFn)(5000, 1000); - const s = hostStreamFor(returned as ComponentValue); - let count = 0; - try { - for (let i = 0; i < 10; i++) { - const vs = await withTimeout(s.readable.read(4096), `read#${i}`); - console.log(` read#${i} -> ${vs.length} bytes`); - if (vs.length === 0) break; - count += vs.length; - } - console.log( - ` RESULT: read ${count}/5000 bytes — ${ - count === 5000 - ? "COMPLETE. The async host-import park is the trigger for R-1." - : "still short." - }`, - ); - } catch (e) { - console.log(` RESULT: HUNG after ${count}/5000 — ${(e as Error).message}`); - } -} - -console.log( - "\nNOTE finding R-2: in run B the concurrent export call rejects with\n" + - " TypeError: Cannot read properties of undefined (reading 'awaiting')\n" + - " at HostActivity.#drainAsync (runtime/src/exec/host_streams.ts:198).\n" + - " `#drainAsync` checks `store.awaiting.size > 0`, then AWAITS\n" + - " (line 184 `await Promise.resolve()`), then takes `[...store.awaiting][0]`\n" + - " — but the set can be emptied while it is suspended, so the index is\n" + - " `undefined` and `t.awaiting` throws. Triage: RUNTIME BUG (a check-then-\n" + - " act race across an await, not a semantics question).", -); - -console.log("\nrepro done"); -Deno.exit(0); diff --git a/translator/deno.json b/translator/deno.json index 9aab189..5f83e1c 100644 --- a/translator/deno.json +++ b/translator/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/translator", - "version": "0.5.2", + "version": "0.6.0", "exports": { ".": "./mod.ts" }, diff --git a/upstream-component-model-repo-findings.md b/upstream-component-model-repo-findings.md index e40af35..ffacf23 100644 --- a/upstream-component-model-repo-findings.md +++ b/upstream-component-model-repo-findings.md @@ -219,13 +219,13 @@ In `cancel_copy`, when the pending event is a stream `COMPLETED`, deliver ## CM-4: `sync-streams.wast:145` overfits wasmtime's scheduler — entry-status timing is not normative **Status:** ADJUDICATED (operator, 2026-08-10) — upstream **test defect**, -not a reference-semantics issue. **Filing kit READY** (2026-08-11, closes -[polyengine#43](https://github.com/polymorph-components/polyengine/issues/43)): -`upstream-issue-sync-streams-schedule-overfit.md` (ready-to-file draft) + -`upstream-sync-streams-schedule-agnostic.patch` (applies at the spec repo -root, verified against 73b7ad5; both arms exercised green through the -polyengine pipeline, FIFO + seeds — see the kit PR for the recipe). Filing -itself tracked by +not a reference-semantics issue. **Filing kit DROPPED as stale** (verified +only against 73b7ad5; the submodule pin has since advanced to 2f13265 +(CM#705), which rewrote the targeted assertion — `upstream-issue- +sync-streams-schedule-overfit.md` and `upstream-sync-streams-schedule- +agnostic.patch` were removed from the repo root; re-derive the patch against +the current pin before filing). Filing +itself remains tracked by [polyengine#15](https://github.com/polymorph-components/polyengine/issues/15). Archived evidence tree (mechanism docs, both experiment patches, trace, verify script): `4f3351f:exams/wasmtime-exclusivity/`. diff --git a/upstream-issue-sync-streams-schedule-overfit.md b/upstream-issue-sync-streams-schedule-overfit.md deleted file mode 100644 index 3d541ac..0000000 --- a/upstream-issue-sync-streams-schedule-overfit.md +++ /dev/null @@ -1,112 +0,0 @@ -# Upstream issue draft: sync-streams.wast:145 asserts scheduler policy, not semantics - -Target repo: WebAssembly/component-model -Status: not yet filed (filing tracked by polyengine#15; adjudication record in -polyengine#43) -Companion artifact: `upstream-sync-streams-schedule-agnostic.patch` (applies -at the spec repo root, verified against 73b7ad5) - ---- - -Title: **test/async/sync-streams.wast:145 pins one scheduler policy: STARTED vs STARTING for entry into a gated instance is not normative** - -`test/async/sync-streams.wast:145` (at 73b7ad5) hard-asserts that an -async-lowered call to `$C.set` reports STARTED in its packed result: - -```wat -(local.set $ret (call $set (local.get $rx))) -(if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) - (then unreachable)) -``` - -At that instant `$C`'s exclusivity gate is held by the previous task: -`$C.get` has resolved (its `task.return` was delivered at line 32) but is -parked mid-frame in a synchronous `stream.write` (line 38) — and is ready to -resume, since `$D.run` already completed the first rendezvous. Whether the -new call reports STARTING or STARTED is then decided by *when* the host -evaluates the callee's admission, and the spec's two reference points -disagree: - -- **canonical-abi/definitions.py decides eagerly, at the call instant**: - `canon_lower` invokes the callee synchronously (definitions.py:2281), - `canon_lift` creates and resumes the callee thread on the spot - (:2211–2212), the thread parks at the still-held gate (:484–486), and the - caller packs whatever state the subtask reached (:2306). Answer: - **STARTING** — deterministically, under every schedule the deterministic - profile can produce. The reference fails its own test suite's assertion. - -- **wasmtime defers the decision**: `start_call` queues the callee and - suspends the caller until the first status event; ready work queued ahead - (the parked-but-ready `$C.get`) runs to invocation exit and releases the - gate before the new call's readiness is evaluated, so the callee is - admitted and the caller learns **STARTED** (deterministic under wasmtime's - FIFO; line refs and an execution trace against wasmtime main and v47.0.3 - are available on request). - -The gate *semantics* are not in question — definitions.py's -`exclusive_thread` (released only at the event-loop wait :2187–2188 and task -exit :506–508), wasmtime's `do_not_enter` bracketing, and the -CanonicalABI.md:3740–3746 prose all agree the gate spans the whole core -invocation, mid-frame parks included. What differs is scheduler policy on -top of agreed semantics, and both policies are conforming: the spec -deliberately leaves task scheduling nondeterministic. A hard STARTED -assertion therefore pins the co-developed runner's policy (the wast corpus -documents wasmtime as its runner), while the reference interpreter itself -answers STARTING — and the contradiction has no detector today because CI -runs `run_tests.py` but never executes the wast corpus against -definitions.py. - -## Proposed fix - -Make the region schedule-agnostic; the test's real content — the sibling -call is eventually admitted once the gate-holder exits, and the stream -rendezvous completes correctly — is preserved under both policies: - -1. accept STARTING **or** STARTED from the lower; -2. on STARTING, `waitable-set.wait` for the subtask's admission (STARTED) - before touching the new stream — RETURNED cannot arrive first, because - `$C.set` reads from a stream nothing has written to yet; -3. leave every other assertion in the file unchanged. - -Patch attached (also happy to open it as a PR). Under a deferred-entry host -the test takes the STARTED arm, whose assertions are byte-identical to -today's — no coverage is lost on the current runner. Under an eager-entry -host (the reference's policy) the STARTING arm finally gives this scenario a -green path. We verified both arms on a Component Model runtime that -implements hold-lifetime gates with a drain-to-quiescence entry decision: -the patched test passes as written (STARTED arm), and a reordered variant -that makes the gate-holder unready at the call site — forcing the STARTING -answer — passes the STARTING arm's waits and asserts, including under -seeded-shuffle scheduling. - -## Secondary, structural - -Reference↔corpus contradictions of this class stay invisible until an -independent implementation trips over them. Running the wast corpus against -definitions.py in CI (or, short of that, flagging assertions known to encode -runner policy) would give them a detector. - ---- - -Filing notes (not part of the issue body): - -- Adjudication record: polyengine#43 (final operator comment, 2026-08-10); - tracker entry CM-4 in `upstream-component-model-repo-findings.md`. Same - class as NOTE-1 (tests assuming the deterministic profile), sharper - instance. -- Archived evidence tree (mechanism analysis with wasmtime line refs for - both vintages, `trace-sync-streams-wasmtime-dev.log`, `verify-cm4.sh` - whose legs 0–1 reproduce the reference-side STARTING answer against - pristine definitions.py): `4f3351f:exams/wasmtime-exclusivity/`. -- Patch verification (2026-08-11, this repo at the 73b7ad5 submodule pin): - pristine/patched/STARTING-variant legs all 2/2 through testgen + - RuntimeExecutor, FIFO and POLYENGINE_SCHED_SEED=1/4242; the variant - hard-asserts STARTING so its pass proves the new arm executed. Recipe: see - the PR that added this file. -- wasmtime-side check of the patched test needs a **dev** CLI (`wasmtime - wast -W component-model-async=y -W component-model-more-async-builtins=y - test/async/sync-streams.wast`); 47-era release CLIs cannot parse the - post-#655 suite syntax. By construction the patch only adds an arm - wasmtime never takes, but running it pre-filing is a reasonable courtesy. -- Keep the polyengine-specific framing out of the filed text: the issue body - above names no polyengine internals beyond "a Component Model runtime". diff --git a/upstream-sync-streams-schedule-agnostic.patch b/upstream-sync-streams-schedule-agnostic.patch deleted file mode 100644 index bf0207a..0000000 --- a/upstream-sync-streams-schedule-agnostic.patch +++ /dev/null @@ -1,53 +0,0 @@ -diff --git a/test/async/sync-streams.wast b/test/async/sync-streams.wast -index 7d31a10..83c7c6b 100644 ---- a/test/async/sync-streams.wast -+++ b/test/async/sync-streams.wast -@@ -137,14 +137,36 @@ - (call $stream.drop-readable (local.get $rx)) - - ;; ($rx, $tx) = stream.new -- ;; $C.set($rx) blocks on stream.read, so the async call returns STARTED -+ ;; $C.set($rx) blocks on stream.read. $C's exclusivity gate is still -+ ;; held by the resolved-but-parked $C.get task, so whether this call -+ ;; reports STARTING or STARTED is host scheduler policy, not -+ ;; semantics: a host may decide entry eagerly at the call instant -+ ;; (STARTING) or defer the decision until runnable work queued ahead -+ ;; of the call has drained (STARTED). Accept both; on STARTING, wait -+ ;; for the subtask to be admitted (STARTED) before touching the -+ ;; stream. - (local.set $ret64 (call $stream.new)) - (local.set $rx (i32.wrap_i64 (local.get $ret64))) - (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - (local.set $ret (call $set (local.get $rx))) -- (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) -- (then unreachable)) - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) -+ (local.set $ws (call $waitable-set.new)) -+ (call $waitable.join (local.get $subtask) (local.get $ws)) -+ (if (i32.eq (i32.const 0 (; STARTING ;)) (i32.and (local.get $ret) (i32.const 0xf))) -+ (then -+ ;; admitted only after the parked $C.get task exits: wait for -+ ;; STARTED. RETURNED cannot arrive first: $C.set reads from a -+ ;; stream nothing has written to yet. -+ (local.set $ret (call $waitable-set.wait (local.get $ws) (i32.const 0))) -+ (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $ret)) -+ (then unreachable)) -+ (if (i32.ne (local.get $subtask) (i32.load (i32.const 0))) -+ (then unreachable)) -+ (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.load (i32.const 4))) -+ (then unreachable))) -+ (else -+ (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) -+ (then unreachable)))) - - ;; (stream.write $tx $bufp 4) will succeed without blocking - (local.set $bufp (i32.const 16)) -@@ -156,8 +178,6 @@ - (call $stream.drop-writable (local.get $tx)) - - ;; wait for $C.set to finish, which implies all its checks passed -- (local.set $ws (call $waitable-set.new)) -- (call $waitable.join (local.get $subtask) (local.get $ws)) - (local.set $ret (call $waitable-set.wait (local.get $ws) (i32.const 0))) - (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $ret)) - (then unreachable)) diff --git a/wasi/deno.json b/wasi/deno.json index 7fcbdfe..4ca2c14 100644 --- a/wasi/deno.json +++ b/wasi/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/wasi", - "version": "0.5.2", + "version": "0.6.0", "exports": { ".": "./src/mod.ts", "./cli": "./src/cli.ts", diff --git a/wasi/src/cli.ts b/wasi/src/cli.ts index 77fe57e..1719cf5 100644 --- a/wasi/src/cli.ts +++ b/wasi/src/cli.ts @@ -46,8 +46,6 @@ export interface CliOptions { cwd?: string; /** `get-stdin`'s buffer contents; default empty (matches contract: "stdin (empty)"). */ stdinBuffer?: Uint8Array; - /** Also `console.log`/`console.error` captured stdout/stderr writes. Default false. */ - passthrough?: boolean; /** `exit()` throws `ExitError` instead of merely recording. Default false. */ throwOnExit?: boolean; } @@ -96,18 +94,15 @@ function concat(chunks: Uint8Array[]): Uint8Array { export function cli(options: CliOptions = {}): CliResult { const stdoutChunks: Uint8Array[] = []; const stderrChunks: Uint8Array[] = []; - const passthrough = options.passthrough ?? false; let exited = false; let exitOk: boolean | undefined; let exitCode: number | undefined; const stdout = new OutputStream((chunk) => { stdoutChunks.push(chunk); - if (passthrough) console.log(new TextDecoder().decode(chunk)); }); const stderr = new OutputStream((chunk) => { stderrChunks.push(chunk); - if (passthrough) console.error(new TextDecoder().decode(chunk)); }); const captured: CliCaptured = { @@ -123,13 +118,11 @@ export function cli(options: CliOptions = {}): CliResult { /** 0.3 write-via-stream into a capture buffer (the promise IS the future — embedder-api.md §"Streams and futures"). */ const captureViaStream = ( chunks: Uint8Array[], - mirror: ((text: string) => void) | undefined, ) => async (data: CliByteSource): Promise => { for await (const chunk of data as AsyncIterable) { const bytes = chunk instanceof Uint8Array ? chunk : Uint8Array.from(chunk); chunks.push(bytes); - mirror?.(new TextDecoder().decode(bytes)); } return { kind: "ok" }; }; @@ -195,16 +188,10 @@ export function cli(options: CliOptions = {}): CliResult { ], }, "wasi:cli/stdout@0.3": { - writeViaStream: captureViaStream( - stdoutChunks, - passthrough ? (t) => console.log(t) : undefined, - ), + writeViaStream: captureViaStream(stdoutChunks), }, "wasi:cli/stderr@0.3": { - writeViaStream: captureViaStream( - stderrChunks, - passthrough ? (t) => console.error(t) : undefined, - ), + writeViaStream: captureViaStream(stderrChunks), }, "wasi:cli/terminal-input@0.3": { TerminalInput }, "wasi:cli/terminal-output@0.3": { TerminalOutput }, diff --git a/wasi/src/http.ts b/wasi/src/http.ts index 97f1410..a9a9592 100644 --- a/wasi/src/http.ts +++ b/wasi/src/http.ts @@ -23,11 +23,11 @@ // VERSION KEYS: 0.3.x releases fold onto the `@0.3` compatibility track // (contracts/embedder-api.md §"Version canonicalization"), so the // default registration serves every released 0.3.x with one provider — -// the same flagship track-key pattern as the rest of this package. The -// pre-consolidation rc SNAPSHOTS (`0.3.0-rc-*`) are prereleases, which -// resolve exact-only: a guest pinned to one names it via -// `http({ version: "0.3.0-rc-..." })`, which re-keys the fragment at -// that exact id instead. +// the same flagship track-key pattern as the rest of this package. A +// guest pinned to a pre-consolidation rc SNAPSHOT (`0.3.0-rc-*`, which +// resolves exact-only) is out of scope for this fragment; an embedder +// serving one re-keys the `imports` object it gets back from `http()` +// manually (documented escape, unaffected by this fragment's surface). // // Body/trailers plumbing is the same stream+future choreography the TCP // provider proved: constructors return `[resource, transmission-future]` @@ -212,13 +212,6 @@ const encoder = new TextEncoder(); const decoder = new TextDecoder(); export interface HttpOptions { - /** - * Override the registration keys for a guest pinned to a PRERELEASE - * snapshot (`0.3.0-rc-*`), which the resolver matches exactly — no - * track exists for prereleases. Default: the `@0.3` track, serving - * every released 0.3.x. - */ - version?: string; /** Observe every entry point the guest reaches (see sockets' onCall). */ onCall?: (call: string) => void; /** @@ -375,7 +368,6 @@ export interface ResponseClass { */ export function http(options: HttpOptions = {}): HttpFragment { const onCall = options.onCall ?? ((): void => {}); - const v = options.version ?? HTTP_TRACK; const allowRequest = options.allowRequest; // --- fields ----------------------------------------------------------------- @@ -1040,8 +1032,8 @@ export function http(options: HttpOptions = {}): HttpFragment { return { imports: { - [`wasi:http/types@${v}`]: { Fields, Request, RequestOptions, Response }, - [`wasi:http/client@${v}`]: { send }, + "wasi:http/types@0.3": { Fields, Request, RequestOptions, Response }, + "wasi:http/client@0.3": { send }, }, Fields: Fields as unknown as FieldsClass, Request: Request as unknown as RequestClass, diff --git a/wasi/src/internal/sockets_02.ts b/wasi/src/internal/sockets_02.ts index 4247e82..6cbf52e 100644 --- a/wasi/src/internal/sockets_02.ts +++ b/wasi/src/internal/sockets_02.ts @@ -452,9 +452,7 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { } let conn: TcpConn | undefined; try { - conn = state.listener.tryAccept === undefined - ? undefined - : state.listener.tryAccept(); + conn = state.listener.tryAccept(); } catch (e) { raise02(e, "tcp-socket.accept"); } @@ -600,8 +598,8 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { if (this.#state === "listening" && listen !== undefined) { const l = listen.listener; return new Pollable( - () => l.acceptReady === undefined ? true : l.acceptReady(), - () => l.waitAccept === undefined ? Promise.resolve() : l.waitAccept(), + () => l.acceptReady(), + () => l.waitAccept(), ); } return new Pollable(); // no pending operation: ready @@ -657,9 +655,6 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { #applyKeepAlive(): void { const conn = this.#conn; if (this.#state !== "connected" || conn === undefined) return; - if (conn.setKeepAlive === undefined) { - throw err02("not-supported", "tcp-socket: no keep-alive control on this backend"); - } try { conn.setKeepAlive(this.#keepAliveEnabled, Number(this.#keepAliveIdleNs / 1_000_000n)); } catch (e) { @@ -787,7 +782,7 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { this.#streams = streams; // OS-level (dis)connect, fire-and-forget: failures surface on the // first datagram op (doc comment above). - if (remoteAddress !== undefined && conn.connect !== undefined) { + if (remoteAddress !== undefined) { void conn.connect({ transport: "udp", hostname: ipHostname(remoteAddress), @@ -797,7 +792,7 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { }); } else if (remoteAddress === undefined && wasConnected) { try { - conn.disconnect?.(); + conn.disconnect(); } catch { // Not connected at the OS level (compat backends). } @@ -902,9 +897,9 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { const conn = this.#conn; if (conn === undefined) return; try { - if (this.#hopLimit !== undefined) conn.setTtl?.(this.#hopLimit); - if (this.#recvBuffer !== undefined) conn.setRecvBufferSize?.(Number(this.#recvBuffer)); - if (this.#sendBuffer !== undefined) conn.setSendBufferSize?.(Number(this.#sendBuffer)); + if (this.#hopLimit !== undefined) conn.setTtl(this.#hopLimit); + if (this.#recvBuffer !== undefined) conn.setRecvBufferSize(Number(this.#recvBuffer)); + if (this.#sendBuffer !== undefined) conn.setSendBufferSize(Number(this.#sendBuffer)); } catch (e) { raise02(e, "udp-socket (applying cached options)"); } @@ -947,7 +942,7 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { while (out.length < max) { let item: [Uint8Array, NetAddr] | undefined; try { - item = this.#conn.tryReceive === undefined ? undefined : this.#conn.tryReceive(); + item = this.#conn.tryReceive(); } catch (e) { raise02(e, "incoming-datagram-stream.receive"); } @@ -965,8 +960,8 @@ export function sockets02(onCall: (call: string) => void): Sockets02Fragment { onCall("incoming-datagram-stream.subscribe"); const conn = this.#conn; return new Pollable( - () => conn.receiveReady === undefined ? true : conn.receiveReady(), - () => conn.waitReceive === undefined ? Promise.resolve() : conn.waitReceive(), + () => conn.receiveReady(), + () => conn.waitReceive(), ); } diff --git a/wasi/src/internal/sockets_03.ts b/wasi/src/internal/sockets_03.ts index cff5d76..3a4ffb8 100644 --- a/wasi/src/internal/sockets_03.ts +++ b/wasi/src/internal/sockets_03.ts @@ -156,12 +156,6 @@ export function sockets03(onCall: (call: string) => void): { } this.#applyCachedOptions(); } - if (this.#conn.connect === undefined) { - throw componentError( - { kind: "not-supported" }, - "udp-socket.connect: this host's datagram backend has no connected mode", - ); - } try { await this.#conn.connect({ transport: "udp", @@ -183,7 +177,7 @@ export function sockets03(onCall: (call: string) => void): { ); } try { - this.#conn.disconnect?.(); + this.#conn.disconnect(); } catch (e) { throw mapPlatformError(e, "udp-socket.disconnect"); } @@ -422,12 +416,12 @@ export function sockets03(onCall: (call: string) => void): { const conn = this.#conn; if (conn === undefined) return; try { - if (this.#hopLimit !== undefined) conn.setTtl?.(this.#hopLimit); + if (this.#hopLimit !== undefined) conn.setTtl(this.#hopLimit); if (this.#recvBuffer !== undefined) { - conn.setRecvBufferSize?.(Number(this.#recvBuffer)); + conn.setRecvBufferSize(Number(this.#recvBuffer)); } if (this.#sendBuffer !== undefined) { - conn.setSendBufferSize?.(Number(this.#sendBuffer)); + conn.setSendBufferSize(Number(this.#sendBuffer)); } } catch (e) { throw mapPlatformError(e, "udp-socket (applying cached options)"); @@ -968,12 +962,6 @@ export function sockets03(onCall: (call: string) => void): { #applyKeepAlive(): void { const conn = this.#conn; if (this.#state !== "connected" || conn === undefined) return; - if (conn.setKeepAlive === undefined) { - throw componentError( - { kind: "not-supported" }, - "tcp-socket: this host's TCP backend has no keep-alive control", - ); - } try { conn.setKeepAlive(this.#keepAliveEnabled, Number(this.#keepAliveIdleNs / 1_000_000n)); } catch (e) { diff --git a/wasi/src/internal/sockets_platform.ts b/wasi/src/internal/sockets_platform.ts index ad4f0eb..f4faa93 100644 --- a/wasi/src/internal/sockets_platform.ts +++ b/wasi/src/internal/sockets_platform.ts @@ -91,25 +91,23 @@ export interface DatagramConn { send(p: Uint8Array, addr?: NetAddr): Promise; receive(): Promise<[Uint8Array, NetAddr]>; close(): void; - /** OS-level connected mode (kernel filters + default destination). - * Optional capability: absent = the provider answers `not-supported`. */ - connect?(addr: NetAddr): Promise; - disconnect?(): void; - /** IP_TTL / IPV6_UNICAST_HOPS. Optional capability. */ - setTtl?(ttl: number): void; - /** SO_RCVBUF / SO_SNDBUF. Optional capabilities. */ - getRecvBufferSize?(): number; - setRecvBufferSize?(size: number): void; - getSendBufferSize?(): number; - setSendBufferSize?(size: number): void; + /** OS-level connected mode (kernel filters + default destination). */ + connect(addr: NetAddr): Promise; + disconnect(): void; + /** IP_TTL / IPV6_UNICAST_HOPS. */ + setTtl(ttl: number): void; + /** SO_RCVBUF / SO_SNDBUF. */ + getRecvBufferSize(): number; + setRecvBufferSize(size: number): void; + getSendBufferSize(): number; + setSendBufferSize(size: number): void; /** Non-blocking queue access + readiness (the 0.2 datagram streams: - * poll-shaped receive instead of the promise-shaped one above). - * Optional capabilities. */ - tryReceive?(): [Uint8Array, NetAddr] | undefined; - receiveReady?(): boolean; + * poll-shaped receive instead of the promise-shaped one above). */ + tryReceive(): [Uint8Array, NetAddr] | undefined; + receiveReady(): boolean; /** The CURRENT epoch's wake promise (promise-swap: settles when a * datagram arrives, the socket errors, or it closes; re-armed per event). */ - waitReceive?(): Promise; + waitReceive(): Promise; } export type ListenDatagram = (options: { @@ -127,9 +125,8 @@ export interface TcpConn { write(p: Uint8Array): Promise; closeWrite(): Promise; close(): void; - /** SO_KEEPALIVE + TCP_KEEPIDLE (node exposes exactly this pair). - * Optional capability: absent = the provider answers `not-supported`. */ - setKeepAlive?(enabled: boolean, idleMs: number): void; + /** SO_KEEPALIVE + TCP_KEEPIDLE (node exposes exactly this pair). */ + setKeepAlive(enabled: boolean, idleMs: number): void; } export type TcpConnect = (options: { @@ -151,11 +148,11 @@ export interface TcpListener { settled(): Promise; accept(): Promise; close(): void; - /** Non-blocking accept + readiness (the 0.2 poll-shaped accept). - * Optional capabilities; same promise-swap contract as `waitReceive`. */ - tryAccept?(): TcpConn | undefined; - acceptReady?(): boolean; - waitAccept?(): Promise; + /** Non-blocking accept + readiness (the 0.2 poll-shaped accept); + * same promise-swap contract as `waitReceive`. */ + tryAccept(): TcpConn | undefined; + acceptReady(): boolean; + waitAccept(): Promise; } export type TcpListen = (options: { diff --git a/wasi/tests/http_test.ts b/wasi/tests/http_test.ts index e3a5be1..96a3655 100644 --- a/wasi/tests/http_test.ts +++ b/wasi/tests/http_test.ts @@ -337,17 +337,12 @@ Deno.test("http dispose: an unsent request settles its transmission future as er // --- fragment shape ------------------------------------------------------------------- -Deno.test("http fragment: the @0.3 track by default; rc snapshots re-key exactly", () => { +Deno.test("http fragment: the @0.3 track by default", () => { assertTrue( `wasi:http/types@${HTTP_TRACK}` in imports && `wasi:http/client@${HTTP_TRACK}` in imports, "track keys registered", ); - const calls: string[] = []; - const custom = http({ version: "0.3.0-rc-2099-01-01", onCall: (c) => calls.push(c) }); - assertTrue("wasi:http/types@0.3.0-rc-2099-01-01" in custom.imports, "rc override re-keys exactly"); - new custom.Fields(); - assertEq(JSON.stringify(calls), JSON.stringify(["fields.constructor"])); }); Deno.test("fragment: imports table has no handler key (header recipe: embedder registers `send` under its own handler key)", () => {