diff --git a/AGENTS.md b/AGENTS.md index 17b861c..03be33d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,8 @@ - Runtime semantics: the pinned Component Model spec and `third_party/component-model/design/mvp/canonical-abi/definitions.py`. - Wasmtime is corroborating evidence, not the tie-breaker. The single named - corpus exception is defined in + Wasmtime is corroborating evidence, not the tie-breaker. The authority policy + is defined in [architecture §1](docs/architecture.md#1-goals). - Interfaces: `contracts/`. Semantic contract changes are versioned events owned by the orchestrator; implementation tracks report conflicts rather than diff --git a/Cargo.lock b/Cargo.lock index 3e5ad77..eac909e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -99,15 +99,15 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cobs" @@ -178,8 +178,8 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.136.0-dev" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=4675ee16b703b33948073a5ff6b961367371e7a1#4675ee16b703b33948073a5ff6b961367371e7a1" +version = "0.137.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=cc546eee265b805dda0f034b2007d110a541093d#cc546eee265b805dda0f034b2007d110a541093d" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -187,8 +187,8 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.136.0-dev" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=4675ee16b703b33948073a5ff6b961367371e7a1#4675ee16b703b33948073a5ff6b961367371e7a1" +version = "0.137.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=cc546eee265b805dda0f034b2007d110a541093d#cc546eee265b805dda0f034b2007d110a541093d" dependencies = [ "serde", "serde_derive", @@ -197,8 +197,8 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.136.0-dev" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=4675ee16b703b33948073a5ff6b961367371e7a1#4675ee16b703b33948073a5ff6b961367371e7a1" +version = "0.137.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=cc546eee265b805dda0f034b2007d110a541093d#cc546eee265b805dda0f034b2007d110a541093d" dependencies = [ "cranelift-bitset", "serde", @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -253,12 +253,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fnv" version = "1.0.7" @@ -283,24 +277,25 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", - "stable_deref_trait", ] [[package]] name = "gimli" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" dependencies = [ "fnv", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", + "stable_deref_trait", ] [[package]] @@ -334,9 +329,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -358,9 +353,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "json-from-wast" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c74cef75a485ea9c18b6f7c1928bda2ef3a5727227fe7ebab4260626bc75f98e" +checksum = "7a27a2a28655413fb7a515592b0b2b79631c0a34ea51122603320664d6de395c" dependencies = [ "anyhow", "serde", @@ -388,9 +383,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" @@ -519,9 +514,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" dependencies = [ "serde", ] @@ -540,9 +535,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "3.0.3" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -640,9 +635,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasm-encoder" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" dependencies = [ "leb128fmt", "wasmparser", @@ -650,9 +645,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" dependencies = [ "bitflags", "hashbrown 0.17.1", @@ -663,9 +658,9 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a65e30fb2cd3cc5cb7761a6064787b2abf017e6566d8d0dedf2bb7f55ad5383" +checksum = "0510ce2ff0ad7c5510ad3dfcb2cedba4cf5d9f7e08d1d06e4d8cf8e5638aef95" dependencies = [ "anyhow", "termcolor", @@ -674,8 +669,8 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "49.0.0-dev" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=4675ee16b703b33948073a5ff6b961367371e7a1#4675ee16b703b33948073a5ff6b961367371e7a1" +version = "50.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=cc546eee265b805dda0f034b2007d110a541093d#cc546eee265b805dda0f034b2007d110a541093d" dependencies = [ "anyhow", "cpp_demangle", @@ -692,7 +687,6 @@ dependencies = [ "semver", "serde", "serde_derive", - "sha2", "smallvec", "target-lexicon", "wasm-encoder", @@ -704,13 +698,13 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "49.0.0-dev" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=4675ee16b703b33948073a5ff6b961367371e7a1#4675ee16b703b33948073a5ff6b961367371e7a1" +version = "50.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=cc546eee265b805dda0f034b2007d110a541093d#cc546eee265b805dda0f034b2007d110a541093d" [[package]] name = "wasmtime-internal-core" -version = "49.0.0-dev" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=4675ee16b703b33948073a5ff6b961367371e7a1#4675ee16b703b33948073a5ff6b961367371e7a1" +version = "50.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=cc546eee265b805dda0f034b2007d110a541093d#cc546eee265b805dda0f034b2007d110a541093d" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -720,12 +714,12 @@ dependencies = [ [[package]] name = "wast" -version = "258.0.0" +version = "259.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f7defc7ecca8b19ac7f824598eadd0c53985ee00c74060d65051e9da5b58a1" +checksum = "c69beba8d9da07af9a0971b559149a2ac4729895b55144de4fddbf5a02660648" dependencies = [ "bumpalo", - "gimli 0.32.3", + "gimli 0.34.0", "leb128fmt", "memchr", "unicode-width", @@ -734,9 +728,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.258.0" +version = "1.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7555c008cca87f2ac58d9f83ccda7e7b44611093ce28eb28f052e7c78024b9bf" +checksum = "c6eec44b0c80391b20fb7ad9ea440c72c5d382385bdf74444bb18321c425017e" dependencies = [ "wast", ] @@ -767,9 +761,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4daaa3cd97ae49ecd0a99dc009d453f93e0f083dd3be38c0f24a83a93e37ac" +checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" dependencies = [ "anyhow", "hashbrown 0.17.1", diff --git a/Cargo.toml b/Cargo.toml index 0f6c1d7..a1f4596 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,10 @@ publish = false [workspace.dependencies] # wasmtime-environ and wit-parser ride together: the pinned wasmtime rev -# (main, 49.0.0-dev) links wasmparser 0.258, and wit-parser 0.258 is the +# (main, 50.0.0-dev) links wasmparser 0.259, and wit-parser 0.259 is the # same wasm-tools release train (docs/architecture.md §4.1, §9). Bump both -# here, together — and third_party/component-model to the rev wasmtime's -# own tests/component-model submodule pins; `just test-rust` asserts the -# crates still agree. -wasmtime-environ = { git = "https://github.com/bytecodealliance/wasmtime", rev = "4675ee16b703b33948073a5ff6b961367371e7a1", features = ["component-model", "compile", "anyhow"] } -wit-parser = { version = "=0.258.0", features = ["serde"] } +# here together. The spec submodule is pinned independently to merged #719, +# including idle-drop semantics beyond Wasmtime's own corpus pin. +# `just test-rust` asserts the crates still agree. +wasmtime-environ = { git = "https://github.com/bytecodealliance/wasmtime", rev = "cc546eee265b805dda0f034b2007d110a541093d", features = ["component-model", "compile", "anyhow"] } +wit-parser = { version = "=0.259.0", features = ["serde"] } diff --git a/contracts/descriptor-ir.md b/contracts/descriptor-ir.md index 459ea6f..472821d 100644 --- a/contracts/descriptor-ir.md +++ b/contracts/descriptor-ir.md @@ -44,8 +44,9 @@ plan-format.md): `stringEncoding` (`utf8` | `utf16` | `latin1+utf16`), nullable `memory`, `realloc`, `postReturn`, `callback`, plus `async`, `cancellable`, and the expected flat `coreType` (`{params, results}` of `i32|i64|f32|f64`). This mirrors `wasmtime_environ::component::CanonicalOptions` minus runtime-irrelevant -fields; `data_model` is fixed to linear memory (the GC data model is rejected by -the shim). +fields, except the retained `cancellable` wire slot is always `false` and has no +execution meaning. `data_model` is fixed to linear memory (the GC data model is +rejected by the shim). ## Flattening diff --git a/contracts/embedder-api.md b/contracts/embedder-api.md index 9acd74f..7192630 100644 --- a/contracts/embedder-api.md +++ b/contracts/embedder-api.md @@ -364,10 +364,13 @@ runtime/tests/embedder/platform_class_test.ts): `result`-typed import. Bridge: catch and rethrow `new ComponentException(payload)`. -### Getters and setters (pre-ruling — not yet implementable) +### Getters and setters (specified upstream; frontend support pending) Getter/setter forms from component-model#701 are not implemented in the facade ([#254](https://github.com/polymorph-components/polyengine/issues/254)). The +merged spec includes them, but pinned wasmparser/wit-parser 0.259 do not expose +getter/setter function kinds. Updating the spec pin therefore does not enable +accessors in translation or bindgen. The intended mapping below is a design decision, not a supported API. Forms include `[get]foo`/`[set]foo`, instance members such as `[method][get]r.foo`, and static members such as `[static][set]r.foo`. diff --git a/contracts/intrinsics.md b/contracts/intrinsics.md index 58e1911..a01bb53 100644 --- a/contracts/intrinsics.md +++ b/contracts/intrinsics.md @@ -7,8 +7,8 @@ in `runtime/src/intrinsics/`. The pinned `wasmtime-environ` enums define the calling convention: `fact::Import` for adapter imports and `component::Trampoline` for host trampolines. The shim rejects variants it cannot represent. Component Model -semantics remain governed by the pinned spec and `definitions.py`, subject to -[architecture §1](../docs/architecture.md#1-goals)'s named exception. +semantics remain governed by the pinned spec and `definitions.py`; see +[architecture §1](../docs/architecture.md#1-goals). ## Universal semantics @@ -39,6 +39,12 @@ The runtime receives trampolines, context intrinsics, or ordinary core functions, memories, and flags globals; it never consumes `fact::Import` directly. `modules[].intrinsics` records import names and resolved categories. +Format 6's `enter-sync-call` takes `(calleeAsync, calleeInstance)`; caller +identity comes from the current logical thread. Wasmtime may omit the +enter/exit pair for thread-transparent adapters whose callees cannot observe +thread state. Such omission does not authorize store-wide scheduling across an +active synchronous call in another component instance. + - **Instance flags:** a mutable i32 `WebAssembly.Global`, initially 1, represents `may_leave` as a 0/1 boolean, not a bitmask. - **Traps:** each `runtime.trap` is a nullary import. Its plan declaration @@ -67,7 +73,10 @@ directly. `modules[].intrinsics` records import names and resolved categories. - **Destructive removal:** validation follows handle removal, as in the reference. If a removed stream/future end fails validation, local unwind retires it; a later table walk cannot find it. Successful transfers do not - retire their shared state. Peer notification failure must neither replace the + retire their shared state. Endpoint identity and pending notifications survive + transfer; event delivery uses the destination handle index. Custody checks + include slot tenure so a leave-and-return transfer cannot be reclaimed by an + earlier owner. Peer notification failure must neither replace the original trap nor skip drop observers. - **Nested exception barriers:** trap trampolines re-record the pending host trap before throwing, preserving its cause through outer barriers. @@ -91,6 +100,14 @@ task scheduler and JSPI activation bridge as the implicit thread. Publishing a task result does not destroy its remaining explicit threads; the worker owns host-call teardown after result delivery. +Task cancellation is recorded as pending. Startup admission and callback +WAIT/YIELD explicitly observe it; thread and waitable builtins do not consume +cancellation. A callback observes pending cancellation before releasing its +exclusive slot or inspecting its next WAIT handle. Unresolved async +`subtask.cancel` yields before checking resolution again; the synchronous form +waits for resolution while retaining the claim and lenders. JSPI is required +when that yield suspends a Wasm frame. + `thread.new-indirect` currently accepts the canonical final `(i32) -> ()` and `(i64) -> ()` start-function types. Its native `ref.test` validator rejects some functions whose valid non-final/derived type is structurally equivalent @@ -170,7 +187,9 @@ Tests exercise the same corpus under both modes where applicable. - WASI's marked blocking declarations select JSPI even when a particular call completes immediately. The plain fast path applies to sync-only plans with no marked imports, not to every call that happens not to wait. -- The async `subtask.cancel` determinacy park is a named non-atomicity - divergence. The synchronous form still waits for **resolution**, never merely - a STARTED event. Copy-cancel completion superseding is the separate CM-3 - exception. Both are documented in architecture §6. +- Async `subtask.cancel` includes the reference's yield when cancellation is + unresolved. Its JSPI continuation must settle before the next canonical + scheduling decision. The synchronous form waits for **resolution**, never + merely a STARTED event. Stream cancellation preserves progress and reports + `CANCELLED` unless peer drop takes precedence; future completion retains its + transferred payload's `COMPLETED` verdict. diff --git a/contracts/plan-format.md b/contracts/plan-format.md index 955e645..bb4826d 100644 --- a/contracts/plan-format.md +++ b/contracts/plan-format.md @@ -6,10 +6,19 @@ binary. This document is the interface between `crates/translator-shim` (producer) and `runtime/` (consumer); see also [descriptor-ir.md](descriptor-ir.md) and [intrinsics.md](intrinsics.md). -Current `formatVersion`: **5**. Loaders require strict equality. Schema changes +Current `formatVersion`: **6**. Loaders require strict equality. Schema changes bump the version and update producer and consumer together; editorial changes to this document do not change the wire format. +Format 6 versions the changed FACT `enter-sync-call` core signature: +`(calleeAsync, calleeInstance)`, replacing format 5's +`(callerInstance, calleeAsync, calleeInstance)`. Re-translate old deploy-time +envelopes with the matching translator; do not rewrite only the version number. +Old runtimes reject new plans and new runtimes reject old plans before any +initializer runs. The `cancellable` fields remain false-only wire slots: +the producer emits `false`, the loader rejects `true`, and execution no longer +uses them. Removed nonzero builtin immediates are validation errors. + ## Decisions (with rationale) 1. **Own schema, not wasmtime's.** The shim maps wasmtime's unstable internal @@ -44,10 +53,10 @@ with that version can produce different adapters. ```jsonc { - "formatVersion": 5, + "formatVersion": 6, "producer": { "shimVersion": "…", // crates/translator-shim crate version - "wasmtimeEnviron": "49.0.0-dev+4675ee1", // crate version + pinned git rev + "wasmtimeEnviron": "50.0.0-dev+cc546ee", // crate version + pinned git rev "features": ["cm-async", "…"] // wasmparser feature set used // (incl. cm-fixed-length-lists, cm-map, // cm-implements, cm-threading) — @@ -346,7 +355,7 @@ nondeterminism as a bug. module metadata; missing providers must not satisfy a type-mismatch assertion. Cover matching, mismatching, unused, and omitted imports, and verify rejection before start-function side effects. This is a follow-up design constraint, - not an optional field or a change to formatVersion 5; the schema and its + not an optional field or a change to formatVersion 6; the schema and its version transition must be reviewed together with the implementation. - `values` section (the component-level value-definition feature): out of scope (wasmtime parity, docs/architecture.md §7). diff --git a/crates/testgen/Cargo.toml b/crates/testgen/Cargo.toml index 687f58d..5ad0a07 100644 --- a/crates/testgen/Cargo.toml +++ b/crates/testgen/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] anyhow = "1" -json-from-wast = "0.258.0" +json-from-wast = "0.259.0" serde_json = { version = "1", features = ["preserve_order"] } serde = { version = "1", features = ["derive"] } -wast = "258.0.0" +wast = "259.0.0" diff --git a/crates/translator-shim/Cargo.toml b/crates/translator-shim/Cargo.toml index d794b7e..f11746a 100644 --- a/crates/translator-shim/Cargo.toml +++ b/crates/translator-shim/Cargo.toml @@ -20,4 +20,4 @@ base64 = { version = "0.22", default-features = false, features = ["alloc"] } [dev-dependencies] # For generating test components from WAT in tests (same wasm-tools family # version as wasmparser above). -wat = "1.258" +wat = "1.259" diff --git a/crates/translator-shim/README.md b/crates/translator-shim/README.md index 5eaaa61..a160757 100644 --- a/crates/translator-shim/README.md +++ b/crates/translator-shim/README.md @@ -2,7 +2,7 @@ The `wasmtime-environ` component frontend compiled as a native Rust library and a wasm32 translator. It validates components, resolves linkage, synthesizes -FACT fused adapters, and emits **plan format 5**. The +FACT fused adapters, and emits **plan format 6**. The [plan contract](../../contracts/plan-format.md) owns the schema; [architecture](../../docs/architecture.md) explains the frontend/runtime split. Dependency pins live in the root `Cargo.toml` and `Cargo.lock`. diff --git a/crates/translator-shim/src/fact_string_limits.rs b/crates/translator-shim/src/fact_string_limits.rs index e41e784..41eb707 100644 --- a/crates/translator-shim/src/fact_string_limits.rs +++ b/crates/translator-shim/src/fact_string_limits.rs @@ -8,7 +8,7 @@ use wasmtime_environ::wasmparser::{self, BlockType, Operator as Op, Parser, Payl const MAX: i64 = (1 << 28) - 1; const OLD: i64 = (1 << 31) - 1; -const PIN: &str = "rev = \"4675ee16b703b33948073a5ff6b961367371e7a1\""; +const PIN: &str = "rev = \"cc546eee265b805dda0f034b2007d110a541093d\""; pub(super) fn correct(wasm: &[u8]) -> Result> { ensure!( diff --git a/crates/translator-shim/src/lib.rs b/crates/translator-shim/src/lib.rs index 5c30363..85145e8 100644 --- a/crates/translator-shim/src/lib.rs +++ b/crates/translator-shim/src/lib.rs @@ -38,7 +38,7 @@ use wasmtime_environ::component::{ComponentTypesBuilder, Translator}; use wasmtime_environ::{ScopeVec, Tunables, wasmparser}; /// Pinned `wasmtime-environ` crate version + short git rev (root Cargo.toml). -pub const WASMTIME_ENVIRON_VERSION: &str = "49.0.0-dev+4675ee1"; +pub const WASMTIME_ENVIRON_VERSION: &str = "50.0.0-dev+cc546ee"; pub mod error; mod fact_string_limits; diff --git a/crates/translator-shim/src/plan.rs b/crates/translator-shim/src/plan.rs index 44f8173..8b47d17 100644 --- a/crates/translator-shim/src/plan.rs +++ b/crates/translator-shim/src/plan.rs @@ -12,7 +12,7 @@ //! `ModuleImport { ty, import }`, `Instance { ty, exports }`, `Type(TypeDef)`. //! - `Component::options: PrimaryMap` where //! `CanonicalOptions { instance, string_encoding, callback, post_return, -//! async_, cancellable, core_type, data_model }` and memory/realloc live +//! async_, core_type, data_model }` and memory/realloc live //! inside `data_model: CanonicalOptionsDataModel::LinearMemory(..)`. //! - Types come from `ComponentTypesBuilder::finish(&component)`, which yields //! a `ComponentTypes` indexable by every `Type*Index`, including @@ -38,7 +38,7 @@ use wasmtime_environ::component::{ use wasmtime_environ::{EntityIndex, ModuleInternedTypeIndex, PrimaryMap, WasmValType}; /// `formatVersion` this producer emits (contracts/plan-format.md). -pub const FORMAT_VERSION: u32 = 5; +pub const FORMAT_VERSION: u32 = 6; // --------------------------------------------------------------------------- // Plan schema (serde structs; field order == emission order == contract order) @@ -1008,13 +1008,12 @@ impl<'a> PlanBuilder<'a> { index, instance: instance.as_u32(), }, - T::ThreadYield { - instance, - cancellable, - } => TrampolineDecl::ThreadYield { + T::ThreadYield { instance } => TrampolineDecl::ThreadYield { index, instance: instance.as_u32(), - cancellable: *cancellable, + // CONTRACT: format v6 retains this removed upstream slot for + // old plans; new translations must emit only false. + cancellable: false, }, T::SubtaskDrop { instance } => TrampolineDecl::SubtaskDrop { index, @@ -1202,45 +1201,30 @@ impl<'a> PlanBuilder<'a> { index, instance: instance.as_u32(), }, - T::ThreadSuspend { - instance, - cancellable, - } => TrampolineDecl::ThreadSuspend { + T::ThreadSuspend { instance } => TrampolineDecl::ThreadSuspend { index, instance: instance.as_u32(), - cancellable: *cancellable, + cancellable: false, }, - T::ThreadSuspendThenResume { - instance, - cancellable, - } => TrampolineDecl::ThreadSuspendThenResume { + T::ThreadSuspendThenResume { instance } => TrampolineDecl::ThreadSuspendThenResume { index, instance: instance.as_u32(), - cancellable: *cancellable, + cancellable: false, }, - T::ThreadYieldThenResume { - instance, - cancellable, - } => TrampolineDecl::ThreadYieldThenResume { + T::ThreadYieldThenResume { instance } => TrampolineDecl::ThreadYieldThenResume { index, instance: instance.as_u32(), - cancellable: *cancellable, + cancellable: false, }, - T::ThreadSuspendThenPromote { - instance, - cancellable, - } => TrampolineDecl::ThreadSuspendThenPromote { + T::ThreadSuspendThenPromote { instance } => TrampolineDecl::ThreadSuspendThenPromote { index, instance: instance.as_u32(), - cancellable: *cancellable, + cancellable: false, }, - T::ThreadYieldThenPromote { - instance, - cancellable, - } => TrampolineDecl::ThreadYieldThenPromote { + T::ThreadYieldThenPromote { instance } => TrampolineDecl::ThreadYieldThenPromote { index, instance: instance.as_u32(), - cancellable: *cancellable, + cancellable: false, }, }) } @@ -1553,7 +1537,9 @@ impl<'a> PlanBuilder<'a> { post_return: opts.post_return.map(|p| p.as_u32()), callback: opts.callback.map(|c| c.as_u32()), r#async: opts.async_, - cancellable: opts.cancellable, + // CONTRACT: format v6 retains this removed upstream slot for old + // plans; new translations must emit only false. + cancellable: false, core_type: self.core_func_type(opts.core_type)?, }) } diff --git a/crates/translator-shim/testdata/async-lift.wat b/crates/translator-shim/testdata/async-lift.wat index 4af9487..1b77d99 100644 --- a/crates/translator-shim/testdata/async-lift.wat +++ b/crates/translator-shim/testdata/async-lift.wat @@ -22,4 +22,4 @@ (core instance $i (instantiate $M (with "" (instance (export "task.return" (func $task-return)))))) (func (export "f") async (param "x" u32) (result u32) - (canon lift (core func $i "f") async (callback (func $i "cb"))))) + (canon lift (core func $i "f") async (callback (core func $i "cb"))))) diff --git a/crates/translator-shim/testdata/async-linked.wat b/crates/translator-shim/testdata/async-linked.wat index 210134b..6c0e162 100644 --- a/crates/translator-shim/testdata/async-linked.wat +++ b/crates/translator-shim/testdata/async-linked.wat @@ -19,7 +19,7 @@ (core instance $ia (instantiate $MA (with "" (instance (export "task.return" (func $task-return)))))) (func (export "add") async (param "a" u32) (param "b" u32) (result u32) - (canon lift (core func $ia "add-impl") async (callback (func $ia "cb"))))) + (canon lift (core func $ia "add-impl") async (callback (core func $ia "cb"))))) (component $B (import "adder" (instance $adder @@ -32,7 +32,7 @@ ;; instantiated first to avoid a circular dependency. (core module $MEM (memory (export "mem") 1)) (core instance $imem (instantiate $MEM)) - (core func $add_async (canon lower (func $adder "add") async (memory $imem "mem"))) + (core func $add_async (canon lower (func $adder "add") async (memory (core memory $imem "mem")))) (core module $MB (import "adder" "add" (func $add (param i32 i32) (result i32))) diff --git a/crates/translator-shim/testdata/deadlock-sync-call.wat b/crates/translator-shim/testdata/deadlock-sync-call.wat index 1b28d7e..335a278 100644 --- a/crates/translator-shim/testdata/deadlock-sync-call.wat +++ b/crates/translator-shim/testdata/deadlock-sync-call.wat @@ -46,7 +46,7 @@ (export "waitable-set.new" (func $waitable-set.new)) (export "waitable-set.wait" (func $waitable-set.wait)))))) (func (export "block") async (param "a" u32) (param "b" u32) (result u32) - (canon lift (core func $ia "block-impl") async (callback (func $ia "cb"))))) + (canon lift (core func $ia "block-impl") async (callback (core func $ia "cb"))))) (component $B (import "blocker" (instance $blocker diff --git a/crates/translator-shim/testdata/error-context.wat b/crates/translator-shim/testdata/error-context.wat index 8ed0e25..e920d98 100644 --- a/crates/translator-shim/testdata/error-context.wat +++ b/crates/translator-shim/testdata/error-context.wat @@ -6,7 +6,7 @@ (component (core module $Mem (memory (export "mem") 1)) (core instance $mem (instantiate $Mem)) - (core func $ec-new (canon error-context.new (memory $mem "mem"))) + (core func $ec-new (canon error-context.new (memory (core memory $mem "mem")))) (core func $ec-drop (canon error-context.drop)) (core module $M (import "" "ec-new" (func $ec-new (param i32 i32) (result i32))) diff --git a/crates/translator-shim/testdata/gen.sh b/crates/translator-shim/testdata/gen.sh index 41c6a5d..e9c0a80 100755 --- a/crates/translator-shim/testdata/gen.sh +++ b/crates/translator-shim/testdata/gen.sh @@ -3,7 +3,7 @@ # # Requires wasm-tools (spike used 1.247.0). Note `wasm-tools parse` only # converts text to binary; validation is done by the translator itself -# (wasmparser 0.258 via the pinned wasmtime-environ git rev, see root +# (wasmparser 0.259 via the pinned wasmtime-environ git rev, see root # Cargo.toml) and, as a cross-check, by native wasmtime 47: # # wasmtime compile testdata/trivial.wasm @@ -11,7 +11,7 @@ # # (wasm-tools 1.247's own `validate --features component-model,cm-async` also # passes, but its validator predates the async-function-type requirement that -# wasmparser 0.258 enforces, so it is not the authority here.) +# wasmparser 0.259 enforces, so it is not the authority here.) # # Fixtures whose syntax is newer than the installed CLI (e.g. `relend-borrow`, # which uses `(dtor (core func ...))`) are generated from the pinned `wat` diff --git a/crates/translator-shim/testdata/imports.wat b/crates/translator-shim/testdata/imports.wat index 82132cc..29ed7f2 100644 --- a/crates/translator-shim/testdata/imports.wat +++ b/crates/translator-shim/testdata/imports.wat @@ -31,7 +31,7 @@ (canon lower (func $log) (core func $log')) (canon lower (func $add) (core func $add')) (canon lower (func $greet) - (memory $mem "mem") (realloc (func $mem "realloc")) + (memory (core memory $mem "mem")) (realloc (core func $mem "realloc")) (core func $greet')) (core module $M diff --git a/crates/translator-shim/testdata/transcode.wat b/crates/translator-shim/testdata/transcode.wat index 172417e..a6caa61 100644 --- a/crates/translator-shim/testdata/transcode.wat +++ b/crates/translator-shim/testdata/transcode.wat @@ -16,7 +16,7 @@ (func (export "take") (param "s" string) (result u32) (canon lift (core func $m "take") (memory (core memory $m "mem")) - (realloc (func $m "realloc")) + (realloc (core func $m "realloc")) string-encoding=utf8))) (component $Utf16 @@ -28,7 +28,7 @@ (core instance $m (instantiate $M)) (core func $take' (canon lower (func $take) (memory (core memory $m "mem")) - (realloc (func $m "realloc")) + (realloc (core func $m "realloc")) string-encoding=utf16)) (core module $D (import "" "take" (func $take (param i32 i32) (result i32))) diff --git a/crates/translator-shim/tests/fact_string_source_limits.rs b/crates/translator-shim/tests/fact_string_source_limits.rs index a171bb3..741f8fc 100644 --- a/crates/translator-shim/tests/fact_string_source_limits.rs +++ b/crates/translator-shim/tests/fact_string_source_limits.rs @@ -3,14 +3,20 @@ use wasmtime_environ::wasmparser::{Operator, Parser, Payload}; #[test] fn fact_string_encoding_matrix() { - let bytes = wat::parse_str(include_str!( - "../../../runtime/tests/fixtures/fact-string-source-limits.wat" - )) - .unwrap(); + // wasm-tools 0.259 requires nested core export references. Keep consuming + // the runtime-owned probe source without editing the other track's file. + let source = include_str!("../../../runtime/tests/fixtures/fact-string-source-limits.wat") + .replace("(memory $m \"memory\")", "(memory (core memory $m \"memory\"))") + .replace( + "(memory $mem \"memory\")", + "(memory (core memory $mem \"memory\"))", + ) + .replace("(realloc (func $m", "(realloc (core func $m"); + let bytes = wat::parse_str(source).unwrap(); let translated = translate(&bytes).unwrap(); assert_eq!( translated.plan.producer.wasmtime_environ, - "49.0.0-dev+4675ee1" + "50.0.0-dev+cc546ee" ); let mut limits = [0, 0]; for adapter in &translated.adapters { diff --git a/crates/translator-shim/tests/translate.rs b/crates/translator-shim/tests/translate.rs index 219dea0..a68e8a6 100644 --- a/crates/translator-shim/tests/translate.rs +++ b/crates/translator-shim/tests/translate.rs @@ -6,6 +6,7 @@ use translator_shim::plan::{ ValTypeJson, FORMAT_VERSION, }; use translator_shim::{to_envelope_json, translate, Phase, Translation}; +use wasmtime_environ::wasmparser::{Parser, Payload, TypeRef}; fn build(name: &str) -> Vec { let path = format!("{}/testdata/{name}.wat", env!("CARGO_MANIFEST_DIR")); @@ -70,6 +71,84 @@ fn trivial() { } } +/// The Wasmtime 50 frontend changed FACT's enter-sync-call core ABI from +/// three arguments to two. Format 6 prevents old loaders from instantiating +/// adapters whose intrinsic arguments they would decode incorrectly. +#[test] +fn producer_format_tracks_enter_sync_call_abi() { + assert_eq!(FORMAT_VERSION, 6); + assert_eq!(translate(&build("trivial")).unwrap().plan.format_version, 6); +} + +#[test] +fn enter_sync_call_adapter_import_has_two_arguments() { + let bytes = wat::parse_str( + r#"(component + (component $a + (core func $get (canon context.get i32 0)) + (core module $m + (import "" "get" (func $get (result i32))) + (func (export "f") (result i32) call $get)) + (core instance $i (instantiate $m + (with "" (instance (export "get" (func $get)))))) + (func (export "f") (result u32) (canon lift (core func $i "f")))) + (component $b + (import "f" (func $f (result u32))) + (core func $f' (canon lower (func $f))) + (core module $m + (import "" "f" (func $f (result i32))) + (func (export "g") (result i32) call $f)) + (core instance $i (instantiate $m + (with "" (instance (export "f" (func $f')))))) + (func (export "g") (result u32) (canon lift (core func $i "g")))) + (instance $a (instantiate $a)) + (instance $b (instantiate $b (with "f" (func $a "f")))) + (export "g" (func $b "g")))"#, + ) + .unwrap(); + let translated = translate(&bytes).unwrap(); + let adapter = translated + .adapters + .iter() + .find(|adapter| { + translated.plan.modules.iter().any(|module| { + matches!(module, ModuleEntry::Adapter { file, intrinsics, .. } + if file == &adapter.file + && intrinsics.iter().any(|i| i.name == "enter-sync-call")) + }) + }) + .expect("context-sensitive adapter must bracket its callee"); + let mut types = None; + for payload in Parser::new(0).parse_all(&adapter.wasm) { + match payload.unwrap() { + Payload::TypeSection(section) => { + types = Some( + section + .into_iter_err_on_gc_types() + .map(Result::unwrap) + .collect::>(), + ); + } + Payload::ImportSection(imports) => { + for import in imports.into_imports() { + let import = import.unwrap(); + if import.module == "async" && import.name == "enter-sync-call" { + let TypeRef::Func(index) = import.ty else { + panic!("enter-sync-call must be a function") + }; + let ty = &types.as_ref().unwrap()[index as usize]; + assert_eq!(ty.params().len(), 2); + assert!(ty.results().is_empty()); + return; + } + } + } + _ => {} + } + } + panic!("enter-sync-call import missing"); +} + /// (b) Cross-instance call between two inline component instances: FACT must /// generate at least one fused adapter module, instantiated with /// `instance: null` and carrying a categorized intrinsics manifest. @@ -100,8 +179,9 @@ fn linked_generates_fact_adapter() { assert_eq!(adapter_inits.len(), t.adapters.len()); // Manifest categories cover the fixture's intrinsic surface - // (intrinsics.md §A): callee core-def, instance flags, trap + - // enter/exit-sync-call trampolines. (`task-may-block` was a + // (intrinsics.md §A): callee core-def, instance flags, and trap. + // The 50.0.0-dev frontend's thread-transparency analysis intentionally + // elides enter/exit-sync-call for this synchronous adapter. (`task-may-block` was a // `CoreDef::TaskMayBlock` category; upstream removed that variant at the // pinned wasmtime-environ rev, see root Cargo.toml.) let manifests: Vec = adapter_entries(&t) @@ -117,8 +197,6 @@ fn linked_generates_fact_adapter() { "core-def", "instance-flags", "trampoline:trap", - "trampoline:enter-sync-call", - "trampoline:exit-sync-call", ] { assert!( manifests.iter().any(|c| c == expected), @@ -566,6 +644,36 @@ fn verdict_phase_validation() { } } +/// The post-cancellable encoding keeps a reserved byte at each affected +/// opcode, and the merged spec requires that byte to be zero +/// (third_party/component-model/test/binary/binary.wast:1185-1282). +#[test] +fn rejects_all_eight_nonzero_removed_cancellable_bytes() { + let memory_prefix: &[u8] = &[ + 0, 97, 115, 109, 13, 0, 1, 0, 1, 22, 0, 97, 115, 109, 1, 0, 0, 0, 5, 3, 1, 0, 1, + 7, 7, 1, 3, 109, 101, 109, 2, 0, 2, 4, 1, 0, 0, 0, 6, 9, 1, 0, 2, 1, 0, 3, + 109, 101, 109, 8, 4, 1, + ]; + let mut cases = vec![]; + for opcode in [0x20, 0x21] { + let mut bytes = memory_prefix.to_vec(); + bytes.extend([opcode, 1, 0]); + cases.push(bytes); + } + for opcode in [0x29, 0x0c, 0x2a, 0x2b, 0x2c, 0x2d] { + cases.push(vec![0, 97, 115, 109, 13, 0, 1, 0, 8, 3, 1, opcode, 1]); + } + + for bytes in cases { + let error = translate(&bytes).expect_err("nonzero reserved byte was accepted"); + assert_eq!(error.phase, Phase::Validation, "{error}"); + assert!( + error.message.contains("zero byte") || error.message.contains("reserved"), + "{error}" + ); + } +} + /// A valid component the plan format cannot express is `unsupported` — never /// `validation`, which would be a false claim about the component. #[test] diff --git a/ct-runner/deno.json b/ct-runner/deno.json index 73e006b..bdf2183 100644 --- a/ct-runner/deno.json +++ b/ct-runner/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/ct-runner", - "version": "0.6.8", + "version": "0.7.0", "exports": { ".": "./src/mod.ts" }, diff --git a/docs/architecture.md b/docs/architecture.md index d50238e..be76e55 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,26 +37,22 @@ scheduling, deterministic NaNs, and JS-native host value shapes. It also has named divergences (§6). The conformance harness accommodates engine-specific trap wording; diagnostic text is not the public API. -**One bounded exception (operator decision):** when `definitions.py` conflicts with the spec -repository's own WAST corpus and wasmtime implements the corpus side, the -corpus semantics may be adopted as a working assumption. This requires -verification against wasmtime source or a trace, a named finding in -[upstream-component-model-repo-findings.md](../upstream-component-model-repo-findings.md), -and reversal if upstream adjudicates otherwise. Currently this applies -only to CM-3. A schedule-dependent assertion cannot invoke the exception: -if two conforming schedulers can answer differently, the assertion pins a -policy rather than semantics. Wasmtime behavior alone is insufficient. - -**Pending upstream stream/future assumption.** Until -[component-model PR #719](https://github.com/WebAssembly/component-model/pull/719) -is adjudicated, this checkout adopts only its drop-delivery rule at head -`35e9769957627c2bee5cd445b998b08b3c652c86`; the submodule remains pinned at -`7c676115e93cd7d54c1732d95c54c6a3de7c5ae0`. A stream event tests for peer -drop when the event is consumed, upgrades the result to `DROPPED`, preserves -the accumulated element count, and retires the end. A future `COMPLETED` stays -`COMPLETED` because its payload transferred; a pending future `CANCELLED` -upgrades to `DROPPED` with zero progress if the peer is gone. Already-delivered -events are not rewritten. The PR's broader paired-end refactor is not adopted. +**Pinned stream/future semantics.** The spec is pinned at merged +[PR #719](https://github.com/WebAssembly/component-model/pull/719), +`a53b241d4d50487d256900fcad77cd6cda51808f`, including #720's idle-drop rule +and #721's partial/zero-length tests. Drop notifies an idle peer as well as +one with a pending copy. The endpoint owns that notification across transfers; +consuming it reports the current owner's handle index and retires the end. +Stream events observe peer drop at consumption, preserving accumulated progress. +Future payload completion stays `COMPLETED`; an undelivered cancellation becomes +`DROPPED` if its peer is gone. Already-delivered events are not rewritten. + +The merged reference also specifies cancellation of an unobserved stream copy +as `CANCELLED|count`, resolving the historical CM-3 exception. There is no +current corpus/reference exception. Findings remain recorded in +[upstream-component-model-repo-findings.md](../upstream-component-model-repo-findings.md). +Schedule-dependent assertions cannot override the reference's candidate rules; +Wasmtime behavior alone is insufficient. ## 2. Non-goals @@ -319,15 +315,15 @@ the signal is aborted in a microtask, never inside the guest activation. These are embedding policies permitted by the reference's host-callee cancellation hook. +**Guest cancellation delivery.** A request records pending task state; ordinary +thread parks and waitable builtins do not consume it. Startup admission checks +it after wakeup. Callback WAIT/YIELD delivers it before releasing exclusivity +or resolving the requested waitable-set handle. An unresolved async +`subtask.cancel` yields once, then checks resolution again; the synchronous +form retains its wait claim and lenders until resolution. + Named differences from the reference or other hosts: -- **Async `subtask.cancel` under JSPI is not atomic** - ([#92](https://github.com/polymorph-components/polyengine/issues/92)). - The runtime may park for a determinate cancellation result across the - engine's mandatory microtask hop. Ready sibling threads can run during - that park. See `createSubtaskCancel` in - `runtime/src/intrinsics/async_builtins.ts` and - `runtime/tests/cancel_bracket_race_test.ts`. - **Per-instance poisoning** ([#173](https://github.com/polymorph-components/polyengine/issues/173)). A trap escaping a guest activation permanently poisons that instance; @@ -336,15 +332,6 @@ Named differences from the reference or other hosts: trap state; wasmtime's store-level trap handling is not this policy. The same-instance exemption in `entryRefusal` permits destructor self-drops. Reentrance into an otherwise live instance is valid. -- **Cancelling an unobserved completed stream copy reports - `CANCELLED|count`**, preserving the count rather than delivering the - pending `COMPLETED|count` - ([#296](https://github.com/polymorph-components/polyengine/issues/296)). - This is the sole §1 corpus/reference exception, - [CM-3](../upstream-component-model-repo-findings.md#cm-3-cancel_copy-returns-a-stale-completed-where-wasmtime-reports-cancelled). - See `takeCancelEvent` in `runtime/src/intrinsics/stream_builtins.ts`. - This cancellation-time `COMPLETED`→`CANCELLED` remap is distinct from the - PR #719 consumption-time peer-drop upgrade above; peer drop takes precedence. ## 7. Canonical ABI decisions @@ -584,19 +571,19 @@ browsers, engine shells, and server runtimes. Engine trap-message normalization belongs to `TRAP_MESSAGE_EQUIVALENTS` in `harness/src/runner.ts`, not the runtime. -Expected failures are classified, not counted as conformance. Known -classes include the explicit-thread type-interface restriction -([#12](https://github.com/polymorph-components/polyengine/issues/12)), sync scheduling gaps -([#249](https://github.com/polymorph-components/polyengine/issues/249)), -and upstream-unimplemented features -([#248](https://github.com/polymorph-components/polyengine/issues/248)). +Expected failures are classified, not counted as conformance. Remaining +base classes and their tracking issues are listed in +[harness/src/xfail.ts](../harness/src/xfail.ts); broader capability restrictions +such as the explicit-thread type interface +([#12](https://github.com/polymorph-components/polyengine/issues/12)) need not +be exercised by an expected-failure row. Per-lane overlays distinguish engine limitations from runtime failures. The current base classification is in [harness/src/xfail.ts](../harness/src/xfail.ts). Unexpected failures and stale expected failures fail their gate. -The current Deno and Chromium official-corpus baseline executes 1,506 of -1,511 commands: 1,468 pass, 38 are exact xfails, and five text directives are -unsupported. There are no runtime/capability skips. The corpus exercises the +Current verified corpus counts are recorded in the +[harness README](../harness/README.md#supplementary-wasmtime-coverage). +There are no runtime/capability skips. The corpus exercises the implemented explicit-thread family using canonical-final start signatures; it does not exercise the valid non-final/derived signature restriction described in the intrinsic contract, so these counts are not a full-thread-conformance diff --git a/docs/references.md b/docs/references.md index 577b545..5ffe5cf 100644 --- a/docs/references.md +++ b/docs/references.md @@ -2,7 +2,7 @@ Use the checked-in dependency pins when investigating behavior. Links to upstream `main` show current upstream work, not necessarily this checkout's -semantics. The authority policy, including the sole CM-3 exception, is in +semantics. The authority policy is in [architecture §1](architecture.md#1-goals). ## Component Model spec (submodule: `third_party/component-model`) @@ -24,11 +24,10 @@ reference, and WAST corpus used by this checkout. Its principal sources are: Current upstream sources are at [WebAssembly/component-model](https://github.com/WebAssembly/component-model). -The narrow provisional semantics adopted from -[PR #719](https://github.com/WebAssembly/component-model/pull/719) are fixed to -head `35e9769957627c2bee5cd445b998b08b3c652c86` and recorded in -[architecture §1](architecture.md#1-goals); they do not move the submodule pin -or adopt that PR's structural refactor. +The spec pin is merged [PR #719](https://github.com/WebAssembly/component-model/pull/719), +`a53b241d4d50487d256900fcad77cd6cda51808f`, including idle drop delivery from +#720 and partial/zero-length tests from #721. It supersedes the earlier +provisional adoption and resolves CM-3; see [architecture §1](architecture.md#1-goals). The [Component Model book](https://component-model.bytecodealliance.org/) is introductory documentation, not the semantic tie-breaker. Local discrepancies belong in @@ -51,22 +50,22 @@ Local discrepancies belong in - [Stack-switching proposal](https://github.com/WebAssembly/stack-switching): related core-wasm work, not polyengine's current scheduling mechanism. -## wasmtime internals (pinned: wasmtime-environ **49.0.0-dev+4675ee1**, a git rev of `main`) +## wasmtime internals (pinned: wasmtime-environ **50.0.0-dev+cc546ee**, a git rev of `main`) The revision is declared in [Cargo.toml](../Cargo.toml) and resolved in [Cargo.lock](../Cargo.lock). Update source links with the pin. -- [Environ at the pinned revision](https://github.com/bytecodealliance/wasmtime/tree/4675ee16b703b33948073a5ff6b961367371e7a1/crates/environ/src): +- [Environ at the pinned revision](https://github.com/bytecodealliance/wasmtime/tree/cc546eee265b805dda0f034b2007d110a541093d/crates/environ/src): component translation and plan structures under `component/`; fused adapter generation in `fact.rs` and `fact/`. -- [Adapter translation](https://github.com/bytecodealliance/wasmtime/blob/4675ee16b703b33948073a5ff6b961367371e7a1/crates/environ/src/component/translate/adapt.rs): +- [Adapter translation](https://github.com/bytecodealliance/wasmtime/blob/cc546eee265b805dda0f034b2007d110a541093d/crates/environ/src/component/translate/adapt.rs): how component linkage is translated into FACT adapters. -- [Component-model tests at the same revision](https://github.com/bytecodealliance/wasmtime/tree/4675ee16b703b33948073a5ff6b961367371e7a1/tests/misc_testsuite/component-model): +- [Component-model tests at the same revision](https://github.com/bytecodealliance/wasmtime/tree/cc546eee265b805dda0f034b2007d110a541093d/tests/misc_testsuite/component-model): converted and classified separately by `just test-wasmtime` (see [harness/README.md](../harness/README.md#supplementary-wasmtime-coverage)); supplementary reference material, not merged into the official corpus or an independent check of the reused frontend. -- [Async test-programs guests at the same revision](https://github.com/bytecodealliance/wasmtime/tree/4675ee16b703b33948073a5ff6b961367371e7a1/crates/test-programs/src/bin): +- [Async test-programs guests at the same revision](https://github.com/bytecodealliance/wasmtime/tree/cc546eee265b805dda0f034b2007d110a541093d/crates/test-programs/src/bin): `async_round_trip_stackless` and `async_short_reads`, built by `just wasmtime-guests` and driven through the public embedder API by `just test-wasmtime-guests`. @@ -82,9 +81,9 @@ agree with the wasmtime frontend. - [wasm-tools](https://github.com/bytecodealliance/wasm-tools): CLI and libraries, including `wasmparser`, `wasm-encoder`, `wit-parser`, `wast`, and `json-from-wast`. -- [wasmparser](https://docs.rs/wasmparser/0.258.0/wasmparser/): parsing and validation. -- [wasm-encoder](https://docs.rs/wasm-encoder/0.258.0/wasm_encoder/): core and component binary generation. -- [wit-parser](https://docs.rs/wit-parser/0.258.0/wit_parser/): bindgen's WIT input. +- [wasmparser](https://docs.rs/wasmparser/0.259.0/wasmparser/): parsing and validation. +- [wasm-encoder](https://docs.rs/wasm-encoder/0.259.0/wasm_encoder/): core and component binary generation. +- [wit-parser](https://docs.rs/wit-parser/0.259.0/wit_parser/): bindgen's WIT input. - [wast](https://docs.rs/wast/): WAST parsing; use the version in the lockfile. - [json-from-wast](https://docs.rs/json-from-wast/): testgen's JSON-command and wasm-artifact conversion. - [wit-bindgen](https://github.com/bytecodealliance/wit-bindgen) and its diff --git a/harness/README.md b/harness/README.md index d3c22f8..1e8b728 100644 --- a/harness/README.md +++ b/harness/README.md @@ -98,7 +98,16 @@ to [#372](https://github.com/polymorph-components/polyengine/issues/372) (runtime-semantics, diagnostic-mismatch, imported-module, cascade, provider-control, exception-handling — plan v0 / diagnostic gaps against Wasmtime's own assertions, not the spec corpus above). The supplementary thread -fixtures now execute rather than being skipped. This does not imply +fixtures now execute rather than being skipped. + +One thread-transparency row is classified separately as a specification +conflict under [#380](https://github.com/polymorph-components/polyengine/issues/380). +Wasmtime permits an outer-instance ready thread to progress a nested synchronous +lift, while the pinned Component Model `canon_lift` loop restricts candidates to +the synchronous callee instance. The row still executes and its exact failure is +checked; it is neither excluded nor treated as a diagnostic equivalence. + +This does not imply unrestricted explicit-thread conformance: valid non-final or derived start-function signatures can be rejected by the runtime's nominal `ref.test` validator, as documented in `contracts/intrinsics.md`; the current Wasmtime @@ -147,11 +156,11 @@ The capacity knob is intentionally not a production capability; the bounded reuse test above covers its leak-detection purpose without pretending to reproduce Wasmtime's host configuration surface. -The official Deno/Chromium aggregate at the current Component Model pin is -1,511 commands, 1,506 executed, 1,468 passed, 38 exact xfails, zero +The official Deno, Chromium, and Firefox aggregate at the current Component Model pin is +1,622 commands, 1,617 executed, 1,593 passed, 24 exact xfails, zero runtime/capability skips, and five unsupported text directives. Seeded runs omit the three-command `async-calls-sync` deterministic-profile fixture, giving -1,508 commands, 1,503 executed, and 1,465 passed with the same 38 xfails and +1,619 commands, 1,614 executed, and 1,590 passed with the same 24 xfails and five unsupported directives. `just test-wasmtime-guests` builds the two upstream async guest binaries this diff --git a/harness/browser/expectations/chromium.ts b/harness/browser/expectations/chromium.ts index 2912845..8a5c4c9 100644 --- a/harness/browser/expectations/chromium.ts +++ b/harness/browser/expectations/chromium.ts @@ -15,11 +15,11 @@ export const chromium: LaneExpectation = { deltas: [], // Identical to the Deno lane's TOTAL row. totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/browser/expectations/firefox.ts b/harness/browser/expectations/firefox.ts index f6dd176..5dd4067 100644 --- a/harness/browser/expectations/firefox.ts +++ b/harness/browser/expectations/firefox.ts @@ -19,11 +19,11 @@ export const firefox: LaneExpectation = { // Findings lane: totals are recorded for drift detection but the driver // does not gate on them (`required: false`). totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/bun-pinned.ts b/harness/shell/expectations/bun-pinned.ts index 7e3782d..f6e72d1 100644 --- a/harness/shell/expectations/bun-pinned.ts +++ b/harness/shell/expectations/bun-pinned.ts @@ -43,11 +43,11 @@ export const bunPinned: ShellLaneExpectation = { "default. Findings-only until a CI track record, then promote.", deltas: [], totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/jsc-pinned.ts b/harness/shell/expectations/jsc-pinned.ts index 0d7f998..d9c599a 100644 --- a/harness/shell/expectations/jsc-pinned.ts +++ b/harness/shell/expectations/jsc-pinned.ts @@ -19,11 +19,11 @@ export const jscPinned: ShellLaneExpectation = { "hash-pinned rev.", deltas: [], totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/jsc-trunk.ts b/harness/shell/expectations/jsc-trunk.ts index df30c8a..d842f7d 100644 --- a/harness/shell/expectations/jsc-trunk.ts +++ b/harness/shell/expectations/jsc-trunk.ts @@ -53,11 +53,11 @@ export const jscTrunk: ShellLaneExpectation = { "Trunk drift is a finding to triage, never a gate.", deltas: [], totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/node-pinned.ts b/harness/shell/expectations/node-pinned.ts index 5b0b950..322ab50 100644 --- a/harness/shell/expectations/node-pinned.ts +++ b/harness/shell/expectations/node-pinned.ts @@ -32,11 +32,11 @@ export const nodePinned: ShellLaneExpectation = { "(see this file's header).", deltas: [], totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/sm-nightly.ts b/harness/shell/expectations/sm-nightly.ts index ff35517..18e5c65 100644 --- a/harness/shell/expectations/sm-nightly.ts +++ b/harness/shell/expectations/sm-nightly.ts @@ -41,11 +41,11 @@ export const smNightly: ShellLaneExpectation = { "tail-calls/relaxed-simd), JSPI round trip verified end to end.", deltas: [], totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/sm-pinned.ts b/harness/shell/expectations/sm-pinned.ts index 3728e98..45cf773 100644 --- a/harness/shell/expectations/sm-pinned.ts +++ b/harness/shell/expectations/sm-pinned.ts @@ -22,11 +22,11 @@ export const smPinned: ShellLaneExpectation = { "the sm-nightly canary.", deltas: [], totals: { - commands: 1511, - executed: 1506, - passed: 1468, + commands: 1622, + executed: 1617, + passed: 1593, failed: 0, - xfail: 38, + xfail: 24, pendingRuntime: 0, pendingCapability: 0, unsupportedDirective: 5, diff --git a/harness/shell/expectations/types.ts b/harness/shell/expectations/types.ts index 2283709..08ca072 100644 --- a/harness/shell/expectations/types.ts +++ b/harness/shell/expectations/types.ts @@ -5,7 +5,10 @@ // track's dispatch); only the `lane` tag differs, since these are engine // shells, not browsers. -import type { LaneDelta, LaneTotals } from "../../browser/expectations/types.ts"; +import type { + LaneDelta, + LaneTotals, +} from "../../browser/expectations/types.ts"; export type { LaneDelta, LaneTotals }; export { deltaKey } from "../../browser/expectations/types.ts"; diff --git a/harness/src/wasmtime-expectations.ts b/harness/src/wasmtime-expectations.ts index b877578..ec0c6e7 100644 --- a/harness/src/wasmtime-expectations.ts +++ b/harness/src/wasmtime-expectations.ts @@ -51,6 +51,11 @@ export const WASMTIME_FAILURE_CLASSES: Readonly< reason: "command has no current instance after its owning setup failure", issue: "https://github.com/polymorph-components/polyengine/issues/372", }, + "spec-conflict": { + reason: + "Wasmtime allows an outer-instance thread to progress a nested synchronous lift; the Component Model reference restricts candidates to the synchronous callee instance", + issue: "https://github.com/polymorph-components/polyengine/issues/380", + }, }; export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] = @@ -173,6 +178,18 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] = }, ], }, + { + class: "spec-conflict", + files: [{ + file: "thread-transparency/reentrancy.json", + rows: [{ + lines: [114], + cause: + 'Error: expected trap "wasm `unreachable` instruction executed", got "wasm trap: cannot block a synchronous task before returning"', + status: "failed", + }], + }], + }, { class: "runtime-semantics", files: [ diff --git a/harness/src/xfail.ts b/harness/src/xfail.ts index 5af02de..df67ce9 100644 --- a/harness/src/xfail.ts +++ b/harness/src/xfail.ts @@ -7,45 +7,13 @@ export interface XfailEntry { reason: string; } -const issue248 = - "https://github.com/polymorph-components/polyengine/issues/248"; const issue249 = "https://github.com/polymorph-components/polyengine/issues/249"; export const XFAIL: XfailEntry[] = [ - // The pinned Wasmtime frontend does not yet implement the Component Model - // name-folding conflicts (CM#703/#704); test/nyi.txt lists this file. - ...[ - [150, "`foobar` conflicts with `foo-bar`"], - [155, "`FOOBAR` conflicts with `foo-bar`"], - [160, "`foob-ar` conflicts with `foo-bar`"], - [165, "the static-qualified folded name conflicts"], - [170, "the method-qualified folded name conflicts"], - ].map(([line, detail]) => ({ - file: "validation/kebab.json", - line: line as number, - reason: - `expected assert_invalid because ${detail}, but the pinned frontend validated it; name-rules NYI, ${issue248}`, - })), - - // The pinned frontend likewise lacks CM#688's elem_size < 2^28 rule; - // test/nyi.txt lists this file. Line 64 is the pointer-width boundary case. - ...[26, 32, 38, 44, 49, 58, 64].map((line) => ({ - file: "validation/max-value-size.json", - line, - reason: - `expected assert_invalid (exceeds maximum byte size), but the pinned frontend validated it; max-value-size NYI, ${issue248}`, - })), - // These are current runtime diagnostic/scheduling gaps, not deferred-thread // cascades. The thread built-ins instantiate and execute; each row below was // re-run directly after that support landed. - { - file: "async/during-sync-call-no-sibling-resume.json", - line: 156, - reason: - `guest reaches unreachable instead of returning during the no-sibling-resume schedule; sync scheduling gap, ${issue249}`, - }, ...[46, 48, 50, 52].map((line) => ({ file: "async/self-switch-traps.json", line, @@ -58,12 +26,6 @@ export const XFAIL: XfailEntry[] = [ reason: `operation traps as required, but reports "cannot resume thread" instead of "cannot resume thread which is not suspended"; diagnostic mismatch, ${issue249}`, })), - { - file: "async/trap-if-block-and-sync.json", - line: 316, - reason: - `reports the host driver's deadlock diagnostic instead of the synchronous-callee cannot-block diagnostic; sync scheduling gap, ${issue249}`, - }, ...[328, 330, 332].map((line) => ({ file: "async/trap-if-block-and-sync.json", line, diff --git a/runtime/README.md b/runtime/README.md index 76260a0..39371ec 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -44,7 +44,7 @@ package entry points. The digest entry point is unstable support for generated code, not a hand-written host API; regenerate bindings when updating the runtime. -The loader accepts only `formatVersion: 5`. Translate with a matching toolchain +The loader accepts only `formatVersion: 6`. Translate with a matching toolchain rather than reusing an older plan. See the [plan contract](../contracts/plan-format.md) for the wire format and executor obligations, and [architecture](../docs/architecture.md) for engine support, diff --git a/runtime/deno.json b/runtime/deno.json index 69feffd..e2ff21e 100644 --- a/runtime/deno.json +++ b/runtime/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/runtime", - "version": "0.6.8", + "version": "0.7.0", "exports": { "./cache": "./src/cache/mod.ts", "./plan": "./src/plan/mod.ts", diff --git a/runtime/src/cabi/async_values.ts b/runtime/src/cabi/async_values.ts index 43c76c8..f414270 100644 --- a/runtime/src/cabi/async_values.ts +++ b/runtime/src/cabi/async_values.ts @@ -126,6 +126,7 @@ function liftAsyncValue( // Host-wrapper re-arm hook (contracts/embedder-api.md §"Streams and futures"): the readable // end just left a guest table, so whoever receives it can act on it again. // See `bindOnLower` in exec/host_streams.ts for the retention rule. + (end as { index?: number | null }).index = null; (end.shared as { onLifted?: ((i: unknown) => void) | null }).onLifted?.( inst, ); @@ -183,8 +184,11 @@ export function lowerStream( (inst as unknown as { store?: unknown }).store; (v as { onLowered?: ((i: unknown) => void) | null }).onLowered?.(inst); cx.checkpoint?.(); - const end = new ReadableStreamEnd(v, declared); + // definitions.py:1411-1425 transfers the endpoint itself. Reuse it so an + // idle peer-drop notification survives the interval between lift and lower. + const end = v.claimReadableEnd(declared); const i = inst!.handles.add(end); + end.index = i; cx.preparedCustody?.inserted(inst!.handles, i, end); cx.checkpoint?.(); return i; @@ -221,8 +225,9 @@ export function lowerFuture( (inst as unknown as { store?: unknown }).store; (v as { onLowered?: ((i: unknown) => void) | null }).onLowered?.(inst); cx.checkpoint?.(); - const end = new ReadableFutureEnd(v, declared); + const end = v.claimReadableEnd(declared); const i = inst!.handles.add(end); + end.index = i; cx.preparedCustody?.inserted(inst!.handles, i, end); cx.checkpoint?.(); return i; diff --git a/runtime/src/cabi/handles.ts b/runtime/src/cabi/handles.ts index 788349e..0cda760 100644 --- a/runtime/src/cabi/handles.ts +++ b/runtime/src/cabi/handles.ts @@ -29,6 +29,7 @@ export class Table { array: (T | null)[] = [null]; free: number[] = []; + readonly tenure: number[] = [0]; get(i: number): T { // Indices are u32; a negative i is out of range, and JS `array[-1]` is @@ -44,10 +45,12 @@ export class Table { i = this.free.pop()!; assert_(this.array[i] === null); this.array[i] = e; + this.tenure[i] = (this.tenure[i] ?? 0) + 1; } else { i = this.array.length; trapIf(i > Table.MAX_LENGTH, "table full"); this.array.push(e); + this.tenure.push(1); } return i; } diff --git a/runtime/src/cabi/values.ts b/runtime/src/cabi/values.ts index f8c056b..fb55459 100644 --- a/runtime/src/cabi/values.ts +++ b/runtime/src/cabi/values.ts @@ -31,6 +31,7 @@ interface Acquisition { table?: import("./handles.ts").Table; index?: number; entry?: unknown; + tenure?: number; } export interface PreparedTransfer { @@ -79,6 +80,7 @@ export class PreparedCustody { a.table = table; a.index = index; a.entry = entry; + a.tenure = table.tenure[index]; } delivered(): void { @@ -101,7 +103,10 @@ export class PreparedCustody { if (a.table !== undefined) { // The receiver may legitimately have transferred the handle onward // during reentry. Remove only the exact entry still under our custody. - if (a.table.array[a.index!] !== a.entry) continue; + if ( + a.table.array[a.index!] !== a.entry || + a.table.tenure[a.index!] !== a.tenure + ) continue; a.table.remove(a.index!); } a.cleanup(); diff --git a/runtime/src/embedder/copy.ts b/runtime/src/embedder/copy.ts index 91e1bab..cf8fa15 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.6.8"; +export const RUNTIME_VERSION = "0.7.0"; /** * Compose a cross-copy diagnostic: what was foreign, which copy is speaking, diff --git a/runtime/src/embedder/streams.ts b/runtime/src/embedder/streams.ts index e694e83..ae08ac8 100644 --- a/runtime/src/embedder/streams.ts +++ b/runtime/src/embedder/streams.ts @@ -532,10 +532,14 @@ interface WriterOperation { export class StreamWriter implements ProtocolStreamWriter { #stream: Stream; #active: WriterOperation | null = null; + #closed = false; constructor(stream: Stream) { this.#stream = stream; - stream.onBound(() => this.#launch()); + stream.onBound(() => { + if (this.#closed) hostOf(this.#stream).writable.drop(); + else this.#launch(); + }); // realm boundary realm-local pill (see Stream's constructor above for rationale). defineRealmLocal(this); } @@ -555,6 +559,7 @@ export class StreamWriter implements ProtocolStreamWriter { } #start(run: (op: WriterOperation) => Promise): Promise { + if (this.#closed) return Promise.resolve(0); if (this.#active !== null) { throw new TypeError( "a write is already in flight on this stream's writable end; " + @@ -707,6 +712,13 @@ export class StreamWriter implements ProtocolStreamWriter { /** End-of-stream. */ async close(): Promise { + this.#closed = true; + const op = this.#active; + if (op !== null && !op.started) { + op.cancelled = true; + this.#resolve(op, 0); + } + if (!this.#stream.bound) return; await this.#stream.whenBound(); if (this.#stream.bound) hostOf(this.#stream).writable.drop(); } diff --git a/runtime/src/exec/boundary.ts b/runtime/src/exec/boundary.ts index 308f966..ff40854 100644 --- a/runtime/src/exec/boundary.ts +++ b/runtime/src/exec/boundary.ts @@ -32,7 +32,6 @@ import { import { assert_, AssertionError, Trap } from "../cabi/trap.ts"; import { type BlockRequest, - type Cancelled, componentBoundaryTrapCarrier, type ComponentInstanceState, consumeSchedulerFailure, @@ -195,9 +194,7 @@ export interface ResolvedOptions { postReturn: (() => CoreFn | undefined) | null; callback: (() => CoreFn | undefined) | null; async: boolean; - /** Cancellability for option-indexed built-ins, including - * `canon_waitable_set_wait` / `canon_waitable_set_poll`. Other built-ins - * such as thread.yield carry their flags on the trampoline declaration. */ + /** Legacy format-6 wire slot, validated false by the loader. */ cancellable: boolean; coreType: CoreFuncType; instance: ComponentInstanceState; @@ -475,14 +472,9 @@ function driveLoop( // direct driveSyncLift loop. Share the coordinator's quantum and cross a // platform-task boundary before continuing so guest YIELD cannot starve // timers (scheduler-cycle2.md:9-11). - void handoffWorkQuantum(store, drainState); - return driveAsync( - store, - done, - what, - idle, - onBackgroundFailure, - false, + const handoff = handoffWorkQuantum(store, drainState); + return handoff.then(() => + drive(store, done, what, idle, onBackgroundFailure) ); } } @@ -644,11 +636,17 @@ function serviceRequiredSyncStep( store.finishSyncProgress(point); continue; } - const inst = task?.inst; + const inst = point.syncInstance as typeof task.inst; if (store.serviceSettledStepFor(inst)) { state.syncHopProbe = null; return "progress"; } + if (point.task?.hasPendingCancel?.() === true && point.ready()) { + if (store.tickForInstance(inst)) { + state.syncHopProbe = null; + return "progress"; + } + } const hops = entryHopThreads(store, inst); if (hops.length > 0 || store.pendingResumptions.has(owner)) return "wait"; if (store.tickForInstance(inst)) { @@ -656,12 +654,11 @@ function serviceRequiredSyncStep( return "progress"; } store.finishSyncProgress(point); - point.abandon( - componentBoundaryTrapCarrier( - new Trap("wasm trap: cannot block a synchronous task before returning"), - owner, - ), + const failure = componentBoundaryTrapCarrier( + new Trap("wasm trap: cannot block a synchronous task before returning"), + owner, ); + point.abandon(failure); state.syncHopProbe = null; return "progress"; } @@ -835,9 +832,10 @@ async function runStoreDrain(store: Store, state: DrainState): Promise { if (syncVerdict === "progress") continue; if (syncVerdict === "wait") { const point = store.requiredSyncParks[0]; - const hops = point === undefined - ? [] - : entryHopThreads(store, point.logicalOwner.task?.inst); + const hops = point === undefined ? [] : entryHopThreads( + store, + point.syncInstance as typeof point.logicalOwner.task.inst, + ); const blockers = point !== undefined && store.pendingResumptions.has(point.owner) ? [...hops, point.owner] @@ -913,6 +911,7 @@ async function runStoreDrain(store: Store, state: DrainState): Promise { // step. Admission gets a turn before another ordinary tick, so a // perpetual callback YIELD loop cannot starve a pending sync export. store.refreshSyncRequirements(); + if (store.requiredSyncParks.length > 0) break; if (serviceAdmissionStep(store, state)) break; if (chargeWorkQuantum(state)) { await handoffWorkQuantum(store, state); @@ -1335,7 +1334,7 @@ export function createLiftedFunction(input: { terminalFailed = true; terminalCause = e; if ( - task.state === "initial" && thread.waiting() && thread.cancellable + task.state === "initial" && thread.waiting() ) { thread.abandonWaiting(); } @@ -1552,8 +1551,11 @@ function entryHopThreads( if (store.awaiting.size === 0) return []; const suspended = new Set(); for (const w of store.waiting) { - const owner = (w as { owner?: unknown }).owner; - if (owner !== undefined && owner !== null) suspended.add(owner); + const point = w as { owner?: unknown; boundaryReturned?: boolean }; + if ( + point.owner !== undefined && point.owner !== null && + point.boundaryReturned === true + ) suspended.add(point.owner); } const out: { awaiting: Promise | null }[] = []; for (const t of store.awaiting) { @@ -1736,7 +1738,6 @@ export function* awaitCore( if (raw.length === 1 && isPromiseLike(raw[0])) { const settled = yield { readyFunc: null, - cancellable: false, // Map post-resumption RuntimeError just like a synchronous core throw. awaitValue: Promise.resolve(raw[0] as unknown as Promise).then( undefined, @@ -1798,7 +1799,7 @@ function* liftBody(input: { prepared: PreparedTransfer | null; admissionCheckpoint: () => void; preserveWasmException?: boolean; -}): Generator { +}): Generator { const { name, ft, opts, core, stats, task } = input; const thread = input.thread(); const inst = opts.instance; @@ -2128,7 +2129,6 @@ export function createLoweredImport(input: { store, task: currentTask(), readyFunc: () => outcome !== undefined, - cancellable: false, blockReason: "host-import", produce: () => { // Poisoning records a marker; it need not abandon this suspension. @@ -2272,7 +2272,7 @@ export function* runCallbackLoop(input: { callback: CoreFn; packed: number; stats: ExecutionStats; -}): Generator { +}): Generator { const { name, task, thread, inst, callback, stats } = input; let [code, si] = unpackCallbackResult(input.packed); @@ -2284,17 +2284,28 @@ export function* runCallbackLoop(input: { inst.exclusiveThread === task.implicitThread, "callback loop without holding the exclusive thread", ); + if (task.deliverPendingCancel()) { + stats.callbackInvocations++; + const [next] = normalizeCoreValues( + yield* awaitCore( + callback, + [EventCode.TASK_CANCELLED, 0, 0], + thread, + ), + ["i32"], + `${name} callback result`, + ) as [number]; + [code, si] = unpackCallbackResult(next); + continue; + } // Admit other needs-exclusive tasks between invocations. Event delivery // and cancellation wait for the slot to be free before reclaiming it. inst.exclusiveThread = null; let event: EventTuple; switch (code) { case CallbackCode.YIELD: { - const cancelled = yield* thread.waitUntil( - () => inst.exclusiveThread === null, - true, - ); - event = cancelled + yield* thread.waitUntil(() => inst.exclusiveThread === null); + event = task.deliverPendingCancel() ? [EventCode.TASK_CANCELLED, 0, 0] : [EventCode.NONE, 0, 0]; break; @@ -2305,10 +2316,10 @@ export function* runCallbackLoop(input: { !(wset instanceof WaitableSet), `callback returned WAIT with index ${si}, which is not a waitable set`, ); - event = yield* (wset as WaitableSet).waitForEventAnd( + event = yield* (wset as WaitableSet).waitForCallbackEvent( thread, + task, () => inst.exclusiveThread === null, - true, ); break; } diff --git a/runtime/src/intrinsics/async_builtins.ts b/runtime/src/intrinsics/async_builtins.ts index 202990e..7776c44 100644 --- a/runtime/src/intrinsics/async_builtins.ts +++ b/runtime/src/intrinsics/async_builtins.ts @@ -18,7 +18,6 @@ import { blockCurrentActivation } from "../jspi/mod.ts"; import type { SuspensionMode } from "../jspi/mod.ts"; -import type { Cancelled } from "../task/mod.ts"; import { assert_, trap, trapIf } from "../cabi/trap.ts"; import { CoreValueIter, @@ -206,8 +205,8 @@ export function createWaitableSetNew(inst: ComponentInstanceState): CoreFn { /** * definitions.py `canon_waitable_set_wait`. Returns a pending event directly, - * or suspends the calling wasm frame using JSPI until an event or cancellable - * task cancellation arrives. Plain mode cannot perform the blocking case. + * or suspends the calling wasm frame using JSPI until an event arrives. Plain + * mode cannot perform the blocking case. */ export function createWaitableSetWait( decl: { options: number }, @@ -216,8 +215,6 @@ export function createWaitableSetWait( mode: SuspensionMode = "plain", ): CoreFn { const opts = ctx.options(decl.options); - // `cancellable` is a canonical option, not a trampoline field. - const cancellable = opts.cancellable; return (si?: number, ptr?: number) => { // Guest-supplied index/pointer are u32; core wasm delivers i32 args // signed. Normalize at the entry boundary. @@ -230,9 +227,7 @@ export function createWaitableSetWait( const wset = requireWaitableSet(inst, si, "waitable-set.wait"); const task = currentTask() as Task; let event: EventTuple; - if (task.deliverPendingCancel(cancellable)) { - event = [EventCode.TASK_CANCELLED, 0, 0]; - } else if (wset.hasPendingEvent()) { + if (wset.hasPendingEvent()) { // No waiter count is needed for immediate delivery: no other thread can // observe the reference's increment/decrement bracket without a yield. traceCopy(`waitable-set.wait si=${si} FAST (pending event)`); @@ -248,11 +243,8 @@ export function createWaitableSetWait( store: inst.store, task, readyFunc: () => wset.hasPendingEvent(), - cancellable, - produce: (cancelled: Cancelled) => { - const ev: EventTuple = cancelled - ? [EventCode.TASK_CANCELLED, 0, 0] - : wset.getPendingEvent(); + produce: () => { + const ev: EventTuple = wset.getPendingEvent(); return unpackEvent(opts, inst, ptr, ev); }, onSettled: () => { @@ -277,8 +269,6 @@ export function createWaitableSetPoll( inst: ComponentInstanceState, ): CoreFn { const opts = ctx.options(decl.options); - /** See `createWaitableSetWait`: `cancellable` is an option, not a decl field. */ - const cancellable = opts.cancellable; return (si?: number, ptr?: number) => { si = (si ?? 0) >>> 0; ptr = (ptr ?? 0) >>> 0; @@ -287,7 +277,7 @@ export function createWaitableSetPoll( "waitable-set.poll: cannot leave component instance", ); const wset = requireWaitableSet(inst, si, "waitable-set.poll"); - const event = wset.poll(currentTask(), cancellable); + const event = wset.poll(currentTask()); return unpackEvent(opts, inst, ptr, event); }; } @@ -475,21 +465,22 @@ export function createSubtaskCancel( if (mode !== "jspi") { if (st.resolved()) return finish(); - if (!async_) { - needsJspi( - "synchronous subtask.cancel whose callee did not resolve " + - "immediately (the calling wasm frame must block)", - ); - } - return BLOCKED; + if (async_) return BLOCKED; + needsJspi( + "synchronous subtask.cancel whose callee did not resolve " + + "immediately (the calling wasm frame must block)", + ); } - if (!ready()) { + // definitions.py:2366-2377: both forms relinquish one turn whenever + // on_cancel leaves the subtask unresolved. The async form uses yield + // even when the host callee is already determinate, then rechecks and + // returns BLOCKED only after resumption. + if (!ready() || (async_ && !st.resolved())) { parked = true; return blockCurrentActivation({ store: inst.store, task: currentTask(), - readyFunc: ready, - cancellable: false, + readyFunc: async_ ? () => true : ready, // First finish the exact cancelled callee's pending JSPI // continuation. Only after that canonical return/park boundary can // the synchronous form's unresolved wait become CM blocking. @@ -497,6 +488,10 @@ export function createSubtaskCancel( syncBlockWhen: () => !async_ && determinate() && !calleeContinuationPending() && !st.resolved(), + // The cancellation callback belongs to the cancelled callee. It + // must run before the caller's synchronous wait can be judged + // unprogressable (definitions.py:2366-2375). + syncInstance: st.calleeTask?.inst ?? inst, produce: () => { st.hasSyncWaiter = false; return st.resolved() ? finish() : BLOCKED; @@ -528,11 +523,10 @@ export function createSubtaskCancel( * *built-in* form needs a suspendable stack. */ export function createThreadYield( - decl: { cancellable?: boolean }, + _decl: object, mode: SuspensionMode = "plain", declaredInst?: ComponentInstanceState, ): CoreFn { - const cancellable = decl.cancellable === true; return () => { trapIf( declaredInst !== undefined && !declaredInst.mayLeave, @@ -543,12 +537,9 @@ export function createThreadYield( !thread.task.inst.mayLeave, "thread.yield: cannot leave component instance", ); - // A pending cancellation is deliverable without suspending at all - // (definitions.py `Thread.yield_` -> `wait_until` -> `deliver_pending_cancel`). - if (thread.task.deliverPendingCancel(cancellable)) return 1; if (mode === "jspi") { // definitions.py `Thread.yield_` is - // `wait_until(lambda: True, cancellable)`: immediately ready, but it + // `wait_until(lambda: True)`: immediately ready, but it // goes through the scheduler, so other threads get a turn first. A // suspension point with an always-true `readyFunc` is exactly that -- // `Store.tick` selects it under the configured scheduling policy. @@ -556,8 +547,7 @@ export function createThreadYield( store: thread.task.inst.store, task: thread.task, readyFunc: () => true, - cancellable, - produce: (cancelled: Cancelled) => (cancelled ? 1 : 0), + produce: () => 0, }) as unknown as number; } needsJspi( diff --git a/runtime/src/intrinsics/fact_calls.ts b/runtime/src/intrinsics/fact_calls.ts index a64614e..cf8323d 100644 --- a/runtime/src/intrinsics/fact_calls.ts +++ b/runtime/src/intrinsics/fact_calls.ts @@ -55,7 +55,6 @@ import { MAX_FLAT_RESULTS } from "../cabi/mod.ts"; import type { CoreValue, FuncType, ValType } from "../cabi/types.ts"; import { type BlockRequest, - type Cancelled, type ComponentInstanceState, currentTask, dbgId, @@ -329,7 +328,7 @@ function mkCalleeTask(input: { }; }): { task: Task; - body: (t: Thread) => Generator; + body: (t: Thread) => Generator; } { const { prepared, callee, callback, postReturn, ctx, calleeUsesAsyncAbi } = input; @@ -413,7 +412,7 @@ function mkCalleeTask(input: { const body = function* ( thread: Thread, - ): Generator { + ): Generator { if (!(yield* task.enterImplicitThread(thread))) return; const calleeArgs = task.start(); traceCopy(`mkCalleeTask callee canBlock=${canBlock} mode=${mode}`); @@ -554,8 +553,9 @@ export function createSyncStartCall( ); if (refusal !== null) trap(refusal); } + let thread: Thread; try { - const thread = spawn(task, body); + thread = spawn(task, body); thread.resume(); } catch (e) { // A trap poisons the callee instance. A *capability signal* does not — @@ -584,7 +584,6 @@ export function createSyncStartCall( store: prepared.callerInst.store, task: currentTask(), readyFunc: () => callerResults !== null, - cancellable: false, produce: () => { lenderScope.releaseLenders(); return shapeResults(callerResults as CoreValue[] | null); @@ -812,7 +811,6 @@ export function createAsyncStartCall( store: prepared.callerInst.store, task: currentTask(), readyFunc: determinate, - cancellable: false, // Determining async-start status first finishes the named callee's // mandatory entry continuation. It is not itself a synchronous // Component Model wait by the caller. @@ -838,11 +836,11 @@ export function createAsyncStartCall( /** Create a thread whose body needs a reference to the thread itself. */ function spawn( task: Task, - body: (t: Thread) => Generator, + body: (t: Thread) => Generator, ): Thread { // Forward reference: the generator body only runs once `thread` below // is assigned (spawn returns before the body executes). - function* threadBody(): Generator { + function* threadBody(): Generator { yield* body(thread); } const thread: Thread = new Thread(task, threadBody()); diff --git a/runtime/src/intrinsics/mod.ts b/runtime/src/intrinsics/mod.ts index b9c8b14..798d1b9 100644 --- a/runtime/src/intrinsics/mod.ts +++ b/runtime/src/intrinsics/mod.ts @@ -125,6 +125,15 @@ const FACT_TRAP_MESSAGES: Record = { 33: "unaligned pointer", 46: "reference count overflow", 49: "uncaught exception propagated out of component", + 50: + "cannot read from stream after being notified that the writable end dropped", + 51: + "cannot write to stream after being notified that the readable end dropped", + // Upstream trap_encoding.rs says "stream" here despite naming a future; + // use the operation's native meaning rather than copying that typo. + 52: + "cannot write to future after previous write succeeded or readable end dropped", + 53: "cannot lift stream after being notified that the writable end dropped", }; export { UnsupportedFeatureError } from "./errors.ts"; @@ -399,26 +408,23 @@ function createTrampolineBody( } // FACT sync-call borrow brackets (wasmtime-environ `fact.rs`): - // async.enter-sync-call(caller_instance: i32, async: i32, - // callee_instance: i32) -> () + // async.enter-sync-call(async: i32, callee_instance: i32) -> () // async.exit-sync-call() -> () case "enter-sync-call": - return ( - callerInstance?: number, - async_?: number, - calleeInstance?: number, - ) => { + return (async_?: number, calleeInstance?: number) => { + // wasmtime cc546ee component.rs:104 and fact.rs:730-738. The caller is + // the current logical activation, no longer an adapter argument. + const caller = maybeCurrentThread(); + const callerInst = caller?.task.inst; // Reentrance is valid. `entryRefusal` enforces per-instance poisoning, // a runtime divergence, and reports the original trap. if ( - typeof callerInstance === "number" && typeof calleeInstance === "number" ) { - const callerInst = ctx.componentInstance(callerInstance >>> 0); const calleeInst = ctx.componentInstance(calleeInstance >>> 0); const refusal = entryRefusal( calleeInst, - callerInst, + callerInst ?? null, "cannot enter component instance", ); if (refusal !== null) trap(refusal); @@ -430,7 +436,6 @@ function createTrampolineBody( // Capture before a possible admission park. The store later calls // `produce` without the caller's synchronous bracket, but the nested // activation remains a logical child of this canonical caller. - const caller = maybeCurrentThread(); // Normal return must match enter/exit on the same activation's stack; // trap unwind releases any scopes whose exit was skipped. const enter = () => { @@ -473,7 +478,6 @@ function createTrampolineBody( // CONTRACT: this is the already-running caller's synchronous wait, // not a new callee task's `enter_implicit_thread` wait // (definitions.py:458-465). It cannot consume caller cancellation. - cancellable: false, produce: () => { const activation = enter(); if (activation !== undefined) { @@ -627,7 +631,7 @@ function createTrampolineBody( ); case "thread-yield": return createThreadYield( - decl as unknown as { cancellable?: boolean }, + decl, ctx.suspensionMode, declaredInstance(decl, ctx), ); diff --git a/runtime/src/intrinsics/stream_builtins.ts b/runtime/src/intrinsics/stream_builtins.ts index c4ca407..270d174 100644 --- a/runtime/src/intrinsics/stream_builtins.ts +++ b/runtime/src/intrinsics/stream_builtins.ts @@ -98,8 +98,12 @@ export function createStreamNew( return () => { trapIf(!inst.mayLeave, "stream.new: cannot leave component instance"); const shared = new SharedStreamImpl(ctx.streamElem(decl.streamTable)); - const ri = inst.handles.add(new ReadableStreamEnd(shared, shared.t)); - const wi = inst.handles.add(new WritableStreamEnd(shared, shared.t)); + const reader = new ReadableStreamEnd(shared, shared.t); + const writer = new WritableStreamEnd(shared, shared.t); + const ri = inst.handles.add(reader); + const wi = inst.handles.add(writer); + reader.index = ri; + writer.index = wi; return packEnds(ri, wi); }; } @@ -112,8 +116,12 @@ export function createFutureNew( return () => { trapIf(!inst.mayLeave, "future.new: cannot leave component instance"); const shared = new SharedFutureImpl(ctx.futureElem(decl.futureTable)); - const ri = inst.handles.add(new ReadableFutureEnd(shared, shared.t)); - const wi = inst.handles.add(new WritableFutureEnd(shared, shared.t)); + const reader = new ReadableFutureEnd(shared, shared.t); + const writer = new WritableFutureEnd(shared, shared.t); + const ri = inst.handles.add(reader); + const wi = inst.handles.add(writer); + reader.index = ri; + writer.index = wi; return packEnds(ri, wi); }; } @@ -148,6 +156,7 @@ function streamCopy(input: { const e = inst.handles.get(i); trapIf(!(e instanceof EndT), "stream copy: wrong end type for this handle"); const end = e as ReadableStreamEnd | WritableStreamEnd; + end.index = i; trapIf(!valTypeEqual(end.elem, elem), "stream copy: element type mismatch"); // wasmtime distinguishes the two non-IDLE states in its message, and the // suite asserts the exact text: DONE means the other end has gone away (or @@ -183,11 +192,11 @@ function streamCopy(input: { ): EventTuple => { reclaim(); assert_(end.copying(), "stream event on a non-copying end"); - // CONTRACT: WebAssembly/component-model#719 at head 35e9769957627c2: + // CONTRACT: current definitions.py End.copy delivery rules: // stream drop is observed when the pending event is consumed, not when it // is armed. Preserve all progress accumulated before delivery. This runs - // before takeCancelEvent's CM-3 COMPLETED -> CANCELLED remap, so peer drop - // has precedence and remains DROPPED. + // before the reference's COMPLETED -> CANCELLED remap on cancellation, so + // peer drop has precedence and remains DROPPED. const delivered = end.shared.dropped ? CopyResult.DROPPED : result; end.state = delivered === CopyResult.DROPPED ? CopyState.DONE @@ -201,7 +210,8 @@ function streamCopy(input: { delivered >= 0 && delivered < 2 ** 4, "stream event: packed result out of 4-bit range", ); - return [eventCode, i, (delivered | (buffer.progress << 4)) >>> 0]; + assert_(end.index !== null, "stream event without owner index"); + return [eventCode, end.index, (delivered | (buffer.progress << 4)) >>> 0]; }; end.state = CopyState.COPYING; @@ -240,6 +250,7 @@ function futureCopy(input: { const e = inst.handles.get(i); trapIf(!(e instanceof EndT), "future copy: wrong end type for this handle"); const end = e as ReadableFutureEnd | WritableFutureEnd; + end.index = i; trapIf(!valTypeEqual(end.elem, elem), "future copy: element type mismatch"); // Writable DONE covers either a completed write or a dropped readable end. trapIf( @@ -266,7 +277,7 @@ function futureCopy(input: { "future event/progress disagreement", ); assert_(end.copying(), "future event on a non-copying end"); - // CONTRACT: WebAssembly/component-model#719 at head 35e9769957627c2: + // CONTRACT: current definitions.py FutureEnd.copy delivery rules: // transferred payload makes COMPLETED final even if the peer later drops; // only an undelivered CANCELLED is upgraded when its peer is gone. const delivered = result === CopyResult.CANCELLED && end.shared.dropped @@ -277,7 +288,8 @@ function futureCopy(input: { delivered === CopyResult.COMPLETED ? CopyState.DONE : CopyState.IDLE; - return [eventCode, i, delivered]; + assert_(end.index !== null, "future event without owner index"); + return [eventCode, end.index, delivered]; }; end.state = CopyState.COPYING; @@ -347,7 +359,6 @@ function finishCopy( store: inst.store, task: currentTask(), readyFunc: () => end.hasPendingEvent(), - cancellable: false, produce: () => { end.hasSyncWaiter = false; const p = take(); @@ -379,7 +390,7 @@ function finishCopy( /** * `cancel_copy`'s reporting tail, shared by its immediate and blocking exits. - * Both exits apply the CM-3 completion-superseding exception below. + * Both exits apply the current stream completion-superseding rule below. */ function takeCancelEvent( end: CopyEnd, @@ -392,13 +403,9 @@ function takeCancelEvent( !end.copying() && code === eventCode && index === i, `unexpected event delivered by ${what}`, ); - // CM-3 exception (upstream-component-model-repo-findings.md): adopt the - // corpus/wasmtime semantics pending upstream adjudication, rather than - // definitions.py `cancel_copy`'s verbatim pending event. An undelivered - // stream COMPLETED becomes CANCELLED with the same element count; - // DROPPED and future COMPLETED remain unchanged. See - // `test/async/big-interleaving-test.wast` and wasmtime's - // `futures_and_streams.rs` cancellation handling; docs/architecture.md §1. + // Current definitions.py `cancel_copy`: an undelivered stream COMPLETED + // becomes CANCELLED with the same element count; DROPPED and future + // COMPLETED remain unchanged. const isStreamEvent = eventCode === EventCode.STREAM_READ || eventCode === EventCode.STREAM_WRITE; if (isStreamEvent && (payload & 0xf) === CopyResult.COMPLETED) { @@ -410,7 +417,7 @@ function takeCancelEvent( return payload; } -/** definitions.py `cancel_copy`, with the CM-3 exception in takeCancelEvent. */ +/** Current definitions.py `cancel_copy`. */ function cancelCopy(input: { EndT: EndCtor; eventCode: EventCode; @@ -448,7 +455,6 @@ function cancelCopy(input: { store: inst.store, task: currentTask(), readyFunc: () => end.hasPendingEvent(), - cancellable: false, produce: () => takeCancelEvent(end, eventCode, i, what), }) as unknown as number; } @@ -862,11 +868,13 @@ function transferAsyncEnd(input: { end.inWaitableSet(), `cannot lift ${what} while it's in a waitable set`, ); - const Ctor = EndT as unknown as new ( - shared: unknown, - elem: ValType | null, - ) => CopyEnd; - return dstInst.handles.add(new Ctor(end.shared, dstElem)); + // definitions.py:1411-1425 transfers the endpoint itself, including any + // idle-drop notification already attached to that waitable. + end.elem = dstElem; + end.index = null; + const dstIdx = dstInst.handles.add(end); + end.index = dstIdx; + return dstIdx; }); } diff --git a/runtime/src/intrinsics/thread_builtins.ts b/runtime/src/intrinsics/thread_builtins.ts index 562b262..45cdf60 100644 --- a/runtime/src/intrinsics/thread_builtins.ts +++ b/runtime/src/intrinsics/thread_builtins.ts @@ -7,7 +7,6 @@ import type { CoreValue } from "../cabi/types.ts"; import { awaitCore, type CoreFn } from "../exec/boundary.ts"; import { blockCurrentActivation, type SuspensionMode } from "../jspi/mod.ts"; import { - type Cancelled, currentThreadExactlyForInstance, needsJspi, Thread, @@ -21,7 +20,7 @@ export interface ThreadTrampolineContext { enterThreadFunction(fn: CoreFn): CoreFn; } -type ThreadDecl = { instance: number; cancellable?: boolean }; +type ThreadDecl = { instance: number }; // Generated from the checked WAT below. `ref.test` checks a nominal final // reference without executing the target. It covers the canonical final types @@ -237,7 +236,6 @@ function createThreadPark( targetKind: TargetKind, ): CoreFn { const inst = declaredInst(decl, ctx); - const cancellable = decl.cancellable === true; return (index?: number) => { requireMayLeave(inst, `thread.${park}`); const caller = currentThreadExactlyForInstance(inst); @@ -249,8 +247,6 @@ function createThreadPark( trapIf(!target.explicitlySuspended(), "cannot resume thread"); } } - // definitions.py validates target/self/state before cancellation delivery. - if (caller.task.deliverPendingCancel(cancellable)) return 1; if (ctx.suspensionMode !== "jspi") { needsJspi( `thread.${park}${targetKind === "none" ? "" : `-then-${targetKind}`}`, @@ -264,9 +260,8 @@ function createThreadPark( store: inst.store, task: caller.task, readyFunc: park === "yield" ? () => true : null, - cancellable, explicitSuspend: park === "suspend", - produce: (cancelled: Cancelled) => cancelled ? 1 : 0, + produce: () => 0, }); if (resumeTarget) { // The caller's canonical state is now published. Transfer directly to diff --git a/runtime/src/jspi/bridge.ts b/runtime/src/jspi/bridge.ts index 527b241..010735a 100644 --- a/runtime/src/jspi/bridge.ts +++ b/runtime/src/jspi/bridge.ts @@ -55,7 +55,6 @@ import { withActivation, } from "../task/mod.ts"; import type { - Cancelled, CurrentThreadLike, RequiredSyncPark, SchedulableThread, @@ -461,6 +460,8 @@ export class SuspensionPoint readonly #syncBlockWhen?: () => boolean; readonly #explicitSuspend: boolean; #explicitReady = false; + /** Instance whose ready threads the enclosing synchronous operation drives. */ + syncInstance: unknown; constructor( store: Store, @@ -468,16 +469,8 @@ export class SuspensionPoint readonly task: any, /** Resumable once this holds; `null` = only an explicit resume. */ readonly readyFunc: (() => boolean) | null, - /** - * definitions.py `Thread.cancellable` — set for the duration of this - * park, cleared when the point resumes (parity with task/thread.ts's - * `Thread.cancellable`, which the reference evaluates as a live - * predicate: a point that is no longer parked is never a - * `request_cancellation` candidate). - */ - public cancellable: boolean, /** Produces the value to hand back to wasm at resume time. */ - private readonly produce: (cancelled: Cancelled) => T, + private readonly produce: () => T, // deno-lint-ignore no-explicit-any owner?: any, logicalOwner?: CurrentThreadLike | null, @@ -508,6 +501,7 @@ export class SuspensionPoint | "mandatory-continuation" = "component-model", explicitSuspend = false, syncBlockWhen?: () => boolean, + syncInstance?: unknown, ) { this.#store = store; this.owner = owner ?? maybeCurrentThread() ?? task?.implicitThread ?? null; @@ -515,6 +509,7 @@ export class SuspensionPoint this.blockReason = blockReason; this.#syncBlockWhen = syncBlockWhen; this.#explicitSuspend = explicitSuspend; + this.syncInstance = syncInstance; if (this.logicalOwner !== null && "activePark" in this.logicalOwner) { (this.logicalOwner as Thread).activePark = this; } @@ -541,17 +536,7 @@ export class SuspensionPoint if (this.#done) return false; if (this.#explicitReady) return true; if (this.readyFunc !== null && this.readyFunc()) return true; - // definitions.py `ready_or_cancelled` (`Thread.wait_until` line 369), - // ported to task/thread.ts:waitUntil: a cancel that arrived while this - // task was not cancellable (parked as `pending-cancel`) makes the block - // point ready on its own, otherwise the wakeup is lost until some - // unrelated event happens to satisfy `readyFunc` — possibly never. - // Only the implicit callback thread is gated by callback exclusivity. - // Explicit sibling threads remain cancellation candidates independently. - const lockAvailable = this.logicalOwner === this.task?.implicitThread - ? this.task.implicitThreadCancellable() === true - : true; - return this.cancellable && this.#taskHasPendingCancel() && lockAvailable; + return false; } explicitlySuspended(): boolean { @@ -568,7 +553,22 @@ export class SuspensionPoint const semanticBlock = this.blockReason === "component-model" || (this.blockReason === "mandatory-continuation" && this.#syncBlockWhen?.() === true); - if (semanticBlock && logicalTask?.ft?.async === false && rootLive) { + let syncOwner = this.logicalOwner ?? undefined; + while (syncOwner !== undefined && syncOwner.task?.ft?.async !== false) { + syncOwner = + (syncOwner as CurrentThreadLike & { parent?: CurrentThreadLike }) + .parent; + } + if ( + semanticBlock && syncOwner !== undefined && rootLive + ) { + // CONTRACT: the sync lift services candidates in its callee instance + // (definitions.py:2102-2106). Operations such as synchronous + // subtask.cancel may instead name a child instance whose pending + // continuation must run first (definitions.py:2366-2375). + if (this.syncInstance === undefined) { + this.syncInstance = syncOwner.task?.inst; + } this.#store.requireSyncProgress(this as unknown as RequiredSyncPark); } } @@ -582,25 +582,9 @@ export class SuspensionPoint this.#store.requestService(); } - /** - * `task` is untyped here and some parks carry a stub (no `Task` at all — - * instantiation-time built-ins, and the tests that stand in for them), so - * both cancel hooks are feature-detected. No task, no pending cancel. - */ - #taskHasPendingCancel(): boolean { - return typeof this.task?.hasPendingCancel === "function" && - this.task.hasPendingCancel() === true; - } - /** Settle the import's Promise; the engine resumes the wasm activation. */ - resume(cancelled: Cancelled = false): void { + resume(): void { assert_(!this.#done, "resume of an already-resumed suspension point"); - // Mirrors task/thread.ts:187-190 (definitions.py:367 `Thread.resume`): - // a cancelled resume is only legal at a cancellable block point (#93). - assert_( - this.cancellable || !cancelled, - "cancelled resume of a non-cancellable suspension point", - ); if (SP_TRACE) { console.error( `[sp] resume ${dbgId(this)} owner=${dbgId(this.owner)}\n${ @@ -609,20 +593,9 @@ export class SuspensionPoint ); } this.#done = true; - // AFTER the block (definitions.py `Thread.wait_until` line 372, ported to - // task/thread.ts:waitUntil): a plain wakeup taken through the - // pending-cancel disjunct in `ready()` becomes a cancelled resume, and - // that delivery wins over any event that became pending meanwhile. - if ( - typeof this.task?.deliverPendingCancel === "function" && - this.task.deliverPendingCancel(this.cancellable) === true - ) { - cancelled = true; - } - this.cancellable = false; this.#store.stopWaiting(this); try { - this.#resumeInner(cancelled); + this.#resumeInner(); } finally { // Terminal state reached, by whichever of the two paths below. See // `onSettled`: this is the backstop, not the primary cleanup site, so @@ -632,10 +605,10 @@ export class SuspensionPoint } } - #resumeInner(cancelled: Cancelled): void { + #resumeInner(): void { let value: T; try { - value = this.produce(cancelled); + value = this.produce(); } catch (e) { // A trap computed at resume time (e.g. the event turned out to be a // trapping one) must reach the guest as a rejection of the import's @@ -748,8 +721,7 @@ export function blockCurrentActivation(input: { // deno-lint-ignore no-explicit-any task: any; readyFunc: (() => boolean) | null; - cancellable: boolean; - produce: (cancelled: Cancelled) => T; + produce: () => T; /** * Optional `finally`-style hook — see `SuspensionPoint.onSettled`. Use it * for state that must be discharged however the park ends, including the @@ -764,6 +736,8 @@ export function blockCurrentActivation(input: { /** A mandatory continuation can become a genuine CM wait after its named * dependency reaches a canonical return/park boundary. */ syncBlockWhen?: () => boolean; + /** Override the instance driven while this synchronous operation is parked. */ + syncInstance?: unknown; /** This park is the resumable state of a `thread.suspend*` operation. */ explicitSuspend?: boolean; }): Promise { @@ -799,7 +773,6 @@ export function blockCurrentActivation(input: { input.store, input.task, input.readyFunc, - input.cancellable, input.produce, owner, logicalOwner, @@ -807,6 +780,7 @@ export function blockCurrentActivation(input: { input.blockReason, input.explicitSuspend, input.syncBlockWhen, + input.syncInstance, ); Promise.resolve().then(() => { point.boundaryReturned = true; diff --git a/runtime/src/plan/loader.ts b/runtime/src/plan/loader.ts index 39b9065..71ac850 100644 --- a/runtime/src/plan/loader.ts +++ b/runtime/src/plan/loader.ts @@ -68,7 +68,7 @@ export class TranslateError extends Error { * from being interpreted with different wire semantics. * @internal */ -export const SUPPORTED_FORMAT_VERSION = 5; +export const SUPPORTED_FORMAT_VERSION = 6; /** * A types-table entry after conversion. @@ -521,6 +521,28 @@ function validateTrampoline(t: unknown, where: string): void { const tr = t as Record; expectString(tr, "kind", where); expectNumber(tr, "index", where); + if ( + typeof tr.kind === "string" && + [ + "thread-suspend", + "thread-yield", + "thread-suspend-then-resume", + "thread-yield-then-resume", + "thread-suspend-then-promote", + "thread-yield-then-promote", + ].includes(tr.kind) + ) { + expectNumber(tr, "instance", where); + expectBoolean(tr, "cancellable", where); + // CONTRACT: `.cancellable` is a false-only legacy wire slot (contracts/ + // plan-format.md); see the same rule on canonical options below. + expect( + tr.cancellable === false, + where, + `.cancellable=true is unsupported by this runtime`, + ); + return; + } switch (tr.kind) { case "lower-import": expectNumber(tr, "lowered", where); @@ -569,6 +591,15 @@ function validateCanonicalOptions(o: unknown, where: string): void { expectNumberOrNull(co, "callback", where); expectBoolean(co, "async", where); expectBoolean(co, "cancellable", where); + // CONTRACT: `.cancellable` is a false-only legacy wire slot (contracts/ + // plan-format.md); the merged Component Model reference removed builtin + // cancellability. Accept false plans only; executing true would resurrect + // semantics no longer specified. + expect( + co.cancellable === false, + where, + `.cancellable=true is unsupported by this runtime`, + ); expect(isRecord(co.coreType), where, `.coreType must be an object`); const ct = co.coreType as Record; expectArray(ct, "params", `${where}.coreType`); diff --git a/runtime/src/task/mod.ts b/runtime/src/task/mod.ts index 9dad920..25d718e 100644 --- a/runtime/src/task/mod.ts +++ b/runtime/src/task/mod.ts @@ -7,18 +7,11 @@ import type { ComponentInstanceLike } from "../cabi/context.ts"; import type { ComponentValue, FuncType } from "../cabi/types.ts"; import { assert_, trapIf } from "../cabi/trap.ts"; import { - type Cancelled, - CANCELLED_TRUE, - chooseCandidate, claimActivationAmbient, type CurrentThreadLike, dbgId, - isInstancePoisoned, isSynchronousAmbient, maybeCurrentThread, - NeedsJspi, - notifyInstancePoisoned, - PendingCapability, physicalOwnerOf, popLogicalActivation, pushLogicalActivation, @@ -200,7 +193,7 @@ export class Task { */ *enterImplicitThread( thread: Thread, - ): Generator { + ): Generator { assert_(this.state === "initial", "enter_implicit_thread after start"); this.implicitThread = thread; if (this.ft.async === true) { @@ -212,13 +205,12 @@ export class Task { // too, even if backpressure has since cleared. if (hasBackpressure() || this.inst.numWaitingToEnter > 0) { this.inst.numWaitingToEnter += 1; - let cancelled: Cancelled; try { - cancelled = yield* thread.waitUntil(() => !hasBackpressure(), true); + yield* thread.waitUntil(() => !hasBackpressure()); } finally { this.inst.numWaitingToEnter -= 1; } - if (cancelled) { + if (this.deliverPendingCancel()) { this.cancel(); return false; } @@ -296,9 +288,8 @@ export class Task { } /** - * definitions.py `Task.request_cancellation`. Delivered to a - * cancellable thread if one exists; otherwise recorded as pending, to be - * picked up at the next cancellable block point (`deliverPendingCancel`). + * definitions.py `Task.request_cancellation`. Cancellation is recorded as + * pending; startup admission and the callback loop consume it explicitly. * * `caller` is retained for the call-site shape (fact_calls.ts's * `subtask.onCancel`) and for diagnostics; no condition here consults it @@ -307,80 +298,17 @@ export class Task { requestCancellation(caller: ComponentInstanceState | null): void { void caller; if (this.state === "initial") { - this.state = "cancel-delivered"; - this.implicitThread!.resume(CANCELLED_TRUE); + // definitions.py:462-470 records cancellation before resuming startup; + // builtin/thread cancellability no longer participates in selection. + this.state = "pending-cancel"; + this.implicitThread!.resumeStartupCancellation(); return; } assert_( this.state === "started", `request_cancellation in state ${this.state}`, ); - // Include JSPI SuspensionPoints: their owning generator waits on a - // non-cancellable awaitValue, while the actual cancellable park is in - // store.waiting. Resume delivers the flag to that point's produce callback. - type Cancellable = { - cancellable: boolean; - resume(cancelled?: boolean): void; - }; - let candidates: Cancellable[] = this.threads.filter((t) => t.cancellable); - const excludeImplicit = !this.implicitThreadCancellable(); - if (excludeImplicit) { - candidates = candidates.filter((t) => t !== this.implicitThread); - } - // Explicit-thread SuspensionPoints are independent of the callback lock; - // only the implicit thread's point is excluded while another thread holds it. - const store = this.inst.store as unknown as { - waiting: ({ - task?: unknown; - logicalOwner?: unknown; - owner?: unknown; - } & Cancellable)[]; - }; - for (const w of store.waiting) { - // A SuspensionPoint names its canonical recipient as logicalOwner; older - // low-level points may only expose physical owner. Plain Thread entries - // are their own recipient. Normalize before applying the callback lock so - // the implicit thread cannot be filtered above and re-added here merely - // because its direct waiting entry has no logicalOwner field. - const recipient = w.logicalOwner ?? w.owner ?? w; - if ( - w.task === this && w.cancellable === true && - (!excludeImplicit || recipient !== this.implicitThread) && - !candidates.includes(w) - ) { - candidates.push(w); - } - } - // Poisoned instances cannot run a cancellation recipient. - if (candidates.length > 0 && !isInstancePoisoned(this.inst)) { - this.state = "cancel-delivered"; - try { - chooseCandidate(candidates).resume(CANCELLED_TRUE); - } catch (e) { - // Escaping delivery failures poison the recipient, except capability signals. - if (!(e instanceof NeedsJspi) && !(e instanceof PendingCapability)) { - notifyInstancePoisoned( - this.inst as unknown as { handles: Iterable }, - e, - ); - } - throw e; - } - } else { - this.state = "pending-cancel"; - } - } - - /** - * Live exclusivity conjunct for cancellability. `canon_lift`'s callback - * waits use lock_available; static park flags alone cannot represent a - * sibling taking the slot. Both delivery selection and pending-cancel - * readiness consult this predicate. - */ - implicitThreadCancellable(): boolean { - return !(this.ft.async === true && this.needsExclusive() && - this.inst.exclusiveThread !== null && - this.inst.exclusiveThread !== this.implicitThread); + this.state = "pending-cancel"; } /** definitions.py `Task.has_pending_cancel`. */ @@ -389,8 +317,8 @@ export class Task { } /** definitions.py `Task.deliver_pending_cancel`. */ - deliverPendingCancel(cancellable: boolean): boolean { - if (cancellable && this.hasPendingCancel()) { + deliverPendingCancel(): boolean { + if (this.hasPendingCancel()) { this.state = "cancel-delivered"; return true; } @@ -475,6 +403,7 @@ export class SynchronousActivation { this.task.failureOwner = (parent?.task?.failureOwner ?? parent?.task ?? this.task) as Task; this.thread = new Thread(this.task, (function* () {})()); + this.thread.parent = parent; const physical = this.parent === undefined ? this.thread : physicalOwnerOf(this.parent) as Thread; diff --git a/runtime/src/task/scheduler.ts b/runtime/src/task/scheduler.ts index bef857c..ca61720 100644 --- a/runtime/src/task/scheduler.ts +++ b/runtime/src/task/scheduler.ts @@ -13,21 +13,15 @@ import { assert_, trapIf } from "../cabi/trap.ts"; import type { ComponentInstanceLike } from "../cabi/context.ts"; -/** definitions.py `Cancelled`. */ -export const CANCELLED_FALSE = false; -export const CANCELLED_TRUE = true; -export type Cancelled = boolean; - /** * What a thread body yields when it wants to stop running. Mirrors the - * reference's `Thread.wait_until` (`ready_func` + `cancellable`); `suspend` + * reference's `Thread.wait_until` (`ready_func`); `suspend` * is `wait_until` with no ready condition (`ready_func === null`), and * `yield_` is `wait_until(() => true)`. */ export interface BlockRequest { /** Resumable once this returns true; `null` = only an explicit resume. */ readyFunc: (() => boolean) | null; - cancellable: boolean; /** Promise park, separate from scheduler readiness. Settlement resumes the * generator with a value or throws the rejection into its unwind path. */ awaitValue?: Promise; @@ -35,8 +29,7 @@ export interface BlockRequest { /** * A thread body: yields block requests, and receives back either the - * cancelled flag (for a scheduler block point) or the resolved value of an - * `awaitValue` request. + * resolved value of an `awaitValue` request. */ // deno-lint-ignore no-explicit-any export type ThreadBody = Generator; @@ -798,7 +791,7 @@ export function currentInstance(): any { export interface SchedulableThread { ready(): boolean; waiting(): boolean; - resume(cancelled?: Cancelled): void; + resume(): void; // deno-lint-ignore no-explicit-any task: any; /** Refresh a dynamic synchronous-lift obligation at a safe service boundary. */ @@ -811,6 +804,8 @@ export interface SchedulableThread { export interface RequiredSyncPark extends SchedulableThread { abandon(reason: unknown): void; waiting(): boolean; + /** Instance whose ready threads the enclosing synchronous operation drives. */ + readonly syncInstance: unknown; readonly logicalOwner: CurrentThreadLike; readonly owner: CurrentThreadLike; } diff --git a/runtime/src/task/streams.ts b/runtime/src/task/streams.ts index 1e8c8a0..c38753f 100644 --- a/runtime/src/task/streams.ts +++ b/runtime/src/task/streams.ts @@ -17,7 +17,7 @@ import { storeListIntoValidRange } from "../cabi/store.ts"; import { alignment, alignTo, elemSize } from "../cabi/layout.ts"; import { despecialize, valTypeEqual } from "../cabi/types.ts"; import type { ComponentValue, ValType } from "../cabi/types.ts"; -import { Waitable } from "./waitable.ts"; +import { EventCode, Waitable } from "./waitable.ts"; import { isInstancePoisoned, setOnInstancePoisoned } from "./scheduler.ts"; /** Cross-boundary structural equality, comparing resource origins, not local tables. @@ -339,10 +339,12 @@ export interface SharedBase { readonly t: ValType | null; dropped: boolean; cancel(): void; - drop(): void; + drop(dropping?: CopyEnd): void; } export class SharedStreamImpl implements SharedBase { + readableEnd: ReadableStreamEnd | null = null; + writableEnd: WritableStreamEnd | null = null; /** * Optional hook fired when this shared object is lowered into a component * instance (`lower_stream`/`lower_future`). Host-owned ends use it to learn @@ -406,6 +408,14 @@ export class SharedStreamImpl implements SharedBase { constructor(readonly t: ValType | null) {} + claimReadableEnd(elem: ValType | null): ReadableStreamEnd { + const end = this.readableEnd ?? new ReadableStreamEnd(this, elem); + assert_(end.index === null, "stream readable end already has an owner"); + end.elem = elem; + if (this.dropped) end.notifyIdleDrop(); + return end; + } + resetPending(): void { this.setPending(null, null, null, null); } @@ -433,11 +443,21 @@ export class SharedStreamImpl implements SharedBase { this.resetAndNotifyPending(CopyResult.CANCELLED); } - drop(): void { + drop(dropping?: CopyEnd): void { if (!this.dropped) { this.dropped = true; - if (this.pendingBuffer) this.resetAndNotifyPending(CopyResult.DROPPED); - this.notifyDropped(); + try { + if (this.pendingBuffer) this.resetAndNotifyPending(CopyResult.DROPPED); + } finally { + if (dropping?.side === "readable") this.writableEnd?.notifyIdleDrop(); + else if (dropping?.side === "writable") { + this.readableEnd?.notifyIdleDrop(); + } else { + this.readableEnd?.notifyIdleDrop(); + this.writableEnd?.notifyIdleDrop(); + } + this.notifyDropped(); + } } } @@ -454,7 +474,7 @@ export class SharedStreamImpl implements SharedBase { this.setPending(inst, dstBuffer, onCopy, onCopyDone); } else { this.#assertSameElemType(dstBuffer); - this.#trapOnSameInstance(inst); + this.#trapOnSameInstance(inst, dstBuffer); if (this.pendingBuffer.remain() > 0) { if (dstBuffer.remain() > 0) { const n = Math.min(dstBuffer.remain(), this.pendingBuffer.remain()); @@ -503,7 +523,7 @@ export class SharedStreamImpl implements SharedBase { this.setPending(inst, srcBuffer, onCopy, onCopyDone); } else { this.#assertSameElemType(srcBuffer); - this.#trapOnSameInstance(inst); + this.#trapOnSameInstance(inst, srcBuffer); if (this.pendingBuffer.remain() > 0) { if (srcBuffer.remain() > 0) { const n = Math.min(srcBuffer.remain(), this.pendingBuffer.remain()); @@ -584,9 +604,10 @@ export class SharedStreamImpl implements SharedBase { * non-number element type would need the source and destination lifts to * interleave, which the reference has not specified yet. */ - #trapOnSameInstance(inst: unknown): void { + #trapOnSameInstance(inst: unknown, arriving: GuestBuffer): void { trapIf( - inst === this.pendingInst && !noneOrNumberType(this.t), + inst === this.pendingInst && this.pendingBuffer!.remain() > 0 && + arriving.remain() > 0 && !noneOrNumberType(this.t), "cannot read from and write to intra-component stream", ); } @@ -594,6 +615,8 @@ export class SharedStreamImpl implements SharedBase { /** definitions.py `SharedFutureImpl`. Exactly one element. */ export class SharedFutureImpl implements SharedBase { + readableEnd: ReadableFutureEnd | null = null; + writableEnd: WritableFutureEnd | null = null; /** * Optional hook fired when this shared object is lowered into a component * instance (`lower_stream`/`lower_future`). Host-owned ends use it to learn @@ -663,6 +686,14 @@ export class SharedFutureImpl implements SharedBase { constructor(readonly t: ValType | null) {} + claimReadableEnd(elem: ValType | null): ReadableFutureEnd { + const end = this.readableEnd ?? new ReadableFutureEnd(this, elem); + assert_(end.index === null, "future readable end already has an owner"); + end.elem = elem; + if (this.dropped) end.notifyIdleDrop(); + return end; + } + resetPending(): void { this.setPending(null, null, null); } @@ -688,11 +719,21 @@ export class SharedFutureImpl implements SharedBase { this.resetAndNotifyPending(CopyResult.CANCELLED); } - drop(): void { + drop(dropping?: CopyEnd): void { if (!this.dropped) { this.dropped = true; - if (this.pendingBuffer) this.resetAndNotifyPending(CopyResult.DROPPED); - this.notifyDropped(); + try { + if (this.pendingBuffer) this.resetAndNotifyPending(CopyResult.DROPPED); + } finally { + if (dropping?.side === "readable") this.writableEnd?.notifyIdleDrop(); + else if (dropping?.side === "writable") { + this.readableEnd?.notifyIdleDrop(); + } else { + this.readableEnd?.notifyIdleDrop(); + this.writableEnd?.notifyIdleDrop(); + } + this.notifyDropped(); + } } } @@ -744,11 +785,13 @@ export class SharedFutureImpl implements SharedBase { */ export abstract class CopyEnd extends Waitable { state: CopyState = CopyState.IDLE; + /** Current owner-table index; updated across readable-end transfer. */ + index: number | null = null; constructor( readonly shared: SharedBase, // Standalone ends inherit their shared descriptor; guest sites stamp locals. - readonly elem: ValType | null = shared.t, + public elem: ValType | null = shared.t, ) { super(); } @@ -760,12 +803,25 @@ export abstract class CopyEnd extends Waitable { * as required by the conformance corpus. */ abstract readonly side: "readable" | "writable"; + abstract readonly eventCode: EventCode; copying(): boolean { return this.state === CopyState.COPYING || this.state === CopyState.CANCELLING_COPY; } + abstract notifyIdleDrop(): void; + + protected armIdleDrop(abandonReason: Error | null = null): void { + if (this.state === CopyState.DONE || this.hasPendingEvent()) return; + this.setPendingEvent(() => { + if (abandonReason !== null) throw futureAbandonTrap(abandonReason); + this.state = CopyState.DONE; + assert_(this.index !== null, "idle async event without owner index"); + return [this.eventCode, this.index, CopyResult.DROPPED]; + }); + } + override drop(): void { trapIf( this.copying(), @@ -773,7 +829,7 @@ export abstract class CopyEnd extends Waitable { ? `cannot remove busy ${this.kind}` : `cannot drop busy ${this.kind}`, ); - this.shared.drop(); + this.shared.drop(this); super.drop(); } } @@ -781,7 +837,15 @@ export abstract class CopyEnd extends Waitable { export class ReadableStreamEnd extends CopyEnd { override readonly kind = "stream"; override readonly side = "readable"; + override readonly eventCode = EventCode.STREAM_READ; declare readonly shared: SharedStreamImpl; + constructor(shared: SharedStreamImpl, elem: ValType | null = shared.t) { + super(shared, elem); + shared.readableEnd = this; + } + notifyIdleDrop(): void { + this.armIdleDrop(); + } copy( inst: unknown, dst: GuestBuffer, @@ -795,7 +859,15 @@ export class ReadableStreamEnd extends CopyEnd { export class WritableStreamEnd extends CopyEnd { override readonly kind = "stream"; override readonly side = "writable"; + override readonly eventCode = EventCode.STREAM_WRITE; declare readonly shared: SharedStreamImpl; + constructor(shared: SharedStreamImpl, elem: ValType | null = shared.t) { + super(shared, elem); + shared.writableEnd = this; + } + notifyIdleDrop(): void { + this.armIdleDrop(); + } copy( inst: unknown, src: GuestBuffer, @@ -809,7 +881,15 @@ export class WritableStreamEnd extends CopyEnd { export class ReadableFutureEnd extends CopyEnd { override readonly kind = "future"; override readonly side = "readable"; + override readonly eventCode = EventCode.FUTURE_READ; declare readonly shared: SharedFutureImpl; + constructor(shared: SharedFutureImpl, elem: ValType | null = shared.t) { + super(shared, elem); + shared.readableEnd = this; + } + notifyIdleDrop(): void { + this.armIdleDrop(this.shared.abandonReason); + } copy(inst: unknown, dst: GuestBuffer, onCopyDone: OnCopyDone): void { this.shared.read(inst, dst, onCopyDone); } @@ -818,7 +898,15 @@ export class ReadableFutureEnd extends CopyEnd { export class WritableFutureEnd extends CopyEnd { override readonly kind = "future"; override readonly side = "writable"; + override readonly eventCode = EventCode.FUTURE_WRITE; declare readonly shared: SharedFutureImpl; + constructor(shared: SharedFutureImpl, elem: ValType | null = shared.t) { + super(shared, elem); + shared.writableEnd = this; + } + notifyIdleDrop(): void { + this.armIdleDrop(); + } copy(inst: unknown, src: GuestBuffer, onCopyDone: OnCopyDone): void { this.shared.write(inst, src, onCopyDone); } @@ -922,21 +1010,17 @@ interface PoisonedInstanceLike { */ export function dropSharedForTeardown( shared: SharedStreamImpl | SharedFutureImpl, + dropping?: CopyEnd, ): void { if (shared.dropped) return; - shared.dropped = true; - try { - if (shared.pendingBuffer) { - const pi = shared.pendingInst; - const parkedInDeadGuest = typeof pi === "object" && pi !== null && - (isInstancePoisoned(pi) || retiredInstances.has(pi)); - if (parkedInDeadGuest) shared.resetPending(); - else shared.resetAndNotifyPending(CopyResult.DROPPED); - } - } finally { - // Release producer/host retention even if the peer's notification throws. - shared.notifyDropped(); - } + const pi = shared.pendingInst; + const parkedInDeadGuest = shared.pendingBuffer !== null && + typeof pi === "object" && pi !== null && + (isInstancePoisoned(pi) || retiredInstances.has(pi)); + if (parkedInDeadGuest) shared.resetPending(); + // Use the ordinary endpoint transition so healthy idle peers receive their + // endpoint-owned notification and drop observers fire consistently. + shared.drop(dropping); } /** @@ -1000,7 +1084,10 @@ function retireAsyncEnds( let failed = false; for (const e of ends) { try { - dropSharedForTeardown(e.shared as SharedStreamImpl | SharedFutureImpl); + dropSharedForTeardown( + e.shared as SharedStreamImpl | SharedFutureImpl, + e, + ); } catch (err) { if (!failed) { failed = true; diff --git a/runtime/src/task/thread.ts b/runtime/src/task/thread.ts index 09c6a17..5de1442 100644 --- a/runtime/src/task/thread.ts +++ b/runtime/src/task/thread.ts @@ -17,9 +17,6 @@ import { assert_ } from "../cabi/trap.ts"; import { abortLogicalChildren, type BlockRequest, - type Cancelled, - CANCELLED_FALSE, - CANCELLED_TRUE, isInstancePoisoned, NeedsJspi, notifyInstancePoisoned, @@ -42,6 +39,8 @@ export interface ThreadPark extends SchedulableThread { } export class Thread implements SchedulableThread { + /** Real synchronous logical caller, populated only by SynchronousActivation. */ + parent?: import("./scheduler.ts").CurrentThreadLike; /** Physical generator activation used for JSPI awaiting/resumption. */ physicalOwner: Thread = this; /** Persistent logical descendants, including while their stack is unpublished. */ @@ -71,12 +70,6 @@ export class Thread implements SchedulableThread { /** Slot in `inst.threads`, assigned by `Task.registerThread`. */ index: number | null = null; - /** - * Cancellability of the current park, cleared on resume. Callback lock - * availability is checked separately by `Task.implicitThreadCancellable`. - */ - cancellable = false; - #state: ThreadState = "suspended"; #body: ThreadBody; #readyFunc: (() => boolean) | null = null; @@ -118,11 +111,11 @@ export class Thread implements SchedulableThread { } /** definitions.py `Thread.stop_waiting_internal`. */ - #stopWaiting(cancelled: Cancelled): void { + #stopWaiting(): void { assert_(this.waiting() && this.#readyFunc !== null); assert_( - cancelled || this.ready(), - "stopWaiting on a thread that is neither ready nor cancelled", + this.ready(), + "stopWaiting on a thread that is not ready", ); this.#readyFunc = null; this.#state = "suspended"; @@ -186,34 +179,36 @@ export class Thread implements SchedulableThread { } /** `Thread.resume`: run until the next block or completion. */ - resume(cancelled: Cancelled = CANCELLED_FALSE): void { + resume(): void { assert_( !this.running() && !this.done(), "resume() on a running or finished thread", ); - assert_( - this.cancellable || !cancelled, - "cancelled resume of a non-cancellable block point", - ); - if (this.waiting()) this.#stopWaiting(cancelled); - this.#resumeInternal(cancelled); + if (this.waiting()) this.#stopWaiting(); + this.#resumeInternal(undefined); } - /** Retire a cancellable wait without delivering task cancellation. */ + /** Wake an INITIAL task's admission wait after cancellation was recorded. */ + resumeStartupCancellation(): void { + assert_(this.waiting(), "startup cancellation of a non-waiting thread"); + this.#readyFunc = null; + this.#state = "suspended"; + this.#store.stopWaiting(this); + this.#resumeInternal(undefined); + } + + /** Retire a startup wait without delivering task cancellation. */ abandonWaiting(): void { - assert_( - this.waiting() && this.cancellable, - "abandon of non-cancellable wait", - ); - this.#stopWaiting(CANCELLED_TRUE); + assert_(this.waiting(), "abandon of non-waiting thread"); + this.#readyFunc = null; + this.#state = "suspended"; + this.#store.stopWaiting(this); this.#body.return?.(); this.#state = "done"; - this.cancellable = false; } #resumeInternal(sendValue: unknown, failure?: { error: unknown }): void { this.#state = "running"; - this.cancellable = false; pushCurrentThread(this); let step: IteratorResult; try { @@ -244,7 +239,6 @@ export class Thread implements SchedulableThread { return; } const req = step.value; - this.cancellable = req.cancellable; if (req.awaitValue !== undefined) { // Promise parks are driver-owned, with eager settlement tracking to // order their bookkeeping before later scheduler ticks. @@ -283,36 +277,19 @@ export class Thread implements SchedulableThread { */ *waitUntil( readyFunc: () => boolean, - cancellable = false, - ): Generator { + ): Generator { assert_(this.running(), "waitUntil on a non-running thread"); - if (this.task.deliverPendingCancel(cancellable)) return CANCELLED_TRUE; - // Pending cancellation is itself a wakeup, but the implicit callback - // thread cannot receive it while another thread holds exclusivity. - const readyOrCancelled = () => - readyFunc() || - (cancellable && this.task.hasPendingCancel() && - (this !== this.task.implicitThread || - this.task.implicitThreadCancellable())); - const cancelled = yield { readyFunc: readyOrCancelled, cancellable }; - // As in Thread.wait_until, pending cancellation wins over a ready event - // after the block as well as before it. - if (this.task.deliverPendingCancel(cancellable)) return CANCELLED_TRUE; - return cancelled; + yield { readyFunc }; } /** definitions.py `Thread.suspend`. */ - *suspend( - cancellable: boolean, - ): Generator { + *suspend(): Generator { assert_(this.running(), "suspend on a non-running thread"); - if (this.task.deliverPendingCancel(cancellable)) return CANCELLED_TRUE; - const cancelled = yield { readyFunc: null, cancellable }; - return cancelled; + yield { readyFunc: null }; } /** definitions.py `Thread.yield_`: wait with an always-ready predicate. */ - *yield_(cancellable: boolean): Generator { - return yield* this.waitUntil(() => true, cancellable); + *yield_(): Generator { + return yield* this.waitUntil(() => true); } } diff --git a/runtime/src/task/waitable.ts b/runtime/src/task/waitable.ts index e3082af..1d95b64 100644 --- a/runtime/src/task/waitable.ts +++ b/runtime/src/task/waitable.ts @@ -3,7 +3,7 @@ import { assert_, trapIf } from "../cabi/trap.ts"; import { chooseCandidate } from "./scheduler.ts"; -import type { BlockRequest, Cancelled } from "./scheduler.ts"; +import type { BlockRequest } from "./scheduler.ts"; import type { Thread } from "./thread.ts"; /** definitions.py `EventCode`. */ @@ -49,13 +49,13 @@ export class Waitable { */ *waitForPendingEvent( thread: Thread, - ): Generator { + ): Generator { assert_( !this.inWaitableSet() && !this.hasSyncWaiter, "waitForPendingEvent on a joined or already-awaited waitable", ); this.hasSyncWaiter = true; - yield* thread.waitUntil(() => this.hasPendingEvent(), false); + yield* thread.waitUntil(() => this.hasPendingEvent()); this.hasSyncWaiter = false; } @@ -80,11 +80,11 @@ export class Waitable { /** definitions.py `Waitable.drop`. */ drop(): void { - assert_( - !this.hasPendingEvent(), - "dropping a waitable with a pending event", - ); assert_(!this.hasSyncWaiter, "dropping a waitable with a sync waiter"); + // definitions.py:744-746: dropping a waitable discards, rather than + // consumes, any pending notification. This matters for an idle stream end + // whose peer-drop event has not yet been observed. + this.pendingEvent = null; this.join(null); } } @@ -130,15 +130,34 @@ export class WaitableSet { *waitForEventAnd( thread: Thread, readyFunc: () => boolean, - cancellable: boolean, - ): Generator { + ): Generator { + this.numWaiting += 1; + try { + yield* thread.waitUntil(() => readyFunc() && this.hasPendingEvent()); + return this.getPendingEvent(); + } finally { + this.numWaiting -= 1; + } + } + + /** Callback-ABI wait. Cancellation readiness and priority belong to + * `canon_lift`'s callback loop, not generic Thread waiting + * (definitions.py:2073-2094). */ + *waitForCallbackEvent( + thread: Thread, + task: { + hasPendingCancel(): boolean; + deliverPendingCancel(): boolean; + }, + lockAvailable: () => boolean, + ): Generator { this.numWaiting += 1; try { - const cancelled = yield* thread.waitUntil( - () => readyFunc() && this.hasPendingEvent(), - cancellable, + yield* thread.waitUntil(() => + lockAvailable() && + (task.hasPendingCancel() || this.hasPendingEvent()) ); - return cancelled + return task.deliverPendingCancel() ? [EventCode.TASK_CANCELLED, 0, 0] : this.getPendingEvent(); } finally { @@ -149,9 +168,8 @@ export class WaitableSet { /** definitions.py `WaitableSet.wait_for_event`. */ *waitForEvent( thread: Thread, - cancellable: boolean, - ): Generator { - return yield* this.waitForEventAnd(thread, () => true, cancellable); + ): Generator { + return yield* this.waitForEventAnd(thread, () => true); } /** @@ -159,10 +177,7 @@ export class WaitableSet { * plain function rather than a generator. */ // deno-lint-ignore no-explicit-any - poll(task: any, cancellable: boolean): EventTuple { - if (task.deliverPendingCancel(cancellable)) { - return [EventCode.TASK_CANCELLED, 0, 0]; - } + poll(_task?: any): EventTuple { if (!this.hasPendingEvent()) return [EventCode.NONE, 0, 0]; return this.getPendingEvent(); } diff --git a/runtime/tests/async_builtins_test.ts b/runtime/tests/async_builtins_test.ts index b863ccc..44820a7 100644 --- a/runtime/tests/async_builtins_test.ts +++ b/runtime/tests/async_builtins_test.ts @@ -12,7 +12,6 @@ import { BLOCKED, createSubtaskCancel, createWaitableJoin, - createWaitableSetPoll, createWaitableSetWait, } from "../src/intrinsics/async_builtins.ts"; import { @@ -31,6 +30,7 @@ import { import { ComponentInstanceState, CopyResult, + currentTask, EventCode, NeedsJspi, popCurrentThread, @@ -163,23 +163,6 @@ function mkWaitFixture(cancellable: boolean): WaitFixture { }; } -Deno.test("waitable-set.wait: a cancellable wait delivers TASK_CANCELLED", () => { - const f = mkWaitFixture(true); - const wait = createWaitableSetWait({ options: 0 }, f.ctx, f.inst); - // A cancellation that could not be delivered to a cancellable block point - // yet (definitions.py `Task.request_cancellation` -> PENDING_CANCEL). - f.task.state = "pending-cancel"; - const code = f.run(() => wait(f.seti, 64)) as number; - assertEq(code, EventCode.TASK_CANCELLED); - // definitions.py `Task.deliver_pending_cancel`: delivery moves the task on. - assertEq(f.task.state, "cancel-delivered"); - // `unpack_event` (line 2422) stores the two payload words; both are 0 for - // TASK_CANCELLED. - const dv = new DataView(f.memory.buffer); - assertEq(dv.getUint32(64, true), 0); - assertEq(dv.getUint32(68, true), 0); -}); - Deno.test("waitable-set.wait: a non-cancellable wait does not deliver it", () => { const f = mkWaitFixture(false); const wait = createWaitableSetWait({ options: 0 }, f.ctx, f.inst); @@ -201,21 +184,6 @@ Deno.test("waitable-set.wait: a non-cancellable wait does not deliver it", () => assertEq(dv.getUint32(68, true), 2); }); -Deno.test("waitable-set.poll: cancellable vs non-cancellable", () => { - const yes = mkWaitFixture(true); - const pollYes = createWaitableSetPoll({ options: 0 }, yes.ctx, yes.inst); - yes.task.state = "pending-cancel"; - assertEq(yes.run(() => pollYes(yes.seti, 64)), EventCode.TASK_CANCELLED); - assertEq(yes.task.state, "cancel-delivered"); - - const no = mkWaitFixture(false); - const pollNo = createWaitableSetPoll({ options: 0 }, no.ctx, no.inst); - no.task.state = "pending-cancel"; - // definitions.py `WaitableSet.poll`: not cancellable and no event -> NONE. - assertEq(no.run(() => pollNo(no.seti, 64)), EventCode.NONE); - assertEq(no.task.state, "pending-cancel"); -}); - Deno.test("waitable-set.wait: without an event and without a cancel, needs JSPI", () => { const f = mkWaitFixture(true); const wait = createWaitableSetWait({ options: 0 }, f.ctx, f.inst); @@ -287,6 +255,45 @@ Deno.test("callback loop: WAIT with an index that is not a waitable set traps", assertTraps(() => fn(), "not a waitable set"); }); +Deno.test("callback loop: pending cancellation precedes invalid WAIT and retains exclusivity", () => { + const inst = new ComponentInstanceState(0, new Store()); + const notASet = inst.handles.add({ definitelyNot: "a waitable set" }); + let event = -1; + let held = false; + const callback = (...args: number[]) => { + event = args[0]; + const task = currentTask() as Task; + held = inst.exclusiveThread === task.implicitThread; + task.cancel(); + return 0; + }; + const ft: FuncType = { params: [], results: [], async: true }; + const opts: ResolvedOptions = { + stringEncoding: "utf8", + memory: null, + realloc: null, + postReturn: null, + callback: () => callback as never, + async: true, + cancellable: false, + coreType: { params: [], results: ["i32"] }, + instance: inst, + }; + const fn = createLiftedFunction({ + name: "cancel-before-invalid-wait", + ft, + opts, + core: () => { + (currentTask() as Task).requestCancellation(null); + return 2 | (notASet << 4); + }, + stats: newStats(), + }); + fn(); + assertEq(event, EventCode.TASK_CANCELLED); + assertEq(held, true); +}); + Deno.test("callback loop: EXIT without resolving the task traps", () => { // definitions.py `Task.unregister_thread` (line 510): // `trap_if(self.state != Task.State.RESOLVED)` once the last thread goes. diff --git a/runtime/tests/async_lower_test.ts b/runtime/tests/async_lower_test.ts index bf01c22..77124c8 100644 --- a/runtime/tests/async_lower_test.ts +++ b/runtime/tests/async_lower_test.ts @@ -306,8 +306,10 @@ Deno.test("cancellation discard: deferCancel() opts a host import out of discard const [, subtaski] = unpackSubtaskResult(packed); const subtask = f.inst.handles.get(subtaski) as Subtask; - const cancel = createSubtaskCancel({ async: true }, f.inst); - const rc = f.asGuest(() => cancel(subtaski)) as number; + const cancel = createSubtaskCancel({ async: true }, f.inst, "jspi"); + const pending = f.asGuest(() => cancel(subtaski)) as Promise; + f.store.tick(); + const rc = await pending; assertEq(rc, BLOCKED); // The request is recorded, and the subtask is untouched otherwise. assertEq(subtask.cancellationRequested, true); @@ -333,7 +335,7 @@ Deno.test("cancellation discard: deferCancel() opts a host import out of discard assertEq(subtask.resolveDelivered(), true); }); -Deno.test("async lower: a second subtask.cancel traps (deferCancel: already cancelled)", () => { +Deno.test("async lower: a second subtask.cancel traps (deferCancel: already cancelled)", async () => { // definitions.py line 2475: `trap_if(subtask.cancellation_requested)`. // // Reaching that trap needs a subtask that is still UNRESOLVED after its @@ -344,8 +346,10 @@ Deno.test("async lower: a second subtask.cancel traps (deferCancel: already canc const f = mkFixture(deferCancel(() => new Promise(() => {}))); const packed = f.asGuest(() => f.call(1, 64)) as number; const [, subtaski] = unpackSubtaskResult(packed); - const cancel = createSubtaskCancel({ async: true }, f.inst); - assertEq(f.asGuest(() => cancel(subtaski)), BLOCKED); + const cancel = createSubtaskCancel({ async: true }, f.inst, "jspi"); + const pending = f.asGuest(() => cancel(subtaski)) as Promise; + f.store.tick(); + assertEq(await pending, BLOCKED); let raised: unknown; try { f.asGuest(() => cancel(subtaski)); diff --git a/runtime/tests/bindgen/fixtures/hello.envelope.json b/runtime/tests/bindgen/fixtures/hello.envelope.json index 9bd8654..6d00fab 100644 --- a/runtime/tests/bindgen/fixtures/hello.envelope.json +++ b/runtime/tests/bindgen/fixtures/hello.envelope.json @@ -1 +1 @@ -{"plan":{"formatVersion":5,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"49.0.0-dev+4675ee1","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"4f6afd22b20a96b1b9b7bee56c97dd79c18b67c830ef5fd234b4104ae5a1231f","len":20575},"modules":[{"kind":"embedded","offset":12,"len":20290}],"initializers":[{"op":"instantiate-module","module":0,"instance":0,"args":[]},{"op":"extract-memory","index":0,"export":{"instance":0,"item":{"name":"memory","space":"memory"}}},{"op":"extract-realloc","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_realloc","space":"func"}}},{"op":"extract-post-return","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_greet","space":"func"}}}],"trampolines":[],"canonicalOptions":[{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":0,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}}],"types":[{"kind":"func","params":[{"label":"name","type":{"kind":"string"}}],"results":[{"kind":"string"}],"async":false}],"resourceTables":[],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"lifted-func","name":"greet","coreDef":{"kind":"export","instance":0,"item":{"name":"greet","space":"func"}},"options":0,"type":0}],"worldDigest":"sha256:e558f75402f429b8c2478213e3b0422fa553b725b8d587faa416b55f97e1c880"},"adapters":[]} \ No newline at end of file +{"plan":{"formatVersion":6,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"50.0.0-dev+cc546ee","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"a35d229559007b68c92b05fe6c3154ca8c7b7b4f0a3dde0f6f7790ecccf1a214","len":20575},"modules":[{"kind":"embedded","offset":12,"len":20290}],"initializers":[{"op":"instantiate-module","module":0,"instance":0,"args":[]},{"op":"extract-memory","index":0,"export":{"instance":0,"item":{"name":"memory","space":"memory"}}},{"op":"extract-realloc","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_realloc","space":"func"}}},{"op":"extract-post-return","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_greet","space":"func"}}}],"trampolines":[],"canonicalOptions":[{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":0,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}}],"types":[{"kind":"func","params":[{"label":"name","type":{"kind":"string"}}],"results":[{"kind":"string"}],"async":false}],"resourceTables":[],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"lifted-func","name":"greet","coreDef":{"kind":"export","instance":0,"item":{"name":"greet","space":"func"}},"options":0,"type":0}],"worldDigest":"sha256:e558f75402f429b8c2478213e3b0422fa553b725b8d587faa416b55f97e1c880"},"adapters":[]} \ No newline at end of file diff --git a/runtime/tests/bindgen/fixtures/resources.envelope.json b/runtime/tests/bindgen/fixtures/resources.envelope.json index 09ac284..e4f288d 100644 --- a/runtime/tests/bindgen/fixtures/resources.envelope.json +++ b/runtime/tests/bindgen/fixtures/resources.envelope.json @@ -1 +1 @@ -{"plan":{"formatVersion":5,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"49.0.0-dev+4675ee1","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"c932f1ad7a5c9ac5db487c4a22292fa307fe68b38c755cb57be4df7694364112","len":24076},"modules":[{"kind":"embedded","offset":12,"len":20766},{"kind":"embedded","offset":20780,"len":106},{"kind":"embedded","offset":20888,"len":96}],"initializers":[{"op":"instantiate-module","module":1,"instance":0,"args":[]},{"op":"resource","index":0,"rep":"i32","dtor":{"kind":"export","instance":0,"item":{"name":"0","space":"func"}},"instance":0},{"op":"instantiate-module","module":0,"instance":0,"args":[{"kind":"trampoline","index":0},{"kind":"trampoline","index":1},{"kind":"trampoline","index":2}]},{"op":"instantiate-module","module":2,"instance":0,"args":[{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[dtor]counter","space":"func"}},{"kind":"export","instance":0,"item":{"name":"$imports","space":"table"}}]}],"trampolines":[{"kind":"resource-rep","index":0,"instance":0,"resource":0},{"kind":"resource-new","index":1,"instance":0,"resource":0},{"kind":"resource-drop","index":2,"instance":0,"resource":0}],"canonicalOptions":[{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i64"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i64"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":[],"results":["i32"]}}],"types":[{"kind":"func","params":[{"label":"initial","type":{"kind":"u64"}}],"results":[{"kind":"own","resource":0}],"async":false},{"kind":"func","params":[{"label":"self","type":{"kind":"borrow","resource":0}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"a","type":{"kind":"own","resource":0}},{"label":"b","type":{"kind":"own","resource":0}}],"results":[{"kind":"own","resource":0}],"async":false},{"kind":"func","params":[{"label":"a","type":{"kind":"borrow","resource":0}},{"label":"b","type":{"kind":"borrow","resource":0}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"c","type":{"kind":"borrow","resource":0}},{"label":"by","type":{"kind":"u64"}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"c","type":{"kind":"own","resource":0}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[],"results":[{"kind":"u32"}],"async":false}],"resourceTables":[{"kind":"concrete","resource":0,"instance":0},{"kind":"concrete","resource":0,"instance":1}],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"instance","name":"polyengine:resources/counters","exports":[{"kind":"type","name":"counter","type":{"kind":"resource","resource":1}},{"kind":"lifted-func","name":"[constructor]counter","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[constructor]counter","space":"func"}},"options":0,"type":0},{"kind":"lifted-func","name":"[method]counter.increment","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[method]counter.increment","space":"func"}},"options":1,"type":1},{"kind":"lifted-func","name":"[method]counter.get","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[method]counter.get","space":"func"}},"options":1,"type":1},{"kind":"lifted-func","name":"[static]counter.merge","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[static]counter.merge","space":"func"}},"options":2,"type":2},{"kind":"lifted-func","name":"make-counter","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[constructor]counter","space":"func"}},"options":0,"type":0},{"kind":"lifted-func","name":"sum-both","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#sum-both","space":"func"}},"options":3,"type":3},{"kind":"lifted-func","name":"bump","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#bump","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"consume","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#consume","space":"func"}},"options":1,"type":5},{"kind":"lifted-func","name":"live-counters","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#live-counters","space":"func"}},"options":5,"type":6}]}],"worldDigest":"sha256:2d8dc56eed12e066c1b6f8677a93a934d6472ec505f7423e580ec69fbabc0939"},"adapters":[]} \ No newline at end of file +{"plan":{"formatVersion":6,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"50.0.0-dev+cc546ee","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"e82638f761eb0495124e9cfcd6e3e0a4d4ff238bd88071919717541d4cc3f021","len":24109},"modules":[{"kind":"embedded","offset":12,"len":20766},{"kind":"embedded","offset":20781,"len":151},{"kind":"embedded","offset":21101,"len":142}],"initializers":[{"op":"instantiate-module","module":1,"instance":0,"args":[]},{"op":"resource","index":0,"rep":"i32","dtor":{"kind":"export","instance":0,"item":{"name":"0","space":"func"}},"instance":0},{"op":"instantiate-module","module":0,"instance":0,"args":[{"kind":"trampoline","index":0},{"kind":"trampoline","index":1},{"kind":"trampoline","index":2}]},{"op":"instantiate-module","module":2,"instance":0,"args":[{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[dtor]counter","space":"func"}},{"kind":"export","instance":0,"item":{"name":"$imports","space":"table"}}]}],"trampolines":[{"kind":"resource-rep","index":0,"instance":0,"resource":0},{"kind":"resource-new","index":1,"instance":0,"resource":0},{"kind":"resource-drop","index":2,"instance":0,"resource":0}],"canonicalOptions":[{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i64"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i64"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":[],"results":["i32"]}}],"types":[{"kind":"func","params":[{"label":"initial","type":{"kind":"u64"}}],"results":[{"kind":"own","resource":0}],"async":false},{"kind":"func","params":[{"label":"self","type":{"kind":"borrow","resource":0}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"a","type":{"kind":"own","resource":0}},{"label":"b","type":{"kind":"own","resource":0}}],"results":[{"kind":"own","resource":0}],"async":false},{"kind":"func","params":[{"label":"a","type":{"kind":"borrow","resource":0}},{"label":"b","type":{"kind":"borrow","resource":0}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"c","type":{"kind":"borrow","resource":0}},{"label":"by","type":{"kind":"u64"}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"c","type":{"kind":"own","resource":0}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[],"results":[{"kind":"u32"}],"async":false}],"resourceTables":[{"kind":"concrete","resource":0,"instance":0},{"kind":"concrete","resource":0,"instance":1}],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"instance","name":"polyengine:resources/counters","exports":[{"kind":"type","name":"counter","type":{"kind":"resource","resource":1}},{"kind":"lifted-func","name":"[constructor]counter","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[constructor]counter","space":"func"}},"options":0,"type":0},{"kind":"lifted-func","name":"[method]counter.increment","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[method]counter.increment","space":"func"}},"options":1,"type":1},{"kind":"lifted-func","name":"[method]counter.get","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[method]counter.get","space":"func"}},"options":1,"type":1},{"kind":"lifted-func","name":"[static]counter.merge","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[static]counter.merge","space":"func"}},"options":2,"type":2},{"kind":"lifted-func","name":"make-counter","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#[constructor]counter","space":"func"}},"options":0,"type":0},{"kind":"lifted-func","name":"sum-both","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#sum-both","space":"func"}},"options":3,"type":3},{"kind":"lifted-func","name":"bump","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#bump","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"consume","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#consume","space":"func"}},"options":1,"type":5},{"kind":"lifted-func","name":"live-counters","coreDef":{"kind":"export","instance":1,"item":{"name":"polyengine:resources/counters#live-counters","space":"func"}},"options":5,"type":6}]}],"worldDigest":"sha256:2d8dc56eed12e066c1b6f8677a93a934d6472ec505f7423e580ec69fbabc0939"},"adapters":[]} \ No newline at end of file diff --git a/runtime/tests/bindgen/fixtures/values.envelope.json b/runtime/tests/bindgen/fixtures/values.envelope.json index 1d8d442..9ba15c5 100644 --- a/runtime/tests/bindgen/fixtures/values.envelope.json +++ b/runtime/tests/bindgen/fixtures/values.envelope.json @@ -1 +1 @@ -{"plan":{"formatVersion":5,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"49.0.0-dev+4675ee1","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"162885f44e0ba20adef264f95c723345a010fcc46fa120a23ff16b9f26cdce9b","len":24250},"modules":[{"kind":"embedded","offset":191,"len":21791}],"initializers":[{"op":"instantiate-module","module":0,"instance":0,"args":[]},{"op":"extract-memory","index":0,"export":{"instance":0,"item":{"name":"memory","space":"memory"}}},{"op":"extract-realloc","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_realloc","space":"func"}}},{"op":"extract-post-return","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-list-u8","space":"func"}}},{"op":"extract-post-return","index":1,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-record","space":"func"}}},{"op":"extract-post-return","index":2,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-variant","space":"func"}}},{"op":"extract-post-return","index":3,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-option","space":"func"}}},{"op":"extract-post-return","index":4,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-list-string","space":"func"}}}],"trampolines":[],"canonicalOptions":[{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i64"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["f32"],"results":["f32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["f64"],"results":["f64"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":0,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":1,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32","f64","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":2,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i64","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":3,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":4,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":1,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32","f64"],"results":["i32"]}}],"types":[{"kind":"func","params":[{"label":"v","type":{"kind":"bool"}}],"results":[{"kind":"bool"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"u64"}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"s64"}}],"results":[{"kind":"s64"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"f32"}}],"results":[{"kind":"f32"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"f64"}}],"results":[{"kind":"f64"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"char"}}],"results":[{"kind":"char"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"string"}}],"results":[{"kind":"string"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"record","fields":[{"label":"a","type":{"kind":"u32"}},{"label":"b","type":{"kind":"string"}},{"label":"c","type":{"kind":"f64"}},{"label":"d","type":{"kind":"bool"}}]}}],"results":[{"kind":"record","fields":[{"label":"a","type":{"kind":"u32"}},{"label":"b","type":{"kind":"string"}},{"label":"c","type":{"kind":"f64"}},{"label":"d","type":{"kind":"bool"}}]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"variant","cases":[{"label":"point","type":null},{"label":"circle","type":{"kind":"f64"}},{"label":"label","type":{"kind":"string"}},{"label":"rect","type":{"kind":"record","fields":[{"label":"w","type":{"kind":"u32"}},{"label":"h","type":{"kind":"u32"}}]}}]}}],"results":[{"kind":"variant","cases":[{"label":"point","type":null},{"label":"circle","type":{"kind":"f64"}},{"label":"label","type":{"kind":"string"}},{"label":"rect","type":{"kind":"record","fields":[{"label":"w","type":{"kind":"u32"}},{"label":"h","type":{"kind":"u32"}}]}}]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"enum","labels":["red","green","blue"]}}],"results":[{"kind":"enum","labels":["red","green","blue"]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"flags","labels":["read","write","exec","admin"]}}],"results":[{"kind":"flags","labels":["read","write","exec","admin"]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"option","type":{"kind":"string"}}}],"results":[{"kind":"option","type":{"kind":"string"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"option","type":{"kind":"option","type":{"kind":"u32"}}}}],"results":[{"kind":"option","type":{"kind":"option","type":{"kind":"u32"}}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"result","ok":{"kind":"u32"},"err":{"kind":"string"}}}],"results":[{"kind":"result","ok":{"kind":"u32"},"err":{"kind":"string"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"list","element":{"kind":"u8"}}}],"results":[{"kind":"list","element":{"kind":"u8"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"list","element":{"kind":"string"}}}],"results":[{"kind":"list","element":{"kind":"string"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"tuple","elements":[{"kind":"u32"},{"kind":"string"},{"kind":"f64"}]}}],"results":[{"kind":"tuple","elements":[{"kind":"u32"},{"kind":"string"},{"kind":"f64"}]}],"async":false}],"resourceTables":[],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"lifted-func","name":"echo-bool","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-bool","space":"func"}},"options":0,"type":0},{"kind":"lifted-func","name":"echo-u64","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-s64","space":"func"}},"options":1,"type":1},{"kind":"lifted-func","name":"echo-s64","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-s64","space":"func"}},"options":1,"type":2},{"kind":"lifted-func","name":"echo-f32","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-f32","space":"func"}},"options":2,"type":3},{"kind":"lifted-func","name":"echo-f64","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-f64","space":"func"}},"options":3,"type":4},{"kind":"lifted-func","name":"echo-char","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-char","space":"func"}},"options":0,"type":5},{"kind":"lifted-func","name":"echo-string","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-list-u8","space":"func"}},"options":4,"type":6},{"kind":"lifted-func","name":"echo-record","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-record","space":"func"}},"options":5,"type":7},{"kind":"lifted-func","name":"echo-variant","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-variant","space":"func"}},"options":6,"type":8},{"kind":"lifted-func","name":"echo-enum","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-enum","space":"func"}},"options":0,"type":9},{"kind":"lifted-func","name":"echo-flags","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-flags","space":"func"}},"options":0,"type":10},{"kind":"lifted-func","name":"echo-option","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-option","space":"func"}},"options":7,"type":11},{"kind":"lifted-func","name":"echo-option-nested","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-option-nested","space":"func"}},"options":8,"type":12},{"kind":"lifted-func","name":"echo-result","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-result","space":"func"}},"options":7,"type":13},{"kind":"lifted-func","name":"echo-list-u8","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-list-u8","space":"func"}},"options":4,"type":14},{"kind":"lifted-func","name":"echo-list-string","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-list-string","space":"func"}},"options":9,"type":15},{"kind":"lifted-func","name":"echo-tuple","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-tuple","space":"func"}},"options":10,"type":16}],"worldDigest":"sha256:86114de37a4752373d7b3a0a6efb78920ee01ae6eb28bbd95b1f6f50703e652e"},"adapters":[]} \ No newline at end of file +{"plan":{"formatVersion":6,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"50.0.0-dev+cc546ee","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"2256c8c3d7d502598d7c8b5be8586beb9cbab2ddc72f7937255f8dbc63759451","len":24250},"modules":[{"kind":"embedded","offset":191,"len":21791}],"initializers":[{"op":"instantiate-module","module":0,"instance":0,"args":[]},{"op":"extract-memory","index":0,"export":{"instance":0,"item":{"name":"memory","space":"memory"}}},{"op":"extract-realloc","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_realloc","space":"func"}}},{"op":"extract-post-return","index":0,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-list-u8","space":"func"}}},{"op":"extract-post-return","index":1,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-record","space":"func"}}},{"op":"extract-post-return","index":2,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-variant","space":"func"}}},{"op":"extract-post-return","index":3,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-option","space":"func"}}},{"op":"extract-post-return","index":4,"def":{"kind":"export","instance":0,"item":{"name":"cabi_post_echo-list-string","space":"func"}}}],"trampolines":[],"canonicalOptions":[{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i64"],"results":["i64"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["f32"],"results":["f32"]}},{"instance":0,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["f64"],"results":["f64"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":0,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":1,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32","f64","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":2,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i64","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":3,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":4,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32"],"results":["i32"]}},{"instance":0,"stringEncoding":"utf8","memory":0,"realloc":0,"postReturn":1,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i32","i32","f64"],"results":["i32"]}}],"types":[{"kind":"func","params":[{"label":"v","type":{"kind":"bool"}}],"results":[{"kind":"bool"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"u64"}}],"results":[{"kind":"u64"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"s64"}}],"results":[{"kind":"s64"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"f32"}}],"results":[{"kind":"f32"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"f64"}}],"results":[{"kind":"f64"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"char"}}],"results":[{"kind":"char"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"string"}}],"results":[{"kind":"string"}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"record","fields":[{"label":"a","type":{"kind":"u32"}},{"label":"b","type":{"kind":"string"}},{"label":"c","type":{"kind":"f64"}},{"label":"d","type":{"kind":"bool"}}]}}],"results":[{"kind":"record","fields":[{"label":"a","type":{"kind":"u32"}},{"label":"b","type":{"kind":"string"}},{"label":"c","type":{"kind":"f64"}},{"label":"d","type":{"kind":"bool"}}]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"variant","cases":[{"label":"point","type":null},{"label":"circle","type":{"kind":"f64"}},{"label":"label","type":{"kind":"string"}},{"label":"rect","type":{"kind":"record","fields":[{"label":"w","type":{"kind":"u32"}},{"label":"h","type":{"kind":"u32"}}]}}]}}],"results":[{"kind":"variant","cases":[{"label":"point","type":null},{"label":"circle","type":{"kind":"f64"}},{"label":"label","type":{"kind":"string"}},{"label":"rect","type":{"kind":"record","fields":[{"label":"w","type":{"kind":"u32"}},{"label":"h","type":{"kind":"u32"}}]}}]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"enum","labels":["red","green","blue"]}}],"results":[{"kind":"enum","labels":["red","green","blue"]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"flags","labels":["read","write","exec","admin"]}}],"results":[{"kind":"flags","labels":["read","write","exec","admin"]}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"option","type":{"kind":"string"}}}],"results":[{"kind":"option","type":{"kind":"string"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"option","type":{"kind":"option","type":{"kind":"u32"}}}}],"results":[{"kind":"option","type":{"kind":"option","type":{"kind":"u32"}}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"result","ok":{"kind":"u32"},"err":{"kind":"string"}}}],"results":[{"kind":"result","ok":{"kind":"u32"},"err":{"kind":"string"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"list","element":{"kind":"u8"}}}],"results":[{"kind":"list","element":{"kind":"u8"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"list","element":{"kind":"string"}}}],"results":[{"kind":"list","element":{"kind":"string"}}],"async":false},{"kind":"func","params":[{"label":"v","type":{"kind":"tuple","elements":[{"kind":"u32"},{"kind":"string"},{"kind":"f64"}]}}],"results":[{"kind":"tuple","elements":[{"kind":"u32"},{"kind":"string"},{"kind":"f64"}]}],"async":false}],"resourceTables":[],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"lifted-func","name":"echo-bool","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-bool","space":"func"}},"options":0,"type":0},{"kind":"lifted-func","name":"echo-u64","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-s64","space":"func"}},"options":1,"type":1},{"kind":"lifted-func","name":"echo-s64","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-s64","space":"func"}},"options":1,"type":2},{"kind":"lifted-func","name":"echo-f32","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-f32","space":"func"}},"options":2,"type":3},{"kind":"lifted-func","name":"echo-f64","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-f64","space":"func"}},"options":3,"type":4},{"kind":"lifted-func","name":"echo-char","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-char","space":"func"}},"options":0,"type":5},{"kind":"lifted-func","name":"echo-string","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-list-u8","space":"func"}},"options":4,"type":6},{"kind":"lifted-func","name":"echo-record","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-record","space":"func"}},"options":5,"type":7},{"kind":"lifted-func","name":"echo-variant","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-variant","space":"func"}},"options":6,"type":8},{"kind":"lifted-func","name":"echo-enum","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-enum","space":"func"}},"options":0,"type":9},{"kind":"lifted-func","name":"echo-flags","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-flags","space":"func"}},"options":0,"type":10},{"kind":"lifted-func","name":"echo-option","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-option","space":"func"}},"options":7,"type":11},{"kind":"lifted-func","name":"echo-option-nested","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-option-nested","space":"func"}},"options":8,"type":12},{"kind":"lifted-func","name":"echo-result","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-result","space":"func"}},"options":7,"type":13},{"kind":"lifted-func","name":"echo-list-u8","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-list-u8","space":"func"}},"options":4,"type":14},{"kind":"lifted-func","name":"echo-list-string","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-list-string","space":"func"}},"options":9,"type":15},{"kind":"lifted-func","name":"echo-tuple","coreDef":{"kind":"export","instance":0,"item":{"name":"echo-tuple","space":"func"}},"options":10,"type":16}],"worldDigest":"sha256:86114de37a4752373d7b3a0a6efb78920ee01ae6eb28bbd95b1f6f50703e652e"},"adapters":[]} \ No newline at end of file diff --git a/runtime/tests/cancel_bracket_race_test.ts b/runtime/tests/cancel_bracket_race_test.ts deleted file mode 100644 index 4f96999..0000000 --- a/runtime/tests/cancel_bracket_race_test.ts +++ /dev/null @@ -1,165 +0,0 @@ -// #92 (test half): a seeded-shuffle regression pinning the "cancel-delivery -// timing" window named in issue #92. -// -// The window: under jspi, `Task.requestCancellation`'s -// `SuspensionPoint.resume()` only *settles* the suspended activation's -// Promise; the resumed wasm frame's own continuation (its remaining -// built-ins, `exit-sync-call`, etc.) runs on a LATER microtask. A concurrent -// host EXPORT call can enter the same instance in between. -// -// 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. - -import { assertEq } from "./support/asserts.ts"; -import { createWaitableSetWait } from "../src/intrinsics/async_builtins.ts"; -import { - createLiftedFunction, - newStats, - type ResolvedOptions, -} from "../src/exec/boundary.ts"; -import { entryRefusal } from "../src/task/scheduler.ts"; -import { - ComponentInstanceState, - popCurrentThread, - pushCurrentThread, - Store, - Task, - type TaskOptions, - Thread, - WaitableSet, -} from "../src/task/mod.ts"; -import type { FuncType } from "../src/cabi/types.ts"; - -const ASYNC_FT: FuncType = { params: [], results: [], async: true }; -const CALLBACK_OPTS: TaskOptions = { - async_: true, - callback: true, - stringEncoding: "utf8", - memory: null, -}; - -Deno.test( - "#92: a concurrent export call entering between a cancellation delivery " + - "and the resumed jspi activation's continuation is admitted, with no " + - "double-resume and consistent final state", - async () => { - const store = new Store(); - const inst = new ComponentInstanceState(0, store); - const wset = new WaitableSet(); - const seti = inst.handles.add(wset); - const memory = new WebAssembly.Memory({ initial: 1 }); - const view = { - addrType: "i32" as const, - get bytes() { - return new Uint8Array(memory.buffer); - }, - get view() { - return new DataView(memory.buffer); - }, - get length() { - return memory.buffer.byteLength; - }, - ptrType: () => "i32" as const, - ptrSize: () => 4 as const, - }; - const opts: ResolvedOptions = { - stringEncoding: "utf8", - // deno-lint-ignore no-explicit-any - memory: view as any, - realloc: null, - postReturn: null, - callback: null, - async: true, - cancellable: true, // the caller's `cancellable` canonical option. - coreType: { params: ["i32", "i32"], results: ["i32"] }, - instance: inst, - }; - const ctx = { - componentInstance: () => inst, - options: () => opts, - resultTypes: () => [], - }; - const task = new Task(ASYNC_FT, CALLBACK_OPTS, inst, () => [], () => {}); - task.state = "started"; - const thread = new Thread(task, (function* () {})()); - - // Park task A at a cancellable `waitable-set.wait` (jspi mode) — no - // event pending, so this is SITE 2's genuine suspension. - const wait = createWaitableSetWait({ options: 0 }, ctx, inst, "jspi"); - pushCurrentThread(thread); - let parked: unknown; - try { - parked = wait(seti, 0); - } finally { - popCurrentThread(thread); - } - assertEq(typeof parked, "object"); // a Promise, per blockCurrentActivation. - assertEq(store.waiting.length, 1); - - // Sanity: nothing refuses entry while the activation is parked. - assertEq(entryRefusal(inst, null, "base"), null); - - // Deliver the cancellation exactly as `Task.requestCancellation` does: - // it finds the parked SuspensionPoint as a candidate (registered with - // `task === task` and `cancellable === true`) and resumes it. - task.requestCancellation(null); - - // THE WINDOW: the resumed activation's own continuation (the `.then()` - // the engine attached to the settled Promise) has not run yet, and the - // instance admits entry. This is exactly the gap #92 names. - assertEq(entryRefusal(inst, null, "base"), null); - assertEq(task.state, "cancel-delivered"); - - // Drive a concurrent EXPORT call into the SAME instance through the real - // 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 = { - stringEncoding: "utf8", - memory: null, - realloc: null, - postReturn: null, - callback: null, - async: false, - cancellable: false, - coreType: { params: [], results: [] }, - instance: inst, - }; - const lifted = createLiftedFunction({ - name: "concurrent-export", - ft: syncFt, - opts: exportOpts, - core: () => undefined, - stats: newStats(), - }); - let raised: unknown; - try { - lifted(); - } catch (e) { - raised = e; - } - // The concurrent entry is admitted — now by the merged reference's own - // rule, not merely as an observed quirk. - assertEq(raised, undefined); - - // Let the cancelled activation's own continuation actually run (the - // microtask the engine scheduled when `resume()` settled its Promise), - // and confirm no double-resume assertion fired and the final state is - // consistent: the SuspensionPoint is done, the WaitableSet was never - // touched by the concurrent export (it did not join or wait), and no - // exception escaped this far. - let sawRejection: unknown; - await (parked as Promise).catch((e) => { - sawRejection = e; - }); - // A cancelled `waitable-set.wait` resolves with TASK_CANCELLED — not a - // rejection — so nothing should have been caught here. - assertEq(sawRejection, undefined); - assertEq(store.waiting.length, 0); - }, -); diff --git a/runtime/tests/dtor_normalization_test.ts b/runtime/tests/dtor_normalization_test.ts index 2f73a20..515c209 100644 --- a/runtime/tests/dtor_normalization_test.ts +++ b/runtime/tests/dtor_normalization_test.ts @@ -40,7 +40,6 @@ Deno.test("#160: a dtor parked on a scheduler-resumable suspension point complet store, task, readyFunc: () => flag, - cancellable: false, produce: () => { finished = true; assertEq(rep, 77); diff --git a/runtime/tests/embedder/streams_test.ts b/runtime/tests/embedder/streams_test.ts index 6cfffcb..4324c72 100644 --- a/runtime/tests/embedder/streams_test.ts +++ b/runtime/tests/embedder/streams_test.ts @@ -274,6 +274,51 @@ Deno.test({ }, }); +Deno.test("streams: writer close before lowering is delivered after bind", async () => { + const { stream, writer } = Stream.create(); + await writer.close(); + const codec = { + element: { kind: "u32" } as const, + where: "close-before-lower", + toHost: (v: unknown) => v as number, + fromHost: (v: number) => v, + }; + const value = stream.takeValue(codec); + const lifted = Stream.fromLifted(value, codec); + assertEq(await lifted.read(1), []); +}); + +for (const operation of ["write", "writeAll", "writeDirect"] as const) { + Deno.test(`streams: close retracts queued ${operation} before bind`, async () => { + const { stream, writer } = Stream.create(); + let callbacks = 0; + const pending = operation === "write" + ? writer.write([7]) + : operation === "writeAll" + ? writer.writeAll([7]) + : writer.writeDirect(() => { + callbacks++; + return "done"; + }); + await writer.close(); + assertEq(await pending, 0); + const codec = { + element: operation === "writeDirect" + ? { kind: "u8" } as const + : { kind: "u32" } as const, + where: `close-before-${operation}`, + toHost: (v: unknown) => v as number, + fromHost: (v: number) => v, + }; + const value = stream.takeValue(codec); + const lifted = Stream.fromLifted(value, codec); + const closed = await lifted.read(1); + assertEq(closed.length, 0); + assertEq(callbacks, 0); + assertEq(await writer.write([8]), 0, "closed writer stays non-busy"); + }); +} + Deno.test({ name: "streams: lift-side cross-store mismatch is a host TypeError", ignore: false, diff --git a/runtime/tests/enter_sync_call_reentrance_test.ts b/runtime/tests/enter_sync_call_reentrance_test.ts index cac3b07..4325fb8 100644 --- a/runtime/tests/enter_sync_call_reentrance_test.ts +++ b/runtime/tests/enter_sync_call_reentrance_test.ts @@ -81,7 +81,7 @@ const C = 1; Deno.test("enter-sync-call: an idle sibling callee is enterable", () => { const { enter, exit, inst } = fixture(); - enter(A, 0, C); + enter(0, C); assertEq(currentTask().inst === inst(C), true, "callee task is current"); assertEq(currentThread().storage, [0, 0], "callee slots start fresh"); exit(); @@ -92,10 +92,10 @@ Deno.test("enter-sync-call: a sibling cycle A -> C -> A no longer traps (CM#705) // 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, inst } = fixture(); - enter(A, 0, C); + enter(0, C); const c = currentThread(); c.storage[0] = 41; - enter(C, 0, A); + enter(0, A); assertEq(currentTask().inst === inst(A), true); assertEq(currentThread().storage, [0, 0]); exit(); @@ -110,7 +110,7 @@ Deno.test("enter-sync-call: a POISONED callee is refused, naming the trap", () = notifyInstancePoisoned(inst(A), new Error("earlier boom")); let msg = ""; try { - enter(C, 0, A); + enter(0, A); } catch (e) { msg = String((e as Error).message ?? e); } @@ -124,20 +124,22 @@ Deno.test("enter-sync-call: a POISONED callee is refused, naming the trap", () = assertEq(msg.includes("earlier boom"), true, msg); }); -Deno.test("enter-sync-call: a poisoned instance calling ITSELF passes vacuously", () => { +Deno.test("enter-sync-call: a poisoned current instance calling itself passes vacuously", () => { // `entryRefusal`'s `caller !== callee` guard passes a self-call // vacuously, even against a marked instance. const { enter, exit, inst } = fixture(); notifyInstancePoisoned(inst(A), new Error("earlier boom")); - enter(A, 0, A); - exit(); + withActivation({ storage: [0, 0], task: { inst: inst(A) } }, () => { + enter(0, A); + exit(); + }); }); Deno.test("enter-sync-call: an acyclic sibling chain A -> B -> C never traps", () => { const { enter, exit, inst } = fixture(); const B = 2; - enter(A, 0, B); - enter(B, 0, C); + enter(0, B); + enter(0, C); assertEq(currentTask().inst === inst(C), true); exit(); assertEq(currentTask().inst === inst(B), true); @@ -152,7 +154,7 @@ Deno.test("enter-sync-call: trap unwind retires nested task identity", () => { let caught: unknown; try { withActivation(parent, () => { - enter(A, 0, C); + enter(0, C); assertEq(currentTask().inst === inst(C), true); throw boom; }); @@ -184,10 +186,9 @@ Deno.test("nested sync suspension keeps physical owner and logical task separate store, task: logical.task, readyFunc: () => true, - cancellable: false, produce: () => undefined, }); - yield { readyFunc: null, cancellable: false, awaitValue: promise }; + yield { readyFunc: null, awaitValue: promise }; logical.finish(); })(), ); @@ -232,10 +233,9 @@ Deno.test("nested sync post-hop trap retires persistent logical descendants", as store, task: logical.task, readyFunc: () => true, - cancellable: false, produce: () => undefined, }); - yield { readyFunc: null, cancellable: false, awaitValue: promise }; + yield { readyFunc: null, awaitValue: promise }; throw boom; })(), ); diff --git a/runtime/tests/entry_deferral_test.ts b/runtime/tests/entry_deferral_test.ts index fdd2692..34c2d95 100644 --- a/runtime/tests/entry_deferral_test.ts +++ b/runtime/tests/entry_deferral_test.ts @@ -52,7 +52,6 @@ import { assertEq } from "./support/asserts.ts"; import { type BlockRequest, - type Cancelled, ComponentInstanceState, SubtaskState, Task, @@ -77,9 +76,9 @@ const CALLBACK_OPTS: TaskOptions = { function spawn( task: Task, - body: (t: Thread) => Generator, + body: (t: Thread) => Generator, ): Thread { - function* threadBody(): Generator { + function* threadBody(): Generator { yield* body(thread); } const thread: Thread = new Thread(task, threadBody()); @@ -117,7 +116,7 @@ Deno.test( // `inst.exclusive_thread`. Same as wasmtime suspending its guest fiber // with the `enter_instance` bracket still open (concurrent.rs // `wait_for_event` :2199-2213). - yield* thread.waitUntil(() => readReady, false); + yield* thread.waitUntil(() => readReady); pumpSawWhileParked = pokeEntered; pump.exitImplicitThread(thread); }); @@ -218,7 +217,7 @@ Deno.test( holder.start(); // Parked, but READY: from any other task's point of view this is // drainable work. - yield* thread.waitUntil(() => go, false); + yield* thread.waitUntil(() => go); holder.return_([]); holder.exitImplicitThread(thread); }); diff --git a/runtime/tests/event_driven_drain_test.ts b/runtime/tests/event_driven_drain_test.ts index 49dd656..7a0f4bb 100644 --- a/runtime/tests/event_driven_drain_test.ts +++ b/runtime/tests/event_driven_drain_test.ts @@ -298,6 +298,7 @@ Deno.test({ const park = { owner: hop, task: hop.task, + boundaryReturned: true, ready: () => false, waiting: () => true, resume() {}, @@ -355,6 +356,7 @@ Deno.test("ordinary service stops when the first tail creates an unqueued hop", const park = { owner: hop, task: hop.task, + boundaryReturned: true, ready: () => false, waiting: () => true, resume() {}, @@ -461,7 +463,7 @@ Deno.test("finite guest work continues past task.return and a fairness yield", a (function* () { for (let i = 0; i < 20; i++) { steps++; - yield { readyFunc: () => true, cancellable: false }; + yield { readyFunc: () => true }; } finished = true; })(), diff --git a/runtime/tests/fixtures/old-v5-linked.component.wasm b/runtime/tests/fixtures/old-v5-linked.component.wasm new file mode 100644 index 0000000..1af83ad Binary files /dev/null and b/runtime/tests/fixtures/old-v5-linked.component.wasm differ diff --git a/runtime/tests/fixtures/old-v5-linked.envelope.json b/runtime/tests/fixtures/old-v5-linked.envelope.json new file mode 100644 index 0000000..c855df2 --- /dev/null +++ b/runtime/tests/fixtures/old-v5-linked.envelope.json @@ -0,0 +1 @@ +{"plan":{"formatVersion":5,"producer":{"shimVersion":"0.1.0","wasmtimeEnviron":"49.0.0-dev+4675ee1","features":["cm-async","cm-async-stackful","cm-more-async-builtins","cm-error-context","cm-fixed-length-lists","cm-map","cm-implements","cm-threading"]},"component":{"sha256":"5426c2bda0374ec37dc5ab8faa525543c443c087acf6bde7f6b028f9ded8a3b9","len":8175},"modules":[{"kind":"embedded","offset":21,"len":71},{"kind":"embedded","offset":101,"len":1385},{"kind":"embedded","offset":3189,"len":38},{"kind":"embedded","offset":3236,"len":1602},{"kind":"adapter","file":"adapters/4.wasm","len":4213,"intrinsics":[{"module":"flags","name":"instance1","category":"instance-flags","def":{"kind":"instance-flags","instance":1}},{"module":"flags","name":"instance2","category":"instance-flags","def":{"kind":"instance-flags","instance":2}},{"module":"callee","name":"adapter0","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"sync-4-param","space":"func"}}},{"module":"runtime","name":"trap23","category":"trampoline:trap","def":{"kind":"trampoline","index":4}},{"module":"async","name":"enter-sync-call","category":"trampoline:enter-sync-call","def":{"kind":"trampoline","index":5}},{"module":"async","name":"exit-sync-call","category":"trampoline:exit-sync-call","def":{"kind":"trampoline","index":6}},{"module":"runtime","name":"trap49","category":"trampoline:trap","def":{"kind":"trampoline","index":7}},{"module":"memory","name":"m0","category":"core-def","def":{"kind":"export","instance":2,"item":{"name":"mem","space":"memory"}}},{"module":"async","name":"[start-call]adapter1","category":"trampoline:async-start-call","def":{"kind":"trampoline","index":8}},{"module":"sync","name":"[prepare-call]adapter1","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":9}},{"module":"callee","name":"adapter2","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"sync-5-param","space":"func"}}},{"module":"runtime","name":"trap33","category":"trampoline:trap","def":{"kind":"trampoline","index":10}},{"module":"runtime","name":"trap1","category":"trampoline:trap","def":{"kind":"trampoline","index":11}},{"module":"sync","name":"[prepare-call]adapter3","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":12}},{"module":"memory","name":"m1","category":"core-def","def":{"kind":"export","instance":0,"item":{"name":"mem","space":"memory"}}},{"module":"realloc","name":"f11","category":"core-def","def":{"kind":"export","instance":0,"item":{"name":"realloc","space":"func"}}},{"module":"callee","name":"adapter4","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"sync-17-param","space":"func"}}},{"module":"context","name":"get0","category":"unsafe-intrinsic:context-get-i32-0","def":{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-0"}},{"module":"context","name":"get1","category":"unsafe-intrinsic:context-get-i32-1","def":{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-1"}},{"module":"context","name":"set0","category":"unsafe-intrinsic:context-set-i32-0","def":{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-0"}},{"module":"context","name":"set1","category":"unsafe-intrinsic:context-set-i32-1","def":{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-1"}},{"module":"sync","name":"[prepare-call]adapter5","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}},{"module":"callee","name":"adapter6","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"sync-1-result","space":"func"}}},{"module":"sync","name":"[prepare-call]adapter7","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":12}},{"module":"callee","name":"adapter8","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"sync-16-result","space":"func"}}},{"module":"sync","name":"[prepare-call]adapter9","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}},{"module":"callee","name":"adapter10","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"sync-17-result","space":"func"}}},{"module":"sync","name":"[prepare-call]adapter11","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}},{"module":"callback","name":"f24","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"unreachable-cb","space":"func"}}},{"module":"flags","name":"instance3","category":"instance-flags","def":{"kind":"instance-flags","instance":3}},{"module":"callee","name":"adapter12","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-4-param","space":"func"}}},{"module":"sync","name":"[start-call]adapter12","category":"trampoline:sync-start-call","def":{"kind":"trampoline","index":14}},{"module":"sync","name":"[prepare-call]adapter12","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":9}}]},{"kind":"adapter","file":"adapters/5.wasm","len":4368,"intrinsics":[{"module":"flags","name":"instance1","category":"instance-flags","def":{"kind":"instance-flags","instance":1}},{"module":"callback","name":"f0","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"unreachable-cb","space":"func"}}},{"module":"flags","name":"instance3","category":"instance-flags","def":{"kind":"instance-flags","instance":3}},{"module":"memory","name":"m0","category":"core-def","def":{"kind":"export","instance":5,"item":{"name":"mem","space":"memory"}}},{"module":"callee","name":"adapter13","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-4-param","space":"func"}}},{"module":"async","name":"[start-call]adapter13","category":"trampoline:async-start-call","def":{"kind":"trampoline","index":15}},{"module":"sync","name":"[prepare-call]adapter13","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":9}},{"module":"callee","name":"adapter14","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-5-param","space":"func"}}},{"module":"sync","name":"[start-call]adapter14","category":"trampoline:sync-start-call","def":{"kind":"trampoline","index":14}},{"module":"sync","name":"[prepare-call]adapter14","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":16}},{"module":"runtime","name":"trap33","category":"trampoline:trap","def":{"kind":"trampoline","index":10}},{"module":"runtime","name":"trap1","category":"trampoline:trap","def":{"kind":"trampoline","index":11}},{"module":"sync","name":"[prepare-call]adapter15","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":12}},{"module":"memory","name":"m1","category":"core-def","def":{"kind":"export","instance":0,"item":{"name":"mem","space":"memory"}}},{"module":"realloc","name":"f10","category":"core-def","def":{"kind":"export","instance":0,"item":{"name":"realloc","space":"func"}}},{"module":"callee","name":"adapter16","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-17-param","space":"func"}}},{"module":"context","name":"get0","category":"unsafe-intrinsic:context-get-i32-0","def":{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-0"}},{"module":"context","name":"get1","category":"unsafe-intrinsic:context-get-i32-1","def":{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-1"}},{"module":"context","name":"set0","category":"unsafe-intrinsic:context-set-i32-0","def":{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-0"}},{"module":"context","name":"set1","category":"unsafe-intrinsic:context-set-i32-1","def":{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-1"}},{"module":"sync","name":"[start-call]adapter16","category":"trampoline:sync-start-call","def":{"kind":"trampoline","index":14}},{"module":"sync","name":"[prepare-call]adapter16","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}},{"module":"sync","name":"[prepare-call]adapter17","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}},{"module":"callee","name":"adapter18","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-1-result","space":"func"}}},{"module":"sync","name":"[start-call]adapter18","category":"trampoline:sync-start-call","def":{"kind":"trampoline","index":17}},{"module":"sync","name":"[prepare-call]adapter18","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":18}},{"module":"sync","name":"[prepare-call]adapter19","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":12}},{"module":"callee","name":"adapter20","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-16-result","space":"func"}}},{"module":"sync","name":"[start-call]adapter20","category":"trampoline:sync-start-call","def":{"kind":"trampoline","index":14}},{"module":"sync","name":"[prepare-call]adapter20","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":12}},{"module":"sync","name":"[prepare-call]adapter21","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":12}},{"module":"callee","name":"adapter22","category":"core-def","def":{"kind":"export","instance":1,"item":{"name":"async-17-result","space":"func"}}},{"module":"sync","name":"[start-call]adapter22","category":"trampoline:sync-start-call","def":{"kind":"trampoline","index":14}},{"module":"sync","name":"[prepare-call]adapter22","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}},{"module":"sync","name":"[prepare-call]adapter23","category":"trampoline:prepare-call","def":{"kind":"trampoline","index":13}}]}],"initializers":[{"op":"instantiate-module","module":0,"instance":1,"args":[]},{"op":"extract-memory","index":0,"export":{"instance":0,"item":{"name":"mem","space":"memory"}}},{"op":"instantiate-module","module":1,"instance":1,"args":[{"kind":"export","instance":0,"item":{"name":"mem","space":"memory"}},{"kind":"trampoline","index":0},{"kind":"trampoline","index":1},{"kind":"trampoline","index":2},{"kind":"trampoline","index":3}]},{"op":"instantiate-module","module":2,"instance":2,"args":[]},{"op":"extract-callback","index":0,"def":{"kind":"export","instance":1,"item":{"name":"unreachable-cb","space":"func"}}},{"op":"instantiate-module","module":4,"instance":null,"args":[{"kind":"instance-flags","instance":1},{"kind":"instance-flags","instance":2},{"kind":"export","instance":1,"item":{"name":"sync-4-param","space":"func"}},{"kind":"trampoline","index":4},{"kind":"trampoline","index":5},{"kind":"trampoline","index":6},{"kind":"trampoline","index":7},{"kind":"export","instance":2,"item":{"name":"mem","space":"memory"}},{"kind":"trampoline","index":8},{"kind":"trampoline","index":9},{"kind":"export","instance":1,"item":{"name":"sync-5-param","space":"func"}},{"kind":"trampoline","index":10},{"kind":"trampoline","index":11},{"kind":"trampoline","index":12},{"kind":"export","instance":0,"item":{"name":"mem","space":"memory"}},{"kind":"export","instance":0,"item":{"name":"realloc","space":"func"}},{"kind":"export","instance":1,"item":{"name":"sync-17-param","space":"func"}},{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-0"},{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-1"},{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-0"},{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-1"},{"kind":"trampoline","index":13},{"kind":"export","instance":1,"item":{"name":"sync-1-result","space":"func"}},{"kind":"trampoline","index":12},{"kind":"export","instance":1,"item":{"name":"sync-16-result","space":"func"}},{"kind":"trampoline","index":13},{"kind":"export","instance":1,"item":{"name":"sync-17-result","space":"func"}},{"kind":"trampoline","index":13},{"kind":"export","instance":1,"item":{"name":"unreachable-cb","space":"func"}},{"kind":"instance-flags","instance":3},{"kind":"export","instance":1,"item":{"name":"async-4-param","space":"func"}},{"kind":"trampoline","index":14},{"kind":"trampoline","index":9}]},{"op":"instantiate-module","module":3,"instance":2,"args":[{"kind":"export","instance":2,"item":{"name":"mem","space":"memory"}},{"kind":"export","instance":3,"item":{"name":"adapter0","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter1","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter2","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter3","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter4","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter5","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter6","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter7","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter8","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter9","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter10","space":"func"}},{"kind":"export","instance":3,"item":{"name":"adapter11","space":"func"}}]},{"op":"instantiate-module","module":2,"instance":3,"args":[]},{"op":"instantiate-module","module":5,"instance":null,"args":[{"kind":"instance-flags","instance":1},{"kind":"export","instance":1,"item":{"name":"unreachable-cb","space":"func"}},{"kind":"instance-flags","instance":3},{"kind":"export","instance":5,"item":{"name":"mem","space":"memory"}},{"kind":"export","instance":1,"item":{"name":"async-4-param","space":"func"}},{"kind":"trampoline","index":15},{"kind":"trampoline","index":9},{"kind":"export","instance":1,"item":{"name":"async-5-param","space":"func"}},{"kind":"trampoline","index":14},{"kind":"trampoline","index":16},{"kind":"trampoline","index":10},{"kind":"trampoline","index":11},{"kind":"trampoline","index":12},{"kind":"export","instance":0,"item":{"name":"mem","space":"memory"}},{"kind":"export","instance":0,"item":{"name":"realloc","space":"func"}},{"kind":"export","instance":1,"item":{"name":"async-17-param","space":"func"}},{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-0"},{"kind":"unsafe-intrinsic","intrinsic":"context-get-i32-1"},{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-0"},{"kind":"unsafe-intrinsic","intrinsic":"context-set-i32-1"},{"kind":"trampoline","index":14},{"kind":"trampoline","index":13},{"kind":"trampoline","index":13},{"kind":"export","instance":1,"item":{"name":"async-1-result","space":"func"}},{"kind":"trampoline","index":17},{"kind":"trampoline","index":18},{"kind":"trampoline","index":12},{"kind":"export","instance":1,"item":{"name":"async-16-result","space":"func"}},{"kind":"trampoline","index":14},{"kind":"trampoline","index":12},{"kind":"trampoline","index":12},{"kind":"export","instance":1,"item":{"name":"async-17-result","space":"func"}},{"kind":"trampoline","index":14},{"kind":"trampoline","index":13},{"kind":"trampoline","index":13}]},{"op":"instantiate-module","module":3,"instance":3,"args":[{"kind":"export","instance":5,"item":{"name":"mem","space":"memory"}},{"kind":"export","instance":3,"item":{"name":"adapter12","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter13","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter14","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter15","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter16","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter17","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter18","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter19","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter20","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter21","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter22","space":"func"}},{"kind":"export","instance":6,"item":{"name":"adapter23","space":"func"}}]}],"trampolines":[{"kind":"task-return","index":0,"instance":1,"results":0,"resultType":0,"options":0},{"kind":"task-return","index":1,"instance":1,"results":1,"resultType":1,"options":1},{"kind":"task-return","index":2,"instance":1,"results":3,"resultType":2,"options":2},{"kind":"task-return","index":3,"instance":1,"results":5,"resultType":3,"options":3},{"kind":"trap","index":4,"code":23},{"kind":"enter-sync-call","index":5},{"kind":"exit-sync-call","index":6},{"kind":"trap","index":7,"code":49},{"kind":"async-start-call","index":8,"callback":null,"postReturn":null},{"kind":"prepare-call","index":9,"memory":null},{"kind":"trap","index":10,"code":33},{"kind":"trap","index":11,"code":1},{"kind":"prepare-call","index":12,"memory":null},{"kind":"prepare-call","index":13,"memory":0},{"kind":"sync-start-call","index":14,"callback":0},{"kind":"async-start-call","index":15,"callback":0,"postReturn":null},{"kind":"prepare-call","index":16,"memory":null},{"kind":"sync-start-call","index":17,"callback":0},{"kind":"prepare-call","index":18,"memory":null}],"canonicalOptions":[{"instance":1,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":[],"results":[]}},{"instance":1,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["f64"],"results":[]}},{"instance":1,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32","i64","f32","f64","i32","i64","f32","f64","i32","i64","f32","f64","i32","i64","f32","f64"],"results":[]}},{"instance":1,"stringEncoding":"utf8","memory":0,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":["i32"],"results":[]}},{"instance":2,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":[],"results":["i32"]}},{"instance":3,"stringEncoding":"utf8","memory":null,"realloc":null,"postReturn":null,"callback":null,"async":false,"cancellable":false,"coreType":{"params":[],"results":["i32"]}}],"types":[{"kind":"tuple","elements":[]},{"kind":"tuple","elements":[{"kind":"f64"}]},{"kind":"tuple","elements":[{"kind":"tuple","elements":[{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"}]}]},{"kind":"tuple","elements":[{"kind":"tuple","elements":[{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"},{"kind":"u64"},{"kind":"f32"},{"kind":"f64"},{"kind":"u32"}]}]},{"kind":"func","params":[],"results":[{"kind":"u32"}],"async":true}],"resourceTables":[],"streamTables":[],"futureTables":[],"errorContextTables":[],"importedResources":[],"imports":[],"exports":[{"kind":"lifted-func","name":"sync-calls-sync-4-param","coreDef":{"kind":"export","instance":4,"item":{"name":"call-sync-4-param","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"sync-calls-async-4-param","coreDef":{"kind":"export","instance":7,"item":{"name":"call-sync-4-param","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"async-calls-sync-4-param","coreDef":{"kind":"export","instance":4,"item":{"name":"call-async-4-param","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"async-calls-async-4-param","coreDef":{"kind":"export","instance":7,"item":{"name":"call-async-4-param","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"sync-calls-sync-5-param","coreDef":{"kind":"export","instance":4,"item":{"name":"call-sync-5-param","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"sync-calls-async-5-param","coreDef":{"kind":"export","instance":7,"item":{"name":"call-sync-5-param","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"async-calls-sync-5-param","coreDef":{"kind":"export","instance":4,"item":{"name":"call-async-5-param","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"async-calls-async-5-param","coreDef":{"kind":"export","instance":7,"item":{"name":"call-async-5-param","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"sync-calls-sync-17-param","coreDef":{"kind":"export","instance":4,"item":{"name":"call-sync-17-param","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"sync-calls-async-17-param","coreDef":{"kind":"export","instance":7,"item":{"name":"call-sync-17-param","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"async-calls-sync-17-param","coreDef":{"kind":"export","instance":4,"item":{"name":"call-async-17-param","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"async-calls-async-17-param","coreDef":{"kind":"export","instance":7,"item":{"name":"call-async-17-param","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"sync-calls-sync-1-result","coreDef":{"kind":"export","instance":4,"item":{"name":"call-sync-1-result","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"sync-calls-async-1-result","coreDef":{"kind":"export","instance":7,"item":{"name":"call-sync-1-result","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"async-calls-sync-1-result","coreDef":{"kind":"export","instance":4,"item":{"name":"call-async-1-result","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"async-calls-async-1-result","coreDef":{"kind":"export","instance":7,"item":{"name":"call-async-1-result","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"sync-calls-sync-16-result","coreDef":{"kind":"export","instance":4,"item":{"name":"call-sync-16-result","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"sync-calls-async-16-result","coreDef":{"kind":"export","instance":7,"item":{"name":"call-sync-16-result","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"async-calls-sync-16-result","coreDef":{"kind":"export","instance":4,"item":{"name":"call-async-16-result","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"async-calls-async-16-result","coreDef":{"kind":"export","instance":7,"item":{"name":"call-async-16-result","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"sync-calls-sync-17-result","coreDef":{"kind":"export","instance":4,"item":{"name":"call-sync-17-result","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"sync-calls-async-17-result","coreDef":{"kind":"export","instance":7,"item":{"name":"call-sync-17-result","space":"func"}},"options":5,"type":4},{"kind":"lifted-func","name":"async-calls-sync-17-result","coreDef":{"kind":"export","instance":4,"item":{"name":"call-async-17-result","space":"func"}},"options":4,"type":4},{"kind":"lifted-func","name":"async-calls-async-17-result","coreDef":{"kind":"export","instance":7,"item":{"name":"call-async-17-result","space":"func"}},"options":5,"type":4}],"worldDigest":"sha256:c1d8eff01ed4fce64c981faa011eec16d38a0f925d3ad3b75067c54450f0790c"},"adapters":[{"file":"adapters/4.wasm","wasm":"AGFzbQEAAAABhwETYAR/fn18AGAAAGADf39/AGAEf359fAR/fn18YAR/fn18AX9gBHB/f38Bf2AMcHB/f39/f39/fn18AGAFf359fH8AYAF/BX9+fXx/YAF/AX9gCXBwf39/f39/fwBgBH9/f38Bf2ABfwBgAAF/YAABfGACfH8AYAJ/fwBgA39/fwF/YAJwfwACrQUhBWZsYWdzCWluc3RhbmNlMQN/AQVmbGFncwlpbnN0YW5jZTIDfwEGY2FsbGVlCGFkYXB0ZXIwAAAHcnVudGltZQZ0cmFwMjMAAQVhc3luYw9lbnRlci1zeW5jLWNhbGwAAgVhc3luYw5leGl0LXN5bmMtY2FsbAABB3J1bnRpbWUGdHJhcDQ5AAEGbWVtb3J5Am0wAgAABWFzeW5jFFtzdGFydC1jYWxsXWFkYXB0ZXIxAAUEc3luYxZbcHJlcGFyZS1jYWxsXWFkYXB0ZXIxAAYGY2FsbGVlCGFkYXB0ZXIyAAcHcnVudGltZQZ0cmFwMzMAAQdydW50aW1lBXRyYXAxAAEEc3luYxZbcHJlcGFyZS1jYWxsXWFkYXB0ZXIzAAoGbWVtb3J5Am0xAgAAB3JlYWxsb2MDZjExAAsGY2FsbGVlCGFkYXB0ZXI0AAwHY29udGV4dARnZXQwAA0HY29udGV4dARnZXQxAA0HY29udGV4dARzZXQwAAwHY29udGV4dARzZXQxAAwEc3luYxZbcHJlcGFyZS1jYWxsXWFkYXB0ZXI1AAoGY2FsbGVlCGFkYXB0ZXI2AA4Ec3luYxZbcHJlcGFyZS1jYWxsXWFkYXB0ZXI3AAoGY2FsbGVlCGFkYXB0ZXI4AA0Ec3luYxZbcHJlcGFyZS1jYWxsXWFkYXB0ZXI5AAoGY2FsbGVlCWFkYXB0ZXIxMAANBHN5bmMXW3ByZXBhcmUtY2FsbF1hZGFwdGVyMTEACghjYWxsYmFjawNmMjQAEQVmbGFncwlpbnN0YW5jZTMDfwEGY2FsbGVlCWFkYXB0ZXIxMgAEBHN5bmMVW3N0YXJ0LWNhbGxdYWRhcHRlcjEyABIEc3luYxdbcHJlcGFyZS1jYWxsXWFkYXB0ZXIxMgAGAxwbAAMBBAcIAQkMCQEJDgEPCQwBEAkMARAJAwEAB60FIghhZGFwdGVyMAAcFVthc3luYy1zdGFydF1hZGFwdGVyMQAdFlthc3luYy1yZXR1cm5dYWRhcHRlcjEAHghhZGFwdGVyMQAfCGFkYXB0ZXIyACAVW2FzeW5jLXN0YXJ0XWFkYXB0ZXIzACEWW2FzeW5jLXJldHVybl1hZGFwdGVyMwAiCGFkYXB0ZXIzACMIYWRhcHRlcjQAJBVbYXN5bmMtc3RhcnRdYWRhcHRlcjUAJRZbYXN5bmMtcmV0dXJuXWFkYXB0ZXI1ACYIYWRhcHRlcjUAJwhhZGFwdGVyNgAoFVthc3luYy1zdGFydF1hZGFwdGVyNwApFlthc3luYy1yZXR1cm5dYWRhcHRlcjcAKghhZGFwdGVyNwArCGFkYXB0ZXI4ACwVW2FzeW5jLXN0YXJ0XWFkYXB0ZXI5AC0WW2FzeW5jLXJldHVybl1hZGFwdGVyOQAuCGFkYXB0ZXI5AC8JYWRhcHRlcjEwADAWW2FzeW5jLXN0YXJ0XWFkYXB0ZXIxMQAxF1thc3luYy1yZXR1cm5dYWRhcHRlcjExADIJYWRhcHRlcjExADMWW2FzeW5jLXN0YXJ0XWFkYXB0ZXIxMgA0F1thc3luYy1yZXR1cm5dYWRhcHRlcjEyADUJYWRhcHRlcjEyADYYW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIxAAAYW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIzAAcYW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXI1AAwYW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXI3ABIYW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXI5ABQZW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIxMQAWGVthZGFwdGVyLWNhbGxlZV1hZGFwdGVyMTIAGQriFBtJAQJ/AkACQB9AAQIAAkAjASIEDQAQAQALQQJBAUEBEAIjACEFQQAkACAAIAEgAiADIAUkABAAEANBACQBIAQkAQsMAQsQBAALCxgBAX8jACEEQQAkACAAIAEgAiADIAQkAAsQAQF/IwEhAEEAJAEgACQBCyYA0h3SHkECQQFBAEEBQQBBfyAAIAEgAiADEAbSAEEEQQBBABAFC0sBAn8CQAJAH0ABAgACQCMBIgUNABABAAtBAkEBQQEQAiMAIQZBACQAIAAgASACIAMgBCAGJAAQBxADQQAkASAFJAELDAELEAQACwtNAQF/IwAhAUEAJAAgAEEHcQRAEAgACwJAAkA/AK1CEIYgAK1BKK18Wg0BCxAJAAsgACgCACAAKQMIIAAqAhAgACsDGCAAKAIgIAEkAAsQAQF/IwEhAEEAJAEgACQBCyAA0iHSIkECQQFBAEEBQQBBfyAAEArSB0EFQQBBABAFC+8CAQR/AkACQB9AAQIAAkAjASIBDQAQAQALQQJBAUEBEAIjACECQQAkACAAQQdxBEAQCAALAkACQD8ArUIQhiAArUGIAa18Wg0BCxAJAAtBAEEAQQhBiAEQDSEDEA4hBEEAEA9BABAQEAsgAxAPIAQQECEEIARBB3EEQBAIAAsCQAJAPwGtQhCGIAStQYgBrXxaDQELEAkACyAEIAAoAgA2QgEAIAQgACkDCDdDAQggBCAAKgIQOEIBECAEIAArAxg5QwEYIAQgACgCIDZCASAgBCAAKQMoN0MBKCAEIAAqAjA4QgEwIAQgACsDODlDATggBCAAKAJANkIBQCAEIAApA0g3QwFIIAQgACoCUDhCAVAgBCAAKwNYOUMBWCAEIAAoAmA2QgFgIAQgACkDaDdDAWggBCAAKgJwOEIBcCAEIAArA3g5QwF4IAQgACgCgAE2QgGAASAEIAIkABAMEANBACQBIAEkAQsMAQsQBAALC74CAQN/IwAhAUEAJAAgAEEHcQRAEAgACwJAAkA/AK1CEIYgAK1BiAGtfFoNAQsQCQALQQBBAEEIQYgBEA0hAhAOIQNBABAPQQAQEBALIAIQDyADEBAhAyADQQdxBEAQCAALAkACQD8BrUIQhiADrUGIAa18Wg0BCxAJAAsgAyAAKAIANkIBACADIAApAwg3QwEIIAMgACoCEDhCARAgAyAAKwMYOUMBGCADIAAoAiA2QgEgIAMgACkDKDdDASggAyAAKgIwOEIBMCADIAArAzg5QwE4IAMgACgCQDZCAUAgAyAAKQNIN0MBSCADIAAqAlA4QgFQIAMgACsDWDlDAVggAyAAKAJgNkIBYCADIAApA2g3QwFoIAMgACoCcDhCAXAgAyAAKwN4OUMBeCADIAAoAoABNkIBgAEgAyABJAALEAEBfyMBIQBBACQBIAAkAQsgANIl0iZBAkEBQQBBAUEAQX8gABAR0gxBAUEAQQAQBQtHAgJ/AXwCfAJAH3wBAgACQCMBIgANABABAAtBAkEBQQEQAiMAIQFBACQAIAEkABASIQIQA0EAJAEgAiAAJAELDAELEAQACwsQAQF/IwAhAEEAJAAgACQACzsBAX8jASECQQAkASABQQdxBEAQCAALAkACQD8ArUIQhiABrUEIrXxaDQELEAkACyABIAA5AwAgAiQBCyAA0inSKkECQQFBAUEBQQBBfiAAEBPSEkEAQQFBABAFC70CAQJ/AkACQB9AAQIAAkAjASIBDQAQAQALQQJBAUEBEAIjACECQQAkACACJAAQFCECEANBACQBIAJBB3EEQBAIAAsCQAJAPwGtQhCGIAKtQYABrXxaDQELEAkACyAAQQdxBEAQCAALAkACQD8ArUIQhiAArUGAAa18Wg0BCxAJAAsgACACKEIBADYCACAAIAIpQwEINwMIIAAgAipCARA4AhAgACACK0MBGDkDGCAAIAIoQgEgNgIgIAAgAilDASg3AyggACACKkIBMDgCMCAAIAIrQwE4OQM4IAAgAihCAUA2AkAgACACKUMBSDcDSCAAIAIqQgFQOAJQIAAgAitDAVg5A1ggACACKEIBYDYCYCAAIAIpQwFoNwNoIAAgAipCAXA4AnAgACACK0MBeDkDeCABJAELDAELEAQACwsQAQF/IwAhAEEAJAAgACQAC4oCAQF/IwEhAkEAJAEgAEEHcQRAEAgACwJAAkA/Aa1CEIYgAK1BgAGtfFoNAQsQCQALIAFBB3EEQBAIAAsCQAJAPwCtQhCGIAGtQYABrXxaDQELEAkACyABIAAoQgEANgIAIAEgAClDAQg3AwggASAAKkIBEDgCECABIAArQwEYOQMYIAEgAChCASA2AiAgASAAKUMBKDcDKCABIAAqQgEwOAIwIAEgACtDATg5AzggASAAKEIBQDYCQCABIAApQwFINwNIIAEgACpCAVA4AlAgASAAK0MBWDkDWCABIAAoQgFgNgJgIAEgAClDAWg3A2ggASAAKkIBcDgCcCABIAArQwF4OQN4IAIkAQsgANIt0i5BAkEBQQNBAUEAQX4gABAV0hRBAEEBQQAQBQvKAgECfwJAAkAfQAECAAJAIwEiAQ0AEAEAC0ECQQFBARACIwAhAkEAJAAgAiQAEBYhAhADQQAkASACQQdxBEAQCAALAkACQD8BrUIQhiACrUGIAa18Wg0BCxAJAAsgAEEHcQRAEAgACwJAAkA/AK1CEIYgAK1BiAGtfFoNAQsQCQALIAAgAihCAQA2AgAgACACKUMBCDcDCCAAIAIqQgEQOAIQIAAgAitDARg5AxggACACKEIBIDYCICAAIAIpQwEoNwMoIAAgAipCATA4AjAgACACK0MBODkDOCAAIAIoQgFANgJAIAAgAilDAUg3A0ggACACKkIBUDgCUCAAIAIrQwFYOQNYIAAgAihCAWA2AmAgACACKUMBaDcDaCAAIAIqQgFwOAJwIAAgAitDAXg5A3ggACACKEIBgAE2AoABIAEkAQsMAQsQBAALCxABAX8jACEAQQAkACAAJAALlwIBAX8jASECQQAkASAAQQdxBEAQCAALAkACQD8BrUIQhiAArUGIAa18Wg0BCxAJAAsgAUEHcQRAEAgACwJAAkA/AK1CEIYgAa1BiAGtfFoNAQsQCQALIAEgAChCAQA2AgAgASAAKUMBCDcDCCABIAAqQgEQOAIQIAEgACtDARg5AxggASAAKEIBIDYCICABIAApQwEoNwMoIAEgACpCATA4AjAgASAAK0MBODkDOCABIAAoQgFANgJAIAEgAClDAUg3A0ggASAAKkIBUDgCUCABIAArQwFYOQNYIAEgAChCAWA2AmAgASAAKUMBaDcDaCABIAAqQgFwOAJwIAEgACtDAXg5A3ggASAAKEIBgAE2AoABIAIkAQsgANIx0jJBAkEBQQVBAUEAQX4gABAX0hZBAEEBQQAQBQsYAQF/IwAhBEEAJAAgACABIAIgAyAEJAALEAEBfyMCIQBBACQCIAAkAgsiANI00jVBA0EBQQBBAUEAQQAgACABIAIgAxAb0hlBBBAaCw=="},{"file":"adapters/5.wasm","wasm":"AGFzbQEAAAABygEYYAN/f38Bf2AEf359fAF/YAR/fn18BH9+fXxgAABgBHB/f38Bf2AMcHB/f39/f39/fn18AGAFf359fH8Bf2AFf359fH8Ff359fH9gBX9+fXx/AGACcH8AYA1wcH9/f39/f39+fXx/AGABfwV/fn18f2ABfwF/YAlwcH9/f39/f38AYAR/f39/AX9gAAF/YAF/AGABfAF8YAABfGACcH8BfGAIcHB/f39/f38AYAJ8fwBgEX9+fXx/fn18f359fH9+fXx/AGACf38AArAGIwVmbGFncwlpbnN0YW5jZTEDfwEIY2FsbGJhY2sCZjAAAAVmbGFncwlpbnN0YW5jZTMDfwEGbWVtb3J5Am0wAgAABmNhbGxlZQlhZGFwdGVyMTMAAQVhc3luYxVbc3RhcnQtY2FsbF1hZGFwdGVyMTMABARzeW5jF1twcmVwYXJlLWNhbGxdYWRhcHRlcjEzAAUGY2FsbGVlCWFkYXB0ZXIxNAAGBHN5bmMVW3N0YXJ0LWNhbGxdYWRhcHRlcjE0AAkEc3luYxdbcHJlcGFyZS1jYWxsXWFkYXB0ZXIxNAAKB3J1bnRpbWUGdHJhcDMzAAMHcnVudGltZQV0cmFwMQADBHN5bmMXW3ByZXBhcmUtY2FsbF1hZGFwdGVyMTUADQZtZW1vcnkCbTECAAAHcmVhbGxvYwNmMTAADgZjYWxsZWUJYWRhcHRlcjE2AAwHY29udGV4dARnZXQwAA8HY29udGV4dARnZXQxAA8HY29udGV4dARzZXQwABAHY29udGV4dARzZXQxABAEc3luYxVbc3RhcnQtY2FsbF1hZGFwdGVyMTYACQRzeW5jF1twcmVwYXJlLWNhbGxdYWRhcHRlcjE2AA0Ec3luYxdbcHJlcGFyZS1jYWxsXWFkYXB0ZXIxNwANBmNhbGxlZQlhZGFwdGVyMTgADwRzeW5jFVtzdGFydC1jYWxsXWFkYXB0ZXIxOAATBHN5bmMXW3ByZXBhcmUtY2FsbF1hZGFwdGVyMTgAFARzeW5jF1twcmVwYXJlLWNhbGxdYWRhcHRlcjE5AA0GY2FsbGVlCWFkYXB0ZXIyMAAPBHN5bmMVW3N0YXJ0LWNhbGxdYWRhcHRlcjIwAAkEc3luYxdbcHJlcGFyZS1jYWxsXWFkYXB0ZXIyMAANBHN5bmMXW3ByZXBhcmUtY2FsbF1hZGFwdGVyMjEADQZjYWxsZWUJYWRhcHRlcjIyAA8Ec3luYxVbc3RhcnQtY2FsbF1hZGFwdGVyMjIACQRzeW5jF1twcmVwYXJlLWNhbGxdYWRhcHRlcjIyAA0Ec3luYxdbcHJlcGFyZS1jYWxsXWFkYXB0ZXIyMwANAyIhAgMBBwMICwMMDAMQDAMMAxESAxUMAxYQAxYMAxcQAxcMB+oHLBZbYXN5bmMtc3RhcnRdYWRhcHRlcjEzAB8XW2FzeW5jLXJldHVybl1hZGFwdGVyMTMAIAlhZGFwdGVyMTMAIRZbYXN5bmMtc3RhcnRdYWRhcHRlcjE0ACIXW2FzeW5jLXJldHVybl1hZGFwdGVyMTQAIwlhZGFwdGVyMTQAJBZbYXN5bmMtc3RhcnRdYWRhcHRlcjE1ACUXW2FzeW5jLXJldHVybl1hZGFwdGVyMTUAJglhZGFwdGVyMTUAJxZbYXN5bmMtc3RhcnRdYWRhcHRlcjE2ACgXW2FzeW5jLXJldHVybl1hZGFwdGVyMTYAKQlhZGFwdGVyMTYAKhZbYXN5bmMtc3RhcnRdYWRhcHRlcjE3ACsXW2FzeW5jLXJldHVybl1hZGFwdGVyMTcALAlhZGFwdGVyMTcALRZbYXN5bmMtc3RhcnRdYWRhcHRlcjE4AC4XW2FzeW5jLXJldHVybl1hZGFwdGVyMTgALwlhZGFwdGVyMTgAMBZbYXN5bmMtc3RhcnRdYWRhcHRlcjE5ADEXW2FzeW5jLXJldHVybl1hZGFwdGVyMTkAMglhZGFwdGVyMTkAMxZbYXN5bmMtc3RhcnRdYWRhcHRlcjIwADQXW2FzeW5jLXJldHVybl1hZGFwdGVyMjAANQlhZGFwdGVyMjAANhZbYXN5bmMtc3RhcnRdYWRhcHRlcjIxADcXW2FzeW5jLXJldHVybl1hZGFwdGVyMjEAOAlhZGFwdGVyMjEAORZbYXN5bmMtc3RhcnRdYWRhcHRlcjIyADoXW2FzeW5jLXJldHVybl1hZGFwdGVyMjIAOwlhZGFwdGVyMjIAPBZbYXN5bmMtc3RhcnRdYWRhcHRlcjIzAD0XW2FzeW5jLXJldHVybl1hZGFwdGVyMjMAPglhZGFwdGVyMjMAPxlbYWRhcHRlci1jYWxsZWVdYWRhcHRlcjEzAAEZW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIxNAAEGVthZGFwdGVyLWNhbGxlZV1hZGFwdGVyMTUABBlbYWRhcHRlci1jYWxsZWVdYWRhcHRlcjE2AAsZW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIxNwALGVthZGFwdGVyLWNhbGxlZV1hZGFwdGVyMTgAExlbYWRhcHRlci1jYWxsZWVdYWRhcHRlcjE5ABMZW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIyMAAXGVthZGFwdGVyLWNhbGxlZV1hZGFwdGVyMjEAFxlbYWRhcHRlci1jYWxsZWVdYWRhcHRlcjIyABsZW2FkYXB0ZXItY2FsbGVlXWFkYXB0ZXIyMwAbCvQRIRgBAX8jACEEQQAkACAAIAEgAiADIAQkAAsQAQF/IwEhAEEAJAEgACQBCyYA0h/SIEEDQQFBAEEBQQBBfyAAIAEgAiADEAPSAUEEQQFBARACCxoBAX8jACEFQQAkACAAIAEgAiADIAQgBSQACxABAX8jASEAQQAkASAAJAELJADSItIjQQNBAUEAQQFBAEEAIAAgASACIAMgBBAG0gRBBRAFC00BAX8jACEBQQAkACAAQQdxBEAQBwALAkACQD8ArUIQhiAArUEorXxaDQELEAgACyAAKAIAIAApAwggACoCECAAKwMYIAAoAiAgASQACxABAX8jASEAQQAkASAAJAELIADSJdImQQNBAUEAQQFBAEF/IAAQCdIEQQVBAUEBEAILvgIBA38jACEBQQAkACAAQQdxBEAQBwALAkACQD8ArUIQhiAArUGIAa18Wg0BCxAIAAtBAEEAQQhBiAEQDCECEA0hA0EAEA5BABAPEAogAhAOIAMQDyEDIANBB3EEQBAHAAsCQAJAPwGtQhCGIAOtQYgBrXxaDQELEAgACyADIAAoAgA2QgEAIAMgACkDCDdDAQggAyAAKgIQOEIBECADIAArAxg5QwEYIAMgACgCIDZCASAgAyAAKQMoN0MBKCADIAAqAjA4QgEwIAMgACsDODlDATggAyAAKAJANkIBQCADIAApA0g3QwFIIAMgACoCUDhCAVAgAyAAKwNYOUMBWCADIAAoAmA2QgFgIAMgACkDaDdDAWggAyAAKgJwOEIBcCADIAArA3g5QwF4IAMgACgCgAE2QgGAASADIAEkAAsQAQF/IwEhAEEAJAEgACQBCxwA0ijSKUEDQQFBAEEBQQBBACAAEBHSC0EBEBALvgIBA38jACEBQQAkACAAQQdxBEAQBwALAkACQD8ArUIQhiAArUGIAa18Wg0BCxAIAAtBAEEAQQhBiAEQDCECEA0hA0EAEA5BABAPEAogAhAOIAMQDyEDIANBB3EEQBAHAAsCQAJAPwGtQhCGIAOtQYgBrXxaDQELEAgACyADIAAoAgA2QgEAIAMgACkDCDdDAQggAyAAKgIQOEIBECADIAArAxg5QwEYIAMgACgCIDZCASAgAyAAKQMoN0MBKCADIAAqAjA4QgEwIAMgACsDODlDATggAyAAKAJANkIBQCADIAApA0g3QwFIIAMgACoCUDhCAVAgAyAAKwNYOUMBWCADIAAoAmA2QgFgIAMgACkDaDdDAWggAyAAKgJwOEIBcCADIAArA3g5QwF4IAMgACgCgAE2QgGAASADIAEkAAsQAQF/IwEhAEEAJAEgACQBCyAA0ivSLEEDQQFBAEEBQQBBfyAAEBLSC0EBQQFBARACCxABAX8jACEAQQAkACAAJAALEgEBfyMBIQFBACQBIAAgASQBCxoA0i7SL0EDQQFBAUEBQQBBARAV0hNBABAUCxABAX8jACEAQQAkACAAJAALOwEBfyMBIQJBACQBIAFBB3EEQBAHAAsCQAJAPwCtQhCGIAGtQQitfFoNAQsQCAALIAEgADkDACACJAELIADSMdIyQQNBAUEBQQFBAEF+IAAQFtITQQBBAUEBEAILEAEBfyMAIQBBACQAIAAkAAulAQEBfyMBIRFBACQBIBBBB3EEQBAHAAsCQAJAPwCtQhCGIBCtQYABrXxaDQELEAgACyAQIAA2AgAgECABNwMIIBAgAjgCECAQIAM5AxggECAENgIgIBAgBTcDKCAQIAY4AjAgECAHOQM4IBAgCDYCQCAQIAk3A0ggECAKOAJQIBAgCzkDWCAQIAw2AmAgECANNwNoIBAgDjgCcCAQIA85A3ggESQBCxwA0jTSNUEDQQFBA0EBQQBBECAAEBnSF0EAEBgLEAEBfyMAIQBBACQAIAAkAAulAQEBfyMBIRFBACQBIBBBB3EEQBAHAAsCQAJAPwCtQhCGIBCtQYABrXxaDQELEAgACyAQIAA2AgAgECABNwMIIBAgAjgCECAQIAM5AxggECAENgIgIBAgBTcDKCAQIAY4AjAgECAHOQM4IBAgCDYCQCAQIAk3A0ggECAKOAJQIBAgCzkDWCAQIAw2AmAgECANNwNoIBAgDjgCcCAQIA85A3ggESQBCyAA0jfSOEEDQQFBA0EBQQBBfiAAEBrSF0EAQQFBARACCxABAX8jACEAQQAkACAAJAALlwIBAX8jASECQQAkASAAQQdxBEAQBwALAkACQD8BrUIQhiAArUGIAa18Wg0BCxAIAAsgAUEHcQRAEAcACwJAAkA/AK1CEIYgAa1BiAGtfFoNAQsQCAALIAEgAChCAQA2AgAgASAAKUMBCDcDCCABIAAqQgEQOAIQIAEgACtDARg5AxggASAAKEIBIDYCICABIAApQwEoNwMoIAEgACpCATA4AjAgASAAK0MBODkDOCABIAAoQgFANgJAIAEgAClDAUg3A0ggASAAKkIBUDgCUCABIAArQwFYOQNYIAEgAChCAWA2AmAgASAAKUMBaDcDaCABIAAqQgFwOAJwIAEgACtDAXg5A3ggASAAKEIBgAE2AoABIAIkAQsgANI60jtBA0EBQQVBAUEAQf////8HIAAQHdIbQQAQHAsQAQF/IwAhAEEAJAAgACQAC5cCAQF/IwEhAkEAJAEgAEEHcQRAEAcACwJAAkA/Aa1CEIYgAK1BiAGtfFoNAQsQCAALIAFBB3EEQBAHAAsCQAJAPwCtQhCGIAGtQYgBrXxaDQELEAgACyABIAAoQgEANgIAIAEgAClDAQg3AwggASAAKkIBEDgCECABIAArQwEYOQMYIAEgAChCASA2AiAgASAAKUMBKDcDKCABIAAqQgEwOAIwIAEgACtDATg5AzggASAAKEIBQDYCQCABIAApQwFINwNIIAEgACpCAVA4AlAgASAAK0MBWDkDWCABIAAoQgFgNgJgIAEgAClDAWg3A2ggASAAKkIBcDgCcCABIAArQwF4OQN4IAEgAChCAYABNgKAASACJAELIADSPdI+QQNBAUEFQQFBAEF+IAAQHtIbQQBBAUEBEAIL"}]} \ No newline at end of file diff --git a/runtime/tests/fixtures/thread-switch-matrix.wasm b/runtime/tests/fixtures/thread-switch-matrix.wasm index 9af1ce5..a15aa5f 100644 Binary files a/runtime/tests/fixtures/thread-switch-matrix.wasm and b/runtime/tests/fixtures/thread-switch-matrix.wasm differ diff --git a/runtime/tests/fixtures/thread-switch-matrix.wat b/runtime/tests/fixtures/thread-switch-matrix.wat index 989b46f..4bf9ad1 100644 --- a/runtime/tests/fixtures/thread-switch-matrix.wat +++ b/runtime/tests/fixtures/thread-switch-matrix.wat @@ -20,10 +20,10 @@ (core func $thread-new (canon thread.new-indirect $start-type (core table $table-export))) (canon thread.resume-later (core func $resume-later)) - (canon thread.suspend cancellable (core func $suspend-cancellable)) + (canon thread.suspend (core func $suspend-cancellable)) (canon thread.suspend-then-resume (core func $suspend-then-resume)) - (canon thread.suspend-then-resume cancellable + (canon thread.suspend-then-resume (core func $suspend-then-resume-cancellable)) (canon thread.yield-then-resume (core func $yield-then-resume)) diff --git a/runtime/tests/host_boundary_preparation_test.ts b/runtime/tests/host_boundary_preparation_test.ts index ebce3d5..6c012ec 100644 --- a/runtime/tests/host_boundary_preparation_test.ts +++ b/runtime/tests/host_boundary_preparation_test.ts @@ -17,6 +17,7 @@ import { import type { ComponentValue, FuncType, ValType } from "../src/cabi/types.ts"; import { lowerFlatValues, + PreparedCustody, PreparedValues, prepareRawValues, } from "../src/cabi/values.ts"; @@ -41,6 +42,8 @@ import { Task, Thread, unpackSubtaskResult, + WaitableSet, + WritableStreamEnd, } from "../src/task/mod.ts"; import { GuestResource, @@ -149,6 +152,61 @@ function liveEntries(table: Table): unknown[] { return table.array.filter((entry) => entry !== null); } +Deno.test("prepared custody ignores same-slot reuse after transfer away and back", () => { + const inst = new ComponentInstanceState(0, new Store()); + const custody = new PreparedCustody(); + const original = {}; + let cleaned = 0; + custody.acquire(() => cleaned++); + const index = inst.handles.add(original); + custody.inserted(inst.handles, index, original); + assertEq(inst.handles.remove(index), original); + const replacement = {}; + assertEq(inst.handles.add(replacement), index); + custody.cleanup(); + assertEq(inst.handles.get(index), replacement); + assertEq(cleaned, 0); +}); + +Deno.test("prepared custody survives leave-return transfer without dropping receiver", () => { + const store = new Store(); + const source = new ComponentInstanceState(0, store); + const destination = new ComponentInstanceState(1, store); + const custody = new PreparedCustody(); + const end = new SharedStreamImpl(null).claimReadableEnd(null); + let cleaned = 0; + custody.acquire(() => cleaned++); + const sourceIndex = source.handles.add(end); + custody.inserted(source.handles, sourceIndex, end); + source.handles.remove(sourceIndex); + const destinationIndex = destination.handles.add(end); + destination.handles.remove(destinationIndex); + const returnedIndex = source.handles.add(end); + assertEq(returnedIndex, sourceIndex); + custody.cleanup(); + assert( + source.handles.get(returnedIndex) === end, + "returned endpoint survives", + ); + assertEq(cleaned, 0); +}); + +Deno.test("failed custody teardown notifies a healthy idle stream peer", () => { + const inst = new ComponentInstanceState(0, new Store()); + const shared = new SharedStreamImpl(null); + const readable = shared.claimReadableEnd(null); + const writable = new WritableStreamEnd(shared); + const wi = inst.handles.add(writable); + writable.index = wi; + const set = new WaitableSet(); + writable.join(set); + const custody = new PreparedCustody(); + custody.acquire(() => shared.drop(readable)); + custody.cleanup(); + assertEq(shared.dropped, true); + assertEq(set.getPendingEvent(), [3, wi, 1]); +}); + function memoryHarness(reallocEffect?: () => void) { const memory = new WebAssembly.Memory({ initial: 1 }); let bump = 1024; diff --git a/runtime/tests/host_import_cancel_test.ts b/runtime/tests/host_import_cancel_test.ts index a03332c..fe38bf7 100644 --- a/runtime/tests/host_import_cancel_test.ts +++ b/runtime/tests/host_import_cancel_test.ts @@ -586,9 +586,11 @@ Deno.test("cancellation discard: deferCancel() keeps run-to-completion — BLOCK const f = mkFixture(deferCancel(() => d.promise)); const { subtaski, subtask } = inFlight(f); - const rc = f.asGuest(() => - createSubtaskCancel({ async: true }, f.inst)(subtaski) - ); + const pending = f.asGuest(() => + createSubtaskCancel({ async: true }, f.inst, "jspi")(subtaski) + ) as Promise; + f.store.tick(); + const rc = await pending; assertEq(rc, BLOCKED); assertEq(subtask.resolved(), false); assertEq(subtask.state, SubtaskState.STARTED); @@ -692,15 +694,14 @@ Deno.test("abortable(): a discard aborts the signal — one microtask LATER, nev const { subtaski } = inFlight(f); const signal = r.seen.last as AbortSignal; - const rc = f.asGuest(() => - createSubtaskCancel({ async: true }, f.inst)(subtaski) - ); + const pending = f.asGuest(() => + createSubtaskCancel({ async: true }, f.inst, "jspi")(subtaski) + ) as Promise; + f.store.tick(); + const rc = await pending; assertEq(rc, SubtaskState.CANCELLED_BEFORE_RETURNED); - // SYNCHRONOUSLY after the built-in returned: still unaborted. This is the - // assertion that pins the deferral rather than merely the abort. - assertEq(signal.aborted, false); - - await Promise.resolve(); + // The mandatory async-cancel yield gives the queued abort microtask a turn + // before the built-in's Promise settles. assertEq(signal.aborted, true); }); @@ -740,9 +741,11 @@ Deno.test("abortable(): deferCancel + abortable — cancellation never discards, const signal = r.seen.last as AbortSignal; assertEq(r.seen.count, FT.params.length + 1); - const rc = f.asGuest(() => - createSubtaskCancel({ async: true }, f.inst)(subtaski) - ); + const pending = f.asGuest(() => + createSubtaskCancel({ async: true }, f.inst, "jspi")(subtaski) + ) as Promise; + f.store.tick(); + const rc = await pending; assertEq(rc, BLOCKED); for (let i = 0; i < 5; i++) await Promise.resolve(); assertEq(signal.aborted, false); diff --git a/runtime/tests/integration/e2e_hello_test.ts b/runtime/tests/integration/e2e_hello_test.ts index 065c95a..b40f0f2 100644 --- a/runtime/tests/integration/e2e_hello_test.ts +++ b/runtime/tests/integration/e2e_hello_test.ts @@ -14,11 +14,65 @@ import { Translator } from "../../src/shim/mod.ts"; import { instantiateComponent } from "../../src/exec/mod.ts"; import { SUPPORTED_FORMAT_VERSION } from "../../src/plan/mod.ts"; import { isInstancePoisoned } from "../../src/task/scheduler.ts"; +import type { WireEnvelope } from "../../src/plan/format.ts"; function assert(cond: boolean, msg: string): asserts cond { if (!cond) throw new Error(`assertion failed: ${msg}`); } +/** Minimal core-wasm type/import reader for the old FACT ABI assertion. */ +function enterSyncCallParamCount(bytes: Uint8Array): number { + let p = 8; + const u32 = () => { + let n = 0, shift = 0, b: number; + do { + b = bytes[p++]; + n |= (b & 0x7f) << shift; + shift += 7; + } while (b & 0x80); + return n >>> 0; + }; + const text = () => { + const n = u32(); + const s = new TextDecoder().decode(bytes.subarray(p, p + n)); + p += n; + return s; + }; + const types: number[] = []; + let enterType = -1; + while (p < bytes.length) { + const id = bytes[p++], size = u32(), end = p + size; + if (id === 1) { + for (let n = u32(); n > 0; n--) { + assert(bytes[p++] === 0x60, "old adapter function type"); + const params = u32(); + types.push(params); + p += params; + const results = u32(); + p += results; + } + } else if (id === 2) { + for (let n = u32(); n > 0; n--) { + const module = text(), name = text(), kind = bytes[p++]; + if (kind === 0) { + const type = u32(); + if (module === "async" && name === "enter-sync-call") { + enterType = type; + } + } else if (kind === 2) { + const flags = u32(); + u32(); + if (flags & 1) u32(); + } else if (kind === 3) p += 2; + else throw new Error(`unsupported old adapter import kind ${kind}`); + } + } + p = end; + } + assert(enterType >= 0, "old FACT adapter imports enter-sync-call"); + return types[enterType]; +} + const root = new URL("../../../", import.meta.url); async function readArtifact(rel: string, hint: string): Promise { @@ -43,7 +97,7 @@ Deno.test("hello: full pipeline shim -> plan -> executor -> greet()", async () = const { plan, adapters } = translator.translate(helloWasm); assertEq(plan.formatVersion, SUPPORTED_FORMAT_VERSION); - assertEq(plan.producer.wasmtimeEnviron, "49.0.0-dev+4675ee1"); + assertEq(plan.producer.wasmtimeEnviron, "50.0.0-dev+cc546ee"); assertEq(adapters.size, 0); // no cross-component links in hello const component = await instantiateComponent({ @@ -108,6 +162,55 @@ Deno.test("hello: executor validates formatVersion and hash", async () => { } assert(failed.includes("formatVersion"), `got: ${failed}`); + // A coherent linked format-5 artifact set produced by the previous pinned + // translator is rejected before adapter compilation or instantiation. + const oldEnvelope = JSON.parse( + await Deno.readTextFile( + new URL("../fixtures/old-v5-linked.envelope.json", import.meta.url), + ), + ) as WireEnvelope; + const oldComponent = await Deno.readFile( + new URL("../fixtures/old-v5-linked.component.wasm", import.meta.url), + ); + assert(oldEnvelope.plan !== undefined, "old envelope must contain a plan"); + const oldAdapters = new Map( + (oldEnvelope.adapters ?? []).map((adapter) => [ + adapter.file, + Uint8Array.from(atob(adapter.wasm), (byte) => byte.charCodeAt(0)), + ]), + ); + const oldAdapter4 = oldAdapters.get("adapters/4.wasm"); + assert(oldAdapter4 !== undefined, "old envelope embeds adapter 4"); + const adapterModule = await WebAssembly.compile(oldAdapter4); + const enter = WebAssembly.Module.imports(adapterModule).find((i) => + i.module === "async" && i.name === "enter-sync-call" + ); + assert( + enter?.kind === "function", + "old FACT adapter imports enter-sync-call", + ); + assertEq(enterSyncCallParamCount(oldAdapter4), 3); + const oldModules = oldEnvelope.plan.modules.filter((m) => + m.kind === "adapter" + ); + assertEq(oldModules.length, 2); + for (const module of oldModules) { + const adapter = oldAdapters.get(module.file); + assert(adapter !== undefined, `old envelope embeds ${module.file}`); + assertEq(module.len, adapter.length); + } + failed = ""; + try { + await instantiateComponent({ + plan: oldEnvelope.plan, + componentBytes: oldComponent, + adapters: oldAdapters, + }); + } catch (e) { + failed = String(e); + } + assert(failed.includes("unsupported plan formatVersion 5"), `got: ${failed}`); + // Component-bytes mismatch (hash check) fails fast. const tampered = helloWasm.slice(); tampered[tampered.length - 1] ^= 0xff; diff --git a/runtime/tests/integration/e2e_suite_test.ts b/runtime/tests/integration/e2e_suite_test.ts index 17b44d2..76fef41 100644 --- a/runtime/tests/integration/e2e_suite_test.ts +++ b/runtime/tests/integration/e2e_suite_test.ts @@ -541,22 +541,18 @@ Deno.test({ }, }); -// test/binary/binary.wast:1433 — a component exporting one of its own +// test/binary/binary.wast:1533 — a component exporting one of its own // embedded core modules (the `module` export kind, contracts/plan-format.md // 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). 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`). +// the wast source (an empty module). Locate it by the current source command; +// the preceding malformed binary cases make positional artifact names move. Deno.test({ - name: "suite binary.116: a core-module export surfaces as WebAssembly.Module", + name: "suite binary.124: a core-module export surfaces as WebAssembly.Module", ignore: !ready, fn: async () => { - const c = await instantiate("binary", "binary.116.wasm"); + const c = await instantiate("binary", "binary.124.wasm"); const m = c.exports["m"]; assertEq( m instanceof WebAssembly.Module, diff --git a/runtime/tests/jspi/bridge_test.ts b/runtime/tests/jspi/bridge_test.ts index a3aeaa6..4ca2f0f 100644 --- a/runtime/tests/jspi/bridge_test.ts +++ b/runtime/tests/jspi/bridge_test.ts @@ -53,7 +53,6 @@ Deno.test("bridge: a suspension point is just a parked thread to the scheduler", store, task, () => flag, - false, () => 42, ); // Registered in the store's waiting list exactly like a generator thread. @@ -67,18 +66,17 @@ Deno.test("bridge: a suspension point is just a parked thread to the scheduler", assertEq(store.waiting.length, 0); }); -Deno.test("bridge: resume delivers the cancelled flag to the value producer", () => { +Deno.test("bridge: resume delivers the producer value", () => { const store = new Store(); const inst = new ComponentInstanceState(0, store); const point = new SuspensionPoint( store, mkTask(inst), () => true, - true, - (cancelled) => (cancelled ? "cancelled" : "normal"), + () => "normal", ); - point.resume(true); - return point.promise.then((v) => assertEq(v, "cancelled")); + point.resume(); + return point.promise.then((v) => assertEq(v, "normal")); }); Deno.test("bridge: a trap computed at resume time becomes a rejection", async () => { @@ -92,7 +90,6 @@ Deno.test("bridge: a trap computed at resume time becomes a rejection", async () store, mkTask(inst), () => true, - false, () => { throw boom; }, @@ -118,7 +115,6 @@ Deno.test("bridge: a suspension does not make the instance unusable", async () = store, mkTask(inst), () => flag, - false, () => 1, ); assertEq(entryRefusal(inst, null, "base"), null, "entry is admitted"); @@ -135,7 +131,6 @@ Deno.test("bridge: abandon rejects without resuming the guest", async () => { store, mkTask(inst), () => false, - false, () => 0, ); const reason = new Error("torn down"); @@ -409,7 +404,6 @@ Deno.test({ store, task, () => true, - false, () => 1, ); point.resume(); diff --git a/runtime/tests/jspi/explicit_ambient_test.ts b/runtime/tests/jspi/explicit_ambient_test.ts index d828560..7b8af7f 100644 --- a/runtime/tests/jspi/explicit_ambient_test.ts +++ b/runtime/tests/jspi/explicit_ambient_test.ts @@ -110,7 +110,6 @@ Deno.test({ store, task: { implicitThread: thread }, readyFunc: () => true, - cancellable: false, produce: () => x + 1, }); } diff --git a/runtime/tests/jspi/thread_switch_matrix_test.ts b/runtime/tests/jspi/thread_switch_matrix_test.ts index 7ff2a99..10a805d 100644 --- a/runtime/tests/jspi/thread_switch_matrix_test.ts +++ b/runtime/tests/jspi/thread_switch_matrix_test.ts @@ -32,7 +32,6 @@ async function bounded(promise: Promise, label: string): Promise { if (timer !== undefined) clearTimeout(timer); } } - async function fresh( onMark?: (value: number, component: Component) => void, gates: GateOptions = {}, @@ -56,7 +55,6 @@ async function fresh( holder.current = component; return component; } - async function run( name: string, expectedValue: number, @@ -102,51 +100,6 @@ Deno.test("real guest: promote ignores suspended target and runs ready target fi await run("promote", 104, [4, 41, 40, 42]); }); -function requestCurrentTaskCancellation(component: Component): void { - const tasks = new Set( - component.handle.componentInstances - .filter((inst) => inst !== undefined) - .flatMap((inst) => [...inst.threads].map((thread) => thread.task)), - ); - assertEquals(tasks.size, 1, "mark must run with exactly one live task"); - [...tasks][0].requestCancellation(null); -} - -Deno.test("real guest: valid pending cancellation cancels without transfer", async () => { - const component = await fresh((value, current) => { - if (value === 6) requestCurrentTaskCancellation(current); - }); - assertEquals( - await bounded( - component.exports.cancelValid() as Promise, - "cancel-valid", - ), - 105, - ); - assertEquals(JSON.stringify(component.order), JSON.stringify([6, 61])); -}); - -for (const name of ["cancelInvalidIndex", "cancelSelf", "cancelWrongState"]) { - Deno.test(`real guest: ${name} validates before pending cancellation`, async () => { - let requestedTask: { state: string } | null = null; - const component = await fresh((value, current) => { - if (value !== 6) return; - const tasks = current.handle.componentInstances - .filter((inst) => inst !== undefined) - .flatMap((inst) => [...inst.threads].map((thread) => thread.task)); - assert(tasks.length > 0); - requestedTask = tasks[0]; - tasks[0].requestCancellation(null); - }); - const failure = await assertRejects(() => - bounded(component.exports[name]() as Promise, name) - ); - assert(isTrap(failure), `expected validation trap, got ${failure}`); - assert(requestedTask !== null); - assertEquals((requestedTask as { state: string }).state, "pending-cancel"); - }); -} - Deno.test("real guest: explicit child trap remains the original cause", async () => { const component = await fresh(); const failure = await assertRejects(() => @@ -169,95 +122,6 @@ Deno.test("real guest: normal last explicit child reports no async result", asyn ); }); -for (const timing of ["pending", "immediate"] as const) { - Deno.test(`real guest: explicit child receives ${timing} cancellation while sibling owns exclusive slot`, async () => { - const childGate = Promise.withResolvers(); - const childEntered = Promise.withResolvers(); - const holderGate = Promise.withResolvers(); - const holderEntered = Promise.withResolvers(); - const childAtPark = Promise.withResolvers(); - const component = await fresh( - (value) => { - if (value === 70) childAtPark.resolve(); - }, - { - childGate: () => { - childEntered.resolve(); - return childGate.promise; - }, - holderGate: () => { - holderEntered.resolve(); - return holderGate.promise; - }, - }, - ); - - const originResult = component.exports.childCancellable() as Promise< - number - >; - await bounded(childEntered.promise, "child initial noncancellable park"); - const instance = component.handle.componentInstances.find((inst) => - inst !== undefined && [...inst.threads].length > 0 - )!; - const origin = [...instance.threads][0].task; - assertEquals( - origin.implicitThread?.index, - null, - "origin implicit thread exited", - ); - - const holderResult = component.exports.lockHolder() as Promise; - try { - await bounded(holderEntered.promise, "exclusive holder park"); - const exclusive = instance.exclusiveThread; - assert(exclusive !== null, "lock-holder must own the exclusive slot"); - assert( - exclusive.task !== origin, - "exclusive holder must be a sibling task", - ); - - if (timing === "pending") origin.requestCancellation(null); - childGate.resolve(); - await bounded(childAtPark.promise, "child cancellable park"); - if (timing === "immediate") { - await bounded( - (async () => { - while ( - !(instance.store as unknown as { - waiting: { task?: unknown; cancellable: boolean }[]; - }).waiting.some((waiter) => - waiter.task === origin && waiter.cancellable - ) - ) { - await Promise.resolve(); - } - })(), - "registered child cancellable park", - ); - origin.requestCancellation(null); - } - - assertEquals( - await bounded(originResult, `child-cancellable-${timing}`), - 110, - ); - assertEquals( - JSON.stringify(component.order), - JSON.stringify([90, 70, 71]), - ); - assertEquals( - instance.exclusiveThread, - exclusive, - "holder keeps the slot", - ); - } finally { - childGate.resolve(); - holderGate.resolve(); - } - assertEquals(await bounded(holderResult, "holder cleanup"), 109); - }); -} - Deno.test("real guest: post-result ready child continues and suspended child is retained", async () => { const component = await fresh(); assertEquals( diff --git a/runtime/tests/lift_background_return_test.ts b/runtime/tests/lift_background_return_test.ts index 8814281..3e9d4ed 100644 --- a/runtime/tests/lift_background_return_test.ts +++ b/runtime/tests/lift_background_return_test.ts @@ -85,7 +85,7 @@ Deno.test({ immortal = new Thread( task as never, (function* () { - yield { readyFunc: () => false, cancellable: false }; + yield { readyFunc: () => false }; })(), ); task.registerThread(immortal); @@ -94,7 +94,7 @@ Deno.test({ const returner = new Thread( task as never, (function* () { - yield { readyFunc: () => wake, cancellable: false }; + yield { readyFunc: () => wake }; task.return_([]); })(), ); diff --git a/runtime/tests/lift_done_verdict_test.ts b/runtime/tests/lift_done_verdict_test.ts index 4c96789..2950ba1 100644 --- a/runtime/tests/lift_done_verdict_test.ts +++ b/runtime/tests/lift_done_verdict_test.ts @@ -111,7 +111,7 @@ Deno.test({ background = new Thread( task as never, (function* () { - yield { readyFunc: () => false, cancellable: false }; + yield { readyFunc: () => false }; })(), ); task.registerThread(background); diff --git a/runtime/tests/park_state_settle_test.ts b/runtime/tests/park_state_settle_test.ts index 9dc7e87..5b99737 100644 --- a/runtime/tests/park_state_settle_test.ts +++ b/runtime/tests/park_state_settle_test.ts @@ -177,22 +177,12 @@ Deno.test("#106 SITE 2: abandon discharges numWaiting; waitable-set.drop stays l assertEq(await settled, "rejected: store teardown"); }); -Deno.test("#106 SITE 2: cancelled resume decrements exactly once (produce runs, hook is sole owner)", async () => { - const t = parkOnWait(true); - // Deliver cancellation exactly as Task.requestCancellation does — the - // produce(cancelled=true) leg (cancel_bracket_race_test.ts's machinery). - t.task.requestCancellation(null); - await (t.parked as Promise); - assertEq(t.wset.numWaiting, 0); // 0, not -1: the decrement is not doubled - t.drop(t.seti); -}); - Deno.test("#106 SITE 2: produce-throw still discharges numWaiting", async () => { const t = parkOnWait(false); const settled = rejected(t.parked as Promise); // resume with no pending event: `produce` calls `getPendingEvent()` on an // empty set and throws — the produce-throw settle leg. - t.point.resume(false); + t.point.resume(); assertEq(t.wset.numWaiting, 0); t.drop(t.seti); assert((await settled).startsWith("rejected:"), "the park rejected"); @@ -339,7 +329,7 @@ Deno.test("#106 host-import park: success path releases exactly once (hook obser resolve(undefined); await Promise.resolve(); // the recorded-outcome microtask - point.resume(false); // produce: onResolve + deliverResolve, then the hook + point.resume(); // produce: onResolve + deliverResolve, then the hook await parked; // deliverResolve ran inside produce; the onSettled backstop saw @@ -365,7 +355,7 @@ Deno.test("#106 host-import park: rejection (produce-throw) — poisoning trap, reject(new Error("host bug")); await Promise.resolve(); - point.resume(false); // produce throws `done.error` — the trap path + point.resume(); // produce throws `done.error` — the trap path // This settle poisons the caller (the trap-unwind/lender-release obligation // owes no release here — the analysis lives at the site); the hook unwinds anyway as harmless diff --git a/runtime/tests/plan_v3_test.ts b/runtime/tests/plan_v3_test.ts index 1312882..f845284 100644 --- a/runtime/tests/plan_v3_test.ts +++ b/runtime/tests/plan_v3_test.ts @@ -83,8 +83,15 @@ function expectPlanError(fn: () => unknown, includes: string): void { // Loader // --------------------------------------------------------------------------- -Deno.test("the format version is a strict-equality gate (at 5 since plan v5)", () => { - assertEq(SUPPORTED_FORMAT_VERSION, 5); +Deno.test("the format version is a strict-equality gate (at 6)", () => { + assertEq(SUPPORTED_FORMAT_VERSION, 6); + let oldError = ""; + try { + loadPlan(minimalPlan({ formatVersion: 5 })); + } catch (e) { + oldError = String(e); + } + assertEq(oldError.includes("unsupported plan formatVersion 5"), true); expectPlanError( () => loadPlan(minimalPlan({ formatVersion: 2 })), "unsupported plan formatVersion 2", diff --git a/runtime/tests/resource_lender_park_settle_test.ts b/runtime/tests/resource_lender_park_settle_test.ts index 36c6274..bdc6c76 100644 --- a/runtime/tests/resource_lender_park_settle_test.ts +++ b/runtime/tests/resource_lender_park_settle_test.ts @@ -220,7 +220,7 @@ Deno.test("#102: sync-start-call park success path is unchanged (release before // produced value. assertEq(h.handle.numLends, 0); }); - point.resume(false); + point.resume(); assertEq(h.handle.numLends, 0); // released inside `produce`, not by a hook await parked; assertEq(resolved, true); @@ -248,13 +248,13 @@ Deno.test("#102: sync-start-call park releases lenders when produce throws", asy throw new Error("resume-time trap"); }; - point.resume(false); + point.resume(); assertEq(h.handle.numLends, 0); canonResourceDrop(h.caller, h.rt, h.handleIndex); assertEq(await settled, "rejected: resume-time trap"); }); -Deno.test("#102: a cancelled resume cannot reach these non-cancellable parks", async () => { +Deno.test("#102: abandonment is the non-producing teardown path", async () => { const h = mkHarness(); const parked = h.run("sync", neverResolves); assert(parked instanceof Promise, "the caller's activation parked"); @@ -265,21 +265,8 @@ Deno.test("#102: a cancelled resume cannot reach these non-cancellable parks", a const point = h.point(); assert(point !== undefined, "the suspension point is registered as waiting"); - // Settle path 3: rejected by `SuspensionPoint.resume`'s assert (#93), which - // fires before the point is marked done — so this is not a settle path at - // all, and there is no non-poisoning continuation to release into. Pinned - // here so a future `cancellable: true` at this site has to revisit the - // enumeration. - let threw: unknown = null; - try { - point.resume(true); - } catch (e) { - threw = e; - } - assert(threw !== null, "a cancelled resume of a non-cancellable point traps"); - assertEq(point.waiting(), true); // still parked: NOT a terminal transition - - // Teardown then still discharges the lenders. + // Generic cancellation injection no longer exists. Teardown abandons the + // park without running its producer and still discharges lenders. point.abandon(new Error("teardown after the illegal resume")); assertEq(h.handle.numLends, 0); assertEq(await settled, "rejected: teardown after the illegal resume"); @@ -326,7 +313,7 @@ Deno.test("#102: async-start-call determinacy park does NOT unwind a live subtas // `deliverResolve` later (definitions.py `Subtask.deliver_resolve`, 904) — // so the backstop must stay out of the way. A blanket unwind here would // cancel a perfectly good call. - point.resume(false); + point.resume(); const packed = await parked; assertEq(typeof packed, "number"); assertEq(h.handle.numLends, 1); diff --git a/runtime/tests/resume_claim_discipline_test.ts b/runtime/tests/resume_claim_discipline_test.ts index de2fd89..a427f09 100644 --- a/runtime/tests/resume_claim_discipline_test.ts +++ b/runtime/tests/resume_claim_discipline_test.ts @@ -13,8 +13,6 @@ import { ComponentInstanceState, - instancePoisonCause, - isInstancePoisoned, popCurrentThread, pushCurrentThread, releaseActivationAmbient, @@ -26,11 +24,7 @@ import { withActivation, } from "../src/task/mod.ts"; import { createWaitableSetWait } from "../src/intrinsics/async_builtins.ts"; -import { - blockCurrentActivation, - type SuspensionPoint, -} from "../src/jspi/mod.ts"; -import { Trap } from "../src/cabi/mod.ts"; +import type { SuspensionPoint } from "../src/jspi/mod.ts"; import type { FuncType } from "../src/cabi/types.ts"; import type { ResolvedOptions } from "../src/exec/boundary.ts"; @@ -170,7 +164,7 @@ Deno.test("resume success arm: delivery from the pending-entry holder consumes i // is now running under its wasm-entry bracket, where it synchronously // delivers a cancellation whose `produce` SUCCEEDS (TASK_CANCELLED). store.addPendingResumption(caller.thread); - withActivation(caller.thread, () => parked.point.resume(true)); + withActivation(caller.thread, () => parked.point.resume()); assert( !store.pendingResumptions.has(caller.thread), "the caller's entry was consumed: its window closed when it ran", @@ -199,7 +193,7 @@ Deno.test("resume trap arm: delivery from the pending-entry holder consumes it t // fix this arm recorded the resumed ambient WITHOUT consuming the // caller's, so the (then single-slot) gate asserted and preempted // `#fail(e)`. - withActivation(caller.thread, () => parked.point.resume(false)); + withActivation(caller.thread, () => parked.point.resume()); } catch (e) { threw = e; } @@ -241,7 +235,7 @@ Deno.test("resume from an EMPTY bracket self-consumes via the claims-top fallbac // First resumption: B's cancellation delivered from outside any activation // (a driver) — records B, both the activation-ambient claim and the // store's pending entry. - parkedB.point.resume(true); + parkedB.point.resume(); assert(store.pendingResumptions.has(b.thread), "B's entry is pending"); // Second resumption in the same turn, again from an empty bracket: // `activationOf()` is the entryStack top ?? the activation-claims top, and @@ -251,7 +245,7 @@ Deno.test("resume from an EMPTY bracket self-consumes via the claims-top fallbac // hazard.) let threw: unknown = null; try { - parkedA.point.resume(true); + parkedA.point.resume(); } catch (e) { threw = e; } @@ -283,7 +277,7 @@ Deno.test("resume from a DIFFERENT running activation while a resumption is pend // against Z's store's set and correctly consumes nothing. let threw: unknown = null; try { - withActivation(x.thread, () => parkedZ.point.resume(true)); + withActivation(x.thread, () => parkedZ.point.resume()); } catch (e) { threw = e; } @@ -316,7 +310,7 @@ Deno.test("resume from a DIFFERENT running activation while a resumption is pend store.addPendingResumption(y.thread); let threw: unknown = null; try { - withActivation(x.thread, () => parkedZ.point.resume(true)); + withActivation(x.thread, () => parkedZ.point.resume()); } catch (e) { threw = e; } @@ -334,70 +328,3 @@ Deno.test("resume from a DIFFERENT running activation while a resumption is pend cleanupAmbient(x, y, z); } }); - -Deno.test("guest-shaped: a trapping cancellation delivery while the canceller's claim is live (#158 mechanism A)", async () => { - // The f7abf96 class ("request_cancellation: a trap during delivery poisons - // the callee", task_test.ts) on the SuspensionPoint arm: the callee is - // parked in a cancellable built-in whose `produce` traps when handed - // `cancelled`, and the canceller delivers it while still holding its own - // engine-resume claim. - // Intra-store, as a real cancellation delivery is. - const store = new Store(); - const callee = mkWorld({ store }); - const canceller = mkWorld({ store }); - try { - const parked = callee.run(() => - blockCurrentActivation({ - store: callee.store, - task: callee.task, - readyFunc: null, - cancellable: true, - produce: (cancelled) => { - if (cancelled) throw new Trap("boom during cancel delivery"); - return 0; - }, - }) - ); - const outcome = rejection(parked); - const point = callee.point(); - assert(point !== undefined, "the suspension point is waiting"); - - store.addPendingResumption(canceller.thread); - let threw: unknown = null; - try { - withActivation( - canceller.thread, - () => callee.task.requestCancellation(null), - ); - } catch (e) { - threw = e; - } - assert( - !String(threw).includes("two activations claim the resumed ambient"), - `the #158 assertion must not preempt the trap: ${String(threw)}`, - ); - assert( - callee.task.state === "cancel-delivered", - `parity: the state is set first, got ${callee.task.state}`, - ); - // `SuspensionPoint.resume` never rethrows a `produce` error: the trap - // reaches the guest as a rejection of the import's Promise (the engine - // turns it back into a wasm trap), so `requestCancellation` sees no throw - // on this arm and the instance-poisoning branch of its catch does not run. - // See the report on #158: whether the SP arm should also poison is a - // separate question from the mechanism-A claim asymmetry fixed here. - const r = await outcome; - assert(r.ok === false, "the parked promise rejected"); - assert( - String(r.e).includes("boom during cancel delivery"), - `the guest receives its own trap: ${String(r.e)}`, - ); - assert( - isInstancePoisoned(callee.inst) === false && - instancePoisonCause(callee.inst) === undefined, - "pinning current behavior: the SP arm does not poison the callee", - ); - } finally { - cleanupAmbient(callee, canceller); - } -}); diff --git a/runtime/tests/settled_deferral_test.ts b/runtime/tests/settled_deferral_test.ts index 84be3bb..895d004 100644 --- a/runtime/tests/settled_deferral_test.ts +++ b/runtime/tests/settled_deferral_test.ts @@ -13,7 +13,6 @@ import { assertEq } from "./support/asserts.ts"; import { driveStoreAsync } from "../src/exec/boundary.ts"; import { type BlockRequest, - type Cancelled, ComponentInstanceState, Store, Task, @@ -32,11 +31,11 @@ const SYNC_OPTS: TaskOptions = { function spawn( task: Task, - body: (t: Thread) => Generator, + body: (t: Thread) => Generator, ): Thread { // Forward reference: the generator body only runs once `thread` below // is assigned (spawn returns before the body executes). - function* threadBody(): Generator { + function* threadBody(): Generator { yield* body(thread); } const thread: Thread = new Thread(task, threadBody()); @@ -55,7 +54,7 @@ Deno.test("driveAsync: a sibling's tail dispatches immediately (CM#705)", async const bThread = spawn(bTask, function* (thread) { yield* bTask.enterImplicitThread(thread); bTask.start(); - yield { readyFunc: null, cancellable: false, awaitValue: parkPromise }; + yield { readyFunc: null, awaitValue: parkPromise }; order.push("b tail ran"); bTask.return_([]); bTask.exitImplicitThread(thread); diff --git a/runtime/tests/stale_cancellable_flag_test.ts b/runtime/tests/stale_cancellable_flag_test.ts deleted file mode 100644 index 536aa25..0000000 --- a/runtime/tests/stale_cancellable_flag_test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// `Thread.cancellable` is set at each block point and never cleared on resume, -// so a callback task's implicit thread still looks cancellable while its frame -// RUNS holding `inst.exclusiveThread`. `Task.requestCancellation` then picks it -// (`excludeImplicit` is false — the holder IS the implicit thread) and calls -// `Thread.resume`, which asserts "resume() on a running or finished thread". -// The reference evaluates cancellability live (`cancellable = lock_available`, -// definitions.py 2167/2175): with the slot held it is False, so -// `request_cancellation` (499-503) records PENDING_CANCEL and delivers it at -// the task's next cancellable wait. - -import { assertEq } from "./support/asserts.ts"; -import { - type BlockRequest, - type Cancelled, - ComponentInstanceState, - Store, - Task, - type TaskOptions, - Thread, -} from "../src/task/mod.ts"; -import type { FuncType } from "../src/cabi/types.ts"; - -const ASYNC_FT: FuncType = { params: [], results: [], async: true }; - -const CALLBACK_OPTS: TaskOptions = { - async_: true, - callback: true, - stringEncoding: "utf8", - memory: null, -}; - -function spawn( - task: Task, - body: (t: Thread) => Generator, -): Thread { - // Forward reference: the generator body only runs once `thread` below - // is assigned (spawn returns before the body executes). - function* threadBody(): Generator { - yield* body(thread); - } - const thread: Thread = new Thread(task, threadBody()); - return thread; -} - -Deno.test("requestCancellation reaching a RUNNING implicit thread parks as pending-cancel", () => { - const store = new Store(); - const inst = new ComponentInstanceState(0, store); - const a = new Task(ASYNC_FT, CALLBACK_OPTS, inst, () => [], () => {}); - - let thrown: unknown = null; - let stateAfterRequest = ""; - - const ta = spawn(a, function* (thread) { - yield* a.enterImplicitThread(thread); // takes inst.exclusiveThread - a.start(); - // A cancellable wait that returns normally; `thread.cancellable` stays true. - yield* thread.waitUntil(() => true, true); - // Now the frame is RUNNING and holds the exclusive slot. This stands in - // for a nested task of the caller's instance running `subtask.cancel` on - // A's handle through a FACT sync start-call. - try { - a.requestCancellation(null); - } catch (e) { - thrown = e; - } - stateAfterRequest = a.state; - // Reference: the request is pending and is delivered at the next - // cancellable block point. - const cancelled = yield* thread.waitUntil(() => true, true); - if (cancelled) a.cancel(); - else a.return_([]); - a.exitImplicitThread(thread); - }); - ta.resume(); - for (let i = 0; i < 20 && store.tick(); i++); - - if (thrown !== null) { - throw new Error( - `requestCancellation on a running implicit thread threw instead of ` + - `parking the request: ${thrown}`, - ); - } - assertEq(stateAfterRequest, "pending-cancel"); - assertEq(a.state, "resolved"); -}); diff --git a/runtime/tests/stream_delivery_delta_test.ts b/runtime/tests/stream_delivery_delta_test.ts index 48d5eb6..14f016f 100644 --- a/runtime/tests/stream_delivery_delta_test.ts +++ b/runtime/tests/stream_delivery_delta_test.ts @@ -22,6 +22,7 @@ import { createStreamDropWritable, createStreamNew, createStreamRead, + createStreamTransfer, createStreamWrite, type StreamTrampolineContext, } from "../src/intrinsics/stream_builtins.ts"; @@ -35,6 +36,7 @@ import { EventCode, popCurrentThread, pushCurrentThread, + SharedStreamImpl, Store, Task, Thread, @@ -98,6 +100,8 @@ function fixture(kind: "stream" | "future", elem: { kind: "u8" } | null) { futureElem: () => elem, resultTypes: () => [], suspensionMode: "plain" as const, + streamTableInstance: () => inst, + futureTableInstance: () => inst, } as unknown as StreamTrampolineContext; const task = new Task( { params: [], results: [], async: true }, @@ -130,6 +134,76 @@ function fixture(kind: "stream" | "future", elem: { kind: "u8" } | null) { }; } +Deno.test("idle drop is one-shot and drop-before-join remains observable", () => { + const f = fixture("stream", { kind: "u8" }); + const writer = f.inst.handles.get(f.wi) as WritableStreamEnd; + f.run(() => + createStreamDropReadable({ streamTable: 0 }, f.ctx, f.inst)(f.ri) + ); + assertEq(writer.hasPendingEvent(), true); + const set = new WaitableSet(); + writer.join(set); + const [code, index, payload] = set.getPendingEvent(); + assertEq([code, index, payload], [ + EventCode.STREAM_WRITE, + f.wi, + CopyResult.DROPPED, + ]); + assertEq(writer.hasPendingEvent(), false); + assertEq(writer.state, CopyState.DONE); +}); + +Deno.test("FACT transfer preserves endpoint identity and reports destination index", () => { + const f = fixture("stream", { kind: "u8" }); + const end = f.inst.handles.get(f.ri); + f.run(() => + createStreamDropWritable({ streamTable: 0 }, f.ctx, f.inst)(f.wi) + ); + const dstInst = new ComponentInstanceState(1, f.inst.store); + // Give the destination an independent occupied prefix so its assigned index + // cannot coincidentally equal the source slot. + while (dstInst.handles.add(new WaitableSet()) <= f.ri) { + // keep filling + } + const ctx = { + ...(f.ctx as unknown as Record), + streamTableInstance: (index: number) => index === 0 ? f.inst : dstInst, + streamElem: () => ({ kind: "u8" }), + } as unknown as StreamTrampolineContext; + const transfer = createStreamTransfer(ctx as never); + const dst = f.run(() => transfer(f.ri, 0, 1)) as number; + assert(dst !== f.ri, "test must force a distinct destination index"); + assert( + end === dstInst.handles.get(dst), + "FACT transfer must move the same endpoint", + ); + const moved = end as { getPendingEvent(): [EventCode, number, number] }; + const [, index, payload] = moved.getPendingEvent(); + assertEq(index, dst); + assertEq(payload, CopyResult.DROPPED); +}); + +for (const first of ["read", "write"] as const) { + for (const [firstLength, secondLength] of [[0, 0], [0, 1], [1, 0]] as const) { + Deno.test(`same-instance nonnumeric ${first} ${firstLength}/${secondLength} is permitted`, () => { + const shared = new SharedStreamImpl({ kind: "string" }); + const inst = {}; + const buffer = (length: number) => ({ + t: { kind: "string" } as const, + remain: () => length, + isZeroLength: () => length === 0, + }); + const call = (side: "read" | "write", length: number) => { + if (side === "read") { + shared.read(inst, buffer(length) as never, () => {}, () => {}); + } else shared.write(inst, buffer(length) as never, () => {}, () => {}); + }; + call(first, firstLength); + call(first === "read" ? "write" : "read", secondLength); + }); + } +} + for (const pending of ["read", "write"] as const) { for ( const scenario of [ diff --git a/runtime/tests/subtask_cancel_sync_waiter_window_test.ts b/runtime/tests/subtask_cancel_sync_waiter_window_test.ts index b073dfd..f166fac 100644 --- a/runtime/tests/subtask_cancel_sync_waiter_window_test.ts +++ b/runtime/tests/subtask_cancel_sync_waiter_window_test.ts @@ -31,11 +31,6 @@ import { withActivation, } from "../src/task/mod.ts"; import type { SuspensionPoint } from "../src/jspi/mod.ts"; -import { - blockCurrentActivation, - enterWasm, - suspendingImport, -} from "../src/jspi/mod.ts"; import type { FuncType } from "../src/cabi/types.ts"; import { createAsyncStartCall, @@ -45,12 +40,6 @@ import { } from "../src/intrinsics/fact_calls.ts"; import { newStats } from "../src/exec/boundary.ts"; -const FACT_GUEST = await WebAssembly.compile( - await Deno.readFile( - new URL("./subtask_cancel_async_fact.wasm", import.meta.url), - ), -); - const FT: FuncType = { params: [], results: [], async: true }; const OPTS: TaskOptions = { async_: true, @@ -88,18 +77,18 @@ Deno.test( const wset = new WaitableSet(); const seti = inst.handles.add(wset); asGuest(() => createWaitableJoin(inst)(subtaski, seti)); - assertEq(wset.poll(callerTask, false), [EventCode.NONE, 0, 0]); + assertEq(wset.poll(callerTask), [EventCode.NONE, 0, 0]); const lender = { numLends: 0 }; subtask.addLender(lender); subtask.resolve(SubtaskState.CANCELLED_BEFORE_RETURNED, []); subtask.setSubtaskPendingEvent(subtaski); assertEq( - wset.poll(callerTask, false), + wset.poll(callerTask), [EventCode.SUBTASK, subtaski, SubtaskState.CANCELLED_BEFORE_RETURNED], ); assertEq(subtask.resolveDelivered(), true); assertEq(lender.numLends, 0); - assertEq(wset.poll(callerTask, false), [EventCode.NONE, 0, 0]); + assertEq(wset.poll(callerTask), [EventCode.NONE, 0, 0]); }, ); @@ -205,34 +194,8 @@ function startFactSubtask( }; } -async function instantiateCancellingGuest( - store: Store, -): Promise<() => unknown> { - const block = suspendingImport( - () => { - const task = currentTask() as Task; - return blockCurrentActivation({ - store, - task, - readyFunc: () => false, - cancellable: true, - produce: () => 1, - }); - }, - "jspi", - ); - const instance = await WebAssembly.instantiate(FACT_GUEST, { - host: { - block, - "task-cancel": () => (currentTask() as Task).cancel(), - }, - // TypeScript's WebAssembly import union has not caught up with JSPI. - } as unknown as WebAssembly.Imports); - return enterWasm(instance.exports.run as () => unknown, "jspi"); -} - for (const progressBeforeCancel of [true, false]) { - Deno.test(`sync subtask.cancel waits past STARTED progress ${progressBeforeCancel ? "before" : "during"} its park`, async () => { + Deno.test(`sync subtask.cancel holds claim and lenders past STARTED (${progressBeforeCancel})`, async () => { const f = startFactSubtask(); if (!progressBeforeCancel) f.st.getPendingEvent(); const lender = { numLends: 0 }; @@ -241,10 +204,18 @@ for (const progressBeforeCancel of [true, false]) { createSubtaskCancel({ async: false }, f.caller, "jspi")(f.i) ) as unknown as Promise; assertEq(pending instanceof Promise, true); - assertEq(f.cancelled(), true); assertEq(f.st.hasSyncWaiter, true); if (!progressBeforeCancel) f.st.setSubtaskPendingEvent(f.i); - assertEq(f.store.tick(), false, "STARTED is not resolution"); + assertEq( + f.store.tick(), + true, + "pending cancellation callback is serviceable", + ); + assertEq(f.cancelled(), true); + assertEq(f.st.state, SubtaskState.STARTED); + assertEq(f.st.resolved(), false, "STARTED progress is not resolution"); + assertEq(f.st.hasSyncWaiter, true); + assertEq(lender.numLends, 1); const joinSet = new WaitableSet(); const joinSeti = f.caller.handles.add(joinSet); assertTrap(() => @@ -252,7 +223,6 @@ for (const progressBeforeCancel of [true, false]) { ); assertEq(lender.numLends, 1); - // A normal callback event lets the cooperatively cancelled callee resolve. f.resolveOnCallback(); const signal = new Subtask(); signal.join(f.set); @@ -264,115 +234,6 @@ for (const progressBeforeCancel of [true, false]) { assertEq(await pending, SubtaskState.CANCELLED_BEFORE_RETURNED); assertEq(f.st.resolveDelivered(), true); assertEq(f.st.hasSyncWaiter, false); - assertEq(f.st.hasPendingEvent(), false); assertEq(lender.numLends, 0); - f.asGuest(() => createWaitableJoin(f.caller)(f.i, joinSeti)); - assertEq(f.st.inWaitableSet(), true); }); } - -Deno.test( - "#345 FACT/JSPI: terminal event cannot be stolen before async cancel consumes it", - async () => { - const store = new Store(); - const guest = await instantiateCancellingGuest(store); - const f = startFactSubtask(guest, true, store); - f.st.getPendingEvent(); - const lender = { numLends: 0 }; - f.st.addLender(lender); - const pending = f.asGuest(() => - createSubtaskCancel({ async: true }, f.caller, "jspi")(f.i) - ) as unknown as Promise; - assertEq(pending instanceof Promise, true); - assertEq(f.cancelled(), false); - assertEq(f.st.resolved(), false); - assertEq(f.st.hasSyncWaiter, true); - - const joinSet = new WaitableSet(); - const joinSeti = f.caller.handles.add(joinSet); - assertTrap(() => - f.asGuest(() => createWaitableJoin(f.caller)(f.i, joinSeti)) - ); - assertEq( - joinSet.poll(new Task(FT, OPTS, f.caller, () => [], () => {}), false), - [EventCode.NONE, 0, 0], - ); - - // Let the engine run the resumed guest continuation. It calls task.cancel, - // and FACT publishes the terminal event before cancel's park is resumed. - await Promise.resolve(); - await Promise.resolve(); - assertEq(f.st.resolved(), true); - assertEq(f.st.hasPendingEvent(), true); - assertEq(f.st.hasSyncWaiter, true); - - assertTrap(() => - f.asGuest(() => createWaitableJoin(f.caller)(f.i, joinSeti)) - ); - assertEq( - joinSet.poll(new Task(FT, OPTS, f.caller, () => [], () => {}), false), - [ - EventCode.NONE, - 0, - 0, - ], - ); - const point = f.store.waiting.find((w) => w.task !== f.st.calleeTask) as - | SuspensionPoint - | undefined; - if (point === undefined) throw new Error("missing async cancel park"); - point.resume(); - assertEq(await pending, SubtaskState.CANCELLED_BEFORE_RETURNED); - assertEq(f.st.hasPendingEvent(), false); - assertEq(f.st.resolveDelivered(), true); - assertEq(f.st.hasSyncWaiter, false); - assertEq(lender.numLends, 0); - assertEq(f.store.waiting.length, 0); - assertTrap(() => - f.asGuest(() => - createSubtaskCancel({ async: true }, f.caller, "jspi")(f.i) - ) - ); - }, -); - -Deno.test( - "#345 FACT/JSPI: unresolved async cancel returns BLOCKED, then join/poll resolves", - async () => { - const f = startFactSubtask(); - f.st.getPendingEvent(); - const lender = { numLends: 0 }; - f.st.addLender(lender); - assertEq( - await f.asGuest(() => - createSubtaskCancel({ async: true }, f.caller, "jspi")(f.i) - ), - BLOCKED, - ); - assertEq(f.cancelled(), true); - assertEq(f.st.hasSyncWaiter, false); - const joinSet = new WaitableSet(); - const seti = f.caller.handles.add(joinSet); - f.asGuest(() => createWaitableJoin(f.caller)(f.i, seti)); - f.resolveOnCallback(); - const signal = new Subtask(); - signal.join(f.set); - signal.setSubtaskPendingEvent(1); - assertEq(f.store.tick(), true); - assertEq( - joinSet.poll(new Task(FT, OPTS, f.caller, () => [], () => {}), false), - [EventCode.SUBTASK, f.i, SubtaskState.CANCELLED_BEFORE_RETURNED], - ); - assertEq(f.st.resolveDelivered(), true); - assertEq(lender.numLends, 0); - assertEq( - joinSet.poll(new Task(FT, OPTS, f.caller, () => [], () => {}), false), - [ - EventCode.NONE, - 0, - 0, - ], - ); - assertEq(f.store.waiting.length, 0); - }, -); diff --git a/runtime/tests/suspension_point_pending_cancel_test.ts b/runtime/tests/suspension_point_pending_cancel_test.ts deleted file mode 100644 index ba50aa6..0000000 --- a/runtime/tests/suspension_point_pending_cancel_test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// `SuspensionPoint`/`blockCurrentActivation` carried the pre-#302 shape of -// `Thread.waitUntil`: it parked on the raw `readyFunc`, returned the raw -// cancelled flag, and left `cancellable` set after resuming. The reference -// (definitions.py `Thread.wait_until` 361-373) parks on `ready_func() or -// (cancellable() and task.has_pending_cancel())` and re-runs -// `deliver_pending_cancel` AFTER the block; task/thread.ts:waitUntil ports -// that, and this pins the jspi block path to the same behavior. -// -// The producing shape: a cancel that arrives while a sibling activation of the -// instance holds `exclusiveThread` parks as `pending-cancel` -// (`Task.requestCancellation` excludes the implicit thread — and hence its -// suspension points — while the lock is held). When the slot frees, the -// reference wakes the parked block point and hands it Cancelled.TRUE. -// -// Issue #300 notes this is unreachable through real guests today: the only -// producer of a pending-cancel against a live cancellable wait is the -// exclusive-slot exclusion, which no real `SuspensionPoint` owner meets. So -// the point is driven directly, with fake threads and no wasm, in the style of -// wait_until_pending_cancel_test.ts. - -import { assertEq } from "./support/asserts.ts"; -import { - type BlockRequest, - type Cancelled, - ComponentInstanceState, - Store, - Task, - type TaskOptions, - Thread, -} from "../src/task/mod.ts"; -import { SuspensionPoint } from "../src/jspi/mod.ts"; -import type { FuncType } from "../src/cabi/types.ts"; - -const ASYNC_FT: FuncType = { params: [], results: [], async: true }; - -/** Async-typed, callback ABI: `needsExclusive()` is true. */ -const CALLBACK_OPTS: TaskOptions = { - async_: true, - callback: true, - stringEncoding: "utf8", - memory: null, -}; - -function spawn( - task: Task, - body: (t: Thread) => Generator, -): Thread { - // Forward reference: the generator body only runs once `thread` below - // is assigned (spawn returns before the body executes). - function* threadBody(): Generator { - yield* body(thread); - } - const thread: Thread = new Thread(task, threadBody()); - return thread; -} - -function mkTask(inst: ComponentInstanceState, opts: TaskOptions): Task { - return new Task(ASYNC_FT, opts, inst, () => [], () => {}); -} - -/** - * Drive the store to quiescence; bounded so a lost wakeup is not a hang. - * - * Settling a suspension point takes a `pendingResumptions` entry against the - * resumed activation, and `tick` refuses to run while one is outstanding. In a - * real jspi run the engine resumes the wasm activation, which closes its own - * window by parking again or finishing; here nothing does, so the loop plays - * that edge (`Store.releasePendingOf`, the settle-side half) itself. - */ -function runToQuiescence(store: Store, task: Task): void { - for (let i = 0; i < 50; i++) { - if (!store.tick()) break; - store.releasePendingOf(task.implicitThread); - } -} - -/** Sibling B: takes the exclusive slot, parks non-cancellably until `gate`. */ -function spawnSibling( - inst: ComponentInstanceState, - gate: { open: boolean }, -): Thread { - const b = mkTask(inst, CALLBACK_OPTS); - const tb = spawn(b, function* (thread) { - yield* b.enterImplicitThread(thread); - b.start(); - yield* thread.waitUntil(() => gate.open, false); - b.return_([]); - b.exitImplicitThread(thread); - }); - tb.resume(); - return tb; -} - -/** - * Task A blocking the jspi way: a cancellable `SuspensionPoint` sits in - * `store.waiting` while the Thread that owns the activation parks - * non-cancellably on the settle of that point (the `awaitValue` seam, modelled - * here as a plain flag rather than a Promise). - */ -function spawnJspiBlocker( - inst: ComponentInstanceState, - readyFunc: (() => boolean) | null, - observed: boolean[], -): Task { - const a = mkTask(inst, CALLBACK_OPTS); - const ta = spawn(a, function* (thread) { - yield* a.enterImplicitThread(thread); - a.start(); - inst.exclusiveThread = null; // released across the block, as the loop does - const settled = { done: false }; - new SuspensionPoint( - inst.store, - a, - readyFunc, - true, // cancellable block point - (cancelled) => { - observed.push(cancelled === true); - settled.done = true; - return 0; - }, - thread, - ); - yield* thread.waitUntil(() => settled.done, false); - inst.exclusiveThread = thread; // retake, as the loop does - if (observed[0]) a.cancel(); - else a.return_([]); - a.exitImplicitThread(thread); - }); - ta.resume(); - return a; -} - -Deno.test("jspi block: a cancel pending behind a sibling's exclusive slot resumes the suspension point as cancelled", () => { - const store = new Store(); - const inst = new ComponentInstanceState(0, store); - const observed: boolean[] = []; - - // `readyFunc` mirrors the callback loop's "the lock is free" condition, so - // the point IS woken pre-fix — what it was handed is the question. - const a = spawnJspiBlocker( - inst, - () => inst.exclusiveThread === null, - observed, - ); - - const gate = { open: false }; - const tb = spawnSibling(inst, gate); - assertEq(inst.exclusiveThread === tb, true, "B holds the exclusive slot"); - - a.requestCancellation(null); - // Agreed by both: A is not cancellable while B holds the lock, and its - // suspension point is a frame of A's implicit thread. - assertEq(a.state, "pending-cancel"); - - gate.open = true; - runToQuiescence(store, a); - - // Reference: the post-block `deliver_pending_cancel` converts the - // Cancelled.FALSE resumption into Cancelled.TRUE. Pre-fix: a spurious false. - assertEq(observed, [true]); - assertEq(a.state, "resolved"); -}); - -Deno.test("jspi block: a suspension point whose readyFunc never holds is still woken by the pending cancel", () => { - const store = new Store(); - const inst = new ComponentInstanceState(0, store); - const observed: boolean[] = []; - - // Nothing this point waits for will ever happen (an empty waitable set): - // only the reference's `cancellable() and has_pending_cancel()` disjunct - // can make it ready. - const a = spawnJspiBlocker(inst, () => false, observed); - - const gate = { open: false }; - spawnSibling(inst, gate); - a.requestCancellation(null); - assertEq(a.state, "pending-cancel"); - - gate.open = true; - runToQuiescence(store, a); - - // Pre-fix: never woken at all — `observed` stays empty and A never resolves. - assertEq(observed, [true]); - assertEq(a.state, "resolved"); -}); diff --git a/runtime/tests/sync_callee_progress_test.ts b/runtime/tests/sync_callee_progress_test.ts index 2406129..24b9d83 100644 --- a/runtime/tests/sync_callee_progress_test.ts +++ b/runtime/tests/sync_callee_progress_test.ts @@ -64,7 +64,6 @@ async function parkedSyncChild(input: { store: inst.store, task: child.task, readyFunc: () => false, - cancellable: false, produce: () => 7, blockReason: input.blockReason, })) as Promise; @@ -148,12 +147,8 @@ Deno.test("same-instance runnable producer may satisfy a sync callee", async () assertEq(await result, 7); }); -Deno.test("async-typed callee and host-import latency are allowed to remain pending", async () => { - for ( - const options of [{ asyncTyped: true }, { - blockReason: "host-import" as const, - }] - ) { +Deno.test("host-import latency is allowed to remain pending", async () => { + for (const options of [{ blockReason: "host-import" as const }]) { const { result, resume } = await parkedSyncChild(options); const verdict = await Promise.race([ result.then(() => "settled"), @@ -166,3 +161,105 @@ Deno.test("async-typed callee and host-import latency are allowed to remain pend await result; } }); + +Deno.test("async-typed child under a live async root remains pending", async () => { + const { result, resume } = await parkedSyncChild({ asyncTyped: true }); + const verdict = await Promise.race([ + result.then(() => "settled", () => "rejected"), + new Promise((resolve) => setTimeout(() => resolve("pending"), 10)), + ]); + assertEq(verdict, "pending"); + resume(); + assertEq(await result, 7); +}); + +Deno.test("three-level async child drives the discovered sync callee instance", async () => { + const store = new ComponentInstanceState(99).store; + const a = new ComponentInstanceState(0, store); + const b = new ComponentInstanceState(1, store); + const c = new ComponentInstanceState(2, store); + const rootThread = root(a).thread; + const syncB = new SynchronousActivation(b, false, rootThread); + const asyncC = new SynchronousActivation(c, true, syncB.thread); + popLogicalActivation(asyncC.thread); + popLogicalActivation(syncB.thread); + const result = withActivation(asyncC.thread, () => + blockCurrentActivation({ + store, + task: asyncC.task, + readyFunc: () => false, + produce: () => 9, + })) as Promise; + const point = store.waiting[0]; + const producerTask = new Task( + { params: [], results: [], async: false }, + SYNC_OPTS, + b, + () => [], + () => {}, + ); + const producer = new Thread( + producerTask, + // deno-lint-ignore require-yield + (function* () { + point.resume(); + })(), + ); + producerTask.registerThread(producer); + producer.resumeLater(); + await Promise.resolve(); + requestStoreService(store); + assertEq(await result, 9); +}); + +Deno.test("three-level async child cannot be rescued by an outer ready producer", async () => { + const store = new ComponentInstanceState(99).store; + const a = new ComponentInstanceState(0, store); + const b = new ComponentInstanceState(1, store); + const c = new ComponentInstanceState(2, store); + const rootThread = root(a).thread; + const syncB = new SynchronousActivation(b, false, rootThread); + const asyncC = new SynchronousActivation(c, true, syncB.thread); + popLogicalActivation(asyncC.thread); + popLogicalActivation(syncB.thread); + let outerRan = false; + let rescued = false; + const result = withActivation(asyncC.thread, () => + blockCurrentActivation({ + store, + task: asyncC.task, + readyFunc: () => false, + produce: () => { + rescued = true; + return 9; + }, + })) as Promise; + const producerTask = new Task( + { params: [], results: [], async: false }, + SYNC_OPTS, + a, + () => [], + () => {}, + ); + const producer = new Thread( + producerTask, + // deno-lint-ignore require-yield + (function* () { + outerRan = true; + })(), + ); + producerTask.registerThread(producer); + producer.resumeLater(); + await Promise.resolve(); + requestStoreService(store); + const cause = await semanticRejection(result); + assert( + cause instanceof Trap && + cause.message.includes( + "cannot block a synchronous task before returning", + ), + ); + assertEq(rescued, false, "outer work did not rescue the B-owned wait"); + // Once B has unwound, ordinary service may run A. + assertEq(outerRan, true); +}); diff --git a/runtime/tests/task_test.ts b/runtime/tests/task_test.ts index cf3de9e..a678dc8 100644 --- a/runtime/tests/task_test.ts +++ b/runtime/tests/task_test.ts @@ -10,7 +10,6 @@ import { assertEq } from "./support/asserts.ts"; import { Trap } from "../src/cabi/mod.ts"; import { type BlockRequest, - type Cancelled, chooseCandidate, ComponentInstanceState, driveSyncLift, @@ -19,7 +18,6 @@ import { isInstancePoisoned, notifyInstancePoisoned, packSubtaskResult, - PendingCapability, schedulerPolicy, schedulerSeedForTesting, Store, @@ -83,11 +81,11 @@ const STACKFUL_OPTS: TaskOptions = { */ function spawn( task: Task, - body: (t: Thread) => Generator, + body: (t: Thread) => Generator, ): Thread { // Forward reference: the generator body only runs once `thread` below // is assigned (spawn returns before the body executes). - function* threadBody(): Generator { + function* threadBody(): Generator { yield* body(thread); } const thread: Thread = new Thread(task, threadBody()); @@ -138,8 +136,7 @@ Deno.test("thread: wait_until blocks and the store resumes when ready", () => { yield* task.enterImplicitThread(thread); task.start(); order.push("before"); - const cancelled = yield* thread.waitUntil(() => flag, false); - assertEq(cancelled, false); + yield* thread.waitUntil(() => flag); order.push("after"); task.return_([]); task.exitImplicitThread(thread); @@ -183,7 +180,7 @@ Deno.test("canon_lift sync loop: traps when no thread can make progress", () => yield* task.enterImplicitThread(thread); task.start(); // Blocks forever: nothing will ever make this ready. - yield* thread.waitUntil(() => false, false); + yield* thread.waitUntil(() => false); task.return_([]); task.exitImplicitThread(thread); }); @@ -230,7 +227,7 @@ Deno.test("cm705: tick resumes a ready sibling thread during a live host entry", const bThread = spawn(bTask, function* (thread) { yield* bTask.enterImplicitThread(thread); bTask.start(); - yield* thread.waitUntil(() => flag, false); + yield* thread.waitUntil(() => flag); order.push("b ran"); bTask.return_([]); bTask.exitImplicitThread(thread); @@ -278,7 +275,7 @@ Deno.test("cm705: serviceSettled dispatches a sibling tail immediately", async ( const bThread = spawn(bTask, function* (thread) { yield* bTask.enterImplicitThread(thread); bTask.start(); - const v = yield { readyFunc: null, cancellable: false, awaitValue: p }; + const v = yield { readyFunc: null, awaitValue: p }; void v; order.push("b tail ran"); bTask.return_([]); @@ -315,7 +312,7 @@ Deno.test("cm705: the phantom-state gate holds for a serviceable tail", async () const bThread = spawn(bTask, function* (thread) { yield* bTask.enterImplicitThread(thread); bTask.start(); - yield { readyFunc: null, cancellable: false, awaitValue: p }; + yield { readyFunc: null, awaitValue: p }; bTask.return_([]); bTask.exitImplicitThread(thread); }); @@ -328,7 +325,7 @@ Deno.test("cm705: the phantom-state gate holds for a serviceable tail", async () const aThread = spawn(aTask, function* (thread) { yield* aTask.enterImplicitThread(thread); aTask.start(); - yield* thread.waitUntil(() => flag, false); + yield* thread.waitUntil(() => flag); order.push("a ran"); aTask.return_([]); aTask.exitImplicitThread(thread); @@ -365,7 +362,7 @@ Deno.test("cm705: a poisoned instance's tail retires without running", async () const bThread = spawn(bTask, function* (thread) { yield* bTask.enterImplicitThread(thread); bTask.start(); - yield { readyFunc: null, cancellable: false, awaitValue: p }; + yield { readyFunc: null, awaitValue: p }; order.push("b tail ran"); bTask.return_([]); bTask.exitImplicitThread(thread); @@ -397,7 +394,7 @@ Deno.test("cm705: stale settled entries are removed", async () => { const bThread = spawn(bTask, function* (thread) { yield* bTask.enterImplicitThread(thread); bTask.start(); - yield { readyFunc: null, cancellable: false, awaitValue: p }; + yield { readyFunc: null, awaitValue: p }; bTask.return_([]); bTask.exitImplicitThread(thread); }); @@ -558,7 +555,7 @@ Deno.test("exclusive thread: a second callback task waits for the first", () => yield* task.enterImplicitThread(thread); order.push(`${name}:in`); task.start(); - if (!release.now) yield* thread.waitUntil(() => release.now, false); + if (!release.now) yield* thread.waitUntil(() => release.now); task.return_([]); task.exitImplicitThread(thread); order.push(`${name}:out`); @@ -619,7 +616,7 @@ Deno.test("waitable set: poll returns NONE rather than blocking", () => { const inst = new ComponentInstanceState(0); const task = mkTask(inst, ASYNC_FT, CALLBACK_OPTS); const wset = new WaitableSet(); - const [code] = wset.poll(task, false); + const [code] = wset.poll(task); assertEq(code, EventCode.NONE); }); @@ -689,34 +686,7 @@ Deno.test("subtask: the async-lower return value packs state and index", () => { // Cancellation // --------------------------------------------------------------------------- -Deno.test("cancellation: a request at a cancellable block point is delivered", () => { - const store = new Store(); - const inst = new ComponentInstanceState(0, store); - let sawCancel = false; - let resolvedWith: unknown = "unset"; - const task = mkTask(inst, ASYNC_FT, STACKFUL_OPTS, (r) => { - resolvedWith = r; - }); - const thread = spawn(task, function* (thread) { - yield* task.enterImplicitThread(thread); - task.start(); - const cancelled = yield* thread.waitUntil(() => false, true); - if (cancelled) { - sawCancel = true; - task.cancel(); - } - task.exitImplicitThread(thread); - }); - thread.resume(); - assertEq(task.state, "started"); - task.requestCancellation(null); - assertEq(sawCancel, true); - // definitions.py `Task.cancel`: `on_resolve(None)`. - assertEq(resolvedWith, null); - assertEq(task.state, "resolved"); -}); - -Deno.test("cancellation: with no cancellable thread it becomes pending", () => { +Deno.test("cancellation: stackful waits do not consume pending cancellation", () => { const store = new Store(); const inst = new ComponentInstanceState(0, store); const gate = { open: false }; @@ -724,12 +694,8 @@ Deno.test("cancellation: with no cancellable thread it becomes pending", () => { const thread = spawn(task, function* (thread) { yield* task.enterImplicitThread(thread); task.start(); - // Non-cancellable block point. - yield* thread.waitUntil(() => gate.open, false); - // ... and then a cancellable one, which picks up the pending request. - const cancelled = yield* thread.waitUntil(() => false, true); - assertEq(cancelled, true); - task.cancel(); + yield* thread.waitUntil(() => gate.open); + task.return_([]); task.exitImplicitThread(thread); }); thread.resume(); @@ -758,7 +724,7 @@ Deno.test("tick: a trap under tick records the poison marker", async () => { const parkedThread = spawn(parkedTask, function* (thread) { yield* parkedTask.enterImplicitThread(thread); parkedTask.start(); - yield { readyFunc: null, cancellable: false, awaitValue: p }; + yield { readyFunc: null, awaitValue: p }; order.push("parked tail ran"); parkedTask.return_([]); parkedTask.exitImplicitThread(thread); @@ -772,7 +738,7 @@ Deno.test("tick: a trap under tick records the poison marker", async () => { const trapThread = spawn(trapTask, function* (thread) { yield* trapTask.enterImplicitThread(thread); trapTask.start(); - yield* thread.waitUntil(() => flag, false); + yield* thread.waitUntil(() => flag); throw new Trap("boom under tick"); }); trapThread.resume(); @@ -802,54 +768,6 @@ Deno.test("tick: a trap under tick records the poison marker", async () => { assertEq(order.length, 0, "retired quietly: the body never ran"); }); -Deno.test("request_cancellation: a trap during delivery poisons the callee", () => { - // definitions.py `Task.request_cancellation` (@ 2f13265) wraps the delivery - // `resume(Cancelled.TRUE)` in no handler at all. polyengine records the - // trap as per-instance poisoning instead of ending the world - // (polyengine#164/#212). - const store = new Store(); - const callerInst = new ComponentInstanceState(0, store); - const b = new ComponentInstanceState(1, store); - const task = mkTask(b, ASYNC_FT, STACKFUL_OPTS); - const thread = spawn(task, function* (thread) { - yield* task.enterImplicitThread(thread); - task.start(); - const cancelled = yield* thread.waitUntil(() => false, true); - if (cancelled) throw new Trap("boom during cancel delivery"); - }); - thread.resume(); - assertEq(task.state, "started"); - - assertThrows( - () => task.requestCancellation(callerInst), - "boom during cancel delivery", - ); - assertEq(isInstancePoisoned(b), true, "the callee is a corpse"); - assertEq(task.state, "cancel-delivered", "parity: the state is set first"); -}); - -Deno.test("request_cancellation: a capability signal does not poison", () => { - // Capability signals mark the RUNTIME incomplete, not the component - // faulted: nothing is poisoned, exactly as in `Store.tick`. - const store = new Store(); - const callerInst = new ComponentInstanceState(0, store); - const b = new ComponentInstanceState(1, store); - const task = mkTask(b, ASYNC_FT, STACKFUL_OPTS); - const thread = spawn(task, function* (thread) { - yield* task.enterImplicitThread(thread); - task.start(); - const cancelled = yield* thread.waitUntil(() => false, true); - if (cancelled) throw new PendingCapability("x"); - }); - thread.resume(); - - assertThrows( - () => task.requestCancellation(callerInst), - "pending-capability: x", - ); - assertEq(isInstancePoisoned(b), false, "nothing is poisoned"); -}); - // --------------------------------------------------------------------------- // Poisoning is the refusal mechanism // --------------------------------------------------------------------------- @@ -899,7 +817,7 @@ Deno.test("re-key: tick does not resume a marked instance", () => { const thread = spawn(task, function* (thread) { yield* task.enterImplicitThread(thread); task.start(); - yield* thread.waitUntil(() => flag, false); + yield* thread.waitUntil(() => flag); order.push("ran"); task.return_([]); task.exitImplicitThread(thread); @@ -917,16 +835,11 @@ Deno.test("re-key: requestCancellation leaves a marked callee's request pending" const store = new Store(); const callerInst = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); - let sawCancel = false; const task = mkTask(b, ASYNC_FT, STACKFUL_OPTS); const thread = spawn(task, function* (thread) { yield* task.enterImplicitThread(thread); task.start(); - const cancelled = yield* thread.waitUntil(() => false, true); - if (cancelled) { - sawCancel = true; - task.cancel(); - } + yield* thread.waitUntil(() => false); task.exitImplicitThread(thread); }); thread.resume(); @@ -935,7 +848,6 @@ Deno.test("re-key: requestCancellation leaves a marked callee's request pending" notifyInstancePoisoned(b, new Trap("boom")); task.requestCancellation(callerInst); - assertEq(sawCancel, false, "delivery is refused by the marker"); assertEq(task.state, "pending-cancel"); }); @@ -1033,7 +945,7 @@ Deno.test("scheduler: a seeded schedule still resolves the same task set", () => const thread = spawn(task, function* (thread) { yield* task.enterImplicitThread(thread); task.start(); - yield* thread.yield_(false); + yield* thread.yield_(); task.return_([]); task.exitImplicitThread(thread); done.push(name); diff --git a/runtime/tests/thread_builtins_test.ts b/runtime/tests/thread_builtins_test.ts index 69dbf5b..85741c3 100644 --- a/runtime/tests/thread_builtins_test.ts +++ b/runtime/tests/thread_builtins_test.ts @@ -217,7 +217,7 @@ Deno.test("SUPPORTED LIMITATION: equivalent non-final thread start types are rej } }); -Deno.test("thread switch validates operands before delivering pending cancellation", () => { +Deno.test("thread switch validates operands without consuming pending cancellation", () => { const module = moduleFromWatBytes(TABLE_FIXTURE); const f = fixture(module.exports.t as WebAssembly.Table); const create = createThreadNewIndirect( @@ -226,11 +226,11 @@ Deno.test("thread switch validates operands before delivering pending cancellati ); const switchedCtx = { ...f.ctx, suspensionMode: "jspi" as const }; const suspendThenResume = createThreadSuspendThenResume( - { instance: 0, cancellable: true }, + { instance: 0 }, switchedCtx, ); const yieldThenResume = createThreadYieldThenResume( - { instance: 0, cancellable: true }, + { instance: 0 }, switchedCtx, ); @@ -255,13 +255,8 @@ Deno.test("thread switch validates operands before delivering pending cancellati const child = create(0, 7) as number; f.task.state = "pending-cancel"; - assertEq(suspendThenResume(child), 1, "valid target receives cancellation"); - assertEq(f.task.state, "cancel-delivered"); - assertEq( - f.inst.threads.get(child).explicitlySuspended(), - true, - "target not switched", - ); + suspendThenResume(child); + assertEq(f.task.state, "pending-cancel"); } finally { popCurrentThread(f.parent); } diff --git a/runtime/tests/wait_until_pending_cancel_test.ts b/runtime/tests/wait_until_pending_cancel_test.ts index 294ba4d..3537e22 100644 --- a/runtime/tests/wait_until_pending_cancel_test.ts +++ b/runtime/tests/wait_until_pending_cancel_test.ts @@ -1,12 +1,6 @@ -// `Thread.waitUntil` drops the reference's cancel-aware ready predicate and -// its post-block `deliver_pending_cancel` (definitions.py `Thread.wait_until` -// lines 361-373: `ready_or_cancelled = ready_func() or (cancellable() and -// task.has_pending_cancel())`, then `if deliver_pending_cancel: return -// Cancelled.TRUE` AFTER the block). Ours parks on the raw `readyFunc` and -// returns the raw cancelled flag, so a callback task whose cancel went -// `pending-cancel` behind a sibling's `exclusiveThread` is (a) never woken at -// all when the slot frees with no event pending, (b) woken with a spurious -// NONE in the YIELD arm, (c) handed the queued event before TASK_CANCELLED. +// Callback cancellation is explicit in `canon_lift`, not a generic property of +// `Thread.waitUntil` (definitions.py:2073-2094). These generator-level models +// pin the callback loop's readiness and post-wait delivery ordering. // // The bodies below mirror exec/boundary.ts `runCallbackLoop`'s WAIT/YIELD arms // (:2640-2700) — release the slot across the wait, retake it after — with fake @@ -15,7 +9,6 @@ import { assertEq } from "./support/asserts.ts"; import { type BlockRequest, - type Cancelled, ComponentInstanceState, EventCode, type EventTuple, @@ -40,11 +33,11 @@ const CALLBACK_OPTS: TaskOptions = { function spawn( task: Task, - body: (t: Thread) => Generator, + body: (t: Thread) => Generator, ): Thread { // Forward reference: the generator body only runs once `thread` below // is assigned (spawn returns before the body executes). - function* threadBody(): Generator { + function* threadBody(): Generator { yield* body(thread); } const thread: Thread = new Thread(task, threadBody()); @@ -74,7 +67,7 @@ function spawnSibling( const tb = spawn(b, function* (thread) { yield* b.enterImplicitThread(thread); b.start(); - yield* thread.waitUntil(() => gate.open, false); + yield* thread.waitUntil(() => gate.open); b.return_([]); b.exitImplicitThread(thread); }); @@ -93,11 +86,13 @@ Deno.test("callback WAIT: cancel pending behind a sibling's exclusive slot is de yield* a.enterImplicitThread(thread); // takes inst.exclusiveThread a.start(); inst.exclusiveThread = null; // runCallbackLoop: release across the wait - const ev = yield* wset.waitForEventAnd( - thread, - () => inst.exclusiveThread === null, - true, + yield* thread.waitUntil(() => + inst.exclusiveThread === null && + (a.hasPendingCancel() || wset.hasPendingEvent()) ); + const ev = a.deliverPendingCancel() + ? [EventCode.TASK_CANCELLED, 0, 0] as EventTuple + : wset.getPendingEvent(); events.push(ev); inst.exclusiveThread = thread; // retake, as the loop does if (ev[0] === EventCode.TASK_CANCELLED) a.cancel(); @@ -111,15 +106,14 @@ Deno.test("callback WAIT: cancel pending behind a sibling's exclusive slot is de assertEq(inst.exclusiveThread === tb, true, "B holds the exclusive slot"); a.requestCancellation(null); - // Agreed by both: A is not cancellable while B holds the lock. + // The request remains pending while B holds the lock. assertEq(a.state, "pending-cancel"); gate.open = true; runToQuiescence(store); assertEq(inst.exclusiveThread, null, "B released the slot"); - // Reference: A is ready via `cancellable() and has_pending_cancel()`, - // resumes, and the post-block `deliver_pending_cancel` yields TASK_CANCELLED. + // Callback readiness includes pending cancellation explicitly. assertEq(events, [[EventCode.TASK_CANCELLED, 0, 0]]); assertEq(a.state, "resolved"); }); @@ -134,11 +128,11 @@ Deno.test("callback YIELD: a pending cancel released by the slot must resume as yield* a.enterImplicitThread(thread); a.start(); inst.exclusiveThread = null; - const cancelled = yield* thread.waitUntil( - () => inst.exclusiveThread === null, - true, + yield* thread.waitUntil(() => + inst.exclusiveThread === null && a.hasPendingCancel() ); - observed.push(cancelled === true); + const cancelled = a.deliverPendingCancel(); + observed.push(cancelled); inst.exclusiveThread = thread; if (cancelled) a.cancel(); else a.return_([]); @@ -154,9 +148,7 @@ Deno.test("callback YIELD: a pending cancel released by the slot must resume as gate.open = true; runToQuiescence(store); - // Reference: the post-block `deliver_pending_cancel` converts the - // Cancelled.FALSE resumption into Cancelled.TRUE, so the callback sees - // (TASK_CANCELLED,0,0). Ours resumes with false → a spurious (NONE,0,0). + // The callback loop performs delivery after the pure wait. assertEq(observed, [true]); }); @@ -171,11 +163,13 @@ Deno.test("callback WAIT with an event pending: TASK_CANCELLED is delivered firs yield* a.enterImplicitThread(thread); a.start(); inst.exclusiveThread = null; - const ev = yield* wset.waitForEventAnd( - thread, - () => inst.exclusiveThread === null, - true, + yield* thread.waitUntil(() => + inst.exclusiveThread === null && + (a.hasPendingCancel() || wset.hasPendingEvent()) ); + const ev = a.deliverPendingCancel() + ? [EventCode.TASK_CANCELLED, 0, 0] as EventTuple + : wset.getPendingEvent(); events.push(ev); inst.exclusiveThread = thread; if (ev[0] === EventCode.TASK_CANCELLED) a.cancel(); @@ -198,9 +192,8 @@ Deno.test("callback WAIT with an event pending: TASK_CANCELLED is delivered firs gate.open = true; runToQuiescence(store); - // Reference: `deliver_pending_cancel` runs AFTER the block and BEFORE - // `get_pending_event`, so TASK_CANCELLED wins and the SUBTASK event is left - // on the waitable. Ours delivers the SUBTASK event first. + // `deliver_pending_cancel` runs before `get_pending_event`, so the event is + // retained. assertEq(events, [[EventCode.TASK_CANCELLED, 0, 0]]); assertEq(w.hasPendingEvent(), true, "the SUBTASK event is still pending"); }); diff --git a/runtime/tests/wasmtime/cancel_starting_reuse_test.ts b/runtime/tests/wasmtime/cancel_starting_reuse_test.ts index ffc4b9d..2fbd95a 100644 --- a/runtime/tests/wasmtime/cancel_starting_reuse_test.ts +++ b/runtime/tests/wasmtime/cancel_starting_reuse_test.ts @@ -8,7 +8,6 @@ import { assertEq } from "../support/asserts.ts"; import { type BlockRequest, - type Cancelled, ComponentInstanceState, Store, Subtask, @@ -25,7 +24,7 @@ import { function parkedEntryThread(task: Task): Thread { const holder: { thread?: Thread } = {}; - const body = (function* (): Generator { + const body = (function* (): Generator { if (!(yield* task.enterImplicitThread(holder.thread!))) return; throw new Error("backpressured STARTING task unexpectedly entered"); })(); diff --git a/third_party/component-model b/third_party/component-model index 7c67611..a53b241 160000 --- a/third_party/component-model +++ b/third_party/component-model @@ -1 +1 @@ -Subproject commit 7c676115e93cd7d54c1732d95c54c6a3de7c5ae0 +Subproject commit a53b241d4d50487d256900fcad77cd6cda51808f diff --git a/tools/translate/README.md b/tools/translate/README.md index bda248a..b56cdab 100644 --- a/tools/translate/README.md +++ b/tools/translate/README.md @@ -25,7 +25,7 @@ translator build. The default shim is are checked before writing the output. The `.plan.json` file is an **envelope**, not a bare plan: it includes the plan -and base64-encoded FACT adapters. Plans currently use `formatVersion: 5`; deploy +and base64-encoded FACT adapters. Plans currently use `formatVersion: 6`; deploy with a matching runtime, which rejects other format versions. See the [plan contract](../../contracts/plan-format.md). diff --git a/tools/wasmtime/metadata.json b/tools/wasmtime/metadata.json index 4c37da5..e6c3b07 100644 --- a/tools/wasmtime/metadata.json +++ b/tools/wasmtime/metadata.json @@ -1,5 +1,5 @@ { - "source_revision": "4675ee16b703b33948073a5ff6b961367371e7a1", + "source_revision": "cc546eee265b805dda0f034b2007d110a541093d", "files": [ { "path": "adapter.json", @@ -577,6 +577,21 @@ "exceptions": true } }, + { + "path": "thread-transparency/blocks.json", + "directives": { + "component_model_async": true + } + }, + { + "path": "thread-transparency/reentrancy.json", + "directives": { + "component_model_async": true, + "component_model_async_stackful": true, + "component_model_more_async_builtins": true, + "component_model_threading": true + } + }, { "path": "trap.json", "directives": { diff --git a/translator/deno.json b/translator/deno.json index e965288..fe7e594 100644 --- a/translator/deno.json +++ b/translator/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/translator", - "version": "0.6.8", + "version": "0.7.0", "exports": { ".": "./mod.ts" }, diff --git a/upstream-component-model-repo-findings.md b/upstream-component-model-repo-findings.md index bcf18b9..dd68179 100644 --- a/upstream-component-model-repo-findings.md +++ b/upstream-component-model-repo-findings.md @@ -5,7 +5,7 @@ link here from [architecture §1](docs/architecture.md#1-goals). Entries are not authorization to file publicly; no filing is claimed without a link. Current checks below use the local `third_party/component-model` pin -`7c676115e93cd7d54c1732d95c54c6a3de7c5ae0`. Historical observations name their +`a53b241d4d50487d256900fcad77cd6cda51808f`. Historical observations name their source snapshot separately. Spec paths are relative to `design/mvp/` unless otherwise stated. Prefer function references over line numbers when rechecking. @@ -73,13 +73,14 @@ the grammar and CanonicalABI.md exposed only `backpressure.inc` and ## CM-3: `cancel_copy` returns a stale COMPLETED where wasmtime reports CANCELLED -**Status:** DRAFT, unfiled. This is the approved corpus exception in -[architecture §1](docs/architecture.md#1-goals), reversible if upstream -adjudicates otherwise. The current pin still returns the pending payload -unchanged in `cancel_copy`. +**Status:** RESOLVED by merged Component Model #719 at `a53b241`. +`StreamEnd.notify` now observes `CANCELLING_COPY` when the event is consumed and +returns `CANCELLED` with retained progress; peer drop takes precedence. The +runtime behavior is now specified, so the former corpus exception is retired. +The evidence below records the earlier pin's discrepancy. **Found:** 2026-08-08, implementing the stream copy protocol. -This finding remains separate from the provisional drop-delivery rule adopted +This finding was separate from the provisional drop-delivery rule adopted from component-model PR #719 at head `35e9769957627c2bee5cd445b998b08b3c652c86`. That rule upgrades an undelivered event when its peer is found dropped at consumption; CM-3 instead remaps an @@ -123,7 +124,7 @@ The official suite asserts wasmtime's answer, not the reference's: `test/async/big-interleaving-test.wast` writes 8, reads 4, then cancels the write without polling and expects `0x42` (`CANCELLED | 4<<4`). Under the reference's rule the answer is `0x40`. This no-poll write cancellation is -still present at the current pin. The neighboring test first polls the +present at the original pin. The neighboring test first polls the write completion, then starts and cancels another read; it does not exercise the disputed pending write event. The evidence is at lines 1520-1531 (no-poll write cancellation) and 1504-1518 (poll/read cancellation). diff --git a/wasi/deno.json b/wasi/deno.json index ba8d547..02e1cc2 100644 --- a/wasi/deno.json +++ b/wasi/deno.json @@ -1,6 +1,6 @@ { "name": "@polyengine/wasi", - "version": "0.6.8", + "version": "0.7.0", "exports": { ".": "./src/mod.ts", "./cli": "./src/cli.ts",