From 2bd1139a21694db62f2ea642b9c561d0d589ad0d Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Fri, 24 Jul 2026 10:57:56 +0800 Subject: [PATCH 01/21] Unify the ABI for JavaScript imports and exports --- .github/workflows/ci.yaml | 8 + client/Cargo.toml | 2 +- client/e2e/Cargo.toml | 12 + client/e2e/examples/primitive.rs | 236 +++++++ client/e2e/examples/string.rs | 99 +++ client/e2e/src/lib.rs | 1 + client/js-bindgen/src/lib.rs | 2 +- client/js-sys/src/array/array.gen.rs | 386 +++++----- client/js-sys/src/array/mod.rs | 131 ++-- client/js-sys/src/bigint/bigint.gen.rs | 28 +- client/js-sys/src/exception.rs | 46 ++ client/js-sys/src/externref.rs | 28 +- client/js-sys/src/hazard.rs | 548 +++++++++++++-- client/js-sys/src/interop/mod.rs | 2 + client/js-sys/src/interop/primitive.rs | 665 ++++++++++++++++++ client/js-sys/src/interop/string.rs | 52 ++ client/js-sys/src/lib.rs | 5 +- client/js-sys/src/macro.rs | 427 ++--------- client/js-sys/src/macro/abi.rs | 388 ++++++++++ client/js-sys/src/macro/export.rs | 383 ++++++++++ client/js-sys/src/macro/js_import.rs | 232 ++++++ client/js-sys/src/macro/result.rs | 193 +++++ client/js-sys/src/macro/text.rs | 263 +++++++ client/js-sys/src/macro/wat.rs | 132 ++++ client/js-sys/src/macro/wat_import.rs | 220 ++++++ client/js-sys/src/number/number.gen.rs | 31 +- client/js-sys/src/numeric.rs | 234 ------ client/js-sys/src/string/mod.rs | 87 +-- client/js-sys/src/string/string.gen.rs | 306 ++++---- client/js-sys/src/util.rs | 207 +++--- client/js-sys/src/value/mod.rs | 207 +++++- client/js-sys/src/value/value.gen.rs | 51 +- client/js-sys/tests/hazard.rs | 118 ++++ client/js-sys/tests/numeric.rs | 10 + client/js-sys/tests/optional.rs | 124 ++++ client/web-sys/src/console.gen.rs | 122 ++-- host/Cargo.toml | 1 + host/cli-lib/Cargo.toml | 1 - host/cli-lib/src/js/imports.d.mts | 8 +- host/cli-lib/src/js/imports.mjs | 11 +- host/cli-lib/src/js/imports.mts | 26 +- host/cli-lib/src/lib.rs | 70 +- host/cli/Cargo.toml | 25 + host/cli/src/main.rs | 145 ++++ host/dev/Cargo.toml | 1 + host/dev/src/client/e2e.rs | 220 ++++++ host/dev/src/client/metadata.rs | 8 +- host/dev/src/client/mod.rs | 14 +- host/dev/src/client/test.rs | 24 +- host/js-sys-bindgen/src/export.rs | 238 +++++++ host/js-sys-bindgen/src/function.rs | 373 ++++------ host/js-sys-bindgen/src/hygiene.rs | 84 +-- host/js-sys-bindgen/src/lib.rs | 2 + host/js-sys-bindgen/src/macro.rs | 38 +- host/js-sys-bindgen/src/tests/macro/export.rs | 251 +++++++ .../src/tests/macro/function.rs | 327 +++++---- host/js-sys-bindgen/src/tests/macro/member.rs | 244 +++---- host/js-sys-bindgen/src/tests/macro/mod.rs | 24 +- host/js-sys-bindgen/src/tests/macro/type.rs | 145 ++-- host/js-sys-bindgen/src/tests/type.rs | 61 +- host/js-sys-bindgen/src/tests/web_idl.rs | 28 +- host/js-sys-bindgen/src/type.rs | 51 +- host/ld/src/js.rs | 37 + host/ld/src/post.rs | 20 +- host/ld/src/pre.rs | 6 + host/macro/src/lib.rs | 15 + 66 files changed, 6204 insertions(+), 2280 deletions(-) create mode 100644 client/e2e/Cargo.toml create mode 100644 client/e2e/examples/primitive.rs create mode 100644 client/e2e/examples/string.rs create mode 100644 client/e2e/src/lib.rs create mode 100644 client/js-sys/src/exception.rs create mode 100644 client/js-sys/src/interop/mod.rs create mode 100644 client/js-sys/src/interop/primitive.rs create mode 100644 client/js-sys/src/interop/string.rs create mode 100644 client/js-sys/src/macro/abi.rs create mode 100644 client/js-sys/src/macro/export.rs create mode 100644 client/js-sys/src/macro/js_import.rs create mode 100644 client/js-sys/src/macro/result.rs create mode 100644 client/js-sys/src/macro/text.rs create mode 100644 client/js-sys/src/macro/wat.rs create mode 100644 client/js-sys/src/macro/wat_import.rs delete mode 100644 client/js-sys/src/numeric.rs create mode 100644 client/js-sys/tests/hazard.rs create mode 100644 client/js-sys/tests/optional.rs create mode 100644 host/cli/Cargo.toml create mode 100644 host/cli/src/main.rs create mode 100644 host/dev/src/client/e2e.rs create mode 100644 host/js-sys-bindgen/src/export.rs create mode 100644 host/js-sys-bindgen/src/tests/macro/export.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54173e2a..b280e532 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,6 +93,11 @@ jobs: run: | cargo ${{ matrix.os.sub-command }} build -p js-bindgen-ld ${{ matrix.os.args }} --release cp target/${{ matrix.os.path }}release/js-bindgen-ld${{ matrix.os.exe }} ${{ runner.temp }}/host-tools/ + - name: Build `js-bindgen` + working-directory: host + run: | + cargo ${{ matrix.os.sub-command }} build -p js-bindgen-cli ${{ matrix.os.args }} --release + cp target/${{ matrix.os.path }}release/js-bindgen${{ matrix.os.exe }} ${{ runner.temp }}/host-tools/ - name: Build `js-bindgen-runner` working-directory: host run: | @@ -139,6 +144,7 @@ jobs: target-feature: - { name: default } - { name: atomics, rust: nightly, components: -c rust-src, description: Atomics } + - { name: exception-handling, rust: nightly, description: Exception Handling } env: JBG_DEV_TOOLS: 1 @@ -204,6 +210,7 @@ jobs: target-feature: - { name: default } - { name: atomics, rust: nightly, components: -c rust-src, description: Atomics } + - { name: exception-handling, rust: nightly, description: Exception Handling } env: JBG_DEV_TOOLS: 1 @@ -276,6 +283,7 @@ jobs: target-feature: - { name: default } - { name: atomics, rust: nightly, components: -c rust-src, description: " Atomics" } + - { name: exception-handling, rust: nightly, description: " Exception Handling" } exclude: - runner: { name: mac-os } target: { name: wasm64 } diff --git a/client/Cargo.toml b/client/Cargo.toml index 408a38b1..d122cba3 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,7 +6,7 @@ publish = false [workspace] resolver = "3" -members = ["js-bindgen", "js-sys", "test", "web-sys"] +members = ["e2e", "js-bindgen", "js-sys", "test", "web-sys"] [workspace.package] edition = "2024" diff --git a/client/e2e/Cargo.toml b/client/e2e/Cargo.toml new file mode 100644 index 00000000..5847a46d --- /dev/null +++ b/client/e2e/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "js-bindgen-e2e" +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +publish = false + +[dev-dependencies] +js-sys = { workspace = true, features = ["macro"] } + +[lints] +workspace = true diff --git a/client/e2e/examples/primitive.rs b/client/e2e/examples/primitive.rs new file mode 100644 index 00000000..cc2e7e95 --- /dev/null +++ b/client/e2e/examples/primitive.rs @@ -0,0 +1,236 @@ +#[rustfmt::skip] +fn main() { + // ;; exports["not_bool"](false) === true + // ;; exports["not_bool"](true) === false + // ;; exports["add_i32"](-2_147_483_648, 1) === -2_147_483_647 + // ;; exports["add_u32"](0xffff_fffe, 1) === 0xffff_ffff + // ;; exports["add_f32"](Math.fround(1 / 3), 0) === Math.fround(1 / 3) + // ;; exports["add_f64"](Number.MAX_SAFE_INTEGER, 1) === 9_007_199_254_740_992 + // ;; exports["add_i64"](-(1n << 63n), 1n) === -(1n << 63n) + 1n + // ;; exports["add_u64"](0xffff_ffff_ffff_fffen, 1n) === 0xffff_ffff_ffff_ffffn + // ;; (() => { const value = exports["isize_min"](); const one = typeof value === "bigint" ? 1n : 1; return exports["add_isize"](value, one) === value + one })() + // ;; exports["add_u128"](1n << 96n, 3n) === (1n << 96n) + 3n + // ;; exports["add_i128"](-(1n << 96n), -3n) === -(1n << 96n) - 3n + // ;; exports["add_i32_ref"](-2_147_483_648, 1) === -2_147_483_647 + // ;; exports["add_u128_ref"](1n << 96n, 3n) === (1n << 96n) + 3n + // ;; exports["not_bool_ref"](false) === true + // ;; (() => { const value = exports["usize_max"](); return value === (typeof value === "bigint" ? 0xffff_ffff_ffff_ffffn : 0xffff_ffff) })() + // ;; exports["option_bool"](undefined) === undefined + // ;; exports["option_bool"](false) === true + // ;; exports["option_i16"](undefined) === undefined + // ;; exports["option_i16"](-32_768) === -32_767 + // ;; exports["option_u32"](undefined) === undefined + // ;; exports["option_u32"](0xffff_fffe) === 0xffff_ffff + // ;; exports["option_f32"](undefined) === undefined + // ;; Number.isNaN(exports["option_f32"](NaN)) + // ;; exports["option_f64"](undefined) === undefined + // ;; exports["option_f64"](Number.MAX_VALUE) === Number.MAX_VALUE + // ;; exports["option_i64"](undefined) === undefined + // ;; exports["option_i64"](-(1n << 63n)) === -(1n << 63n) + 1n + // ;; exports["option_u64"](undefined) === undefined + // ;; exports["option_u64"](0xffff_ffff_ffff_fffen) === 0xffff_ffff_ffff_ffffn + // ;; (() => { const value = exports["isize_min"](); const one = typeof value === "bigint" ? 1n : 1; return exports["option_isize"](value) === value + one })() + // ;; exports["option_isize"](undefined) === undefined + // ;; (() => { const value = exports["usize_max"](); return exports["option_usize"](value) === value })() + // ;; exports["option_usize"](undefined) === undefined + // ;; exports["option_u128"](undefined) === undefined + // ;; exports["option_u128"]((1n << 128n) - 2n) === (1n << 128n) - 1n + // ;; exports["option_i128"](undefined) === undefined + // ;; exports["option_i128"](-(1n << 127n)) === -(1n << 127n) + 1n + // ;; exports["checked_add_u128"](1n << 96n, 3n) === (1n << 96n) + 3n + // ;; (() => { try { exports["checked_add_u128"]((1n << 128n) - 1n, 1n); return false } catch (error) { return error === "overflow" } })() + // ;; exports["import_result_i64"](41n) === 42n + // ;; (() => { try { exports["import_result_i64"](-1n); return false } catch (error) { return error === "i64 error" } })() + // ;; (() => { try { exports["import_result_i64"](-2n); return false } catch (error) { return error === undefined } })() + // ;; (() => { try { exports["import_result_i64"](-3n); return false } catch (error) { return error === null } })() + // ;; exports["import_result_u128"](1n << 96n) === (1n << 96n) + 1n + // ;; (() => { try { exports["import_result_u128"]((1n << 128n) - 1n); return false } catch (error) { return error === "u128 error" } })() +} + +use js_sys::{JsString, JsValue, js_sys}; + +type JsResult = Result; + +js_sys::js_bindgen::embed_js!( + module = "primitive", + name = "result.i64", + "(value) => {{", + " if (value === -2n) throw undefined", + " if (value === -3n) throw null", + " if (value < 0n) throw 'i64 error'", + " return value + 1n", + "}}", +); + +js_sys::js_bindgen::embed_js!( + module = "primitive", + name = "result.u128", + "(value) => {{", + " if (value === (1n << 128n) - 1n) throw 'u128 error'", + " return value + 1n", + "}}", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "result.i64")] + fn import_result_i64_raw(value: i64) -> Result; + + #[js_sys(js_embed = "result.u128")] + fn import_result_u128_raw(value: u128) -> Result; +} + +#[js_sys] +fn not_bool(value: bool) -> bool { + !value +} + +#[js_sys] +fn add_i32(value: i32, delta: i32) -> i32 { + value + delta +} + +#[js_sys] +fn add_u32(value: u32, delta: u32) -> u32 { + value + delta +} + +#[js_sys] +fn add_f32(value: f32, delta: f32) -> f32 { + value + delta +} + +#[js_sys] +fn add_f64(value: f64, delta: f64) -> f64 { + value + delta +} + +#[js_sys] +fn add_i64(value: i64, delta: i64) -> i64 { + value + delta +} + +#[js_sys] +fn add_u64(value: u64, delta: u64) -> u64 { + value + delta +} + +#[js_sys] +fn add_isize(value: isize, delta: isize) -> isize { + value + delta +} + +#[js_sys] +fn isize_min() -> isize { + isize::MIN +} + +#[js_sys] +fn add_u128(value: u128, delta: u128) -> u128 { + value + delta +} + +#[js_sys] +fn add_i128(value: i128, delta: i128) -> i128 { + value + delta +} + +#[expect( + clippy::trivially_copy_pass_by_ref, + reason = "tests reference ABI conversion" +)] +#[js_sys] +fn add_i32_ref(value: &i32, delta: &i32) -> i32 { + *value + *delta +} + +#[js_sys] +fn add_u128_ref(value: &u128, delta: &u128) -> u128 { + *value + *delta +} + +#[expect( + clippy::trivially_copy_pass_by_ref, + reason = "tests reference ABI conversion" +)] +#[js_sys] +fn not_bool_ref(value: &bool) -> bool { + !*value +} + +#[js_sys] +fn usize_max() -> usize { + usize::MAX +} + +#[js_sys] +fn option_bool(value: Option) -> Option { + value.map(|value| !value) +} + +#[js_sys] +fn option_i16(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn option_u32(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn option_f32(value: Option) -> Option { + value +} + +#[js_sys] +fn option_f64(value: Option) -> Option { + value +} + +#[js_sys] +fn option_i64(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn option_u64(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn option_isize(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn option_usize(value: Option) -> Option { + value +} + +#[js_sys] +fn option_u128(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn option_i128(value: Option) -> Option { + value.map(|value| value + 1) +} + +#[js_sys] +fn checked_add_u128(value: u128, delta: u128) -> JsResult { + value + .checked_add(delta) + .ok_or_else(|| JsString::from("overflow")) +} + +#[js_sys] +fn import_result_i64(value: i64) -> Result { + import_result_i64_raw(value) +} + +#[js_sys] +fn import_result_u128(value: u128) -> Result { + import_result_u128_raw(value) +} diff --git a/client/e2e/examples/string.rs b/client/e2e/examples/string.rs new file mode 100644 index 00000000..ff80cdfa --- /dev/null +++ b/client/e2e/examples/string.rs @@ -0,0 +1,99 @@ +#[rustfmt::skip] +fn main() { + // ;; exports["rust_string"]() + // ;; exports["rust_js_string"]() === "Hello from Rust! 🦀" + // ;; exports["identity"]("Hello from JavaScript! 🦀") === "Hello from JavaScript! 🦀" + // ;; exports["borrowed_js_string"]("borrowed") === true + // ;; exports["borrowed_js_value"]("value") === true + // ;; exports["borrowed_js_array"]([1, 2, 3]) === 3 + // ;; exports["optional_js_string"](false) === undefined + // ;; exports["optional_js_string"](true) === "optional" + // ;; exports["roundtrip"]("", "") + // ;; exports["roundtrip"]("Hello, World!", "Hello, World!") + // ;; exports["roundtrip"]("你好,世界!🦀", "你好,世界!🦀") + // ;; exports["roundtrip"]("a\0b", "a\0b") + // ;; exports["roundtrip"]("\ud800", "\ufffd") + // ;; (() => { const value = "js-bindgen 🦀 ".repeat(8_192); return exports["roundtrip"](value, value) })() + // ;; exports["result_js_string"](true) === "ok" + // ;; (() => { try { exports["result_js_string"](false); return false } catch (error) { return error === "error" } })() + // ;; exports["import_result_js_string"]("ok") === "ok!" + // ;; (() => { try { exports["import_result_js_string"]("error"); return false } catch (error) { return error === "string error" } })() +} + +use js_sys::{JsArray, JsString, JsValue, js_sys}; + +js_sys::js_bindgen::embed_js!( + module = "string", + name = "result.js_string", + "(value) => {{", + " if (value === 'error') throw 'string error'", + " return `${{value}}!`", + "}}", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "result.js_string")] + fn import_result_js_string_raw(value: JsString) -> Result; +} + +#[expect(clippy::cmp_owned, reason = "checked")] +#[js_sys] +fn rust_string() -> bool { + JsString::from("Hello from Rust! 🦀") == "Hello from Rust! 🦀" +} + +#[js_sys] +fn rust_js_string() -> JsString { + JsString::from("Hello from Rust! 🦀") +} + +#[js_sys] +fn identity(value: JsString) -> JsString { + value +} + +#[js_sys] +fn borrowed_js_string(value: &JsString) -> bool { + value.eq(&"borrowed") +} + +#[js_sys] +fn borrowed_js_value(value: &JsValue) -> bool { + let expected = JsString::from("value"); + value == expected.as_ref() +} + +#[js_sys] +fn borrowed_js_array(value: &JsArray) -> u32 { + value.length() +} + +#[js_sys] +fn optional_js_string(some: bool) -> Option { + some.then(|| JsString::from("optional")) +} + +#[expect(clippy::cmp_owned, reason = "checked")] +#[js_sys] +fn roundtrip(value: JsString, expected: JsString) -> bool { + let rust_value = String::from(&value); + let rust_expected = String::from(&expected); + drop(value); + drop(expected); + JsString::from(rust_value.as_str()) == rust_expected +} + +#[js_sys] +fn result_js_string(ok: bool) -> Result { + if ok { + Ok(JsString::from("ok")) + } else { + Err(JsString::from("error")) + } +} + +#[js_sys] +fn import_result_js_string(value: JsString) -> Result { + import_result_js_string_raw(value) +} diff --git a/client/e2e/src/lib.rs b/client/e2e/src/lib.rs new file mode 100644 index 00000000..0c9ac1ac --- /dev/null +++ b/client/e2e/src/lib.rs @@ -0,0 +1 @@ +#![no_std] diff --git a/client/js-bindgen/src/lib.rs b/client/js-bindgen/src/lib.rs index 8476fafa..26844d77 100644 --- a/client/js-bindgen/src/lib.rs +++ b/client/js-bindgen/src/lib.rs @@ -14,4 +14,4 @@ unsafe extern "C" {} #[doc(hidden)] pub mod r#macro; -pub use js_bindgen_macro::{embed_js, import_js, unsafe_global_wat}; +pub use js_bindgen_macro::{embed_js, export_js, import_js, unsafe_global_wat}; diff --git a/client/js-sys/src/array/array.gen.rs b/client/js-sys/src/array/array.gen.rs index c36a2307..067c81c0 100644 --- a/client/js-sys/src/array/array.gen.rs +++ b/client/js-sys/src/array/array.gen.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{InputJsConv, OutputJsConv, OutputWatConv, Input, InputWatConv, Output, JsCast}; +use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; use crate::util::{PtrConst, PtrLength, PtrMut}; #[repr(transparent)] @@ -25,46 +25,29 @@ impl From> for JsValue { } } -unsafe impl Input for &JsArray { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; +unsafe impl JsCast for JsArray {} - type Type = <&'static JsValue as Input>::Type; +unsafe impl IntoJS for JsArray { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.value) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } -unsafe impl JsCast for JsArray {} - -unsafe impl Output for JsArray { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; +unsafe impl OptionIntoJS for JsArray { + type OptionAbi = ::OptionAbi; - type Type = ::Type; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: Output::from_raw(raw), - _type: PhantomData, - } + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } impl JsArray { pub fn length(self: &JsArray) -> u32 { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"length\" (func $js_sys.import.length (@sym (name \"js_sys.import.length\")) (param {}) (result {}))){}", - "(func $js_sys.length (@sym) (param {}) (param $self {}) (result {})", - " local.get $self{}", " call $js_sys.import.length (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < & JsValue > (), interpolate - r#macro::wat_output_import_type:: < u32 > (), interpolate r#macro::wat_imports!((& - JsValue), u32), interpolate r#macro::wat_indirect!(u32), interpolate < & JsValue as - Input > ::WAT_TYPE, interpolate r#macro::wat_direct:: < u32 > (), interpolate - r#macro::wat_input!(& JsValue), interpolate r#macro::wat_output!(u32), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "length", adapter = + "js_sys.length", inputs = [("arg0", & JsValue)], output = u32,), } js_bindgen::import_js! { @@ -73,25 +56,32 @@ impl JsArray { required_embeds = [ r#macro::js_input_embed::<&JsValue>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}", - interpolate r#macro::js_select!("(self) => ", "(self) => {\n", (&JsValue), u32), - interpolate r#macro::js_parameter!("self", &JsValue), - interpolate r#macro::js_output!( - "\treturn ", - "self.length", - "self.length", - u32, - &JsValue, + "{}", + interpolate r#macro::js_import!( + direct_open = r#macro::js_function!("(", ") => ", ("arg0", & JsValue)), direct_call + = "arg0_0.length", indirect_call = "arg0_0.length", inputs = [("arg0", & JsValue)], + output = u32, ), } unsafe extern "C" { #[link_name = "js_sys.length"] - fn length(this: <&JsValue as Input>::Type) -> ::Type; + fn length( + arg0_0: r#macro::InputSlot1<&JsValue>, + arg0_1: r#macro::InputSlot2<&JsValue>, + arg0_2: r#macro::InputSlot3<&JsValue>, + arg0_3: r#macro::InputSlot4<&JsValue>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { length(Input::into_raw(self)) }) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { + r#macro::split_input_as::<&JsValue>(self) + }; + unsafe { length(arg0_0, arg0_1, arg0_2, arg0_3) } + }) } } @@ -100,19 +90,9 @@ pub(super) unsafe fn array_js_value_decode( len: PtrLength, ) -> JsArray { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"array_js_value_decode\" (func $js_sys.import.array_js_value_decode (@sym (name \"js_sys.import.array_js_value_decode\")) (param {} {}) (result {}))){}", - "(func $js_sys.array_js_value_decode (@sym) (param {}) (param $array {}) (param $len {}) (result {})", - " local.get $array{}", " local.get $len{}", - " call $js_sys.import.array_js_value_decode (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < PtrConst < JsValue > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < JsValue > > (), interpolate - r#macro::wat_output_import_type:: < JsArray < JsValue > > (), interpolate - r#macro::wat_imports!((PtrConst < JsValue >, PtrLength < JsValue >), JsArray < JsValue >), - interpolate r#macro::wat_indirect!(JsArray < JsValue >), interpolate < PtrConst < JsValue > - as Input > ::WAT_TYPE, interpolate < PtrLength < JsValue > as Input > ::WAT_TYPE, - interpolate r#macro::wat_direct:: < JsArray < JsValue > > (), interpolate - r#macro::wat_input!(PtrConst < JsValue >), interpolate r#macro::wat_input!(PtrLength < - JsValue >), interpolate r#macro::wat_output!(JsArray < JsValue >), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_js_value_decode", + adapter = "js_sys.array_js_value_decode", inputs = [("arg0", PtrConst < JsValue >), ("arg1", + PtrLength < JsValue >)], output = JsArray < JsValue >,), } js_bindgen::import_js! { @@ -123,35 +103,38 @@ pub(super) unsafe fn array_js_value_decode( r#macro::js_input_embed::>(), r#macro::js_input_embed::>(), r#macro::js_output_embed::>(), + r#macro::js_result_embed::>(), ], - "{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(array, len) => {\n", - (PtrConst, PtrLength), - JsArray, - ), - interpolate r#macro::js_parameter!("array", PtrConst), - interpolate r#macro::js_parameter!("len", PtrLength), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['array.js_value.decode']", - "this.#jsEmbed.js_sys['array.js_value.decode'](array, len)", - JsArray, - PtrConst, - PtrLength, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['array.js_value.decode']", + indirect_call = "this.#jsEmbed.js_sys['array.js_value.decode'](arg0_0, arg1_0)", inputs + = [("arg0", PtrConst < JsValue >), ("arg1", PtrLength < JsValue >)], output = JsArray < + JsValue >, ), } unsafe extern "C" { #[link_name = "js_sys.array_js_value_decode"] fn array_js_value_decode( - array: as Input>::Type, - len: as Input>::Type, - ) -> as Output>::Type; + arg0_0: r#macro::InputSlot1>, + arg0_1: r#macro::InputSlot2>, + arg0_2: r#macro::InputSlot3>, + arg0_3: r#macro::InputSlot4>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + ) -> r#macro::OutputRet>; } - Output::from_raw(unsafe { array_js_value_decode(Input::into_raw(array), Input::into_raw(len)) }) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::>(array); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(len); + unsafe { + array_js_value_decode(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) + } + }) } pub(super) unsafe fn array_js_value_encode( @@ -162,25 +145,10 @@ pub(super) unsafe fn array_js_value_encode( externref_len: i32, ) -> bool { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"array_js_value_encode\" (func $js_sys.import.array_js_value_encode (@sym (name \"js_sys.import.array_js_value_encode\")) (param {} {} {} {} {}) (result {}))){}", - "(func $js_sys.array_js_value_encode (@sym) (param {}) (param $array {}) (param $array_ptr {}) (param $array_len {}) (param $externref_ptr {}) (param $externref_len {}) (result {})", - " local.get $array{}", " local.get $array_ptr{}", " local.get $array_len{}", - " local.get $externref_ptr{}", " local.get $externref_len{}", - " call $js_sys.import.array_js_value_encode (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < & JsArray > (), interpolate - r#macro::wat_input_import_type:: < PtrMut < JsValue > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < JsValue > > (), interpolate - r#macro::wat_input_import_type:: < PtrConst < i32 > > (), interpolate - r#macro::wat_input_import_type:: < i32 > (), interpolate r#macro::wat_output_import_type:: < - bool > (), interpolate r#macro::wat_imports!((& JsArray, PtrMut < JsValue >, PtrLength < - JsValue >, PtrConst < i32 >, i32), bool), interpolate r#macro::wat_indirect!(bool), - interpolate < & JsArray as Input > ::WAT_TYPE, interpolate < PtrMut < JsValue > as Input > - ::WAT_TYPE, interpolate < PtrLength < JsValue > as Input > ::WAT_TYPE, interpolate < - PtrConst < i32 > as Input > ::WAT_TYPE, interpolate < i32 as Input > ::WAT_TYPE, interpolate - r#macro::wat_direct:: < bool > (), interpolate r#macro::wat_input!(& JsArray), interpolate - r#macro::wat_input!(PtrMut < JsValue >), interpolate r#macro::wat_input!(PtrLength < JsValue - >), interpolate r#macro::wat_input!(PtrConst < i32 >), interpolate r#macro::wat_input!(i32), - interpolate r#macro::wat_output!(bool), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_js_value_encode", + adapter = "js_sys.array_js_value_encode", inputs = [("arg0", & JsArray), ("arg1", PtrMut < + JsValue >), ("arg2", PtrLength < JsValue >), ("arg3", PtrConst < i32 >), ("arg4", i32)], + output = bool,), } js_bindgen::import_js! { @@ -194,69 +162,84 @@ pub(super) unsafe fn array_js_value_encode( r#macro::js_input_embed::>(), r#macro::js_input_embed::(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(array, array_ptr, array_len, externref_ptr, externref_len) => {\n", - (&JsArray, PtrMut, PtrLength, PtrConst, i32), - bool, - ), - interpolate r#macro::js_parameter!("array", &JsArray), - interpolate r#macro::js_parameter!("array_ptr", PtrMut), - interpolate r#macro::js_parameter!("array_len", PtrLength), - interpolate r#macro::js_parameter!("externref_ptr", PtrConst), - interpolate r#macro::js_parameter!("externref_len", i32), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['array.js_value.encode']", - "this.#jsEmbed.js_sys['array.js_value.encode'](array, array_ptr, array_len, externref_ptr, externref_len)", - bool, - &JsArray, - PtrMut, - PtrLength, - PtrConst, - i32, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['array.js_value.encode']", + indirect_call = + "this.#jsEmbed.js_sys['array.js_value.encode'](arg0_0, arg1_0, arg2_0, arg3_0, arg4_0)", + inputs = [("arg0", & JsArray), ("arg1", PtrMut < JsValue >), ("arg2", PtrLength < + JsValue >), ("arg3", PtrConst < i32 >), ("arg4", i32)], output = bool, ), } unsafe extern "C" { #[link_name = "js_sys.array_js_value_encode"] fn array_js_value_encode( - array: <&JsArray as Input>::Type, - array_ptr: as Input>::Type, - array_len: as Input>::Type, - externref_ptr: as Input>::Type, - externref_len: ::Type, - ) -> ::Type; + arg0_0: r#macro::InputSlot1<&JsArray>, + arg0_1: r#macro::InputSlot2<&JsArray>, + arg0_2: r#macro::InputSlot3<&JsArray>, + arg0_3: r#macro::InputSlot4<&JsArray>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + arg2_0: r#macro::InputSlot1>, + arg2_1: r#macro::InputSlot2>, + arg2_2: r#macro::InputSlot3>, + arg2_3: r#macro::InputSlot4>, + arg3_0: r#macro::InputSlot1>, + arg3_1: r#macro::InputSlot2>, + arg3_2: r#macro::InputSlot3>, + arg3_3: r#macro::InputSlot4>, + arg4_0: r#macro::InputSlot1, + arg4_1: r#macro::InputSlot2, + arg4_2: r#macro::InputSlot3, + arg4_3: r#macro::InputSlot4, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { - array_js_value_encode( - Input::into_raw(array), - Input::into_raw(array_ptr), - Input::into_raw(array_len), - Input::into_raw(externref_ptr), - Input::into_raw(externref_len), - ) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsArray>(array); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(array_ptr); + let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::< + PtrLength, + >(array_len); + let (arg3_0, arg3_1, arg3_2, arg3_3) = r#macro::split_input::>(externref_ptr); + let (arg4_0, arg4_1, arg4_2, arg4_3) = r#macro::split_input::(externref_len); + unsafe { + array_js_value_encode( + arg0_0, + arg0_1, + arg0_2, + arg0_3, + arg1_0, + arg1_1, + arg1_2, + arg1_3, + arg2_0, + arg2_1, + arg2_2, + arg2_3, + arg3_0, + arg3_1, + arg3_2, + arg3_3, + arg4_0, + arg4_1, + arg4_2, + arg4_3, + ) + } }) } pub(super) unsafe fn array_u32_decode(array: PtrConst, len: PtrLength) -> JsArray { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"array_u32_decode\" (func $js_sys.import.array_u32_decode (@sym (name \"js_sys.import.array_u32_decode\")) (param {} {}) (result {}))){}", - "(func $js_sys.array_u32_decode (@sym) (param {}) (param $array {}) (param $len {}) (result {})", - " local.get $array{}", " local.get $len{}", - " call $js_sys.import.array_u32_decode (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < PtrConst < u32 > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < u32 > > (), interpolate - r#macro::wat_output_import_type:: < JsArray < u32 > > (), interpolate - r#macro::wat_imports!((PtrConst < u32 >, PtrLength < u32 >), JsArray < u32 >), interpolate - r#macro::wat_indirect!(JsArray < u32 >), interpolate < PtrConst < u32 > as Input > - ::WAT_TYPE, interpolate < PtrLength < u32 > as Input > ::WAT_TYPE, interpolate - r#macro::wat_direct:: < JsArray < u32 > > (), interpolate r#macro::wat_input!(PtrConst < u32 - >), interpolate r#macro::wat_input!(PtrLength < u32 >), interpolate - r#macro::wat_output!(JsArray < u32 >), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_decode", + adapter = "js_sys.array_u32_decode", inputs = [("arg0", PtrConst < u32 >), ("arg1", + PtrLength < u32 >)], output = JsArray < u32 >,), } js_bindgen::import_js! { @@ -267,35 +250,35 @@ pub(super) unsafe fn array_u32_decode(array: PtrConst, len: PtrLength) r#macro::js_input_embed::>(), r#macro::js_input_embed::>(), r#macro::js_output_embed::>(), + r#macro::js_result_embed::>(), ], - "{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(array, len) => {\n", - (PtrConst, PtrLength), - JsArray, - ), - interpolate r#macro::js_parameter!("array", PtrConst), - interpolate r#macro::js_parameter!("len", PtrLength), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['view.getUint32']", - "this.#jsEmbed.js_sys['view.getUint32'](array, len)", - JsArray, - PtrConst, - PtrLength, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['view.getUint32']", indirect_call + = "this.#jsEmbed.js_sys['view.getUint32'](arg0_0, arg1_0)", inputs = [("arg0", PtrConst + < u32 >), ("arg1", PtrLength < u32 >)], output = JsArray < u32 >, ), } unsafe extern "C" { #[link_name = "js_sys.array_u32_decode"] fn array_u32_decode( - array: as Input>::Type, - len: as Input>::Type, - ) -> as Output>::Type; + arg0_0: r#macro::InputSlot1>, + arg0_1: r#macro::InputSlot2>, + arg0_2: r#macro::InputSlot3>, + arg0_3: r#macro::InputSlot4>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + ) -> r#macro::OutputRet>; } - Output::from_raw(unsafe { array_u32_decode(Input::into_raw(array), Input::into_raw(len)) }) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::>(array); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(len); + unsafe { array_u32_decode(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } + }) } pub(super) unsafe fn array_u32_encode( @@ -304,20 +287,9 @@ pub(super) unsafe fn array_u32_encode( len: PtrLength, ) -> bool { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"array_u32_encode\" (func $js_sys.import.array_u32_encode (@sym (name \"js_sys.import.array_u32_encode\")) (param {} {} {}) (result {}))){}", - "(func $js_sys.array_u32_encode (@sym) (param {}) (param $array {}) (param $ptr {}) (param $len {}) (result {})", - " local.get $array{}", " local.get $ptr{}", " local.get $len{}", - " call $js_sys.import.array_u32_encode (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < & JsArray < u32 > > (), interpolate - r#macro::wat_input_import_type:: < PtrMut < u32 > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < u32 > > (), interpolate - r#macro::wat_output_import_type:: < bool > (), interpolate r#macro::wat_imports!((& JsArray - < u32 >, PtrMut < u32 >, PtrLength < u32 >), bool), interpolate - r#macro::wat_indirect!(bool), interpolate < & JsArray < u32 > as Input > ::WAT_TYPE, - interpolate < PtrMut < u32 > as Input > ::WAT_TYPE, interpolate < PtrLength < u32 > as Input - > ::WAT_TYPE, interpolate r#macro::wat_direct:: < bool > (), interpolate - r#macro::wat_input!(& JsArray < u32 >), interpolate r#macro::wat_input!(PtrMut < u32 >), - interpolate r#macro::wat_input!(PtrLength < u32 >), interpolate r#macro::wat_output!(bool), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_encode", + adapter = "js_sys.array_u32_encode", inputs = [("arg0", & JsArray < u32 >), ("arg1", PtrMut + < u32 >), ("arg2", PtrLength < u32 >)], output = bool,), } js_bindgen::import_js! { @@ -329,38 +301,54 @@ pub(super) unsafe fn array_u32_encode( r#macro::js_input_embed::>(), r#macro::js_input_embed::>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(array, ptr, len) => {\n", - (&JsArray, PtrMut, PtrLength), - bool, - ), - interpolate r#macro::js_parameter!("array", &JsArray), - interpolate r#macro::js_parameter!("ptr", PtrMut), - interpolate r#macro::js_parameter!("len", PtrLength), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['array.u32.encode']", - "this.#jsEmbed.js_sys['array.u32.encode'](array, ptr, len)", - bool, - &JsArray, - PtrMut, - PtrLength, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['array.u32.encode']", + indirect_call = "this.#jsEmbed.js_sys['array.u32.encode'](arg0_0, arg1_0, arg2_0)", + inputs = [("arg0", & JsArray < u32 >), ("arg1", PtrMut < u32 >), ("arg2", PtrLength < + u32 >)], output = bool, ), } unsafe extern "C" { #[link_name = "js_sys.array_u32_encode"] fn array_u32_encode( - array: <&JsArray as Input>::Type, - ptr: as Input>::Type, - len: as Input>::Type, - ) -> ::Type; + arg0_0: r#macro::InputSlot1<&JsArray>, + arg0_1: r#macro::InputSlot2<&JsArray>, + arg0_2: r#macro::InputSlot3<&JsArray>, + arg0_3: r#macro::InputSlot4<&JsArray>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + arg2_0: r#macro::InputSlot1>, + arg2_1: r#macro::InputSlot2>, + arg2_2: r#macro::InputSlot3>, + arg2_3: r#macro::InputSlot4>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { - array_u32_encode(Input::into_raw(array), Input::into_raw(ptr), Input::into_raw(len)) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsArray>(array); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(ptr); + let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::>(len); + unsafe { + array_u32_encode( + arg0_0, + arg0_1, + arg0_2, + arg0_3, + arg1_0, + arg1_1, + arg1_2, + arg1_3, + arg2_0, + arg2_1, + arg2_2, + arg2_3, + ) + } }) } diff --git a/client/js-sys/src/array/mod.rs b/client/js-sys/src/array/mod.rs index c3862d45..de2e1255 100644 --- a/client/js-sys/src/array/mod.rs +++ b/client/js-sys/src/array/mod.rs @@ -10,7 +10,7 @@ use core::ptr; pub use self::array::JsArray; use crate::JsValue; use crate::externref::ExternrefTable; -use crate::hazard::{Input, InputJsConv, InputWatConv, JsCast}; +use crate::hazard::{IntoJS, IntoJsConv, JsCast}; use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; impl JsArray { @@ -34,19 +34,18 @@ where } } -// SAFETY: Implementation. -unsafe impl<'a, T, const N: usize> Input for &'a [T; N] +// SAFETY: The array delegates to the slice implementation with the same +// element representation. +unsafe impl<'a, T, const N: usize> IntoJS for &'a [T; N] where - &'a [T]: Input, + &'a [T]: IntoJS, { - const WAT_TYPE: &'static str = <&[T] as Input>::WAT_TYPE; - const WAT_CONV: Option = <&[T] as Input>::WAT_CONV; - const JS_CONV: Option = <&[T] as Input>::JS_CONV; + const JS_CONV: Option = <&[T] as IntoJS>::JS_CONV; - type Type = <&'a [T] as Input>::Type; + type Abi = <&'a [T] as IntoJS>::Abi; - fn into_raw(self) -> Self::Type { - self.as_slice().into_raw() + fn into_abi(self) -> Self::Abi { + self.as_slice().into_abi() } } @@ -150,17 +149,23 @@ js_bindgen::embed_js!( "", // Default value helps browsers to optimize. " let tableIndex = 0", - " if (arrLen > refLen) {{", - " tableIndex = table.grow(arrLen - refLen)", + " const reused = Math.min(arrLen, refLen)", + " const refIndices = this.#jsEmbed.js_sys['view.getInt32'](", + " refPtr + (refLen - reused) * 4,", + " reused,", + " )", + " const elemIndices = new Array(arrLen)", + " if (arrLen > reused) {{", + " tableIndex = table.grow(arrLen - reused)", " }}", "", - " let refIndex = refLen - 1", + " let refIndex = reused - 1", "", " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", " let elemIndex", "", " if (refIndex >= 0) {{", - " elemIndex = this.#jsEmbed.js_sys['view.getInt32'](refPtr + refIndex * 4, 1)[0]", + " elemIndex = refIndices[refIndex]", " refIndex--", " }} else {{", " elemIndex = tableIndex", @@ -168,29 +173,30 @@ js_bindgen::embed_js!( " }}", "", " table.set(elemIndex, array[arrayIndex])", - " this.#jsEmbed.js_sys['view.setInt32'](arrPtr + arrayIndex * 4, [elemIndex])", + " elemIndices[arrayIndex] = elemIndex", " }}", "", + " this.#jsEmbed.js_sys['view.setInt32'](arrPtr, elemIndices)", " return true", "}}", ); +js_bindgen::embed_js!( + module = "js_sys", + name = "array.js_value.decode", + required_embeds = [("js_sys", "view.getInt32")], + "(ptr, len) => {{", + " const array = new Array(len)", + " const refIndices = this.#jsEmbed.js_sys['view.getInt32'](ptr, len)", + " for (let arrayIndex = 0; arrayIndex < len; arrayIndex++) {{", + " array[arrayIndex] = this.#jsEmbed.js_sys['externref.table'].get(refIndices[arrayIndex])", + " }}", + " return array", + "}}", +); + impl From<&[T]> for JsArray { fn from(value: &[T]) -> Self { - js_bindgen::embed_js!( - module = "js_sys", - name = "array.js_value.decode", - required_embeds = [("js_sys", "view.getInt32")], - "(ptr, len) => {{", - " const array = new Array(len)", - " for (let arrayIndex = 0; arrayIndex < len; arrayIndex++) {{", - " const [refIndex] = this.#jsEmbed.js_sys['view.getInt32'](ptr + arrayIndex * 4, 1)", - " array[arrayIndex] = this.#jsEmbed.js_sys['externref.table'].get(refIndex)", - " }}", - " return array", - "}}", - ); - let slice = JsValue::from_slice(value); // SAFETY: Parameters are correct. let result = @@ -200,32 +206,17 @@ impl From<&[T]> for JsArray { } } -// SAFETY: Implementation. -unsafe impl Input for &[T] { - const WAT_TYPE: &'static str = Self::Type::WAT_TYPE; - const WAT_CONV: Option = Self::Type::WAT_CONV; - const JS_CONV: Option = Some(InputJsConv { - embed: Some(("js_sys", "array.rust.js_value")), - pre: " = this.#jsEmbed.js_sys['array.rust.js_value'](", - post: Some(")"), - }); - - type Type = ExternSlice; - - fn into_raw(self) -> Self::Type { - js_bindgen::embed_js!( - module = "js_sys", - name = "array.rust.js_value", - required_embeds = [ - ("js_sys", "extern_ref"), - ("js_sys", "array.js_value.decode") - ], - "(dataPtr) => {{", - " const {{ ptr, len }} = this.#jsEmbed.js_sys['extern_ref'](dataPtr)", - " return this.#jsEmbed.js_sys['array.js_value.decode'](ptr, len)", - "}}", - ); +// SAFETY: The two slots point to borrowed `JsValue` table indices, which the +// JavaScript decoder resolves before the import is called. +unsafe impl IntoJS for &[T] { + const JS_CONV: Option = Some( + IntoJsConv::new("this.#jsEmbed.js_sys['array.js_value.decode']($slot1, $slot2)") + .with_embed(("js_sys", "array.js_value.decode")), + ); + type Abi = ExternSlice; + + fn into_abi(self) -> Self::Abi { ExternSlice::new(JsValue::from_slice(self)) } } @@ -305,29 +296,17 @@ impl From<&[u32]> for JsArray { } } -// SAFETY: Implementation. -unsafe impl Input for &[u32] { - const WAT_TYPE: &'static str = Self::Type::WAT_TYPE; - const WAT_CONV: Option = Self::Type::WAT_CONV; - const JS_CONV: Option = Some(InputJsConv { - embed: Some(("js_sys", "array.rust.u32")), - pre: " = this.#jsEmbed.js_sys['array.rust.u32'](", - post: Some(")"), - }); - - type Type = ExternSlice; - - fn into_raw(self) -> Self::Type { - js_bindgen::embed_js!( - module = "js_sys", - name = "array.rust.u32", - required_embeds = [("js_sys", "extern_ref"), ("js_sys", "view.getUint32")], - "(dataPtr) => {{", - " const {{ ptr, len }} = this.#jsEmbed.js_sys.extern_ref(dataPtr)", - " return this.#jsEmbed.js_sys['view.getUint32'](ptr, len)", - "}}", - ); +// SAFETY: The two slots describe a borrowed `u32` slice, which JavaScript +// copies into an array before the import is called. +unsafe impl IntoJS for &[u32] { + const JS_CONV: Option = Some( + IntoJsConv::new("this.#jsEmbed.js_sys['view.getUint32']($slot1, $slot2)") + .with_embed(("js_sys", "view.getUint32")), + ); + + type Abi = ExternSlice; + fn into_abi(self) -> Self::Abi { ExternSlice::new(self) } } diff --git a/client/js-sys/src/bigint/bigint.gen.rs b/client/js-sys/src/bigint/bigint.gen.rs index 9cb49c10..a9168807 100644 --- a/client/js-sys/src/bigint/bigint.gen.rs +++ b/client/js-sys/src/bigint/bigint.gen.rs @@ -3,7 +3,7 @@ #![allow(warnings)] use crate::JsValue; -use crate::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; +use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; #[repr(transparent)] pub struct JsBigInt(JsValue); @@ -20,28 +20,20 @@ impl From for JsValue { } } -unsafe impl Input for &JsBigInt { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; +unsafe impl JsCast for JsBigInt {} - type Type = <&'static JsValue as Input>::Type; +unsafe impl IntoJS for JsBigInt { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } -unsafe impl JsCast for JsBigInt {} - -unsafe impl Output for JsBigInt { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; - - type Type = ::Type; +unsafe impl OptionIntoJS for JsBigInt { + type OptionAbi = ::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } diff --git a/client/js-sys/src/exception.rs b/client/js-sys/src/exception.rs new file mode 100644 index 00000000..0e2faacb --- /dev/null +++ b/client/js-sys/src/exception.rs @@ -0,0 +1,46 @@ +use core::cell::Cell; + +use crate::JsValue; +#[cfg(not(target_feature = "exception-handling"))] +use crate::externref; + +#[cfg(target_feature = "exception-handling")] +js_bindgen::import_js!( + module = "js_sys", + name = "exception.tag", + "WebAssembly.JSTag", +); + +thread_local! { + static EXCEPTION: Cell = const { Cell::new(0) }; +} + +fn set(index: i32) { + EXCEPTION.with(|exception| { + debug_assert_eq!(exception.get(), 0); + exception.set(index); + }); +} + +/// Stores the `externref` table index for an exception caught by Wasm. +#[cfg(target_feature = "exception-handling")] +#[unsafe(export_name = "js_sys.exception.store")] +extern "C" fn store(index: i32) { + set(index); +} + +/// Reserves an `externref` table entry for an exception caught by JavaScript. +/// +/// JavaScript fills the returned table entry before returning to Wasm. +#[cfg(not(target_feature = "exception-handling"))] +#[unsafe(export_name = "js_sys.exception.store")] +extern "C" fn store() -> i32 { + let index = externref::reserve(); + set(index); + index +} + +pub(crate) fn take() -> Option { + let index = EXCEPTION.with(Cell::take); + (index != 0).then(|| JsValue::new(index)) +} diff --git a/client/js-sys/src/externref.rs b/client/js-sys/src/externref.rs index f886f1af..49cdb7d6 100644 --- a/client/js-sys/src/externref.rs +++ b/client/js-sys/src/externref.rs @@ -10,6 +10,8 @@ js_bindgen::unsafe_global_wat!( "(import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ \"js_sys.externref.table\")) 2 externref))", "(import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32)))", + "(import \"env\" \"js_sys.externref.recycle\" (func $js_sys.externref.recycle (@sym) (param \ + i32)))", "(func $js_sys.externref.grow (@sym) (param $size i32) (result i32)", " ref.null extern", " local.get $size", @@ -27,6 +29,21 @@ js_bindgen::unsafe_global_wat!( " local.get $index", " table.get $js_sys.import.externref.table (@reloc)", ")", + "(func $js_sys.externref.take (@sym) (param $index i32) (result externref)", + " (local $value externref)", + " local.get $index", + " table.get $js_sys.import.externref.table (@reloc)", + " local.set $value", + " ;; Indices zero and one are reserved for `undefined` and `null`.", + " local.get $index", + " i32.const 2", + " i32.ge_u", + " if", + " local.get $index", + " call $js_sys.externref.recycle (@reloc)", + " end", + " local.get $value", + ")", "(func $js_sys.externref.remove (@sym) (param $index i32)", " local.get $index", " ref.null extern", @@ -120,7 +137,16 @@ impl ExternrefTable { } } +pub(crate) fn reserve() -> i32 { + EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().next()) +} + #[unsafe(export_name = "js_sys.externref.next")] extern "C" fn next() -> i32 { - EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().next()) + reserve() +} + +#[unsafe(export_name = "js_sys.externref.recycle")] +extern "C" fn recycle(index: i32) { + EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().remove(index)); } diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 6bfeb3ae..33169f96 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -1,81 +1,541 @@ -use core::mem::ManuallyDrop; +use core::mem::{ManuallyDrop, MaybeUninit}; use core::ptr; use crate::JsValue; +/// One carrier position in the Wasm function `ABI`. +/// +/// # Safety +/// +/// `WAT_TYPE` must describe the carrier's Rust Wasm `ABI`. Each conversion must +/// consume or produce that type as appropriate. `WAT_TYPE` must not be empty +/// except for [`EmptySlot`]. +pub unsafe trait Slot { + const WAT_TYPE: &'static str; + const INTO_JS_WAT_CONV: Option = None; + const FROM_JS_WAT_CONV: Option = None; +} + +/// Converts a Rust-side `ABI` carrier to and from primitive Wasm slots. +/// +/// Types that occupy one `ABI` slot use themselves as `Slot1`. Multi-slot +/// carriers represent each primitive independently. +/// +/// # Safety +/// +/// The slots and their order must match the generated `extern` function +/// signature and return layout. Unused trailing slots must be [`EmptySlot`], +/// which is zero-sized and omitted from the Wasm `ABI`. +pub unsafe trait WasmAbi: Sized { + type Slot1: Slot; + type Slot2: Slot; + type Slot3: Slot; + type Slot4: Slot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4); + fn join(slot1: Self::Slot1, slot2: Self::Slot2, slot3: Self::Slot3, slot4: Self::Slot4) + -> Self; +} + +#[derive(Clone, Copy)] +pub enum ReturnMode { + Direct, + Indirect, +} + +/// A [`WasmAbi`] that can be returned through the Rust `extern "C"` `ABI`. +/// /// # Safety /// -/// This directly manipulates Wasm output and therefor all bets are off! (TODO) -pub unsafe trait Input { - const WAT_TYPE: &str; - const WAT_CONV: Option = None; - const JS_CONV: Option = None; +/// `MODE` must match the `ABI` of [`WasmRet`]. A direct return must use +/// exactly one non-empty slot. An indirect return uses the target's native +/// pointer type for its hidden return parameter. +pub unsafe trait ReturnAbi: WasmAbi { + const MODE: ReturnMode; +} - type Type; +impl ReturnMode { + #[must_use] + pub const fn is_direct(self) -> bool { + matches!(self, Self::Direct) + } +} - fn into_raw(self) -> Self::Type; +/// The FFI-safe return representation of a [`WasmAbi`] value. +#[doc(hidden)] +#[repr(C)] +pub struct WasmRet { + slot1: T::Slot1, + slot2: T::Slot2, + slot3: T::Slot3, + slot4: T::Slot4, } -pub struct InputWatConv { +impl WasmRet { + #[must_use] + #[inline] + pub fn from_abi(value: T) -> Self { + let (slot1, slot2, slot3, slot4) = value.split(); + + Self { + slot1, + slot2, + slot3, + slot4, + } + } + + #[must_use] + #[inline] + pub fn join(self) -> T { + T::join(self.slot1, self.slot2, self.slot3, self.slot4) + } + + #[doc(hidden)] + #[must_use] + pub const fn slot_offset() -> usize { + match SLOT { + 0 => core::mem::offset_of!(Self, slot1), + 1 => core::mem::offset_of!(Self, slot2), + 2 => core::mem::offset_of!(Self, slot3), + 3 => core::mem::offset_of!(Self, slot4), + _ => panic!("invalid WasmRet slot"), + } + } +} + +/// A zero-sized placeholder for an unused Wasm `ABI` slot. +#[doc(hidden)] +#[derive(Default)] +#[repr(C)] +pub struct EmptySlot([u8; 0]); + +impl EmptySlot { + #[must_use] + pub const fn new() -> Self { + Self([]) + } +} + +// SAFETY: `EmptySlot` is an absent slot and therefore has no WAT type. +unsafe impl Slot for EmptySlot { + const WAT_TYPE: &'static str = ""; +} + +// SAFETY: Every non-empty `Slot` is a complete single-slot `ABI` carrier. +// `EmptySlot` maps to an entirely empty carrier. +unsafe impl WasmAbi for T { + type Slot1 = Self; + type Slot2 = EmptySlot; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + (self, EmptySlot::new(), EmptySlot::new(), EmptySlot::new()) + } + + fn join(slot1: Self::Slot1, _: Self::Slot2, _: Self::Slot3, _: Self::Slot4) -> Self { + slot1 + } +} + +// SAFETY: The first slot is the presence tag, followed by up to three payload +// slots. +unsafe impl WasmAbi for Option +where + T: WasmAbi, + T::Slot1: Default, + T::Slot2: Default, + T::Slot3: Default, +{ + type Slot1 = u32; + type Slot2 = T::Slot1; + type Slot3 = T::Slot2; + type Slot4 = T::Slot3; + + #[inline] + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + match self { + None => ( + 0, + Default::default(), + Default::default(), + Default::default(), + ), + Some(value) => { + let (slot1, slot2, slot3, _) = value.split(); + (1, slot1, slot2, slot3) + } + } + } + + #[inline] + fn join( + is_some: Self::Slot1, + slot1: Self::Slot2, + slot2: Self::Slot3, + slot3: Self::Slot4, + ) -> Self { + if is_some == 0 { + None + } else { + Some(T::join(slot1, slot2, slot3, EmptySlot::new())) + } + } +} + +#[derive(Clone, Copy)] +pub struct WatConv { pub import: Option<&'static str>, pub conv: &'static str, pub r#type: &'static str, } -pub struct InputJsConv { - pub embed: Option<(&'static str, &'static str)>, - pub pre: &'static str, - pub post: Option<&'static str>, +/// # Safety +/// +/// `Abi`, `into_abi`, and `JS_CONV` must describe one consistent conversion +/// from a Rust value to a JavaScript value. `into_abi` produces the primitive +/// slots and `JS_CONV` combines them. Multi-slot `ABI` representations must +/// define `JS_CONV`. +pub unsafe trait IntoJS { + const JS_CONV: Option = None; + + type Abi: WasmAbi; + + fn into_abi(self) -> Self::Abi; +} + +/// Converts a Rust function result into its JavaScript return representation. +/// +/// Ordinary values delegate to [`IntoJS`]. Types such as [`Result`] may also +/// describe JavaScript control flow, such as throwing an error. +pub trait ReturnIntoJS { + const JS_CONV: ReturnConv; + + type Abi: ReturnAbi; + + fn return_into_abi(self) -> Self::Abi; +} + +impl ReturnIntoJS for T +where + T: IntoJS, + T::Abi: ReturnAbi, +{ + const JS_CONV: ReturnConv = ReturnConv::Value(T::JS_CONV); + + type Abi = T::Abi; + + fn return_into_abi(self) -> Self::Abi { + self.into_abi() + } } +/// Extends [`IntoJS`] with the representation of `Option`. +/// /// # Safety /// -/// This directly manipulates Wasm output and therefor all bets are off! (TODO) -pub unsafe trait Output { - const WAT_TYPE: &str; - const WAT_CONV: Option = None; - const JS_CONV: Option = None; +/// `OptionAbi`, `option_into_abi`, and `OPTION_JS_CONV` must describe one +/// consistent conversion from `Option` to a JavaScript value. +pub unsafe trait OptionIntoJS: IntoJS + Sized { + const OPTION_JS_CONV: Option = Self::JS_CONV; - type Type; + type OptionAbi: WasmAbi; - fn from_raw(raw: Self::Type) -> Self; + fn option_into_abi(value: Option) -> Self::OptionAbi; } -pub struct OutputWatConv { - pub import: Option<&'static str>, - pub direct: bool, - pub conv: &'static str, - pub r#type: &'static str, +// SAFETY: Delegated to the `OptionIntoJS` implementation. +unsafe impl IntoJS for Option { + const JS_CONV: Option = T::OPTION_JS_CONV; + + type Abi = T::OptionAbi; + + fn into_abi(self) -> Self::Abi { + T::option_into_abi(self) + } +} + +/// Converts primitive `ABI` slots into one JavaScript value. +#[derive(Clone, Copy)] +pub struct IntoJsConv { + pub(crate) embed: Option<(&'static str, &'static str)>, + pub(crate) template: &'static str, +} + +/// Describes how a function return is handled at the JavaScript boundary. +#[derive(Clone, Copy)] +pub enum ReturnConv { + /// The value is returned normally. + Value(Option), + /// `Ok` is returned normally and `Err` follows the exception path. + Result(Option), +} + +impl ReturnConv { + #[must_use] + pub const fn conversion(self) -> Option { + match self { + Self::Value(value) | Self::Result(value) => value, + } + } + + #[must_use] + pub const fn is_result(self) -> bool { + matches!(self, Self::Result(_)) + } } -pub struct OutputJsConv { - pub embed: Option<(&'static str, &'static str)>, - pub pre: &'static str, - pub post: &'static str, +/// Converts one JavaScript value into primitive `ABI` slots. +#[derive(Clone, Copy)] +pub struct FromJsConv { + pub(crate) embed: Option<(&'static str, &'static str)>, + pub(crate) templates: [&'static str; 4], + pub(crate) sret: Option<&'static str>, +} + +impl IntoJsConv { + /// Produces one JavaScript value from `$slot1` through `$slot4`. + #[must_use] + pub const fn new(template: &'static str) -> Self { + Self { + embed: None, + template, + } + } + + #[must_use] + pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { + self.embed = Some(embed); + self + } +} + +impl FromJsConv { + /// Produces `ABI` slots from `$value`. + #[must_use] + pub const fn slot1(template: &'static str) -> Self { + Self { + embed: None, + templates: [template, "", "", ""], + sret: None, + } + } + + #[must_use] + pub const fn slot2(mut self, template: &'static str) -> Self { + self.templates[1] = template; + self + } + + #[must_use] + pub const fn slot3(mut self, template: &'static str) -> Self { + self.templates[2] = template; + self + } + + #[must_use] + pub const fn slot4(mut self, template: &'static str) -> Self { + self.templates[3] = template; + self + } + + /// Stores the slots in an indirect return area. + /// + /// The function receives every non-empty slot in order, followed by the + /// indirect return pointer. + #[must_use] + pub const fn sret(mut self, function: &'static str) -> Self { + self.sret = Some(function); + self + } + + #[must_use] + pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { + self.embed = Some(embed); + self + } } /// # Safety /// -/// This MUST only be implemented on types that are `#[transparent]` over a -/// [`JsValue`]. (TODO) -pub unsafe trait JsCast: Sized { - #[must_use] - fn unchecked_from(value: JsValue) -> Self { - // This seems to be the only way to transmute between two owned types without - // copying when the size is unknown. In this case the size is unknown because - // `Self` is a generic. +/// `Abi`, `from_abi`, and `JS_CONV` must describe one consistent conversion +/// from a JavaScript value to a Rust value. `JS_CONV` produces the primitive +/// slots and `from_abi` reconstructs the Rust value. Multi-slot `ABI` +/// representations must define one slot template for every non-empty slot. +/// Indirect return `ABIs` must define an `sret` function. +pub unsafe trait FromJS { + const JS_CONV: Option = None; + + type Abi: ReturnAbi; - union Transmute { - from: ManuallyDrop, - to: ManuallyDrop, + fn from_abi(raw: Self::Abi) -> Self; +} + +/// Converts the return value of a JavaScript import into its Rust result. +/// +/// `Abi` describes the successful return value. The raw carrier may be +/// uninitialized when JavaScript throws, so implementations that catch +/// exceptions must inspect the exception state before decoding it. +pub trait ReturnFromJS { + const JS_CONV: ReturnConv; + + type Abi: ReturnAbi; + + fn return_from_abi(raw: MaybeUninit>) -> Self; +} + +impl ReturnFromJS for T +where + T: FromJS, +{ + const JS_CONV: ReturnConv = ReturnConv::Value(T::JS_CONV); + + type Abi = T::Abi; + + fn return_from_abi(raw: MaybeUninit>) -> Self { + // SAFETY: An ordinary JavaScript import always initializes its return + // value before the adapter returns. + T::from_abi(unsafe { raw.assume_init() }.join()) + } +} + +/// The return `ABI` for exporting [`Result`] to JavaScript. +/// +/// The first two slots carry the error and its presence tag. The remaining two +/// slots carry the successful value. +#[doc(hidden)] +pub struct ResultIntoJsAbi { + value: Result::Abi>, +} + +// SAFETY: The first slot transfers an error `externref`, the second is the +// error tag, and the remaining slots match the successful value's `ABI`. +unsafe impl WasmAbi for ResultIntoJsAbi +where + T: WasmAbi, + T::Slot1: Default, + T::Slot2: Default, +{ + type Slot1 = ::Abi; + type Slot2 = u32; + type Slot3 = T::Slot1; + type Slot4 = T::Slot2; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + match self.value { + Ok(value) => { + let (slot1, slot2, _, _) = value.split(); + (JsValue::UNDEFINED.into_abi(), 0, slot1, slot2) + } + Err(error) => (error, 1, Default::default(), Default::default()), } + } - let transmute = Transmute { - from: ManuallyDrop::new(value), + fn join( + error: Self::Slot1, + is_error: Self::Slot2, + slot1: Self::Slot3, + slot2: Self::Slot4, + ) -> Self { + let value = if is_error == 0 { + Ok(T::join(slot1, slot2, EmptySlot::new(), EmptySlot::new())) + } else { + Err(error) }; + + Self { value } + } +} + +// SAFETY: `ResultIntoJsAbi` is returned through a hidden pointer. +unsafe impl ReturnAbi for ResultIntoJsAbi +where + T: WasmAbi, + T::Slot1: Default, + T::Slot2: Default, +{ + const MODE: ReturnMode = ReturnMode::Indirect; +} + +impl ReturnIntoJS for Result +where + T: IntoJS, + E: Into, + T::Abi: ReturnAbi, + ::Slot1: Default, + ::Slot2: Default, +{ + const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); + + type Abi = ResultIntoJsAbi; + + fn return_into_abi(self) -> Self::Abi { + let value = match self { + Ok(value) => Ok(value.into_abi()), + Err(error) => Err(error.into().into_abi()), + }; + + ResultIntoJsAbi { value } + } +} + +impl ReturnFromJS for Result +where + T: FromJS, +{ + const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); + + type Abi = T::Abi; + + fn return_from_abi(raw: MaybeUninit>) -> Self { + if let Some(error) = crate::exception::take() { + #[cfg(not(target_feature = "exception-handling"))] + if ::MODE.is_direct() { + // SAFETY: A direct Wasm return is always initialized. On the + // exception path it contains only the JavaScript fallback value. + drop(T::from_abi(unsafe { raw.assume_init() }.join())); + } + + Err(error) + } else { + // SAFETY: Without a stored exception, the JavaScript import + // initialized its successful return value. + Ok(T::from_abi(unsafe { raw.assume_init() }.join())) + } + } +} + +/// A type that can be borrowed from an owned JavaScript conversion. +/// +/// The anchor owns the converted value for the duration of an exported +/// function call and provides the reference passed to that function. +pub trait RefFromJS { + type Anchor: FromJS + core::borrow::Borrow; +} + +impl RefFromJS for T { + type Anchor = T; +} + +/// # Safety +/// +/// This must only be implemented for types that are transparent over +/// [`JsValue`]. +pub unsafe trait JsCast: Sized { + #[must_use] + fn unchecked_as_ref(&self) -> &JsValue { + let ptr: *const JsValue = ptr::from_ref(self).cast(); + // SAFETY: The trait assumes that `Self` is `#[transparent]` over a `JsValue`. + unsafe { &*ptr } + } + + #[must_use] + fn unchecked_from(value: JsValue) -> Self { + let value = ManuallyDrop::new(value); + let ptr: *const Self = ptr::from_ref(&*value).cast(); // SAFETY: The trait assumes that `Self` is `#[transparent]` over a `JsValue`. - let result = unsafe { transmute.to }; - ManuallyDrop::into_inner(result) + unsafe { ptr.read() } } #[must_use] diff --git a/client/js-sys/src/interop/mod.rs b/client/js-sys/src/interop/mod.rs new file mode 100644 index 00000000..4e8d1f32 --- /dev/null +++ b/client/js-sys/src/interop/mod.rs @@ -0,0 +1,2 @@ +mod primitive; +mod string; diff --git a/client/js-sys/src/interop/primitive.rs b/client/js-sys/src/interop/primitive.rs new file mode 100644 index 00000000..6c8acef1 --- /dev/null +++ b/client/js-sys/src/interop/primitive.rs @@ -0,0 +1,665 @@ +use crate::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionIntoJS, ReturnAbi, ReturnMode, Slot, + WasmAbi, +}; +use crate::r#macro::const_concat; + +macro_rules! slot { + ($wat:literal, $($ty:ty),+ $(,)?) => {$( + // SAFETY: The declared WAT type describes this primitive `ABI` slot. + unsafe impl Slot for $ty { + const WAT_TYPE: &'static str = $wat; + } + + // SAFETY: Primitive scalar values are returned directly. + unsafe impl ReturnAbi for $ty { + const MODE: ReturnMode = ReturnMode::Direct; + } + )+}; +} + +macro_rules! from_js { + ($($ty:ty),+ $(,)?) => {$( + // SAFETY: The JavaScript adapter produces this primitive's native `ABI` + // slot, which is returned unchanged. + unsafe impl FromJS for $ty { + type Abi = Self; + + fn from_abi(raw: Self::Abi) -> Self { + raw + } + } + )*}; +} + +macro_rules! identity { + ($($ty:ty),+ $(,)?) => {$( + // SAFETY: This primitive is already represented by its native `ABI` slot. + unsafe impl IntoJS for $ty { + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } + } + + from_js!($ty); + )*}; +} + +macro_rules! sentinel_option { + ( + carrier: $carrier:ty, + sentinel: $sentinel:expr, + js_sentinel: $js_sentinel:literal, + types: [$([$($ty:ident),+ $(,)?] => { + to_js: $to_js:literal, + from_js: $from_js:literal, + }),+ $(,)?], + ) => {$($( + // SAFETY: The sentinel lies outside the value range of this type. + unsafe impl OptionIntoJS for $ty { + const OPTION_JS_CONV: Option = Some(IntoJsConv::new(const_concat!( + "$slot1 === ", + $js_sentinel, + " ? undefined : ", + $to_js + ))); + + type OptionAbi = $carrier; + + fn option_into_abi(value: Option) -> Self::OptionAbi { + value.map_or($sentinel, |value| { + sentinel_option!(@into_abi value, $ty, $carrier) + }) + } + } + + // SAFETY: The sentinel is decoded before the carrier is converted back. + unsafe impl FromJS for Option<$ty> { + const JS_CONV: Option = Some(FromJsConv::slot1(const_concat!( + "((value) => value == null ? ", + $js_sentinel, + " : ", + $from_js, + ")($value)" + ))); + + type Abi = $carrier; + + #[expect( + clippy::allow_attributes, + reason = "the generic expansion covers both signed and unsigned carriers" + )] + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "JavaScript normalizes the carrier to this type's value range" + )] + fn from_abi(raw: Self::Abi) -> Self { + if raw == $sentinel { + None + } else { + Some(sentinel_option!(@from_abi raw, $ty)) + } + } + } + )+)+}; + (@into_abi $value:ident, bool, $carrier:ty) => { + <$carrier>::from($value) + }; + (@into_abi $value:ident, usize, $carrier:ty) => { + { + #[expect( + clippy::cast_precision_loss, + reason = "wasm32 usize values are exactly representable by f64" + )] + let carrier = $value as $carrier; + carrier + } + }; + (@into_abi $value:ident, isize, $carrier:ty) => { + { + #[expect( + clippy::cast_precision_loss, + reason = "wasm32 isize values are exactly representable by f64" + )] + let carrier = $value as $carrier; + carrier + } + }; + (@into_abi $value:ident, $ty:ident, $carrier:ty) => { + $value as $carrier + }; + (@from_abi $raw:ident, bool) => { + $raw != 0 + }; + (@from_abi $raw:ident, $ty:ident) => { + $raw as $ty + }; +} + +macro_rules! indirect_option { + ($($ty:ty => { + decode: $decode:literal, + encode: $encode:literal, + slots: [$($slot:literal),+ $(,)?], + }),+ $(,)?) => {$( + // SAFETY: The optional value is represented by a presence tag followed by + // its payload slots and is returned through a hidden pointer. + unsafe impl ReturnAbi for Option<$ty> { + const MODE: ReturnMode = ReturnMode::Indirect; + } + + // SAFETY: The decoder combines the presence tag and payload slots into one + // optional JavaScript value. + unsafe impl OptionIntoJS for $ty { + const OPTION_JS_CONV: Option = Some( + IntoJsConv::new(indirect_option!(@decode $decode, [$($slot),+])) + .with_embed(("js_sys", $decode)), + ); + + type OptionAbi = Option; + + fn option_into_abi(value: Option) -> Self::OptionAbi { + value + } + } + + // SAFETY: The encoder writes a JavaScript value as a presence tag and the + // payload slots expected by `Option<$ty>`. + unsafe impl FromJS for Option<$ty> { + const JS_CONV: Option = Some( + indirect_option!(@output [$($slot),+]) + .sret(const_concat!("this.#jsEmbed.js_sys['", $encode, "']")) + .with_embed(("js_sys", $encode)), + ); + + type Abi = Self; + + fn from_abi(raw: Self::Abi) -> Self { + raw + } + } + )+}; + (@decode $decode:literal, [$slot1:literal, $slot2:literal]) => { + const_concat!("this.#jsEmbed.js_sys['", $decode, "']($slot1, $slot2)") + }; + (@decode $decode:literal, [$slot1:literal, $slot2:literal, $slot3:literal]) => { + const_concat!("this.#jsEmbed.js_sys['", $decode, "']($slot1, $slot2, $slot3)") + }; + (@output [$slot1:literal, $slot2:literal]) => { + FromJsConv::slot1($slot1).slot2($slot2) + }; + (@output [$slot1:literal, $slot2:literal, $slot3:literal]) => { + FromJsConv::slot1($slot1).slot2($slot2).slot3($slot3) + }; +} + +slot!("i32", bool, u8, u16, u32, i8, i16, i32); +slot!("i64", u64, i64); +slot!("f32", f32); +slot!("f64", f64); +#[cfg(target_arch = "wasm32")] +slot!("i32", isize, usize); +#[cfg(target_arch = "wasm64")] +slot!("i64", isize, usize); + +identity!(u8, u16, i8, i16, i32, i64, isize, f32, f64); +from_js!(bool, u32, u64, usize); + +// SAFETY: The JavaScript conversion normalizes the `i32` Wasm slot to a +// `boolean`. +unsafe impl IntoJS for bool { + const JS_CONV: Option = Some(IntoJsConv::new("!!$slot1")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: The JavaScript conversion reinterprets the `i32` Wasm slot as an +// unsigned 32-bit number. +unsafe impl IntoJS for u32 { + const JS_CONV: Option = Some(IntoJsConv::new("$slot1 >>> 0")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: The JavaScript conversion normalizes the `i64` Wasm slot to an +// unsigned 64-bit `BigInt`. +unsafe impl IntoJS for u64 { + const JS_CONV: Option = Some(IntoJsConv::new("BigInt.asUintN(64, $slot1)")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: On `wasm32`, `usize` uses an `i32` slot that JavaScript normalizes to +// an unsigned 32-bit number. +#[cfg(target_arch = "wasm32")] +unsafe impl IntoJS for usize { + const JS_CONV: Option = Some(IntoJsConv::new("$slot1 >>> 0")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: On `wasm64`, `usize` uses an `i64` slot that JavaScript normalizes to +// an unsigned 64-bit `BigInt`. +#[cfg(target_arch = "wasm64")] +unsafe impl IntoJS for usize { + const JS_CONV: Option = Some(IntoJsConv::new("BigInt.asUintN(64, $slot1)")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: `u128` is represented by its low and high 64-bit halves. +unsafe impl WasmAbi for u128 { + type Slot1 = u64; + type Slot2 = u64; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + #[expect( + clippy::cast_possible_truncation, + reason = "each cast extracts one 64-bit slot" + )] + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + ( + self as u64, + (self >> 64) as u64, + EmptySlot::new(), + EmptySlot::new(), + ) + } + + fn join(slot1: Self::Slot1, slot2: Self::Slot2, _: Self::Slot3, _: Self::Slot4) -> Self { + (Self::from(slot2) << 64) | Self::from(slot1) + } +} + +// SAFETY: `WasmRet` is returned through a hidden pointer. +unsafe impl ReturnAbi for u128 { + const MODE: ReturnMode = ReturnMode::Indirect; +} + +// SAFETY: The JavaScript decoder combines the low and high 64-bit slots into +// one unsigned `BigInt`. +unsafe impl IntoJS for u128 { + const JS_CONV: Option = Some( + IntoJsConv::new("this.#jsEmbed.js_sys['numeric.u128.decode']($slot1, $slot2)") + .with_embed(("js_sys", "numeric.u128.decode")), + ); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: The JavaScript encoder splits an unsigned `BigInt` into the low and +// high 64-bit slots expected by `u128`. +unsafe impl FromJS for u128 { + const JS_CONV: Option = Some( + FromJsConv::slot1("$value") + .slot2("$value >> 64n") + .sret("this.#jsEmbed.js_sys['numeric.128.encode']") + .with_embed(("js_sys", "numeric.128.encode")), + ); + + type Abi = Self; + + fn from_abi(raw: Self::Abi) -> Self { + raw + } +} + +// SAFETY: `i128` is represented by its low unsigned and high signed 64-bit +// halves. +unsafe impl WasmAbi for i128 { + type Slot1 = u64; + type Slot2 = i64; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "each cast preserves the corresponding 64-bit bit pattern" + )] + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + ( + self as u64, + (self >> 64) as i64, + EmptySlot::new(), + EmptySlot::new(), + ) + } + + fn join(slot1: Self::Slot1, slot2: Self::Slot2, _: Self::Slot3, _: Self::Slot4) -> Self { + (Self::from(slot2) << 64) | Self::from(slot1) + } +} + +// SAFETY: `WasmRet` is returned through a hidden pointer. +unsafe impl ReturnAbi for i128 { + const MODE: ReturnMode = ReturnMode::Indirect; +} + +// SAFETY: The JavaScript decoder combines the low unsigned and high signed +// 64-bit slots into one signed `BigInt`. +unsafe impl IntoJS for i128 { + const JS_CONV: Option = Some( + IntoJsConv::new("this.#jsEmbed.js_sys['numeric.i128.decode']($slot1, $slot2)") + .with_embed(("js_sys", "numeric.i128.decode")), + ); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: The JavaScript encoder splits a signed `BigInt` into the low +// unsigned and high signed 64-bit slots expected by `i128`. +unsafe impl FromJS for i128 { + const JS_CONV: Option = Some( + FromJsConv::slot1("$value") + .slot2("$value >> 64n") + .sret("this.#jsEmbed.js_sys['numeric.128.encode']") + .with_embed(("js_sys", "numeric.128.encode")), + ); + + type Abi = Self; + + fn from_abi(raw: Self::Abi) -> Self { + raw + } +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "numeric.u128.decode", + "(lo, hi) => {{", + " return hi === 0n", + " ? BigInt.asUintN(64, lo)", + " : BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "numeric.i128.decode", + "(lo, hi) => {{", + " return BigInt.asUintN(64, lo) | (hi << 64n)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "numeric.128.encode", + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (lo, hi, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setBigInt64(out, lo, true)", + " view.setBigInt64(out + 8, hi, true)", + " }}", + "}})()", +); + +// Outside the value range of every type encoded by the `i32` sentinel scheme. +const I32_OPTION_SENTINEL: i32 = 0x00ff_ffff; +// `Number.MAX_SAFE_INTEGER` cannot collide with a `wasm32` `usize`, `i32`, +// `u32`, or widened `f32` value. +const F64_OPTION_SENTINEL: f64 = 9_007_199_254_740_991.0; + +sentinel_option! { + carrier: i32, + sentinel: I32_OPTION_SENTINEL, + js_sentinel: "0x00ff_ffff", + types: [ + [i8, u8, i16, u16] => { + to_js: "$slot1", + from_js: "value", + }, + [bool] => { + to_js: "$slot1 !== 0", + from_js: "value ? 1 : 0", + }, + ], +} + +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "Number.MAX_SAFE_INTEGER", + types: [ + [i32] => { + to_js: "$slot1", + from_js: "value >> 0", + }, + [u32] => { + to_js: "$slot1", + from_js: "value >>> 0", + }, + [f32] => { + to_js: "$slot1", + from_js: "Math.fround(value)", + }, + ], +} + +#[cfg(target_arch = "wasm32")] +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "Number.MAX_SAFE_INTEGER", + types: [ + [isize] => { + to_js: "$slot1", + from_js: "value >> 0", + }, + [usize] => { + to_js: "$slot1", + from_js: "value >>> 0", + }, + ], +} + +indirect_option! { + f64 => { + decode: "optional.f64.decode", + encode: "optional.f64.encode", + slots: ["$value == null ? 0 : 1", "$value == null ? 0 : $value"], + }, + i64 => { + decode: "optional.i64.decode", + encode: "optional.i64.encode", + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + }, + u64 => { + decode: "optional.u64.decode", + encode: "optional.u64.encode", + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + }, +} + +#[cfg(target_arch = "wasm64")] +indirect_option! { + isize => { + decode: "optional.i64.decode", + encode: "optional.i64.encode", + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + }, + usize => { + decode: "optional.u64.decode", + encode: "optional.u64.encode", + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + }, +} + +indirect_option! { + u128 => { + decode: "optional.u128.decode", + encode: "optional.128.encode", + slots: [ + "$value == null ? 0 : 1", + "$value == null ? 0n : $value", + "$value == null ? 0n : $value >> 64n", + ], + }, + i128 => { + decode: "optional.i128.decode", + encode: "optional.128.encode", + slots: [ + "$value == null ? 0 : 1", + "$value == null ? 0n : $value", + "$value == null ? 0n : $value >> 64n", + ], + }, +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.f64.decode", + "(isSome, value) => {{", + " if (isSome === 0) return undefined", + " return value", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.f64.encode", + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, value, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setFloat64(out + 8, value, true)", + " }}", + "}})()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.i64.decode", + "(isSome, value) => {{", + " if (isSome === 0) return undefined", + " return value", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.i64.encode", + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, value, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setBigInt64(out + 8, value, true)", + " }}", + "}})()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.u64.decode", + "(isSome, value) => {{", + " if (isSome === 0) return undefined", + " return BigInt.asUintN(64, value)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.u64.encode", + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, value, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setBigUint64(out + 8, value, true)", + " }}", + "}})()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.u128.decode", + "(isSome, lo, hi) => {{", + " if (isSome === 0) return undefined", + " return hi === 0n", + " ? BigInt.asUintN(64, lo)", + " : BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.i128.decode", + "(isSome, lo, hi) => {{", + " if (isSome === 0) return undefined", + " return BigInt.asUintN(64, lo) | (hi << 64n)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "optional.128.encode", + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, lo, hi, out) => {{", + " if (out + 24 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setBigInt64(out + 8, lo, true)", + " view.setBigInt64(out + 16, hi, true)", + " }}", + "}})()", +); diff --git a/client/js-sys/src/interop/string.rs b/client/js-sys/src/interop/string.rs new file mode 100644 index 00000000..77ca6c09 --- /dev/null +++ b/client/js-sys/src/interop/string.rs @@ -0,0 +1,52 @@ +use crate::hazard::{IntoJS, IntoJsConv}; +use crate::util::ExternSlice; + +#[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] +js_bindgen::embed_js!( + module = "js_sys", + name = "string.decode", + "(() => {{", + " const decoder = new TextDecoder('utf-8', {{", + " fatal: false,", + " ignoreBOM: false,", + " }})", + " return (ptr, len) => {{", + " const view = new Uint8Array(this.#memory.buffer, ptr, len)", + " return decoder.decode(view)", + " }}", + "}})()", +); + +#[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] +js_bindgen::embed_js!( + module = "js_sys", + name = "string.decode", + required_embeds = [("js_sys", "string.sab")], + "(() => {{", + " const decoder = new TextDecoder('utf-8', {{", + " fatal: false,", + " ignoreBOM: false,", + " }})", + " return (ptr, len) => {{", + " const view = new Uint8Array(this.#memory.buffer, ptr, len)", + " return decoder.decode(", + " this.#jsEmbed.js_sys['string.sab'] ? view : view.slice()", + " )", + " }}", + "}})()", +); + +// SAFETY: The UTF-8 byte slice is decoded into a JavaScript string before the +// import is called. +unsafe impl IntoJS for &str { + const JS_CONV: Option = Some( + IntoJsConv::new("this.#jsEmbed.js_sys['string.decode']($slot1, $slot2)") + .with_embed(("js_sys", "string.decode")), + ); + + type Abi = ExternSlice; + + fn into_abi(self) -> Self::Abi { + ExternSlice::new(self.as_bytes()) + } +} diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index d9d71b84..f7b93f7e 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -11,12 +11,15 @@ extern crate alloc; mod util; mod array; mod bigint; +mod exception; mod externref; pub mod hazard; +// Implementations for passing Rust standard types across the JavaScript +// boundary. +mod interop; #[doc(hidden)] pub mod r#macro; mod number; -mod numeric; mod panic; mod string; mod value; diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index fe3c8da7..95799752 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -1,385 +1,42 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! wat_imports { - (($($input:ty),*) $(, $output:ty)? $(,)?) => {{ - const VALUES: &[&str] = &[ - $($crate::r#macro::wat_input_import::<$input>(),)* - $($crate::r#macro::wat_output_import::<$output>(),)? - ]; - const SIZE: usize = { - let mut size = 0; - let mut index = 0; - - while index < VALUES.len() { - if let Some(value) = $crate::r#macro::wat_import_iter(VALUES, index) { - size += 1 + value.len(); - } - - index += 1; - } - - size - }; - - const IMPORTS: [u8; SIZE] = { - let mut imports = [0; SIZE]; - let mut byte_index = 0; - let mut value_index = 0; - - while value_index < VALUES.len() { - if let Some(value) = $crate::r#macro::wat_import_iter(VALUES, value_index) { - imports[byte_index] = b'\n'; - byte_index += 1; - - let value = value.as_bytes(); - let mut index = 0; - - while index < value.len() { - imports[byte_index] = value[index]; - byte_index += 1; - index += 1; - } - } - - value_index += 1; - } - - imports - }; - - if let ::core::result::Result::Ok(value) = ::core::str::from_utf8(&IMPORTS) { - value - } else { - ::core::panic!() - } - }}; -} - -pub use wat_imports; - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_indirect { - ($ty:ty) => { - if $crate::r#macro::direct::<$ty>() { - "" - } else { - $crate::r#macro::const_concat!(<$ty as $crate::hazard::Output>::WAT_TYPE, " ") - } - }; -} - -pub use wat_indirect; - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_input { - ($ty:ty) => { - if ::core::option::Option::is_some(&<$ty as $crate::hazard::Input>::WAT_CONV) { - const CONV: &::core::primitive::str = $crate::r#macro::wat_input_conv::<$ty>(); - - $crate::r#macro::const_concat!("\n ", CONV) - } else { - "" - } - }; -} - -pub use wat_input; - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_output { - ($ty:ty) => { - if ::core::option::Option::is_some(&<$ty as $crate::hazard::Output>::WAT_CONV) { - const CONV: &::core::primitive::str = $crate::r#macro::wat_output_conv::<$ty>(); - - if $crate::r#macro::direct::<$ty>() { - $crate::r#macro::const_concat!("\n ", CONV) - } else { - $crate::r#macro::const_concat!("\n local.get 0\n ", CONV) - } - } else { - "" - } - }; -} - -pub use wat_output; - -#[doc(hidden)] -#[macro_export] -macro_rules! js_select { - ($a:expr, $b:expr, ($($input:ty),*) $(, $output:ty)? $(,)?) => {'outer: { - $( - if ::core::option::Option::is_some(&<$input as $crate::hazard::Input>::JS_CONV) { - break 'outer $b; - } - )* - - $( - if ::core::option::Option::is_some(&<$output as $crate::hazard::Output>::JS_CONV) { - break 'outer $b; - } - )? - - $a - }}; -} - -pub use js_select; - -#[doc(hidden)] -#[macro_export] -macro_rules! js_parameter { - ($par:literal, $ty:ty $(,)?) => { - if let ::core::option::Option::Some($crate::hazard::InputJsConv { post, .. }) = - <$ty as $crate::hazard::Input>::JS_CONV - { - const CONV: &::core::primitive::str = $crate::r#macro::js_input_conv_pre::<$ty>(); - - if ::core::option::Option::is_some(&post) { - const POST_CONV: &::core::primitive::str = - $crate::r#macro::js_input_conv_post::<$ty>(); - - $crate::r#macro::const_concat!("\t", $par, CONV, $par, POST_CONV, "\n") - } else { - $crate::r#macro::const_concat!("\t", $par, CONV, "\n") - } - } else { - "" - } - }; -} - -pub use js_parameter; - -#[doc(hidden)] -#[macro_export] -macro_rules! js_output { - ($start:literal, $direct_call:literal, $indirect_call:literal, $output:ty, $($input:ty),* $(,)?) => {{ - let indirect_condition = ::core::option::Option::is_some(&<$output as $crate::hazard::Output>::JS_CONV) - $(|| ::core::option::Option::is_some(&<$input as $crate::hazard::Input>::JS_CONV))*; - - if ::core::option::Option::is_some(&<$output as $crate::hazard::Output>::JS_CONV) { - const CONV: [&::core::primitive::str; 2] = $crate::r#macro::js_output_conv::<$output>(); - - if indirect_condition { - $crate::r#macro::const_concat!($start, CONV[0], $indirect_call, CONV[1], "\n}") - } else { - $crate::r#macro::const_concat!(CONV[0], $direct_call, CONV[1]) - } - } else { - if indirect_condition { - $crate::r#macro::const_concat!($start, $indirect_call, "\n}") - } else { - $crate::r#macro::const_concat!($direct_call) - } - } - }}; -} - -pub use js_output; - -#[doc(hidden)] -#[macro_export] -macro_rules! const_concat { - ($($value:expr),*) => {{ - const LEN: ::core::primitive::usize = $(::core::primitive::str::len($value) +)* 0; - const VALUE: [::core::primitive::u8; LEN] = { - let mut value = [0; LEN]; - - let mut index = 0; - - $( - let mut local_index = 0; - let limit = index + ::core::primitive::str::len($value); - let bytes = ::core::primitive::str::as_bytes($value); - while index < limit { - value[index] = bytes[local_index]; - index += 1; - local_index += 1; - } - )* - - value - }; - - if let ::core::result::Result::Ok(value) = ::core::str::from_utf8(&VALUE) { - value - } else { - ::core::panic!() - } - }}; -} - -pub use const_concat; - -use crate::hazard::{Input, InputJsConv, InputWatConv, Output, OutputJsConv, OutputWatConv}; - -#[must_use] -pub const fn wat_direct() -> &'static str { - if direct::() { T::WAT_TYPE } else { "" } -} - -#[must_use] -pub const fn wat_import_iter<'a>(values: &[&'a str], index: usize) -> Option<&'a str> { - let value = values[index]; - - if value.is_empty() { - return None; - } - - let mut c_index = 0; - - while c_index < index { - let c_value = values[c_index]; - - if value.len() == c_value.len() { - let mut l_index = 0; - let mut equal = true; - - while l_index < value.len() { - if value.as_bytes()[l_index] != c_value.as_bytes()[l_index] { - equal = false; - break; - } - - l_index += 1; - } - - if equal { - return None; - } - } - - c_index += 1; - } - - Some(value) -} - -#[must_use] -pub const fn wat_input_import() -> &'static str { - if let Some(InputWatConv { - import: Some(import), - .. - }) = T::WAT_CONV - { - import - } else { - "" - } -} - -#[must_use] -pub const fn wat_input_import_type() -> &'static str { - if let Some(InputWatConv { r#type, .. }) = T::WAT_CONV { - r#type - } else { - T::WAT_TYPE - } -} - -#[must_use] -pub const fn wat_input_conv() -> &'static str { - if let Some(InputWatConv { conv, .. }) = T::WAT_CONV { - conv - } else { - "" - } -} - -#[must_use] -pub const fn wat_output_import() -> &'static str { - if let Some(OutputWatConv { - import: Some(import), - .. - }) = T::WAT_CONV - { - import - } else { - "" - } -} - -#[must_use] -pub const fn wat_output_import_type() -> &'static str { - if let Some(OutputWatConv { r#type, .. }) = T::WAT_CONV { - r#type - } else { - T::WAT_TYPE - } -} - -#[must_use] -pub const fn wat_output_conv() -> &'static str { - if let Some(OutputWatConv { conv, .. }) = T::WAT_CONV { - conv - } else { - "" - } -} - -#[must_use] -pub const fn direct() -> bool { - if let Some(OutputWatConv { direct, .. }) = T::WAT_CONV { - direct - } else { - true - } -} - -#[must_use] -pub const fn js_input_embed() -> (&'static str, &'static str) { - if let Some(InputJsConv { - embed: Some(embed), .. - }) = T::JS_CONV - { - embed - } else { - ("", "") - } -} - -#[must_use] -pub const fn js_output_embed() -> (&'static str, &'static str) { - if let Some(OutputJsConv { - embed: Some(embed), .. - }) = T::JS_CONV - { - embed - } else { - ("", "") - } -} - -#[must_use] -pub const fn js_input_conv_pre() -> &'static str { - if let Some(InputJsConv { pre, .. }) = T::JS_CONV { - pre - } else { - "" - } -} - -#[must_use] -pub const fn js_input_conv_post() -> &'static str { - if let Some(InputJsConv { - post: Some(post), .. - }) = T::JS_CONV - { - post - } else { - "" - } -} - -#[must_use] -pub const fn js_output_conv() -> [&'static str; 2] { - if let Some(OutputJsConv { pre, post, .. }) = T::JS_CONV { - [pre, post] - } else { - [""; 2] - } -} +mod abi; +mod export; +mod js_import; +mod result; +mod text; +mod wat; +mod wat_import; + +pub use abi::*; +pub use result::*; +pub use text::*; +pub use wat::*; + +// Text rendering. +pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; +// JavaScript export adapters. +pub use crate::{ + js_export, js_export_arguments, js_export_input_arguments, js_export_output_expression, + js_export_parameters, +}; +// JavaScript import adapters. +pub use crate::{ + js_function, js_import, js_indirect_function, js_input_parameters, js_needs_adapter, js_output, + js_parameter, +}; +// WAT export adapters. +pub use crate::{ + wat_export, wat_export_direct, wat_export_imports, wat_export_indirect, wat_export_input_gets, + wat_export_input_params, wat_export_input_raw_param, wat_export_input_raw_types, + wat_export_result_loads, wat_export_result_types, +}; +// WAT import adapters. +pub use crate::{ + wat_import, wat_import_input_types, wat_import_result, wat_imports, wat_input_gets, + wat_input_import_types, wat_input_params, wat_output, wat_output_get, wat_output_import_param, + wat_output_param, wat_output_result, +}; +// Shared WAT and exception helpers. +pub use crate::{ + wat_import_list, wat_result_catch, wat_result_default, wat_result_try, wat_slot_params, + wat_slot_types, +}; diff --git a/client/js-sys/src/macro/abi.rs b/client/js-sys/src/macro/abi.rs new file mode 100644 index 00000000..9703e2eb --- /dev/null +++ b/client/js-sys/src/macro/abi.rs @@ -0,0 +1,388 @@ +use crate::hazard::{ + FromJS, IntoJS, ReturnAbi, ReturnFromJS, ReturnIntoJS, Slot, WasmAbi, WasmRet, WatConv, +}; + +// Rust ABI shims used by generated import and export functions. + +pub type InputSlot1 = <::Abi as WasmAbi>::Slot1; +pub type InputSlot2 = <::Abi as WasmAbi>::Slot2; +pub type InputSlot3 = <::Abi as WasmAbi>::Slot3; +pub type InputSlot4 = <::Abi as WasmAbi>::Slot4; + +pub type OutputSlot1 = <::Abi as WasmAbi>::Slot1; +pub type OutputSlot2 = <::Abi as WasmAbi>::Slot2; +pub type OutputSlot3 = <::Abi as WasmAbi>::Slot3; +pub type OutputSlot4 = <::Abi as WasmAbi>::Slot4; +pub type OutputRet = core::mem::MaybeUninit::Abi>>; + +pub type ReturnSlot1 = <::Abi as WasmAbi>::Slot1; +pub type ReturnSlot2 = <::Abi as WasmAbi>::Slot2; +pub type ReturnSlot3 = <::Abi as WasmAbi>::Slot3; +pub type ReturnSlot4 = <::Abi as WasmAbi>::Slot4; + +#[must_use] +#[inline] +pub fn split_input( + value: T, +) -> (InputSlot1, InputSlot2, InputSlot3, InputSlot4) { + WasmAbi::split(T::into_abi(value)) +} + +#[must_use] +#[inline] +pub fn join_from_js( + slot1: ::Slot1, + slot2: ::Slot2, + slot3: ::Slot3, + slot4: ::Slot4, +) -> T { + T::from_abi(T::Abi::join(slot1, slot2, slot3, slot4)) +} + +#[must_use] +#[inline] +pub fn return_to_js(value: T) -> WasmRet { + WasmRet::from_abi(T::return_into_abi(value)) +} + +/// Lowers a value through a different [`IntoJS`] implementation with the same +/// ABI. This is reserved for generated `#[js_sys(type = ...)]` overrides, where +/// `T` must also describe the value's WAT and JavaScript conversions. +/// +/// # Safety +/// +/// The value's lowering must have the semantics expected by `T`; sharing an ABI +/// alone does not make two [`IntoJS`] implementations interchangeable. +#[must_use] +#[inline] +pub unsafe fn split_input_as( + value: impl IntoJS, +) -> (InputSlot1, InputSlot2, InputSlot3, InputSlot4) { + WasmAbi::split(IntoJS::into_abi(value)) +} + +#[must_use] +#[inline] +pub fn join_output(value: OutputRet) -> T { + T::return_from_abi(value) +} + +// Compile-time validation of conversion metadata. + +#[must_use] +pub const fn into_js_is_multislot() -> bool { + ! as Slot>::WAT_TYPE.is_empty() + || ! as Slot>::WAT_TYPE.is_empty() + || ! as Slot>::WAT_TYPE.is_empty() +} + +pub const fn validate_into_js() { + assert!( + !into_js_is_multislot::() || T::JS_CONV.is_some(), + "multi-slot IntoJS implementations must define IntoJS::JS_CONV", + ); +} + +pub const fn validate_return_from_js() { + let indirect = !return_from_js_is_direct::(); + let conversion = T::JS_CONV.conversion(); + let (templates, sret) = match conversion { + None => ([""; 4], None), + Some(conv) => (conv.templates, conv.sret), + }; + let slots = from_js_wat_slots::(); + let mut slot = 0; + + while slot < slots.len() { + assert!( + conversion.is_none() || templates[slot].is_empty() == slots[slot].abi.is_empty(), + "FromJS::JS_CONV templates must match its non-empty ABI slots", + ); + slot += 1; + } + + assert!( + !indirect || conversion.is_some(), + "indirect FromJS implementations must define FromJS::JS_CONV", + ); + assert!( + indirect == sret.is_some(), + "FromJS::JS_CONV must define sret exactly for indirect returns", + ); +} + +// WAT metadata shared by import and export adapters. + +/// The `WAT` representation of one `ABI` slot at a JavaScript boundary. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct WatSlot { + /// The carrier type in the Rust function ABI. + pub abi: &'static str, + /// The type visible at the JavaScript boundary. + pub boundary: &'static str, + /// An optional WAT import required by the conversion. + pub import: &'static str, + /// WAT instructions that convert between `abi` and `boundary`. + pub conv: &'static str, +} + +const fn wat_slot(wat_conv: Option) -> WatSlot { + let (boundary, import, conv) = match wat_conv { + Some(WatConv { + import, + conv, + r#type, + }) => ( + r#type, + match import { + Some(import) => import, + None => "", + }, + conv, + ), + None => (S::WAT_TYPE, "", ""), + }; + + WatSlot { + abi: S::WAT_TYPE, + boundary, + import, + conv, + } +} + +#[must_use] +pub const fn into_js_wat_slots() -> [WatSlot; 4] { + [ + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + ] +} + +#[must_use] +pub const fn from_js_wat_slots() -> [WatSlot; 4] { + [ + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + ] +} + +#[must_use] +pub const fn return_into_js_wat_slots() -> [WatSlot; 4] { + [ + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + ] +} + +#[must_use] +pub const fn wat_direct() -> &'static str { + if return_from_js_is_direct::() { + from_js_wat_slots::()[0].abi + } else { + "" + } +} + +#[must_use] +pub const fn wat_indirect_type() -> &'static str { + if return_from_js_is_direct::() { + "" + } else { + crate::util::WAT_PTR_TYPE + } +} + +#[must_use] +pub const fn wat_indirect_import_type() -> &'static str { + if return_from_js_is_direct::() { + "" + } else { + into_js_wat_slots::>()[0].boundary + } +} + +#[must_use] +pub const fn wat_indirect_conv() -> &'static str { + if return_from_js_is_direct::() { + "" + } else { + into_js_wat_slots::>()[0].conv + } +} + +#[must_use] +pub const fn wat_output_import() -> &'static str { + if return_from_js_is_direct::() { + from_js_wat_slots::()[0].import + } else { + "" + } +} + +#[must_use] +pub const fn wat_output_import_type() -> &'static str { + if return_from_js_is_direct::() { + from_js_wat_slots::()[0].boundary + } else { + "" + } +} + +#[must_use] +pub const fn wat_output_conv() -> &'static str { + if return_from_js_is_direct::() { + from_js_wat_slots::()[0].conv + } else { + "" + } +} + +#[must_use] +pub const fn return_from_js_is_direct() -> bool { + ::MODE.is_direct() +} + +#[must_use] +pub const fn return_from_js_is_result() -> bool { + T::JS_CONV.is_result() +} + +pub const fn validate_return_into_js() { + let conv = T::JS_CONV.conversion(); + let multislot = if T::JS_CONV.is_result() { + ! as Slot>::WAT_TYPE.is_empty() + } else { + ! as Slot>::WAT_TYPE.is_empty() + || ! as Slot>::WAT_TYPE.is_empty() + || ! as Slot>::WAT_TYPE.is_empty() + }; + + assert!( + !multislot || conv.is_some(), + "multi-slot ReturnIntoJS implementations must define a JavaScript conversion", + ); +} + +#[must_use] +pub const fn return_into_js_is_direct() -> bool { + ::MODE.is_direct() +} + +#[must_use] +pub const fn export_output_frame_size() -> usize { + // LLVM keeps the Wasm stack pointer 16-byte aligned. Rounding every adapter + // frame to that alignment preserves the invariant when the frame is allocated. + const STACK_ALIGNMENT: usize = 16; + let size = core::mem::size_of::>(); + + (size + STACK_ALIGNMENT - 1) & !(STACK_ALIGNMENT - 1) +} + +#[must_use] +pub const fn export_output_slot_offset() -> usize { + WasmRet::::slot_offset::() +} + +#[must_use] +pub const fn wat_pointer_type() -> &'static str { + crate::util::WAT_PTR_TYPE +} + +// JavaScript conversion metadata. + +#[must_use] +pub const fn js_input_embed() -> (&'static str, &'static str) { + js_embed(match T::JS_CONV { + Some(conv) => conv.embed, + None => None, + }) +} + +#[must_use] +pub const fn js_return_embed() -> (&'static str, &'static str) { + js_embed(match T::JS_CONV.conversion() { + Some(conv) => conv.embed, + None => None, + }) +} + +#[must_use] +pub const fn js_output_embed() -> (&'static str, &'static str) { + js_embed(match T::JS_CONV.conversion() { + Some(conv) => conv.embed, + None => None, + }) +} + +#[must_use] +pub const fn js_result_embed() -> (&'static str, &'static str) { + if T::JS_CONV.is_result() { + ("js_sys", "externref.table") + } else { + ("", "") + } +} + +const fn js_embed(embed: Option<(&'static str, &'static str)>) -> (&'static str, &'static str) { + if let Some(embed) = embed { + embed + } else { + ("", "") + } +} + +#[must_use] +pub const fn js_input_template() -> &'static str { + if let Some(conv) = T::JS_CONV { + conv.template + } else { + "" + } +} + +#[must_use] +pub const fn js_export_output_template() -> &'static str { + let template = match T::JS_CONV.conversion() { + Some(conv) => conv.template, + None => "", + }; + + if template.is_empty() { + "$slot1" + } else { + template + } +} + +#[must_use] +pub const fn return_into_js_is_result() -> bool { + T::JS_CONV.is_result() +} + +#[must_use] +pub const fn js_output_templates() -> [&'static str; 4] { + if let Some(conv) = T::JS_CONV.conversion() { + conv.templates + } else { + ["$value", "", "", ""] + } +} + +#[must_use] +pub const fn js_output_sret() -> &'static str { + if let Some(conv) = T::JS_CONV.conversion() + && let Some(sret) = conv.sret + { + sret + } else { + "" + } +} diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs new file mode 100644 index 00000000..2de7f246 --- /dev/null +++ b/client/js-sys/src/macro/export.rs @@ -0,0 +1,383 @@ +// WAT adapter helpers. + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_imports { + (($($input:ty),*) $(, $output:ty)? $(,)?) => { + $crate::r#macro::wat_import_list!( + $($crate::r#macro::from_js_wat_slots::<$input>()[0].import,)* + $($crate::r#macro::from_js_wat_slots::<$input>()[1].import,)* + $($crate::r#macro::from_js_wat_slots::<$input>()[2].import,)* + $($crate::r#macro::from_js_wat_slots::<$input>()[3].import,)* + $($crate::r#macro::return_into_js_wat_slots::<$output>()[0].import,)? + $($crate::r#macro::return_into_js_wat_slots::<$output>()[1].import,)? + $($crate::r#macro::return_into_js_wat_slots::<$output>()[2].import,)? + $($crate::r#macro::return_into_js_wat_slots::<$output>()[3].import,)? + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_input_raw_types { + ($ty:ty $(,)?) => { + $crate::r#macro::wat_slot_types!($crate::r#macro::from_js_wat_slots::<$ty>(), abi,) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_input_raw_param { + ($ty:ty $(,)?) => {{ + const TYPES: &::core::primitive::str = + $crate::r#macro::wat_export_input_raw_types!($ty); + + $crate::r#macro::const_concat_if!( + !TYPES.is_empty() => [" (param ", TYPES, ")"], + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_input_params { + ($par:literal, $ty:ty $(,)?) => { + $crate::r#macro::wat_slot_params!( + $par, + $crate::r#macro::from_js_wat_slots::<$ty>(), + boundary, + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_input_gets { + ($par:literal, $ty:ty $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::from_js_wat_slots::<$ty>(); + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => [" local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], + !SLOTS[1].abi.is_empty() => [" local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], + !SLOTS[2].abi.is_empty() => [" local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], + !SLOTS[3].abi.is_empty() => [" local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_result_types { + ($ty:ty $(,)?) => { + $crate::r#macro::wat_slot_types!( + $crate::r#macro::return_into_js_wat_slots::<$ty>(), + boundary, + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_result_loads { + ($ty:ty $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::return_into_js_wat_slots::<$ty>(); + const OFFSET_0: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 0>() + ); + const OFFSET_1: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 1>() + ); + const OFFSET_2: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 2>() + ); + const OFFSET_3: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 3>() + ); + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => [" local.get $retptr\n ", SLOTS[0].abi, ".load offset=", OFFSET_0, $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], + !SLOTS[1].abi.is_empty() => [" local.get $retptr\n ", SLOTS[1].abi, ".load offset=", OFFSET_1, $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], + !SLOTS[2].abi.is_empty() => [" local.get $retptr\n ", SLOTS[2].abi, ".load offset=", OFFSET_2, $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], + !SLOTS[3].abi.is_empty() => [" local.get $retptr\n ", SLOTS[3].abi, ".load offset=", OFFSET_3, $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_direct { + ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => { + $crate::r#macro::const_concat!( + $crate::r#macro::wat_export_imports!(($($input),*)), + "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", + $raw, + "\"))", + $($crate::r#macro::wat_export_input_raw_param!($input),)* + "))\n", + "(func $export (@sym (name \"", + $export, + "\"))", + $($crate::r#macro::wat_export_input_params!($par, $input),)* + "\n", + $($crate::r#macro::wat_export_input_gets!($par, $input),)* + " call $raw (@reloc)\n", + ")" + ) + }; + ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + const SLOT: $crate::r#macro::WatSlot = + $crate::r#macro::return_into_js_wat_slots::<$output>()[0]; + + $crate::r#macro::const_concat!( + $crate::r#macro::wat_export_imports!(($($input),*), $output), + "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", + $raw, + "\"))", + $($crate::r#macro::wat_export_input_raw_param!($input),)* + " (result ", + SLOT.abi, + ")))\n", + "(func $export (@sym (name \"", + $export, + "\"))", + $($crate::r#macro::wat_export_input_params!($par, $input),)* + " (result ", + SLOT.boundary, + ")\n", + $($crate::r#macro::wat_export_input_gets!($par, $input),)* + " call $raw (@reloc)", + $crate::r#macro::wat_conv_prefix(SLOT.conv), + SLOT.conv, + "\n)" + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export_indirect { + ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); + const SIZE: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_frame_size::<$output>() + ); + const RESULT_TYPES: &::core::primitive::str = + $crate::r#macro::wat_export_result_types!($output); + + $crate::r#macro::const_concat!( + $crate::r#macro::wat_export_imports!(($($input),*), $output), + "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", + $raw, + "\")) (param ", + POINTER, + ")", + $($crate::r#macro::wat_export_input_raw_param!($input),)* + "))\n", + "(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut ", + POINTER, + ")))\n", + "(func $export (@sym (name \"", + $export, + "\"))", + $($crate::r#macro::wat_export_input_params!($par, $input),)* + " (result ", + RESULT_TYPES, + ")\n", + " (local $retptr ", + POINTER, + ")\n", + " global.get $__stack_pointer\n ", + POINTER, + ".const ", + SIZE, + "\n ", + POINTER, + ".sub\n local.tee $retptr\n global.set $__stack_pointer\n", + " local.get $retptr\n", + $($crate::r#macro::wat_export_input_gets!($par, $input),)* + " call $raw (@reloc)\n", + $crate::r#macro::wat_export_result_loads!($output), + " local.get $retptr\n ", + POINTER, + ".const ", + SIZE, + "\n ", + POINTER, + ".add\n global.set $__stack_pointer\n)" + ) + }}; +} + +/// Generates the complete WAT adapter for one Rust export. +#[doc(hidden)] +#[macro_export] +macro_rules! wat_export { + ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ + $($crate::r#macro::validate_return_from_js::<$input>();)* + + $crate::r#macro::wat_export_direct!($raw, $export, ($(($par, $input)),*)) + }}; + ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + $($crate::r#macro::validate_return_from_js::<$input>();)* + $crate::r#macro::validate_return_into_js::<$output>(); + + if $crate::r#macro::return_into_js_is_direct::<$output>() { + $crate::r#macro::wat_export_direct!( + $raw, + $export, + ($(($par, $input)),*), + $output, + ) + } else { + $crate::r#macro::wat_export_indirect!( + $raw, + $export, + ($(($par, $input)),*), + $output, + ) + } + }}; +} + +// JavaScript wrapper helpers. + +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_input_arguments { + ($par:literal, $ty:ty $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::from_js_wat_slots::<$ty>(); + const TEMPLATES: [&::core::primitive::str; 4] = + $crate::r#macro::js_output_templates::<$ty>(); + const VALUES: [&::core::primitive::str; 4] = [ + $crate::r#macro::js_template!(TEMPLATES[0], value = $par), + $crate::r#macro::js_template!(TEMPLATES[1], value = $par), + $crate::r#macro::js_template!(TEMPLATES[2], value = $par), + $crate::r#macro::js_template!(TEMPLATES[3], value = $par), + ]; + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => [VALUES[0]], + !SLOTS[1].abi.is_empty() => [", ", VALUES[1]], + !SLOTS[2].abi.is_empty() => [", ", VALUES[2]], + !SLOTS[3].abi.is_empty() => [", ", VALUES[3]], + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_parameters { + () => { + "" + }; + (($par:literal, $ty:ty) $(,)?) => { + $par + }; + (($par:literal, $ty:ty), $(($rest_par:literal, $rest_ty:ty)),+ $(,)?) => { + $crate::r#macro::const_concat!( + $par, + ", ", + $crate::r#macro::js_export_parameters!($(($rest_par, $rest_ty)),+) + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_arguments { + () => { + "" + }; + (($par:literal, $ty:ty) $(,)?) => { + $crate::r#macro::js_export_input_arguments!($par, $ty) + }; + (($par:literal, $ty:ty), $(($rest_par:literal, $rest_ty:ty)),+ $(,)?) => {{ + const FIRST: &::core::primitive::str = + $crate::r#macro::js_export_input_arguments!($par, $ty); + const REST: &::core::primitive::str = + $crate::r#macro::js_export_arguments!($(($rest_par, $rest_ty)),+); + + $crate::r#macro::const_concat!( + FIRST, + $crate::r#macro::separator_between(FIRST, REST), + REST + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_output_expression { + ($ty:ty $(,)?) => {{ + const DIRECT: ::core::primitive::bool = $crate::r#macro::return_into_js_is_direct::<$ty>(); + const RESULT: ::core::primitive::bool = $crate::r#macro::return_into_js_is_result::<$ty>(); + const VALUES: [&::core::primitive::str; 4] = if RESULT { + ["ret[2]", "ret[3]", "", ""] + } else if DIRECT { + ["ret", "", "", ""] + } else { + ["ret[0]", "ret[1]", "ret[2]", "ret[3]"] + }; + + $crate::r#macro::js_template!( + $crate::r#macro::js_export_output_template::<$ty>(), + slots = VALUES, + ) + }}; +} + +/// Generates the complete JavaScript wrapper for one Rust export. +#[doc(hidden)] +#[macro_export] +macro_rules! js_export { + ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ + $($crate::r#macro::validate_return_from_js::<$input>();)* + const PARAMETERS: &::core::primitive::str = + $crate::r#macro::js_export_parameters!($(($par, $input)),*); + const ARGUMENTS: &::core::primitive::str = + $crate::r#macro::js_export_arguments!($(($par, $input)),*); + + $crate::r#macro::const_concat!( + "(", + PARAMETERS, + ") => {\n instance.exports['", + $export, + "'](", + ARGUMENTS, + ")\n}" + ) + }}; + ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + $($crate::r#macro::validate_return_from_js::<$input>();)* + $crate::r#macro::validate_return_into_js::<$output>(); + const PARAMETERS: &::core::primitive::str = + $crate::r#macro::js_export_parameters!($(($par, $input)),*); + const ARGUMENTS: &::core::primitive::str = + $crate::r#macro::js_export_arguments!($(($par, $input)),*); + const OUTPUT: &::core::primitive::str = + $crate::r#macro::js_export_output_expression!($output); + const THROW: &::core::primitive::str = + if $crate::r#macro::return_into_js_is_result::<$output>() { + " if (ret[1] !== 0) throw ret[0]\n" + } else { + "" + }; + + $crate::r#macro::const_concat!( + "(", + PARAMETERS, + ") => {\n const ret = instance.exports['", + $export, + "'](", + ARGUMENTS, + ")\n", + THROW, + " return ", + OUTPUT, + "\n}" + ) + }}; +} diff --git a/client/js-sys/src/macro/js_import.rs b/client/js-sys/src/macro/js_import.rs new file mode 100644 index 00000000..15bcada2 --- /dev/null +++ b/client/js-sys/src/macro/js_import.rs @@ -0,0 +1,232 @@ +/// Generates the complete JavaScript adapter for one import. +#[doc(hidden)] +#[macro_export] +macro_rules! js_import { + ( + direct_open = $direct_open:expr, + direct_call = $direct_call:expr, + indirect_call = $indirect_call:expr, + inputs = [$(($par:literal, $input:ty)),* $(,)?], + ) => {{ + const WRAPPED: ::core::primitive::bool = + $crate::r#macro::js_needs_adapter!(($($input),*)); + const OPEN: &::core::primitive::str = if WRAPPED { + $crate::r#macro::js_function!("(", ") => {\n", $(($par, $input)),*) + } else { + $direct_open + }; + const BODY: &::core::primitive::str = if WRAPPED { + $crate::r#macro::const_concat!($indirect_call, "\n}") + } else { + $direct_call + }; + + $crate::r#macro::const_concat!( + OPEN, + $($crate::r#macro::js_parameter!($par, $input),)* + BODY + ) + }}; + ( + direct_open = $direct_open:expr, + direct_call = $direct_call:expr, + indirect_call = $indirect_call:expr, + inputs = [$(($par:literal, $input:ty)),* $(,)?], + output = $output:ty, + ) => {{ + const WRAPPED: ::core::primitive::bool = + $crate::r#macro::js_needs_adapter!(($($input),*), $output); + const OPEN: &::core::primitive::str = if WRAPPED { + $crate::r#macro::js_indirect_function!( + "(", + ") => {\n", + ($output), + $(($par, $input)),* + ) + } else { + $direct_open + }; + + $crate::r#macro::const_concat!( + OPEN, + $($crate::r#macro::js_parameter!($par, $input),)* + $crate::r#macro::js_output!( + WRAPPED, + " return ", + $direct_call, + $indirect_call, + $output, + ) + ) + }}; +} + +// Adapter selection. + +#[doc(hidden)] +#[macro_export] +macro_rules! js_needs_adapter { + (($($input:ty),*) $(, $output:ty)? $(,)?) => {{ + 'outer: { + $( + $crate::r#macro::validate_into_js::<$input>(); + + if ::core::option::Option::is_some( + &<$input as $crate::hazard::IntoJS>::JS_CONV, + ) { + break 'outer true; + } + )* + + $( + $crate::r#macro::validate_return_from_js::<$output>(); + + if $crate::r#macro::catches_result_in_js::<$output>() + || ::core::option::Option::is_some( + &<$output as $crate::hazard::ReturnFromJS>::JS_CONV.conversion(), + ) + { + break 'outer true; + } + )? + + false + } + }}; +} + +// Input rendering. + +#[doc(hidden)] +#[macro_export] +macro_rules! js_input_parameters { + ($par:literal, $ty:ty $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::into_js_wat_slots::<$ty>(); + + $crate::r#macro::const_concat_if!( + true => [$par, "_0"], + !SLOTS[1].abi.is_empty() => [", ", $par, "_1"], + !SLOTS[2].abi.is_empty() => [", ", $par, "_2"], + !SLOTS[3].abi.is_empty() => [", ", $par, "_3"], + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! js_function { + ($pre:literal, $post:literal $(,)?) => { + $crate::r#macro::const_concat!($pre, $post) + }; + ($pre:literal, $post:literal, ($par:literal, $ty:ty) $(, ($rest_par:literal, $rest_ty:ty))* $(,)?) => { + $crate::r#macro::const_concat!( + $pre, + $crate::r#macro::js_input_parameters!($par, $ty), + $(", ", $crate::r#macro::js_input_parameters!($rest_par, $rest_ty),)* + $post + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! js_indirect_function { + ($pre:literal, $post:literal, (), $(($par:literal, $ty:ty)),* $(,)?) => { + $crate::r#macro::js_function!($pre, $post, $(($par, $ty)),*) + }; + ($pre:literal, $post:literal, ($output:ty), $(($par:literal, $ty:ty)),* $(,)?) => {{ + const PARAMETERS: &::core::primitive::str = + $crate::r#macro::js_function!("", "", $(($par, $ty)),*); + const INDIRECT: ::core::primitive::bool = + !$crate::r#macro::return_from_js_is_direct::<$output>(); + const RETURN: &::core::primitive::str = if INDIRECT { "$retptr" } else { "" }; + const SEPARATOR: &::core::primitive::str = + if INDIRECT && !PARAMETERS.is_empty() { ", " } else { "" }; + + $crate::r#macro::const_concat!($pre, RETURN, SEPARATOR, PARAMETERS, $post) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! js_parameter { + ($par:literal, $ty:ty $(,)?) => {{ + const HAS_CONV: ::core::primitive::bool = + ::core::option::Option::is_some(&<$ty as $crate::hazard::IntoJS>::JS_CONV); + const TEMPLATE: &::core::primitive::str = $crate::r#macro::js_input_template::<$ty>(); + const SLOTS: [&::core::primitive::str; 4] = [ + $crate::r#macro::const_concat!($par, "_0"), + $crate::r#macro::const_concat!($par, "_1"), + $crate::r#macro::const_concat!($par, "_2"), + $crate::r#macro::const_concat!($par, "_3"), + ]; + const CONV: &::core::primitive::str = $crate::r#macro::js_template!( + TEMPLATE, + slots = SLOTS, + ); + + $crate::r#macro::const_concat_if!( + HAS_CONV => [" ", $par, "_0 = ", CONV, "\n"], + ) + }}; +} + +// Return rendering. + +#[doc(hidden)] +#[macro_export] +macro_rules! js_output { + ($wrapped:expr, $start:literal, $direct_call:literal, $indirect_call:literal, $output:ty $(,)?) => {{ + const OUTPUT_WRAPPED: ::core::primitive::bool = $wrapped; + const DIRECT_RETURN: ::core::primitive::bool = + $crate::r#macro::return_from_js_is_direct::<$output>(); + const CATCH_RESULT: ::core::primitive::bool = + $crate::r#macro::catches_result_in_js::<$output>(); + const CALL: &::core::primitive::str = if OUTPUT_WRAPPED { + $indirect_call + } else { + $direct_call + }; + const TEMPLATES: [&::core::primitive::str; 4] = + $crate::r#macro::js_output_templates::<$output>(); + const TEMPLATE_VALUE: &::core::primitive::str = if DIRECT_RETURN { CALL } else { "$ret" }; + const SLOTS: [&::core::primitive::str; 4] = [ + $crate::r#macro::js_template!(TEMPLATES[0], value = TEMPLATE_VALUE), + $crate::r#macro::js_template!(TEMPLATES[1], value = TEMPLATE_VALUE), + $crate::r#macro::js_template!(TEMPLATES[2], value = TEMPLATE_VALUE), + $crate::r#macro::js_template!(TEMPLATES[3], value = TEMPLATE_VALUE), + ]; + const SRET: &::core::primitive::str = $crate::r#macro::js_output_sret::<$output>(); + const INDENT: &::core::primitive::str = if CATCH_RESULT { " " } else { " " }; + const VALUE_START: &::core::primitive::str = if DIRECT_RETURN { + if CATCH_RESULT { + " return " + } else if OUTPUT_WRAPPED { + $start + } else { + "" + } + } else { + $crate::r#macro::const_concat!(INDENT, "const $ret = ") + }; + const OUTPUT_VALUE: &::core::primitive::str = if DIRECT_RETURN { SLOTS[0] } else { CALL }; + const SRET_CALL: &::core::primitive::str = $crate::r#macro::const_concat_if!( + !DIRECT_RETURN => ["\n", INDENT, SRET, "(", SLOTS[0]], + !DIRECT_RETURN && !SLOTS[1].is_empty() => [", ", SLOTS[1]], + !DIRECT_RETURN && !SLOTS[2].is_empty() => [", ", SLOTS[2]], + !DIRECT_RETURN && !SLOTS[3].is_empty() => [", ", SLOTS[3]], + !DIRECT_RETURN => [", $retptr)"], + ); + const TRY: &::core::primitive::str = $crate::r#macro::js_result_try::<$output>(); + const END: &::core::primitive::str = if CATCH_RESULT { + $crate::r#macro::js_result_catch::<$output>(DIRECT_RETURN) + } else if OUTPUT_WRAPPED { + "\n}" + } else { + "" + }; + + $crate::r#macro::const_concat!(TRY, VALUE_START, OUTPUT_VALUE, SRET_CALL, END) + }}; +} diff --git a/client/js-sys/src/macro/result.rs b/client/js-sys/src/macro/result.rs new file mode 100644 index 00000000..a7f1c154 --- /dev/null +++ b/client/js-sys/src/macro/result.rs @@ -0,0 +1,193 @@ +use crate::hazard::ReturnFromJS; + +#[cfg(not(target_feature = "exception-handling"))] +const DIRECT_CATCH: &str = concat!( + "\n } catch ($error) {", + "\n const $index = this.#instance.exports['js_sys.exception.store']()", + "\n this.#jsEmbed.js_sys['externref.table'].set($index, $error)", + "\n return false", + "\n }", + "\n}", +); +#[cfg(not(target_feature = "exception-handling"))] +const INDIRECT_CATCH: &str = concat!( + "\n } catch ($error) {", + "\n const $index = this.#instance.exports['js_sys.exception.store']()", + "\n this.#jsEmbed.js_sys['externref.table'].set($index, $error)", + "\n }", + "\n}", +); + +#[cfg(target_feature = "exception-handling")] +const WAT_TAG_IMPORT: &str = "(import \"js_sys\" \"exception.tag\" (tag $js_sys.exception.tag \ + (@sym (name \"js_sys.exception.tag\")) (param externref)))"; +#[cfg(target_feature = "exception-handling")] +const WAT_INSERT_IMPORT: &str = concat!( + "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) ", + "(param externref) (result i32)))", +); +#[cfg(target_feature = "exception-handling")] +const WAT_STORE_IMPORT: &str = concat!( + "(import \"env\" \"js_sys.exception.store\" (func $js_sys.exception.store (@sym) ", + "(param i32)))", +); +#[cfg(target_feature = "exception-handling")] +const WAT_CATCH: &str = concat!( + "\n return", + "\n )", + "\n unreachable", + "\n )", + "\n call $js_sys.externref.insert (@reloc)", + "\n call $js_sys.exception.store (@reloc)", +); + +#[must_use] +pub const fn catches_result_in_js() -> bool { + #[cfg(target_feature = "exception-handling")] + { + false + } + + #[cfg(not(target_feature = "exception-handling"))] + { + crate::r#macro::return_from_js_is_result::() + } +} + +#[must_use] +pub const fn js_result_try() -> &'static str { + #[cfg(target_feature = "exception-handling")] + { + "" + } + + #[cfg(not(target_feature = "exception-handling"))] + { + if crate::r#macro::return_from_js_is_result::() { + " try {\n" + } else { + "" + } + } +} + +#[must_use] +pub const fn js_result_catch(direct: bool) -> &'static str { + #[cfg(target_feature = "exception-handling")] + { + let _ = direct; + "" + } + + #[cfg(not(target_feature = "exception-handling"))] + { + if !crate::r#macro::return_from_js_is_result::() { + "" + } else if direct { + DIRECT_CATCH + } else { + INDIRECT_CATCH + } + } +} + +#[must_use] +pub const fn wat_result_imports() -> [&'static str; 3] { + #[cfg(target_feature = "exception-handling")] + { + if crate::r#macro::return_from_js_is_result::() { + [WAT_TAG_IMPORT, WAT_INSERT_IMPORT, WAT_STORE_IMPORT] + } else { + [""; 3] + } + } + + #[cfg(not(target_feature = "exception-handling"))] + { + [""; 3] + } +} + +#[must_use] +pub const fn wat_result_try() -> &'static str { + #[cfg(target_feature = "exception-handling")] + { + if crate::r#macro::return_from_js_is_result::() { + "\n (block $js_sys.exception.catch (result externref)\n (try_table (catch \ + $js_sys.exception.tag $js_sys.exception.catch) (@reloc)" + } else { + "" + } + } + + #[cfg(not(target_feature = "exception-handling"))] + { + "" + } +} + +#[must_use] +pub const fn wat_result_catch() -> &'static str { + #[cfg(target_feature = "exception-handling")] + { + if crate::r#macro::return_from_js_is_result::() { + WAT_CATCH + } else { + "" + } + } + + #[cfg(not(target_feature = "exception-handling"))] + { + "" + } +} + +#[must_use] +pub const fn wat_result_default() -> &'static str { + #[cfg(target_feature = "exception-handling")] + { + if !crate::r#macro::return_from_js_is_result::() + || !crate::r#macro::return_from_js_is_direct::() + { + return ""; + } + + match crate::r#macro::wat_direct::().as_bytes() { + b"i32" => "\n i32.const 0", + b"i64" => "\n i64.const 0", + b"f32" => "\n f32.const 0", + b"f64" => "\n f64.const 0", + _ => panic!("unsupported direct return type"), + } + } + + #[cfg(not(target_feature = "exception-handling"))] + { + "" + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_result_try { + ($ty:ty) => { + $crate::r#macro::wat_result_try::<$ty>() + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_result_catch { + ($ty:ty) => { + $crate::r#macro::wat_result_catch::<$ty>() + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_result_default { + ($ty:ty) => { + $crate::r#macro::wat_result_default::<$ty>() + }; +} diff --git a/client/js-sys/src/macro/text.rs b/client/js-sys/src/macro/text.rs new file mode 100644 index 00000000..35cb4605 --- /dev/null +++ b/client/js-sys/src/macro/text.rs @@ -0,0 +1,263 @@ +#[doc(hidden)] +#[macro_export] +macro_rules! js_template { + ($template:expr, value = $value:expr $(,)?) => { + $crate::r#macro::js_template!(@render $template, [$value, "", "", "", ""]) + }; + ($template:expr, slots = $slots:expr $(,)?) => {{ + const JS_TEMPLATE_SLOTS: [&::core::primitive::str; 4] = $slots; + $crate::r#macro::js_template!(@render $template, [ + "", + JS_TEMPLATE_SLOTS[0], + JS_TEMPLATE_SLOTS[1], + JS_TEMPLATE_SLOTS[2], + JS_TEMPLATE_SLOTS[3], + ]) + }}; + (@render $template:expr, [$value:expr, $slot1:expr, $slot2:expr, $slot3:expr, $slot4:expr $(,)?]) => {{ + const JS_TEMPLATE_REPLACEMENTS: [&::core::primitive::str; 5] = + [$value, $slot1, $slot2, $slot3, $slot4]; + const JS_TEMPLATE_LEN: ::core::primitive::usize = $crate::r#macro::js_template_len( + $template, + &JS_TEMPLATE_REPLACEMENTS, + ); + const JS_TEMPLATE_VALUE: [::core::primitive::u8; JS_TEMPLATE_LEN] = + $crate::r#macro::render_js_template::( + $template, + &JS_TEMPLATE_REPLACEMENTS, + ); + + // SAFETY: Rendering only replaces complete ASCII placeholders with valid strings. + unsafe { ::core::str::from_utf8_unchecked(&JS_TEMPLATE_VALUE) } + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! const_concat { + ($($value:expr),* $(,)?) => {{ + const VALUES: &[&::core::primitive::str] = &[$($value),*]; + const LEN: ::core::primitive::usize = $crate::r#macro::const_concat_len(VALUES); + const VALUE: [::core::primitive::u8; LEN] = { + let mut value = [0; LEN]; + let mut index = 0; + let mut value_index = 0; + + while value_index < VALUES.len() { + let mut local_index = 0; + let bytes = ::core::primitive::str::as_bytes(VALUES[value_index]); + + while local_index < bytes.len() { + value[index] = bytes[local_index]; + index += 1; + local_index += 1; + } + + value_index += 1; + } + + value + }; + + // SAFETY: Joining valid strings keeps the result valid. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! const_concat_if { + ($($condition:expr => [$($value:expr),* $(,)?]),* $(,)?) => {{ + const GROUPS: &[(::core::primitive::bool, &[&::core::primitive::str])] = &[ + $(($condition, &[$($value),*]),)* + ]; + const LEN: ::core::primitive::usize = + $crate::r#macro::const_concat_if_len(GROUPS); + const VALUE: [::core::primitive::u8; LEN] = { + let mut value = [0; LEN]; + let mut index = 0; + let mut group_index = 0; + + while group_index < GROUPS.len() { + if GROUPS[group_index].0 { + let values = GROUPS[group_index].1; + let mut value_index = 0; + + while value_index < values.len() { + let bytes = ::core::primitive::str::as_bytes(values[value_index]); + let mut byte_index = 0; + + while byte_index < bytes.len() { + value[index] = bytes[byte_index]; + index += 1; + byte_index += 1; + } + + value_index += 1; + } + } + + group_index += 1; + } + + value + }; + + // SAFETY: Joining valid strings keeps the result valid. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! const_integer_str { + ($value:expr $(,)?) => {{ + const INTEGER: $crate::js_bindgen::r#macro::ConstInteger<::core::primitive::usize> = + $crate::js_bindgen::r#macro::ConstInteger($value); + const LEN: ::core::primitive::usize = INTEGER.__jbg_len(); + const VALUE: [::core::primitive::u8; LEN] = INTEGER.__jbg_to_le_bytes::(); + + // SAFETY: The integer formatter only emits ASCII digits. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } + }}; +} + +#[must_use] +pub const fn separator(value: &str) -> &'static str { + if value.is_empty() { "" } else { " " } +} + +#[must_use] +pub const fn separator_between(left: &str, right: &str) -> &'static str { + if left.is_empty() || right.is_empty() { + "" + } else { + ", " + } +} + +#[must_use] +pub const fn const_concat_len(values: &[&str]) -> usize { + let mut len = 0; + let mut index = 0; + + while index < values.len() { + len += values[index].len(); + index += 1; + } + + len +} + +#[must_use] +pub const fn const_concat_if_len(groups: &[(bool, &[&str])]) -> usize { + let mut len = 0; + let mut group_index = 0; + + while group_index < groups.len() { + if groups[group_index].0 { + let values = groups[group_index].1; + let mut value_index = 0; + + while value_index < values.len() { + len += values[value_index].len(); + value_index += 1; + } + } + + group_index += 1; + } + + len +} + +const JS_TEMPLATE_PLACEHOLDERS: [&str; 5] = ["$value", "$slot1", "$slot2", "$slot3", "$slot4"]; + +const fn js_template_placeholder(template: &[u8], index: usize) -> usize { + if template[index] != b'$' { + return JS_TEMPLATE_PLACEHOLDERS.len(); + } + + let mut placeholder = 0; + + while placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { + let candidate = JS_TEMPLATE_PLACEHOLDERS[placeholder].as_bytes(); + + if index + candidate.len() <= template.len() { + let mut byte = 0; + let mut matches = true; + + while byte < candidate.len() { + if template[index + byte] != candidate[byte] { + matches = false; + break; + } + + byte += 1; + } + + if matches { + return placeholder; + } + } + + placeholder += 1; + } + + JS_TEMPLATE_PLACEHOLDERS.len() +} + +#[must_use] +pub const fn js_template_len(template: &str, replacements: &[&str; 5]) -> usize { + let template = template.as_bytes(); + let mut input = 0; + let mut output = 0; + + while input < template.len() { + let placeholder = js_template_placeholder(template, input); + + if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { + output += replacements[placeholder].len(); + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else { + output += 1; + input += 1; + } + } + + output +} + +#[must_use] +pub const fn render_js_template( + template: &str, + replacements: &[&str; 5], +) -> [u8; LEN] { + let template = template.as_bytes(); + let mut rendered = [0; LEN]; + let mut input = 0; + let mut output = 0; + + while input < template.len() { + let placeholder = js_template_placeholder(template, input); + + if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { + let replacement = replacements[placeholder].as_bytes(); + let mut byte = 0; + + while byte < replacement.len() { + rendered[output] = replacement[byte]; + output += 1; + byte += 1; + } + + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else { + rendered[output] = template[input]; + output += 1; + input += 1; + } + } + + rendered +} diff --git a/client/js-sys/src/macro/wat.rs b/client/js-sys/src/macro/wat.rs new file mode 100644 index 00000000..2e4c8505 --- /dev/null +++ b/client/js-sys/src/macro/wat.rs @@ -0,0 +1,132 @@ +#[must_use] +pub const fn wat_conv_prefix(value: &str) -> &'static str { + if value.is_empty() { "" } else { "\n " } +} + +#[must_use] +pub const fn wat_import_iter<'a>(values: &[&'a str], index: usize) -> Option<&'a str> { + let value = values[index]; + + if value.is_empty() { + return None; + } + + let mut candidate_index = 0; + + while candidate_index < index { + let candidate = values[candidate_index]; + + if value.len() == candidate.len() { + let mut byte_index = 0; + let mut equal = true; + + while byte_index < value.len() { + if value.as_bytes()[byte_index] != candidate.as_bytes()[byte_index] { + equal = false; + break; + } + + byte_index += 1; + } + + if equal { + return None; + } + } + + candidate_index += 1; + } + + Some(value) +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_import_list { + ($($value:expr),* $(,)?) => {{ + const VALUES: &[&::core::primitive::str] = &[$($value),*]; + const SIZE: ::core::primitive::usize = { + let mut size = 0; + let mut index = 0; + + while index < VALUES.len() { + if let ::core::option::Option::Some(value) = + $crate::r#macro::wat_import_iter(VALUES, index) + { + size += 1 + value.len(); + } + + index += 1; + } + + size + }; + + const IMPORTS: [::core::primitive::u8; SIZE] = { + let mut imports = [0; SIZE]; + let mut byte_index = 0; + let mut value_index = 0; + + while value_index < VALUES.len() { + if let ::core::option::Option::Some(value) = + $crate::r#macro::wat_import_iter(VALUES, value_index) + { + imports[byte_index] = b'\n'; + byte_index += 1; + + let value = value.as_bytes(); + let mut index = 0; + + while index < value.len() { + imports[byte_index] = value[index]; + byte_index += 1; + index += 1; + } + } + + value_index += 1; + } + + imports + }; + + if let ::core::result::Result::Ok(value) = ::core::str::from_utf8(&IMPORTS) { + value + } else { + ::core::panic!() + } + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_slot_types { + ($slots:expr, $field:ident $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; + + $crate::r#macro::const_concat!( + SLOTS[0].$field, + $crate::r#macro::separator(SLOTS[1].$field), + SLOTS[1].$field, + $crate::r#macro::separator(SLOTS[2].$field), + SLOTS[2].$field, + $crate::r#macro::separator(SLOTS[3].$field), + SLOTS[3].$field + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_slot_params { + ($par:literal, $slots:expr, $field:ident $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => [" (param $", $par, "_0 ", SLOTS[0].$field, ")"], + !SLOTS[1].abi.is_empty() => [" (param $", $par, "_1 ", SLOTS[1].$field, ")"], + !SLOTS[2].abi.is_empty() => [" (param $", $par, "_2 ", SLOTS[2].$field, ")"], + !SLOTS[3].abi.is_empty() => [" (param $", $par, "_3 ", SLOTS[3].$field, ")"], + ) + }}; +} diff --git a/client/js-sys/src/macro/wat_import.rs b/client/js-sys/src/macro/wat_import.rs new file mode 100644 index 00000000..b6d76b36 --- /dev/null +++ b/client/js-sys/src/macro/wat_import.rs @@ -0,0 +1,220 @@ +/// Generates the complete WAT adapter for one JavaScript import. +#[doc(hidden)] +#[macro_export] +macro_rules! wat_import { + ( + module = $crate_name:expr, + import = $import_name:expr, + adapter = $foreign_name:expr, + inputs = [$(($par:literal, $input:ty)),* $(,)?], + $(output = $output:ty,)? + ) => {{ + const INPUT_TYPES: &::core::primitive::str = + $crate::r#macro::wat_import_input_types!($($input),*); + const INPUT_PARAM: &::core::primitive::str = $crate::r#macro::const_concat_if!( + !INPUT_TYPES.is_empty() => [" (param ", INPUT_TYPES, ")"], + ); + + $crate::r#macro::const_concat!( + "(import \"", + $crate_name, + "\" \"", + $import_name, + "\" (func $", + $crate_name, + ".import.", + $import_name, + " (@sym (name \"", + $crate_name, + ".import.", + $import_name, + "\"))", + $($crate::r#macro::wat_output_import_param!($output),)? + INPUT_PARAM, + $($crate::r#macro::wat_import_result!($output),)? + "))", + $crate::r#macro::wat_imports!(($($input),*) $(, $output)?), + "\n(func $", + $foreign_name, + " (@sym)", + $($crate::r#macro::wat_output_param!($output),)? + $($crate::r#macro::wat_input_params!($par, $input),)* + $($crate::r#macro::wat_output_result!($output),)? + $($crate::r#macro::wat_result_try!($output),)? + $($crate::r#macro::wat_output_get!($output),)? + $("\n", $crate::r#macro::wat_input_gets!($par, $input),)* + "\n call $", + $crate_name, + ".import.", + $import_name, + " (@reloc)", + $($crate::r#macro::wat_output!($output),)? + $($crate::r#macro::wat_result_catch!($output),)? + $($crate::r#macro::wat_result_default!($output),)? + "\n)" + ) + }}; +} + +// Imported function signature and conversion dependencies. + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_import_input_types { + () => { + "" + }; + ($first:ty $(, $rest:ty)* $(,)?) => { + $crate::r#macro::const_concat!( + $crate::r#macro::wat_input_import_types!($first), + $(" ", $crate::r#macro::wat_input_import_types!($rest),)* + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_imports { + (($($input:ty),*) $(, $output:ty)? $(,)?) => { + $crate::r#macro::wat_import_list!( + $($crate::r#macro::into_js_wat_slots::<$input>()[0].import,)* + $($crate::r#macro::into_js_wat_slots::<$input>()[1].import,)* + $($crate::r#macro::into_js_wat_slots::<$input>()[2].import,)* + $($crate::r#macro::into_js_wat_slots::<$input>()[3].import,)* + $($crate::r#macro::wat_output_import::<$output>(),)? + $($crate::r#macro::wat_result_imports::<$output>()[0],)? + $($crate::r#macro::wat_result_imports::<$output>()[1],)? + $($crate::r#macro::wat_result_imports::<$output>()[2],)? + ) + }; +} + +// Return adapter. + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_output { + ($ty:ty) => { + if !$crate::r#macro::return_from_js_is_direct::<$ty>() { + "" + } else if !$crate::r#macro::wat_output_conv::<$ty>().is_empty() { + const CONV: &::core::primitive::str = $crate::r#macro::wat_output_conv::<$ty>(); + + $crate::r#macro::const_concat!("\n ", CONV) + } else { + "" + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_output_import_param { + ($ty:ty) => { + if $crate::r#macro::return_from_js_is_direct::<$ty>() { + "" + } else { + $crate::r#macro::const_concat!( + " (param $retptr ", + $crate::r#macro::wat_indirect_import_type::<$ty>(), + ")" + ) + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_output_param { + ($ty:ty) => { + if $crate::r#macro::return_from_js_is_direct::<$ty>() { + "" + } else { + $crate::r#macro::const_concat!( + " (param $retptr ", + $crate::r#macro::wat_indirect_type::<$ty>(), + ")" + ) + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_import_result { + ($ty:ty) => { + if $crate::r#macro::return_from_js_is_direct::<$ty>() { + $crate::r#macro::const_concat!( + " (result ", + $crate::r#macro::wat_output_import_type::<$ty>(), + ")" + ) + } else { + "" + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_output_result { + ($ty:ty) => { + if $crate::r#macro::return_from_js_is_direct::<$ty>() { + $crate::r#macro::const_concat!(" (result ", $crate::r#macro::wat_direct::<$ty>(), ")") + } else { + "" + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_output_get { + ($ty:ty) => {{ + if $crate::r#macro::return_from_js_is_direct::<$ty>() { + "" + } else { + const CONV: &::core::primitive::str = $crate::r#macro::wat_indirect_conv::<$ty>(); + + $crate::r#macro::const_concat!( + "\n local.get $retptr", + $crate::r#macro::wat_conv_prefix(CONV), + CONV + ) + } + }}; +} + +// Input adapter. + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_input_import_types { + ($ty:ty $(,)?) => { + $crate::r#macro::wat_slot_types!($crate::r#macro::into_js_wat_slots::<$ty>(), boundary,) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_input_params { + ($par:literal, $ty:ty $(,)?) => { + $crate::r#macro::wat_slot_params!($par, $crate::r#macro::into_js_wat_slots::<$ty>(), abi,) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_input_gets { + ($par:literal, $ty:ty $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::into_js_wat_slots::<$ty>(); + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => ["", " local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv], + !SLOTS[1].abi.is_empty() => ["\n", " local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv], + !SLOTS[2].abi.is_empty() => ["\n", " local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv], + !SLOTS[3].abi.is_empty() => ["\n", " local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv], + ) + }}; +} diff --git a/client/js-sys/src/number/number.gen.rs b/client/js-sys/src/number/number.gen.rs index dc460790..e09bce2d 100644 --- a/client/js-sys/src/number/number.gen.rs +++ b/client/js-sys/src/number/number.gen.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use crate::JsValue; -use crate::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; +use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; #[repr(transparent)] pub struct JsNumber { @@ -24,31 +24,20 @@ impl From> for JsValue { } } -unsafe impl Input for &JsNumber { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; +unsafe impl JsCast for JsNumber {} - type Type = <&'static JsValue as Input>::Type; +unsafe impl IntoJS for JsNumber { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.value) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } -unsafe impl JsCast for JsNumber {} - -unsafe impl Output for JsNumber { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; - - type Type = ::Type; +unsafe impl OptionIntoJS for JsNumber { + type OptionAbi = ::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self { - value: Output::from_raw(raw), - _type: PhantomData, - } + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } diff --git a/client/js-sys/src/numeric.rs b/client/js-sys/src/numeric.rs deleted file mode 100644 index f4e593c6..00000000 --- a/client/js-sys/src/numeric.rs +++ /dev/null @@ -1,234 +0,0 @@ -use core::mem; - -use crate::hazard::{Input, InputJsConv, InputWatConv, Output, OutputJsConv, OutputWatConv}; -use crate::r#macro::const_concat; -use crate::util::{ExternValue, WAT_PTR_TYPE}; - -macro_rules! input_output { - ($wasm:literal, $($ty:ty),*) => {$( - // SAFETY: Implementation. - unsafe impl Input for $ty { - const WAT_TYPE: &str = $wasm; - - type Type = Self; - - fn into_raw(self) -> Self::Type { - self - } - } - - output!($wasm, $ty); - )*}; -} - -macro_rules! output { - ($wasm:literal, $($ty:ty),*) => {$( - // SAFETY: Implementation. - unsafe impl Output for $ty { - const WAT_TYPE: &str = $wasm; - - type Type = Self; - - fn from_raw(raw: Self::Type) -> Self { - raw - } - } - )*}; -} - -output!("i32", bool); - -input_output!("i32", u8, u16); -output!("i32", u32); -output!("i64", u64); - -input_output!("i32", i8, i16, i32); -input_output!("i64", i64); - -input_output!("f32", f32); -input_output!("f64", f64); - -// SAFETY: Implementation. -unsafe impl Input for bool { - const WAT_TYPE: &str = "i32"; - const JS_CONV: Option = Some(InputJsConv { - embed: None, - pre: " = !!", - post: Some(""), - }); - - type Type = Self; - - fn into_raw(self) -> Self::Type { - self - } -} - -// SAFETY: Implementation. -unsafe impl Input for u32 { - const WAT_TYPE: &str = "i32"; - const JS_CONV: Option = Some(InputJsConv { - embed: None, - pre: " >>>= 0", - post: None, - }); - - type Type = Self; - - fn into_raw(self) -> Self::Type { - self - } -} - -// SAFETY: Implementation. -unsafe impl Input for u64 { - const WAT_TYPE: &str = "i64"; - const JS_CONV: Option = Some(InputJsConv { - embed: None, - pre: " = BigInt.asUintN(64, ", - post: Some(")"), - }); - - type Type = Self; - - fn into_raw(self) -> Self::Type { - self - } -} - -// SAFETY: Implementation. -unsafe impl Input for u128 { - const WAT_TYPE: &str = Self::Type::WAT_TYPE; - const WAT_CONV: Option = Self::Type::WAT_CONV; - const JS_CONV: Option = Some(InputJsConv { - embed: Some(("js_sys", "numeric.u128.decode")), - pre: " = this.#jsEmbed.js_sys['numeric.u128.decode'](", - post: Some(")"), - }); - - type Type = ExternValue; - - fn into_raw(self) -> Self::Type { - js_bindgen::embed_js!( - module = "js_sys", - name = "numeric.u128.decode", - required_embeds = [("js_sys", "view.getBigUint64")], - "(ptr) => {{", - " const [lo, hi] = this.#jsEmbed.js_sys['view.getBigUint64'](ptr, 2)", - " return lo | (hi << 64n)", - "}}", - ); - - ExternValue::new(AlignedValue(self.to_le_bytes())) - } -} - -// SAFETY: Implementation. -unsafe impl Output for u128 { - const WAT_TYPE: &str = WAT_PTR_TYPE; - const WAT_CONV: Option = Some(OutputWatConv { - import: Some(const_concat!( - "(import \"env\" \"js_sys.numeric.128\" (func $js_sys.numeric.128 (@sym) (param i64 \ - i64 ", - WAT_PTR_TYPE, - ")))" - )), - direct: false, - conv: "call $js_sys.numeric.128 (@reloc)", - r#type: "i64 i64", - }); - const JS_CONV: Option = Some(OutputJsConv { - embed: Some(("js_sys", "numeric.128.encode")), - pre: "this.#jsEmbed.js_sys['numeric.128.encode'](", - post: ")", - }); - - type Type = Self; - - fn from_raw(raw: Self::Type) -> Self { - raw - } -} - -// SAFETY: Implementation. -unsafe impl Input for i128 { - const WAT_TYPE: &str = Self::Type::WAT_TYPE; - const WAT_CONV: Option = Self::Type::WAT_CONV; - const JS_CONV: Option = Some(InputJsConv { - embed: Some(("js_sys", "numeric.i128.decode")), - pre: " = this.#jsEmbed.js_sys['numeric.i128.decode'](", - post: Some(")"), - }); - - type Type = ExternValue; - - fn into_raw(self) -> Self::Type { - js_bindgen::embed_js!( - module = "js_sys", - name = "numeric.i128.decode", - required_embeds = [ - ("js_sys", "view.getBigUint64"), - ("js_sys", "view.getBigInt64") - ], - "(ptr) => {{", - " const [lo] = this.#jsEmbed.js_sys['view.getBigUint64'](ptr, 1)", - " const [hi] = this.#jsEmbed.js_sys['view.getBigInt64'](ptr + 8, 1)", - " return lo | (hi << 64n)", - "}}", - ); - - ExternValue::new(AlignedValue(self.to_le_bytes())) - } -} - -// SAFETY: Implementation. -unsafe impl Output for i128 { - const WAT_TYPE: &str = WAT_PTR_TYPE; - const WAT_CONV: Option = Some(OutputWatConv { - import: Some(const_concat!( - "(import \"env\" \"js_sys.numeric.128\" (func $js_sys.numeric.128 (@sym) (param i64 \ - i64 ", - WAT_PTR_TYPE, - ")))" - )), - direct: false, - conv: "call $js_sys.numeric.128 (@reloc)", - r#type: "i64 i64", - }); - const JS_CONV: Option = Some(OutputJsConv { - embed: Some(("js_sys", "numeric.128.encode")), - pre: "this.#jsEmbed.js_sys['numeric.128.encode'](", - post: ")", - }); - - type Type = Self; - - fn from_raw(raw: Self::Type) -> Self { - raw - } -} - -#[repr(C, align(8))] -pub struct AlignedValue([u8; 16]); - -const _: () = { - debug_assert!(mem::align_of::>() == 8); -}; - -js_bindgen::embed_js!( - module = "js_sys", - name = "numeric.128.encode", - "(value) => {{", - " const lo = BigInt.asIntN(64, value)", - " const hi = BigInt.asIntN(64, value >> 64n)", - " return [lo, hi]", - "}}", -); - -js_bindgen::unsafe_global_wat!( - "(func $js_sys.numeric.128 (@sym) (param $lo i64) (param $hi i64) (param $out {})", - " (i64.store offset=0 local.get $out local.get $lo)", - " (i64.store offset=8 local.get $out local.get $hi)", - ")", - interpolate WAT_PTR_TYPE, -); diff --git a/client/js-sys/src/string/mod.rs b/client/js-sys/src/string/mod.rs index 74378bff..62f6a181 100644 --- a/client/js-sys/src/string/mod.rs +++ b/client/js-sys/src/string/mod.rs @@ -8,8 +8,7 @@ use core::fmt::{self, Display, Formatter}; pub use self::string::JsString; use crate::JsValue; -use crate::hazard::{Input, InputJsConv, InputWatConv}; -use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; +use crate::util::{PtrConst, PtrLength, PtrMut}; impl JsString { #[must_use] @@ -55,42 +54,6 @@ impl PartialEq for JsString { impl From<&str> for JsString { fn from(value: &str) -> Self { - #[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] - js_bindgen::embed_js!( - module = "js_sys", - name = "string.decode", - "(ptr, len) => {{", - " const decoder = new TextDecoder('utf-8', {{", - " fatal: false,", - " ignoreBOM: false,", - " }})", - " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " return decoder.decode(view)", - "}}", - ); - - #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] - js_bindgen::embed_js!( - module = "js_sys", - name = "string.decode", - required_embeds = [("js_sys", "string.sab")], - "(ptr, len) => {{", - " const decoder = new TextDecoder('utf-8', {{", - " fatal: false,", - " ignoreBOM: false,", - " }})", - " let view", - "", - " if (this.#jsEmbed.js_sys['string.sab']) {{", - " view = new Uint8Array(this.#memory.buffer, ptr, len)", - " }} else {{", - " view = new Uint8Array(this.#memory.buffer).slice(ptr, ptr + len)", - " }}", - "", - " return decoder.decode(view)", - "}}", - ); - // SAFETY: Parameters are correct. unsafe { string::string_decode( @@ -103,19 +66,27 @@ impl From<&str> for JsString { impl From<&JsString> for String { fn from(value: &JsString) -> Self { + js_bindgen::embed_js!( + module = "js_sys", + name = "string.encoder", + "new TextEncoder()", + ); + js_bindgen::embed_js!( module = "js_sys", name = "string.utf8_length", - "(string) => new TextEncoder().encode(string).length", + required_embeds = [("js_sys", "string.encoder")], + "(string) => this.#jsEmbed.js_sys['string.encoder'].encode(string).length", ); #[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] js_bindgen::embed_js!( module = "js_sys", name = "string.encode", + required_embeds = [("js_sys", "string.encoder")], "(string, ptr, len) => {{", " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " new TextEncoder().encodeInto(string, view)", + " this.#jsEmbed.js_sys['string.encoder'].encodeInto(string, view)", "}}", ); @@ -123,13 +94,13 @@ impl From<&JsString> for String { js_bindgen::embed_js!( module = "js_sys", name = "string.encode", - required_embeds = [("js_sys", "string.sab")], + required_embeds = [("js_sys", "string.encoder"), ("js_sys", "string.sab")], "(string, ptr, len) => {{", " if (this.#jsEmbed.js_sys['string.sab']) {{", " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " new TextEncoder().encodeInto(string, view)", + " this.#jsEmbed.js_sys['string.encoder'].encodeInto(string, view)", " }} else {{", - " const bytes = new TextEncoder().encode(string)", + " const bytes = this.#jsEmbed.js_sys['string.encoder'].encode(string)", " new Uint8Array(this.#memory.buffer).set(bytes, ptr)", " }}", "}}", @@ -158,7 +129,8 @@ impl From<&JsString> for String { ); } - // SAFETY: + // SAFETY: `string.encode` initializes exactly `len` bytes with valid + // UTF-8 produced by `TextEncoder`. unsafe { vec.set_len(len); Self::from_utf8_unchecked(vec) @@ -183,30 +155,3 @@ js_bindgen::embed_js!( " }}", "}})()", ); - -// SAFETY: Implementation. -unsafe impl Input for &str { - const WAT_TYPE: &'static str = Self::Type::WAT_TYPE; - const WAT_CONV: Option = Self::Type::WAT_CONV; - const JS_CONV: Option = Some(InputJsConv { - embed: Some(("js_sys", "string.rust.decode")), - pre: " = this.#jsEmbed.js_sys['string.rust.decode'](", - post: Some(")"), - }); - - type Type = ExternSlice; - - fn into_raw(self) -> Self::Type { - js_bindgen::embed_js!( - module = "js_sys", - name = "string.rust.decode", - required_embeds = [("js_sys", "extern_ref"), ("js_sys", "string.decode")], - "(dataPtr) => {{", - " const {{ ptr, len }} = this.#jsEmbed.js_sys['extern_ref'](dataPtr)", - " return this.#jsEmbed.js_sys['string.decode'](ptr, len)", - "}}", - ); - - ExternSlice::new(self.as_bytes()) - } -} diff --git a/client/js-sys/src/string/string.gen.rs b/client/js-sys/src/string/string.gen.rs index b88ed367..28737d27 100644 --- a/client/js-sys/src/string/string.gen.rs +++ b/client/js-sys/src/string/string.gen.rs @@ -3,7 +3,7 @@ #![allow(warnings)] use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{InputJsConv, OutputJsConv, OutputWatConv, Input, InputWatConv, Output, JsCast}; +use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; use crate::util::{PtrConst, PtrLength, PtrMut}; #[derive(Clone, Debug)] @@ -22,42 +22,28 @@ impl From for JsValue { } } -unsafe impl Input for &JsString { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; +unsafe impl JsCast for JsString {} - type Type = <&'static JsValue as Input>::Type; +unsafe impl IntoJS for JsString { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } -unsafe impl JsCast for JsString {} - -unsafe impl Output for JsString { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; +unsafe impl OptionIntoJS for JsString { + type OptionAbi = ::OptionAbi; - type Type = ::Type; - - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } pub(super) fn string_constructor(value: &JsValue) -> JsString { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"string_constructor\" (func $js_sys.import.string_constructor (@sym (name \"js_sys.import.string_constructor\")) (param {}) (result {}))){}", - "(func $js_sys.string_constructor (@sym) (param {}) (param $value {}) (result {})", - " local.get $value{}", " call $js_sys.import.string_constructor (@reloc){}", ")", - interpolate r#macro::wat_input_import_type:: < & JsValue > (), interpolate - r#macro::wat_output_import_type:: < JsString > (), interpolate r#macro::wat_imports!((& - JsValue), JsString), interpolate r#macro::wat_indirect!(JsString), interpolate < & JsValue - as Input > ::WAT_TYPE, interpolate r#macro::wat_direct:: < JsString > (), interpolate - r#macro::wat_input!(& JsValue), interpolate r#macro::wat_output!(JsString), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_constructor", + adapter = "js_sys.string_constructor", inputs = [("arg0", & JsValue)], output = JsString,), } js_bindgen::import_js! { @@ -66,43 +52,36 @@ pub(super) fn string_constructor(value: &JsValue) -> JsString { required_embeds = [ r#macro::js_input_embed::<&JsValue>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}", - interpolate r#macro::js_select!("", "(value) => {\n", (&JsValue), JsString), - interpolate r#macro::js_parameter!("value", &JsValue), - interpolate r#macro::js_output!( - "\treturn ", - "globalThis.String", - "globalThis.String(value)", - JsString, - &JsValue, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "globalThis.String", indirect_call = + "globalThis.String(arg0_0)", inputs = [("arg0", & JsValue)], output = JsString, ), } unsafe extern "C" { #[link_name = "js_sys.string_constructor"] - fn string_constructor(value: <&JsValue as Input>::Type) -> ::Type; + fn string_constructor( + arg0_0: r#macro::InputSlot1<&JsValue>, + arg0_1: r#macro::InputSlot2<&JsValue>, + arg0_2: r#macro::InputSlot3<&JsValue>, + arg0_3: r#macro::InputSlot4<&JsValue>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { string_constructor(Input::into_raw(value)) }) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(value); + unsafe { string_constructor(arg0_0, arg0_1, arg0_2, arg0_3) } + }) } pub(super) unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrLength) -> bool { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"string_eq\" (func $js_sys.import.string_eq (@sym (name \"js_sys.import.string_eq\")) (param {} {} {}) (result {}))){}", - "(func $js_sys.string_eq (@sym) (param {}) (param $string {}) (param $array {}) (param $len {}) (result {})", - " local.get $string{}", " local.get $array{}", " local.get $len{}", - " call $js_sys.import.string_eq (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < & JsString > (), interpolate - r#macro::wat_input_import_type:: < PtrConst < u8 > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < u8 > > (), interpolate - r#macro::wat_output_import_type:: < bool > (), interpolate r#macro::wat_imports!((& - JsString, PtrConst < u8 >, PtrLength < u8 >), bool), interpolate - r#macro::wat_indirect!(bool), interpolate < & JsString as Input > ::WAT_TYPE, interpolate < - PtrConst < u8 > as Input > ::WAT_TYPE, interpolate < PtrLength < u8 > as Input > ::WAT_TYPE, - interpolate r#macro::wat_direct:: < bool > (), interpolate r#macro::wat_input!(& JsString), - interpolate r#macro::wat_input!(PtrConst < u8 >), interpolate r#macro::wat_input!(PtrLength - < u8 >), interpolate r#macro::wat_output!(bool), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_eq", adapter = + "js_sys.string_eq", inputs = [("arg0", & JsString), ("arg1", PtrConst < u8 >), ("arg2", + PtrLength < u8 >)], output = bool,), } js_bindgen::import_js! { @@ -114,56 +93,62 @@ pub(super) unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrL r#macro::js_input_embed::>(), r#macro::js_input_embed::>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(string, array, len) => {\n", - (&JsString, PtrConst, PtrLength), - bool, - ), - interpolate r#macro::js_parameter!("string", &JsString), - interpolate r#macro::js_parameter!("array", PtrConst), - interpolate r#macro::js_parameter!("len", PtrLength), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['string.eq']", - "this.#jsEmbed.js_sys['string.eq'](string, array, len)", - bool, - &JsString, - PtrConst, - PtrLength, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.eq']", indirect_call = + "this.#jsEmbed.js_sys['string.eq'](arg0_0, arg1_0, arg2_0)", inputs = [("arg0", & + JsString), ("arg1", PtrConst < u8 >), ("arg2", PtrLength < u8 >)], output = bool, ), } unsafe extern "C" { #[link_name = "js_sys.string_eq"] fn string_eq( - string: <&JsString as Input>::Type, - array: as Input>::Type, - len: as Input>::Type, - ) -> ::Type; + arg0_0: r#macro::InputSlot1<&JsString>, + arg0_1: r#macro::InputSlot2<&JsString>, + arg0_2: r#macro::InputSlot3<&JsString>, + arg0_3: r#macro::InputSlot4<&JsString>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + arg2_0: r#macro::InputSlot1>, + arg2_1: r#macro::InputSlot2>, + arg2_2: r#macro::InputSlot3>, + arg2_3: r#macro::InputSlot4>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { - string_eq(Input::into_raw(string), Input::into_raw(array), Input::into_raw(len)) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsString>(string); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(array); + let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::>(len); + unsafe { + string_eq( + arg0_0, + arg0_1, + arg0_2, + arg0_3, + arg1_0, + arg1_1, + arg1_2, + arg1_3, + arg2_0, + arg2_1, + arg2_2, + arg2_3, + ) + } }) } pub(super) unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"string_decode\" (func $js_sys.import.string_decode (@sym (name \"js_sys.import.string_decode\")) (param {} {}) (result {}))){}", - "(func $js_sys.string_decode (@sym) (param {}) (param $array {}) (param $len {}) (result {})", - " local.get $array{}", " local.get $len{}", - " call $js_sys.import.string_decode (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < PtrConst < u8 > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < u8 > > (), interpolate - r#macro::wat_output_import_type:: < JsString > (), interpolate - r#macro::wat_imports!((PtrConst < u8 >, PtrLength < u8 >), JsString), interpolate - r#macro::wat_indirect!(JsString), interpolate < PtrConst < u8 > as Input > ::WAT_TYPE, - interpolate < PtrLength < u8 > as Input > ::WAT_TYPE, interpolate r#macro::wat_direct:: < - JsString > (), interpolate r#macro::wat_input!(PtrConst < u8 >), interpolate - r#macro::wat_input!(PtrLength < u8 >), interpolate r#macro::wat_output!(JsString), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_decode", adapter + = "js_sys.string_decode", inputs = [("arg0", PtrConst < u8 >), ("arg1", PtrLength < u8 >)], + output = JsString,), } js_bindgen::import_js! { @@ -174,47 +159,41 @@ pub(super) unsafe fn string_decode(array: PtrConst, len: PtrLength) -> J r#macro::js_input_embed::>(), r#macro::js_input_embed::>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(array, len) => {\n", - (PtrConst, PtrLength), - JsString, - ), - interpolate r#macro::js_parameter!("array", PtrConst), - interpolate r#macro::js_parameter!("len", PtrLength), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['string.decode']", - "this.#jsEmbed.js_sys['string.decode'](array, len)", - JsString, - PtrConst, - PtrLength, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.decode']", indirect_call = + "this.#jsEmbed.js_sys['string.decode'](arg0_0, arg1_0)", inputs = [("arg0", PtrConst < + u8 >), ("arg1", PtrLength < u8 >)], output = JsString, ), } unsafe extern "C" { #[link_name = "js_sys.string_decode"] fn string_decode( - array: as Input>::Type, - len: as Input>::Type, - ) -> ::Type; + arg0_0: r#macro::InputSlot1>, + arg0_1: r#macro::InputSlot2>, + arg0_2: r#macro::InputSlot3>, + arg0_3: r#macro::InputSlot4>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { string_decode(Input::into_raw(array), Input::into_raw(len)) }) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::>(array); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(len); + unsafe { string_decode(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } + }) } pub(super) fn string_utf8_length(string: &JsString) -> f64 { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"string_utf8_length\" (func $js_sys.import.string_utf8_length (@sym (name \"js_sys.import.string_utf8_length\")) (param {}) (result {}))){}", - "(func $js_sys.string_utf8_length (@sym) (param {}) (param $string {}) (result {})", - " local.get $string{}", " call $js_sys.import.string_utf8_length (@reloc){}", ")", - interpolate r#macro::wat_input_import_type:: < & JsString > (), interpolate - r#macro::wat_output_import_type:: < f64 > (), interpolate r#macro::wat_imports!((& - JsString), f64), interpolate r#macro::wat_indirect!(f64), interpolate < & JsString as Input - > ::WAT_TYPE, interpolate r#macro::wat_direct:: < f64 > (), interpolate - r#macro::wat_input!(& JsString), interpolate r#macro::wat_output!(f64), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_utf8_length", + adapter = "js_sys.string_utf8_length", inputs = [("arg0", & JsString)], output = f64,), } js_bindgen::import_js! { @@ -224,41 +203,37 @@ pub(super) fn string_utf8_length(string: &JsString) -> f64 { ("js_sys", "string.utf8_length"), r#macro::js_input_embed::<&JsString>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}", - interpolate r#macro::js_select!("", "(string) => {\n", (&JsString), f64), - interpolate r#macro::js_parameter!("string", &JsString), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['string.utf8_length']", - "this.#jsEmbed.js_sys['string.utf8_length'](string)", - f64, - &JsString, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.utf8_length']", + indirect_call = "this.#jsEmbed.js_sys['string.utf8_length'](arg0_0)", inputs = [("arg0", + & JsString)], output = f64, ), } unsafe extern "C" { #[link_name = "js_sys.string_utf8_length"] - fn string_utf8_length(string: <&JsString as Input>::Type) -> ::Type; + fn string_utf8_length( + arg0_0: r#macro::InputSlot1<&JsString>, + arg0_1: r#macro::InputSlot2<&JsString>, + arg0_2: r#macro::InputSlot3<&JsString>, + arg0_3: r#macro::InputSlot4<&JsString>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { string_utf8_length(Input::into_raw(string)) }) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsString>(string); + unsafe { string_utf8_length(arg0_0, arg0_1, arg0_2, arg0_3) } + }) } pub(super) unsafe fn string_encode(string: &JsString, array: PtrMut, len: PtrLength) { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"string_encode\" (func $js_sys.import.string_encode (@sym (name \"js_sys.import.string_encode\")) (param {} {} {}))){}", - "(func $js_sys.string_encode (@sym) (param $string {}) (param $array {}) (param $len {})", - " local.get $string{}", " local.get $array{}", " local.get $len{}", - " call $js_sys.import.string_encode (@reloc)", ")", interpolate - r#macro::wat_input_import_type:: < & JsString > (), interpolate - r#macro::wat_input_import_type:: < PtrMut < u8 > > (), interpolate - r#macro::wat_input_import_type:: < PtrLength < u8 > > (), interpolate - r#macro::wat_imports!((& JsString, PtrMut < u8 >, PtrLength < u8 >),), interpolate < & - JsString as Input > ::WAT_TYPE, interpolate < PtrMut < u8 > as Input > ::WAT_TYPE, - interpolate < PtrLength < u8 > as Input > ::WAT_TYPE, interpolate r#macro::wat_input!(& - JsString), interpolate r#macro::wat_input!(PtrMut < u8 >), interpolate - r#macro::wat_input!(PtrLength < u8 >), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_encode", adapter + = "js_sys.string_encode", inputs = [("arg0", & JsString), ("arg1", PtrMut < u8 >), ("arg2", + PtrLength < u8 >)],), } js_bindgen::import_js! { @@ -270,30 +245,51 @@ pub(super) unsafe fn string_encode(string: &JsString, array: PtrMut, len: Pt r#macro::js_input_embed::>(), r#macro::js_input_embed::>(), ], - "{}{}{}{}{}", - interpolate r#macro::js_select!( - "", - "(string, array, len) => {\n", - (&JsString, PtrMut, PtrLength), - ), - interpolate r#macro::js_parameter!("string", &JsString), - interpolate r#macro::js_parameter!("array", PtrMut), - interpolate r#macro::js_parameter!("len", PtrLength), - interpolate r#macro::js_select!( - "this.#jsEmbed.js_sys['string.encode']", - "this.#jsEmbed.js_sys['string.encode'](string, array, len)\n}", - (&JsString, PtrMut, PtrLength), + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.encode']", indirect_call = + "this.#jsEmbed.js_sys['string.encode'](arg0_0, arg1_0, arg2_0)", inputs = [("arg0", & + JsString), ("arg1", PtrMut < u8 >), ("arg2", PtrLength < u8 >)], ), } unsafe extern "C" { #[link_name = "js_sys.string_encode"] fn string_encode( - string: <&JsString as Input>::Type, - array: as Input>::Type, - len: as Input>::Type, + arg0_0: r#macro::InputSlot1<&JsString>, + arg0_1: r#macro::InputSlot2<&JsString>, + arg0_2: r#macro::InputSlot3<&JsString>, + arg0_3: r#macro::InputSlot4<&JsString>, + arg1_0: r#macro::InputSlot1>, + arg1_1: r#macro::InputSlot2>, + arg1_2: r#macro::InputSlot3>, + arg1_3: r#macro::InputSlot4>, + arg2_0: r#macro::InputSlot1>, + arg2_1: r#macro::InputSlot2>, + arg2_2: r#macro::InputSlot3>, + arg2_3: r#macro::InputSlot4>, ); } - unsafe { string_encode(Input::into_raw(string), Input::into_raw(array), Input::into_raw(len)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsString>(string); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(array); + let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::>(len); + unsafe { + string_encode( + arg0_0, + arg0_1, + arg0_2, + arg0_3, + arg1_0, + arg1_1, + arg1_2, + arg1_3, + arg2_0, + arg2_1, + arg2_2, + arg2_3, + ) + } + }; } diff --git a/client/js-sys/src/util.rs b/client/js-sys/src/util.rs index b30b4d00..1c0d4af7 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -1,8 +1,7 @@ use core::marker::PhantomData; -use core::mem; use core::mem::MaybeUninit; -use crate::hazard::{Input, InputJsConv, InputWatConv}; +use crate::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; macro_rules! thread_local { ($($vis:vis static $name:ident: $ty:ty = $value:expr;)*) => { @@ -34,41 +33,41 @@ impl LocalKey { } } -#[repr(C)] -pub struct ExternValue(T); - -impl ExternValue { - pub(crate) const WAT_TYPE: &str = WAT_PTR_TYPE; - #[cfg(target_arch = "wasm32")] - pub(crate) const WAT_CONV: Option = None; - #[cfg(target_arch = "wasm64")] - pub(crate) const WAT_CONV: Option = Some(InputWatConv { - import: None, - conv: "f64.convert_i64_u", - r#type: "f64", - }); - - pub(crate) fn new(value: T) -> Self { - Self(value) - } -} - -#[repr(C)] pub struct ExternSlice { ptr: PtrConst, len: PtrLength, } -#[expect(dead_code, reason = "custom sections are considered dead-code")] -impl ExternSlice { - pub(crate) const WAT_TYPE: &str = ExternValue::<()>::WAT_TYPE; - pub(crate) const WAT_CONV: Option = ExternValue::<()>::WAT_CONV; +// SAFETY: `ExternSlice` is represented by `PtrConst` and `PtrLength`. +unsafe impl WasmAbi for ExternSlice { + type Slot1 = PtrConst; + type Slot2 = PtrLength; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + ( + self.ptr, + self.len, + EmptySlot::default(), + EmptySlot::default(), + ) + } - #[cfg(target_arch = "wasm32")] - const VIEW_FN: &str = "view.getUint32"; - #[cfg(target_arch = "wasm64")] - const VIEW_FN: &str = "view.getFloat64"; + fn join( + slot1: Self::Slot1, + slot2: Self::Slot2, + _slot3: Self::Slot3, + _slot4: Self::Slot4, + ) -> Self { + Self { + ptr: slot1, + len: slot2, + } + } +} +impl ExternSlice { pub(crate) fn new(value: &[T]) -> Self { Self { ptr: PtrConst::new(value), @@ -77,78 +76,57 @@ impl ExternSlice { } } -// Verify that we can access `ExternSlice` via a `TypedArray` with two elements. -const _: () = { - debug_assert!( - mem::align_of::>() == mem::size_of::< as Input>::Type>() - ); -}; - -js_bindgen::embed_js!( - module = "js_sys", - name = "extern_ref", - required_embeds = [("js_sys", ExternSlice::<()>::VIEW_FN)], - "(refPtr) => {{", - " const [ptr, len] = this.#jsEmbed.js_sys['{}'](refPtr, 2)", - " return {{ ptr, len }}", - "}}", - interpolate ExternSlice::<()>::VIEW_FN, -); - #[cfg(target_arch = "wasm32")] -type WatUsizeType = u32; +type JsPointerType = u32; #[cfg(target_arch = "wasm64")] -type WatUsizeType = f64; +type JsPointerType = f64; + +pub(crate) const WAT_PTR_TYPE: &str = ::WAT_TYPE; #[cfg(target_arch = "wasm32")] -pub(crate) const WAT_PTR_TYPE: &str = "i32"; +const PTR_INTO_JS_WAT_CONV: Option = None; + #[cfg(target_arch = "wasm64")] -pub(crate) const WAT_PTR_TYPE: &str = "i64"; +const PTR_INTO_JS_WAT_CONV: Option = Some(WatConv { + import: None, + conv: "f64.convert_i64_u", + r#type: "f64", +}); #[repr(transparent)] -pub(crate) struct PtrConst { - ptr: ::Type, - _ty: PhantomData, +pub struct PtrConst { + ptr: *const T, } impl PtrConst { pub(crate) fn new(value: &[T]) -> Self { - let ptr = value.as_ptr(); - - #[cfg(target_arch = "wasm64")] - #[expect( - clippy::cast_precision_loss, - reason = "can't be larger than `MAX_SAFE_INTEGER`" - )] - let ptr = ptr.addr() as ::Type; - Self { - ptr, - _ty: PhantomData, + ptr: value.as_ptr(), } } } -// SAFETY: Delegated to already implemented types. -unsafe impl Input for PtrConst { - const WAT_TYPE: &str = WatUsizeType::WAT_TYPE; - const WAT_CONV: Option = WatUsizeType::WAT_CONV; - const JS_CONV: Option = WatUsizeType::JS_CONV; +// SAFETY: `PtrConst` is transparent over a native Wasm pointer. On `wasm64`, +// the WAT adapter converts it to `f64` without losing precision. +unsafe impl Slot for PtrConst { + const WAT_TYPE: &'static str = WAT_PTR_TYPE; + const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; +} + +// SAFETY: The JavaScript conversion matches the WAT boundary representation. +unsafe impl IntoJS for PtrConst { + const JS_CONV: Option = JsPointerType::JS_CONV; - #[cfg(target_arch = "wasm32")] - type Type = *const T; - #[cfg(target_arch = "wasm64")] - type Type = f64; + type Abi = Self; - fn into_raw(self) -> Self::Type { - self.ptr + fn into_abi(self) -> Self::Abi { + self } } #[repr(transparent)] pub(crate) struct PtrMut { - ptr: ::Type, - _ty: PhantomData, + ptr: *mut T, } impl PtrMut { @@ -165,39 +143,31 @@ impl PtrMut { } fn internal(ptr: *mut T) -> Self { - #[cfg(target_arch = "wasm64")] - #[expect( - clippy::cast_precision_loss, - reason = "can't be larger than `MAX_SAFE_INTEGER`" - )] - let ptr = ptr.addr() as ::Type; - - Self { - ptr, - _ty: PhantomData, - } + Self { ptr } } } -// SAFETY: Delegated to already implemented types. -unsafe impl Input for PtrMut { - const WAT_TYPE: &str = WatUsizeType::WAT_TYPE; - const WAT_CONV: Option = WatUsizeType::WAT_CONV; - const JS_CONV: Option = WatUsizeType::JS_CONV; +// SAFETY: `PtrMut` is transparent over a native Wasm pointer. On `wasm64`, +// the WAT adapter converts it to `f64` without losing precision. +unsafe impl Slot for PtrMut { + const WAT_TYPE: &'static str = WAT_PTR_TYPE; + const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; +} + +// SAFETY: The JavaScript conversion matches the WAT boundary representation. +unsafe impl IntoJS for PtrMut { + const JS_CONV: Option = JsPointerType::JS_CONV; - #[cfg(target_arch = "wasm32")] - type Type = *mut T; - #[cfg(target_arch = "wasm64")] - type Type = f64; + type Abi = Self; - fn into_raw(self) -> Self::Type { - self.ptr + fn into_abi(self) -> Self::Abi { + self } } #[repr(transparent)] -pub(crate) struct PtrLength { - len: ::Type, +pub struct PtrLength { + len: usize, _ty: PhantomData, } @@ -215,13 +185,6 @@ impl PtrLength { } fn internal(len: usize) -> Self { - #[cfg(target_arch = "wasm64")] - #[expect( - clippy::cast_precision_loss, - reason = "can't be larger than `MAX_SAFE_INTEGER`" - )] - let len = len as ::Type; - Self { len, _ty: PhantomData, @@ -229,19 +192,21 @@ impl PtrLength { } } -// SAFETY: Delegated to already implemented types. -unsafe impl Input for PtrLength { - const WAT_TYPE: &str = WatUsizeType::WAT_TYPE; - const WAT_CONV: Option = WatUsizeType::WAT_CONV; - const JS_CONV: Option = WatUsizeType::JS_CONV; +// SAFETY: `PtrLength` is transparent over `usize`. On `wasm64`, the WAT +// adapter converts it to `f64` without losing precision. +unsafe impl Slot for PtrLength { + const WAT_TYPE: &'static str = WAT_PTR_TYPE; + const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; +} + +// SAFETY: The JavaScript conversion matches the WAT boundary representation. +unsafe impl IntoJS for PtrLength { + const JS_CONV: Option = JsPointerType::JS_CONV; - #[cfg(target_arch = "wasm32")] - type Type = usize; - #[cfg(target_arch = "wasm64")] - type Type = f64; + type Abi = Self; - fn into_raw(self) -> Self::Type { - self.len + fn into_abi(self) -> Self::Abi { + self } } diff --git a/client/js-sys/src/value/mod.rs b/client/js-sys/src/value/mod.rs index d5f122a2..cefb07b9 100644 --- a/client/js-sys/src/value/mod.rs +++ b/client/js-sys/src/value/mod.rs @@ -3,11 +3,13 @@ mod value; use core::marker::PhantomData; -use core::mem::MaybeUninit; +use core::mem::{ManuallyDrop, MaybeUninit}; use core::slice; use crate::externref::EXTERNREF_TABLE; -use crate::hazard::{Input, InputWatConv, JsCast, Output, OutputWatConv}; +use crate::hazard::{ + FromJS, FromJsConv, IntoJS, JsCast, OptionIntoJS, ReturnAbi, ReturnMode, Slot, WatConv, +}; #[derive(Debug)] #[repr(transparent)] @@ -16,11 +18,101 @@ pub struct JsValue { _local: PhantomData<*const ()>, } +/// The Wasm `ABI` carrier for an owned `externref` table index. +#[doc(hidden)] +#[repr(transparent)] +pub struct JsValueAbi(i32); + +/// The Wasm `ABI` carrier for a borrowed `externref` table index. +#[doc(hidden)] +#[repr(transparent)] +pub struct JsValueRefAbi(i32); + +/// The Wasm `ABI` carrier for an optional `externref` table index. +#[doc(hidden)] +#[repr(transparent)] +pub struct OptionalJsValueAbi(i32); + +impl Default for JsValueAbi { + fn default() -> Self { + Self(JsValue::UNDEFINED.index) + } +} + +// SAFETY: `JsValueAbi` transfers ownership of an `i32` table index across the +// JS boundary. +unsafe impl Slot for JsValueAbi { + const WAT_TYPE: &'static str = "i32"; + const INTO_JS_WAT_CONV: Option = Some(WatConv { + import: Some( + "(import \"env\" \"js_sys.externref.take\" (func $js_sys.externref.take (@sym) (param \ + i32) (result externref)))", + ), + conv: "call $js_sys.externref.take (@reloc)", + r#type: "externref", + }); + const FROM_JS_WAT_CONV: Option = Some(WatConv { + import: Some( + "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) \ + (param externref) (result i32)))", + ), + conv: "call $js_sys.externref.insert (@reloc)", + r#type: "externref", + }); +} + +// SAFETY: A transparent `i32` carrier is returned directly. +unsafe impl ReturnAbi for JsValueAbi { + const MODE: ReturnMode = ReturnMode::Direct; +} + +// SAFETY: `JsValueRefAbi` borrows an `externref` table entry for the duration +// of the JS call. +unsafe impl Slot for JsValueRefAbi { + const WAT_TYPE: &'static str = "i32"; + const INTO_JS_WAT_CONV: Option = Some(WatConv { + import: Some( + "(import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param \ + i32) (result externref)))", + ), + conv: "call $js_sys.externref.get (@reloc)", + r#type: "externref", + }); +} + +// SAFETY: `OptionalJsValueAbi` is an `i32` table index. At the JS boundary, +// null is represented by index zero and non-null `externref` values are +// inserted into the `externref` table. +unsafe impl Slot for OptionalJsValueAbi { + const WAT_TYPE: &'static str = "i32"; + const INTO_JS_WAT_CONV: Option = Some(WatConv { + import: Some( + "(import \"env\" \"js_sys.externref.take\" (func $js_sys.externref.take (@sym) (param \ + i32) (result externref)))", + ), + conv: "call $js_sys.externref.take (@reloc)", + r#type: "externref", + }); + const FROM_JS_WAT_CONV: Option = Some(WatConv { + import: Some( + "(import \"env\" \"js_sys.optional.js_value\" (func $js_sys.optional.js_value (@sym) \ + (param externref) (result i32)))", + ), + conv: "call $js_sys.optional.js_value (@reloc)", + r#type: "externref", + }); +} + +// SAFETY: A transparent `i32` carrier is returned directly. +unsafe impl ReturnAbi for OptionalJsValueAbi { + const MODE: ReturnMode = ReturnMode::Direct; +} + impl JsValue { pub const UNDEFINED: Self = Self::new(0); pub const NULL: Self = Self::new(1); - const fn new(index: i32) -> Self { + pub(crate) const fn new(index: i32) -> Self { Self { index, _local: PhantomData, @@ -93,48 +185,95 @@ impl Drop for JsValue { } } -// SAFETY: Implementation for all `JsValue`s. -unsafe impl Input for &JsValue { - const WAT_TYPE: &'static str = "i32"; - const WAT_CONV: Option = Some(InputWatConv { - import: Some( - "(import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param \ - i32) (result externref)))", - ), - conv: "call $js_sys.externref.get (@reloc)", - r#type: "externref", - }); +// SAFETY: `JsCast` guarantees that `T` is transparent over `JsValue`, so a +// shared reference has the same `externref` table index `ABI`. +unsafe impl IntoJS for &T { + type Abi = JsValueRefAbi; - type Type = i32; - - fn into_raw(self) -> Self::Type { - self.index + fn into_abi(self) -> Self::Abi { + JsValueRefAbi(self.unchecked_as_ref().index) } } -// SAFETY: The OG type. +// SAFETY: `JsValue` is transparently represented by itself. unsafe impl JsCast for JsValue {} -// SAFETY: Implementation for all `JsValue`s. -unsafe impl Output for JsValue { - const WAT_TYPE: &str = "i32"; - const WAT_CONV: Option = Some(OutputWatConv { - import: Some( - "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) \ - (param externref) (result i32)))", - ), - direct: true, - conv: "call $js_sys.externref.insert (@reloc)", - r#type: "externref", - }); +// SAFETY: The owned table index is transferred to JavaScript and recycled +// after the WAT adapter has loaded its `externref`. +unsafe impl IntoJS for JsValue { + type Abi = JsValueAbi; + + fn into_abi(self) -> Self::Abi { + let value = ManuallyDrop::new(self); + JsValueAbi(value.index) + } +} - type Type = i32; +// SAFETY: `JsCast` guarantees that `T` is transparent over `JsValue`, so an +// `externref` table index can be reconstructed as any `T: JsCast`. +unsafe impl FromJS for T { + type Abi = JsValueAbi; - fn from_raw(raw: Self::Type) -> Self { - Self::new(raw) + fn from_abi(raw: Self::Abi) -> Self { + T::unchecked_from(JsValue::new(raw.0)) } } +// SAFETY: `None` uses the reserved undefined index, which no borrowed +// `JsValue` can produce. +unsafe impl OptionIntoJS for &T { + type OptionAbi = JsValueRefAbi; + + fn option_into_abi(value: Option) -> Self::OptionAbi { + value.map_or(JsValueRefAbi(JsValue::UNDEFINED.index), |value| { + IntoJS::into_abi(value.unchecked_as_ref()) + }) + } +} + +// SAFETY: `None` becomes index zero. A present value transfers its owned table +// index to JavaScript. +unsafe impl OptionIntoJS for JsValue { + type OptionAbi = OptionalJsValueAbi; + + fn option_into_abi(value: Option) -> Self::OptionAbi { + match value { + None => OptionalJsValueAbi(Self::UNDEFINED.index), + Some(value) => { + let JsValueAbi(index) = IntoJS::into_abi(value); + OptionalJsValueAbi(index) + } + } + } +} + +// SAFETY: Null or undefined JS values become index zero; all other `externref` +// values are inserted into the `externref` table and reconstructed as `T`. +unsafe impl FromJS for Option { + const JS_CONV: Option = Some(FromJsConv::slot1("($value) ?? null")); + + type Abi = OptionalJsValueAbi; + + fn from_abi(raw: Self::Abi) -> Self { + (raw.0 != JsValue::UNDEFINED.index).then(|| T::unchecked_from(JsValue::new(raw.0))) + } +} + +js_bindgen::unsafe_global_wat!( + "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) (param \ + externref) (result i32)))", + "(func $js_sys.optional.js_value (@sym) (param $value externref) (result i32)", + " local.get $value", + " ref.is_null", + " if (result i32)", + " i32.const 0", + " else", + " local.get $value", + " call $js_sys.externref.insert (@reloc)", + " end", + ")", +); + impl PartialEq for JsValue { fn eq(&self, other: &Self) -> bool { js_bindgen::embed_js!( diff --git a/client/js-sys/src/value/value.gen.rs b/client/js-sys/src/value/value.gen.rs index cc646c88..58d7eb84 100644 --- a/client/js-sys/src/value/value.gen.rs +++ b/client/js-sys/src/value/value.gen.rs @@ -3,23 +3,14 @@ #![allow(warnings)] use crate::{js_bindgen, r#macro}; -use crate::hazard::{Input, Output}; use super::JsValue; use crate::util::PtrLength; pub(super) fn js_value_partial_eq(value1: &JsValue, value2: &JsValue) -> bool { js_bindgen::unsafe_global_wat! { - "(import \"js_sys\" \"js_value_partial_eq\" (func $js_sys.import.js_value_partial_eq (@sym (name \"js_sys.import.js_value_partial_eq\")) (param {} {}) (result {}))){}", - "(func $js_sys.js_value_partial_eq (@sym) (param {}) (param $value1 {}) (param $value2 {}) (result {})", - " local.get $value1{}", " local.get $value2{}", - " call $js_sys.import.js_value_partial_eq (@reloc){}", ")", interpolate - r#macro::wat_input_import_type:: < & JsValue > (), interpolate - r#macro::wat_input_import_type:: < & JsValue > (), interpolate - r#macro::wat_output_import_type:: < bool > (), interpolate r#macro::wat_imports!((& - JsValue), bool), interpolate r#macro::wat_indirect!(bool), interpolate < & JsValue as Input - > ::WAT_TYPE, interpolate < & JsValue as Input > ::WAT_TYPE, interpolate - r#macro::wat_direct:: < bool > (), interpolate r#macro::wat_input!(& JsValue), interpolate - r#macro::wat_input!(& JsValue), interpolate r#macro::wat_output!(bool), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "js_value_partial_eq", + adapter = "js_sys.js_value_partial_eq", inputs = [("arg0", & JsValue), ("arg1", & JsValue)], + output = bool,), } js_bindgen::import_js! { @@ -29,29 +20,35 @@ pub(super) fn js_value_partial_eq(value1: &JsValue, value2: &JsValue) -> bool { ("js_sys", "js_value.partial_eq"), r#macro::js_input_embed::<&JsValue>(), r#macro::js_output_embed::(), + r#macro::js_result_embed::(), ], - "{}{}{}{}", - interpolate r#macro::js_select!("", "(value1, value2) => {\n", (&JsValue), bool), - interpolate r#macro::js_parameter!("value1", &JsValue), - interpolate r#macro::js_parameter!("value2", &JsValue), - interpolate r#macro::js_output!( - "\treturn ", - "this.#jsEmbed.js_sys['js_value.partial_eq']", - "this.#jsEmbed.js_sys['js_value.partial_eq'](value1, value2)", - bool, - &JsValue, + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['js_value.partial_eq']", + indirect_call = "this.#jsEmbed.js_sys['js_value.partial_eq'](arg0_0, arg1_0)", inputs = + [("arg0", & JsValue), ("arg1", & JsValue)], output = bool, ), } unsafe extern "C" { #[link_name = "js_sys.js_value_partial_eq"] fn js_value_partial_eq( - value1: <&JsValue as Input>::Type, - value2: <&JsValue as Input>::Type, - ) -> ::Type; + arg0_0: r#macro::InputSlot1<&JsValue>, + arg0_1: r#macro::InputSlot2<&JsValue>, + arg0_2: r#macro::InputSlot3<&JsValue>, + arg0_3: r#macro::InputSlot4<&JsValue>, + arg1_0: r#macro::InputSlot1<&JsValue>, + arg1_1: r#macro::InputSlot2<&JsValue>, + arg1_2: r#macro::InputSlot3<&JsValue>, + arg1_3: r#macro::InputSlot4<&JsValue>, + ) -> r#macro::OutputRet; } - Output::from_raw(unsafe { - js_value_partial_eq(Input::into_raw(value1), Input::into_raw(value2)) + r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(value1); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::<&JsValue>(value2); + unsafe { + js_value_partial_eq(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) + } }) } diff --git a/client/js-sys/tests/hazard.rs b/client/js-sys/tests/hazard.rs new file mode 100644 index 00000000..b9c3a2b2 --- /dev/null +++ b/client/js-sys/tests/hazard.rs @@ -0,0 +1,118 @@ +use js_bindgen_test::test; +use js_sys::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; +use js_sys::js_sys; + +js_bindgen::embed_js!( + module = "hazard", + name = "pair", + "(value) => value[0] === 1 && value[1] === 2", +); +js_bindgen::embed_js!( + module = "hazard", + name = "quad", + "(value) => value.length === 4 &&", + "value[0] === 1 && value[1] === 2 && value[2] === 3 && value[3] === 4", +); + +#[repr(transparent)] +struct NumberSlot(u32); + +// SAFETY: `NumberSlot` is an i32 carrier converted to a JS Number on input. +unsafe impl Slot for NumberSlot { + const WAT_TYPE: &'static str = "i32"; + const INTO_JS_WAT_CONV: Option = Some(WatConv { + import: None, + conv: "f64.convert_i32_u", + r#type: "f64", + }); +} + +struct Pair(u32, u32); + +struct Quad(u32, u32, u32, u32); + +// SAFETY: `Pair` is represented by its two `u32` fields in order. +unsafe impl WasmAbi for Pair { + type Slot1 = NumberSlot; + type Slot2 = NumberSlot; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + ( + NumberSlot(self.0), + NumberSlot(self.1), + EmptySlot::new(), + EmptySlot::new(), + ) + } + + fn join(slot1: Self::Slot1, slot2: Self::Slot2, _: Self::Slot3, _: Self::Slot4) -> Self { + Self(slot1.0, slot2.0) + } +} + +// SAFETY: `Pair` lowers to two reusable `NumberSlot` carriers before the JS +// conversion combines them into one logical argument. +unsafe impl IntoJS for Pair { + const JS_CONV: Option = Some(IntoJsConv::new("[$slot1, $slot2]")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +// SAFETY: `Quad` is represented by its four `u32` fields in order. +unsafe impl WasmAbi for Quad { + type Slot1 = NumberSlot; + type Slot2 = NumberSlot; + type Slot3 = NumberSlot; + type Slot4 = NumberSlot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + ( + NumberSlot(self.0), + NumberSlot(self.1), + NumberSlot(self.2), + NumberSlot(self.3), + ) + } + + fn join( + slot1: Self::Slot1, + slot2: Self::Slot2, + slot3: Self::Slot3, + slot4: Self::Slot4, + ) -> Self { + Self(slot1.0, slot2.0, slot3.0, slot4.0) + } +} + +// SAFETY: `Quad` lowers to four reusable `NumberSlot` carriers before the JS +// conversion combines them into one logical argument. +unsafe impl IntoJS for Quad { + const JS_CONV: Option = Some(IntoJsConv::new("[$slot1, $slot2, $slot3, $slot4]")); + + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self + } +} + +#[test] +fn input_slot_conversions() { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "pair")] + fn pair(value: Pair) -> bool; + + #[js_sys(js_embed = "quad")] + fn quad(value: Quad) -> bool; + } + + assert!(pair(Pair(1, 2))); + assert!(quad(Quad(1, 2, 3, 4))); +} diff --git a/client/js-sys/tests/numeric.rs b/client/js-sys/tests/numeric.rs index 58646775..9c66beaa 100644 --- a/client/js-sys/tests/numeric.rs +++ b/client/js-sys/tests/numeric.rs @@ -72,5 +72,15 @@ macro_rules! internal { unsigned!(JsNumber, u8, u16, u32); unsigned!(JsBigInt, u64, u128); +#[cfg(target_arch = "wasm32")] +unsigned!(JsNumber, usize); +#[cfg(target_arch = "wasm64")] +unsigned!(JsBigInt, usize); + signed!(JsNumber, i8, i16, i32); signed!(JsBigInt, i64, i128); + +#[cfg(target_arch = "wasm32")] +signed!(JsNumber, isize); +#[cfg(target_arch = "wasm64")] +signed!(JsBigInt, isize); diff --git a/client/js-sys/tests/optional.rs b/client/js-sys/tests/optional.rs new file mode 100644 index 00000000..233ea0b1 --- /dev/null +++ b/client/js-sys/tests/optional.rs @@ -0,0 +1,124 @@ +#![cfg_attr(target_arch = "wasm64", feature(simd_wasm64))] + +#[cfg(target_arch = "wasm32")] +use core::arch::wasm32 as wasm; +#[cfg(target_arch = "wasm64")] +use core::arch::wasm64 as wasm; + +use js_bindgen_test::test; +use js_sys::{JsArray, JsString, JsValue, js_sys}; + +js_bindgen::embed_js!(module = "optional", name = "test", "(value) => value"); + +macro_rules! assert_roundtrip { + ($($function:ident: $ty:ty => [$($value:expr),+ $(,)?]),+ $(,)?) => { + $( + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "test")] + fn $function(value: Option<$ty>) -> Option<$ty>; + } + + assert_eq!($function(None), None); + $( + assert_eq!($function(Some($value)), Some($value)); + )+ + )+ + }; +} + +#[test] +fn numeric() { + assert_roundtrip! { + bool_option: bool => [false, true], + i8_option: i8 => [i8::MIN, 0, i8::MAX], + u8_option: u8 => [u8::MIN, u8::MAX], + i16_option: i16 => [i16::MIN, 0, i16::MAX], + u16_option: u16 => [u16::MIN, u16::MAX], + i32_option: i32 => [i32::MIN, 0, i32::MAX], + u32_option: u32 => [u32::MIN, u32::MAX], + i64_option: i64 => [i64::MIN, 0, i64::MAX], + u64_option: u64 => [u64::MIN, u64::MAX], + isize_option: isize => [isize::MIN, 0, isize::MAX], + usize_option: usize => [usize::MIN, usize::MAX], + i128_option: i128 => [i128::MIN, -(1_i128 << 64), -1, 0, 1_i128 << 64, i128::MAX], + u128_option: u128 => [u128::MIN, u128::from(u64::MAX), 1_u128 << 64, u128::MAX], + } + + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "test")] + fn f32_option(value: Option) -> Option; + #[js_sys(js_embed = "test")] + fn f64_option(value: Option) -> Option; + } + + assert!(f32_option(None).is_none()); + assert!(f64_option(None).is_none()); + for value in [ + f32::NEG_INFINITY, + f32::MIN, + -0.0, + 0.0, + f32::MAX, + f32::INFINITY, + ] { + assert_eq!(f32_option(Some(value)).unwrap().to_bits(), value.to_bits()); + } + assert!(f32_option(Some(f32::NAN)).unwrap().is_nan()); + + for value in [ + f64::NEG_INFINITY, + f64::MIN, + -0.0, + 0.0, + f64::MAX, + f64::INFINITY, + ] { + assert_eq!(f64_option(Some(value)).unwrap().to_bits(), value.to_bits()); + } + assert!(f64_option(Some(f64::NAN)).unwrap().is_nan()); + + assert_eq!(u128_option(Some(u128::MAX)), Some(u128::MAX)); + assert_eq!(u128_option(None), None); + assert_ne!(wasm::memory_grow::<0>(1), usize::MAX); + assert_eq!(i64_option(Some(i64::MIN)), Some(i64::MIN)); + assert_eq!(u64_option(Some(u64::MAX)), Some(u64::MAX)); + assert_eq!(isize_option(Some(isize::MIN)), Some(isize::MIN)); + assert_eq!(usize_option(Some(usize::MAX)), Some(usize::MAX)); + assert_eq!(u128_option(Some(1_u128 << 64)), Some(1_u128 << 64)); + assert_eq!(i128_option(Some(-1)), Some(-1)); + assert_eq!(i128_option(None), None); +} + +#[test] +fn js_value() { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "test")] + fn js_value_option(value: Option<&JsValue>) -> Option; + + #[js_sys(js_embed = "test")] + fn js_array_option(value: Option<&JsArray>) -> Option; + + #[js_sys(js_embed = "test")] + fn js_string_option(value: Option<&JsString>) -> Option; + } + + assert_eq!(js_value_option(None), None); + assert_eq!(js_value_option(Some(&JsValue::UNDEFINED)), None); + assert_eq!(js_value_option(Some(&JsValue::NULL)), None); + + let string = JsString::from("test"); + let string = js_value_option(Some(string.as_ref())).unwrap(); + assert_eq!(JsString::new(&string), "test"); + + let array = JsArray::from(&[JsValue::UNDEFINED]); + let array = js_array_option(Some(&array)).unwrap(); + assert_eq!(array.length(), 1); + assert!(js_array_option(None).is_none()); + + assert!(js_string_option(None).is_none()); + let string = JsString::from("test"); + assert_eq!(js_string_option(Some(&string)).unwrap(), "test"); +} diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs index e6d01ee6..95122f63 100644 --- a/client/web-sys/src/console.gen.rs +++ b/client/web-sys/src/console.gen.rs @@ -3,122 +3,136 @@ #![allow(warnings)] use js_sys::{js_bindgen, r#macro}; -use js_sys::hazard::{Input, Output}; use js_sys::JsValue; use js_sys::hazard::JsCast; pub fn log0() { js_bindgen::unsafe_global_wat! { - "(import \"web_sys\" \"console.log0\" (func $web_sys.import.console.log0 (@sym (name \"web_sys.import.console.log0\"))))", - "(func $web_sys.console.log0 (@sym)", " call $web_sys.import.console.log0 (@reloc)", ")", + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log0", adapter + = "web_sys.console.log0", inputs = [],), } - js_bindgen::import_js! (module = "web_sys", name = "console.log0", "globalThis.console.log"); + js_bindgen::import_js! { + module = "web_sys", + name = "console.log0", + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "globalThis.console.log", indirect_call = + "globalThis.console.log()", inputs = [], + ), + } unsafe extern "C" { #[link_name = "web_sys.console.log0"] fn log0(); } - unsafe { log0() }; + { unsafe { log0() } }; } pub fn log(data: &[T]) { js_bindgen::unsafe_global_wat! { - "(import \"web_sys\" \"console.log\" (func $web_sys.import.console.log (@sym (name \"web_sys.import.console.log\")) (param {}))){}", - "(func $web_sys.console.log (@sym) (param $data {})", " local.get $data{}", - " call $web_sys.import.console.log (@reloc)", ")", interpolate - r#macro::wat_input_import_type:: < & [JsValue] > (), interpolate r#macro::wat_imports!((& - [JsValue]),), interpolate < & [JsValue] as Input > ::WAT_TYPE, interpolate - r#macro::wat_input!(& [JsValue]), + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log", adapter = + "web_sys.console.log", inputs = [("arg0", & [JsValue])],), } js_bindgen::import_js! { module = "web_sys", name = "console.log", required_embeds = [r#macro::js_input_embed::<&[JsValue]>()], - "{}{}{}", - interpolate r#macro::js_select!("", "(data) => {\n", (&[JsValue])), - interpolate r#macro::js_parameter!("data", &[JsValue]), - interpolate r#macro::js_select!( - "globalThis.console.log", - "globalThis.console.log(data)\n}", - (&[JsValue]), + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "globalThis.console.log", indirect_call = + "globalThis.console.log(arg0_0)", inputs = [("arg0", & [JsValue])], ), } unsafe extern "C" { #[link_name = "web_sys.console.log"] - fn log(data: <&[JsValue] as Input>::Type); + fn log( + arg0_0: r#macro::InputSlot1<&[JsValue]>, + arg0_1: r#macro::InputSlot2<&[JsValue]>, + arg0_2: r#macro::InputSlot3<&[JsValue]>, + arg0_3: r#macro::InputSlot4<&[JsValue]>, + ); } - unsafe { log(Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { + r#macro::split_input_as::<&[JsValue]>(data) + }; + unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } pub fn log2(data1: &JsValue, data2: &JsValue) { js_bindgen::unsafe_global_wat! { - "(import \"web_sys\" \"console.log2\" (func $web_sys.import.console.log2 (@sym (name \"web_sys.import.console.log2\")) (param {} {}))){}", - "(func $web_sys.console.log2 (@sym) (param $data1 {}) (param $data2 {})", - " local.get $data1{}", " local.get $data2{}", - " call $web_sys.import.console.log2 (@reloc)", ")", interpolate - r#macro::wat_input_import_type:: < & JsValue > (), interpolate - r#macro::wat_input_import_type:: < & JsValue > (), interpolate r#macro::wat_imports!((& - JsValue),), interpolate < & JsValue as Input > ::WAT_TYPE, interpolate < & JsValue as Input - > ::WAT_TYPE, interpolate r#macro::wat_input!(& JsValue), interpolate r#macro::wat_input!(& - JsValue), + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log2", adapter + = "web_sys.console.log2", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), } js_bindgen::import_js! { module = "web_sys", name = "console.log2", required_embeds = [r#macro::js_input_embed::<&JsValue>()], - "{}{}{}{}", - interpolate r#macro::js_select!("", "(data1, data2) => {\n", (&JsValue)), - interpolate r#macro::js_parameter!("data1", &JsValue), - interpolate r#macro::js_parameter!("data2", &JsValue), - interpolate r#macro::js_select!( - "globalThis.console.log", - "globalThis.console.log(data1, data2)\n}", - (&JsValue), + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "globalThis.console.log", indirect_call = + "globalThis.console.log(arg0_0, arg1_0)", inputs = [("arg0", & JsValue), ("arg1", & + JsValue)], ), } unsafe extern "C" { #[link_name = "web_sys.console.log2"] - fn log2(data1: <&JsValue as Input>::Type, data2: <&JsValue as Input>::Type); + fn log2( + arg0_0: r#macro::InputSlot1<&JsValue>, + arg0_1: r#macro::InputSlot2<&JsValue>, + arg0_2: r#macro::InputSlot3<&JsValue>, + arg0_3: r#macro::InputSlot4<&JsValue>, + arg1_0: r#macro::InputSlot1<&JsValue>, + arg1_1: r#macro::InputSlot2<&JsValue>, + arg1_2: r#macro::InputSlot3<&JsValue>, + arg1_3: r#macro::InputSlot4<&JsValue>, + ); } - unsafe { log2(Input::into_raw(data1), Input::into_raw(data2)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(data1); + let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::<&JsValue>(data2); + unsafe { log2(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } + }; } pub fn error(data: &JsValue) { js_bindgen::unsafe_global_wat! { - "(import \"web_sys\" \"console.error\" (func $web_sys.import.console.error (@sym (name \"web_sys.import.console.error\")) (param {}))){}", - "(func $web_sys.console.error (@sym) (param $data {})", " local.get $data{}", - " call $web_sys.import.console.error (@reloc)", ")", interpolate - r#macro::wat_input_import_type:: < & JsValue > (), interpolate r#macro::wat_imports!((& - JsValue),), interpolate < & JsValue as Input > ::WAT_TYPE, interpolate r#macro::wat_input!(& - JsValue), + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.error", adapter + = "web_sys.console.error", inputs = [("arg0", & JsValue)],), } js_bindgen::import_js! { module = "web_sys", name = "console.error", required_embeds = [r#macro::js_input_embed::<&JsValue>()], - "{}{}{}", - interpolate r#macro::js_select!("", "(data) => {\n", (&JsValue)), - interpolate r#macro::js_parameter!("data", &JsValue), - interpolate r#macro::js_select!( - "globalThis.console.error", - "globalThis.console.error(data)\n}", - (&JsValue), + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "globalThis.console.error", indirect_call = + "globalThis.console.error(arg0_0)", inputs = [("arg0", & JsValue)], ), } unsafe extern "C" { #[link_name = "web_sys.console.error"] - fn error(data: <&JsValue as Input>::Type); + fn error( + arg0_0: r#macro::InputSlot1<&JsValue>, + arg0_1: r#macro::InputSlot2<&JsValue>, + arg0_2: r#macro::InputSlot3<&JsValue>, + arg0_3: r#macro::InputSlot4<&JsValue>, + ); } - unsafe { error(Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(data); + unsafe { error(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } diff --git a/host/Cargo.toml b/host/Cargo.toml index 8e7a6f94..7e64b1e5 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -3,6 +3,7 @@ resolver = "3" members = [ "cargo-js-sys", "cargo-shim", + "cli", "cli-lib", "dev", "inline-snap", diff --git a/host/cli-lib/Cargo.toml b/host/cli-lib/Cargo.toml index caae29f6..dfa14b23 100644 --- a/host/cli-lib/Cargo.toml +++ b/host/cli-lib/Cargo.toml @@ -15,7 +15,6 @@ test = false anyhow = { workspace = true } foldhash = { workspace = true } hashbrown = { workspace = true, features = ["default-hasher", "serde"] } -itertools = { workspace = true } serde = { workspace = true, features = ["alloc", "derive"] } wasmparser = { workspace = true } diff --git a/host/cli-lib/src/js/imports.d.mts b/host/cli-lib/src/js/imports.d.mts index 325c27f6..30edea0a 100644 --- a/host/cli-lib/src/js/imports.d.mts +++ b/host/cli-lib/src/js/imports.d.mts @@ -1,8 +1,12 @@ +export type JsBindgenInstance = { + instance: WebAssembly.Instance; + exports: WebAssembly.Instance["exports"]; +}; export declare class JsBindgen { #private; constructor(module: WebAssembly.Module, memory?: WebAssembly.Memory); get importObject(): WebAssembly.Imports; extendImportObject(imports: WebAssembly.Imports): void; - instantiate(): Promise; - static instantiateStreaming(...args: Parameters | []): Promise; + instantiate(): Promise; + static instantiateStreaming(...args: Parameters | []): Promise; } diff --git a/host/cli-lib/src/js/imports.mjs b/host/cli-lib/src/js/imports.mjs index 0a26804b..f764a718 100644 --- a/host/cli-lib/src/js/imports.mjs +++ b/host/cli-lib/src/js/imports.mjs @@ -1,5 +1,8 @@ export class JsBindgen { #finished = false; + // @ts-expect-error: Used by generated imports that catch exceptions. + // eslint-disable-next-line no-unused-private-class-members + #instance; #importObject; // @ts-expect-error: Used in placeholder. // eslint-disable-next-line no-unused-private-class-members, @typescript-eslint/no-explicit-any @@ -53,8 +56,14 @@ export class JsBindgen { throw new Error("create a new `JsBindgen` class"); } return WebAssembly.instantiate(this.#module, this.#importObject).then(instance => { + this.#instance = instance; this.#finished = true; - return instance; + const jsExports = JBG_PLACEHOLDER_JS_EXPORT; + const exports = Object.assign(Object.create(null), instance.exports, jsExports); + return { + instance, + exports, + }; }); } static async instantiateStreaming(...args) { diff --git a/host/cli-lib/src/js/imports.mts b/host/cli-lib/src/js/imports.mts index 1fb18279..89e22f22 100644 --- a/host/cli-lib/src/js/imports.mts +++ b/host/cli-lib/src/js/imports.mts @@ -2,9 +2,18 @@ declare const JBG_PLACEHOLDER_MEMORY: WebAssembly.Memory // eslint-disable-next-line @typescript-eslint/no-explicit-any declare const JBG_PLACEHOLDER_JS_EMBED: Record> declare const JBG_PLACEHOLDER_IMPORT_OBJECT: WebAssembly.Imports +declare const JBG_PLACEHOLDER_JS_EXPORT: WebAssembly.Instance["exports"] + +export type JsBindgenInstance = { + instance: WebAssembly.Instance + exports: WebAssembly.Instance["exports"] +} export class JsBindgen { #finished = false + // @ts-expect-error: Used by generated imports that catch exceptions. + // eslint-disable-next-line no-unused-private-class-members + #instance: WebAssembly.Instance #importObject: WebAssembly.Imports // @ts-expect-error: Used in placeholder. // eslint-disable-next-line no-unused-private-class-members, @typescript-eslint/no-explicit-any @@ -61,20 +70,31 @@ export class JsBindgen { } } - async instantiate(): Promise { + async instantiate(): Promise { if (this.#finished) { throw new Error("create a new `JsBindgen` class") } return WebAssembly.instantiate(this.#module, this.#importObject).then(instance => { + this.#instance = instance this.#finished = true - return instance + + const jsExports = JBG_PLACEHOLDER_JS_EXPORT + const exports = Object.assign( + Object.create(null) as WebAssembly.Instance["exports"], + instance.exports, + jsExports + ) + return { + instance, + exports, + } }) } static async instantiateStreaming( ...args: Parameters | [] - ): Promise { + ): Promise { let response if (args.length === 0) { diff --git a/host/cli-lib/src/lib.rs b/host/cli-lib/src/lib.rs index 0f6ff164..537e6c22 100644 --- a/host/cli-lib/src/lib.rs +++ b/host/cli-lib/src/lib.rs @@ -6,7 +6,6 @@ use std::ops::Deref; use anyhow::Result; use foldhash::fast::FixedState; use hashbrown::HashMap; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use wasmparser::MemoryType; @@ -20,6 +19,7 @@ pub struct JsOutput<'a, T: Deref + Display + Eq + Hash + Serialize pub main_memory: MainMemory<'a>, pub js_import: FixedHashMap>, pub js_embed: FixedHashMap>, + pub js_export: FixedHashMap, } #[derive(Clone, Copy, Deserialize, Serialize)] @@ -34,7 +34,9 @@ impl + Display + Eq + Hash + Serialize> JsOutput<'_, T> { let (js_file_memory, rest) = IMPORTS_JS.split_once("JBG_PLACEHOLDER_MEMORY").unwrap(); let (js_file_embed, rest) = rest.split_once("JBG_PLACEHOLDER_JS_EMBED").unwrap(); - let (js_file_import, js_file_4) = rest.split_once("JBG_PLACEHOLDER_IMPORT_OBJECT").unwrap(); + let (js_file_import, rest) = rest.split_once("JBG_PLACEHOLDER_IMPORT_OBJECT").unwrap(); + let (js_file_export, js_file_finish) = + rest.split_once("JBG_PLACEHOLDER_JS_EXPORT").unwrap(); // `WebAssembly.Memory`. output.write_all(js_file_memory.as_bytes())?; @@ -75,19 +77,7 @@ impl + Display + Eq + Hash + Serialize> JsOutput<'_, T> { for (name, js) in embeds { write!(output, "\t\t\t\t'{name}': ")?; - - for (position, line) in js.lines().with_position() { - if position.is_middle() || position.is_last() { - if line.is_empty() { - output.write_all(b"\n")?; - } else { - output.write_all(b"\n\t\t\t\t")?; - } - } - - output.write_all(line.as_bytes())?; - } - + write_indented_js(&mut output, js, b"\t\t\t\t")?; output.write_all(b",\n")?; } @@ -111,19 +101,7 @@ impl + Display + Eq + Hash + Serialize> JsOutput<'_, T> { for (name, js) in names { write!(output, "\t\t\t\t'{name}': ")?; - - for (position, line) in js.lines().with_position() { - if position.is_middle() || position.is_last() { - if line.is_empty() { - output.write_all(b"\n")?; - } else { - output.write_all(b"\n\t\t\t\t")?; - } - } - - output.write_all(line.as_bytes())?; - } - + write_indented_js(&mut output, js, b"\t\t\t\t")?; output.write_all(b",\n")?; } @@ -132,9 +110,41 @@ impl + Display + Eq + Hash + Serialize> JsOutput<'_, T> { output.write_all(b"\t\t}")?; - // Finish - output.write_all(js_file_4.as_bytes())?; + // JS export wrappers. + output.write_all(js_file_export.as_bytes())?; + output.write_all(b"{\n")?; + + for (name, js) in &self.js_export { + write!(output, " '{name}': ")?; + write_indented_js(&mut output, js, b" ")?; + output.write_all(b",\n")?; + } + + output.write_all(b" }")?; + + // Finish. + output.write_all(js_file_finish.as_bytes())?; Ok(()) } } + +fn write_indented_js( + output: &mut impl Write, + js: &str, + continuation_indent: &[u8], +) -> std::io::Result<()> { + for (index, line) in js.lines().enumerate() { + if index != 0 { + output.write_all(b"\n")?; + + if !line.is_empty() { + output.write_all(continuation_indent)?; + } + } + + output.write_all(line.as_bytes())?; + } + + Ok(()) +} diff --git a/host/cli/Cargo.toml b/host/cli/Cargo.toml new file mode 100644 index 00000000..ce9d0692 --- /dev/null +++ b/host/cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "js-bindgen-cli" +version = "0.1.0" +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +include = { workspace = true } + +[[bin]] +bench = false +name = "js-bindgen" +path = "src/main.rs" +test = false + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +js-bindgen-cli-lib = { workspace = true } +js-bindgen-shared = { workspace = true, features = ["memmap"] } +postcard = { workspace = true } +wasm-encoder = { workspace = true } +wasmparser = { workspace = true } + +[lints] +workspace = true diff --git a/host/cli/src/main.rs b/host/cli/src/main.rs new file mode 100644 index 00000000..df468c10 --- /dev/null +++ b/host/cli/src/main.rs @@ -0,0 +1,145 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::Parser as _; +use js_bindgen_cli_lib::{JS_OUTPUT_SECTION, JsOutput}; +use js_bindgen_shared::ReadFile; +use wasm_encoder::{Module, RawSection}; +use wasmparser::{MemoryType, Parser, Payload, TypeRef}; + +#[derive(clap::Parser)] +#[command(name = "js-bindgen", version, about, long_about = None)] +struct Cli { + /// Final linked Wasm artifact containing js-bindgen `metadata`. + input: PathBuf, + + /// Directory in which to write the generated `.wasm` and `.mjs` files. + #[arg(short, long)] + out_dir: PathBuf, + + /// Keep custom sections other than `js_bindgen.js_output` in the output + /// Wasm. + #[arg(long)] + keep_custom_sections: bool, +} + +fn main() -> Result<()> { + Cli::parse().run() +} + +impl Cli { + fn run(self) -> Result<()> { + let input = ReadFile::new(&self.input) + .with_context(|| format!("failed to read Wasm file: {}", self.input.display()))?; + let output = process(&input, self.keep_custom_sections)?; + let file_name = self + .input + .file_name() + .context("input path must have a file name")?; + + fs::create_dir_all(&self.out_dir).with_context(|| { + format!( + "failed to create output directory: {}", + self.out_dir.display() + ) + })?; + + let wasm_path = self.out_dir.join(file_name); + let js_path = wasm_path.with_extension("mjs"); + + fs::write(&wasm_path, output.wasm) + .with_context(|| format!("failed to write Wasm file: {}", wasm_path.display()))?; + fs::write(&js_path, output.js) + .with_context(|| format!("failed to write JS file: {}", js_path.display()))?; + + println!("{}", wasm_path.display()); + println!("{}", js_path.display()); + + Ok(()) + } +} + +struct Output { + wasm: Vec, + js: Vec, +} + +struct Memory<'a> { + module: &'a str, + name: &'a str, + data: MemoryType, +} + +fn process(input: &[u8], keep_custom_sections: bool) -> Result { + let mut module = Module::new(); + let mut js_output = None; + let mut memories = Vec::new(); + + for payload in Parser::new(0).parse_all(input) { + let payload = payload.context("input should be valid Wasm")?; + let section = payload.as_section(); + + match payload { + Payload::ImportSection(imports) => { + for import in imports.into_imports() { + let import = import.context("import should be parsable")?; + + if let TypeRef::Memory(data) = import.ty { + memories.push(Memory { + module: import.module, + name: import.name, + data, + }); + } + } + + copy_section(&mut module, input, section)?; + } + Payload::CustomSection(custom) => { + if custom.name() == JS_OUTPUT_SECTION { + js_output = Some( + postcard::from_bytes(custom.data()) + .context("JS output section should be valid")?, + ); + } else if keep_custom_sections { + copy_section(&mut module, input, section)?; + } + } + Payload::Version { .. } | Payload::CodeSectionEntry(_) | Payload::End(_) => {} + _ => copy_section(&mut module, input, section)?, + } + } + + let js_output: JsOutput<&str> = js_output.context("unable to find JS output section")?; + let main_memory = memories + .iter() + .find(|memory| { + memory.module == js_output.main_memory.module + && memory.name == js_output.main_memory.name + }) + .context("unable to find the encoded main memory import")?; + let mut js = Vec::new(); + + js_output.js(&mut js, main_memory.data)?; + + Ok(Output { + wasm: module.finish(), + js, + }) +} + +fn copy_section( + output: &mut Module, + input: &[u8], + section: Option<(u8, core::ops::Range)>, +) -> Result<()> { + let (id, range) = section.context("expected a complete Wasm section")?; + + output.section(&RawSection { + id, + data: &input[range], + }); + + Ok(()) +} diff --git a/host/dev/Cargo.toml b/host/dev/Cargo.toml index 06424ca5..25727f05 100644 --- a/host/dev/Cargo.toml +++ b/host/dev/Cargo.toml @@ -15,6 +15,7 @@ cargo_metadata = { workspace = true } clap = { workspace = true } paste = { workspace = true } strum = { workspace = true } +tempfile = { workspace = true } [dev-dependencies] cargo_metadata = { workspace = true, features = ["builder"] } diff --git a/host/dev/src/client/e2e.rs b/host/dev/src/client/e2e.rs new file mode 100644 index 00000000..5e292964 --- /dev/null +++ b/host/dev/src/client/e2e.rs @@ -0,0 +1,220 @@ +use std::fmt::Write as _; +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, bail, ensure}; +use cargo_metadata::{Artifact, Message, TargetKind}; +use tempfile::TempDir; + +use super::permutation::Permutation; +use super::test::Engine; +use super::util; +use crate::command; + +pub struct E2e { + _dir: TempDir, + examples: Vec, +} + +struct Example { + dir: PathBuf, + name: String, + tests: Vec, +} + +impl E2e { + pub fn build( + permutation: &Permutation, + nightly_toolchain: &str, + verbose: bool, + ) -> Result<(Self, Duration)> { + let start = Instant::now(); + let mut command = util::cargo(permutation, nightly_toolchain, "build"); + command + .args(["-p", "js-bindgen-e2e", "--examples"]) + .arg("--message-format=json-render-diagnostics"); + let output = command.output().context("failed to build E2E examples")?; + let mut artifacts = Vec::new(); + + for message in Message::parse_stream(Cursor::new(output.stdout)) { + match message? { + Message::CompilerArtifact(artifact) + if artifact.target.kind.contains(&TargetKind::Example) => + { + artifacts.push(artifact); + } + Message::CompilerMessage(message) if verbose => { + if let Some(rendered) = message.message.rendered { + eprint!("{rendered}"); + } + } + _ => {} + } + } + + if !output.status.success() { + if !output.stderr.is_empty() { + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + } + + bail!("building E2E examples failed with {}", output.status); + } + + ensure!(!artifacts.is_empty(), "no E2E examples were built"); + let dir = tempfile::tempdir().context("failed to create E2E output directory")?; + let mut examples = Vec::with_capacity(artifacts.len()); + + for artifact in artifacts { + examples.push(Example::build(artifact, dir.path(), verbose)?); + } + + examples.sort_unstable_by(|left, right| left.name.cmp(&right.name)); + + Ok(( + Self { + _dir: dir, + examples, + }, + start.elapsed(), + )) + } + + pub fn run( + &self, + engine: Engine, + node_js_arg: Option<&str>, + verbose: bool, + ) -> Result { + let mut duration = Duration::ZERO; + + for example in &self.examples { + let script = example.script(engine); + let script_path = example.dir.join("test.mjs"); + fs::write(&script_path, script).with_context(|| { + format!("failed to write E2E script: {}", script_path.display()) + })?; + + let mut command = Command::new(engine.binary()); + + match engine { + Engine::Deno => { + command.args(["run", "--allow-read"]); + } + Engine::NodeJs => { + command.args(node_js_arg); + } + Engine::Bun => { + command.arg("run"); + } + } + + command.arg(script_path); + duration += command::run( + &format!("E2E `{}` - {engine}", example.name), + command, + verbose, + )?; + } + + Ok(duration) + } +} + +impl Example { + fn build(artifact: Artifact, output: &Path, verbose: bool) -> Result { + let wasm = artifact + .filenames + .iter() + .find(|path| path.extension() == Some("wasm")) + .with_context(|| { + format!( + "E2E example `{}` did not produce a Wasm artifact", + artifact.target.name + ) + })?; + let source = fs::read_to_string(&artifact.target.src_path) + .with_context(|| format!("failed to read E2E example: {}", artifact.target.src_path))?; + let tests: Vec<_> = source + .lines() + .filter_map(|line| line.trim_start().strip_prefix("// ;;")) + .map(str::trim) + .map(str::to_owned) + .collect(); + + ensure!( + !tests.is_empty(), + "E2E example `{}` has no `// ;;` tests", + artifact.target.name + ); + ensure!( + tests.iter().all(|test| !test.is_empty()), + "E2E example `{}` contains an empty `// ;;` test", + artifact.target.name + ); + + let dir = output.join(&artifact.target.name); + fs::create_dir(&dir) + .with_context(|| format!("failed to create E2E directory: {}", dir.display()))?; + let mut command = if std::env::var_os("JBG_DEV_TOOLS").is_some_and(|value| value == "1") { + Command::new("js-bindgen") + } else { + let mut command = Command::new("cargo"); + command + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap()) + .args(["+stable", "run", "-q", "-p", "js-bindgen-cli", "--"]); + command + }; + command.arg(wasm).arg("--out-dir").arg(&dir); + command::run( + &format!("Generate E2E `{}`", artifact.target.name), + command, + verbose, + )?; + + Ok(Self { + dir, + name: artifact.target.name, + tests, + }) + } + + fn script(&self, engine: Engine) -> String { + let read = match engine { + Engine::Deno => { + format!( + "const bytes = await Deno.readFile(new URL('./{}.wasm', import.meta.url))", + self.name + ) + } + Engine::NodeJs => format!( + "const {{ readFile }} = await import('node:fs/promises')\nconst bytes = await \ + readFile(new URL('./{}.wasm', import.meta.url))", + self.name + ), + Engine::Bun => format!( + "const bytes = await Bun.file(new URL('./{}.wasm', import.meta.url)).arrayBuffer()", + self.name + ), + }; + let mut script = format!( + "import {{ JsBindgen }} from './{}.mjs'\n\n{read}\nconst module = await \ + WebAssembly.compile(bytes)\nconst {{ exports }} = await new \ + JsBindgen(module).instantiate()\n\nfunction assert(value, expression) {{\n if \ + (!value) throw new Error(`assertion failed: ${{expression}}`)\n}}\n", + self.name + ); + + for test in &self.tests { + script.push_str("\nassert("); + script.push_str(test); + script.push_str(", "); + write!(script, "{test:?}").unwrap(); + script.push_str(")\n"); + } + + script + } +} diff --git a/host/dev/src/client/metadata.rs b/host/dev/src/client/metadata.rs index 1965969a..4c658744 100644 --- a/host/dev/src/client/metadata.rs +++ b/host/dev/src/client/metadata.rs @@ -27,6 +27,7 @@ pub fn run( for CargoTarget { kind, + package, name, features, js_sys, @@ -45,11 +46,11 @@ pub fn run( let announce = match kind { TargetKind::Lib => { - command.args(["-p", name]); + command.args(["-p", package]); *title } TargetKind::Example => { - command.args(["--example", name]); + command.args(["-p", package, "--example", name]); &format!("{title} Example") } _ => unreachable!(), @@ -78,6 +79,7 @@ pub fn run( struct CargoTarget<'m> { kind: TargetKind, + package: &'m str, name: &'m str, features: Features<'m>, js_sys: bool, @@ -100,6 +102,7 @@ impl<'m> CargoTarget<'m> { for features in &feature_combinations { targets.push(Self { kind: TargetKind::Lib, + package: &package.name, name: &package.name, features: features.clone(), js_sys, @@ -116,6 +119,7 @@ impl<'m> CargoTarget<'m> { if let TargetKind::Example = kind { targets.push(Self { kind: TargetKind::Example, + package: &package.name, name: &target.name, features: Features::Default, js_sys, diff --git a/host/dev/src/client/mod.rs b/host/dev/src/client/mod.rs index c99997eb..2c84b7e8 100644 --- a/host/dev/src/client/mod.rs +++ b/host/dev/src/client/mod.rs @@ -1,4 +1,5 @@ mod check; +mod e2e; mod fmt; mod metadata; mod permutation; @@ -166,6 +167,7 @@ enum TargetFeature { #[default] Default, Atomics, + ExceptionHandling, } impl Target { @@ -185,14 +187,18 @@ impl Target { fn toolchain(self, target_feature: TargetFeature) -> Toolchain { match (self, target_feature) { - (Self::Wasm64, _) | (_, TargetFeature::Atomics) => Toolchain::Nightly, + (Self::Wasm64, _) | (_, TargetFeature::Atomics | TargetFeature::ExceptionHandling) => { + Toolchain::Nightly + } (Self::Wasm32, TargetFeature::Default) => Toolchain::Any, } } fn args(self, target_feature: TargetFeature) -> &'static [&'static str] { match (self, target_feature) { - (Self::Wasm32, TargetFeature::Default) => &["--target", "wasm32-unknown-unknown"], + (Self::Wasm32, TargetFeature::Default | TargetFeature::ExceptionHandling) => { + &["--target", "wasm32-unknown-unknown"] + } (Self::Wasm32, TargetFeature::Atomics) => &[ "--target", "wasm32-unknown-unknown", @@ -221,12 +227,13 @@ impl TargetFeature { match self { Self::Default => None, Self::Atomics => Some("-Ctarget-feature=+atomics"), + Self::ExceptionHandling => Some("-Ctarget-feature=+exception-handling"), } } fn supports_atomics(self) -> bool { match self { - Self::Default => false, + Self::Default | Self::ExceptionHandling => false, Self::Atomics => true, } } @@ -237,6 +244,7 @@ impl Display for TargetFeature { match self { Self::Default => Ok(()), Self::Atomics => f.write_str("Atomics"), + Self::ExceptionHandling => f.write_str("Exception Handling"), } } } diff --git a/host/dev/src/client/test.rs b/host/dev/src/client/test.rs index 05c98899..2c897f79 100644 --- a/host/dev/src/client/test.rs +++ b/host/dev/src/client/test.rs @@ -10,6 +10,7 @@ use clap::builder::{ArgPredicate, PossibleValue}; use clap::{Args, ValueEnum}; use strum::{EnumCount, EnumIter, IntoEnumIterator}; +use super::e2e::E2e; use super::permutation::{JsSysTargetFeature, Permutation, Profile}; use super::process::ChildWrapper; use super::{ClientArgs, Target, TargetFeature, util}; @@ -66,7 +67,6 @@ impl Test { } else { unreachable!() }; - let tools_installed = env::var_os("JBG_DEV_TOOLS").is_some_and(|value| value == "1"); let start = Instant::now(); @@ -122,9 +122,10 @@ impl Test { } for permutation in Permutation::iter(&targets, Profile::Test, &target_features, true) { + let test_runs: Vec<_> = TestRun::from_permutation(&permutation, &runners).collect(); let mut built = false; - for test_run in TestRun::from_permutation(&permutation, &runners) { + for test_run in &test_runs { if !built { let mut command = util::cargo(&permutation, &self.args.nightly_toolchain, "test"); @@ -147,6 +148,21 @@ impl Test { test_time += command::run(&format!("Run Tests - {test_run}"), command, verbose)?; } + + if test_runs + .iter() + .any(|test_run| matches!(test_run.runner, Runner::Engine(_))) + { + let (e2e, duration) = + E2e::build(&permutation, &self.args.nightly_toolchain, verbose)?; + build_time += duration; + + for test_run in &test_runs { + if let Runner::Engine(engine) = test_run.runner { + test_time += e2e.run(engine, test_run.node_js_arg, verbose)?; + } + } + } } for web_driver in web_drivers { @@ -308,7 +324,7 @@ impl ValueEnum for Runner { } #[derive(Clone, Copy, EnumCount, EnumIter, Eq, PartialEq)] -enum Engine { +pub(super) enum Engine { Deno, NodeJs, Bun, @@ -323,7 +339,7 @@ impl Engine { } } - fn binary(self) -> &'static str { + pub(super) fn binary(self) -> &'static str { match self { Self::Deno => "deno", Self::NodeJs => "node", diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs new file mode 100644 index 00000000..265e2803 --- /dev/null +++ b/host/js-sys-bindgen/src/export.rs @@ -0,0 +1,238 @@ +use std::env; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote_spanned}; +use syn::ext::IdentExt; +use syn::parse::Parser; +use syn::spanned::Spanned; +use syn::{Error, FnArg, ItemFn, LitStr, Path, ReturnType, Type, meta, parse_quote}; + +pub(crate) fn r#macro( + attr: TokenStream, + function: &ItemFn, + crate_: Option<&str>, +) -> Result { + let mut js_sys: Option = None; + + meta::parser(|meta| { + if meta.path.is_ident("js_sys") { + if js_sys.is_some() { + Err(meta.error("duplicate `js_sys` argument")) + } else { + js_sys = Some(meta.value()?.parse()?); + Ok(()) + } + } else { + Err(meta.error("unsupported attribute")) + } + }) + .parse2(attr)?; + + validate(function)?; + + let span = function.span(); + let js_sys: Path = js_sys.unwrap_or_else(|| parse_quote!(::js_sys)); + let js_bindgen: Path = parse_quote!(#js_sys::js_bindgen); + let r#macro: Path = parse_quote!(#js_sys::r#macro); + let ident = &function.sig.ident; + let export_name_value = ident.unraw().to_string(); + let export_name = LitStr::new(&export_name_value, ident.span()); + let crate_name = crate_.map_or_else( + || env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found"), + str::to_owned, + ); + let crate_name = LitStr::new(&crate_name, span); + let output_ty = match &function.sig.output { + ReturnType::Type(_, ty) if matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty()) => { + None + } + ReturnType::Type(_, ty) => Some(ty.as_ref()), + ReturnType::Default => None, + }; + let mut raw_inputs = Vec::new(); + let mut join_inputs = Vec::new(); + let mut arguments = Vec::new(); + let mut codegen_inputs = Vec::new(); + let mut required_embeds = Vec::new(); + + for (index, input) in function.sig.inputs.iter().enumerate() { + let FnArg::Typed(input) = input else { + unreachable!(); + }; + let ty = &input.ty; + let argument = format_ident!("arg{index}", span = input.span()); + let parameter = LitStr::new(&argument.to_string(), input.span()); + let reference = match ty.as_ref() { + Type::Reference(reference) if reference.mutability.is_some() => { + return Err(Error::new_spanned( + reference, + "mutable references are not supported", + )); + } + Type::Reference(reference) => Some(reference), + _ => None, + }; + let js_ty = reference.map_or_else( + || quote_spanned!(input.span()=> #ty), + |reference| { + let ty = &reference.elem; + quote_spanned! {input.span()=> + <#ty as #js_sys::hazard::RefFromJS>::Anchor + } + }, + ); + let mut slots = Vec::new(); + + for slot in 1_usize..=4 { + let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = input.span()); + let slot_alias = format_ident!("OutputSlot{slot}", span = input.span()); + + raw_inputs.push(quote_spanned! {input.span()=> + #slot_ident: #r#macro::#slot_alias<#js_ty> + }); + slots.push(slot_ident); + } + + if let Some(reference) = reference { + let anchor = format_ident!("arg{index}_anchor", span = input.span()); + let ty = &reference.elem; + + join_inputs.push(quote_spanned! {input.span()=> + let #anchor = #r#macro::join_from_js::<#js_ty>(#(#slots),*); + let #argument = ::core::borrow::Borrow::<#ty>::borrow(&#anchor); + }); + } else { + join_inputs.push(quote_spanned! {input.span()=> + let #argument = #r#macro::join_from_js::<#js_ty>(#(#slots),*); + }); + } + + codegen_inputs.push(quote_spanned! {input.span()=> (#parameter, #js_ty) }); + required_embeds.push(quote_spanned!(input.span()=> #r#macro::js_output_embed::<#js_ty>())); + arguments.push(argument); + } + + let call = if function.sig.unsafety.is_some() { + quote_spanned!(span=> unsafe { #ident(#(#arguments),*) }) + } else { + quote_spanned!(span=> #ident(#(#arguments),*)) + }; + let raw_name = LitStr::new(&format!("__export_{export_name_value}"), ident.span()); + let (raw_output, output_argument) = if let Some(output_ty) = output_ty { + ( + quote_spanned! {output_ty.span()=> + -> #js_sys::hazard::WasmRet< + <#output_ty as #js_sys::hazard::ReturnIntoJS>::Abi + > + }, + quote_spanned!(output_ty.span()=> , #output_ty), + ) + } else { + (TokenStream::new(), TokenStream::new()) + }; + let raw_body = if output_ty.is_some() { + quote_spanned! {span=> + #(#join_inputs)* + #r#macro::return_to_js(#call) + } + } else { + quote_spanned! {span=> + #(#join_inputs)* + #call; + } + }; + if let Some(output_ty) = output_ty { + required_embeds + .push(quote_spanned!(output_ty.span()=> #r#macro::js_return_embed::<#output_ty>())); + } + + Ok(quote_spanned! {span=> + #function + + const _: () = { + #[unsafe(export_name = #raw_name)] + extern "C" fn export_raw( + #(#raw_inputs),* + ) #raw_output { + #raw_body + } + + #js_bindgen::unsafe_global_wat! { + "{}", + interpolate #r#macro::wat_export!( + #raw_name, + #export_name, + (#(#codegen_inputs),*) + #output_argument, + ), + } + + #js_bindgen::export_js! { + module = #crate_name, + name = #export_name, + required_embeds = [ + #(#required_embeds),* + ], + "{}", + interpolate #r#macro::js_export!( + #export_name, + (#(#codegen_inputs),*) + #output_argument, + ), + } + }; + }) +} + +fn validate(function: &ItemFn) -> Result<(), Error> { + let sig = &function.sig; + + if let ReturnType::Type(_, ty) = &sig.output + && matches!(ty.as_ref(), Type::Reference(_)) + { + return Err(Error::new_spanned(ty, "cannot return a borrowed reference")); + } + + if let Some(constness) = sig.constness { + return Err(Error::new_spanned( + constness, + "`const` functions are not supported", + )); + } + + if let Some(asyncness) = sig.asyncness { + return Err(Error::new_spanned( + asyncness, + "`async` functions are not supported", + )); + } + + if let Some(abi) = &sig.abi { + return Err(Error::new_spanned( + abi, + "explicit function ABIs are not supported", + )); + } + + if !sig.generics.params.is_empty() || sig.generics.where_clause.is_some() { + return Err(Error::new_spanned( + &sig.generics, + "generic functions are not supported", + )); + } + + if let Some(variadic) = &sig.variadic { + return Err(Error::new_spanned( + variadic, + "variadic functions are not supported", + )); + } + + for input in &sig.inputs { + if let FnArg::Receiver(receiver) = input { + return Err(Error::new_spanned(receiver, "methods are not supported")); + } + } + + Ok(()) +} diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index b543ade8..103e6176 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -1,8 +1,6 @@ -use std::borrow::Cow; -use std::ops::{Deref, DerefMut}; -use std::str::FromStr; +use std::mem; +use std::ops::DerefMut; use std::string::ToString; -use std::{iter, mem}; use itertools::Itertools; use proc_macro2::{Span, TokenStream}; @@ -35,20 +33,24 @@ struct State<'a> { crate_: &'a str, namespace: Option<&'a str>, js_bindgen: Path, - r#macro: Option, - input: Path, - output: Path, + r#macro: Path, import_name: String, foreign_name: String, - input_tys: Vec, - output_ty: Vec, - extern_input_names: Vec, - intern_input_names: Vec, + inputs: Vec, + output_ty: Option, impl_generic_params: TokenStream, r#type: OutputType, span: Span, } +struct InputArg { + abi_type: Type, + rust_name: Ident, + wat_name: syn::LitStr, + slot_names: [Ident; 4], + type_override: bool, +} + enum OutputType { Generate { js_name: Option, @@ -69,6 +71,22 @@ enum MemberType { Setter, } +impl InputArg { + fn new(index: usize, abi_type: Type, rust_name: Ident, type_override: bool) -> Self { + let span = Span::mixed_site(); + let base = format!("arg{index}"); + let slot_name = |slot| Ident::new(&format!("{base}_{slot}"), span); + + Self { + abi_type, + rust_name, + wat_name: syn::LitStr::new(&base, span), + slot_names: [slot_name(0), slot_name(1), slot_name(2), slot_name(3)], + type_override, + } + } +} + impl Function { pub fn new( hygiene: &mut Hygiene<'_>, @@ -106,32 +124,69 @@ impl Function { .. } = item; - let mut state = State::parse( + let state = State::parse( crate_, js_output, namespace, hygiene, &attrs, &mut sig, span, )?; let wat = state.wat(); let js = state.js(); let State { - input, - output, + r#macro, foreign_name, - input_tys, + inputs, output_ty, - extern_input_names, - intern_input_names, impl_generic_params, r#type, .. } = state; let ident = &sig.ident; + let split_inputs = inputs.iter().map(|input| { + let InputArg { + abi_type, + rust_name, + slot_names: [slot1, slot2, slot3, slot4], + type_override, + .. + } = input; + let split_input = if *type_override { + quote_spanned!(span=> unsafe { + #r#macro::split_input_as::<#abi_type>(#rust_name) + }) + } else { + quote_spanned!(span=> #r#macro::split_input::<#abi_type>(#rust_name)) + }; - let mut foreign_call = - quote_spanned!(span=> unsafe { #ident(#(#input::into_raw(#intern_input_names)),*) }); - if output_ty.is_empty() { - foreign_call.extend(quote_spanned!(span=> ;)); + quote_spanned! {span=> + let (#slot1, #slot2, #slot3, #slot4) = #split_input; + } + }); + let foreign_input_names: Vec<_> = inputs.iter().flat_map(|arg| &arg.slot_names).collect(); + let foreign_input_tys: Vec<_> = inputs + .iter() + .flat_map(|arg| { + let ty = &arg.abi_type; + + [ + quote_spanned!(span=> #r#macro::InputSlot1<#ty>), + quote_spanned!(span=> #r#macro::InputSlot2<#ty>), + quote_spanned!(span=> #r#macro::InputSlot3<#ty>), + quote_spanned!(span=> #r#macro::InputSlot4<#ty>), + ] + }) + .collect(); + let foreign_output = output_ty.as_ref().map_or_else( + TokenStream::new, + |ty| quote_spanned!(span=> -> #r#macro::OutputRet<#ty>), + ); + + let foreign_call = quote_spanned! {span=> { + #(#split_inputs)* + unsafe { #ident(#(#foreign_input_names),*) } + }}; + let foreign_call = if output_ty.is_some() { + quote_spanned!(span=> #r#macro::join_output(#foreign_call)) } else { - foreign_call = quote_spanned! (span=> #output::from_raw(#foreign_call)); - } + quote_spanned!(span=> #foreign_call;) + }; let item_fn = parse_quote_spanned! {span=> #(#attrs)* @@ -142,7 +197,7 @@ impl Function { unsafe extern "C" { #[link_name = #foreign_name] - fn #ident(#(#extern_input_names: <#input_tys as #input>::Type),*) #( -> <#output_ty as #output>::Type)*; + fn #ident(#(#foreign_input_names: #foreign_input_tys),*) #foreign_output; } #foreign_call @@ -207,16 +262,17 @@ impl<'a> State<'a> { let mut self_ty = None; - let input_tys = sig + let inputs = sig .inputs .iter_mut() - .map(|arg| { + .enumerate() + .map(|(index, arg)| { if let FnArg::Typed(PatType { attrs, pat, ty, .. }) = arg && let Pat::Ident(PatIdent { attrs: inner_attrs, by_ref: None, mutability: None, - ident: _, + ident, subpat: None, }) = pat.deref_mut() && inner_attrs.is_empty() @@ -242,16 +298,15 @@ impl<'a> State<'a> { })?; } - if let Some(r#type) = r#type { - Ok(r#type) - } else { - Ok(*ty.clone()) - } + let type_override = r#type.is_some(); + let r#type = r#type.unwrap_or_else(|| *ty.clone()); + + Ok(InputArg::new(index, r#type, ident.clone(), type_override)) } else if let FnArg::Receiver(Receiver { attrs, reference: None, mutability: None, - self_token: _, + self_token, colon_token: Some(_), ty, }) = arg && attrs.is_empty() @@ -263,7 +318,7 @@ impl<'a> State<'a> { }) = ty.deref_mut() && let Type::Path(TypePath { qself: None, path }) = elem.deref_mut() { - if !matches!(js_output, FunctionJsOutput::Generate { .. }) { + if !matches!(&js_output, FunctionJsOutput::Generate { .. }) { return Err(Error::new_spanned( path, "`self` is not supported with `js_import` and `js_embed`", @@ -272,7 +327,12 @@ impl<'a> State<'a> { self_ty = Some(path.clone()); let js_value = hygiene.js_value(outer_attrs, span); - Ok(parse_quote! { #and_token #js_value }) + Ok(InputArg::new( + index, + parse_quote! { #and_token #js_value }, + (*self_token).into(), + true, + )) } else { Err(Error::new_spanned(arg, "unsupported arguments found")) } @@ -313,50 +373,24 @@ impl<'a> State<'a> { }; let output_ty = match &sig.output { - ReturnType::Default => Vec::new(), - ReturnType::Type(_, ty) => vec![*ty.clone()], + ReturnType::Default => None, + ReturnType::Type(_, ty) => Some(*ty.clone()), }; - let (extern_input_names, intern_input_names): (Vec<_>, Vec<_>) = sig - .inputs - .iter() - .map(|arg| { - if let FnArg::Typed(PatType { pat, .. }) = arg - && let Pat::Ident(PatIdent { ident, .. }) = pat.deref() - { - (ident.clone(), ident.clone()) - } else if let FnArg::Receiver(Receiver { self_token, .. }) = arg { - (Ident::new("this", Span::mixed_site()), (*self_token).into()) - } else { - unreachable!() - } - }) - .collect(); - let impl_generic_params = Self::impl_generic_params(&r#type, &mut sig.generics); let js_bindgen = hygiene.js_bindgen(outer_attrs, span); - let r#macro = if !input_tys.is_empty() || !output_ty.is_empty() { - Some(hygiene.r#macro(outer_attrs, span)) - } else { - None - }; - let input = hygiene.input(outer_attrs, span); - let output = hygiene.output(outer_attrs, span); + let r#macro = hygiene.r#macro(outer_attrs, span); Ok(Self { crate_, namespace, js_bindgen, r#macro, - input, - output, import_name, foreign_name, - input_tys, + inputs, output_ty, - extern_input_names, - intern_input_names, impl_generic_params, r#type, span, @@ -424,112 +458,59 @@ impl<'a> State<'a> { crate_, js_bindgen, r#macro, - input, import_name, foreign_name, - input_tys, + inputs, output_ty, - intern_input_names, span, .. } = self; + let inputs = inputs.iter().map(|input| { + let name = &input.wat_name; + let ty = &input.abi_type; - let mut input_imports = Vec::new(); - - for ty in input_tys { - if !input_imports.contains(&ty) { - input_imports.push(ty); - } - } - - let mut params = String::new(); - - if !output_ty.is_empty() { - params.push_str(" (param {})"); - } - - for name in intern_input_names { - params.push_str(" (param $"); - params.push_str(&name.to_string()); - params.push_str(" {})"); - } - - let mut wat_param_gets = String::new(); - - for name in intern_input_names { - wat_param_gets.push_str(r#"" local.get $"#); - wat_param_gets.push_str(&name.to_string()); - wat_param_gets.push_str(r#"{}","#); - } - - let params_placeholder = iter::repeat_n("{}", input_tys.len()).join(" "); - let import_params = if input_tys.is_empty() { - String::new() - } else { - format!(" (param {params_placeholder})") - }; - let result = if output_ty.is_empty() { - "" - } else { - " (result {})" - }; - let import_funcs_placeholder = if input_imports.is_empty() && output_ty.is_empty() { - "" - } else { - "{}" - }; - let wat_ret_conv: String = iter::repeat_n("{}", output_ty.len()).collect(); - - let wat = TokenStream::from_str(&format!( - r#""(import \"{crate_}\" \"{import_name}\" (func ${crate_}.import.{import_name} (@sym (name \"{crate_}.import.{import_name}\")){import_params}{result})){import_funcs_placeholder}", - "(func ${foreign_name} (@sym){params}{result}", - {wat_param_gets} - " call ${crate_}.import.{import_name} (@reloc){wat_ret_conv}", - ")","# - )) - .unwrap(); - - let wat_imports = if input_imports.is_empty() && output_ty.is_empty() { - TokenStream::new() - } else { - quote_spanned! {*span=> - interpolate #r#macro::wat_imports!((#(#input_imports),*), #(#output_ty)*), - } - }; + quote_spanned!(*span=> (#name, #ty)) + }); + let output = output_ty.iter(); parse_quote_spanned! {*span=> #js_bindgen::unsafe_global_wat! { - #wat - #(interpolate #r#macro::wat_input_import_type::<#input_tys>(),)* - #(interpolate #r#macro::wat_output_import_type::<#output_ty>(),)* - #wat_imports - #(interpolate #r#macro::wat_indirect!(#output_ty),)* - #(interpolate <#input_tys as #input>::WAT_TYPE,)* - #(interpolate #r#macro::wat_direct::<#output_ty>(),)* - #(interpolate #r#macro::wat_input!(#input_tys),)* - #(interpolate #r#macro::wat_output!(#output_ty),)* + "{}", + interpolate #r#macro::wat_import!( + module = #crate_, + import = #import_name, + adapter = #foreign_name, + inputs = [#(#inputs),*], + #(output = #output,)* + ), } } } - fn js(&mut self) -> Option { + fn js(&self) -> Option { let Self { crate_, js_bindgen, r#macro, import_name, - input_tys, + inputs, output_ty, - intern_input_names, r#type, span, .. } = self; + let input_tys: Vec<_> = inputs.iter().map(|input| &input.abi_type).collect(); + let input_names: Vec<_> = inputs.iter().map(|input| &input.wat_name).collect(); + let input_value_names: Vec<_> = inputs + .iter() + .map(|input| input.slot_names[0].to_string()) + .collect(); + let output_tys: Vec<_> = output_ty.iter().collect(); let js_path = match r#type { OutputType::Generate { js_name, member } => { let base = if member.is_some() { - "self" + input_value_names[0].as_str() } else { "globalThis" }; @@ -552,7 +533,7 @@ impl<'a> State<'a> { let mut unique_inputs = Vec::new(); - for ty in input_tys.iter() { + for &ty in &input_tys { if !unique_inputs.contains(&ty) { unique_inputs.push(ty); } @@ -568,8 +549,9 @@ impl<'a> State<'a> { required_embeds.push(quote_spanned!(*span=> #r#macro::js_input_embed::<#ty>())); } - for ty in output_ty.iter() { + for &ty in &output_tys { required_embeds.push(quote_spanned!(*span=> #r#macro::js_output_embed::<#ty>())); + required_embeds.push(quote_spanned!(*span=> #r#macro::js_result_embed::<#ty>())); } let required_embeds = if required_embeds.is_empty() { @@ -578,100 +560,53 @@ impl<'a> State<'a> { &[quote_spanned!(*span=> required_embeds = [#(#required_embeds),*])] }; - if input_tys.is_empty() && output_ty.is_empty() { - return Some(parse_quote_spanned! {*span=> - #js_bindgen::import_js!( - module = #crate_, - name = #import_name, - #(#required_embeds,)* - #js_path - ); - }); - } - - let js_call_pre = if let Some(member) = r#type.member() - && let MemberType::Setter = member.r#type - { - &format!("{js_path} = {{}}") - } else { - "{}" - }; - let placeholder: String = iter::once("{}") - .chain(iter::repeat_n("{}", input_tys.len())) - .chain(iter::once(if output_ty.is_empty() { - js_call_pre - } else { - "{}" - })) - .collect(); - - let input_names_joined = intern_input_names.iter().join(", "); + let input_names_joined = input_value_names.iter().join(", "); let call_input_names_joined = if r#type.member().is_some() { - Cow::Owned(intern_input_names.iter().skip(1).join(", ")) + input_value_names.iter().skip(1).join(", ") } else { - Cow::Borrowed(&input_names_joined) + input_names_joined.clone() }; - let input_conv = intern_input_names.iter().map(ToString::to_string); - + let js_inputs: Vec<_> = input_names + .iter() + .zip(input_tys.iter()) + .map(|(name, ty)| quote_spanned!(*span=> (#name, #ty))) + .collect(); let direct_fn_open = if r#type.member().is_none() { - String::new() + quote_spanned!(*span=> "") } else { - format!("({input_names_joined}) => ") + quote_spanned!(*span=> + #r#macro::js_function!("(", ") => ", #(#js_inputs),*) + ) }; - let mut indirect_fn_open = format!("({input_names_joined}) => {{\n"); let direct_js_call = if let Some(member) = r#type.member() { match member.r#type { - MemberType::Method => Cow::Owned(format!("{js_path}({call_input_names_joined})")), - MemberType::Getter => Cow::Borrowed(&js_path), - MemberType::Setter => Cow::Owned(format!("{call_input_names_joined}")), + MemberType::Method => format!("{js_path}({call_input_names_joined})"), + MemberType::Getter => js_path.clone(), + MemberType::Setter => format!("{js_path} = {call_input_names_joined}"), } } else { - Cow::Borrowed(&js_path) + js_path.clone() }; let indirect_js_call = if r#type.member().is_some() { - Cow::Borrowed(direct_js_call.as_str()) - } else { - Cow::Owned(format!("{js_path}({input_names_joined})")) - }; - let mut first_output = if output_ty.is_empty() { - Cow::Borrowed(indirect_js_call.deref()) - } else { - Cow::Borrowed("\treturn ") - }; - - let direct_condition = quote_spanned! {*span=> - (#(#unique_inputs),*) #(, #output_ty)* - }; - - let output = if output_ty.is_empty() { - first_output.to_mut().push_str("\n}"); - - quote_spanned! {*span=> - interpolate #r#macro::js_select!(#direct_js_call, #first_output, #direct_condition), - } + direct_js_call.clone() } else { - let mut start = Cow::Borrowed(""); - - if input_tys.is_empty() { - indirect_fn_open.push_str(&first_output); - } else { - start = first_output; - } - - quote_spanned! {*span=> - interpolate #r#macro::js_output!(#start, #direct_js_call, #indirect_js_call, #(#output_ty,)*#(#unique_inputs),*), - } + format!("{js_path}({input_names_joined})") }; + let output = output_ty.iter(); Some(parse_quote_spanned! {*span=> #js_bindgen::import_js! { module = #crate_, name = #import_name, #(#required_embeds,)* - #placeholder, - interpolate #r#macro::js_select!(#direct_fn_open, #indirect_fn_open, #direct_condition), - #(interpolate #r#macro::js_parameter!(#input_conv, #input_tys),)* - #output + "{}", + interpolate #r#macro::js_import!( + direct_open = #direct_fn_open, + direct_call = #direct_js_call, + indirect_call = #indirect_js_call, + inputs = [#(#js_inputs),*], + #(output = #output,)* + ), } }) } diff --git a/host/js-sys-bindgen/src/hygiene.rs b/host/js-sys-bindgen/src/hygiene.rs index 45038edc..dbd05e32 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -32,40 +32,6 @@ impl Hygiene<'_> { } } - pub(crate) fn input(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> Input)); - parse_quote_spanned!(span=> Input) - } - Hygiene::Hygiene { js_sys } => Self::with_js_sys(*js_sys, "e!(hazard::Input), span), - } - } - - pub(crate) fn input_wat_conv(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> InputWatConv)); - parse_quote_spanned!(span=> InputWatConv) - } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::InputWatConv), span) - } - } - } - - pub(crate) fn input_js_conv(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> InputJsConv)); - parse_quote_spanned!(span=> InputJsConv) - } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::InputJsConv), span) - } - } - } - pub(crate) fn js_cast(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { Hygiene::Imports(imports) => { @@ -78,38 +44,26 @@ impl Hygiene<'_> { } } - pub(crate) fn output(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> Output)); - parse_quote_spanned!(span=> Output) - } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::Output), span) - } - } - } - - pub(crate) fn output_wat_conv(&mut self, attrs: &[Attribute], span: Span) -> Path { + pub(crate) fn js_into(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> OutputWatConv)); - parse_quote_spanned!(span=> OutputWatConv) + imports.hazard_push(attrs, parse_quote_spanned!(span=> IntoJS)); + parse_quote_spanned!(span=> IntoJS) } Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::OutputWatConv), span) + Self::with_js_sys(*js_sys, "e!(hazard::IntoJS), span) } } } - pub(crate) fn output_js_conv(&mut self, attrs: &[Attribute], span: Span) -> Path { + pub(crate) fn js_option_into(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> OutputJsConv)); - parse_quote_spanned!(span=> OutputJsConv) + imports.hazard_push(attrs, parse_quote_spanned!(span=> OptionIntoJS)); + parse_quote_spanned!(span=> OptionIntoJS) } Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::OutputJsConv), span) + Self::with_js_sys(*js_sys, "e!(hazard::OptionIntoJS), span) } } } @@ -149,17 +103,6 @@ impl Hygiene<'_> { } } - pub(crate) fn str(&mut self, span: Span) -> Path { - match self { - Hygiene::Imports(_) => { - parse_quote_spanned!(span=> str) - } - Hygiene::Hygiene { .. } => { - parse_quote_spanned!(span=> ::core::primitive::str) - } - } - } - pub(crate) fn from(&mut self, span: Span) -> Path { match self { Hygiene::Imports(_) => { @@ -171,17 +114,6 @@ impl Hygiene<'_> { } } - pub(crate) fn option(&mut self, span: Span) -> Path { - match self { - Hygiene::Imports(_) => { - parse_quote_spanned!(span=> Option) - } - Hygiene::Hygiene { .. } => { - parse_quote_spanned!(span=> ::core::option::Option) - } - } - } - fn with_js_sys(js_sys: Option<&Path>, path: &TokenStream, span: Span) -> Path { let js_sys = js_sys.map_or_else( || Cow::Owned(parse_quote_spanned!(span=> ::js_sys)), diff --git a/host/js-sys-bindgen/src/lib.rs b/host/js-sys-bindgen/src/lib.rs index 2a513783..2bd68b1e 100644 --- a/host/js-sys-bindgen/src/lib.rs +++ b/host/js-sys-bindgen/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "macro")] +mod export; #[cfg(feature = "file")] mod file; mod function; diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index bdcb5821..2f693bee 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -12,22 +12,28 @@ pub fn r#macro( item: TokenStream, imports: Option<&mut ImportManager>, ) -> Result { - let foreign_mod: ItemForeignMod = syn::parse2(item).map_err(Error::into_compile_error)?; - - internal(attr, foreign_mod, None, imports) - .map(|items| items.into_iter().map(Item::into_token_stream).collect()) - .map_err(|(output, error)| { - let error = error.into_compile_error(); - - if let Some(output) = output { - let mut output: TokenStream = - output.into_iter().map(Item::into_token_stream).collect(); - output.extend(error); - output - } else { - error - } - }) + match syn::parse2(item).map_err(Error::into_compile_error)? { + Item::ForeignMod(foreign_mod) => internal(attr, foreign_mod, None, imports) + .map(|items| items.into_iter().map(Item::into_token_stream).collect()) + .map_err(|(output, error)| { + let error = error.into_compile_error(); + + if let Some(output) = output { + let mut output: TokenStream = + output.into_iter().map(Item::into_token_stream).collect(); + output.extend(error); + output + } else { + error + } + }), + Item::Fn(function) => { + crate::export::r#macro(attr, &function, None).map_err(Error::into_compile_error) + } + item => Err( + Error::new_spanned(item, "expected an extern block or function").into_compile_error(), + ), + } } pub(crate) fn internal( diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs new file mode 100644 index 00000000..f7844da5 --- /dev/null +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -0,0 +1,251 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::File; + +fn expand(function: &TokenStream) -> (String, String) { + let function = syn::parse2(quote! { #function }).unwrap(); + let output = crate::export::r#macro(TokenStream::new(), &function, Some("test_crate")).unwrap(); + let output = prettyplease::unparse(&syn::parse2::(output).unwrap()); + let dir = tempfile::tempdir().unwrap(); + let (wat, js_import, js_export) = super::inner(dir.path(), &output).unwrap(); + + assert_eq!(js_import, None); + (wat.unwrap(), js_export.unwrap()) +} + +#[test] +fn borrowed_return_is_rejected() { + let function = syn::parse2(quote! { + fn echo(value: &JsString) -> &JsString { + value + } + }) + .unwrap(); + let error = + crate::export::r#macro(TokenStream::new(), &function, Some("test_crate")).unwrap_err(); + + assert_eq!(error.to_string(), "cannot return a borrowed reference"); +} + +#[test] +fn direct() { + let (wat, js) = expand("e! { + fn echo(value: u32) -> u32 { + value + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "raw" (func $raw (@sym (name "__export_echo")) (param i32) (result i32))) +(func $export (@sym (name "echo")) (param $arg0_0 i32) (result i32) + local.get $arg0_0 + call $raw (@reloc) +)"# + ); + assert_eq!( + js, + r"(arg0) => { + const ret = instance.exports['echo'](arg0) + return ret >>> 0 +}" + ); +} + +#[test] +fn wat_slot_conversions() { + let (wat, js) = expand("e! { + pub fn drop_value(value: JsValue) { + let _ = value; + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "js_sys.externref.insert" (func $js_sys.externref.insert (@sym) (param externref) (result i32))) +(import "env" "raw" (func $raw (@sym (name "__export_drop_value")) (param i32))) +(func $export (@sym (name "drop_value")) (param $arg0_0 externref) + local.get $arg0_0 + call $js_sys.externref.insert (@reloc) + call $raw (@reloc) +)"# + ); + assert_eq!( + js, + r"(arg0) => { + instance.exports['drop_value'](arg0) +}" + ); + + let (wat, js) = expand("e! { + pub fn undefined() -> Option { + None + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "js_sys.externref.take" (func $js_sys.externref.take (@sym) (param i32) (result externref))) +(import "env" "raw" (func $raw (@sym (name "__export_undefined")) (result i32))) +(func $export (@sym (name "undefined")) (result externref) + call $raw (@reloc) + call $js_sys.externref.take (@reloc) +)"# + ); + assert_eq!( + js, + r"() => { + const ret = instance.exports['undefined']() + return ret +}" + ); +} + +#[test] +fn indirect_and_multiple_parameters() { + let (wat, js) = expand("e! { + pub fn add(value: u32, delta: u128) -> u128 { + u128::from(value) + delta + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "raw" (func $raw (@sym (name "__export_add")) (param i32) (param i32) (param i64 i64))) +(import "env" "__stack_pointer" (global $__stack_pointer (mut i32))) +(func $export (@sym (name "add")) (param $arg0_0 i32) (param $arg1_0 i64) (param $arg1_1 i64) (result i64 i64) + (local $retptr i32) + global.get $__stack_pointer + i32.const 16 + i32.sub + local.tee $retptr + global.set $__stack_pointer + local.get $retptr + local.get $arg0_0 + local.get $arg1_0 + local.get $arg1_1 + call $raw (@reloc) + local.get $retptr + i64.load offset=0 + local.get $retptr + i64.load offset=8 + local.get $retptr + i32.const 16 + i32.add + global.set $__stack_pointer +)"# + ); + assert_eq!( + js, + r"(arg0, arg1) => { + const ret = instance.exports['add'](arg0, arg1, arg1 >> 64n) + return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1]) +}" + ); +} + +#[test] +fn result() { + let (wat, js) = expand("e! { + pub fn checked_add(value: u128, delta: u128) -> Result { + value.checked_add(delta).ok_or(JsValue::UNDEFINED) + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "js_sys.externref.take" (func $js_sys.externref.take (@sym) (param i32) (result externref))) +(import "env" "raw" (func $raw (@sym (name "__export_checked_add")) (param i32) (param i64 i64) (param i64 i64))) +(import "env" "__stack_pointer" (global $__stack_pointer (mut i32))) +(func $export (@sym (name "checked_add")) (param $arg0_0 i64) (param $arg0_1 i64) (param $arg1_0 i64) (param $arg1_1 i64) (result externref i32 i64 i64) + (local $retptr i32) + global.get $__stack_pointer + i32.const 32 + i32.sub + local.tee $retptr + global.set $__stack_pointer + local.get $retptr + local.get $arg0_0 + local.get $arg0_1 + local.get $arg1_0 + local.get $arg1_1 + call $raw (@reloc) + local.get $retptr + i32.load offset=0 + call $js_sys.externref.take (@reloc) + local.get $retptr + i32.load offset=4 + local.get $retptr + i64.load offset=8 + local.get $retptr + i64.load offset=16 + local.get $retptr + i32.const 32 + i32.add + global.set $__stack_pointer +)"# + ); + assert_eq!( + js, + r"(arg0, arg1) => { + const ret = instance.exports['checked_add'](arg0, arg0 >> 64n, arg1, arg1 >> 64n) + if (ret[1] !== 0) throw ret[0] + return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[2], ret[3]) +}" + ); +} + +#[test] +fn no_parameters() { + let (wat, js) = expand("e! { + pub fn answer() -> u32 { + 42 + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "raw" (func $raw (@sym (name "__export_answer")) (result i32))) +(func $export (@sym (name "answer")) (result i32) + call $raw (@reloc) +)"# + ); + assert_eq!( + js, + r"() => { + const ret = instance.exports['answer']() + return ret >>> 0 +}" + ); +} + +#[test] +fn no_return_value() { + let (wat, js) = expand("e! { + pub fn nothing(value: u32) -> () { + let _ = value; + } + }); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "raw" (func $raw (@sym (name "__export_nothing")) (param i32))) +(func $export (@sym (name "nothing")) (param $arg0_0 i32) + local.get $arg0_0 + call $raw (@reloc) +)"# + ); + assert_eq!( + js, + r"(arg0) => { + instance.exports['nothing'](arg0) +}" + ); +} diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index cc52a3bb..96b79d59 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -10,42 +10,44 @@ fn basic() { { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\")) (param {}))){}", - "(func $test_crate.log (@sym) (param $data {})", " local.get $data{}", - " call $test_crate.import.log (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "log", required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}{}{}", - interpolate ::js_sys::r#macro::js_select!("", "(data) => {\n", (&JsValue)), - interpolate ::js_sys::r#macro::js_parameter!("data", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "globalThis.log", - "globalThis.log(data)\n}", - (&JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.log", indirect_call = + "globalThis.log(arg0_0)", inputs = [("arg0", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); + fn log( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data); + unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.log (@sym) (param $data i32) - local.get $data + (func $test_crate.log (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.log (@reloc) )", @@ -65,42 +67,44 @@ fn namespace() { { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"console.log\" (func $test_crate.import.console.log (@sym (name \"test_crate.import.console.log\")) (param {}))){}", - "(func $test_crate.console.log (@sym) (param $data {})", " local.get $data{}", - " call $test_crate.import.console.log (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = + "console.log", adapter = "test_crate.console.log", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "console.log", required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}{}{}", - interpolate ::js_sys::r#macro::js_select!("", "(data) => {\n", (&JsValue)), - interpolate ::js_sys::r#macro::js_parameter!("data", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "globalThis.console.log", - "globalThis.console.log(data)\n}", - (&JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.console.log", indirect_call = + "globalThis.console.log(arg0_0)", inputs = [("arg0", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.console.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); + fn log( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data); + unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } }, "(import \"test_crate\" \"console.log\" (func $test_crate.import.console.log (@sym (name \ \"test_crate.import.console.log\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.console.log (@sym) (param $data i32) - local.get $data + (func $test_crate.console.log (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.console.log (@reloc) )", @@ -120,42 +124,44 @@ fn js_sys() { { pub fn log(data: &JsValue) { js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\")) (param {}))){}", - "(func $test_crate.log (@sym) (param $data {})", " local.get $data{}", - " call $test_crate.import.log (@reloc)", ")", interpolate - js_sys::r#macro::wat_input_import_type:: < & JsValue > (), interpolate - js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - js_sys::hazard::Input > ::WAT_TYPE, interpolate js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), } js_sys::js_bindgen::import_js! { module = "test_crate", name = "log", required_embeds = [js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}{}{}", - interpolate js_sys::r#macro::js_select!("", "(data) => {\n", (&JsValue)), - interpolate js_sys::r#macro::js_parameter!("data", &JsValue), - interpolate js_sys::r#macro::js_select!( - "globalThis.log", - "globalThis.log(data)\n}", - (&JsValue), + "{}", + interpolate js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.log", indirect_call = + "globalThis.log(arg0_0)", inputs = [("arg0", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.log"] - fn log(data: <&JsValue as js_sys::hazard::Input>::Type); + fn log( + arg0_0: js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: js_sys::r#macro::InputSlot4<&JsValue>, + ); } - unsafe { log(js_sys::hazard::Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + js_sys::r#macro::split_input::<&JsValue>(data); + unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.log (@sym) (param $data i32) - local.get $data + (func $test_crate.log (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.log (@reloc) )", @@ -175,45 +181,45 @@ fn two_parameters() { { pub fn log(data1: &JsValue, data2: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\")) (param {} {}))){}", - "(func $test_crate.log (@sym) (param $data1 {}) (param $data2 {})", " local.get $data1{}", - " local.get $data2{}", " call $test_crate.import.log (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate < & JsValue as ::js_sys::hazard::Input > - ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& JsValue), - interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), } ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "log", required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}{}{}{}", - interpolate ::js_sys::r#macro::js_select!("", "(data1, data2) => {\n", (&JsValue)), - interpolate ::js_sys::r#macro::js_parameter!("data1", &JsValue), - interpolate ::js_sys::r#macro::js_parameter!("data2", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "globalThis.log", - "globalThis.log(data1, data2)\n}", - (&JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.log", indirect_call = + "globalThis.log(arg0_0, arg1_0)", inputs = [("arg0", & JsValue), ("arg1", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.log"] fn log( - data1: <&JsValue as ::js_sys::hazard::Input>::Type, - data2: <&JsValue as ::js_sys::hazard::Input>::Type, + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + arg1_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg1_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg1_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg1_3: ::js_sys::r#macro::InputSlot4<&JsValue>, ); } - unsafe { - log( - ::js_sys::hazard::Input::into_raw(data1), - ::js_sys::hazard::Input::into_raw(data2), - ) + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data1); + let (arg1_0, arg1_1, arg1_2, arg1_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data2); + unsafe { + log( + arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3, + ) + } }; } }, @@ -221,10 +227,10 @@ fn two_parameters() { \"test_crate.import.log\")) (param externref externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.log (@sym) (param $data1 i32) (param $data2 i32) - local.get $data1 + (func $test_crate.log (@sym) (param $arg0_0 i32) (param $arg1_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) - local.get $data2 + local.get $arg1_0 call $js_sys.externref.get (@reloc) call $test_crate.import.log (@reloc) )", @@ -244,22 +250,28 @@ fn empty() { { pub fn log() { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\"))))", - "(func $test_crate.log (@sym)", " call $test_crate.import.log (@reloc)", ")", + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [],), } - ::js_sys::js_bindgen::import_js!( + ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "log", - "globalThis.log" - ); + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.log", indirect_call = "globalThis.log()", + inputs = [], + ), + } unsafe extern "C" { #[link_name = "test_crate.log"] fn log(); } - unsafe { log() }; + { + unsafe { log() } + }; } }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ @@ -284,42 +296,44 @@ fn js_name() { { pub fn logx(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"logx\" (func $test_crate.import.logx (@sym (name \"test_crate.import.logx\")) (param {}))){}", - "(func $test_crate.logx (@sym) (param $data {})", " local.get $data{}", - " call $test_crate.import.logx (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "logx", + adapter = "test_crate.logx", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "logx", required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}{}{}", - interpolate ::js_sys::r#macro::js_select!("", "(data) => {\n", (&JsValue)), - interpolate ::js_sys::r#macro::js_parameter!("data", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "globalThis.log", - "globalThis.log(data)\n}", - (&JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.log", indirect_call = + "globalThis.log(arg0_0)", inputs = [("arg0", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.logx"] - fn logx(data: <&JsValue as ::js_sys::hazard::Input>::Type); + fn logx( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } - unsafe { logx(::js_sys::hazard::Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data); + unsafe { logx(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } }, "(import \"test_crate\" \"logx\" (func $test_crate.import.logx (@sym (name \ \"test_crate.import.logx\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.logx (@sym) (param $data i32) - local.get $data + (func $test_crate.logx (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.logx (@reloc) )", @@ -340,28 +354,33 @@ fn js_import() { { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\")) (param {}))){}", - "(func $test_crate.log (@sym) (param $data {})", " local.get $data{}", - " call $test_crate.import.log (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), } unsafe extern "C" { #[link_name = "test_crate.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); + fn log( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data); + unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.log (@sym) (param $data i32) - local.get $data + (func $test_crate.log (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.log (@reloc) )", @@ -382,12 +401,8 @@ fn js_embed() { { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\")) (param {}))){}", - "(func $test_crate.log (@sym) (param $data {})", " local.get $data{}", - " call $test_crate.import.log (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& JsValue),), interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -397,30 +412,36 @@ fn js_embed() { ("test_crate", "embed"), ::js_sys::r#macro::js_input_embed::<&JsValue>(), ], - "{}{}{}", - interpolate ::js_sys::r#macro::js_select!("", "(data) => {\n", (&JsValue)), - interpolate ::js_sys::r#macro::js_parameter!("data", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "this.#jsEmbed.test_crate['embed']", - "this.#jsEmbed.test_crate['embed'](data)\n}", - (&JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.test_crate['embed']", indirect_call = + "this.#jsEmbed.test_crate['embed'](arg0_0)", inputs = [("arg0", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); + fn log( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(data); + unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.log (@sym) (param $data i32) - local.get $data + (func $test_crate.log (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.log (@reloc) )", @@ -440,43 +461,37 @@ fn r#return() { { pub fn is_nan() -> JsValue { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"is_nan\" (func $test_crate.import.is_nan (@sym (name \"test_crate.import.is_nan\")) (result {}))){}", - "(func $test_crate.is_nan (@sym) (param {}) (result {})", - " call $test_crate.import.is_nan (@reloc){}", ")", - interpolate::js_sys::r#macro::wat_output_import_type:: < JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((), JsValue), - interpolate::js_sys::r#macro::wat_indirect!(JsValue), - interpolate::js_sys::r#macro::wat_direct:: < JsValue > (), - interpolate::js_sys::r#macro::wat_output!(JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "is_nan", + adapter = "test_crate.is_nan", inputs = [], output = JsValue,), } ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "is_nan", - required_embeds = [::js_sys::r#macro::js_output_embed::()], - "{}{}", - interpolate ::js_sys::r#macro::js_select!("", "() => {\n\treturn ", (), JsValue), - interpolate ::js_sys::r#macro::js_output!( - "", - "globalThis.is_nan", - "globalThis.is_nan()", - JsValue, + required_embeds = [ + ::js_sys::r#macro::js_output_embed::(), + ::js_sys::r#macro::js_result_embed::(), + ], + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.is_nan", indirect_call = + "globalThis.is_nan()", inputs = [], output = JsValue, ), } unsafe extern "C" { #[link_name = "test_crate.is_nan"] - fn is_nan() -> ::Type; + fn is_nan() -> ::js_sys::r#macro::OutputRet; } - ::js_sys::hazard::Output::from_raw(unsafe { is_nan() }) + ::js_sys::r#macro::join_output({ unsafe { is_nan() } }) } }, "(import \"test_crate\" \"is_nan\" (func $test_crate.import.is_nan (@sym (name \ \"test_crate.import.is_nan\")) (result externref))) (import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) (param \ externref) (result i32))) - (func $test_crate.is_nan (@sym) (param ) (result i32) + (func $test_crate.is_nan (@sym) (result i32) call $test_crate.import.is_nan (@reloc) call $js_sys.externref.insert (@reloc) )", @@ -498,22 +513,28 @@ fn cfg() { #[cfg(all())] pub fn log() { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \"test_crate.import.log\"))))", - "(func $test_crate.log (@sym)", " call $test_crate.import.log (@reloc)", ")", + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", + adapter = "test_crate.log", inputs = [],), } - ::js_sys::js_bindgen::import_js!( + ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "log", - "globalThis.log" - ); + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = "", direct_call = "globalThis.log", indirect_call = "globalThis.log()", + inputs = [], + ), + } unsafe extern "C" { #[link_name = "test_crate.log"] fn log(); } - unsafe { log() }; + { + unsafe { log() } + }; } }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index 2233224e..5e64cbba 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -11,39 +11,38 @@ fn method() { impl JsTest { pub fn test(self: &JsTest) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \"test_crate.import.test\")) (param {}))){}", - "(func $test_crate.test (@sym) (param $self {})", " local.get $self{}", - " call $test_crate.import.test (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & ::js_sys::JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& ::js_sys::JsValue),), interpolate < & - ::js_sys::JsValue as ::js_sys::hazard::Input > ::WAT_TYPE, - interpolate::js_sys::r#macro::wat_input!(& ::js_sys::JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", + adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)],), } ::js_sys::js_bindgen::import_js! { module = "test_crate", name = "test", required_embeds = [::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>()], - "{}{}{}", - interpolate ::js_sys::r#macro::js_select!( - "(self) => ", - "(self) => {\n", - (&::js_sys::JsValue), - ), - interpolate ::js_sys::r#macro::js_parameter!("self", &::js_sys::JsValue), - interpolate ::js_sys::r#macro::js_select!( - "self.test()", - "self.test()\n}", - (&::js_sys::JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & + ::js_sys::JsValue)), direct_call = "arg0_0.test()", indirect_call = "arg0_0.test()", + inputs = [("arg0", & ::js_sys::JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.test"] - fn test(this: <&::js_sys::JsValue as ::js_sys::hazard::Input>::Type); + fn test( + arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, + ); } - unsafe { test(::js_sys::hazard::Input::into_raw(self)) }; + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { + ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) + }; + unsafe { test(arg0_0, arg0_1, arg0_2, arg0_3) } + }; } } }, @@ -51,12 +50,12 @@ fn method() { \"test_crate.import.test\")) (param externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.test (@sym) (param $self i32) - local.get $self + (func $test_crate.test (@sym) (param $arg0_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.test (@reloc) )", - "(self) => self.test()", + "(arg0_0) => arg0_0.test()", ); } @@ -73,19 +72,9 @@ fn method_par() { impl JsTest { pub fn test(self: &JsTest, par1: &JsValue, par2: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \"test_crate.import.test\")) (param {} {} {}))){}", - "(func $test_crate.test (@sym) (param $self {}) (param $par1 {}) (param $par2 {})", - " local.get $self{}", " local.get $par1{}", " local.get $par2{}", - " call $test_crate.import.test (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & ::js_sys::JsValue > (), - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& ::js_sys::JsValue, & JsValue),), - interpolate < & ::js_sys::JsValue as ::js_sys::hazard::Input > ::WAT_TYPE, interpolate < - & JsValue as ::js_sys::hazard::Input > ::WAT_TYPE, interpolate < & JsValue as - ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_input!(& - ::js_sys::JsValue), interpolate::js_sys::r#macro::wat_input!(& JsValue), - interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", + adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & + JsValue), ("arg2", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -95,37 +84,47 @@ fn method_par() { ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), ::js_sys::r#macro::js_input_embed::<&JsValue>(), ], - "{}{}{}{}{}", - interpolate ::js_sys::r#macro::js_select!( - "(self, par1, par2) => ", - "(self, par1, par2) => {\n", - (&::js_sys::JsValue, &JsValue), - ), - interpolate ::js_sys::r#macro::js_parameter!("self", &::js_sys::JsValue), - interpolate ::js_sys::r#macro::js_parameter!("par1", &JsValue), - interpolate ::js_sys::r#macro::js_parameter!("par2", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "self.test(par1, par2)", - "self.test(par1, par2)\n}", - (&::js_sys::JsValue, &JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & + ::js_sys::JsValue), ("arg1", & JsValue), ("arg2", & JsValue)), direct_call = + "arg0_0.test(arg1_0, arg2_0)", indirect_call = "arg0_0.test(arg1_0, arg2_0)", inputs + = [("arg0", & ::js_sys::JsValue), ("arg1", & JsValue), ("arg2", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.test"] fn test( - this: <&::js_sys::JsValue as ::js_sys::hazard::Input>::Type, - par1: <&JsValue as ::js_sys::hazard::Input>::Type, - par2: <&JsValue as ::js_sys::hazard::Input>::Type, + arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, + arg1_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg1_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg1_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg1_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + arg2_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg2_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg2_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg2_3: ::js_sys::r#macro::InputSlot4<&JsValue>, ); } - unsafe { - test( - ::js_sys::hazard::Input::into_raw(self), - ::js_sys::hazard::Input::into_raw(par1), - ::js_sys::hazard::Input::into_raw(par2), - ) + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { + ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) + }; + let (arg1_0, arg1_1, arg1_2, arg1_3) = + ::js_sys::r#macro::split_input::<&JsValue>(par1); + let (arg2_0, arg2_1, arg2_2, arg2_3) = + ::js_sys::r#macro::split_input::<&JsValue>(par2); + unsafe { + test( + arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3, + arg2_0, arg2_1, arg2_2, arg2_3, + ) + } }; } } @@ -134,16 +133,16 @@ fn method_par() { \"test_crate.import.test\")) (param externref externref externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.test (@sym) (param $self i32) (param $par1 i32) (param $par2 i32) - local.get $self + (func $test_crate.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) (param $arg2_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) - local.get $par1 + local.get $arg1_0 call $js_sys.externref.get (@reloc) - local.get $par2 + local.get $arg2_0 call $js_sys.externref.get (@reloc) call $test_crate.import.test (@reloc) )", - "(self, par1, par2) => self.test(par1, par2)", + "(arg0_0, arg1_0, arg2_0) => arg0_0.test(arg1_0, arg2_0)", ); } @@ -161,16 +160,9 @@ fn getter() { impl JsTest { pub fn test(self: &JsTest) -> JsValue { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \"test_crate.import.test\")) (param {}) (result {}))){}", - "(func $test_crate.test (@sym) (param {}) (param $self {}) (result {})", - " local.get $self{}", " call $test_crate.import.test (@reloc){}", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & ::js_sys::JsValue > (), - interpolate::js_sys::r#macro::wat_output_import_type:: < JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& ::js_sys::JsValue), JsValue), - interpolate::js_sys::r#macro::wat_indirect!(JsValue), interpolate < & ::js_sys::JsValue - as ::js_sys::hazard::Input > ::WAT_TYPE, interpolate::js_sys::r#macro::wat_direct:: < - JsValue > (), interpolate::js_sys::r#macro::wat_input!(& ::js_sys::JsValue), - interpolate::js_sys::r#macro::wat_output!(JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", + adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)], output = + JsValue,), } ::js_sys::js_bindgen::import_js! { @@ -179,33 +171,31 @@ fn getter() { required_embeds = [ ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), ::js_sys::r#macro::js_output_embed::(), + ::js_sys::r#macro::js_result_embed::(), ], - "{}{}{}", - interpolate ::js_sys::r#macro::js_select!( - "(self) => ", - "(self) => {\n", - (&::js_sys::JsValue), - JsValue, - ), - interpolate ::js_sys::r#macro::js_parameter!("self", &::js_sys::JsValue), - interpolate ::js_sys::r#macro::js_output!( - "\treturn ", - "self.test", - "self.test", - JsValue, - &::js_sys::JsValue, + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & + ::js_sys::JsValue)), direct_call = "arg0_0.test", indirect_call = "arg0_0.test", + inputs = [("arg0", & ::js_sys::JsValue)], output = JsValue, ), } unsafe extern "C" { #[link_name = "test_crate.test"] fn test( - this: <&::js_sys::JsValue as ::js_sys::hazard::Input>::Type, - ) -> ::Type; + arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, + ) -> ::js_sys::r#macro::OutputRet; } - ::js_sys::hazard::Output::from_raw(unsafe { - test(::js_sys::hazard::Input::into_raw(self)) + ::js_sys::r#macro::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { + ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) + }; + unsafe { test(arg0_0, arg0_1, arg0_2, arg0_3) } }) } } @@ -216,13 +206,13 @@ fn getter() { externref))) (import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) (param \ externref) (result i32))) - (func $test_crate.test (@sym) (param ) (param $self i32) (result i32) - local.get $self + (func $test_crate.test (@sym) (param $arg0_0 i32) (result i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) call $test_crate.import.test (@reloc) call $js_sys.externref.insert (@reloc) )", - "(self) => self.test", + "(arg0_0) => arg0_0.test", ); } @@ -240,17 +230,9 @@ fn setter() { impl JsTest { pub fn test(self: &JsTest, value: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \"test_crate.import.test\")) (param {} {}))){}", - "(func $test_crate.test (@sym) (param $self {}) (param $value {})", - " local.get $self{}", " local.get $value{}", - " call $test_crate.import.test (@reloc)", ")", - interpolate::js_sys::r#macro::wat_input_import_type:: < & ::js_sys::JsValue > (), - interpolate::js_sys::r#macro::wat_input_import_type:: < & JsValue > (), - interpolate::js_sys::r#macro::wat_imports!((& ::js_sys::JsValue, & JsValue),), - interpolate < & ::js_sys::JsValue as ::js_sys::hazard::Input > ::WAT_TYPE, interpolate < - & JsValue as ::js_sys::hazard::Input > ::WAT_TYPE, - interpolate::js_sys::r#macro::wat_input!(& ::js_sys::JsValue), - interpolate::js_sys::r#macro::wat_input!(& JsValue), + "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", + adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & + JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -260,34 +242,40 @@ fn setter() { ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), ::js_sys::r#macro::js_input_embed::<&JsValue>(), ], - "{}{}{}self.test = {}", - interpolate ::js_sys::r#macro::js_select!( - "(self, value) => ", - "(self, value) => {\n", - (&::js_sys::JsValue, &JsValue), - ), - interpolate ::js_sys::r#macro::js_parameter!("self", &::js_sys::JsValue), - interpolate ::js_sys::r#macro::js_parameter!("value", &JsValue), - interpolate ::js_sys::r#macro::js_select!( - "value", - "value\n}", - (&::js_sys::JsValue, &JsValue), + "{}", + interpolate ::js_sys::r#macro::js_import!( + direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & + ::js_sys::JsValue), ("arg1", & JsValue)), direct_call = "arg0_0.test = arg1_0", + indirect_call = "arg0_0.test = arg1_0", inputs = [("arg0", & ::js_sys::JsValue), + ("arg1", & JsValue)], ), } unsafe extern "C" { #[link_name = "test_crate.test"] fn test( - this: <&::js_sys::JsValue as ::js_sys::hazard::Input>::Type, - value: <&JsValue as ::js_sys::hazard::Input>::Type, + arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, + arg1_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg1_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg1_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg1_3: ::js_sys::r#macro::InputSlot4<&JsValue>, ); } - unsafe { - test( - ::js_sys::hazard::Input::into_raw(self), - ::js_sys::hazard::Input::into_raw(value), - ) + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { + ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) + }; + let (arg1_0, arg1_1, arg1_2, arg1_3) = + ::js_sys::r#macro::split_input::<&JsValue>(value); + unsafe { + test( + arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3, + ) + } }; } } @@ -296,13 +284,13 @@ fn setter() { \"test_crate.import.test\")) (param externref externref))) (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ externref))) - (func $test_crate.test (@sym) (param $self i32) (param $value i32) - local.get $self + (func $test_crate.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) + local.get $arg0_0 call $js_sys.externref.get (@reloc) - local.get $value + local.get $arg1_0 call $js_sys.externref.get (@reloc) call $test_crate.import.test (@reloc) )", - "(self, value) => self.test = value", + "(arg0_0, arg1_0) => arg0_0.test = arg1_0", ); } diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index ad739296..9abf9bfe 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -43,7 +43,7 @@ macro_rules! test { inline_snap!(output.clone(), $expected); let dir = tempfile::tempdir().unwrap(); - let (wat_output, js_import_output) = + let (wat_output, js_import_output, _) = crate::tests::r#macro::inner(dir.path(), &output).unwrap(); #[allow(clippy::allow_attributes, unused_assignments, unused_mut, reason = "depends on the input")] @@ -72,11 +72,12 @@ macro_rules! test { }}; } +mod export; mod function; mod member; mod r#type; -fn inner(tmp: &Path, source: &str) -> Result<(Option, Option)> { +fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Option)> { let js_sys = env::current_dir()? .parent() .and_then(Path::parent) @@ -188,6 +189,7 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option)> { let mut wat_output = None; let mut js_import_output = None; + let mut js_export_output = None; for message in Message::parse_stream(reader) { if let Message::CompilerArtifact(Artifact { @@ -233,6 +235,22 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option)> { js_import_output = Some(import.js.to_owned()); } + Payload::CustomSection(c) if c.name() == "js_bindgen.export" => { + let mut parser = JsBindgenJsSectionParser::new(&c); + let export = parser.next().unwrap(); + + if export.module != "test_crate" { + continue; + } + + ensure!( + parser.next().is_none(), + "found multiple JS export outputs in a single section: \ + {parser:?}" + ); + + js_export_output = Some(export.js.to_owned()); + } _ => (), } } @@ -243,5 +261,5 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option)> { } } - Ok((wat_output, js_import_output)) + Ok((wat_output, js_import_output, js_export_output)) } diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index 1d8bc531..ec5fd30f 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -23,35 +23,23 @@ fn basic() { } } - unsafe impl ::js_sys::hazard::Input for &JsString { - const WAT_TYPE: &'static ::core::primitive::str = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::InputWatConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::InputJsConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::JS_CONV; + unsafe impl ::js_sys::hazard::JsCast for JsString {} - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; + unsafe impl ::js_sys::hazard::IntoJS for JsString { + type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.0) + fn into_abi(self) -> Self::Abi { + ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::Output for JsString { - const WAT_TYPE: &::core::primitive::str = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::OutputWatConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::OutputJsConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::JS_CONV; - - type Type = <::js_sys::JsValue as ::js_sys::hazard::Output>::Type; + unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { + type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self(::js_sys::hazard::Output::from_raw(raw)) + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + ::js_sys::hazard::OptionIntoJS::option_into_abi( + value.map(::js_sys::JsValue::from), + ) } } }, @@ -88,38 +76,23 @@ fn generic() { } } - unsafe impl ::js_sys::hazard::Input for &JsString { - const WAT_TYPE: &'static ::core::primitive::str = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::InputWatConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::InputJsConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::JS_CONV; + unsafe impl ::js_sys::hazard::JsCast for JsString {} - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; + unsafe impl ::js_sys::hazard::IntoJS for JsString { + type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.value) + fn into_abi(self) -> Self::Abi { + ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::Output for JsString { - const WAT_TYPE: &::core::primitive::str = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::OutputWatConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::OutputJsConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::JS_CONV; - - type Type = <::js_sys::JsValue as ::js_sys::hazard::Output>::Type; + unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { + type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self { - value: ::js_sys::hazard::Output::from_raw(raw), - _type: ::core::marker::PhantomData, - } + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + ::js_sys::hazard::OptionIntoJS::option_into_abi( + value.map(::js_sys::JsValue::from), + ) } } }, @@ -156,38 +129,23 @@ fn default() { } } - unsafe impl ::js_sys::hazard::Input for &JsString { - const WAT_TYPE: &'static ::core::primitive::str = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::InputWatConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::InputJsConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::JS_CONV; + unsafe impl ::js_sys::hazard::JsCast for JsString {} - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; + unsafe impl ::js_sys::hazard::IntoJS for JsString { + type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.value) + fn into_abi(self) -> Self::Abi { + ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::Output for JsString { - const WAT_TYPE: &::core::primitive::str = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::OutputWatConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::OutputJsConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::JS_CONV; - - type Type = <::js_sys::JsValue as ::js_sys::hazard::Output>::Type; + unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { + type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self { - value: ::js_sys::hazard::Output::from_raw(raw), - _type: ::core::marker::PhantomData, - } + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + ::js_sys::hazard::OptionIntoJS::option_into_abi( + value.map(::js_sys::JsValue::from), + ) } } }, @@ -224,38 +182,23 @@ fn r#trait() { } } - unsafe impl ::js_sys::hazard::Input for &JsString { - const WAT_TYPE: &'static ::core::primitive::str = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::InputWatConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::InputJsConv> = - <&::js_sys::JsValue as ::js_sys::hazard::Input>::JS_CONV; + unsafe impl ::js_sys::hazard::JsCast for JsString {} - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; + unsafe impl ::js_sys::hazard::IntoJS for JsString { + type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.value) + fn into_abi(self) -> Self::Abi { + ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::Output for JsString { - const WAT_TYPE: &::core::primitive::str = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_TYPE; - const WAT_CONV: ::core::option::Option<::js_sys::hazard::OutputWatConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::WAT_CONV; - const JS_CONV: ::core::option::Option<::js_sys::hazard::OutputJsConv> = - <::js_sys::JsValue as ::js_sys::hazard::Output>::JS_CONV; - - type Type = <::js_sys::JsValue as ::js_sys::hazard::Output>::Type; + unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { + type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self { - value: ::js_sys::hazard::Output::from_raw(raw), - _type: ::core::marker::PhantomData, - } + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + ::js_sys::hazard::OptionIntoJS::option_into_abi( + value.map(::js_sys::JsValue::from), + ) } } }, diff --git a/host/js-sys-bindgen/src/tests/type.rs b/host/js-sys-bindgen/src/tests/type.rs index fe1eef38..8d26c93a 100644 --- a/host/js-sys-bindgen/src/tests/type.rs +++ b/host/js-sys-bindgen/src/tests/type.rs @@ -20,7 +20,7 @@ fn basic() { }, { use js_sys::JsValue; - use js_sys::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; + use js_sys::hazard::{IntoJS, JsCast, OptionIntoJS}; #[repr(transparent)] struct Test(JsValue); @@ -37,31 +37,24 @@ fn basic() { } } - unsafe impl Input for &Test { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; + unsafe impl JsCast for Test {} - type Type = <&'static JsValue as Input>::Type; + unsafe impl IntoJS for Test { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } - unsafe impl JsCast for Test {} - - unsafe impl Output for Test { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; + unsafe impl OptionIntoJS for Test { + type OptionAbi = ::OptionAbi; - type Type = ::Type; - - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } + }, ); } @@ -85,7 +78,7 @@ fn generic() { { use core::marker::PhantomData; use js_sys::JsValue; - use js_sys::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; + use js_sys::hazard::{IntoJS, JsCast, OptionIntoJS}; #[repr(transparent)] struct Test { @@ -105,34 +98,24 @@ fn generic() { } } - unsafe impl Input for &Test { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; + unsafe impl JsCast for Test {} - type Type = <&'static JsValue as Input>::Type; + unsafe impl IntoJS for Test { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.value) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } - unsafe impl JsCast for Test {} - - unsafe impl Output for Test { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; + unsafe impl OptionIntoJS for Test { + type OptionAbi = ::OptionAbi; - type Type = ::Type; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: Output::from_raw(raw), - _type: PhantomData, - } + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } + }, ); } diff --git a/host/js-sys-bindgen/src/tests/web_idl.rs b/host/js-sys-bindgen/src/tests/web_idl.rs index e637548c..ebd11406 100644 --- a/host/js-sys-bindgen/src/tests/web_idl.rs +++ b/host/js-sys-bindgen/src/tests/web_idl.rs @@ -8,7 +8,7 @@ fn basic() { { #file }, { use js_sys::JsValue; - use js_sys::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; + use js_sys::hazard::{IntoJS, JsCast, OptionIntoJS}; #[repr(transparent)] struct Test(JsValue); @@ -25,29 +25,21 @@ fn basic() { } } - unsafe impl Input for &Test { - const WAT_TYPE: &'static str = <&JsValue as Input>::WAT_TYPE; - const WAT_CONV: Option = <&JsValue as Input>::WAT_CONV; - const JS_CONV: Option = <&JsValue as Input>::JS_CONV; + unsafe impl JsCast for Test {} - type Type = <&'static JsValue as Input>::Type; + unsafe impl IntoJS for Test { + type Abi = ::Abi; - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } - unsafe impl JsCast for Test {} - - unsafe impl Output for Test { - const WAT_TYPE: &str = ::WAT_TYPE; - const WAT_CONV: Option = ::WAT_CONV; - const JS_CONV: Option = ::JS_CONV; - - type Type = ::Type; + unsafe impl OptionIntoJS for Test { + type OptionAbi = ::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + OptionIntoJS::option_into_abi(value.map(JsValue::from)) } } }, diff --git a/host/js-sys-bindgen/src/type.rs b/host/js-sys-bindgen/src/type.rs index a39d4f99..315a8923 100644 --- a/host/js-sys-bindgen/src/type.rs +++ b/host/js-sys-bindgen/src/type.rs @@ -30,26 +30,19 @@ impl Type { .collect(); let js_value = hygiene.js_value(&cfgs, span); - let input = hygiene.input(&cfgs, span); - let input_wat_conv = hygiene.input_wat_conv(&cfgs, span); - let input_js_conv = hygiene.input_js_conv(&cfgs, span); let js_cast = hygiene.js_cast(&cfgs, span); - let output = hygiene.output(&cfgs, span); - let output_wat_conv = hygiene.output_wat_conv(&cfgs, span); - let output_js_conv = hygiene.output_js_conv(&cfgs, span); + let into_js = hygiene.js_into(&cfgs, span); + let option_into_js = hygiene.js_option_into(&cfgs, span); let as_ref = hygiene.as_ref(span); - let str = hygiene.str(span); let from = hygiene.from(span); - let option = hygiene.option(span); let (gen_impl, gen_type, gen_where) = generics.split_for_impl(); - let (fields, semi_token, value, from_raw) = if generics.params.is_empty() { + let (fields, semi_token, value) = if generics.params.is_empty() { ( Fields::Unnamed(parse_quote_spanned! {span=>(#js_value)}), Some(Token![;](span)), quote_spanned! {span=>0}, - quote_spanned! {span=>Self(#output::from_raw(raw))}, ) } else { let phantom_data = hygiene.phantom_data(&cfgs, span); @@ -63,12 +56,6 @@ impl Type { }), None, quote_spanned! {span=>value}, - quote_spanned! {span=> - Self { - value: #output::from_raw(raw), - _type: #phantom_data, - } - }, ) }; @@ -91,33 +78,25 @@ impl Type { }, parse_quote_spanned! {span=> #(#cfgs)* - unsafe impl #gen_impl #input for &#ident #gen_type #gen_where { - const WAT_TYPE: &'static #str = <&#js_value as #input>::WAT_TYPE; - const WAT_CONV: #option<#input_wat_conv> = <&#js_value as #input>::WAT_CONV; - const JS_CONV: #option<#input_js_conv> = <&#js_value as #input>::JS_CONV; - - type Type = <&'static #js_value as #input>::Type; - - fn into_raw(self) -> Self::Type { - #input::into_raw(&self.#value) - } - } + unsafe impl #gen_impl #js_cast for #ident #gen_type #gen_where {} }, parse_quote_spanned! {span=> #(#cfgs)* - unsafe impl #gen_impl #js_cast for #ident #gen_type #gen_where {} + unsafe impl #gen_impl #into_js for #ident #gen_type #gen_where { + type Abi = <#js_value as #into_js>::Abi; + + fn into_abi(self) -> Self::Abi { + #into_js::into_abi(#js_value::from(self)) + } + } }, parse_quote_spanned! {span=> #(#cfgs)* - unsafe impl #gen_impl #output for #ident #gen_type #gen_where { - const WAT_TYPE: &#str = <#js_value as #output>::WAT_TYPE; - const WAT_CONV: #option<#output_wat_conv> = <#js_value as #output>::WAT_CONV; - const JS_CONV: #option<#output_js_conv> = <#js_value as #output>::JS_CONV; - - type Type = <#js_value as #output>::Type; + unsafe impl #gen_impl #option_into_js for #ident #gen_type #gen_where { + type OptionAbi = <#js_value as #option_into_js>::OptionAbi; - fn from_raw(raw: Self::Type) -> Self { - #from_raw + fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { + #option_into_js::option_into_abi(value.map(#js_value::from)) } } }, diff --git a/host/ld/src/js.rs b/host/ld/src/js.rs index f9254b9f..5f1d96c5 100644 --- a/host/ld/src/js.rs +++ b/host/ld/src/js.rs @@ -15,6 +15,8 @@ pub struct JsStore { embed: FixedHashMap>, expected_embed: HashMap>, provided_embed: HashMap>, + export: FixedHashMap, + export_module: FixedHashMap, } struct JsWithEmbeds { @@ -137,6 +139,40 @@ impl JsStore { Ok(()) } + pub fn add_js_exports( + &mut self, + custom_section: &CustomSectionReader<'_>, + ) -> Result> { + let mut names = Vec::new(); + + for export in JsBindgenJsSectionParser::new(custom_section) { + if let Some(previous) = self.export.get(export.name) { + let previous_module = &self.export_module[export.name]; + bail!( + "found multiple JS exports named `{}` from `{}` and `{}`\n JS Export \ + 1:\n{}\n JS Export 2:\n{}", + export.name, + previous_module, + export.module, + previous, + export.js, + ); + } + + self.export + .insert(export.name.to_owned(), export.js.to_owned()); + self.export_module + .insert(export.name.to_owned(), export.module.to_owned()); + names.push(export.name.to_owned()); + + for embed in export.embeds { + self.require_js_embed(embed.into()); + } + } + + Ok(names) + } + fn require_js_embed(&mut self, embed: JsEmbed) { if !self .embed @@ -180,6 +216,7 @@ impl JsStore { main_memory, js_import: self.import, js_embed: self.embed, + js_export: self.export, } } } diff --git a/host/ld/src/post.rs b/host/ld/src/post.rs index d805f2ee..40ad7cc4 100644 --- a/host/ld/src/post.rs +++ b/host/ld/src/post.rs @@ -2,8 +2,8 @@ use anyhow::{Context, Result, bail}; use js_bindgen_cli_lib::{JS_OUTPUT_SECTION, MainMemory}; use js_bindgen_shared::{IS_COMPAT_SECTION, IS_TEST_SECTION}; use wasm_encoder::{ - CustomSection, EntityType, ImportSection, Module, ProducersField, ProducersSection, RawSection, - Section, + CustomSection, EntityType, ExportSection, ImportSection, Module, ProducersField, + ProducersSection, RawSection, Section, }; use wasmparser::{Encoding, KnownCustom, Parser, Payload, TypeRef}; @@ -57,10 +57,26 @@ pub fn processing( import_section.append_to(&mut wasm_output); } + // The WAT adapters need these symbols during linking, but callers should only + // see the public adapters in the final module. + Payload::ExportSection(exports) => { + let mut export_section = ExportSection::new(); + + for export in exports { + let export = export.context("export should be parsable")?; + + if !export.name.starts_with("__export_") { + export_section.export(export.name, export.kind.into(), export.index); + } + } + + export_section.append_to(&mut wasm_output); + } // Don't write back our own custom sections. Payload::CustomSection(c) if c.name() == "js_bindgen.wat" => (), Payload::CustomSection(c) if c.name() == "js_bindgen.import" => (), Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => (), + Payload::CustomSection(c) if c.name() == "js_bindgen.export" => (), // Register ourselves in the producer section. Payload::CustomSection(c) if c.name() == "producers" => { let KnownCustom::Producers(c) = c.as_known() else { diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index 7a747eda..cdd3c65e 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -151,6 +151,12 @@ fn process_object( Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => { js_store.add_js_embeds(c)?; } + // Extract JS export wrappers and keep their WAT adapter symbols alive. + Payload::CustomSection(c) if c.name() == "js_bindgen.export" => { + for name in js_store.add_js_exports(c)? { + add_args.push(format!("--export={name}").into()); + } + } _ => (), } } diff --git a/host/macro/src/lib.rs b/host/macro/src/lib.rs index b5af3fce..76cc09f6 100644 --- a/host/macro/src/lib.rs +++ b/host/macro/src/lib.rs @@ -61,6 +61,21 @@ fn import_js_internal(input: TokenStream) -> Result { js_internal(input, "js_bindgen.import") } +#[proc_macro] +pub fn export_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream { + #[cfg_attr( + not(test), + expect(clippy::useless_conversion, reason = "`proc-macro2` compatibility") + )] + export_js_internal(input.into()) + .unwrap_or_else(|e| e) + .into() +} + +fn export_js_internal(input: TokenStream) -> Result { + js_internal(input, "js_bindgen.export") +} + fn js_internal(input: TokenStream, section: &str) -> Result { let mut input = input.into_iter().peekable(); From 9f4e6f44334337be4e4a53f20fffc54f8486fcbc Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:02:08 +0800 Subject: [PATCH 02/21] Add `wasm*-web-wabi` targets --- .gitignore | 2 + client/Cargo.toml | 6 +- client/wabii/Cargo.toml | 19 ++++ client/wabii/random.64.ron | 24 +++++ client/wabii/random.ron | 20 ++++ client/wabii/src/lib.rs | 24 +++++ client/wabii/src/random.64.wat | 51 ++++++++++ client/wabii/src/random.rs | 33 ++++++ client/wabii/src/random.wat | 47 +++++++++ client/wabii/src/stdio.64.wat | 90 +++++++++++++++++ client/wabii/src/stdio.rs | 29 ++++++ client/wabii/src/stdio.wat | 88 ++++++++++++++++ client/wabii/src/time.rs | 28 ++++++ client/wabii/src/time.wat | 64 ++++++++++++ client/wabii/stdio.64.ron | 48 +++++++++ client/wabii/stdio.ron | 46 +++++++++ client/wabii/time.ron | 24 +++++ client/wabii/update.sh | 11 ++ host/Cargo.toml | 1 + host/dev/Cargo.toml | 2 + host/dev/src/client/test.rs | 22 +++- host/dev/src/codegen.rs | 178 +++++++++++++++++++++++++++++++++ host/dev/src/main.rs | 4 + host/ld/src/pre.rs | 33 +++++- host/tsconfig.base.json | 3 +- web/.cargo/audit.toml | 9 ++ web/.cargo/config.toml | 9 ++ web/Cargo.toml | 33 ++++++ web/playground/Cargo.toml | 18 ++++ web/playground/benches/conv.rs | 38 +++++++ web/playground/src/lib.rs | 6 ++ web/playground/src/main.rs | 36 +++++++ web/rust-toolchain.toml | 4 + 33 files changed, 1045 insertions(+), 5 deletions(-) create mode 100644 client/wabii/Cargo.toml create mode 100644 client/wabii/random.64.ron create mode 100644 client/wabii/random.ron create mode 100644 client/wabii/src/lib.rs create mode 100644 client/wabii/src/random.64.wat create mode 100644 client/wabii/src/random.rs create mode 100644 client/wabii/src/random.wat create mode 100644 client/wabii/src/stdio.64.wat create mode 100644 client/wabii/src/stdio.rs create mode 100644 client/wabii/src/stdio.wat create mode 100644 client/wabii/src/time.rs create mode 100644 client/wabii/src/time.wat create mode 100644 client/wabii/stdio.64.ron create mode 100644 client/wabii/stdio.ron create mode 100644 client/wabii/time.ron create mode 100755 client/wabii/update.sh create mode 100644 host/dev/src/codegen.rs create mode 100644 web/.cargo/audit.toml create mode 100644 web/.cargo/config.toml create mode 100644 web/Cargo.toml create mode 100644 web/playground/Cargo.toml create mode 100644 web/playground/benches/conv.rs create mode 100644 web/playground/src/lib.rs create mode 100644 web/playground/src/main.rs create mode 100644 web/rust-toolchain.toml diff --git a/.gitignore b/.gitignore index 30ec4d6f..7dd545c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /host/target /host/Cargo.lock /rust-toolchain.toml +/web/target +/web/Cargo.lock diff --git a/client/Cargo.toml b/client/Cargo.toml index d122cba3..4c207c6c 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,7 +6,7 @@ publish = false [workspace] resolver = "3" -members = ["e2e", "js-bindgen", "js-sys", "test", "web-sys"] +members = ["e2e", "js-bindgen", "js-sys", "test", "wabii", "web-sys"] [workspace.package] edition = "2024" @@ -15,14 +15,16 @@ license = "MIT OR Apache-2.0" include = [ "!/src/**/*.js-sys.rs", "!/src/**/tests/**", + "/*.ron", "/Cargo.toml", "/LICENSE-APACHE", "/LICENSE-MIT", "/src/**/*.rs", + "/src/**/*.wat", ] [workspace.dependencies] -js-bindgen = { path = "js-bindgen" } +js-bindgen = { path = "js-bindgen", default-features = false } js-bindgen-macro = { path = "../host/macro" } js-bindgen-test = { path = "test" } js-bindgen-test-macro = { path = "../host/test-macro" } diff --git a/client/wabii/Cargo.toml b/client/wabii/Cargo.toml new file mode 100644 index 00000000..194534f9 --- /dev/null +++ b/client/wabii/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "wabii" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +include.workspace = true + +[dependencies] +core = { package = "rustc-std-workspace-core", version = "1.0.0", optional = true } + +[dev-dependencies] +js-bindgen-test = { workspace = true } + +[features] +rustc-dep-of-std = ["dep:core"] + +[lints] +workspace = true diff --git a/client/wabii/random.64.ron b/client/wabii/random.64.ron new file mode 100644 index 00000000..3a8a62d5 --- /dev/null +++ b/client/wabii/random.64.ron @@ -0,0 +1,24 @@ +( + imports: [ + ( + module: "wabii", + name: "random.atomics_fill", + js: r#"(ptr, len) => { + ptr = Number(ptr) + len = Number(len) + const bytes = new Uint8Array(len) + globalThis.crypto.getRandomValues(bytes) + new Uint8Array(this.#memory.buffer, ptr, len).set(bytes) +}"#, + ), + ( + module: "wabii", + name: "random.fill", + js: r#"(ptr, len) => { + ptr = Number(ptr) + len = Number(len) + globalThis.crypto.getRandomValues(new Uint8Array(this.#memory.buffer, ptr, len)) +}"#, + ), + ], +) diff --git a/client/wabii/random.ron b/client/wabii/random.ron new file mode 100644 index 00000000..0b984e5a --- /dev/null +++ b/client/wabii/random.ron @@ -0,0 +1,20 @@ +( + imports: [ + ( + module: "wabii", + name: "random.atomics_fill", + js: r#"(ptr, len) => { + const bytes = new Uint8Array(len) + globalThis.crypto.getRandomValues(bytes) + new Uint8Array(this.#memory.buffer, ptr, len).set(bytes) +}"#, + ), + ( + module: "wabii", + name: "random.fill", + js: r#"(ptr, len) => { + globalThis.crypto.getRandomValues(new Uint8Array(this.#memory.buffer, ptr, len)) +}"#, + ), + ], +) diff --git a/client/wabii/src/lib.rs b/client/wabii/src/lib.rs new file mode 100644 index 00000000..3c231162 --- /dev/null +++ b/client/wabii/src/lib.rs @@ -0,0 +1,24 @@ +#![no_std] + +macro_rules! include_wat { + ($path:literal) => { + #[expect(unused, reason = "link_section")] + const _: () = { + const WAT: &[u8] = include_bytes!($path); + + #[repr(C)] + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; N]); + + #[unsafe(link_section = "js_bindgen.wat")] + static CUSTOM_SECTION: Layout<{ WAT.len() }> = Layout( + #[expect(clippy::cast_possible_truncation, reason = "link_section")] + ::core::primitive::u32::to_le_bytes(WAT.len() as ::core::primitive::u32), + *include_bytes!($path), + ); + }; + }; +} + +pub mod random; +pub mod stdio; +pub mod time; diff --git a/client/wabii/src/random.64.wat b/client/wabii/src/random.64.wat new file mode 100644 index 00000000..46f2be1d --- /dev/null +++ b/client/wabii/src/random.64.wat @@ -0,0 +1,51 @@ +;; @generated by `cargo run -p js-bindgen-dev -- codegen --input --output-dir `. +;; Do not edit by hand. + +(@custom "js_bindgen.import" + ;; wabii:random.atomics_fill + ;; record length: 224 + "\e0\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "random.atomics_fill" + "\13\00" + "random.atomics_fill" + + ;; required_embeds = 0 + "\00" + + ;; js + "(ptr, len) => {\0a" + " ptr = Number(ptr)\0a" + " len = Number(len)\0a" + " const bytes = new Uint8Array(len)\0a" + " globalThis.crypto.getRandomValues(bytes)\0a" + " new Uint8Array(this.#memory.buffer, ptr, len).set(bytes)\0a" + "}" + + ;; wabii:random.fill + ;; record length: 161 + "\a1\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "random.fill" + "\0b\00" + "random.fill" + + ;; required_embeds = 0 + "\00" + + ;; js + "(ptr, len) => {\0a" + " ptr = Number(ptr)\0a" + " len = Number(len)\0a" + " globalThis.crypto.getRandomValues(new Uint8Array(this.#memory.buffer, ptr, len))\0a" + "}" + +) diff --git a/client/wabii/src/random.rs b/client/wabii/src/random.rs new file mode 100644 index 00000000..4f5932b2 --- /dev/null +++ b/client/wabii/src/random.rs @@ -0,0 +1,33 @@ +#[link(wasm_import_module = "wabii")] +unsafe extern "C" { + #[cfg(target_feature = "atomics")] + #[link_name = "random.atomics_fill"] + pub fn random_fill(ptr: *mut u8, len: usize); + #[cfg(not(target_feature = "atomics"))] + #[link_name = "random.fill"] + pub fn random_fill(ptr: *mut u8, len: usize); +} + +#[cfg(target_arch = "wasm32")] +include_wat!("random.wat"); +#[cfg(target_arch = "wasm64")] +include_wat!("random.64.wat"); + +#[cfg(test)] +mod tests { + use js_bindgen_test::test; + + use super::random_fill; + + #[test] + pub fn test_random_fill() { + let mut buf1 = [0; 10]; + let mut buf2 = [0; 10]; + #[expect(clippy::undocumented_unsafe_blocks, reason = "just test")] + unsafe { + random_fill(buf1.as_mut_ptr(), buf1.len()); + random_fill(buf2.as_mut_ptr(), buf2.len()); + } + assert_ne!(buf1, buf2); + } +} diff --git a/client/wabii/src/random.wat b/client/wabii/src/random.wat new file mode 100644 index 00000000..d29a8e52 --- /dev/null +++ b/client/wabii/src/random.wat @@ -0,0 +1,47 @@ +;; @generated by `cargo run -p js-bindgen-dev -- codegen --input --output-dir `. +;; Do not edit by hand. + +(@custom "js_bindgen.import" + ;; wabii:random.atomics_fill + ;; record length: 184 + "\b8\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "random.atomics_fill" + "\13\00" + "random.atomics_fill" + + ;; required_embeds = 0 + "\00" + + ;; js + "(ptr, len) => {\0a" + " const bytes = new Uint8Array(len)\0a" + " globalThis.crypto.getRandomValues(bytes)\0a" + " new Uint8Array(this.#memory.buffer, ptr, len).set(bytes)\0a" + "}" + + ;; wabii:random.fill + ;; record length: 121 + "\79\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "random.fill" + "\0b\00" + "random.fill" + + ;; required_embeds = 0 + "\00" + + ;; js + "(ptr, len) => {\0a" + " globalThis.crypto.getRandomValues(new Uint8Array(this.#memory.buffer, ptr, len))\0a" + "}" + +) diff --git a/client/wabii/src/stdio.64.wat b/client/wabii/src/stdio.64.wat new file mode 100644 index 00000000..b59e32e1 --- /dev/null +++ b/client/wabii/src/stdio.64.wat @@ -0,0 +1,90 @@ +;; @generated by `cargo run -p js-bindgen-dev -- codegen --input --output-dir `. +;; Do not edit by hand. + +(@custom "js_bindgen.import" + ;; wabii:stdio.stdout + ;; record length: 121 + "\79\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "stdio.stdout" + "\0c\00" + "stdio.stdout" + + ;; required_embeds = 1 + "\01" + "\05\00" + "wabii" + "\0c\00" + "stdio.writer" + + ;; js + "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.log(line))" + + ;; wabii:stdio.stderr + ;; record length: 123 + "\7b\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "stdio.stderr" + "\0c\00" + "stdio.stderr" + + ;; required_embeds = 1 + "\01" + "\05\00" + "wabii" + "\0c\00" + "stdio.writer" + + ;; js + "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.error(line))" + +) +(@custom "js_bindgen.embed" + ;; wabii:stdio.writer + ;; record length: 650 + "\8a\02\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "stdio.writer" + "\0c\00" + "stdio.writer" + + ;; required_embeds = 0 + "\00" + + ;; js + "(memory, write) => {\0a" + " const decoder = new TextDecoder('utf-8', {\0a" + " fatal: false,\0a" + " ignoreBOM: false,\0a" + " })\0a" + " let buffer = ''\0a" + " return (ptr, len) => {\0a" + " ptr = Number(ptr)\0a" + " len = Number(len)\0a" + " const view = new Uint8Array(memory.buffer, ptr, len)\0a" + " const input = memory.buffer instanceof ArrayBuffer ? view : view.slice()\0a" + " buffer += decoder.decode(input, { stream: true })\0a" + " for (;;) {\0a" + " const newline = buffer.indexOf('\\n')\0a" + " if (newline === -1) {\0a" + " break\0a" + " }\0a" + " write(buffer.slice(0, newline))\0a" + " buffer = buffer.slice(newline + 1)\0a" + " }\0a" + " }\0a" + "}" + +) diff --git a/client/wabii/src/stdio.rs b/client/wabii/src/stdio.rs new file mode 100644 index 00000000..7fe7dd5e --- /dev/null +++ b/client/wabii/src/stdio.rs @@ -0,0 +1,29 @@ +#[link(wasm_import_module = "wabii")] +unsafe extern "C" { + #[link_name = "stdio.stdout"] + pub fn stdout(ptr: *const u8, len: usize); + #[link_name = "stdio.stderr"] + pub fn stderr(ptr: *const u8, len: usize); +} + +#[cfg(target_arch = "wasm32")] +include_wat!("stdio.wat"); +#[cfg(target_arch = "wasm64")] +include_wat!("stdio.64.wat"); + +#[cfg(test)] +mod tests { + use js_bindgen_test::test; + + use super::{stderr, stdout}; + + #[test] + pub fn test_stdio() { + let text = b"hello world\n"; + #[expect(clippy::undocumented_unsafe_blocks, reason = "just test")] + unsafe { + stdout(text.as_ptr(), text.len()); + stderr(text.as_ptr(), text.len()); + } + } +} diff --git a/client/wabii/src/stdio.wat b/client/wabii/src/stdio.wat new file mode 100644 index 00000000..4b6bb78f --- /dev/null +++ b/client/wabii/src/stdio.wat @@ -0,0 +1,88 @@ +;; @generated by `cargo run -p js-bindgen-dev -- codegen --input --output-dir `. +;; Do not edit by hand. + +(@custom "js_bindgen.import" + ;; wabii:stdio.stdout + ;; record length: 121 + "\79\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "stdio.stdout" + "\0c\00" + "stdio.stdout" + + ;; required_embeds = 1 + "\01" + "\05\00" + "wabii" + "\0c\00" + "stdio.writer" + + ;; js + "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.log(line))" + + ;; wabii:stdio.stderr + ;; record length: 123 + "\7b\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "stdio.stderr" + "\0c\00" + "stdio.stderr" + + ;; required_embeds = 1 + "\01" + "\05\00" + "wabii" + "\0c\00" + "stdio.writer" + + ;; js + "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.error(line))" + +) +(@custom "js_bindgen.embed" + ;; wabii:stdio.writer + ;; record length: 602 + "\5a\02\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "stdio.writer" + "\0c\00" + "stdio.writer" + + ;; required_embeds = 0 + "\00" + + ;; js + "(memory, write) => {\0a" + " const decoder = new TextDecoder('utf-8', {\0a" + " fatal: false,\0a" + " ignoreBOM: false,\0a" + " })\0a" + " let buffer = ''\0a" + " return (ptr, len) => {\0a" + " const view = new Uint8Array(memory.buffer, ptr, len)\0a" + " const input = memory.buffer instanceof ArrayBuffer ? view : view.slice()\0a" + " buffer += decoder.decode(input, { stream: true })\0a" + " for (;;) {\0a" + " const newline = buffer.indexOf('\\n')\0a" + " if (newline === -1) {\0a" + " break\0a" + " }\0a" + " write(buffer.slice(0, newline))\0a" + " buffer = buffer.slice(newline + 1)\0a" + " }\0a" + " }\0a" + "}" + +) diff --git a/client/wabii/src/time.rs b/client/wabii/src/time.rs new file mode 100644 index 00000000..bd74c6c1 --- /dev/null +++ b/client/wabii/src/time.rs @@ -0,0 +1,28 @@ +#[link(wasm_import_module = "wabii")] +unsafe extern "C" { + #[cfg(not(target_feature = "atomics"))] + #[link_name = "time.performance_now"] + pub safe fn performance_now() -> f64; + #[cfg(target_feature = "atomics")] + #[link_name = "time.atomic_performance_now"] + pub safe fn performance_now() -> f64; + #[link_name = "time.date_now"] + pub safe fn date_now() -> f64; +} + +include_wat!("time.wat"); + +#[cfg(test)] +mod tests { + use js_bindgen_test::test; + + use super::{date_now, performance_now}; + + #[test] + pub fn test_now() { + let now1 = performance_now(); + let now2 = date_now(); + assert!(performance_now() - now1 >= 0.0); + assert!(date_now() - now2 >= 0.0); + } +} diff --git a/client/wabii/src/time.wat b/client/wabii/src/time.wat new file mode 100644 index 00000000..bee92347 --- /dev/null +++ b/client/wabii/src/time.wat @@ -0,0 +1,64 @@ +;; @generated by `cargo run -p js-bindgen-dev -- codegen --input --output-dir `. +;; Do not edit by hand. + +(@custom "js_bindgen.import" + ;; wabii:time.performance_now + ;; record length: 64 + "\40\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "time.performance_now" + "\14\00" + "time.performance_now" + + ;; required_embeds = 0 + "\00" + + ;; js + "() => globalThis.performance.now()" + + ;; wabii:time.atomic_performance_now + ;; record length: 171 + "\ab\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "time.atomic_performance_now" + "\1b\00" + "time.atomic_performance_now" + + ;; required_embeds = 0 + "\00" + + ;; js + "(() => {\0a" + " const origin = globalThis.performance.timeOrigin\0a" + " return () => {\0a" + " return origin + globalThis.performance.now()\0a" + " }\0a" + "})()" + + ;; wabii:time.date_now + ;; record length: 31 + "\1f\00\00\00" + + ;; module = "wabii" + "\05\00" + "wabii" + + ;; name = "time.date_now" + "\0d\00" + "time.date_now" + + ;; required_embeds = 0 + "\00" + + ;; js + "Date.now" + +) diff --git a/client/wabii/stdio.64.ron b/client/wabii/stdio.64.ron new file mode 100644 index 00000000..cffcd569 --- /dev/null +++ b/client/wabii/stdio.64.ron @@ -0,0 +1,48 @@ +( + embeds: [ + ( + module: "wabii", + name: "stdio.writer", + js: r#"(memory, write) => { + const decoder = new TextDecoder('utf-8', { + fatal: false, + ignoreBOM: false, + }) + let buffer = '' + return (ptr, len) => { + ptr = Number(ptr) + len = Number(len) + const view = new Uint8Array(memory.buffer, ptr, len) + const input = memory.buffer instanceof ArrayBuffer ? view : view.slice() + buffer += decoder.decode(input, { stream: true }) + for (;;) { + const newline = buffer.indexOf('\n') + if (newline === -1) { + break + } + write(buffer.slice(0, newline)) + buffer = buffer.slice(newline + 1) + } + } +}"#, + ), + ], + imports: [ + ( + module: "wabii", + name: "stdio.stdout", + required_embeds: [ + (module: "wabii", name: "stdio.writer"), + ], + js: "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.log(line))", + ), + ( + module: "wabii", + name: "stdio.stderr", + required_embeds: [ + (module: "wabii", name: "stdio.writer"), + ], + js: "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.error(line))", + ), + ], +) diff --git a/client/wabii/stdio.ron b/client/wabii/stdio.ron new file mode 100644 index 00000000..97c2f983 --- /dev/null +++ b/client/wabii/stdio.ron @@ -0,0 +1,46 @@ +( + embeds: [ + ( + module: "wabii", + name: "stdio.writer", + js: r#"(memory, write) => { + const decoder = new TextDecoder('utf-8', { + fatal: false, + ignoreBOM: false, + }) + let buffer = '' + return (ptr, len) => { + const view = new Uint8Array(memory.buffer, ptr, len) + const input = memory.buffer instanceof ArrayBuffer ? view : view.slice() + buffer += decoder.decode(input, { stream: true }) + for (;;) { + const newline = buffer.indexOf('\n') + if (newline === -1) { + break + } + write(buffer.slice(0, newline)) + buffer = buffer.slice(newline + 1) + } + } +}"#, + ), + ], + imports: [ + ( + module: "wabii", + name: "stdio.stdout", + required_embeds: [ + (module: "wabii", name: "stdio.writer"), + ], + js: "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.log(line))", + ), + ( + module: "wabii", + name: "stdio.stderr", + required_embeds: [ + (module: "wabii", name: "stdio.writer"), + ], + js: "this.#jsEmbed.wabii['stdio.writer'](this.#memory, (line) => console.error(line))", + ), + ], +) diff --git a/client/wabii/time.ron b/client/wabii/time.ron new file mode 100644 index 00000000..f712704f --- /dev/null +++ b/client/wabii/time.ron @@ -0,0 +1,24 @@ +( + imports: [ + ( + module: "wabii", + name: "time.performance_now", + js: "() => globalThis.performance.now()", + ), + ( + module: "wabii", + name: "time.atomic_performance_now", + js: r#"(() => { + const origin = globalThis.performance.timeOrigin + return () => { + return origin + globalThis.performance.now() + } +})()"#, + ), + ( + module: "wabii", + name: "time.date_now", + js: "Date.now", + ), + ], +) diff --git a/client/wabii/update.sh b/client/wabii/update.sh new file mode 100755 index 00000000..80205332 --- /dev/null +++ b/client/wabii/update.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +cd ../../ +wabii="$PWD/client/wabii" +cd host + +for input in "$wabii"/*.ron; do + cargo run -p js-bindgen-dev -- codegen \ + --input "$input" \ + --output-dir "$wabii/src" +done diff --git a/host/Cargo.toml b/host/Cargo.toml index 7e64b1e5..d7d60cee 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -67,6 +67,7 @@ prettyplease = { version = "0.2", features = ["verbatim"] } proc-macro2 = { version = "1", default-features = false } quote = { version = "1", default-features = false } reqwest = { version = "0.13", default-features = false, features = ["http2"] } +ron = "0.12" rwat = "0.1" serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false, features = ["alloc"] } diff --git a/host/dev/Cargo.toml b/host/dev/Cargo.toml index 25727f05..19a7b61f 100644 --- a/host/dev/Cargo.toml +++ b/host/dev/Cargo.toml @@ -14,6 +14,8 @@ anyhow = { workspace = true } cargo_metadata = { workspace = true } clap = { workspace = true } paste = { workspace = true } +ron = { workspace = true } +serde = { workspace = true, features = ["derive"] } strum = { workspace = true } tempfile = { workspace = true } diff --git a/host/dev/src/client/test.rs b/host/dev/src/client/test.rs index 2c897f79..0807cbb7 100644 --- a/host/dev/src/client/test.rs +++ b/host/dev/src/client/test.rs @@ -127,16 +127,29 @@ impl Test { for test_run in &test_runs { if !built { + // `wabii`'s `rustc-dep-of-std` feature links against the `sysroot`'s + // `core`. Test it separately without enabling that internal feature. let mut command = util::cargo(&permutation, &self.args.nightly_toolchain, "test"); command .arg("--workspace") .arg("--all-features") + .args(["--exclude", "wabii"]) .arg("--no-run"); build_time += command::run(&format!("Build Tests - {permutation}"), command, verbose)?; + let mut command = + util::cargo(&permutation, &self.args.nightly_toolchain, "test"); + command.args(["-p", "wabii", "--no-run"]); + + build_time += command::run( + &format!("Build Tests `wabii` - {permutation}"), + command, + verbose, + )?; + built = true; } @@ -144,9 +157,16 @@ impl Test { command .envs(test_run.envs()) .arg("--workspace") - .arg("--all-features"); + .arg("--all-features") + .args(["--exclude", "wabii"]); test_time += command::run(&format!("Run Tests - {test_run}"), command, verbose)?; + + let mut command = util::cargo(&permutation, &self.args.nightly_toolchain, "test"); + command.envs(test_run.envs()).args(["-p", "wabii"]); + + test_time += + command::run(&format!("Run Tests `wabii` - {test_run}"), command, verbose)?; } if test_runs diff --git a/host/dev/src/codegen.rs b/host/dev/src/codegen.rs new file mode 100644 index 00000000..054e3858 --- /dev/null +++ b/host/dev/src/codegen.rs @@ -0,0 +1,178 @@ +use std::fmt::Write as _; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use clap::Args; +use serde::Deserialize; + +#[derive(Args)] +pub struct Codegen { + #[arg(long)] + input: PathBuf, + #[arg(long)] + output_dir: PathBuf, +} + +#[derive(Deserialize)] +struct Spec { + #[serde(default)] + imports: Vec, + #[serde(default)] + embeds: Vec, +} + +#[derive(Deserialize)] +struct JsEntry { + module: String, + name: String, + #[serde(default)] + required_embeds: Vec, + js: String, +} + +#[derive(Deserialize)] +struct RequiredEmbed { + module: String, + name: String, +} + +impl Codegen { + pub fn execute(self) -> Result<()> { + let spec = read_spec(&self.input)?; + let wat = generate_wat(&spec); + let name = self + .input + .file_stem() + .and_then(|name| name.to_str()) + .with_context(|| format!("failed to get file stem from `{}`", self.input.display()))?; + + let path = self.output_dir.join(format!("{name}.wat")); + fs::write(&path, wat).with_context(|| format!("failed to write `{}`", path.display()))?; + println!("generated {}", path.display()); + + Ok(()) + } +} + +fn read_spec(path: &Path) -> Result { + let input = + fs::read_to_string(path).with_context(|| format!("failed to read `{}`", path.display()))?; + + ron::from_str(&input).with_context(|| format!("failed to parse `{}`", path.display())) +} + +fn generate_wat(spec: &Spec) -> String { + let mut output = String::from( + ";; @generated by `cargo run -p js-bindgen-dev -- codegen --input --output-dir \ + `.\n;; Do not edit by hand.\n\n", + ); + + format_section(&mut output, "js_bindgen.import", &spec.imports); + format_section(&mut output, "js_bindgen.embed", &spec.embeds); + + output +} + +fn format_section(output: &mut String, section: &str, entries: &[JsEntry]) { + if entries.is_empty() { + return; + } + + writeln!(output, "(@custom {section:?}").unwrap(); + + for entry in entries { + format_entry(output, entry); + } + + writeln!(output, ")").unwrap(); +} + +fn format_entry(output: &mut String, entry: &JsEntry) { + let record_len = record_len(entry); + + writeln!(output, " ;; {}:{}", entry.module, entry.name).unwrap(); + writeln!(output, " ;; record length: {record_len}").unwrap(); + write_binary_string(output, &record_len.to_le_bytes()); + writeln!(output).unwrap(); + + writeln!(output, " ;; module = {:?}", entry.module).unwrap(); + write_len_prefixed_string(output, &entry.module); + writeln!(output).unwrap(); + + writeln!(output, " ;; name = {:?}", entry.name).unwrap(); + write_len_prefixed_string(output, &entry.name); + writeln!(output).unwrap(); + + writeln!( + output, + " ;; required_embeds = {}", + entry.required_embeds.len() + ) + .unwrap(); + let required_embeds_len = + u8::try_from(entry.required_embeds.len()).expect("too many required JS embeds"); + write_binary_string(output, &[required_embeds_len]); + for embed in &entry.required_embeds { + write_len_prefixed_string(output, &embed.module); + write_len_prefixed_string(output, &embed.name); + } + writeln!(output).unwrap(); + + writeln!(output, " ;; js").unwrap(); + let mut lines = entry.js.lines().peekable(); + while let Some(line) = lines.next() { + write_text_string(output, line, lines.peek().is_some()); + } + writeln!(output).unwrap(); +} + +fn record_len(entry: &JsEntry) -> u32 { + let mut len = 0usize; + len += 2 + entry.module.len(); + len += 2 + entry.name.len(); + len += 1; + + for embed in &entry.required_embeds { + len += 2 + embed.module.len(); + len += 2 + embed.name.len(); + } + len += entry.js.len(); + + u32::try_from(len).expect("JS entry is too large") +} + +fn write_len_prefixed_string(output: &mut String, value: &str) { + let len = u16::try_from(value.len()).expect("JS string is too long"); + write_binary_string(output, &len.to_le_bytes()); + write_text_string(output, value, false); +} + +fn write_binary_string(output: &mut String, bytes: &[u8]) { + output.push_str(" \""); + + for byte in bytes { + write!(output, "\\{byte:02x}").unwrap(); + } + + output.push_str("\"\n"); +} + +fn write_text_string(output: &mut String, value: &str, newline: bool) { + output.push_str(" \""); + + for byte in value.bytes() { + match byte { + b'"' => output.push_str("\\\""), + b'\\' => output.push_str("\\\\"), + 0x20..=0x7e => output.push(byte.into()), + _ => write!(output, "\\{byte:02x}").unwrap(), + } + } + + if newline { + output.push_str("\\0a"); + } + + output.push_str("\"\n"); +} diff --git a/host/dev/src/main.rs b/host/dev/src/main.rs index 0703116f..05da3999 100644 --- a/host/dev/src/main.rs +++ b/host/dev/src/main.rs @@ -2,6 +2,7 @@ mod util; mod check; mod client; +mod codegen; mod command; mod features; mod host; @@ -15,6 +16,7 @@ use strum::EnumIter; use self::check::Check; use self::client::Client; +use self::codegen::Codegen; use self::host::Host; #[derive(Parser)] @@ -52,6 +54,7 @@ enum CliCommand { #[command(subcommand)] host: Host, }, + Codegen(Codegen), } fn main() -> Result<()> { @@ -141,6 +144,7 @@ impl CliCommand { } Self::Client { client } => client.execute(verbose), Self::Host { host } => host.execute(verbose), + Self::Codegen(codegen) => codegen.execute(), } } } diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index cdd3c65e..21f29c44 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -6,6 +6,7 @@ use std::time::SystemTime; use anyhow::Result; use js_bindgen_cli_lib::MainMemory; use js_bindgen_ld_shared::JsBindgenWatSectionParser; +use js_bindgen_shared::ReadFile; use wasmparser::{Parser, Payload}; use crate::args::Arguments; @@ -124,6 +125,17 @@ fn process_object( file_counter += 1; let wasm_path = archive_path.with_added_extension(format!("wasm.{file_counter}.o")); + // The cache is shared by concurrent linker processes. Hold the lock through + // freshness validation, generation, and parsing. + let lock_path = wasm_path.with_added_extension("lock"); + let lock = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(lock_path)?; + lock.lock()?; + let mut wasm_bytes = None; // We first use a fingerprint to quickly determine whether `wasm.o` needs to be // regenerated: https://doc.rust-lang.org/1.92.0/nightly-rustc/cargo/core/compiler/fingerprint/index.html#fingerprints-and-unithashs @@ -137,9 +149,28 @@ fn process_object( .is_none_or(|(t1, t2)| t1 < t2) } { let wasm = js_bindgen_ld_shared::wat_to_object(wasm64, wat)?; - fs::write(&wasm_path, wasm)?; + fs::write(&wasm_path, &wasm)?; + wasm_bytes = Some(wasm); } + let exist_file; + let wasm_object: &[u8] = if let Some(bytes) = &wasm_bytes { + bytes + } else { + exist_file = ReadFile::new(&wasm_path)?; + &exist_file + }; + + process_object( + js_store, + wasm64, + &mut Vec::new(), + &wasm_path, + wasm_object, + js_bindgen_shared::mtime(&std::fs::metadata(&wasm_path)?)?, + )?; + + drop(lock); add_args.push(wasm_path.into()); } } diff --git a/host/tsconfig.base.json b/host/tsconfig.base.json index 0eabdae2..f2ab4554 100644 --- a/host/tsconfig.base.json +++ b/host/tsconfig.base.json @@ -15,6 +15,7 @@ "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "skipLibCheck": true } } diff --git a/web/.cargo/audit.toml b/web/.cargo/audit.toml new file mode 100644 index 00000000..b4db78f7 --- /dev/null +++ b/web/.cargo/audit.toml @@ -0,0 +1,9 @@ +[advisories] +ignore = [ + # `paste`: Unmaintained. + "RUSTSEC-2024-0436", +] + +[output] +deny = ["warnings"] +quiet = false diff --git a/web/.cargo/config.toml b/web/.cargo/config.toml new file mode 100644 index 00000000..ac0b1982 --- /dev/null +++ b/web/.cargo/config.toml @@ -0,0 +1,9 @@ +[build] +target = "wasm32-web-wabi" + +[target.'cfg(all(target_family = "wasm", target_os = "web", target_env = "wabi"))'] +linker = "../host/cargo-shim/linker" +runner = "../host/cargo-shim/runner" + +[env] +JBG_DEV = "1" diff --git a/web/Cargo.toml b/web/Cargo.toml new file mode 100644 index 00000000..062cbc66 --- /dev/null +++ b/web/Cargo.toml @@ -0,0 +1,33 @@ +[workspace] +resolver = "3" +members = ["playground"] + +[workspace.package] +edition = "2024" +rust-version = "1.85" +license = "MIT OR Apache-2.0" + +[workspace.dependencies] +criterion = { version = "0.8.2", default-features = false } +js-bindgen = { path = "../client/js-bindgen" } +js-sys = { path = "../client/js-sys" } + +[workspace.lints.clippy] +alloc_instead_of_core = "warn" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +explicit_deref_methods = "allow" +pedantic = { level = "warn", priority = -1 } +struct_excessive_bools = "allow" +tabs_in_doc_comments = "allow" +undocumented_unsafe_blocks = "warn" +use_self = "warn" +wildcard_imports = "allow" + +# TODO: remove +missing_errors_doc = "allow" +missing_panics_doc = "allow" +too_many_lines = "allow" + +[workspace.lints.rust] +linker_messages = "deny" diff --git a/web/playground/Cargo.toml b/web/playground/Cargo.toml new file mode 100644 index 00000000..4764ba39 --- /dev/null +++ b/web/playground/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "playground" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[[bench]] +harness = false +name = "conv" + +[dev-dependencies] +criterion = { workspace = true } +js-bindgen = { workspace = true } +js-sys = { workspace = true, features = ["macro"] } + +[lints] +workspace = true diff --git a/web/playground/benches/conv.rs b/web/playground/benches/conv.rs new file mode 100644 index 00000000..22d0135e --- /dev/null +++ b/web/playground/benches/conv.rs @@ -0,0 +1,38 @@ +use std::hint::black_box; + +use criterion::{Criterion, criterion_group, criterion_main}; +use js_sys::js_sys; + +js_bindgen::embed_js!(module = "conv", name = "bench", "(value) => value"); +js_bindgen::embed_js!( + module = "conv", + name = "bench_str", + "(value) => !!value" +); + +fn bench_conv_u128(c: &mut Criterion) { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "bench")] + fn conv_u128(value: u128) -> u128; + #[js_sys(js_embed = "bench_str")] + fn conv_str(value: &str) -> bool; + } + + const SMALL: u128 = 4242; + const WIDE: u128 = u128::MAX; + + let mut group = c.benchmark_group("conv_u128"); + group.bench_function("small", |b| { + b.iter(|| black_box(conv_u128(black_box(SMALL)))) + }); + group.bench_function("wide", |b| b.iter(|| black_box(conv_u128(black_box(WIDE))))); + group.finish(); + + c.bench_function("conv_str", |b| { + b.iter(|| black_box(conv_str(black_box("hello world")))) + }); +} + +criterion_group!(benches, bench_conv_u128); +criterion_main!(benches); diff --git a/web/playground/src/lib.rs b/web/playground/src/lib.rs new file mode 100644 index 00000000..000a1f03 --- /dev/null +++ b/web/playground/src/lib.rs @@ -0,0 +1,6 @@ +/// ``` +/// assert_eq!(playground::add(1, 1), 2); +/// ``` +pub fn add(left: u64, right: u64) -> u64 { + left + right +} diff --git a/web/playground/src/main.rs b/web/playground/src/main.rs new file mode 100644 index 00000000..328147e5 --- /dev/null +++ b/web/playground/src/main.rs @@ -0,0 +1,36 @@ +#![feature(random)] +use std::random::random; +use std::time::Instant; + +fn main() { + let ins = Instant::now(); + + let bits: u128 = random(..); + let g1 = (bits >> 96) as u32; + let g2 = (bits >> 80) as u16; + let g3 = (0x4000 | (bits >> 64) & 0x0fff) as u16; + let g4 = (0x8000 | (bits >> 48) & 0x3fff) as u16; + let g5 = (bits & 0xffffffffffff) as u64; + let uuid = format!("{g1:08x}-{g2:04x}-{g3:04x}-{g4:04x}-{g5:012x}"); + + let elapsed = ins.elapsed(); + println!("result: {uuid}, cost: {elapsed:?}"); +} + +#[cfg(test)] +mod tests { + #[test] + #[should_panic] + fn test1() { + panic!() + } + + #[test] + #[ignore = "test2"] + fn test2() { + panic!() + } + + #[test] + fn test3() {} +} diff --git a/web/rust-toolchain.toml b/web/rust-toolchain.toml new file mode 100644 index 00000000..d7377f8e --- /dev/null +++ b/web/rust-toolchain.toml @@ -0,0 +1,4 @@ +#:tombi lint.disabled = true + +[toolchain] +channel = "stage1" From d034e28fb6f3351088eff662b35f4fb2a3711898 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:02:27 +0800 Subject: [PATCH 03/21] Complete the JavaScript boundary ABI --- TODO.md | 1 - client/e2e/examples/primitive.rs | 72 +++ client/js-sys/src/array/array.gen.rs | 28 +- client/js-sys/src/array/mod.rs | 51 +- client/js-sys/src/bigint/bigint.gen.rs | 10 +- client/js-sys/src/externref.rs | 259 +++++++--- client/js-sys/src/hazard.rs | 454 +++++++++++------- client/js-sys/src/interop/primitive.rs | 105 ++-- client/js-sys/src/macro.rs | 29 +- client/js-sys/src/macro/abi.rs | 118 ++++- client/js-sys/src/macro/export.rs | 235 ++++----- client/js-sys/src/macro/js_import.rs | 40 +- client/js-sys/src/macro/result.rs | 105 ++-- client/js-sys/src/macro/text.rs | 5 - client/js-sys/src/macro/wat.rs | 379 ++++++++++++--- client/js-sys/src/macro/wat_import.rs | 191 +++----- client/js-sys/src/number/number.gen.rs | 10 +- client/js-sys/src/string/string.gen.rs | 24 +- client/js-sys/src/util.rs | 17 +- client/js-sys/src/value/mod.rs | 144 +++--- client/js-sys/src/value/value.gen.rs | 2 +- client/js-sys/tests/array.rs | 3 + client/js-sys/tests/hazard.rs | 22 +- client/js-sys/tests/optional.rs | 12 + client/js-sys/tests/value.rs | 13 +- client/web-sys/src/console.gen.rs | 14 +- host/cli-lib/src/js/imports.mjs | 4 +- host/cli-lib/src/js/imports.mts | 4 +- host/cli/src/main.rs | 3 - host/js-sys-bindgen/src/export.rs | 4 +- host/js-sys-bindgen/src/function.rs | 2 +- host/js-sys-bindgen/src/hygiene.rs | 12 - host/js-sys-bindgen/src/tests/macro/export.rs | 283 ++++++++--- .../src/tests/macro/function.rs | 78 +-- host/js-sys-bindgen/src/tests/macro/member.rs | 50 +- host/js-sys-bindgen/src/tests/macro/mod.rs | 6 + host/js-sys-bindgen/src/tests/macro/type.rs | 40 -- host/js-sys-bindgen/src/tests/type.rs | 20 +- host/js-sys-bindgen/src/tests/web_idl.rs | 10 +- host/js-sys-bindgen/src/type.rs | 18 +- host/ld/src/post.rs | 4 +- host/ld/src/pre.rs | 2 +- 42 files changed, 1760 insertions(+), 1123 deletions(-) diff --git a/TODO.md b/TODO.md index 6e0224d9..bba7b644 100644 --- a/TODO.md +++ b/TODO.md @@ -49,7 +49,6 @@ `extern { fn ... }` definition can shadow parameter values. - `js-bindgen` macro custom section generation can produce name collisions with intermediate variables. -- Allocate slots on the `externref` table in batches. - Determine what to do with `js_sys::UnwrapThrowExt`. Avoiding the panic machinery is nice for some very niche use-cases but it might be very annoying for most users. Maybe hide it behind a `cfg` flag? diff --git a/client/e2e/examples/primitive.rs b/client/e2e/examples/primitive.rs index cc2e7e95..a0d57797 100644 --- a/client/e2e/examples/primitive.rs +++ b/client/e2e/examples/primitive.rs @@ -17,6 +17,8 @@ fn main() { // ;; (() => { const value = exports["usize_max"](); return value === (typeof value === "bigint" ? 0xffff_ffff_ffff_ffffn : 0xffff_ffff) })() // ;; exports["option_bool"](undefined) === undefined // ;; exports["option_bool"](false) === true + // ;; exports["option_unit"](undefined) === undefined + // ;; exports["option_unit"](true) === true // ;; exports["option_i16"](undefined) === undefined // ;; exports["option_i16"](-32_768) === -32_767 // ;; exports["option_u32"](undefined) === undefined @@ -37,6 +39,18 @@ fn main() { // ;; exports["option_u128"]((1n << 128n) - 2n) === (1n << 128n) - 1n // ;; exports["option_i128"](undefined) === undefined // ;; exports["option_i128"](-(1n << 127n)) === -(1n << 127n) + 1n + // ;; (() => { const value = {}; return exports["js_value_identity"](value) === value })() + // ;; exports["option_js_value"](undefined) === undefined + // ;; exports["option_js_value"](null) === undefined + // ;; (() => { const value = {}; return exports["option_js_value"](value) === value })() + // ;; exports["import_option_i32"](undefined) === undefined + // ;; exports["import_option_i32"](42) === 42 + // ;; exports["import_option_unit"](undefined) === undefined + // ;; exports["import_option_unit"](true) === true + // ;; exports["result_unit"](true) === undefined + // ;; (() => { try { exports["result_unit"](false); return false } catch (error) { return error === "unit error" } })() + // ;; exports["import_result_unit"](true) === undefined + // ;; (() => { try { exports["import_result_unit"](false); return false } catch (error) { return error === "unit error" } })() // ;; exports["checked_add_u128"](1n << 96n, 3n) === (1n << 96n) + 3n // ;; (() => { try { exports["checked_add_u128"]((1n << 128n) - 1n, 1n); return false } catch (error) { return error === "overflow" } })() // ;; exports["import_result_i64"](41n) === 42n @@ -51,6 +65,20 @@ use js_sys::{JsString, JsValue, js_sys}; type JsResult = Result; +js_sys::js_bindgen::embed_js!( + module = "primitive", + name = "option.i32", + "(value) => value", +); + +js_sys::js_bindgen::embed_js!( + module = "primitive", + name = "result.unit", + "(ok) => {{", + " if (!ok) throw 'unit error'", + "}}", +); + js_sys::js_bindgen::embed_js!( module = "primitive", name = "result.i64", @@ -73,6 +101,15 @@ js_sys::js_bindgen::embed_js!( #[js_sys] extern "js-sys" { + #[js_sys(js_embed = "option.i32")] + fn import_option_i32_raw(value: Option) -> Option; + + #[js_sys(js_embed = "option.i32")] + fn import_option_unit_raw(value: Option<()>) -> Option<()>; + + #[js_sys(js_embed = "result.unit")] + fn import_result_unit_raw(ok: bool) -> Result<(), JsValue>; + #[js_sys(js_embed = "result.i64")] fn import_result_i64_raw(value: i64) -> Result; @@ -168,6 +205,11 @@ fn option_bool(value: Option) -> Option { value.map(|value| !value) } +#[js_sys] +fn option_unit(value: Option<()>) -> Option<()> { + value +} + #[js_sys] fn option_i16(value: Option) -> Option { value.map(|value| value + 1) @@ -218,6 +260,36 @@ fn option_i128(value: Option) -> Option { value.map(|value| value + 1) } +#[js_sys] +fn js_value_identity(value: JsValue) -> JsValue { + value +} + +#[js_sys] +fn option_js_value(value: Option) -> Option { + value +} + +#[js_sys] +fn import_option_i32(value: Option) -> Option { + import_option_i32_raw(value) +} + +#[js_sys] +fn import_option_unit(value: Option<()>) -> Option<()> { + import_option_unit_raw(value) +} + +#[js_sys] +fn result_unit(ok: bool) -> JsResult<()> { + ok.then_some(()).ok_or_else(|| JsString::from("unit error")) +} + +#[js_sys] +fn import_result_unit(ok: bool) -> Result<(), JsValue> { + import_result_unit_raw(ok) +} + #[js_sys] fn checked_add_u128(value: u128, delta: u128) -> JsResult { value diff --git a/client/js-sys/src/array/array.gen.rs b/client/js-sys/src/array/array.gen.rs index 067c81c0..a3748d59 100644 --- a/client/js-sys/src/array/array.gen.rs +++ b/client/js-sys/src/array/array.gen.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; +use crate::hazard::{IntoJS, JsCast}; use crate::util::{PtrConst, PtrLength, PtrMut}; #[repr(transparent)] @@ -35,18 +35,10 @@ unsafe impl IntoJS for JsArray { } } -unsafe impl OptionIntoJS for JsArray { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } -} - impl JsArray { pub fn length(self: &JsArray) -> u32 { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "length", adapter = + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "length", shim = "js_sys.length", inputs = [("arg0", & JsValue)], output = u32,), } @@ -91,7 +83,7 @@ pub(super) unsafe fn array_js_value_decode( ) -> JsArray { js_bindgen::unsafe_global_wat! { "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_js_value_decode", - adapter = "js_sys.array_js_value_decode", inputs = [("arg0", PtrConst < JsValue >), ("arg1", + shim = "js_sys.array_js_value_decode", inputs = [("arg0", PtrConst < JsValue >), ("arg1", PtrLength < JsValue >)], output = JsArray < JsValue >,), } @@ -146,7 +138,7 @@ pub(super) unsafe fn array_js_value_encode( ) -> bool { js_bindgen::unsafe_global_wat! { "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_js_value_encode", - adapter = "js_sys.array_js_value_encode", inputs = [("arg0", & JsArray), ("arg1", PtrMut < + shim = "js_sys.array_js_value_encode", inputs = [("arg0", & JsArray), ("arg1", PtrMut < JsValue >), ("arg2", PtrLength < JsValue >), ("arg3", PtrConst < i32 >), ("arg4", i32)], output = bool,), } @@ -237,9 +229,9 @@ pub(super) unsafe fn array_js_value_encode( pub(super) unsafe fn array_u32_decode(array: PtrConst, len: PtrLength) -> JsArray { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_decode", - adapter = "js_sys.array_u32_decode", inputs = [("arg0", PtrConst < u32 >), ("arg1", - PtrLength < u32 >)], output = JsArray < u32 >,), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_decode", shim + = "js_sys.array_u32_decode", inputs = [("arg0", PtrConst < u32 >), ("arg1", PtrLength < u32 + >)], output = JsArray < u32 >,), } js_bindgen::import_js! { @@ -287,9 +279,9 @@ pub(super) unsafe fn array_u32_encode( len: PtrLength, ) -> bool { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_encode", - adapter = "js_sys.array_u32_encode", inputs = [("arg0", & JsArray < u32 >), ("arg1", PtrMut - < u32 >), ("arg2", PtrLength < u32 >)], output = bool,), + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_encode", shim + = "js_sys.array_u32_encode", inputs = [("arg0", & JsArray < u32 >), ("arg1", PtrMut < u32 + >), ("arg2", PtrLength < u32 >)], output = bool,), } js_bindgen::import_js! { diff --git a/client/js-sys/src/array/mod.rs b/client/js-sys/src/array/mod.rs index de2e1255..02f27e1e 100644 --- a/client/js-sys/src/array/mod.rs +++ b/client/js-sys/src/array/mod.rs @@ -8,10 +8,9 @@ use core::mem::MaybeUninit; use core::ptr; pub use self::array::JsArray; -use crate::JsValue; -use crate::externref::ExternrefTable; use crate::hazard::{IntoJS, IntoJsConv, JsCast}; use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; +use crate::{JsValue, externref}; impl JsArray { #[must_use] @@ -64,7 +63,7 @@ impl Error for TryFromJsArrayError {} impl JsArray { pub fn to_slice(&self, slice: &mut [T]) -> Result<(), TryFromJsArrayError> { let slice = JsValue::from_slice_mut(slice); - let externref = ExternrefTable::current_ptr(); + let slots = externref::reserve_slots(slice.len()); // SAFETY: Parameters are correct. let result = unsafe { @@ -72,13 +71,13 @@ impl JsArray { self.as_any(), PtrMut::new(slice), PtrLength::new(slice), - externref.ptr, - externref.len, + slots.ptr(), + slots.len(), ) }; if result { - ExternrefTable::report_used_slots(slice.len()); + slots.commit(); Ok(()) } else { Err(TryFromJsArrayError) @@ -90,7 +89,7 @@ impl JsArray { slice: &'slice mut [MaybeUninit], ) -> Result<&'slice mut [T], TryFromJsArrayError> { let js_slice = JsValue::from_uninit_slice_mut(slice); - let externref = ExternrefTable::current_ptr(); + let slots = externref::reserve_slots(js_slice.len()); // SAFETY: Parameters are correct. let result = unsafe { @@ -98,13 +97,13 @@ impl JsArray { self.as_any(), PtrMut::from_uninit_slice(js_slice), PtrLength::from_uninit_slice(js_slice), - externref.ptr, - externref.len, + slots.ptr(), + slots.len(), ) }; if result { - ExternrefTable::report_used_slots(js_slice.len()); + slots.commit(); // SAFETY: Correctly initialized in JS. Ok(unsafe { assume_init_mut(slice) }) } else { @@ -114,7 +113,7 @@ impl JsArray { pub fn to_array(&self) -> Result<[T; N], TryFromJsArrayError> { let mut array: MaybeUninit<[T; N]> = MaybeUninit::uninit(); - let externref = ExternrefTable::current_ptr(); + let slots = externref::reserve_slots(N); let js_array = JsValue::from_mut_uninit_array(&mut array); // SAFETY: Parameters are correct. @@ -123,13 +122,13 @@ impl JsArray { self.as_any(), PtrMut::from_uninit_array(js_array), PtrLength::from_uninit_array(js_array), - externref.ptr, - externref.len, + slots.ptr(), + slots.len(), ) }; if result { - ExternrefTable::report_used_slots(N); + slots.commit(); // SAFETY: Correctly initialized in JS. Ok(unsafe { array.assume_init() }) } else { @@ -146,32 +145,14 @@ js_bindgen::embed_js!( " if (array.length !== arrLen) return false", "", " const table = this.#jsEmbed.js_sys['externref.table']", - "", - // Default value helps browsers to optimize. - " let tableIndex = 0", - " const reused = Math.min(arrLen, refLen)", " const refIndices = this.#jsEmbed.js_sys['view.getInt32'](", - " refPtr + (refLen - reused) * 4,", - " reused,", + " refPtr,", + " refLen,", " )", " const elemIndices = new Array(arrLen)", - " if (arrLen > reused) {{", - " tableIndex = table.grow(arrLen - reused)", - " }}", - "", - " let refIndex = reused - 1", "", " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", - " let elemIndex", - "", - " if (refIndex >= 0) {{", - " elemIndex = refIndices[refIndex]", - " refIndex--", - " }} else {{", - " elemIndex = tableIndex", - " tableIndex++", - " }}", - "", + " const elemIndex = refIndices[arrayIndex]", " table.set(elemIndex, array[arrayIndex])", " elemIndices[arrayIndex] = elemIndex", " }}", diff --git a/client/js-sys/src/bigint/bigint.gen.rs b/client/js-sys/src/bigint/bigint.gen.rs index a9168807..e1e47c2b 100644 --- a/client/js-sys/src/bigint/bigint.gen.rs +++ b/client/js-sys/src/bigint/bigint.gen.rs @@ -3,7 +3,7 @@ #![allow(warnings)] use crate::JsValue; -use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; +use crate::hazard::{IntoJS, JsCast}; #[repr(transparent)] pub struct JsBigInt(JsValue); @@ -29,11 +29,3 @@ unsafe impl IntoJS for JsBigInt { IntoJS::into_abi(JsValue::from(self)) } } - -unsafe impl OptionIntoJS for JsBigInt { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } -} diff --git a/client/js-sys/src/externref.rs b/client/js-sys/src/externref.rs index 49cdb7d6..5c9149b2 100644 --- a/client/js-sys/src/externref.rs +++ b/client/js-sys/src/externref.rs @@ -4,46 +4,63 @@ use core::cell::RefCell; use crate::panic::panic; use crate::util::PtrConst; +pub(crate) const WAT_TABLE_IMPORT: &str = "(import \"js_sys\" \"externref.table\" (table \ + $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref))"; +pub(crate) const WAT_NEXT_IMPORT: &str = + "(import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32)))"; +pub(crate) const WAT_RELEASE_IMPORT: &str = "(import \"env\" \"js_sys.externref.release\" (func \ + $js_sys.externref.release (@sym) (param i32)))"; +pub(crate) const WAT_VALUE_LOCAL: &str = " (local $js_sys.externref.value externref)"; +pub(crate) const WAT_INDEX_LOCAL: &str = " (local $js_sys.externref.index i32)"; +pub(crate) const WAT_INSERT_IMPORTS: &str = + crate::const_concat!(WAT_TABLE_IMPORT, "\n", WAT_NEXT_IMPORT); +pub(crate) const WAT_TAKE_IMPORTS: &str = + crate::const_concat!(WAT_TABLE_IMPORT, "\n", WAT_RELEASE_IMPORT); +pub(crate) const WAT_INSERT_LOCALS: &str = + crate::const_concat!(WAT_VALUE_LOCAL, "\n", WAT_INDEX_LOCAL); +pub(crate) const WAT_INSERT_CONV: &str = "\ + local.set $js_sys.externref.value + call $js_sys.externref.next (@reloc) + local.tee $js_sys.externref.index + local.get $js_sys.externref.value + table.set $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index"; +pub(crate) const WAT_OPTIONAL_INSERT_CONV: &str = "\ + local.set $js_sys.externref.value + local.get $js_sys.externref.value + ref.is_null + if (result i32) + i32.const 0 + else + call $js_sys.externref.next (@reloc) + local.tee $js_sys.externref.index + local.get $js_sys.externref.value + table.set $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + end"; +pub(crate) const WAT_GET_CONV: &str = "table.get $js_sys.import.externref.table (@reloc)"; +pub(crate) const WAT_TAKE_CONV: &str = "\ + local.tee $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end"; + js_bindgen::unsafe_global_wat!( // Imports need an explicit name. // See https://github.com/llvm/llvm-project/issues/198509. "(import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ \"js_sys.externref.table\")) 2 externref))", - "(import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32)))", - "(import \"env\" \"js_sys.externref.recycle\" (func $js_sys.externref.recycle (@sym) (param \ - i32)))", "(func $js_sys.externref.grow (@sym) (param $size i32) (result i32)", " ref.null extern", " local.get $size", " table.grow $js_sys.import.externref.table (@reloc)", ")", - "(func $js_sys.externref.insert (@sym) (param $value externref) (result i32)", - " (local $index i32)", - " call $js_sys.externref.next (@reloc)", - " local.tee $index", - " local.get $value", - " table.set $js_sys.import.externref.table (@reloc)", - " local.get $index", - ")", - "(func $js_sys.externref.get (@sym) (param $index i32) (result externref)", - " local.get $index", - " table.get $js_sys.import.externref.table (@reloc)", - ")", - "(func $js_sys.externref.take (@sym) (param $index i32) (result externref)", - " (local $value externref)", - " local.get $index", - " table.get $js_sys.import.externref.table (@reloc)", - " local.set $value", - " ;; Indices zero and one are reserved for `undefined` and `null`.", - " local.get $index", - " i32.const 2", - " i32.ge_u", - " if", - " local.get $index", - " call $js_sys.externref.recycle (@reloc)", - " end", - " local.get $value", - ")", "(func $js_sys.externref.remove (@sym) (param $index i32)", " local.get $index", " ref.null extern", @@ -75,78 +92,164 @@ unsafe extern "C" { safe fn remove(index: i32); } -thread_local! { - pub(crate) static EXTERNREF_TABLE: RefCell = RefCell::new(ExternrefTable::new()); +struct Slab { + data: Vec, + head: usize, + base: usize, } -pub(crate) struct ExternrefTable(Vec); +impl Slab { + const fn new() -> Self { + Self { + data: Vec::new(), + head: 0, + base: 0, + } + } -pub(crate) struct ExternrefTablePtr { - pub(crate) ptr: PtrConst, - pub(crate) len: i32, -} + // `js-sys` is linked as a separate crate. Without forced `inlining`, each + // `externref` conversion retains an extra Wasm function call. + #[expect( + clippy::inline_always, + reason = "avoids a call in every externref conversion" + )] + #[inline(always)] + fn alloc(&mut self) -> usize { + let slot = self.head; + if slot == self.data.len() { + let len = self.data.len(); + if len == self.data.capacity() { + let additional = len.max(128); + let first = grow(index_to_abi(additional)); + if first == -1 { + panic("`externref` table allocation failure"); + } + + let first = index_from_abi(first); + if self.base == 0 { + self.base = first; + } else if self.base + self.data.len() != first { + panic("non-contiguous `externref` table growth"); + } + + if self.data.try_reserve_exact(additional).is_err() { + panic("`externref` slab allocation failure"); + } + } -impl ExternrefTable { - const fn new() -> Self { - Self(Vec::new()) + if self.data.len() >= self.data.capacity() { + panic("`externref` slab capacity mismatch"); + } + self.data.push(slot + 1); + } + + match self.data.get_mut(slot) { + Some(next) => self.head = *next, + None => panic("`externref` slot out of bounds"), + } + + slot + self.base } - fn next(&mut self) -> i32 { - if let Some(slot) = self.0.pop() { - slot - } else { - match grow(1) { - -1 => panic("`externref` table allocation failure"), - slot => slot, + #[expect( + clippy::inline_always, + reason = "avoids a call in every externref conversion" + )] + #[inline(always)] + fn dealloc(&mut self, index: usize) { + if index < self.base { + panic("attempted to free a reserved `externref` slot"); + } + let slot = index - self.base; + + match self.data.get_mut(slot) { + Some(next) => { + *next = self.head; + self.head = slot; } + None => panic("`externref` slot out of bounds"), } } +} - pub(crate) fn remove(&mut self, index: i32) { - self.0.try_reserve(1).expect("failure to grow memory"); +// Replacing `RefCell` with `UnsafeCell` makes the `JsValue` benchmarks about +// 4-8% faster, but `RefCell` detects accidental nested access on one thread. +thread_local! { + static EXTERNREF_SLAB: RefCell = const { RefCell::new(Slab::new()) }; +} - self.0.push(index); - remove(index); +pub(crate) struct ReservedSlots { + slots: Vec, + committed: bool, +} + +impl ReservedSlots { + pub(crate) fn ptr(&self) -> PtrConst { + PtrConst::new(&self.slots) + } + + pub(crate) fn len(&self) -> i32 { + self.slots.len().try_into().unwrap() } - /// Export a pointer and length to the current list. - /// - /// # Safety - /// - /// Reading from that pointer and length is only valid as long as the list - /// is not modified. - pub(crate) fn current_ptr() -> ExternrefTablePtr { - EXTERNREF_TABLE.with(|table| { - let table = &table.try_borrow().unwrap().0; - - ExternrefTablePtr { - ptr: PtrConst::new(table), - len: table.len().try_into().unwrap(), + pub(crate) fn commit(mut self) { + self.committed = true; + } +} + +impl Drop for ReservedSlots { + fn drop(&mut self) { + if !self.committed { + let mut slab = EXTERNREF_SLAB.0.borrow_mut(); + for &index in &self.slots { + slab.dealloc(index_from_abi(index)); } - }) + } + } +} + +pub(crate) fn reserve_slots(count: usize) -> ReservedSlots { + let mut slots = Vec::new(); + slots + .try_reserve_exact(count) + .expect("failure to grow memory"); + + let mut slab = EXTERNREF_SLAB.0.borrow_mut(); + while slots.len() < count { + slots.push(index_to_abi(slab.alloc())); } - /// When using empty slots through [`ExternrefTablePtr`], we report back how - /// many we used. - pub(crate) fn report_used_slots(slots: usize) { - EXTERNREF_TABLE.with(|table| { - let mut table = table.try_borrow_mut().unwrap(); - let new_len = table.0.len().saturating_sub(slots); - table.0.truncate(new_len); - }); + ReservedSlots { + slots, + committed: false, } } +#[cfg(not(target_feature = "exception-handling"))] +#[inline] pub(crate) fn reserve() -> i32 { - EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().next()) + next() +} + +#[unsafe(export_name = "js_sys.externref.release")] +pub(crate) extern "C" fn release(index: i32) { + remove(index); + EXTERNREF_SLAB.0.borrow_mut().dealloc(index_from_abi(index)); } #[unsafe(export_name = "js_sys.externref.next")] extern "C" fn next() -> i32 { - reserve() + index_to_abi(EXTERNREF_SLAB.0.borrow_mut().alloc()) +} + +#[inline] +fn index_to_abi(index: usize) -> i32 { + let index = + u32::try_from(index).unwrap_or_else(|_| panic("`externref` table capacity overflow")); + i32::from_ne_bytes(index.to_ne_bytes()) } -#[unsafe(export_name = "js_sys.externref.recycle")] -extern "C" fn recycle(index: i32) { - EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().remove(index)); +#[inline] +fn index_from_abi(index: i32) -> usize { + usize::try_from(u32::from_ne_bytes(index.to_ne_bytes())).unwrap() } diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 33169f96..46dee466 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -2,6 +2,120 @@ use core::mem::{ManuallyDrop, MaybeUninit}; use core::ptr; use crate::JsValue; +use crate::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; + +// Conversion `metadata`. + +#[derive(Clone, Copy)] +pub struct WatConv { + pub imports: Option<&'static str>, + pub locals: Option<&'static str>, + pub conv: &'static str, + pub r#type: &'static str, +} + +/// Converts primitive `ABI` slots into one JavaScript value. +#[derive(Clone, Copy)] +pub struct IntoJsConv { + pub(crate) embed: Option<(&'static str, &'static str)>, + pub(crate) template: &'static str, +} + +impl IntoJsConv { + /// Produces one JavaScript value from `$slot1` through `$slot4`. + #[must_use] + pub const fn new(template: &'static str) -> Self { + Self { + embed: None, + template, + } + } + + #[must_use] + pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { + self.embed = Some(embed); + self + } +} + +/// Converts one JavaScript value into primitive `ABI` slots. +#[derive(Clone, Copy)] +pub struct FromJsConv { + pub(crate) embed: Option<(&'static str, &'static str)>, + pub(crate) templates: [&'static str; 4], + pub(crate) sret: Option<&'static str>, +} + +impl FromJsConv { + /// Produces `ABI` slots from `$value`. + #[must_use] + pub const fn slot1(template: &'static str) -> Self { + Self { + embed: None, + templates: [template, "", "", ""], + sret: None, + } + } + + #[must_use] + pub const fn slot2(mut self, template: &'static str) -> Self { + self.templates[1] = template; + self + } + + #[must_use] + pub const fn slot3(mut self, template: &'static str) -> Self { + self.templates[2] = template; + self + } + + #[must_use] + pub const fn slot4(mut self, template: &'static str) -> Self { + self.templates[3] = template; + self + } + + /// Stores the slots in an indirect return area. + /// + /// The function receives every non-empty slot in order, followed by the + /// indirect return pointer. + #[must_use] + pub const fn sret(mut self, function: &'static str) -> Self { + self.sret = Some(function); + self + } + + #[must_use] + pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { + self.embed = Some(embed); + self + } +} + +/// Describes how a function return is handled at the JavaScript boundary. +#[derive(Clone, Copy)] +pub enum ReturnConv { + /// The value is returned normally. + Value(Option), + /// `Ok` is returned normally and `Err` follows the exception path. + Result(Option), +} + +impl ReturnConv { + #[must_use] + pub const fn conversion(self) -> Option { + match self { + Self::Value(value) | Self::Result(value) => value, + } + } + + #[must_use] + pub const fn is_result(self) -> bool { + matches!(self, Self::Result(_)) + } +} + +// Wasm `ABI` carriers. /// One carrier position in the Wasm function `ABI`. /// @@ -43,6 +157,13 @@ pub enum ReturnMode { Indirect, } +impl ReturnMode { + #[must_use] + pub const fn is_direct(self) -> bool { + matches!(self, Self::Direct) + } +} + /// A [`WasmAbi`] that can be returned through the Rust `extern "C"` `ABI`. /// /// # Safety @@ -54,13 +175,6 @@ pub unsafe trait ReturnAbi: WasmAbi { const MODE: ReturnMode; } -impl ReturnMode { - #[must_use] - pub const fn is_direct(self) -> bool { - matches!(self, Self::Direct) - } -} - /// The FFI-safe return representation of a [`WasmAbi`] value. #[doc(hidden)] #[repr(C)] @@ -184,12 +298,7 @@ where } } -#[derive(Clone, Copy)] -pub struct WatConv { - pub import: Option<&'static str>, - pub conv: &'static str, - pub r#type: &'static str, -} +// Rust-to-JavaScript conversions. /// # Safety /// @@ -205,6 +314,38 @@ pub unsafe trait IntoJS { fn into_abi(self) -> Self::Abi; } +/// Describes how a value using this `ABI` carrier is encoded as an [`Option`]. +/// +/// This is implemented on the carrier rather than the Rust value so types that +/// share a carrier can also share their optional representation. +/// +/// # Safety +/// +/// `Abi`, `into_option_abi`, and `JS_CONV` must describe one consistent +/// conversion from `Option` to a JavaScript value. +#[doc(hidden)] +pub unsafe trait OptionIntoAbi: WasmAbi { + const JS_CONV: Option = T::JS_CONV; + + type Abi: WasmAbi; + + fn into_option_abi(value: Option) -> Self::Abi; +} + +// SAFETY: Delegated to the optional representation of `T`'s `ABI` carrier. +unsafe impl IntoJS for Option +where + T::Abi: OptionIntoAbi, +{ + const JS_CONV: Option = >::JS_CONV; + + type Abi = >::Abi; + + fn into_abi(self) -> Self::Abi { + >::into_option_abi(self) + } +} + /// Converts a Rust function result into its JavaScript return representation. /// /// Ordinary values delegate to [`IntoJS`]. Types such as [`Result`] may also @@ -214,7 +355,7 @@ pub trait ReturnIntoJS { type Abi: ReturnAbi; - fn return_into_abi(self) -> Self::Abi; + fn into_return_abi(self) -> Self::Abi; } impl ReturnIntoJS for T @@ -226,222 +367,200 @@ where type Abi = T::Abi; - fn return_into_abi(self) -> Self::Abi { + fn into_return_abi(self) -> Self::Abi { self.into_abi() } } -/// Extends [`IntoJS`] with the representation of `Option`. -/// +// JavaScript-to-Rust conversions. + /// # Safety /// -/// `OptionAbi`, `option_into_abi`, and `OPTION_JS_CONV` must describe one -/// consistent conversion from `Option` to a JavaScript value. -pub unsafe trait OptionIntoJS: IntoJS + Sized { - const OPTION_JS_CONV: Option = Self::JS_CONV; - - type OptionAbi: WasmAbi; - - fn option_into_abi(value: Option) -> Self::OptionAbi; -} - -// SAFETY: Delegated to the `OptionIntoJS` implementation. -unsafe impl IntoJS for Option { - const JS_CONV: Option = T::OPTION_JS_CONV; - - type Abi = T::OptionAbi; - - fn into_abi(self) -> Self::Abi { - T::option_into_abi(self) - } -} +/// `Abi`, `from_abi`, and `JS_CONV` must describe one consistent conversion +/// from a JavaScript value to a Rust value. `JS_CONV` produces the primitive +/// slots and `from_abi` reconstructs the Rust value. Multi-slot `ABI` +/// representations must define one slot template for every non-empty slot. +pub unsafe trait FromJS { + const JS_CONV: Option = None; -/// Converts primitive `ABI` slots into one JavaScript value. -#[derive(Clone, Copy)] -pub struct IntoJsConv { - pub(crate) embed: Option<(&'static str, &'static str)>, - pub(crate) template: &'static str, -} + type Abi: WasmAbi; -/// Describes how a function return is handled at the JavaScript boundary. -#[derive(Clone, Copy)] -pub enum ReturnConv { - /// The value is returned normally. - Value(Option), - /// `Ok` is returned normally and `Err` follows the exception path. - Result(Option), + fn from_abi(raw: Self::Abi) -> Self; } -impl ReturnConv { - #[must_use] - pub const fn conversion(self) -> Option { - match self { - Self::Value(value) | Self::Result(value) => value, - } - } +/// Describes how an [`Option`] is decoded for a value using this `ABI` carrier. +/// +/// This is implemented on the carrier rather than the Rust value so types that +/// share a carrier can also share their optional representation. +/// +/// # Safety +/// +/// `Abi`, `from_option_abi`, and `JS_CONV` must describe one consistent +/// conversion from a JavaScript value to `Option`. +#[doc(hidden)] +pub unsafe trait OptionFromAbi: WasmAbi { + const JS_CONV: Option = T::JS_CONV; - #[must_use] - pub const fn is_result(self) -> bool { - matches!(self, Self::Result(_)) - } -} + type Abi: WasmAbi; -/// Converts one JavaScript value into primitive `ABI` slots. -#[derive(Clone, Copy)] -pub struct FromJsConv { - pub(crate) embed: Option<(&'static str, &'static str)>, - pub(crate) templates: [&'static str; 4], - pub(crate) sret: Option<&'static str>, + fn from_option_abi(raw: Self::Abi) -> Option; } -impl IntoJsConv { - /// Produces one JavaScript value from `$slot1` through `$slot4`. - #[must_use] - pub const fn new(template: &'static str) -> Self { - Self { - embed: None, - template, - } - } - - #[must_use] - pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { - self.embed = Some(embed); - self - } -} - -impl FromJsConv { - /// Produces `ABI` slots from `$value`. - #[must_use] - pub const fn slot1(template: &'static str) -> Self { - Self { - embed: None, - templates: [template, "", "", ""], - sret: None, - } - } - - #[must_use] - pub const fn slot2(mut self, template: &'static str) -> Self { - self.templates[1] = template; - self - } +// SAFETY: Delegated to the optional representation of `T`'s `ABI` carrier. +unsafe impl FromJS for Option +where + T::Abi: OptionFromAbi, +{ + const JS_CONV: Option = >::JS_CONV; - #[must_use] - pub const fn slot3(mut self, template: &'static str) -> Self { - self.templates[2] = template; - self - } + type Abi = >::Abi; - #[must_use] - pub const fn slot4(mut self, template: &'static str) -> Self { - self.templates[3] = template; - self - } - - /// Stores the slots in an indirect return area. - /// - /// The function receives every non-empty slot in order, followed by the - /// indirect return pointer. - #[must_use] - pub const fn sret(mut self, function: &'static str) -> Self { - self.sret = Some(function); - self - } - - #[must_use] - pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { - self.embed = Some(embed); - self + fn from_abi(raw: Self::Abi) -> Self { + >::from_option_abi(raw) } } -/// # Safety -/// -/// `Abi`, `from_abi`, and `JS_CONV` must describe one consistent conversion -/// from a JavaScript value to a Rust value. `JS_CONV` produces the primitive -/// slots and `from_abi` reconstructs the Rust value. Multi-slot `ABI` -/// representations must define one slot template for every non-empty slot. -/// Indirect return `ABIs` must define an `sret` function. -pub unsafe trait FromJS { - const JS_CONV: Option = None; - - type Abi: ReturnAbi; - - fn from_abi(raw: Self::Abi) -> Self; -} - /// Converts the return value of a JavaScript import into its Rust result. /// -/// `Abi` describes the successful return value. The raw carrier may be -/// uninitialized when JavaScript throws, so implementations that catch -/// exceptions must inspect the exception state before decoding it. +/// `Abi` describes the successful return value and must support the Rust +/// return `ABI`. Indirect returns must define an `sret` conversion. The raw +/// carrier may be uninitialized when JavaScript throws, so implementations +/// that catch exceptions must inspect the exception state before decoding it. pub trait ReturnFromJS { const JS_CONV: ReturnConv; type Abi: ReturnAbi; - fn return_from_abi(raw: MaybeUninit>) -> Self; + fn from_return_abi(raw: MaybeUninit>) -> Self; } impl ReturnFromJS for T where T: FromJS, + T::Abi: ReturnAbi, { const JS_CONV: ReturnConv = ReturnConv::Value(T::JS_CONV); type Abi = T::Abi; - fn return_from_abi(raw: MaybeUninit>) -> Self { + fn from_return_abi(raw: MaybeUninit>) -> Self { // SAFETY: An ordinary JavaScript import always initializes its return - // value before the adapter returns. + // value before the shim returns. T::from_abi(unsafe { raw.assume_init() }.join()) } } +// Result returns. + /// The return `ABI` for exporting [`Result`] to JavaScript. /// -/// The first two slots carry the error and its presence tag. The remaining two -/// slots carry the successful value. +/// The first two slots carry the successful value. The remaining two carry the +/// error discriminant and table index. #[doc(hidden)] pub struct ResultIntoJsAbi { value: Result::Abi>, } -// SAFETY: The first slot transfers an error `externref`, the second is the -// error tag, and the remaining slots match the successful value's `ABI`. +const RESULT_DISCRIMINANT_LOCAL: &str = " (local $js_sys.result.discriminant i32)"; +const RESULT_ERROR_WAT_CONV: &str = "\ + local.set $js_sys.externref.index + local.get $js_sys.result.discriminant + if (result externref) + local.get $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end + else + ref.null extern + end"; + +/// The discriminant of an exported [`Result`]. +#[doc(hidden)] +#[repr(transparent)] +pub struct ResultDiscriminantAbi(u32); + +// SAFETY: The transparent `i32` discriminant is also recorded in a local for +// the following error slot conversion. +unsafe impl Slot for ResultDiscriminantAbi { + const WAT_TYPE: &'static str = "i32"; + const INTO_JS_WAT_CONV: Option = Some(WatConv { + imports: None, + locals: Some(RESULT_DISCRIMINANT_LOCAL), + conv: "local.tee $js_sys.result.discriminant", + r#type: "i32", + }); +} + +/// An owned `externref` table index transferred by a [`Result`] error. +/// +/// The preceding [`ResultDiscriminantAbi`] controls whether the index is taken +/// from the table. Successful results produce a null placeholder without +/// accessing the table. +#[doc(hidden)] +#[repr(transparent)] +pub struct ResultErrorAbi(::Abi); + +// SAFETY: `JsValue` uses a transparent `i32` table index as its Rust `ABI`. The +// preceding result discriminant is recorded before this conversion runs. +unsafe impl Slot for ResultErrorAbi { + const WAT_TYPE: &'static str = "i32"; + const INTO_JS_WAT_CONV: Option = Some(WatConv { + imports: Some(WAT_TAKE_IMPORTS), + locals: Some(WAT_INDEX_LOCAL), + conv: RESULT_ERROR_WAT_CONV, + r#type: "externref", + }); +} + +// SAFETY: The first two slots match the successful value's `ABI`. The third +// is the error discriminant and the fourth transfers an owned error table +// index. unsafe impl WasmAbi for ResultIntoJsAbi where T: WasmAbi, T::Slot1: Default, T::Slot2: Default, { - type Slot1 = ::Abi; - type Slot2 = u32; - type Slot3 = T::Slot1; - type Slot4 = T::Slot2; + type Slot1 = T::Slot1; + type Slot2 = T::Slot2; + type Slot3 = ResultDiscriminantAbi; + type Slot4 = ResultErrorAbi; fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { match self.value { Ok(value) => { let (slot1, slot2, _, _) = value.split(); - (JsValue::UNDEFINED.into_abi(), 0, slot1, slot2) + ( + slot1, + slot2, + ResultDiscriminantAbi(0), + ResultErrorAbi(JsValue::UNDEFINED.into_abi()), + ) } - Err(error) => (error, 1, Default::default(), Default::default()), + Err(error) => ( + Default::default(), + Default::default(), + ResultDiscriminantAbi(1), + ResultErrorAbi(error), + ), } } fn join( - error: Self::Slot1, - is_error: Self::Slot2, - slot1: Self::Slot3, - slot2: Self::Slot4, + slot1: Self::Slot1, + slot2: Self::Slot2, + is_error: Self::Slot3, + error: Self::Slot4, ) -> Self { - let value = if is_error == 0 { + let value = if is_error.0 == 0 { Ok(T::join(slot1, slot2, EmptySlot::new(), EmptySlot::new())) } else { - Err(error) + Err(error.0) }; Self { value } @@ -462,7 +581,7 @@ impl ReturnIntoJS for Result where T: IntoJS, E: Into, - T::Abi: ReturnAbi, + T::Abi: WasmAbi, ::Slot1: Default, ::Slot2: Default, { @@ -470,7 +589,7 @@ where type Abi = ResultIntoJsAbi; - fn return_into_abi(self) -> Self::Abi { + fn into_return_abi(self) -> Self::Abi { let value = match self { Ok(value) => Ok(value.into_abi()), Err(error) => Err(error.into().into_abi()), @@ -483,12 +602,13 @@ where impl ReturnFromJS for Result where T: FromJS, + T::Abi: ReturnAbi, { const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); type Abi = T::Abi; - fn return_from_abi(raw: MaybeUninit>) -> Self { + fn from_return_abi(raw: MaybeUninit>) -> Self { if let Some(error) = crate::exception::take() { #[cfg(not(target_feature = "exception-handling"))] if ::MODE.is_direct() { @@ -506,6 +626,8 @@ where } } +// Borrowed and cast JavaScript values. + /// A type that can be borrowed from an owned JavaScript conversion. /// /// The anchor owns the converted value for the duration of an exported diff --git a/client/js-sys/src/interop/primitive.rs b/client/js-sys/src/interop/primitive.rs index 6c8acef1..ef798fa4 100644 --- a/client/js-sys/src/interop/primitive.rs +++ b/client/js-sys/src/interop/primitive.rs @@ -1,6 +1,6 @@ use crate::hazard::{ - EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionIntoJS, ReturnAbi, ReturnMode, Slot, - WasmAbi, + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionFromAbi, OptionIntoAbi, ReturnAbi, + ReturnMode, Slot, WasmAbi, }; use crate::r#macro::const_concat; @@ -20,7 +20,7 @@ macro_rules! slot { macro_rules! from_js { ($($ty:ty),+ $(,)?) => {$( - // SAFETY: The JavaScript adapter produces this primitive's native `ABI` + // SAFETY: The JavaScript shim produces this primitive's native `ABI` // slot, which is returned unchanged. unsafe impl FromJS for $ty { type Abi = Self; @@ -58,17 +58,17 @@ macro_rules! sentinel_option { }),+ $(,)?], ) => {$($( // SAFETY: The sentinel lies outside the value range of this type. - unsafe impl OptionIntoJS for $ty { - const OPTION_JS_CONV: Option = Some(IntoJsConv::new(const_concat!( + unsafe impl OptionIntoAbi<$ty> for $ty { + const JS_CONV: Option = Some(IntoJsConv::new(const_concat!( "$slot1 === ", $js_sentinel, " ? undefined : ", $to_js ))); - type OptionAbi = $carrier; + type Abi = $carrier; - fn option_into_abi(value: Option) -> Self::OptionAbi { + fn into_option_abi(value: Option<$ty>) -> Self::Abi { value.map_or($sentinel, |value| { sentinel_option!(@into_abi value, $ty, $carrier) }) @@ -76,13 +76,12 @@ macro_rules! sentinel_option { } // SAFETY: The sentinel is decoded before the carrier is converted back. - unsafe impl FromJS for Option<$ty> { + unsafe impl OptionFromAbi<$ty> for $ty { const JS_CONV: Option = Some(FromJsConv::slot1(const_concat!( - "((value) => value == null ? ", + "$value == null ? ", $js_sentinel, " : ", - $from_js, - ")($value)" + $from_js ))); type Abi = $carrier; @@ -96,7 +95,7 @@ macro_rules! sentinel_option { clippy::cast_sign_loss, reason = "JavaScript normalizes the carrier to this type's value range" )] - fn from_abi(raw: Self::Abi) -> Self { + fn from_option_abi(raw: Self::Abi) -> Option<$ty> { if raw == $sentinel { None } else { @@ -153,31 +152,31 @@ macro_rules! indirect_option { // SAFETY: The decoder combines the presence tag and payload slots into one // optional JavaScript value. - unsafe impl OptionIntoJS for $ty { - const OPTION_JS_CONV: Option = Some( + unsafe impl OptionIntoAbi<$ty> for $ty { + const JS_CONV: Option = Some( IntoJsConv::new(indirect_option!(@decode $decode, [$($slot),+])) .with_embed(("js_sys", $decode)), ); - type OptionAbi = Option; + type Abi = Option<$ty>; - fn option_into_abi(value: Option) -> Self::OptionAbi { + fn into_option_abi(value: Option<$ty>) -> Self::Abi { value } } // SAFETY: The encoder writes a JavaScript value as a presence tag and the // payload slots expected by `Option<$ty>`. - unsafe impl FromJS for Option<$ty> { + unsafe impl OptionFromAbi<$ty> for $ty { const JS_CONV: Option = Some( indirect_option!(@output [$($slot),+]) .sret(const_concat!("this.#jsEmbed.js_sys['", $encode, "']")) .with_embed(("js_sys", $encode)), ); - type Abi = Self; + type Abi = Option<$ty>; - fn from_abi(raw: Self::Abi) -> Self { + fn from_option_abi(raw: Self::Abi) -> Option<$ty> { raw } } @@ -205,13 +204,57 @@ slot!("i32", isize, usize); #[cfg(target_arch = "wasm64")] slot!("i64", isize, usize); +// SAFETY: Unit has no Rust-to-JavaScript payload and becomes `undefined`. +unsafe impl IntoJS for () { + const JS_CONV: Option = Some(IntoJsConv::new("undefined")); + + type Abi = EmptySlot; + + fn into_abi(self) -> Self::Abi { + EmptySlot::new() + } +} + +// SAFETY: JavaScript-to-Rust unit conversion ignores the value and uses a +// direct zero placeholder so it can also represent a successful `Result<()>`. +unsafe impl FromJS for () { + const JS_CONV: Option = Some(FromJsConv::slot1("0")); + + type Abi = u32; + + fn from_abi(_: Self::Abi) -> Self {} +} + +// SAFETY: Zero denotes `None`; one denotes `Some(())`. +unsafe impl OptionIntoAbi<()> for EmptySlot { + const JS_CONV: Option = Some(IntoJsConv::new("$slot1 === 0 ? undefined : true")); + + type Abi = u32; + + fn into_option_abi(value: Option<()>) -> Self::Abi { + u32::from(value.is_some()) + } +} + +// SAFETY: `Nullish` JavaScript values become zero and all other values become +// the presence tag for `Some(())`. +unsafe impl OptionFromAbi<()> for u32 { + const JS_CONV: Option = Some(FromJsConv::slot1("$value == null ? 0 : 1")); + + type Abi = Self; + + fn from_option_abi(raw: Self::Abi) -> Option<()> { + (raw != 0).then_some(()) + } +} + identity!(u8, u16, i8, i16, i32, i64, isize, f32, f64); from_js!(bool, u32, u64, usize); // SAFETY: The JavaScript conversion normalizes the `i32` Wasm slot to a // `boolean`. unsafe impl IntoJS for bool { - const JS_CONV: Option = Some(IntoJsConv::new("!!$slot1")); + const JS_CONV: Option = Some(IntoJsConv::new("$slot1 !== 0")); type Abi = Self; @@ -400,9 +443,7 @@ js_bindgen::embed_js!( module = "js_sys", name = "numeric.u128.decode", "(lo, hi) => {{", - " return hi === 0n", - " ? BigInt.asUintN(64, lo)", - " : BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", + " return BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", "}}", ); @@ -445,11 +486,11 @@ sentinel_option! { types: [ [i8, u8, i16, u16] => { to_js: "$slot1", - from_js: "value", + from_js: "$value", }, [bool] => { to_js: "$slot1 !== 0", - from_js: "value ? 1 : 0", + from_js: "$value ? 1 : 0", }, ], } @@ -461,15 +502,15 @@ sentinel_option! { types: [ [i32] => { to_js: "$slot1", - from_js: "value >> 0", + from_js: "$value >> 0", }, [u32] => { to_js: "$slot1", - from_js: "value >>> 0", + from_js: "$value >>> 0", }, [f32] => { to_js: "$slot1", - from_js: "Math.fround(value)", + from_js: "Math.fround($value)", }, ], } @@ -482,11 +523,11 @@ sentinel_option! { types: [ [isize] => { to_js: "$slot1", - from_js: "value >> 0", + from_js: "$value >> 0", }, [usize] => { to_js: "$slot1", - from_js: "value >>> 0", + from_js: "$value >>> 0", }, ], } @@ -630,9 +671,7 @@ js_bindgen::embed_js!( name = "optional.u128.decode", "(isSome, lo, hi) => {{", " if (isSome === 0) return undefined", - " return hi === 0n", - " ? BigInt.asUintN(64, lo)", - " : BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", + " return BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", "}}", ); diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index 95799752..5d8ee6fc 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -13,30 +13,19 @@ pub use wat::*; // Text rendering. pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; -// JavaScript export adapters. +// JavaScript export shims. pub use crate::{ js_export, js_export_arguments, js_export_input_arguments, js_export_output_expression, js_export_parameters, }; -// JavaScript import adapters. +// JavaScript import shims. pub use crate::{ - js_function, js_import, js_indirect_function, js_input_parameters, js_needs_adapter, js_output, + js_function, js_import, js_indirect_function, js_input_parameters, js_needs_shim, js_output, js_parameter, }; -// WAT export adapters. -pub use crate::{ - wat_export, wat_export_direct, wat_export_imports, wat_export_indirect, wat_export_input_gets, - wat_export_input_params, wat_export_input_raw_param, wat_export_input_raw_types, - wat_export_result_loads, wat_export_result_types, -}; -// WAT import adapters. -pub use crate::{ - wat_import, wat_import_input_types, wat_import_result, wat_imports, wat_input_gets, - wat_input_import_types, wat_input_params, wat_output, wat_output_get, wat_output_import_param, - wat_output_param, wat_output_result, -}; -// Shared WAT and exception helpers. -pub use crate::{ - wat_import_list, wat_result_catch, wat_result_default, wat_result_try, wat_slot_params, - wat_slot_types, -}; +// WAT export shims. +pub use crate::{wat_export, wat_export_direct, wat_export_indirect}; +// WAT import shims. +pub use crate::{wat_import, wat_import_output}; +// Shared WAT helpers. +pub use crate::{wat_imports, wat_input, wat_locals, wat_slots, wat_unique_list}; diff --git a/client/js-sys/src/macro/abi.rs b/client/js-sys/src/macro/abi.rs index 9703e2eb..07aba83a 100644 --- a/client/js-sys/src/macro/abi.rs +++ b/client/js-sys/src/macro/abi.rs @@ -9,6 +9,11 @@ pub type InputSlot2 = <::Abi as WasmAbi>::Slot2; pub type InputSlot3 = <::Abi as WasmAbi>::Slot3; pub type InputSlot4 = <::Abi as WasmAbi>::Slot4; +pub type FromJsSlot1 = <::Abi as WasmAbi>::Slot1; +pub type FromJsSlot2 = <::Abi as WasmAbi>::Slot2; +pub type FromJsSlot3 = <::Abi as WasmAbi>::Slot3; +pub type FromJsSlot4 = <::Abi as WasmAbi>::Slot4; + pub type OutputSlot1 = <::Abi as WasmAbi>::Slot1; pub type OutputSlot2 = <::Abi as WasmAbi>::Slot2; pub type OutputSlot3 = <::Abi as WasmAbi>::Slot3; @@ -42,7 +47,7 @@ pub fn join_from_js( #[must_use] #[inline] pub fn return_to_js(value: T) -> WasmRet { - WasmRet::from_abi(T::return_into_abi(value)) + WasmRet::from_abi(T::into_return_abi(value)) } /// Lowers a value through a different [`IntoJS`] implementation with the same @@ -64,7 +69,7 @@ pub unsafe fn split_input_as( #[must_use] #[inline] pub fn join_output(value: OutputRet) -> T { - T::return_from_abi(value) + T::from_return_abi(value) } // Compile-time validation of conversion metadata. @@ -83,6 +88,30 @@ pub const fn validate_into_js() { ); } +pub const fn validate_from_js() { + let conversion = T::JS_CONV; + let templates = match conversion { + None => [""; 4], + Some(conv) => conv.templates, + }; + let slots = from_js_wat_slots::(); + let mut slot = 0; + + while slot < slots.len() { + assert!( + conversion.is_none() || templates[slot].is_empty() == slots[slot].abi.is_empty(), + "FromJS::JS_CONV templates must match its non-empty ABI slots", + ); + slot += 1; + } + + assert!( + conversion.is_some() + || slots[1].abi.is_empty() && slots[2].abi.is_empty() && slots[3].abi.is_empty(), + "multi-slot FromJS implementations must define FromJS::JS_CONV", + ); +} + pub const fn validate_return_from_js() { let indirect = !return_from_js_is_direct::(); let conversion = T::JS_CONV.conversion(); @@ -90,7 +119,7 @@ pub const fn validate_return_from_js() { None => ([""; 4], None), Some(conv) => (conv.templates, conv.sret), }; - let slots = from_js_wat_slots::(); + let slots = return_from_js_wat_slots::(); let mut slot = 0; while slot < slots.len() { @@ -111,7 +140,7 @@ pub const fn validate_return_from_js() { ); } -// WAT metadata shared by import and export adapters. +// WAT metadata shared by import and export shims. /// The `WAT` representation of one `ABI` slot at a JavaScript boundary. #[doc(hidden)] @@ -121,33 +150,41 @@ pub struct WatSlot { pub abi: &'static str, /// The type visible at the JavaScript boundary. pub boundary: &'static str, - /// An optional WAT import required by the conversion. - pub import: &'static str, + /// Optional WAT imports required by the conversion. + pub imports: &'static str, + /// Optional scratch locals required by the conversion. + pub locals: &'static str, /// WAT instructions that convert between `abi` and `boundary`. pub conv: &'static str, } const fn wat_slot(wat_conv: Option) -> WatSlot { - let (boundary, import, conv) = match wat_conv { + let (boundary, imports, locals, conv) = match wat_conv { Some(WatConv { - import, + imports, + locals, conv, r#type, }) => ( r#type, - match import { - Some(import) => import, + match imports { + Some(imports) => imports, + None => "", + }, + match locals { + Some(locals) => locals, None => "", }, conv, ), - None => (S::WAT_TYPE, "", ""), + None => (S::WAT_TYPE, "", "", ""), }; WatSlot { abi: S::WAT_TYPE, boundary, - import, + imports, + locals, conv, } } @@ -163,7 +200,17 @@ pub const fn into_js_wat_slots() -> [WatSlot; 4] { } #[must_use] -pub const fn from_js_wat_slots() -> [WatSlot; 4] { +pub const fn from_js_wat_slots() -> [WatSlot; 4] { + [ + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + ] +} + +#[must_use] +pub const fn return_from_js_wat_slots() -> [WatSlot; 4] { [ wat_slot::>( as Slot>::FROM_JS_WAT_CONV), wat_slot::>( as Slot>::FROM_JS_WAT_CONV), @@ -185,7 +232,7 @@ pub const fn return_into_js_wat_slots() -> [WatSlot; 4] { #[must_use] pub const fn wat_direct() -> &'static str { if return_from_js_is_direct::() { - from_js_wat_slots::()[0].abi + return_from_js_wat_slots::()[0].abi } else { "" } @@ -219,18 +266,27 @@ pub const fn wat_indirect_conv() -> &'static str { } #[must_use] -pub const fn wat_output_import() -> &'static str { +pub const fn wat_output_imports() -> &'static str { if return_from_js_is_direct::() { - from_js_wat_slots::()[0].import + return_from_js_wat_slots::()[0].imports } else { "" } } +#[must_use] +pub const fn wat_output_locals() -> &'static str { + if return_from_js_is_direct::() { + return_from_js_wat_slots::()[0].locals + } else { + into_js_wat_slots::>()[0].locals + } +} + #[must_use] pub const fn wat_output_import_type() -> &'static str { if return_from_js_is_direct::() { - from_js_wat_slots::()[0].boundary + return_from_js_wat_slots::()[0].boundary } else { "" } @@ -239,7 +295,7 @@ pub const fn wat_output_import_type() -> &'static str { #[must_use] pub const fn wat_output_conv() -> &'static str { if return_from_js_is_direct::() { - from_js_wat_slots::()[0].conv + return_from_js_wat_slots::()[0].conv } else { "" } @@ -258,7 +314,7 @@ pub const fn return_from_js_is_result() -> bool { pub const fn validate_return_into_js() { let conv = T::JS_CONV.conversion(); let multislot = if T::JS_CONV.is_result() { - ! as Slot>::WAT_TYPE.is_empty() + ! as Slot>::WAT_TYPE.is_empty() } else { ! as Slot>::WAT_TYPE.is_empty() || ! as Slot>::WAT_TYPE.is_empty() @@ -278,7 +334,7 @@ pub const fn return_into_js_is_direct() -> bool { #[must_use] pub const fn export_output_frame_size() -> usize { - // LLVM keeps the Wasm stack pointer 16-byte aligned. Rounding every adapter + // LLVM keeps the Wasm stack pointer 16-byte aligned. Rounding every shim // frame to that alignment preserves the invariant when the frame is allocated. const STACK_ALIGNMENT: usize = 16; let size = core::mem::size_of::>(); @@ -322,6 +378,14 @@ pub const fn js_output_embed() -> (&'static str, &'static str) }) } +#[must_use] +pub const fn js_from_embed() -> (&'static str, &'static str) { + js_embed(match T::JS_CONV { + Some(conv) => conv.embed, + None => None, + }) +} + #[must_use] pub const fn js_result_embed() -> (&'static str, &'static str) { if T::JS_CONV.is_result() { @@ -376,6 +440,20 @@ pub const fn js_output_templates() -> [&'static str; 4] { } } +#[must_use] +pub const fn js_from_templates() -> [&'static str; 4] { + if let Some(conv) = T::JS_CONV { + conv.templates + } else { + ["$value", "", "", ""] + } +} + +#[must_use] +pub const fn js_output_has_conversion() -> bool { + T::JS_CONV.conversion().is_some() +} + #[must_use] pub const fn js_output_sret() -> &'static str { if let Some(conv) = T::JS_CONV.conversion() diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs index 2de7f246..bc2f772e 100644 --- a/client/js-sys/src/macro/export.rs +++ b/client/js-sys/src/macro/export.rs @@ -1,127 +1,33 @@ -// WAT adapter helpers. - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_imports { - (($($input:ty),*) $(, $output:ty)? $(,)?) => { - $crate::r#macro::wat_import_list!( - $($crate::r#macro::from_js_wat_slots::<$input>()[0].import,)* - $($crate::r#macro::from_js_wat_slots::<$input>()[1].import,)* - $($crate::r#macro::from_js_wat_slots::<$input>()[2].import,)* - $($crate::r#macro::from_js_wat_slots::<$input>()[3].import,)* - $($crate::r#macro::return_into_js_wat_slots::<$output>()[0].import,)? - $($crate::r#macro::return_into_js_wat_slots::<$output>()[1].import,)? - $($crate::r#macro::return_into_js_wat_slots::<$output>()[2].import,)? - $($crate::r#macro::return_into_js_wat_slots::<$output>()[3].import,)? - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_input_raw_types { - ($ty:ty $(,)?) => { - $crate::r#macro::wat_slot_types!($crate::r#macro::from_js_wat_slots::<$ty>(), abi,) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_input_raw_param { - ($ty:ty $(,)?) => {{ - const TYPES: &::core::primitive::str = - $crate::r#macro::wat_export_input_raw_types!($ty); - - $crate::r#macro::const_concat_if!( - !TYPES.is_empty() => [" (param ", TYPES, ")"], - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_input_params { - ($par:literal, $ty:ty $(,)?) => { - $crate::r#macro::wat_slot_params!( - $par, - $crate::r#macro::from_js_wat_slots::<$ty>(), - boundary, - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_input_gets { - ($par:literal, $ty:ty $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::from_js_wat_slots::<$ty>(); - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => [" local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], - !SLOTS[1].abi.is_empty() => [" local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], - !SLOTS[2].abi.is_empty() => [" local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], - !SLOTS[3].abi.is_empty() => [" local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_result_types { - ($ty:ty $(,)?) => { - $crate::r#macro::wat_slot_types!( - $crate::r#macro::return_into_js_wat_slots::<$ty>(), - boundary, - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_result_loads { - ($ty:ty $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::return_into_js_wat_slots::<$ty>(); - const OFFSET_0: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 0>() - ); - const OFFSET_1: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 1>() - ); - const OFFSET_2: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 2>() - ); - const OFFSET_3: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 3>() - ); - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => [" local.get $retptr\n ", SLOTS[0].abi, ".load offset=", OFFSET_0, $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], - !SLOTS[1].abi.is_empty() => [" local.get $retptr\n ", SLOTS[1].abi, ".load offset=", OFFSET_1, $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], - !SLOTS[2].abi.is_empty() => [" local.get $retptr\n ", SLOTS[2].abi, ".load offset=", OFFSET_2, $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], - !SLOTS[3].abi.is_empty() => [" local.get $retptr\n ", SLOTS[3].abi, ".load offset=", OFFSET_3, $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], - ) - }}; -} +// WAT shim generation. #[doc(hidden)] #[macro_export] macro_rules! wat_export_direct { ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => { $crate::r#macro::const_concat!( - $crate::r#macro::wat_export_imports!(($($input),*)), + $crate::r#macro::wat_imports!( + slots = [ + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + ], + extras = [], + ), "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", $raw, "\"))", - $($crate::r#macro::wat_export_input_raw_param!($input),)* + $($crate::r#macro::wat_input!(export raw_param; $input),)* "))\n", "(func $export (@sym (name \"", $export, "\"))", - $($crate::r#macro::wat_export_input_params!($par, $input),)* + $($crate::r#macro::wat_input!(export params; $par, $input),)* + $crate::r#macro::wat_locals!( + slots = [ + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + ], + extras = [], + ), "\n", - $($crate::r#macro::wat_export_input_gets!($par, $input),)* + $($crate::r#macro::wat_input!(export gets; $par, $input),)* " call $raw (@reloc)\n", ")" ) @@ -131,22 +37,36 @@ macro_rules! wat_export_direct { $crate::r#macro::return_into_js_wat_slots::<$output>()[0]; $crate::r#macro::const_concat!( - $crate::r#macro::wat_export_imports!(($($input),*), $output), + $crate::r#macro::wat_imports!( + slots = [ + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", $raw, "\"))", - $($crate::r#macro::wat_export_input_raw_param!($input),)* + $($crate::r#macro::wat_input!(export raw_param; $input),)* " (result ", SLOT.abi, ")))\n", "(func $export (@sym (name \"", $export, "\"))", - $($crate::r#macro::wat_export_input_params!($par, $input),)* + $($crate::r#macro::wat_input!(export params; $par, $input),)* " (result ", SLOT.boundary, - ")\n", - $($crate::r#macro::wat_export_input_gets!($par, $input),)* + ")", + $crate::r#macro::wat_locals!( + slots = [ + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), + "\n", + $($crate::r#macro::wat_input!(export gets; $par, $input),)* " call $raw (@reloc)", $crate::r#macro::wat_conv_prefix(SLOT.conv), SLOT.conv, @@ -164,16 +84,26 @@ macro_rules! wat_export_indirect { $crate::r#macro::export_output_frame_size::<$output>() ); const RESULT_TYPES: &::core::primitive::str = - $crate::r#macro::wat_export_result_types!($output); + $crate::r#macro::wat_slots!( + types, + $crate::r#macro::return_into_js_wat_slots::<$output>(), + boundary, + ); $crate::r#macro::const_concat!( - $crate::r#macro::wat_export_imports!(($($input),*), $output), + $crate::r#macro::wat_imports!( + slots = [ + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", $raw, "\")) (param ", POINTER, ")", - $($crate::r#macro::wat_export_input_raw_param!($input),)* + $($crate::r#macro::wat_input!(export raw_param; $input),)* "))\n", "(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut ", POINTER, @@ -181,13 +111,21 @@ macro_rules! wat_export_indirect { "(func $export (@sym (name \"", $export, "\"))", - $($crate::r#macro::wat_export_input_params!($par, $input),)* + $($crate::r#macro::wat_input!(export params; $par, $input),)* " (result ", RESULT_TYPES, ")\n", " (local $retptr ", POINTER, - ")\n", + ")", + $crate::r#macro::wat_locals!( + slots = [ + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), + "\n", " global.get $__stack_pointer\n ", POINTER, ".const ", @@ -196,9 +134,13 @@ macro_rules! wat_export_indirect { POINTER, ".sub\n local.tee $retptr\n global.set $__stack_pointer\n", " local.get $retptr\n", - $($crate::r#macro::wat_export_input_gets!($par, $input),)* + $($crate::r#macro::wat_input!(export gets; $par, $input),)* " call $raw (@reloc)\n", - $crate::r#macro::wat_export_result_loads!($output), + $crate::r#macro::wat_slots!( + loads, + $output, + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ), " local.get $retptr\n ", POINTER, ".const ", @@ -210,17 +152,17 @@ macro_rules! wat_export_indirect { }}; } -/// Generates the complete WAT adapter for one Rust export. +/// Generates the complete WAT shim for one Rust export. #[doc(hidden)] #[macro_export] macro_rules! wat_export { ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - $($crate::r#macro::validate_return_from_js::<$input>();)* + $($crate::r#macro::validate_from_js::<$input>();)* $crate::r#macro::wat_export_direct!($raw, $export, ($(($par, $input)),*)) }}; ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - $($crate::r#macro::validate_return_from_js::<$input>();)* + $($crate::r#macro::validate_from_js::<$input>();)* $crate::r#macro::validate_return_into_js::<$output>(); if $crate::r#macro::return_into_js_is_direct::<$output>() { @@ -250,7 +192,7 @@ macro_rules! js_export_input_arguments { const SLOTS: [$crate::r#macro::WatSlot; 4] = $crate::r#macro::from_js_wat_slots::<$ty>(); const TEMPLATES: [&::core::primitive::str; 4] = - $crate::r#macro::js_output_templates::<$ty>(); + $crate::r#macro::js_from_templates::<$ty>(); const VALUES: [&::core::primitive::str; 4] = [ $crate::r#macro::js_template!(TEMPLATES[0], value = $par), $crate::r#macro::js_template!(TEMPLATES[1], value = $par), @@ -315,7 +257,7 @@ macro_rules! js_export_output_expression { const DIRECT: ::core::primitive::bool = $crate::r#macro::return_into_js_is_direct::<$ty>(); const RESULT: ::core::primitive::bool = $crate::r#macro::return_into_js_is_result::<$ty>(); const VALUES: [&::core::primitive::str; 4] = if RESULT { - ["ret[2]", "ret[3]", "", ""] + ["ret[0]", "ret[1]", "", ""] } else if DIRECT { ["ret", "", "", ""] } else { @@ -334,7 +276,7 @@ macro_rules! js_export_output_expression { #[macro_export] macro_rules! js_export { ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - $($crate::r#macro::validate_return_from_js::<$input>();)* + $($crate::r#macro::validate_from_js::<$input>();)* const PARAMETERS: &::core::primitive::str = $crate::r#macro::js_export_parameters!($(($par, $input)),*); const ARGUMENTS: &::core::primitive::str = @@ -343,7 +285,7 @@ macro_rules! js_export { $crate::r#macro::const_concat!( "(", PARAMETERS, - ") => {\n instance.exports['", + ") => {\n wasmExports['", $export, "'](", ARGUMENTS, @@ -351,7 +293,7 @@ macro_rules! js_export { ) }}; ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - $($crate::r#macro::validate_return_from_js::<$input>();)* + $($crate::r#macro::validate_from_js::<$input>();)* $crate::r#macro::validate_return_into_js::<$output>(); const PARAMETERS: &::core::primitive::str = $crate::r#macro::js_export_parameters!($(($par, $input)),*); @@ -359,9 +301,34 @@ macro_rules! js_export { $crate::r#macro::js_export_arguments!($(($par, $input)),*); const OUTPUT: &::core::primitive::str = $crate::r#macro::js_export_output_expression!($output); - const THROW: &::core::primitive::str = - if $crate::r#macro::return_into_js_is_result::<$output>() { - " if (ret[1] !== 0) throw ret[0]\n" + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::return_into_js_wat_slots::<$output>(); + const ERROR_DISCRIMINANT: &::core::primitive::str = + if SLOTS[0].abi.is_empty() { + "ret[0]" + } else if SLOTS[1].abi.is_empty() { + "ret[1]" + } else { + "ret[2]" + }; + const ERROR: &::core::primitive::str = + if SLOTS[0].abi.is_empty() { + "ret[1]" + } else if SLOTS[1].abi.is_empty() { + "ret[2]" + } else { + "ret[3]" + }; + const THROW: &::core::primitive::str = if $crate::r#macro::return_into_js_is_result::< + $output, + >() { + $crate::r#macro::const_concat!( + " if (", + ERROR_DISCRIMINANT, + " !== 0) throw ", + ERROR, + "\n", + ) } else { "" }; @@ -369,7 +336,7 @@ macro_rules! js_export { $crate::r#macro::const_concat!( "(", PARAMETERS, - ") => {\n const ret = instance.exports['", + ") => {\n const ret = wasmExports['", $export, "'](", ARGUMENTS, diff --git a/client/js-sys/src/macro/js_import.rs b/client/js-sys/src/macro/js_import.rs index 15bcada2..4fbcbc3b 100644 --- a/client/js-sys/src/macro/js_import.rs +++ b/client/js-sys/src/macro/js_import.rs @@ -1,4 +1,4 @@ -/// Generates the complete JavaScript adapter for one import. +/// Generates the complete JavaScript shim for one import. #[doc(hidden)] #[macro_export] macro_rules! js_import { @@ -9,7 +9,7 @@ macro_rules! js_import { inputs = [$(($par:literal, $input:ty)),* $(,)?], ) => {{ const WRAPPED: ::core::primitive::bool = - $crate::r#macro::js_needs_adapter!(($($input),*)); + $crate::r#macro::js_needs_shim!(($($input),*)); const OPEN: &::core::primitive::str = if WRAPPED { $crate::r#macro::js_function!("(", ") => {\n", $(($par, $input)),*) } else { @@ -35,7 +35,7 @@ macro_rules! js_import { output = $output:ty, ) => {{ const WRAPPED: ::core::primitive::bool = - $crate::r#macro::js_needs_adapter!(($($input),*), $output); + $crate::r#macro::js_needs_shim!(($($input),*), $output); const OPEN: &::core::primitive::str = if WRAPPED { $crate::r#macro::js_indirect_function!( "(", @@ -61,11 +61,11 @@ macro_rules! js_import { }}; } -// Adapter selection. +// Shim selection. #[doc(hidden)] #[macro_export] -macro_rules! js_needs_adapter { +macro_rules! js_needs_shim { (($($input:ty),*) $(, $output:ty)? $(,)?) => {{ 'outer: { $( @@ -181,6 +181,8 @@ macro_rules! js_output { const OUTPUT_WRAPPED: ::core::primitive::bool = $wrapped; const DIRECT_RETURN: ::core::primitive::bool = $crate::r#macro::return_from_js_is_direct::<$output>(); + const CONVERT_DIRECT: ::core::primitive::bool = + DIRECT_RETURN && $crate::r#macro::js_output_has_conversion::<$output>(); const CATCH_RESULT: ::core::primitive::bool = $crate::r#macro::catches_result_in_js::<$output>(); const CALL: &::core::primitive::str = if OUTPUT_WRAPPED { @@ -190,7 +192,11 @@ macro_rules! js_output { }; const TEMPLATES: [&::core::primitive::str; 4] = $crate::r#macro::js_output_templates::<$output>(); - const TEMPLATE_VALUE: &::core::primitive::str = if DIRECT_RETURN { CALL } else { "$ret" }; + const TEMPLATE_VALUE: &::core::primitive::str = if DIRECT_RETURN && !CONVERT_DIRECT { + CALL + } else { + "$ret" + }; const SLOTS: [&::core::primitive::str; 4] = [ $crate::r#macro::js_template!(TEMPLATES[0], value = TEMPLATE_VALUE), $crate::r#macro::js_template!(TEMPLATES[1], value = TEMPLATE_VALUE), @@ -199,7 +205,9 @@ macro_rules! js_output { ]; const SRET: &::core::primitive::str = $crate::r#macro::js_output_sret::<$output>(); const INDENT: &::core::primitive::str = if CATCH_RESULT { " " } else { " " }; - const VALUE_START: &::core::primitive::str = if DIRECT_RETURN { + const VALUE_START: &::core::primitive::str = if CONVERT_DIRECT { + $crate::r#macro::const_concat!(INDENT, "const $ret = ") + } else if DIRECT_RETURN { if CATCH_RESULT { " return " } else if OUTPUT_WRAPPED { @@ -210,7 +218,14 @@ macro_rules! js_output { } else { $crate::r#macro::const_concat!(INDENT, "const $ret = ") }; - const OUTPUT_VALUE: &::core::primitive::str = if DIRECT_RETURN { SLOTS[0] } else { CALL }; + const OUTPUT_VALUE: &::core::primitive::str = if DIRECT_RETURN && !CONVERT_DIRECT { + SLOTS[0] + } else { + CALL + }; + const DIRECT_CONVERSION: &::core::primitive::str = $crate::r#macro::const_concat_if!( + CONVERT_DIRECT => ["\n", INDENT, "return ", SLOTS[0]], + ); const SRET_CALL: &::core::primitive::str = $crate::r#macro::const_concat_if!( !DIRECT_RETURN => ["\n", INDENT, SRET, "(", SLOTS[0]], !DIRECT_RETURN && !SLOTS[1].is_empty() => [", ", SLOTS[1]], @@ -227,6 +242,13 @@ macro_rules! js_output { "" }; - $crate::r#macro::const_concat!(TRY, VALUE_START, OUTPUT_VALUE, SRET_CALL, END) + $crate::r#macro::const_concat!( + TRY, + VALUE_START, + OUTPUT_VALUE, + DIRECT_CONVERSION, + SRET_CALL, + END + ) }}; } diff --git a/client/js-sys/src/macro/result.rs b/client/js-sys/src/macro/result.rs index a7f1c154..38db99e0 100644 --- a/client/js-sys/src/macro/result.rs +++ b/client/js-sys/src/macro/result.rs @@ -1,43 +1,51 @@ +#[cfg(target_feature = "exception-handling")] +use crate::externref::{ + WAT_INDEX_LOCAL, WAT_INSERT_CONV, WAT_NEXT_IMPORT, WAT_TABLE_IMPORT, WAT_VALUE_LOCAL, +}; use crate::hazard::ReturnFromJS; #[cfg(not(target_feature = "exception-handling"))] -const DIRECT_CATCH: &str = concat!( - "\n } catch ($error) {", - "\n const $index = this.#instance.exports['js_sys.exception.store']()", - "\n this.#jsEmbed.js_sys['externref.table'].set($index, $error)", - "\n return false", - "\n }", - "\n}", -); +const DIRECT_CATCH: &str = " + } catch ($error) { + const $index = this.#instance.exports['js_sys.exception.store']() + this.#jsEmbed.js_sys['externref.table'].set($index, $error) + return false + } +}"; #[cfg(not(target_feature = "exception-handling"))] -const INDIRECT_CATCH: &str = concat!( - "\n } catch ($error) {", - "\n const $index = this.#instance.exports['js_sys.exception.store']()", - "\n this.#jsEmbed.js_sys['externref.table'].set($index, $error)", - "\n }", - "\n}", -); +const INDIRECT_CATCH: &str = " + } catch ($error) { + const $index = this.#instance.exports['js_sys.exception.store']() + this.#jsEmbed.js_sys['externref.table'].set($index, $error) + } +}"; #[cfg(target_feature = "exception-handling")] const WAT_TAG_IMPORT: &str = "(import \"js_sys\" \"exception.tag\" (tag $js_sys.exception.tag \ (@sym (name \"js_sys.exception.tag\")) (param externref)))"; #[cfg(target_feature = "exception-handling")] -const WAT_INSERT_IMPORT: &str = concat!( - "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) ", - "(param externref) (result i32)))", -); +const WAT_STORE_IMPORT: &str = + "(import \"env\" \"js_sys.exception.store\" (func $js_sys.exception.store (@sym) (param i32)))"; #[cfg(target_feature = "exception-handling")] -const WAT_STORE_IMPORT: &str = concat!( - "(import \"env\" \"js_sys.exception.store\" (func $js_sys.exception.store (@sym) ", - "(param i32)))", +const WAT_IMPORTS: &str = crate::const_concat!( + WAT_TAG_IMPORT, + "\n", + WAT_TABLE_IMPORT, + "\n", + WAT_NEXT_IMPORT, + "\n", + WAT_STORE_IMPORT, ); #[cfg(target_feature = "exception-handling")] -const WAT_CATCH: &str = concat!( +const WAT_LOCALS: &str = crate::const_concat!(WAT_VALUE_LOCAL, "\n", WAT_INDEX_LOCAL); +#[cfg(target_feature = "exception-handling")] +const WAT_CATCH: &str = crate::const_concat!( "\n return", "\n )", "\n unreachable", "\n )", - "\n call $js_sys.externref.insert (@reloc)", + "\n ", + WAT_INSERT_CONV, "\n call $js_sys.exception.store (@reloc)", ); @@ -92,19 +100,36 @@ pub const fn js_result_catch(direct: bool) -> &'static str { } #[must_use] -pub const fn wat_result_imports() -> [&'static str; 3] { +pub const fn wat_result_imports() -> &'static str { + #[cfg(target_feature = "exception-handling")] + { + if crate::r#macro::return_from_js_is_result::() { + WAT_IMPORTS + } else { + "" + } + } + + #[cfg(not(target_feature = "exception-handling"))] + { + "" + } +} + +#[must_use] +pub const fn wat_result_locals() -> &'static str { #[cfg(target_feature = "exception-handling")] { if crate::r#macro::return_from_js_is_result::() { - [WAT_TAG_IMPORT, WAT_INSERT_IMPORT, WAT_STORE_IMPORT] + WAT_LOCALS } else { - [""; 3] + "" } } #[cfg(not(target_feature = "exception-handling"))] { - [""; 3] + "" } } @@ -167,27 +192,3 @@ pub const fn wat_result_default() -> &'static str { "" } } - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_result_try { - ($ty:ty) => { - $crate::r#macro::wat_result_try::<$ty>() - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_result_catch { - ($ty:ty) => { - $crate::r#macro::wat_result_catch::<$ty>() - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_result_default { - ($ty:ty) => { - $crate::r#macro::wat_result_default::<$ty>() - }; -} diff --git a/client/js-sys/src/macro/text.rs b/client/js-sys/src/macro/text.rs index 35cb4605..fae18c33 100644 --- a/client/js-sys/src/macro/text.rs +++ b/client/js-sys/src/macro/text.rs @@ -122,11 +122,6 @@ macro_rules! const_integer_str { }}; } -#[must_use] -pub const fn separator(value: &str) -> &'static str { - if value.is_empty() { "" } else { " " } -} - #[must_use] pub const fn separator_between(left: &str, right: &str) -> &'static str { if left.is_empty() || right.is_empty() { diff --git a/client/js-sys/src/macro/wat.rs b/client/js-sys/src/macro/wat.rs index 2e4c8505..197ea0d4 100644 --- a/client/js-sys/src/macro/wat.rs +++ b/client/js-sys/src/macro/wat.rs @@ -4,93 +4,161 @@ pub const fn wat_conv_prefix(value: &str) -> &'static str { } #[must_use] -pub const fn wat_import_iter<'a>(values: &[&'a str], index: usize) -> Option<&'a str> { - let value = values[index]; +const fn wat_line_end(value: &str, start: usize) -> usize { + let bytes = value.as_bytes(); + let mut end = start; - if value.is_empty() { - return None; + while end < bytes.len() && bytes[end] != b'\n' { + end += 1; } - let mut candidate_index = 0; + end +} - while candidate_index < index { - let candidate = values[candidate_index]; +const fn wat_lines_equal( + left: &str, + left_start: usize, + left_end: usize, + right: &str, + right_start: usize, + right_end: usize, +) -> bool { + if left_end - left_start != right_end - right_start { + return false; + } - if value.len() == candidate.len() { - let mut byte_index = 0; - let mut equal = true; + let left = left.as_bytes(); + let right = right.as_bytes(); + let mut offset = 0; - while byte_index < value.len() { - if value.as_bytes()[byte_index] != candidate.as_bytes()[byte_index] { - equal = false; - break; - } + while left_start + offset < left_end { + if left[left_start + offset] != right[right_start + offset] { + return false; + } - byte_index += 1; - } + offset += 1; + } + + true +} + +const fn wat_line_was_seen( + values: &[&str], + value_index: usize, + line_start: usize, + line_end: usize, +) -> bool { + let value = values[value_index]; + let mut candidate_value_index = 0; + + while candidate_value_index <= value_index { + let candidate = values[candidate_value_index]; + let limit = if candidate_value_index == value_index { + line_start + } else { + candidate.len() + }; + let mut candidate_start = 0; + + while candidate_start < limit { + let candidate_end = wat_line_end(candidate, candidate_start); - if equal { - return None; + if candidate_end != candidate_start + && wat_lines_equal( + value, + line_start, + line_end, + candidate, + candidate_start, + candidate_end, + ) { + return true; } + + candidate_start = candidate_end + 1; } - candidate_index += 1; + candidate_value_index += 1; } - Some(value) + false } -#[doc(hidden)] -#[macro_export] -macro_rules! wat_import_list { - ($($value:expr),* $(,)?) => {{ - const VALUES: &[&::core::primitive::str] = &[$($value),*]; - const SIZE: ::core::primitive::usize = { - let mut size = 0; - let mut index = 0; - - while index < VALUES.len() { - if let ::core::option::Option::Some(value) = - $crate::r#macro::wat_import_iter(VALUES, index) - { - size += 1 + value.len(); - } +#[must_use] +pub const fn wat_unique_lines_len(values: &[&str]) -> usize { + let mut size = 0; + let mut value_index = 0; + + while value_index < values.len() { + let value = values[value_index]; + let mut line_start = 0; + + while line_start < value.len() { + let line_end = wat_line_end(value, line_start); - index += 1; + if line_end != line_start + && !wat_line_was_seen(values, value_index, line_start, line_end) + { + size += 1 + line_end - line_start; } - size - }; + line_start = line_end + 1; + } - const IMPORTS: [::core::primitive::u8; SIZE] = { - let mut imports = [0; SIZE]; - let mut byte_index = 0; - let mut value_index = 0; + value_index += 1; + } - while value_index < VALUES.len() { - if let ::core::option::Option::Some(value) = - $crate::r#macro::wat_import_iter(VALUES, value_index) - { - imports[byte_index] = b'\n'; - byte_index += 1; + size +} - let value = value.as_bytes(); - let mut index = 0; +#[must_use] +pub const fn render_wat_unique_lines(values: &[&str]) -> [u8; SIZE] { + let mut output = [0; SIZE]; + let mut output_index = 0; + let mut value_index = 0; - while index < value.len() { - imports[byte_index] = value[index]; - byte_index += 1; - index += 1; - } - } + while value_index < values.len() { + let value = values[value_index]; + let bytes = value.as_bytes(); + let mut line_start = 0; + + while line_start < bytes.len() { + let line_end = wat_line_end(value, line_start); + + if line_end != line_start + && !wat_line_was_seen(values, value_index, line_start, line_end) + { + output[output_index] = b'\n'; + output_index += 1; - value_index += 1; + let mut byte_index = line_start; + while byte_index < line_end { + output[output_index] = bytes[byte_index]; + output_index += 1; + byte_index += 1; + } } - imports - }; + line_start = line_end + 1; + } + + value_index += 1; + } + + output +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_unique_list { + ($($value:expr),* $(,)?) => {{ + const VALUES: &[&::core::primitive::str] = &[$($value),*]; + const SIZE: ::core::primitive::usize = + $crate::r#macro::wat_unique_lines_len(VALUES); + const OUTPUT: [::core::primitive::u8; SIZE] = + $crate::r#macro::render_wat_unique_lines(VALUES); - if let ::core::result::Result::Ok(value) = ::core::str::from_utf8(&IMPORTS) { + if let ::core::result::Result::Ok(value) = ::core::str::from_utf8(&OUTPUT) { value } else { ::core::panic!() @@ -100,26 +168,82 @@ macro_rules! wat_import_list { #[doc(hidden)] #[macro_export] -macro_rules! wat_slot_types { - ($slots:expr, $field:ident $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; +macro_rules! wat_imports { + ( + slots = [$($slots:expr),* $(,)?], + extras = [$($extra:expr),* $(,)?], + ) => { + $crate::r#macro::wat_unique_list!( + $( + ($slots)[0].imports, + ($slots)[1].imports, + ($slots)[2].imports, + ($slots)[3].imports, + )* + $($extra,)* + ) + }; +} - $crate::r#macro::const_concat!( - SLOTS[0].$field, - $crate::r#macro::separator(SLOTS[1].$field), - SLOTS[1].$field, - $crate::r#macro::separator(SLOTS[2].$field), - SLOTS[2].$field, - $crate::r#macro::separator(SLOTS[3].$field), - SLOTS[3].$field +#[doc(hidden)] +#[macro_export] +macro_rules! wat_locals { + ( + slots = [$($slots:expr),* $(,)?], + extras = [$($extra:expr),* $(,)?], + ) => { + $crate::r#macro::wat_unique_list!( + $( + ($slots)[0].locals, + ($slots)[1].locals, + ($slots)[2].locals, + ($slots)[3].locals, + )* + $($extra,)* ) - }}; + }; } +/// Renders the repeated parts of a four-slot `WasmAbi`. #[doc(hidden)] #[macro_export] -macro_rules! wat_slot_params { - ($par:literal, $slots:expr, $field:ident $(,)?) => {{ +macro_rules! wat_slots { + (types, $slots:expr, $field:ident $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; + const SEP1: &::core::primitive::str = + if SLOTS[0].$field.is_empty() { "" } else { " " }; + const SEP2: &::core::primitive::str = + if SLOTS[0].$field.is_empty() && SLOTS[1].$field.is_empty() { + "" + } else { + " " + }; + const SEP3: &::core::primitive::str = + if SLOTS[0].$field.is_empty() + && SLOTS[1].$field.is_empty() + && SLOTS[2].$field.is_empty() + { + "" + } else { + " " + }; + + $crate::r#macro::const_concat_if!( + !SLOTS[0].$field.is_empty() => [SLOTS[0].$field], + !SLOTS[1].$field.is_empty() => [SEP1, SLOTS[1].$field], + !SLOTS[2].$field.is_empty() => [SEP2, SLOTS[2].$field], + !SLOTS[3].$field.is_empty() => [SEP3, SLOTS[3].$field], + ) + }}; + (grouped_param, $slots:expr, $field:ident $(,)?) => {{ + const TYPES: &::core::primitive::str = + $crate::r#macro::wat_slots!(types, $slots, $field); + + $crate::r#macro::const_concat_if!( + !TYPES.is_empty() => [" (param ", TYPES, ")"], + ) + }}; + (params, $par:literal, $slots:expr, $field:ident $(,)?) => {{ const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; $crate::r#macro::const_concat_if!( @@ -129,4 +253,109 @@ macro_rules! wat_slot_params { !SLOTS[3].abi.is_empty() => [" (param $", $par, "_3 ", SLOTS[3].$field, ")"], ) }}; + (import_gets, $par:literal, $slots:expr $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => ["", " local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv], + !SLOTS[1].abi.is_empty() => ["\n", " local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv], + !SLOTS[2].abi.is_empty() => ["\n", " local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv], + !SLOTS[3].abi.is_empty() => ["\n", " local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv], + ) + }}; + (export_gets, $par:literal, $slots:expr $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => [" local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], + !SLOTS[1].abi.is_empty() => [" local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], + !SLOTS[2].abi.is_empty() => [" local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], + !SLOTS[3].abi.is_empty() => [" local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], + ) + }}; + (loads, $ty:ty, $slots:expr $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; + const OFFSET_0: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 0>() + ); + const OFFSET_1: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 1>() + ); + const OFFSET_2: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 2>() + ); + const OFFSET_3: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_slot_offset::<$ty, 3>() + ); + + $crate::r#macro::const_concat_if!( + !SLOTS[0].abi.is_empty() => [" local.get $retptr\n ", SLOTS[0].abi, ".load offset=", OFFSET_0, $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], + !SLOTS[1].abi.is_empty() => [" local.get $retptr\n ", SLOTS[1].abi, ".load offset=", OFFSET_1, $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], + !SLOTS[2].abi.is_empty() => [" local.get $retptr\n ", SLOTS[2].abi, ".load offset=", OFFSET_2, $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], + !SLOTS[3].abi.is_empty() => [" local.get $retptr\n ", SLOTS[3].abi, ".load offset=", OFFSET_3, $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], + ) + }}; +} + +/// Renders input fragments for JavaScript imports and Rust exports. +#[doc(hidden)] +#[macro_export] +macro_rules! wat_input { + (import types;) => { + "" + }; + (import types; $first:ty $(, $rest:ty)* $(,)?) => { + $crate::r#macro::const_concat!( + $crate::r#macro::wat_slots!( + types, + $crate::r#macro::into_js_wat_slots::<$first>(), + boundary, + ), + $( + " ", + $crate::r#macro::wat_slots!( + types, + $crate::r#macro::into_js_wat_slots::<$rest>(), + boundary, + ), + )* + ) + }; + (import params; $par:literal, $ty:ty $(,)?) => { + $crate::r#macro::wat_slots!( + params, + $par, + $crate::r#macro::into_js_wat_slots::<$ty>(), + abi, + ) + }; + (import gets; $par:literal, $ty:ty $(,)?) => { + $crate::r#macro::wat_slots!( + import_gets, + $par, + $crate::r#macro::into_js_wat_slots::<$ty>(), + ) + }; + (export raw_param; $ty:ty $(,)?) => { + $crate::r#macro::wat_slots!( + grouped_param, + $crate::r#macro::from_js_wat_slots::<$ty>(), + abi, + ) + }; + (export params; $par:literal, $ty:ty $(,)?) => { + $crate::r#macro::wat_slots!( + params, + $par, + $crate::r#macro::from_js_wat_slots::<$ty>(), + boundary, + ) + }; + (export gets; $par:literal, $ty:ty $(,)?) => { + $crate::r#macro::wat_slots!( + export_gets, + $par, + $crate::r#macro::from_js_wat_slots::<$ty>(), + ) + }; } diff --git a/client/js-sys/src/macro/wat_import.rs b/client/js-sys/src/macro/wat_import.rs index b6d76b36..f6e775b6 100644 --- a/client/js-sys/src/macro/wat_import.rs +++ b/client/js-sys/src/macro/wat_import.rs @@ -1,16 +1,16 @@ -/// Generates the complete WAT adapter for one JavaScript import. +/// Generates the complete WAT shim for one JavaScript import. #[doc(hidden)] #[macro_export] macro_rules! wat_import { ( module = $crate_name:expr, import = $import_name:expr, - adapter = $foreign_name:expr, + shim = $foreign_name:expr, inputs = [$(($par:literal, $input:ty)),* $(,)?], $(output = $output:ty,)? ) => {{ const INPUT_TYPES: &::core::primitive::str = - $crate::r#macro::wat_import_input_types!($($input),*); + $crate::r#macro::wat_input!(import types; $($input),*); const INPUT_PARAM: &::core::primitive::str = $crate::r#macro::const_concat_if!( !INPUT_TYPES.is_empty() => [" (param ", INPUT_TYPES, ")"], ); @@ -29,88 +29,58 @@ macro_rules! wat_import { ".import.", $import_name, "\"))", - $($crate::r#macro::wat_output_import_param!($output),)? + $($crate::r#macro::wat_import_output!(import_param, $output),)? INPUT_PARAM, - $($crate::r#macro::wat_import_result!($output),)? + $($crate::r#macro::wat_import_output!(import_result, $output),)? "))", - $crate::r#macro::wat_imports!(($($input),*) $(, $output)?), + $crate::r#macro::wat_imports!( + slots = [ + $($crate::r#macro::into_js_wat_slots::<$input>(),)* + ], + extras = [ + $($crate::r#macro::wat_output_imports::<$output>(),)? + $($crate::r#macro::wat_result_imports::<$output>(),)? + ], + ), "\n(func $", $foreign_name, " (@sym)", - $($crate::r#macro::wat_output_param!($output),)? - $($crate::r#macro::wat_input_params!($par, $input),)* - $($crate::r#macro::wat_output_result!($output),)? - $($crate::r#macro::wat_result_try!($output),)? - $($crate::r#macro::wat_output_get!($output),)? - $("\n", $crate::r#macro::wat_input_gets!($par, $input),)* + $($crate::r#macro::wat_import_output!(shim_param, $output),)? + $($crate::r#macro::wat_input!(import params; $par, $input),)* + $($crate::r#macro::wat_import_output!(shim_result, $output),)? + $crate::r#macro::wat_locals!( + slots = [ + $($crate::r#macro::into_js_wat_slots::<$input>(),)* + ], + extras = [ + $($crate::r#macro::wat_output_locals::<$output>(),)? + $($crate::r#macro::wat_result_locals::<$output>(),)? + ], + ), + $($crate::r#macro::wat_result_try::<$output>(),)? + $($crate::r#macro::wat_import_output!(shim_retptr, $output),)? + $( + "\n", + $crate::r#macro::wat_input!(import gets; $par, $input), + )* "\n call $", $crate_name, ".import.", $import_name, " (@reloc)", - $($crate::r#macro::wat_output!($output),)? - $($crate::r#macro::wat_result_catch!($output),)? - $($crate::r#macro::wat_result_default!($output),)? + $($crate::r#macro::wat_import_output!(shim_convert, $output),)? + $($crate::r#macro::wat_result_catch::<$output>(),)? + $($crate::r#macro::wat_result_default::<$output>(),)? "\n)" ) }}; } -// Imported function signature and conversion dependencies. - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_import_input_types { - () => { - "" - }; - ($first:ty $(, $rest:ty)* $(,)?) => { - $crate::r#macro::const_concat!( - $crate::r#macro::wat_input_import_types!($first), - $(" ", $crate::r#macro::wat_input_import_types!($rest),)* - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_imports { - (($($input:ty),*) $(, $output:ty)? $(,)?) => { - $crate::r#macro::wat_import_list!( - $($crate::r#macro::into_js_wat_slots::<$input>()[0].import,)* - $($crate::r#macro::into_js_wat_slots::<$input>()[1].import,)* - $($crate::r#macro::into_js_wat_slots::<$input>()[2].import,)* - $($crate::r#macro::into_js_wat_slots::<$input>()[3].import,)* - $($crate::r#macro::wat_output_import::<$output>(),)? - $($crate::r#macro::wat_result_imports::<$output>()[0],)? - $($crate::r#macro::wat_result_imports::<$output>()[1],)? - $($crate::r#macro::wat_result_imports::<$output>()[2],)? - ) - }; -} - -// Return adapter. - +/// Renders the direct or indirect output fragments of an import shim. #[doc(hidden)] #[macro_export] -macro_rules! wat_output { - ($ty:ty) => { - if !$crate::r#macro::return_from_js_is_direct::<$ty>() { - "" - } else if !$crate::r#macro::wat_output_conv::<$ty>().is_empty() { - const CONV: &::core::primitive::str = $crate::r#macro::wat_output_conv::<$ty>(); - - $crate::r#macro::const_concat!("\n ", CONV) - } else { - "" - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_output_import_param { - ($ty:ty) => { +macro_rules! wat_import_output { + (import_param, $ty:ty $(,)?) => { if $crate::r#macro::return_from_js_is_direct::<$ty>() { "" } else { @@ -121,56 +91,36 @@ macro_rules! wat_output_import_param { ) } }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_output_param { - ($ty:ty) => { + (import_result, $ty:ty $(,)?) => { if $crate::r#macro::return_from_js_is_direct::<$ty>() { - "" - } else { $crate::r#macro::const_concat!( - " (param $retptr ", - $crate::r#macro::wat_indirect_type::<$ty>(), + " (result ", + $crate::r#macro::wat_output_import_type::<$ty>(), ")" ) + } else { + "" } }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_import_result { - ($ty:ty) => { + (shim_param, $ty:ty $(,)?) => { if $crate::r#macro::return_from_js_is_direct::<$ty>() { + "" + } else { $crate::r#macro::const_concat!( - " (result ", - $crate::r#macro::wat_output_import_type::<$ty>(), + " (param $retptr ", + $crate::r#macro::wat_indirect_type::<$ty>(), ")" ) - } else { - "" } }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_output_result { - ($ty:ty) => { + (shim_result, $ty:ty $(,)?) => { if $crate::r#macro::return_from_js_is_direct::<$ty>() { $crate::r#macro::const_concat!(" (result ", $crate::r#macro::wat_direct::<$ty>(), ")") } else { "" } }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_output_get { - ($ty:ty) => {{ + (shim_retptr, $ty:ty $(,)?) => {{ if $crate::r#macro::return_from_js_is_direct::<$ty>() { "" } else { @@ -183,38 +133,15 @@ macro_rules! wat_output_get { ) } }}; -} - -// Input adapter. - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_input_import_types { - ($ty:ty $(,)?) => { - $crate::r#macro::wat_slot_types!($crate::r#macro::into_js_wat_slots::<$ty>(), boundary,) - }; -} + (shim_convert, $ty:ty $(,)?) => { + if !$crate::r#macro::return_from_js_is_direct::<$ty>() { + "" + } else if !$crate::r#macro::wat_output_conv::<$ty>().is_empty() { + const CONV: &::core::primitive::str = $crate::r#macro::wat_output_conv::<$ty>(); -#[doc(hidden)] -#[macro_export] -macro_rules! wat_input_params { - ($par:literal, $ty:ty $(,)?) => { - $crate::r#macro::wat_slot_params!($par, $crate::r#macro::into_js_wat_slots::<$ty>(), abi,) + $crate::r#macro::const_concat!("\n ", CONV) + } else { + "" + } }; } - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_input_gets { - ($par:literal, $ty:ty $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::into_js_wat_slots::<$ty>(); - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => ["", " local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv], - !SLOTS[1].abi.is_empty() => ["\n", " local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv], - !SLOTS[2].abi.is_empty() => ["\n", " local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv], - !SLOTS[3].abi.is_empty() => ["\n", " local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv], - ) - }}; -} diff --git a/client/js-sys/src/number/number.gen.rs b/client/js-sys/src/number/number.gen.rs index e09bce2d..56ff968f 100644 --- a/client/js-sys/src/number/number.gen.rs +++ b/client/js-sys/src/number/number.gen.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use crate::JsValue; -use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; +use crate::hazard::{IntoJS, JsCast}; #[repr(transparent)] pub struct JsNumber { @@ -33,11 +33,3 @@ unsafe impl IntoJS for JsNumber { IntoJS::into_abi(JsValue::from(self)) } } - -unsafe impl OptionIntoJS for JsNumber { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } -} diff --git a/client/js-sys/src/string/string.gen.rs b/client/js-sys/src/string/string.gen.rs index 28737d27..14938d3d 100644 --- a/client/js-sys/src/string/string.gen.rs +++ b/client/js-sys/src/string/string.gen.rs @@ -3,7 +3,7 @@ #![allow(warnings)] use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{IntoJS, JsCast, OptionIntoJS}; +use crate::hazard::{IntoJS, JsCast}; use crate::util::{PtrConst, PtrLength, PtrMut}; #[derive(Clone, Debug)] @@ -32,18 +32,10 @@ unsafe impl IntoJS for JsString { } } -unsafe impl OptionIntoJS for JsString { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } -} - pub(super) fn string_constructor(value: &JsValue) -> JsString { js_bindgen::unsafe_global_wat! { "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_constructor", - adapter = "js_sys.string_constructor", inputs = [("arg0", & JsValue)], output = JsString,), + shim = "js_sys.string_constructor", inputs = [("arg0", & JsValue)], output = JsString,), } js_bindgen::import_js! { @@ -79,7 +71,7 @@ pub(super) fn string_constructor(value: &JsValue) -> JsString { pub(super) unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrLength) -> bool { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_eq", adapter = + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_eq", shim = "js_sys.string_eq", inputs = [("arg0", & JsString), ("arg1", PtrConst < u8 >), ("arg2", PtrLength < u8 >)], output = bool,), } @@ -146,8 +138,8 @@ pub(super) unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrL pub(super) unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_decode", adapter - = "js_sys.string_decode", inputs = [("arg0", PtrConst < u8 >), ("arg1", PtrLength < u8 >)], + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_decode", shim = + "js_sys.string_decode", inputs = [("arg0", PtrConst < u8 >), ("arg1", PtrLength < u8 >)], output = JsString,), } @@ -193,7 +185,7 @@ pub(super) unsafe fn string_decode(array: PtrConst, len: PtrLength) -> J pub(super) fn string_utf8_length(string: &JsString) -> f64 { js_bindgen::unsafe_global_wat! { "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_utf8_length", - adapter = "js_sys.string_utf8_length", inputs = [("arg0", & JsString)], output = f64,), + shim = "js_sys.string_utf8_length", inputs = [("arg0", & JsString)], output = f64,), } js_bindgen::import_js! { @@ -231,8 +223,8 @@ pub(super) fn string_utf8_length(string: &JsString) -> f64 { pub(super) unsafe fn string_encode(string: &JsString, array: PtrMut, len: PtrLength) { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_encode", adapter - = "js_sys.string_encode", inputs = [("arg0", & JsString), ("arg1", PtrMut < u8 >), ("arg2", + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_encode", shim = + "js_sys.string_encode", inputs = [("arg0", & JsString), ("arg1", PtrMut < u8 >), ("arg2", PtrLength < u8 >)],), } diff --git a/client/js-sys/src/util.rs b/client/js-sys/src/util.rs index 1c0d4af7..1fedffd2 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -5,12 +5,14 @@ use crate::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; macro_rules! thread_local { ($($vis:vis static $name:ident: $ty:ty = $value:expr;)*) => { - #[cfg_attr(target_feature = "atomics", thread_local)] - $($vis static $name: $crate::util::LocalKey<$ty> = $crate::util::LocalKey::new($value);)* + $( + #[cfg_attr(target_feature = "atomics", thread_local)] + $vis static $name: $crate::util::LocalKey<$ty> = $crate::util::LocalKey::new($value); + )* }; } -pub(crate) struct LocalKey(T); +pub(crate) struct LocalKey(pub(crate) T); // SAFETY: Multi-threading is not possible without `atomics`. #[cfg(not(target_feature = "atomics"))] @@ -88,7 +90,8 @@ const PTR_INTO_JS_WAT_CONV: Option = None; #[cfg(target_arch = "wasm64")] const PTR_INTO_JS_WAT_CONV: Option = Some(WatConv { - import: None, + imports: None, + locals: None, conv: "f64.convert_i64_u", r#type: "f64", }); @@ -107,7 +110,7 @@ impl PtrConst { } // SAFETY: `PtrConst` is transparent over a native Wasm pointer. On `wasm64`, -// the WAT adapter converts it to `f64` without losing precision. +// the WAT shim converts it to `f64` without losing precision. unsafe impl Slot for PtrConst { const WAT_TYPE: &'static str = WAT_PTR_TYPE; const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; @@ -148,7 +151,7 @@ impl PtrMut { } // SAFETY: `PtrMut` is transparent over a native Wasm pointer. On `wasm64`, -// the WAT adapter converts it to `f64` without losing precision. +// the WAT shim converts it to `f64` without losing precision. unsafe impl Slot for PtrMut { const WAT_TYPE: &'static str = WAT_PTR_TYPE; const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; @@ -193,7 +196,7 @@ impl PtrLength { } // SAFETY: `PtrLength` is transparent over `usize`. On `wasm64`, the WAT -// adapter converts it to `f64` without losing precision. +// shim converts it to `f64` without losing precision. unsafe impl Slot for PtrLength { const WAT_TYPE: &'static str = WAT_PTR_TYPE; const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; diff --git a/client/js-sys/src/value/mod.rs b/client/js-sys/src/value/mod.rs index cefb07b9..f83a2045 100644 --- a/client/js-sys/src/value/mod.rs +++ b/client/js-sys/src/value/mod.rs @@ -6,9 +6,13 @@ use core::marker::PhantomData; use core::mem::{ManuallyDrop, MaybeUninit}; use core::slice; -use crate::externref::EXTERNREF_TABLE; +use crate::externref::{ + WAT_GET_CONV, WAT_INDEX_LOCAL, WAT_INSERT_CONV, WAT_INSERT_IMPORTS, WAT_INSERT_LOCALS, + WAT_OPTIONAL_INSERT_CONV, WAT_TABLE_IMPORT, WAT_TAKE_CONV, WAT_TAKE_IMPORTS, release, +}; use crate::hazard::{ - FromJS, FromJsConv, IntoJS, JsCast, OptionIntoJS, ReturnAbi, ReturnMode, Slot, WatConv, + FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, OptionFromAbi, OptionIntoAbi, ReturnAbi, + ReturnMode, Slot, WatConv, }; #[derive(Debug)] @@ -44,19 +48,15 @@ impl Default for JsValueAbi { unsafe impl Slot for JsValueAbi { const WAT_TYPE: &'static str = "i32"; const INTO_JS_WAT_CONV: Option = Some(WatConv { - import: Some( - "(import \"env\" \"js_sys.externref.take\" (func $js_sys.externref.take (@sym) (param \ - i32) (result externref)))", - ), - conv: "call $js_sys.externref.take (@reloc)", + imports: Some(WAT_TAKE_IMPORTS), + locals: Some(WAT_INDEX_LOCAL), + conv: WAT_TAKE_CONV, r#type: "externref", }); const FROM_JS_WAT_CONV: Option = Some(WatConv { - import: Some( - "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) \ - (param externref) (result i32)))", - ), - conv: "call $js_sys.externref.insert (@reloc)", + imports: Some(WAT_INSERT_IMPORTS), + locals: Some(WAT_INSERT_LOCALS), + conv: WAT_INSERT_CONV, r#type: "externref", }); } @@ -71,11 +71,9 @@ unsafe impl ReturnAbi for JsValueAbi { unsafe impl Slot for JsValueRefAbi { const WAT_TYPE: &'static str = "i32"; const INTO_JS_WAT_CONV: Option = Some(WatConv { - import: Some( - "(import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param \ - i32) (result externref)))", - ), - conv: "call $js_sys.externref.get (@reloc)", + imports: Some(WAT_TABLE_IMPORT), + locals: None, + conv: WAT_GET_CONV, r#type: "externref", }); } @@ -86,19 +84,15 @@ unsafe impl Slot for JsValueRefAbi { unsafe impl Slot for OptionalJsValueAbi { const WAT_TYPE: &'static str = "i32"; const INTO_JS_WAT_CONV: Option = Some(WatConv { - import: Some( - "(import \"env\" \"js_sys.externref.take\" (func $js_sys.externref.take (@sym) (param \ - i32) (result externref)))", - ), - conv: "call $js_sys.externref.take (@reloc)", + imports: Some(WAT_TAKE_IMPORTS), + locals: Some(WAT_INDEX_LOCAL), + conv: WAT_TAKE_CONV, r#type: "externref", }); const FROM_JS_WAT_CONV: Option = Some(WatConv { - import: Some( - "(import \"env\" \"js_sys.optional.js_value\" (func $js_sys.optional.js_value (@sym) \ - (param externref) (result i32)))", - ), - conv: "call $js_sys.optional.js_value (@reloc)", + imports: Some(WAT_INSERT_IMPORTS), + locals: Some(WAT_INSERT_LOCALS), + conv: WAT_OPTIONAL_INSERT_CONV, r#type: "externref", }); } @@ -151,36 +145,38 @@ impl JsValue { } impl Clone for JsValue { + #[inline] fn clone(&self) -> Self { js_bindgen::unsafe_global_wat!( - "(import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param \ - i32) (result externref)))", - "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) \ - (param externref) (result i32)))", + "(import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym \ + (name \"js_sys.externref.table\")) 2 externref))", + "(import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) \ + (result i32)))", "(func $js_sys.js_value.clone (@sym) (param $index i32) (result i32)", + " (local $new_index i32)", + " call $js_sys.externref.next (@reloc)", + " local.tee $new_index", " local.get $index", - " call $js_sys.externref.get (@reloc)", - " call $js_sys.externref.insert (@reloc)", + " table.get $js_sys.import.externref.table (@reloc)", + " table.set $js_sys.import.externref.table (@reloc)", + " local.get $new_index", ")", ); unsafe extern "C" { #[link_name = "js_sys.js_value.clone"] - safe fn clone(size: i32) -> i32; + safe fn clone(index: i32) -> i32; } - if self.index > 1 { - Self::new(clone(self.index)) - } else { - Self::new(self.index) - } + Self::new(clone(self.index)) } } impl Drop for JsValue { + #[inline] fn drop(&mut self) { if self.index > 1 { - EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().remove(self.index)); + release(self.index); } } } @@ -199,7 +195,7 @@ unsafe impl IntoJS for &T { unsafe impl JsCast for JsValue {} // SAFETY: The owned table index is transferred to JavaScript and recycled -// after the WAT adapter has loaded its `externref`. +// after the WAT shim has loaded its `externref`. unsafe impl IntoJS for JsValue { type Abi = JsValueAbi; @@ -219,61 +215,59 @@ unsafe impl FromJS for T { } } -// SAFETY: `None` uses the reserved undefined index, which no borrowed -// `JsValue` can produce. -unsafe impl OptionIntoJS for &T { - type OptionAbi = JsValueRefAbi; +// SAFETY: `None` uses the reserved undefined index, while `Some` preserves the +// borrowed table index produced by the underlying conversion. +unsafe impl OptionIntoAbi for JsValueRefAbi +where + T: IntoJS, +{ + const JS_CONV: Option = T::JS_CONV; + + type Abi = Self; - fn option_into_abi(value: Option) -> Self::OptionAbi { - value.map_or(JsValueRefAbi(JsValue::UNDEFINED.index), |value| { - IntoJS::into_abi(value.unchecked_as_ref()) + fn into_option_abi(value: Option) -> Self::Abi { + value.map_or(Self(JsValue::UNDEFINED.index), |value| { + IntoJS::into_abi(value) }) } } -// SAFETY: `None` becomes index zero. A present value transfers its owned table -// index to JavaScript. -unsafe impl OptionIntoJS for JsValue { - type OptionAbi = OptionalJsValueAbi; +// SAFETY: `None` becomes the reserved undefined index. A present value +// transfers the owned table index produced by the underlying conversion. +unsafe impl OptionIntoAbi for JsValueAbi +where + T: IntoJS, +{ + const JS_CONV: Option = T::JS_CONV; - fn option_into_abi(value: Option) -> Self::OptionAbi { + type Abi = OptionalJsValueAbi; + + fn into_option_abi(value: Option) -> Self::Abi { match value { - None => OptionalJsValueAbi(Self::UNDEFINED.index), + None => OptionalJsValueAbi(JsValue::UNDEFINED.index), Some(value) => { - let JsValueAbi(index) = IntoJS::into_abi(value); + let Self(index) = IntoJS::into_abi(value); OptionalJsValueAbi(index) } } } } -// SAFETY: Null or undefined JS values become index zero; all other `externref` -// values are inserted into the `externref` table and reconstructed as `T`. -unsafe impl FromJS for Option { +// SAFETY: Null or undefined JS values use the reserved undefined index; all +// other values are decoded by the underlying owned table-index conversion. +unsafe impl OptionFromAbi for JsValueAbi +where + T: FromJS, +{ const JS_CONV: Option = Some(FromJsConv::slot1("($value) ?? null")); type Abi = OptionalJsValueAbi; - fn from_abi(raw: Self::Abi) -> Self { - (raw.0 != JsValue::UNDEFINED.index).then(|| T::unchecked_from(JsValue::new(raw.0))) + fn from_option_abi(raw: Self::Abi) -> Option { + (raw.0 != JsValue::UNDEFINED.index).then(|| T::from_abi(Self(raw.0))) } } -js_bindgen::unsafe_global_wat!( - "(import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) (param \ - externref) (result i32)))", - "(func $js_sys.optional.js_value (@sym) (param $value externref) (result i32)", - " local.get $value", - " ref.is_null", - " if (result i32)", - " i32.const 0", - " else", - " local.get $value", - " call $js_sys.externref.insert (@reloc)", - " end", - ")", -); - impl PartialEq for JsValue { fn eq(&self, other: &Self) -> bool { js_bindgen::embed_js!( diff --git a/client/js-sys/src/value/value.gen.rs b/client/js-sys/src/value/value.gen.rs index 58d7eb84..4315962e 100644 --- a/client/js-sys/src/value/value.gen.rs +++ b/client/js-sys/src/value/value.gen.rs @@ -9,7 +9,7 @@ use crate::util::PtrLength; pub(super) fn js_value_partial_eq(value1: &JsValue, value2: &JsValue) -> bool { js_bindgen::unsafe_global_wat! { "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "js_value_partial_eq", - adapter = "js_sys.js_value_partial_eq", inputs = [("arg0", & JsValue), ("arg1", & JsValue)], + shim = "js_sys.js_value_partial_eq", inputs = [("arg0", & JsValue), ("arg1", & JsValue)], output = bool,), } diff --git a/client/js-sys/tests/array.rs b/client/js-sys/tests/array.rs index 6263ba28..897c8bc3 100644 --- a/client/js-sys/tests/array.rs +++ b/client/js-sys/tests/array.rs @@ -20,6 +20,9 @@ fn js_value() { let ffi_array = js(&rust_array); assert_eq!(rust_array.len(), ffi_array.length().try_into().unwrap()); + let mut wrong_length = [JsValue::UNDEFINED; 41]; + assert!(js_array.to_slice(&mut wrong_length).is_err()); + let returned_array: [JsValue; 42] = js_array.to_array().unwrap(); assert_eq!(rust_array, returned_array); diff --git a/client/js-sys/tests/hazard.rs b/client/js-sys/tests/hazard.rs index b9c3a2b2..1982fefb 100644 --- a/client/js-sys/tests/hazard.rs +++ b/client/js-sys/tests/hazard.rs @@ -1,5 +1,5 @@ use js_bindgen_test::test; -use js_sys::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; +use js_sys::hazard::{EmptySlot, FromJS, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; use js_sys::js_sys; js_bindgen::embed_js!( @@ -21,7 +21,8 @@ struct NumberSlot(u32); unsafe impl Slot for NumberSlot { const WAT_TYPE: &'static str = "i32"; const INTO_JS_WAT_CONV: Option = Some(WatConv { - import: None, + imports: None, + locals: None, conv: "f64.convert_i32_u", r#type: "f64", }); @@ -31,6 +32,23 @@ struct Pair(u32, u32); struct Quad(u32, u32, u32, u32); +#[derive(Clone, Copy)] +struct ExportInput(u32); + +// SAFETY: `ExportInput` is reconstructed directly from one `NumberSlot`. +unsafe impl FromJS for ExportInput { + type Abi = NumberSlot; + + fn from_abi(raw: Self::Abi) -> Self { + Self(raw.0) + } +} + +#[js_sys] +fn from_js_only(value: ExportInput) -> u32 { + value.0 +} + // SAFETY: `Pair` is represented by its two `u32` fields in order. unsafe impl WasmAbi for Pair { type Slot1 = NumberSlot; diff --git a/client/js-sys/tests/optional.rs b/client/js-sys/tests/optional.rs index 233ea0b1..d0c2058d 100644 --- a/client/js-sys/tests/optional.rs +++ b/client/js-sys/tests/optional.rs @@ -91,6 +91,18 @@ fn numeric() { assert_eq!(i128_option(None), None); } +#[test] +fn unit() { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "test")] + fn unit_option(value: Option<()>) -> Option<()>; + } + + assert_eq!(unit_option(None), None); + assert_eq!(unit_option(Some(())), Some(())); +} + #[test] fn js_value() { #[js_sys] diff --git a/client/js-sys/tests/value.rs b/client/js-sys/tests/value.rs index 751a10c8..2a104404 100644 --- a/client/js-sys/tests/value.rs +++ b/client/js-sys/tests/value.rs @@ -3,7 +3,8 @@ use js_sys::{JsString, JsValue}; #[test] fn undefined() { - let string = JsString::new(&JsValue::UNDEFINED); + let value = JsValue::UNDEFINED.clone(); + let string = JsString::new(&value); let string = String::from(&string); assert_eq!(string, "undefined"); @@ -11,7 +12,8 @@ fn undefined() { #[test] fn null() { - let string = JsString::new(&JsValue::NULL); + let value = JsValue::NULL.clone(); + let string = JsString::new(&value); let string = String::from(&string); assert_eq!(string, "null"); @@ -23,3 +25,10 @@ fn clone() { let value = value.clone(); assert_eq!(value, "Hello, World!"); } + +#[test] +fn many_live_values() { + let value = JsString::from("Hello, World!"); + let values: Vec<_> = (0..512).map(|_| value.clone()).collect(); + assert_eq!(values.len(), 512); +} diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs index 95122f63..88caf3ee 100644 --- a/client/web-sys/src/console.gen.rs +++ b/client/web-sys/src/console.gen.rs @@ -8,8 +8,8 @@ use js_sys::hazard::JsCast; pub fn log0() { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log0", adapter - = "web_sys.console.log0", inputs = [],), + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log0", shim = + "web_sys.console.log0", inputs = [],), } js_bindgen::import_js! { @@ -32,7 +32,7 @@ pub fn log0() { pub fn log(data: &[T]) { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log", adapter = + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log", shim = "web_sys.console.log", inputs = [("arg0", & [JsValue])],), } @@ -67,8 +67,8 @@ pub fn log(data: &[T]) { pub fn log2(data1: &JsValue, data2: &JsValue) { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log2", adapter - = "web_sys.console.log2", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log2", shim = + "web_sys.console.log2", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), } js_bindgen::import_js! { @@ -106,8 +106,8 @@ pub fn log2(data1: &JsValue, data2: &JsValue) { pub fn error(data: &JsValue) { js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.error", adapter - = "web_sys.console.error", inputs = [("arg0", & JsValue)],), + "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.error", shim = + "web_sys.console.error", inputs = [("arg0", & JsValue)],), } js_bindgen::import_js! { diff --git a/host/cli-lib/src/js/imports.mjs b/host/cli-lib/src/js/imports.mjs index f764a718..cf036a5f 100644 --- a/host/cli-lib/src/js/imports.mjs +++ b/host/cli-lib/src/js/imports.mjs @@ -58,8 +58,10 @@ export class JsBindgen { return WebAssembly.instantiate(this.#module, this.#importObject).then(instance => { this.#instance = instance; this.#finished = true; + // Export wrappers generated by `js-sys` use this stable binding. + const wasmExports = instance.exports; const jsExports = JBG_PLACEHOLDER_JS_EXPORT; - const exports = Object.assign(Object.create(null), instance.exports, jsExports); + const exports = Object.assign(Object.create(null), wasmExports, jsExports); return { instance, exports, diff --git a/host/cli-lib/src/js/imports.mts b/host/cli-lib/src/js/imports.mts index 89e22f22..d33b07e9 100644 --- a/host/cli-lib/src/js/imports.mts +++ b/host/cli-lib/src/js/imports.mts @@ -79,10 +79,12 @@ export class JsBindgen { this.#instance = instance this.#finished = true + // Export wrappers generated by `js-sys` use this stable binding. + const wasmExports = instance.exports const jsExports = JBG_PLACEHOLDER_JS_EXPORT const exports = Object.assign( Object.create(null) as WebAssembly.Instance["exports"], - instance.exports, + wasmExports, jsExports ) return { diff --git a/host/cli/src/main.rs b/host/cli/src/main.rs index df468c10..3aa69825 100644 --- a/host/cli/src/main.rs +++ b/host/cli/src/main.rs @@ -53,9 +53,6 @@ impl Cli { fs::write(&js_path, output.js) .with_context(|| format!("failed to write JS file: {}", js_path.display()))?; - println!("{}", wasm_path.display()); - println!("{}", js_path.display()); - Ok(()) } } diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 265e2803..53d09aaa 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -85,7 +85,7 @@ pub(crate) fn r#macro( for slot in 1_usize..=4 { let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = input.span()); - let slot_alias = format_ident!("OutputSlot{slot}", span = input.span()); + let slot_alias = format_ident!("FromJsSlot{slot}", span = input.span()); raw_inputs.push(quote_spanned! {input.span()=> #slot_ident: #r#macro::#slot_alias<#js_ty> @@ -108,7 +108,7 @@ pub(crate) fn r#macro( } codegen_inputs.push(quote_spanned! {input.span()=> (#parameter, #js_ty) }); - required_embeds.push(quote_spanned!(input.span()=> #r#macro::js_output_embed::<#js_ty>())); + required_embeds.push(quote_spanned!(input.span()=> #r#macro::js_from_embed::<#js_ty>())); arguments.push(argument); } diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index 103e6176..bf426377 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -479,7 +479,7 @@ impl<'a> State<'a> { interpolate #r#macro::wat_import!( module = #crate_, import = #import_name, - adapter = #foreign_name, + shim = #foreign_name, inputs = [#(#inputs),*], #(output = #output,)* ), diff --git a/host/js-sys-bindgen/src/hygiene.rs b/host/js-sys-bindgen/src/hygiene.rs index dbd05e32..0b3d27cd 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -56,18 +56,6 @@ impl Hygiene<'_> { } } - pub(crate) fn js_option_into(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> OptionIntoJS)); - parse_quote_spanned!(span=> OptionIntoJS) - } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::OptionIntoJS), span) - } - } - } - pub(crate) fn r#macro(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { Hygiene::Imports(imports) => { diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs index f7844da5..d781a374 100644 --- a/host/js-sys-bindgen/src/tests/macro/export.rs +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -47,7 +47,7 @@ fn direct() { assert_eq!( js, r"(arg0) => { - const ret = instance.exports['echo'](arg0) + const ret = wasmExports['echo'](arg0) return ret >>> 0 }" ); @@ -63,19 +63,28 @@ fn wat_slot_conversions() { inline_snap::inline_snap!( wat, - r#" -(import "env" "js_sys.externref.insert" (func $js_sys.externref.insert (@sym) (param externref) (result i32))) -(import "env" "raw" (func $raw (@sym (name "__export_drop_value")) (param i32))) -(func $export (@sym (name "drop_value")) (param $arg0_0 externref) - local.get $arg0_0 - call $js_sys.externref.insert (@reloc) - call $raw (@reloc) -)"# + " + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) + (import \"env\" \"raw\" (func $raw (@sym (name \"__export_drop_value\")) (param i32))) + (func $export (@sym (name \"drop_value\")) (param $arg0_0 externref) + (local $js_sys.externref.value externref) + (local $js_sys.externref.index i32) + local.get $arg0_0 + local.set $js_sys.externref.value + call $js_sys.externref.next (@reloc) + local.tee $js_sys.externref.index + local.get $js_sys.externref.value + table.set $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + call $raw (@reloc) + )" ); assert_eq!( js, r"(arg0) => { - instance.exports['drop_value'](arg0) + wasmExports['drop_value'](arg0) }" ); @@ -87,18 +96,29 @@ fn wat_slot_conversions() { inline_snap::inline_snap!( wat, - r#" -(import "env" "js_sys.externref.take" (func $js_sys.externref.take (@sym) (param i32) (result externref))) -(import "env" "raw" (func $raw (@sym (name "__export_undefined")) (result i32))) -(func $export (@sym (name "undefined")) (result externref) - call $raw (@reloc) - call $js_sys.externref.take (@reloc) -)"# + " + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) + (import \"env\" \"raw\" (func $raw (@sym (name \"__export_undefined\")) (result i32))) + (func $export (@sym (name \"undefined\")) (result externref) + (local $js_sys.externref.index i32) + call $raw (@reloc) + local.tee $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end + )" ); assert_eq!( js, r"() => { - const ret = instance.exports['undefined']() + const ret = wasmExports['undefined']() return ret }" ); @@ -142,7 +162,7 @@ fn indirect_and_multiple_parameters() { assert_eq!( js, r"(arg0, arg1) => { - const ret = instance.exports['add'](arg0, arg1, arg1 >> 64n) + const ret = wasmExports['add'](arg0, arg1, arg1 >> 64n) return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1]) }" ); @@ -158,44 +178,197 @@ fn result() { inline_snap::inline_snap!( wat, - r#" -(import "env" "js_sys.externref.take" (func $js_sys.externref.take (@sym) (param i32) (result externref))) -(import "env" "raw" (func $raw (@sym (name "__export_checked_add")) (param i32) (param i64 i64) (param i64 i64))) -(import "env" "__stack_pointer" (global $__stack_pointer (mut i32))) -(func $export (@sym (name "checked_add")) (param $arg0_0 i64) (param $arg0_1 i64) (param $arg1_0 i64) (param $arg1_1 i64) (result externref i32 i64 i64) - (local $retptr i32) - global.get $__stack_pointer - i32.const 32 - i32.sub - local.tee $retptr - global.set $__stack_pointer - local.get $retptr - local.get $arg0_0 - local.get $arg0_1 - local.get $arg1_0 - local.get $arg1_1 - call $raw (@reloc) - local.get $retptr - i32.load offset=0 - call $js_sys.externref.take (@reloc) - local.get $retptr - i32.load offset=4 - local.get $retptr - i64.load offset=8 - local.get $retptr - i64.load offset=16 - local.get $retptr - i32.const 32 - i32.add - global.set $__stack_pointer -)"# + " + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) + (import \"env\" \"raw\" (func $raw (@sym (name \"__export_checked_add\")) (param i32) (param i64 \ + i64) (param i64 i64))) + (import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut i32))) + (func $export (@sym (name \"checked_add\")) (param $arg0_0 i64) (param $arg0_1 i64) (param \ + $arg1_0 i64) (param $arg1_1 i64) (result i64 i64 i32 externref) + (local $retptr i32) + (local $js_sys.result.discriminant i32) + (local $js_sys.externref.index i32) + global.get $__stack_pointer + i32.const 32 + i32.sub + local.tee $retptr + global.set $__stack_pointer + local.get $retptr + local.get $arg0_0 + local.get $arg0_1 + local.get $arg1_0 + local.get $arg1_1 + call $raw (@reloc) + local.get $retptr + i64.load offset=0 + local.get $retptr + i64.load offset=8 + local.get $retptr + i32.load offset=16 + local.tee $js_sys.result.discriminant + local.get $retptr + i32.load offset=20 + local.set $js_sys.externref.index + local.get $js_sys.result.discriminant + if (result externref) + local.get $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end + else + ref.null extern + end + local.get $retptr + i32.const 32 + i32.add + global.set $__stack_pointer + )" + ); + assert_eq!( + js, + r"(arg0, arg1) => { + const ret = wasmExports['checked_add'](arg0, arg0 >> 64n, arg1, arg1 >> 64n) + if (ret[2] !== 0) throw ret[3] + return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1]) +}" + ); +} + +#[test] +fn single_slot_result() { + let (wat, js) = expand("e! { + pub fn checked_add(value: i32, delta: i32) -> Result { + value.checked_add(delta).ok_or(JsValue::UNDEFINED) + } + }); + + inline_snap::inline_snap!( + wat, + " + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) + (import \"env\" \"raw\" (func $raw (@sym (name \"__export_checked_add\")) (param i32) (param \ + i32) (param i32))) + (import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut i32))) + (func $export (@sym (name \"checked_add\")) (param $arg0_0 i32) (param $arg1_0 i32) (result i32 \ + i32 externref) + (local $retptr i32) + (local $js_sys.result.discriminant i32) + (local $js_sys.externref.index i32) + global.get $__stack_pointer + i32.const 16 + i32.sub + local.tee $retptr + global.set $__stack_pointer + local.get $retptr + local.get $arg0_0 + local.get $arg1_0 + call $raw (@reloc) + local.get $retptr + i32.load offset=0 + local.get $retptr + i32.load offset=4 + local.tee $js_sys.result.discriminant + local.get $retptr + i32.load offset=8 + local.set $js_sys.externref.index + local.get $js_sys.result.discriminant + if (result externref) + local.get $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end + else + ref.null extern + end + local.get $retptr + i32.const 16 + i32.add + global.set $__stack_pointer + )" ); assert_eq!( js, r"(arg0, arg1) => { - const ret = instance.exports['checked_add'](arg0, arg0 >> 64n, arg1, arg1 >> 64n) - if (ret[1] !== 0) throw ret[0] - return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[2], ret[3]) + const ret = wasmExports['checked_add'](arg0, arg1) + if (ret[1] !== 0) throw ret[2] + return ret[0] +}" + ); +} + +#[test] +fn unit_result() { + let (wat, js) = expand("e! { + pub fn succeeds() -> Result<(), JsValue> { + Ok(()) + } + }); + + inline_snap::inline_snap!( + wat, + " + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) + (import \"env\" \"raw\" (func $raw (@sym (name \"__export_succeeds\")) (param i32))) + (import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut i32))) + (func $export (@sym (name \"succeeds\")) (result i32 externref) + (local $retptr i32) + (local $js_sys.result.discriminant i32) + (local $js_sys.externref.index i32) + global.get $__stack_pointer + i32.const 16 + i32.sub + local.tee $retptr + global.set $__stack_pointer + local.get $retptr + call $raw (@reloc) + local.get $retptr + i32.load offset=0 + local.tee $js_sys.result.discriminant + local.get $retptr + i32.load offset=4 + local.set $js_sys.externref.index + local.get $js_sys.result.discriminant + if (result externref) + local.get $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end + else + ref.null extern + end + local.get $retptr + i32.const 16 + i32.add + global.set $__stack_pointer + )" + ); + assert_eq!( + js, + r"() => { + const ret = wasmExports['succeeds']() + if (ret[0] !== 0) throw ret[1] + return undefined }" ); } @@ -219,7 +392,7 @@ fn no_parameters() { assert_eq!( js, r"() => { - const ret = instance.exports['answer']() + const ret = wasmExports['answer']() return ret >>> 0 }" ); @@ -245,7 +418,7 @@ fn no_return_value() { assert_eq!( js, r"(arg0) => { - instance.exports['nothing'](arg0) + wasmExports['nothing'](arg0) }" ); } diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index 96b79d59..0b30ec22 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -11,7 +11,7 @@ fn basic() { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), + shim = "test_crate.log", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -44,11 +44,11 @@ fn basic() { }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.log (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.log (@reloc) )", "globalThis.log", @@ -68,7 +68,7 @@ fn namespace() { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = - "console.log", adapter = "test_crate.console.log", inputs = [("arg0", & JsValue)],), + "console.log", shim = "test_crate.console.log", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -101,11 +101,11 @@ fn namespace() { }, "(import \"test_crate\" \"console.log\" (func $test_crate.import.console.log (@sym (name \ \"test_crate.import.console.log\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.console.log (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.console.log (@reloc) )", "globalThis.console.log", @@ -125,7 +125,7 @@ fn js_sys() { pub fn log(data: &JsValue) { js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), + shim = "test_crate.log", inputs = [("arg0", & JsValue)],), } js_sys::js_bindgen::import_js! { @@ -158,11 +158,11 @@ fn js_sys() { }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.log (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.log (@reloc) )", "globalThis.log", @@ -182,7 +182,7 @@ fn two_parameters() { pub fn log(data1: &JsValue, data2: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), + shim = "test_crate.log", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -225,13 +225,13 @@ fn two_parameters() { }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.log (@sym) (param $arg0_0 i32) (param $arg1_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) local.get $arg1_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.log (@reloc) )", "globalThis.log", @@ -251,7 +251,7 @@ fn empty() { pub fn log() { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [],), + shim = "test_crate.log", inputs = [],), } ::js_sys::js_bindgen::import_js! { @@ -297,7 +297,7 @@ fn js_name() { pub fn logx(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "logx", - adapter = "test_crate.logx", inputs = [("arg0", & JsValue)],), + shim = "test_crate.logx", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -330,11 +330,11 @@ fn js_name() { }, "(import \"test_crate\" \"logx\" (func $test_crate.import.logx (@sym (name \ \"test_crate.import.logx\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.logx (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.logx (@reloc) )", "globalThis.log", @@ -355,7 +355,7 @@ fn js_import() { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), + shim = "test_crate.log", inputs = [("arg0", & JsValue)],), } unsafe extern "C" { @@ -377,11 +377,11 @@ fn js_import() { }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.log (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.log (@reloc) )", None, @@ -402,7 +402,7 @@ fn js_embed() { pub fn log(data: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [("arg0", & JsValue)],), + shim = "test_crate.log", inputs = [("arg0", & JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -438,11 +438,11 @@ fn js_embed() { }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.log (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.log (@reloc) )", "this.#jsEmbed.test_crate['embed']", @@ -462,7 +462,7 @@ fn r#return() { pub fn is_nan() -> JsValue { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "is_nan", - adapter = "test_crate.is_nan", inputs = [], output = JsValue,), + shim = "test_crate.is_nan", inputs = [], output = JsValue,), } ::js_sys::js_bindgen::import_js! { @@ -489,11 +489,19 @@ fn r#return() { }, "(import \"test_crate\" \"is_nan\" (func $test_crate.import.is_nan (@sym (name \ \"test_crate.import.is_nan\")) (result externref))) - (import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) (param \ - externref) (result i32))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) (func $test_crate.is_nan (@sym) (result i32) + (local $js_sys.externref.value externref) + (local $js_sys.externref.index i32) call $test_crate.import.is_nan (@reloc) - call $js_sys.externref.insert (@reloc) + local.set $js_sys.externref.value + call $js_sys.externref.next (@reloc) + local.tee $js_sys.externref.index + local.get $js_sys.externref.value + table.set $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index )", "globalThis.is_nan", ); @@ -514,7 +522,7 @@ fn cfg() { pub fn log() { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - adapter = "test_crate.log", inputs = [],), + shim = "test_crate.log", inputs = [],), } ::js_sys::js_bindgen::import_js! { diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index 5e64cbba..da7a7fff 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -12,7 +12,7 @@ fn method() { pub fn test(self: &JsTest) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)],), + shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)],), } ::js_sys::js_bindgen::import_js! { @@ -48,11 +48,11 @@ fn method() { }, "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ \"test_crate.import.test\")) (param externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.test (@sym) (param $arg0_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.test (@reloc) )", "(arg0_0) => arg0_0.test()", @@ -73,7 +73,7 @@ fn method_par() { pub fn test(self: &JsTest, par1: &JsValue, par2: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & + shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & JsValue), ("arg2", & JsValue)],), } @@ -131,15 +131,15 @@ fn method_par() { }, "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ \"test_crate.import.test\")) (param externref externref externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) (param $arg2_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) local.get $arg1_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) local.get $arg2_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.test (@reloc) )", "(arg0_0, arg1_0, arg2_0) => arg0_0.test(arg1_0, arg2_0)", @@ -161,7 +161,7 @@ fn getter() { pub fn test(self: &JsTest) -> JsValue { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)], output = + shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)], output = JsValue,), } @@ -202,15 +202,21 @@ fn getter() { }, "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ \"test_crate.import.test\")) (param externref) (result externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) - (import \"env\" \"js_sys.externref.insert\" (func $js_sys.externref.insert (@sym) (param \ - externref) (result i32))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) (func $test_crate.test (@sym) (param $arg0_0 i32) (result i32) + (local $js_sys.externref.value externref) + (local $js_sys.externref.index i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.test (@reloc) - call $js_sys.externref.insert (@reloc) + local.set $js_sys.externref.value + call $js_sys.externref.next (@reloc) + local.tee $js_sys.externref.index + local.get $js_sys.externref.value + table.set $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index )", "(arg0_0) => arg0_0.test", ); @@ -231,7 +237,7 @@ fn setter() { pub fn test(self: &JsTest, value: &JsValue) { ::js_sys::js_bindgen::unsafe_global_wat! { "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - adapter = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & + shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & JsValue)],), } @@ -282,13 +288,13 @@ fn setter() { }, "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ \"test_crate.import.test\")) (param externref externref))) - (import \"env\" \"js_sys.externref.get\" (func $js_sys.externref.get (@sym) (param i32) (result \ - externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) (func $test_crate.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) local.get $arg0_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) local.get $arg1_0 - call $js_sys.externref.get (@reloc) + table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.test (@reloc) )", "(arg0_0, arg1_0) => arg0_0.test = arg1_0", diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index 9abf9bfe..ae2ff9cb 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -150,6 +150,12 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Op {js_test} + fn assert_optional_js_test() + where + ::core::option::Option: + ::js_sys::hazard::IntoJS + ::js_sys::hazard::FromJS, + {{}} + {source} "# ), diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index ec5fd30f..957d5822 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -32,16 +32,6 @@ fn basic() { ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - - unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { - type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - ::js_sys::hazard::OptionIntoJS::option_into_abi( - value.map(::js_sys::JsValue::from), - ) - } - } }, None, None, @@ -85,16 +75,6 @@ fn generic() { ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - - unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { - type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - ::js_sys::hazard::OptionIntoJS::option_into_abi( - value.map(::js_sys::JsValue::from), - ) - } - } }, None, None, @@ -138,16 +118,6 @@ fn default() { ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - - unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { - type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - ::js_sys::hazard::OptionIntoJS::option_into_abi( - value.map(::js_sys::JsValue::from), - ) - } - } }, None, None, @@ -191,16 +161,6 @@ fn r#trait() { ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) } } - - unsafe impl ::js_sys::hazard::OptionIntoJS for JsString { - type OptionAbi = <::js_sys::JsValue as ::js_sys::hazard::OptionIntoJS>::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - ::js_sys::hazard::OptionIntoJS::option_into_abi( - value.map(::js_sys::JsValue::from), - ) - } - } }, None, None, diff --git a/host/js-sys-bindgen/src/tests/type.rs b/host/js-sys-bindgen/src/tests/type.rs index 8d26c93a..aa6bcde6 100644 --- a/host/js-sys-bindgen/src/tests/type.rs +++ b/host/js-sys-bindgen/src/tests/type.rs @@ -20,7 +20,7 @@ fn basic() { }, { use js_sys::JsValue; - use js_sys::hazard::{IntoJS, JsCast, OptionIntoJS}; + use js_sys::hazard::{IntoJS, JsCast}; #[repr(transparent)] struct Test(JsValue); @@ -47,14 +47,6 @@ fn basic() { } } - unsafe impl OptionIntoJS for Test { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } - } - }, ); } @@ -78,7 +70,7 @@ fn generic() { { use core::marker::PhantomData; use js_sys::JsValue; - use js_sys::hazard::{IntoJS, JsCast, OptionIntoJS}; + use js_sys::hazard::{IntoJS, JsCast}; #[repr(transparent)] struct Test { @@ -108,14 +100,6 @@ fn generic() { } } - unsafe impl OptionIntoJS for Test { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } - } - }, ); } diff --git a/host/js-sys-bindgen/src/tests/web_idl.rs b/host/js-sys-bindgen/src/tests/web_idl.rs index ebd11406..fc7f573d 100644 --- a/host/js-sys-bindgen/src/tests/web_idl.rs +++ b/host/js-sys-bindgen/src/tests/web_idl.rs @@ -8,7 +8,7 @@ fn basic() { { #file }, { use js_sys::JsValue; - use js_sys::hazard::{IntoJS, JsCast, OptionIntoJS}; + use js_sys::hazard::{IntoJS, JsCast}; #[repr(transparent)] struct Test(JsValue); @@ -34,14 +34,6 @@ fn basic() { IntoJS::into_abi(JsValue::from(self)) } } - - unsafe impl OptionIntoJS for Test { - type OptionAbi = ::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - OptionIntoJS::option_into_abi(value.map(JsValue::from)) - } - } }, ); } diff --git a/host/js-sys-bindgen/src/type.rs b/host/js-sys-bindgen/src/type.rs index 315a8923..72c4549e 100644 --- a/host/js-sys-bindgen/src/type.rs +++ b/host/js-sys-bindgen/src/type.rs @@ -9,7 +9,7 @@ use crate::Hygiene; pub struct Type { pub r#struct: ItemStruct, - pub impls: [ItemImpl; 5], + pub impls: [ItemImpl; 4], } impl Type { @@ -32,7 +32,6 @@ impl Type { let js_value = hygiene.js_value(&cfgs, span); let js_cast = hygiene.js_cast(&cfgs, span); let into_js = hygiene.js_into(&cfgs, span); - let option_into_js = hygiene.js_option_into(&cfgs, span); let as_ref = hygiene.as_ref(span); let from = hygiene.from(span); @@ -90,16 +89,6 @@ impl Type { } } }, - parse_quote_spanned! {span=> - #(#cfgs)* - unsafe impl #gen_impl #option_into_js for #ident #gen_type #gen_where { - type OptionAbi = <#js_value as #option_into_js>::OptionAbi; - - fn option_into_abi(value: ::core::option::Option) -> Self::OptionAbi { - #option_into_js::option_into_abi(value.map(#js_value::from)) - } - } - }, ]; item_attrs.append(&mut cfgs); @@ -121,17 +110,16 @@ impl Type { impl IntoIterator for Type { type Item = Item; - type IntoIter = array::IntoIter; + type IntoIter = array::IntoIter; fn into_iter(self) -> Self::IntoIter { - let [impl_1, impl_2, impl_3, impl_4, impl_5] = self.impls; + let [impl_1, impl_2, impl_3, impl_4] = self.impls; [ Item::from(self.r#struct), impl_1.into(), impl_2.into(), impl_3.into(), impl_4.into(), - impl_5.into(), ] .into_iter() } diff --git a/host/ld/src/post.rs b/host/ld/src/post.rs index 40ad7cc4..67b97f2e 100644 --- a/host/ld/src/post.rs +++ b/host/ld/src/post.rs @@ -57,8 +57,8 @@ pub fn processing( import_section.append_to(&mut wasm_output); } - // The WAT adapters need these symbols during linking, but callers should only - // see the public adapters in the final module. + // The WAT shims need these symbols during linking, but callers should only + // see the public shims in the final module. Payload::ExportSection(exports) => { let mut export_section = ExportSection::new(); diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index 21f29c44..a853c5ab 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -182,7 +182,7 @@ fn process_object( Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => { js_store.add_js_embeds(c)?; } - // Extract JS export wrappers and keep their WAT adapter symbols alive. + // Extract JS export wrappers and keep their WAT shim symbols alive. Payload::CustomSection(c) if c.name() == "js_bindgen.export" => { for name in js_store.add_js_exports(c)? { add_args.push(format!("--export={name}").into()); From a204d30d2434269a2673fef601cf05ef834c1342 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:02:39 +0800 Subject: [PATCH 04/21] Add JavaScript binding benchmarks --- .prettierignore | 2 + benchmarks/.gitignore | 3 + benchmarks/Cargo.lock | 187 ++++++++++ benchmarks/Cargo.toml | 8 + benchmarks/README.md | 19 + benchmarks/bench.mjs | 558 +++++++++++++++++++++++++++++ benchmarks/js-bindgen/Cargo.toml | 11 + benchmarks/js-bindgen/src/lib.rs | 407 +++++++++++++++++++++ benchmarks/package-lock.json | 19 + benchmarks/package.json | 10 + benchmarks/wasm-bindgen/Cargo.toml | 11 + benchmarks/wasm-bindgen/src/lib.rs | 395 ++++++++++++++++++++ 12 files changed, 1630 insertions(+) create mode 100644 benchmarks/.gitignore create mode 100644 benchmarks/Cargo.lock create mode 100644 benchmarks/Cargo.toml create mode 100644 benchmarks/README.md create mode 100644 benchmarks/bench.mjs create mode 100644 benchmarks/js-bindgen/Cargo.toml create mode 100644 benchmarks/js-bindgen/src/lib.rs create mode 100644 benchmarks/package-lock.json create mode 100644 benchmarks/package.json create mode 100644 benchmarks/wasm-bindgen/Cargo.toml create mode 100644 benchmarks/wasm-bindgen/src/lib.rs diff --git a/.prettierignore b/.prettierignore index 875a1ee2..ee22dadf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,4 @@ *.mjs +/benchmarks/generated +/benchmarks/target /host/cli-lib/src/js/imports.d.mts diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 00000000..6b81f2a7 --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1,3 @@ +/generated +/node_modules +/target diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock new file mode 100644 index 00000000..a38b7956 --- /dev/null +++ b/benchmarks/Cargo.lock @@ -0,0 +1,187 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "js-bindgen" +version = "0.0.0" +dependencies = [ + "js-bindgen-macro", +] + +[[package]] +name = "js-bindgen-benchmark" +version = "0.0.0" +dependencies = [ + "js-sys", +] + +[[package]] +name = "js-bindgen-macro" +version = "0.1.0" + +[[package]] +name = "js-sys" +version = "0.0.0" +dependencies = [ + "js-bindgen", + "js-sys-macro", +] + +[[package]] +name = "js-sys-bindgen" +version = "0.1.0" +dependencies = [ + "foldhash", + "hashbrown", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys-macro" +version = "0.1.0" +dependencies = [ + "js-sys-bindgen", + "proc-macro2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-benchmark" +version = "0.0.0" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml new file mode 100644 index 00000000..37a297f5 --- /dev/null +++ b/benchmarks/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +resolver = "3" +members = ["js-bindgen", "wasm-bindgen"] + +[workspace.package] +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..93b54080 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,19 @@ +# Benchmarks + +Run the raw call benchmark with Node.js: + +```console +npm install +node bench.mjs +``` + +The comparison uses the in-tree `js-bindgen` and the exact published `wasm-bindgen` version pinned +in `Cargo.toml`. The matching `wasm-bindgen` CLI must be available on `PATH`. + +Warmup, batching, sampling, and statistics are handled by `mitata`. Every implementation of every +benchmark runs in a fresh process. The parent process aggregates the results after both +implementations finish, preventing one benchmark's JIT and GC state from affecting another. + +Benchmark functions are discovered from raw Wasm exports whose names start with `bench_`. To add a +benchmark, export the same `bench_*` function from both Rust crates. The runner infers Number, +BigInt, and reference parameters before measurement and gives every benchmark its own Wasm instance. diff --git a/benchmarks/bench.mjs b/benchmarks/bench.mjs new file mode 100644 index 00000000..f2d3c72b --- /dev/null +++ b/benchmarks/bench.mjs @@ -0,0 +1,558 @@ +import { spawnSync } from "node:child_process" +import { readFile, rm } from "node:fs/promises" +import { dirname, join } from "node:path" +import process from "node:process" +import { fileURLToPath, pathToFileURL } from "node:url" + +import { bench, do_not_optimize, run } from "mitata" + +const benchmarkPath = fileURLToPath(import.meta.url) +const benchmarkDirectory = dirname(benchmarkPath) +const repositoryDirectory = join(benchmarkDirectory, "..") +const targetDirectory = join(benchmarkDirectory, "target") +const generatedDirectory = join(benchmarkDirectory, "generated") +const benchmarkPrefix = "bench_" +const exceptionHandling = process.env.JBG_BENCH_NO_EH !== "1" +const rustflagsVariable = "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS" +const rustflags = [ + process.env[rustflagsVariable], + exceptionHandling && "-Awarnings -Ctarget-feature=+exception-handling", +] + .filter(Boolean) + .join(" ") +const wasmCargoEnvironment = { + [rustflagsVariable]: rustflags, +} +const workerImplementation = process.env.JBG_BENCH_IMPLEMENTATION +const workerBenchmark = process.env.JBG_BENCHMARK + +const filters = process.argv.slice(2).map(filter => filter.toLowerCase()) + +function execute(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: benchmarkDirectory, + env: { ...process.env, ...options.env }, + stdio: "inherit", + }) + + if (result.error) { + throw result.error + } + + if (result.status !== 0) { + throw new Error(`${command} ${args.join(" ")} failed with status ${result.status}`) + } +} + +function executeForOutput(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: benchmarkDirectory, + encoding: "utf8", + env: { ...process.env, ...options.env }, + maxBuffer: 16 * 1024 * 1024, + stdio: ["ignore", "pipe", "inherit"], + }) + + if (result.error) { + throw result.error + } + + if (result.status !== 0) { + throw new Error(`${command} ${args.join(" ")} failed with status ${result.status}`) + } + + return result.stdout +} + +function cargo(args, options) { + execute("cargo", args, options) +} + +async function build() { + await rm(generatedDirectory, { force: true, recursive: true }) + const toolchain = exceptionHandling ? ["+nightly"] : [] + + cargo( + [ + ...toolchain, + "build", + "--quiet", + "--package", + "js-bindgen-benchmark", + "--release", + "--target", + "wasm32-unknown-unknown", + ], + { + env: { + ...wasmCargoEnvironment, + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER: join( + repositoryDirectory, + "host/cargo-shim/linker" + ), + }, + } + ) + + const jsBindgenInput = join( + targetDirectory, + "wasm32-unknown-unknown/release/js_bindgen_benchmark.wasm" + ) + const jsBindgenOutput = join(generatedDirectory, "js-bindgen") + + cargo([ + "run", + "--quiet", + "--manifest-path", + join(repositoryDirectory, "host/Cargo.toml"), + "--package", + "js-bindgen-cli", + "--", + jsBindgenInput, + "--out-dir", + jsBindgenOutput, + ]) + + cargo( + [ + ...toolchain, + "build", + "--quiet", + "--package", + "wasm-bindgen-benchmark", + "--release", + "--target", + "wasm32-unknown-unknown", + ], + { env: wasmCargoEnvironment } + ) + + const wasmBindgenInput = join( + targetDirectory, + "wasm32-unknown-unknown/release/wasm_bindgen_benchmark.wasm" + ) + const wasmBindgenOutput = join(generatedDirectory, "wasm-bindgen") + + execute("wasm-bindgen", [ + wasmBindgenInput, + "--target", + "web", + "--out-dir", + wasmBindgenOutput, + "--no-typescript", + ]) +} + +let instanceId = 0 + +async function loadImplementation(implementation) { + const moduleUrl = pathToFileURL(implementation.modulePath) + moduleUrl.searchParams.set("instance", String(instanceId++)) + + const module = await import(moduleUrl) + const bytes = await readFile(implementation.wasmPath) + + if (implementation.kind === "js-bindgen") { + const wasmModule = await WebAssembly.compile(bytes) + const result = await new module.JsBindgen(wasmModule).instantiate() + return { + raw: result.instance.exports, + wrapped: result.exports, + } + } + + if (implementation.kind === "wasm-bindgen") { + return { + raw: module.initSync({ module: bytes }), + wrapped: module, + } + } + + throw new Error(`unknown implementation: ${implementation.kind}`) +} +const implementations = [ + { + name: "js-bindgen", + kind: "js-bindgen", + modulePath: join(generatedDirectory, "js-bindgen/js_bindgen_benchmark.mjs"), + wasmPath: join(generatedDirectory, "js-bindgen/js_bindgen_benchmark.wasm"), + }, + { + name: "wasm-bindgen", + kind: "wasm-bindgen", + modulePath: join(generatedDirectory, "wasm-bindgen/wasm_bindgen_benchmark.js"), + wasmPath: join(generatedDirectory, "wasm-bindgen/wasm_bindgen_benchmark_bg.wasm"), + }, +] + +function compareBenchmarks(left, right) { + return left.localeCompare(right) +} + +// Wasm functions expose their arity but not their parameter types. Start with +// Number and retry the parameter that rejected it as BigInt. Parameters that +// never coerce the probe are reference values. +function inferArguments(exportName, call, allowThrow = false) { + const kinds = Array(call.length).fill("number") + + while (true) { + const coerced = Array(call.length).fill(false) + let lastCoerced = -1 + let result + let throws = false + const probes = kinds.map((kind, index) => ({ + [Symbol.toPrimitive]() { + coerced[index] = true + lastCoerced = index + return kind === "bigint" ? 42n : 42 + }, + })) + + try { + result = call(...probes) + } catch (error) { + if ( + error instanceof TypeError && + lastCoerced >= 0 && + kinds[lastCoerced] !== "bigint" + ) { + kinds[lastCoerced] = "bigint" + continue + } + + if (!allowThrow) { + throw new Error(`cannot infer parameters for ${exportName}`, { + cause: error, + }) + } + + throws = true + } + + let bigintIndex = 0 + + return { + inputs: kinds.map((kind, index) => { + if (!coerced[index]) { + return {} + } + + if (kind === "bigint") { + return bigintIndex++ === 0 ? 42n : 0n + } + + return 42 + }), + kinds: kinds.map((kind, index) => (coerced[index] ? kind : "reference")), + result, + throws, + } + } +} + +async function discoverBenchmarks(implementation) { + const module = new WebAssembly.Module(await readFile(implementation.wasmPath)) + return WebAssembly.Module.exports(module) + .filter(item => item.kind === "function" && item.name.startsWith(benchmarkPrefix)) + .map(item => item.name) + .sort(compareBenchmarks) +} + +let benchmarkId = 0 + +function createBenchmark(call, inputs, throws) { + const parameterCount = inputs.length + const id = benchmarkId++ + const parameters = Array.from({ length: parameterCount }, (_, index) => `arg${index}`) + const invocation = `call(${parameters.join(", ")})` + const measuredCall = throws + ? ` + try { + result = ${invocation}; + } catch (error) { + result = error; + }` + : `result = ${invocation};` + const setup = parameters + .map( + (_, index) => ` + [${index}]() { + return inputs[${index}]; + },` + ) + .join("") + + // Compile a separate call site for every implementation. Reusing the same + // factory shares V8 optimization feedback between otherwise independent + // benchmarks and makes the result depend on registration order. + return Function( + "call", + "inputs", + "doNotOptimize", + ` + return function* benchmark${id}() { + let result; + + yield {${setup} + bench(${parameters.join(", ")}) { + ${measuredCall} + }, + }; + + doNotOptimize(result); + }; + ` + )(call, inputs, do_not_optimize) +} + +async function runWorker() { + const implementation = implementations.find(({ kind }) => kind === workerImplementation) + + if (!implementation) { + throw new Error(`unknown benchmark implementation: ${workerImplementation}`) + } + + const rawExports = await loadImplementation(implementation) + const rawCall = rawExports.raw[workerBenchmark] + + if (typeof rawCall !== "function") { + throw new Error(`missing Wasm export: ${implementation.name}:${workerBenchmark}`) + } + + const raw = inferArguments(workerBenchmark, rawCall) + + // Probe wrappers on a separate instance. Some raw ABIs transfer owned table + // indices, so probing them must not perturb the instance being measured. + const wrappedExports = await loadImplementation(implementation) + const wrappedCall = wrappedExports.wrapped[workerBenchmark] + + if (typeof wrappedCall !== "function") { + throw new Error(`missing JS export: ${implementation.name}:${workerBenchmark}`) + } + + const wrapped = inferArguments(workerBenchmark, wrappedCall, true) + const returnsReference = + (typeof wrapped.result === "object" && wrapped.result !== null) || + typeof wrapped.result === "function" + const useWrapper = + wrapped.throws || + wrapped.kinds.includes("reference") || + (Array.isArray(raw.result) && returnsReference) + const call = useWrapper ? wrappedCall : rawCall + const { inputs, kinds, throws } = useWrapper ? wrapped : raw + bench(implementation.name, createBenchmark(call, inputs, throws)) + + const result = await run({ format: "quiet", throw: true }) + const trial = result.benchmarks[0] + const measurement = trial?.runs[0] + + if (!measurement?.stats) { + throw new Error(`benchmark failed: ${implementation.name}:${workerBenchmark}`) + } + + const { debug: _, samples: __, ...stats } = measurement.stats + process.stdout.write( + JSON.stringify({ + context: { + arch: result.context.arch, + cpu: result.context.cpu, + exceptionHandling, + runtime: result.context.runtime, + version: result.context.version, + }, + implementation: implementation.name, + kinds, + stats, + throws, + }) + ) +} + +function runCase(implementation, exportName) { + const output = executeForOutput(process.execPath, [...process.execArgv, benchmarkPath], { + env: { + JBG_BENCHMARK: exportName, + JBG_BENCH_IMPLEMENTATION: implementation.kind, + }, + }) + + try { + return JSON.parse(output) + } catch (error) { + throw new Error(`invalid benchmark output from ${implementation.name}:${exportName}`, { + cause: error, + }) + } +} + +function formatTime(nanoseconds) { + if (nanoseconds < 1) { + return `${(nanoseconds * 1000).toFixed(2)} ps` + } + + if (nanoseconds < 1000) { + return `${nanoseconds.toFixed(2)} ns` + } + + if (nanoseconds < 1_000_000) { + return `${(nanoseconds / 1000).toFixed(2)} µs` + } + + if (nanoseconds < 1_000_000_000) { + return `${(nanoseconds / 1_000_000).toFixed(2)} ms` + } + + return `${(nanoseconds / 1_000_000_000).toFixed(2)} s` +} + +function printContext(context) { + console.log(`clk: ~${context.cpu.freq.toFixed(2)} GHz`) + console.log(`cpu: ${context.cpu.name}`) + console.log( + `runtime: ${context.runtime}${context.version ? ` ${context.version}` : ""} (${context.arch})` + ) + console.log(`exception-handling: ${context.exceptionHandling ? "enabled" : "disabled"}`) +} + +function printResults(name, results) { + console.log("") + console.log(`• ${name}`) + console.log("-".repeat(96)) + + for (const { implementation, stats } of results) { + const average = `${formatTime(stats.avg)}/iter`.padStart(15) + const range = `(${formatTime(stats.min)} … ${formatTime(stats.max)})`.padStart(25) + const percentiles = `${formatTime(stats.p75)} / ${formatTime(stats.p99)}`.padStart(20) + console.log(`${implementation.padEnd(18)}${average} ${range} ${percentiles}`) + } + + const baseline = results.find(({ implementation }) => implementation === "js-bindgen") + const comparisons = [] + for (const result of results) { + if (result === baseline) { + continue + } + + const baselineIsFaster = baseline.stats.avg <= result.stats.avg + const ratio = baselineIsFaster + ? result.stats.avg / baseline.stats.avg + : baseline.stats.avg / result.stats.avg + console.log("") + console.log("summary") + console.log( + ` js-bindgen ${ratio.toFixed(2)}x ${ + baselineIsFaster ? "faster" : "slower" + } than ${result.implementation}` + ) + + comparisons.push({ + baseline: baseline.stats.avg, + implementation: result.implementation, + name, + other: result.stats.avg, + ratio, + slower: !baselineIsFaster, + }) + } + + return comparisons +} + +function color(text, code) { + if (!process.stdout.isTTY) { + return text + } + + return `\u001B[${code}m${text}\u001B[0m` +} + +function printComparisons(comparisons) { + console.log("") + console.log("js-bindgen comparison") + console.log("-".repeat(96)) + + for (const comparison of comparisons) { + const result = + `${comparison.name.padEnd(48)} ${comparison.ratio.toFixed(2)}x ` + + `${comparison.slower ? "slower" : "faster"} ` + + `(${formatTime(comparison.baseline)} vs ${formatTime(comparison.other)} ` + + `${comparison.implementation})` + console.log(color(result, comparison.slower ? 31 : 32)) + } +} + +async function runCoordinator() { + await build() + + const discoveredBenchmarks = await Promise.all(implementations.map(discoverBenchmarks)) + const benchmarks = discoveredBenchmarks[0] + + if (benchmarks.length === 0) { + throw new Error(`no ${benchmarkPrefix} exports found`) + } + + for (let index = 1; index < discoveredBenchmarks.length; index++) { + if (benchmarks.join("\n") !== discoveredBenchmarks[index].join("\n")) { + throw new Error( + `${implementations[index].name} exports do not match ${implementations[0].name}` + ) + } + } + + const selectedBenchmarks = benchmarks.filter(exportName => { + if (filters.length === 0) { + return true + } + + return filters.some(filter => exportName.toLowerCase().includes(filter)) + }) + + if (selectedBenchmarks.length === 0) { + throw new Error(`no benchmark matched: ${filters.join(", ")}`) + } + + let printedContext = false + const comparisons = [] + for (const exportName of selectedBenchmarks) { + let expectedKinds + let expectedThrows + const results = [] + + for (const implementation of implementations) { + const result = runCase(implementation, exportName) + + if (expectedKinds && expectedKinds.join() !== result.kinds.join()) { + throw new Error( + `parameter ABI mismatch for ${exportName}: ${expectedKinds.join()} != ${result.kinds.join()}` + ) + } + + if (expectedThrows !== undefined && expectedThrows !== result.throws) { + throw new Error( + `exception behavior mismatch for ${exportName}: ${expectedThrows} != ${result.throws}` + ) + } + + expectedKinds = result.kinds + expectedThrows = result.throws + results.push(result) + + if (!printedContext) { + printContext(result.context) + printedContext = true + } + } + + comparisons.push(...printResults(exportName, results)) + } + + printComparisons(comparisons) +} + +if (workerImplementation === undefined && workerBenchmark === undefined) { + await runCoordinator() +} else if (workerImplementation !== undefined && workerBenchmark !== undefined) { + await runWorker() +} else { + throw new Error("incomplete benchmark worker configuration") +} diff --git a/benchmarks/js-bindgen/Cargo.toml b/benchmarks/js-bindgen/Cargo.toml new file mode 100644 index 00000000..9baaefbf --- /dev/null +++ b/benchmarks/js-bindgen/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "js-bindgen-benchmark" +edition = { workspace = true } +license = { workspace = true } +publish = { workspace = true } + +[lib] +crate-type = ["cdylib"] + +[dependencies] +js-sys = { path = "../../client/js-sys", features = ["macro"] } diff --git a/benchmarks/js-bindgen/src/lib.rs b/benchmarks/js-bindgen/src/lib.rs new file mode 100644 index 00000000..a905912c --- /dev/null +++ b/benchmarks/js-bindgen/src/lib.rs @@ -0,0 +1,407 @@ +use core::{array, hint::black_box}; + +use js_sys::{JsValue, js_sys}; + +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_benchmark", + name = "identity", + "(value) => value", +); + +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_benchmark", + name = "throw_value", + "(value) => {{ throw value }}", +); + +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_benchmark", + name = "length", + "(value) => value.length", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "identity")] + fn import_bool_raw(value: bool) -> bool; + + #[js_sys(js_embed = "identity")] + fn import_i32_raw(value: i32) -> i32; + + #[js_sys(js_embed = "identity")] + fn import_u32_raw(value: u32) -> u32; + + #[js_sys(js_embed = "identity")] + fn import_u64_raw(value: u64) -> u64; + + #[js_sys(js_embed = "identity")] + fn import_f64_raw(value: f64) -> f64; + + #[js_sys(js_embed = "identity")] + fn import_usize_raw(value: usize) -> usize; + + #[js_sys(js_embed = "identity")] + fn import_u128_raw(value: u128) -> u128; + + #[js_sys(js_embed = "identity")] + fn import_option_i16_raw(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn import_option_i32_raw(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn import_option_i64_raw(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn import_option_f64_raw(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn import_option_u128_raw(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn import_option_js_value_raw(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn import_result_unit_raw() -> Result<(), JsValue>; + + #[js_sys(js_embed = "identity")] + fn import_result_i32_raw(value: i32) -> Result; + + #[js_sys(js_embed = "identity")] + fn import_result_u128_raw(value: u128) -> Result; + + #[js_sys(js_embed = "identity")] + fn import_result_js_value_raw(value: JsValue) -> Result; + + #[js_sys(js_embed = "throw_value")] + fn import_result_i32_err_raw(value: i32) -> Result; + + #[js_sys(js_embed = "throw_value")] + fn import_result_u128_err_raw(value: u128) -> Result; + + #[js_sys(js_embed = "throw_value")] + fn import_result_js_value_err_raw(value: JsValue) -> Result; + + #[js_sys(js_embed = "throw_value")] + fn import_result_unit_err_raw() -> Result<(), JsValue>; + + #[js_sys(js_embed = "identity")] + fn import_js_value_raw(value: JsValue) -> JsValue; + + #[js_sys(js_embed = "length")] + fn import_str_raw(value: &str) -> u32; + + #[js_sys(js_embed = "length")] + fn import_u32_slice_raw(value: &[u32]) -> u32; + + #[js_sys(js_embed = "length")] + fn import_js_value_slice_raw(value: &[JsValue]) -> u32; +} + +#[js_sys] +fn bench_export_bool() -> bool { + true +} + +#[js_sys] +fn bench_export_i32(value: i32) -> i32 { + value +} + +#[js_sys] +fn bench_export_u32(value: u32) -> u32 { + value +} + +#[js_sys] +fn bench_export_u64(value: u64) -> u64 { + value +} + +#[js_sys] +fn bench_export_f64(value: f64) -> f64 { + value +} + +#[js_sys] +fn bench_export_usize(value: usize) -> usize { + value +} + +#[js_sys] +fn bench_export_u128(value: u128) -> u128 { + value +} + +#[js_sys] +fn bench_export_option_i16_some(value: i16) -> Option { + Some(value) +} + +#[js_sys] +fn bench_export_option_i16_none() -> Option { + None +} + +#[js_sys] +fn bench_export_option_i32_some(value: i32) -> Option { + Some(value) +} + +#[js_sys] +fn bench_export_option_i32_none() -> Option { + None +} + +#[js_sys] +fn bench_export_option_i64_some(value: i64) -> Option { + Some(value) +} + +#[js_sys] +fn bench_export_option_i64_none() -> Option { + None +} + +#[js_sys] +fn bench_export_option_f64_some(value: f64) -> Option { + Some(value) +} + +#[js_sys] +fn bench_export_option_f64_none() -> Option { + None +} + +#[js_sys] +fn bench_export_option_u128_some(value: u128) -> Option { + Some(value) +} + +#[js_sys] +fn bench_export_option_u128_none() -> Option { + None +} + +#[js_sys] +fn bench_export_option_js_value_some(value: JsValue) -> Option { + Some(value) +} + +#[js_sys] +fn bench_export_option_js_value_none() -> Option { + None +} + +#[js_sys] +fn bench_export_result_unit_ok() -> Result<(), JsValue> { + Ok(()) +} + +#[js_sys] +fn bench_export_result_unit_err() -> Result<(), JsValue> { + Err(JsValue::UNDEFINED) +} + +#[js_sys] +fn bench_export_result_i32_ok(value: i32) -> Result { + Ok(value) +} + +#[js_sys] +fn bench_export_result_i32_err(_value: i32) -> Result { + Err(JsValue::UNDEFINED) +} + +#[js_sys] +fn bench_export_result_u128_ok(value: u128) -> Result { + Ok(value) +} + +#[js_sys] +fn bench_export_result_u128_err(_value: u128) -> Result { + Err(JsValue::UNDEFINED) +} + +#[js_sys] +fn bench_export_result_js_value_ok(value: JsValue) -> Result { + Ok(value) +} + +#[js_sys] +fn bench_export_result_js_value_err(value: JsValue) -> Result { + Err(value) +} + +#[js_sys] +fn bench_export_js_value(value: JsValue) -> JsValue { + value +} + +#[js_sys] +fn bench_export_js_value_ref(value: &JsValue) -> i32 { + black_box(value); + 1 +} + +#[js_sys] +fn bench_export_js_value_alloc(value: JsValue) -> i32 { + let values: [JsValue; 512] = array::from_fn(|_| value.clone()); + black_box(&values); + 512 +} + +#[js_sys] +fn bench_import_i32(value: i32) -> i32 { + import_i32_raw(value) +} + +#[js_sys] +fn bench_import_bool(value: i32) -> bool { + import_bool_raw(value != 0) +} + +#[js_sys] +fn bench_import_u32(value: u32) -> u32 { + import_u32_raw(value) +} + +#[js_sys] +fn bench_import_u64(value: u64) -> u64 { + import_u64_raw(value) +} + +#[js_sys] +fn bench_import_f64(value: f64) -> f64 { + import_f64_raw(value) +} + +#[js_sys] +fn bench_import_usize(value: usize) -> usize { + import_usize_raw(value) +} + +#[js_sys] +fn bench_import_u128(value: u128) -> u128 { + import_u128_raw(value) +} + +#[js_sys] +fn bench_import_option_i16_some(value: Option) -> Option { + import_option_i16_raw(value) +} + +#[js_sys] +fn bench_import_option_i16_none() -> i32 { + i32::from(import_option_i16_raw(None).is_none()) +} + +#[js_sys] +fn bench_import_option_i32_some(value: Option) -> Option { + import_option_i32_raw(value) +} + +#[js_sys] +fn bench_import_option_i32_none() -> i32 { + i32::from(import_option_i32_raw(None).is_none()) +} + +#[js_sys] +fn bench_import_option_i64_some(value: Option) -> Option { + import_option_i64_raw(value) +} + +#[js_sys] +fn bench_import_option_i64_none() -> i32 { + i32::from(import_option_i64_raw(None).is_none()) +} + +#[js_sys] +fn bench_import_option_f64_some(value: Option) -> Option { + import_option_f64_raw(value) +} + +#[js_sys] +fn bench_import_option_f64_none() -> i32 { + i32::from(import_option_f64_raw(None).is_none()) +} + +#[js_sys] +fn bench_import_option_u128_some(value: Option) -> Option { + import_option_u128_raw(value) +} + +#[js_sys] +fn bench_import_option_u128_none() -> i32 { + i32::from(import_option_u128_raw(None).is_none()) +} + +#[js_sys] +fn bench_import_option_js_value_some(value: Option) -> Option { + import_option_js_value_raw(value) +} + +#[js_sys] +fn bench_import_option_js_value_none() -> i32 { + i32::from(import_option_js_value_raw(None).is_none()) +} + +#[js_sys] +fn bench_import_result_unit_ok() -> Result<(), JsValue> { + import_result_unit_raw() +} + +#[js_sys] +fn bench_import_result_unit_err() -> i32 { + i32::from(import_result_unit_err_raw().is_err()) +} + +#[js_sys] +fn bench_import_result_i32_ok(value: i32) -> Result { + import_result_i32_raw(value) +} + +#[js_sys] +fn bench_import_result_i32_err(value: i32) -> i32 { + i32::from(import_result_i32_err_raw(value).is_err()) +} + +#[js_sys] +fn bench_import_result_u128_ok(value: u128) -> Result { + import_result_u128_raw(value) +} + +#[js_sys] +fn bench_import_result_u128_err(value: u128) -> i32 { + i32::from(import_result_u128_err_raw(value).is_err()) +} + +#[js_sys] +fn bench_import_result_js_value_ok(value: JsValue) -> Result { + import_result_js_value_raw(value) +} + +#[js_sys] +fn bench_import_result_js_value_err(value: JsValue) -> i32 { + i32::from(import_result_js_value_err_raw(value).is_err()) +} + +#[js_sys] +fn bench_import_js_value(value: JsValue) -> JsValue { + import_js_value_raw(value) +} + +#[js_sys] +fn bench_import_str() -> u32 { + import_str_raw(black_box("js-bindgen benchmark")) +} + +#[js_sys] +fn bench_import_u32_slice() -> u32 { + import_u32_slice_raw(black_box(&[1, 2, 3, 4, 5, 6, 7, 8])) +} + +#[js_sys] +fn bench_import_js_value_slice(value: JsValue) -> u32 { + import_js_value_slice_raw(core::slice::from_ref(&value)) +} diff --git a/benchmarks/package-lock.json b/benchmarks/package-lock.json new file mode 100644 index 00000000..56b7f061 --- /dev/null +++ b/benchmarks/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "benchmarks", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "mitata": "^1.0.34" + } + }, + "node_modules/mitata": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/mitata/-/mitata-1.0.34.tgz", + "integrity": "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/benchmarks/package.json b/benchmarks/package.json new file mode 100644 index 00000000..e001cc23 --- /dev/null +++ b/benchmarks/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "type": "module", + "scripts": { + "bench": "node bench.mjs" + }, + "devDependencies": { + "mitata": "^1.0.34" + } +} diff --git a/benchmarks/wasm-bindgen/Cargo.toml b/benchmarks/wasm-bindgen/Cargo.toml new file mode 100644 index 00000000..bcc142e4 --- /dev/null +++ b/benchmarks/wasm-bindgen/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasm-bindgen-benchmark" +edition = { workspace = true } +license = { workspace = true } +publish = { workspace = true } + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "=0.2.126" diff --git a/benchmarks/wasm-bindgen/src/lib.rs b/benchmarks/wasm-bindgen/src/lib.rs new file mode 100644 index 00000000..c120f2e3 --- /dev/null +++ b/benchmarks/wasm-bindgen/src/lib.rs @@ -0,0 +1,395 @@ +use core::{array, hint::black_box}; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(inline_js = "export function identity(value) { return value; }")] +extern "C" { + #[wasm_bindgen(js_name = identity)] + fn import_bool_raw(value: bool) -> bool; + + #[wasm_bindgen(js_name = identity)] + fn import_i32_raw(value: i32) -> i32; + + #[wasm_bindgen(js_name = identity)] + fn import_u32_raw(value: u32) -> u32; + + #[wasm_bindgen(js_name = identity)] + fn import_u64_raw(value: u64) -> u64; + + #[wasm_bindgen(js_name = identity)] + fn import_f64_raw(value: f64) -> f64; + + #[wasm_bindgen(js_name = identity)] + fn import_usize_raw(value: usize) -> usize; + + #[wasm_bindgen(js_name = identity)] + fn import_u128_raw(value: u128) -> u128; + + #[wasm_bindgen(js_name = identity)] + fn import_option_i16_raw(value: Option) -> Option; + + #[wasm_bindgen(js_name = identity)] + fn import_option_i32_raw(value: Option) -> Option; + + #[wasm_bindgen(js_name = identity)] + fn import_option_i64_raw(value: Option) -> Option; + + #[wasm_bindgen(js_name = identity)] + fn import_option_f64_raw(value: Option) -> Option; + + #[wasm_bindgen(js_name = identity)] + fn import_option_u128_raw(value: Option) -> Option; + + #[wasm_bindgen(js_name = identity)] + fn import_option_js_value_raw(value: Option) -> Option; + + #[wasm_bindgen(catch, js_name = identity)] + fn import_result_unit_raw() -> Result<(), JsValue>; + + #[wasm_bindgen(catch, js_name = identity)] + fn import_result_i32_raw(value: i32) -> Result; + + #[wasm_bindgen(catch, js_name = identity)] + fn import_result_u128_raw(value: u128) -> Result; + + #[wasm_bindgen(catch, js_name = identity)] + fn import_result_js_value_raw(value: JsValue) -> Result; + + #[wasm_bindgen(js_name = identity)] + fn import_js_value_raw(value: JsValue) -> JsValue; +} + +#[wasm_bindgen(inline_js = "export function throw_value(value) { throw value; }")] +extern "C" { + #[wasm_bindgen(catch, js_name = throw_value)] + fn import_result_i32_err_raw(value: i32) -> Result; + + #[wasm_bindgen(catch, js_name = throw_value)] + fn import_result_u128_err_raw(value: u128) -> Result; + + #[wasm_bindgen(catch, js_name = throw_value)] + fn import_result_js_value_err_raw(value: JsValue) -> Result; + + #[wasm_bindgen(catch, js_name = throw_value)] + fn import_result_unit_err_raw() -> Result<(), JsValue>; +} + +#[wasm_bindgen(inline_js = "export function length(value) { return value.length; }")] +extern "C" { + #[wasm_bindgen(js_name = length)] + fn import_str_raw(value: &str) -> u32; + + #[wasm_bindgen(js_name = length)] + fn import_u32_slice_raw(value: &[u32]) -> u32; + + #[wasm_bindgen(js_name = length)] + fn import_js_value_slice_raw(value: &[JsValue]) -> u32; +} + +#[wasm_bindgen] +pub fn bench_export_bool() -> bool { + true +} + +#[wasm_bindgen] +pub fn bench_export_i32(value: i32) -> i32 { + value +} + +#[wasm_bindgen] +pub fn bench_export_u32(value: u32) -> u32 { + value +} + +#[wasm_bindgen] +pub fn bench_export_u64(value: u64) -> u64 { + value +} + +#[wasm_bindgen] +pub fn bench_export_f64(value: f64) -> f64 { + value +} + +#[wasm_bindgen] +pub fn bench_export_usize(value: usize) -> usize { + value +} + +#[wasm_bindgen] +pub fn bench_export_u128(value: u128) -> u128 { + value +} + +#[wasm_bindgen] +pub fn bench_export_option_i16_some(value: i16) -> Option { + Some(value) +} + +#[wasm_bindgen] +pub fn bench_export_option_i16_none() -> Option { + None +} + +#[wasm_bindgen] +pub fn bench_export_option_i32_some(value: i32) -> Option { + Some(value) +} + +#[wasm_bindgen] +pub fn bench_export_option_i32_none() -> Option { + None +} + +#[wasm_bindgen] +pub fn bench_export_option_i64_some(value: i64) -> Option { + Some(value) +} + +#[wasm_bindgen] +pub fn bench_export_option_i64_none() -> Option { + None +} + +#[wasm_bindgen] +pub fn bench_export_option_f64_some(value: f64) -> Option { + Some(value) +} + +#[wasm_bindgen] +pub fn bench_export_option_f64_none() -> Option { + None +} + +#[wasm_bindgen] +pub fn bench_export_option_u128_some(value: u128) -> Option { + Some(value) +} + +#[wasm_bindgen] +pub fn bench_export_option_u128_none() -> Option { + None +} + +#[wasm_bindgen] +pub fn bench_export_option_js_value_some(value: JsValue) -> Option { + Some(value) +} + +#[wasm_bindgen] +pub fn bench_export_option_js_value_none() -> Option { + None +} + +#[wasm_bindgen] +pub fn bench_export_result_unit_ok() -> Result<(), JsValue> { + Ok(()) +} + +#[wasm_bindgen] +pub fn bench_export_result_unit_err() -> Result<(), JsValue> { + Err(JsValue::UNDEFINED) +} + +#[wasm_bindgen] +pub fn bench_export_result_i32_ok(value: i32) -> Result { + Ok(value) +} + +#[wasm_bindgen] +pub fn bench_export_result_i32_err(_value: i32) -> Result { + Err(JsValue::UNDEFINED) +} + +#[wasm_bindgen] +pub fn bench_export_result_u128_ok(value: u128) -> Result { + Ok(value) +} + +#[wasm_bindgen] +pub fn bench_export_result_u128_err(_value: u128) -> Result { + Err(JsValue::UNDEFINED) +} + +#[wasm_bindgen] +pub fn bench_export_result_js_value_ok(value: JsValue) -> Result { + Ok(value) +} + +#[wasm_bindgen] +pub fn bench_export_result_js_value_err(value: JsValue) -> Result { + Err(value) +} + +#[wasm_bindgen] +pub fn bench_export_js_value(value: JsValue) -> JsValue { + value +} + +#[wasm_bindgen] +pub fn bench_export_js_value_ref(value: &JsValue) -> i32 { + black_box(value); + 1 +} + +#[wasm_bindgen] +pub fn bench_export_js_value_alloc(value: JsValue) -> i32 { + let values: [JsValue; 512] = array::from_fn(|_| value.clone()); + black_box(&values); + 512 +} + +#[wasm_bindgen] +pub fn bench_import_i32(value: i32) -> i32 { + import_i32_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_bool(value: i32) -> bool { + import_bool_raw(value != 0) +} + +#[wasm_bindgen] +pub fn bench_import_u32(value: u32) -> u32 { + import_u32_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_u64(value: u64) -> u64 { + import_u64_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_f64(value: f64) -> f64 { + import_f64_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_usize(value: usize) -> usize { + import_usize_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_u128(value: u128) -> u128 { + import_u128_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_i16_some(value: Option) -> Option { + import_option_i16_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_i16_none() -> i32 { + i32::from(import_option_i16_raw(None).is_none()) +} + +#[wasm_bindgen] +pub fn bench_import_option_i32_some(value: Option) -> Option { + import_option_i32_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_i32_none() -> i32 { + i32::from(import_option_i32_raw(None).is_none()) +} + +#[wasm_bindgen] +pub fn bench_import_option_i64_some(value: Option) -> Option { + import_option_i64_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_i64_none() -> i32 { + i32::from(import_option_i64_raw(None).is_none()) +} + +#[wasm_bindgen] +pub fn bench_import_option_f64_some(value: Option) -> Option { + import_option_f64_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_f64_none() -> i32 { + i32::from(import_option_f64_raw(None).is_none()) +} + +#[wasm_bindgen] +pub fn bench_import_option_u128_some(value: Option) -> Option { + import_option_u128_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_u128_none() -> i32 { + i32::from(import_option_u128_raw(None).is_none()) +} + +#[wasm_bindgen] +pub fn bench_import_option_js_value_some(value: Option) -> Option { + import_option_js_value_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_option_js_value_none() -> i32 { + i32::from(import_option_js_value_raw(None).is_none()) +} + +#[wasm_bindgen] +pub fn bench_import_result_unit_ok() -> Result<(), JsValue> { + import_result_unit_raw() +} + +#[wasm_bindgen] +pub fn bench_import_result_unit_err() -> i32 { + i32::from(import_result_unit_err_raw().is_err()) +} + +#[wasm_bindgen] +pub fn bench_import_result_i32_ok(value: i32) -> Result { + import_result_i32_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_result_i32_err(value: i32) -> i32 { + i32::from(import_result_i32_err_raw(value).is_err()) +} + +#[wasm_bindgen] +pub fn bench_import_result_u128_ok(value: u128) -> Result { + import_result_u128_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_result_u128_err(value: u128) -> i32 { + i32::from(import_result_u128_err_raw(value).is_err()) +} + +#[wasm_bindgen] +pub fn bench_import_result_js_value_ok(value: JsValue) -> Result { + import_result_js_value_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_result_js_value_err(value: JsValue) -> i32 { + i32::from(import_result_js_value_err_raw(value).is_err()) +} + +#[wasm_bindgen] +pub fn bench_import_js_value(value: JsValue) -> JsValue { + import_js_value_raw(value) +} + +#[wasm_bindgen] +pub fn bench_import_str() -> u32 { + import_str_raw(black_box("js-bindgen benchmark")) +} + +#[wasm_bindgen] +pub fn bench_import_u32_slice() -> u32 { + import_u32_slice_raw(black_box(&[1, 2, 3, 4, 5, 6, 7, 8])) +} + +#[wasm_bindgen] +pub fn bench_import_js_value_slice(value: JsValue) -> u32 { + import_js_value_slice_raw(core::slice::from_ref(&value)) +} From 6ef154f68fbca3a5ebb9622f5f97839b3a732dc8 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Tue, 28 Jul 2026 01:17:03 +0800 Subject: [PATCH 05/21] Add `Closure` --- benchmarks/js-bindgen/src/lib.rs | 21 +- benchmarks/wasm-bindgen/src/lib.rs | 18 + client/e2e/examples/closure.rs | 317 ++++++++++++++++++ client/js-sys/src/closure/closure.gen.rs | 39 +++ client/js-sys/src/closure/closure.js-sys.rs | 7 + client/js-sys/src/closure/mod.rs | 351 ++++++++++++++++++++ client/js-sys/src/lib.rs | 4 +- client/js-sys/src/macro.rs | 4 + client/js-sys/src/macro/export.rs | 6 +- host/cli-lib/src/js/imports.mjs | 6 +- host/cli-lib/src/js/imports.mts | 6 +- host/dev/src/client/e2e.rs | 3 + host/js-sys-bindgen/src/closure.rs | 292 ++++++++++++++++ host/js-sys-bindgen/src/lib.rs | 4 + host/js-sys-macro/src/lib.rs | 12 + host/macro/src/tests/import_js.rs | 13 + host/macro/src/util.rs | 7 +- 17 files changed, 1100 insertions(+), 10 deletions(-) create mode 100644 client/e2e/examples/closure.rs create mode 100644 client/js-sys/src/closure/closure.gen.rs create mode 100644 client/js-sys/src/closure/closure.js-sys.rs create mode 100644 client/js-sys/src/closure/mod.rs create mode 100644 host/js-sys-bindgen/src/closure.rs diff --git a/benchmarks/js-bindgen/src/lib.rs b/benchmarks/js-bindgen/src/lib.rs index a905912c..fb076185 100644 --- a/benchmarks/js-bindgen/src/lib.rs +++ b/benchmarks/js-bindgen/src/lib.rs @@ -1,6 +1,6 @@ use core::{array, hint::black_box}; -use js_sys::{JsValue, js_sys}; +use js_sys::{Closure, JsValue, closure, js_sys}; js_sys::js_bindgen::embed_js!( module = "js_bindgen_benchmark", @@ -20,6 +20,12 @@ js_sys::js_bindgen::embed_js!( "(value) => value.length", ); +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_benchmark", + name = "invoke_closure", + "(callback, value) => callback(value)", +); + #[js_sys] extern "js-sys" { #[js_sys(js_embed = "identity")] @@ -96,6 +102,19 @@ extern "js-sys" { #[js_sys(js_embed = "length")] fn import_js_value_slice_raw(value: &[JsValue]) -> u32; + + #[js_sys(js_embed = "invoke_closure")] + fn invoke_closure_raw(callback: &Closure i32>, value: i32) -> i32; +} + +std::thread_local! { + static CALLBACK: Closure i32> = + closure!(dyn FnMut(i32) -> i32, |value| value); +} + +#[js_sys] +fn bench_closure_call(value: i32) -> i32 { + CALLBACK.with(|callback| invoke_closure_raw(callback, value)) } #[js_sys] diff --git a/benchmarks/wasm-bindgen/src/lib.rs b/benchmarks/wasm-bindgen/src/lib.rs index c120f2e3..0938076c 100644 --- a/benchmarks/wasm-bindgen/src/lib.rs +++ b/benchmarks/wasm-bindgen/src/lib.rs @@ -86,6 +86,24 @@ extern "C" { fn import_js_value_slice_raw(value: &[JsValue]) -> u32; } +#[wasm_bindgen( + inline_js = "export function invoke_closure(callback, value) { return callback(value); }" +)] +extern "C" { + #[wasm_bindgen(js_name = invoke_closure)] + fn invoke_closure_raw(callback: &Closure i32>, value: i32) -> i32; +} + +std::thread_local! { + static CALLBACK: Closure i32> = + Closure::new(|value| value); +} + +#[wasm_bindgen] +pub fn bench_closure_call(value: i32) -> i32 { + CALLBACK.with(|callback| invoke_closure_raw(callback, value)) +} + #[wasm_bindgen] pub fn bench_export_bool() -> bool { true diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs new file mode 100644 index 00000000..9a7f48b6 --- /dev/null +++ b/client/e2e/examples/closure.rs @@ -0,0 +1,317 @@ +#[rustfmt::skip] +fn main() { + // ;; exports["closure_i32"](20) === 43 + // ;; exports["closure_u128"](1n << 96n) === (1n << 96n) + 1n + // ;; (() => { const value = {}; return exports["closure_js_value"](value) === value })() + // ;; exports["closure_result"](41, false) === 42 + // ;; (() => { try { exports["closure_result"](41, true); return false } catch (error) { return error === "closure error" } })() + // ;; exports["closure_lifecycle"]() + // ;; exports["closure_fn_reentrant"](20) === 22 + // ;; exports["closure_owned"](20) === 21 + // ;; exports["closure_owned_lifecycle"]() + // ;; exports["closure_once"](20) === 21 + // ;; (() => { try { exports["closure_once_again"](20); return false } catch (error) { return error.message === "FnOnce called more than once" } })() + // ;; exports["closure_option_ref"](20) + // ;; exports["closure_option_owned"](20) + // ;; (() => { const callback = exports["closure_return"](2); return callback(40) === 42 })() + // ;; exports["closure_error_lifecycle"]() + // ;; exports["closure_once_lifecycle"]() + // ;; typeof globalThis.gc !== "function" || typeof FinalizationRegistry === "undefined" || await (async () => { let callback = exports["closure_finalization"](); const unref = callback.unref; callback = null; for (let i = 0; i < 100 && exports["closure_finalization_drops"]() === 0; i++) { globalThis.gc(); await new Promise(resolve => globalThis.setTimeout(resolve, 0)) } if (exports["closure_finalization_drops"]() !== 1) return false; unref(); return exports["closure_finalization_drops"]() === 1 })() +} + +use std::cell::Cell; +use std::sync::atomic::{AtomicU32, Ordering}; + +use js_sys::{Closure, JsString, JsValue, closure, js_sys}; + +static DROPS: AtomicU32 = AtomicU32::new(0); + +struct DropCounter; + +impl Drop for DropCounter { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } +} + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "invoke.twice", + "(callback, value) => callback(value) + callback(value)", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "invoke", + "(callback, value) => callback(value)", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "storage", + "({{ callback: undefined }})", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "save", + required_embeds = [("closure", "storage")], + "(callback) => {{", + " this.#jsEmbed.closure.storage.callback = callback", + "}}", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "is_invalid", + required_embeds = [("closure", "storage")], + "() => {{", + " try {{", + " this.#jsEmbed.closure.storage.callback()", + " return false", + " }} catch (error) {{", + " return error instanceof Error", + " && error.message === 'closure invoked recursively or after being dropped'", + " }}", + "}}", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "invoke.saved", + required_embeds = [("closure", "storage")], + "(value) => this.#jsEmbed.closure.storage.callback(value)", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "invoke.optional", + "(callback, value) => callback?.(value)", +); + +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "release", + required_embeds = [("closure", "storage")], + "() => {{", + " this.#jsEmbed.closure.storage.callback.unref()", + " return true", + "}}", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "invoke.twice")] + fn invoke_i32_twice(callback: &Closure i32>, value: i32) -> i32; + + #[js_sys(js_embed = "invoke")] + fn invoke_u128(callback: &Closure u128>, value: u128) -> u128; + + #[js_sys(js_embed = "invoke")] + fn invoke_js_value( + callback: &Closure JsValue>, + value: JsValue, + ) -> JsValue; + + #[js_sys(js_embed = "invoke")] + fn invoke_result( + callback: &Closure Result>, + value: i32, + ) -> Result; + + #[js_sys(js_embed = "invoke")] + fn invoke_unit(callback: &Closure); + + #[js_sys(js_embed = "invoke.optional")] + fn invoke_optional_ref( + callback: Option<&Closure i32>>, + value: i32, + ) -> Option; + + #[js_sys(js_embed = "invoke.optional")] + fn invoke_optional_owned( + callback: Option i32>>, + value: i32, + ) -> Option; + + #[js_sys(js_embed = "save")] + fn save(callback: &Closure); + + #[js_sys(js_embed = "save")] + fn save_fn(callback: &Closure i32>); + + #[js_sys(js_embed = "save")] + fn save_owned(callback: Closure i32>); + + #[js_sys(js_embed = "save")] + fn save_owned_unit(callback: Closure); + + #[js_sys(js_embed = "is_invalid")] + fn is_invalid() -> bool; + + #[js_sys(js_embed = "invoke.saved")] + fn invoke_saved(value: i32) -> i32; + + #[js_sys(js_embed = "release")] + fn release() -> bool; +} + +#[js_sys] +fn closure_i32(value: i32) -> i32 { + let mut offset = 0; + let callback = closure!(dyn FnMut(i32) -> i32, move |value| { + offset += 1; + value + offset + }); + + invoke_i32_twice(&callback, value) +} + +#[js_sys] +fn closure_u128(value: u128) -> u128 { + let callback = closure!(dyn FnMut(u128) -> u128, move |value| value + 1); + + invoke_u128(&callback, value) +} + +#[js_sys] +fn closure_js_value(value: JsValue) -> JsValue { + let callback = closure!(dyn FnMut(JsValue) -> JsValue, move |value| value); + + invoke_js_value(&callback, value) +} + +#[js_sys] +fn closure_result(value: i32, error: bool) -> Result { + let callback = closure!(dyn FnMut(i32) -> Result, move |value| { + if error { + Err(JsString::from("closure error").into()) + } else { + Ok(value + 1) + } + }); + + invoke_result(&callback, value) +} + +#[js_sys] +fn closure_lifecycle() -> bool { + DROPS.store(0, Ordering::Relaxed); + let counter = DropCounter; + let callback = closure!(dyn FnMut(), move || { + let _ = &counter; + }); + save(&callback); + drop(callback); + DROPS.load(Ordering::Relaxed) == 1 && is_invalid() +} + +#[js_sys] +fn closure_fn_reentrant(value: i32) -> i32 { + let entered = Cell::new(false); + let callback = closure!(dyn Fn(i32) -> i32, move |value| { + if entered.replace(true) { + value + 1 + } else { + invoke_saved(value) + 1 + } + }); + save_fn(&callback); + + invoke_saved(value) +} + +#[js_sys] +fn closure_owned(value: i32) -> i32 { + let callback = closure!(dyn FnMut(i32) -> i32, move |value| value + 1); + save_owned(callback); + + invoke_saved(value) +} + +#[js_sys] +fn closure_owned_lifecycle() -> bool { + DROPS.store(0, Ordering::Relaxed); + let counter = DropCounter; + let callback = closure!(dyn FnMut(), move || { + let _ = &counter; + }); + save_owned_unit(callback); + + release() && DROPS.load(Ordering::Relaxed) == 1 && is_invalid() +} + +#[js_sys] +fn closure_once(value: i32) -> i32 { + let callback = closure!(dyn FnOnce(i32) -> i32, move |value| value + 1); + save_owned(callback); + + invoke_saved(value) +} + +#[js_sys] +fn closure_once_again(value: i32) -> i32 { + invoke_saved(value) +} + +#[js_sys] +fn closure_option_ref(value: i32) -> bool { + let callback = closure!(dyn FnMut(i32) -> i32, move |value| value + 1); + + invoke_optional_ref(Some(&callback), value) == Some(value + 1) + && invoke_optional_ref(None, value).is_none() +} + +#[js_sys] +fn closure_option_owned(value: i32) -> bool { + let callback = closure!(dyn FnMut(i32) -> i32, move |value| value + 1); + + invoke_optional_owned(Some(callback), value) == Some(value + 1) + && invoke_optional_owned(None, value).is_none() +} + +#[js_sys] +fn closure_return(offset: i32) -> Closure i32> { + closure!(dyn FnMut(i32) -> i32, move |value| value + offset) +} + +#[js_sys] +fn closure_error_lifecycle() -> bool { + DROPS.store(0, Ordering::Relaxed); + let counter = DropCounter; + let callback = closure!(dyn FnMut(i32) -> Result, move |_| { + let _ = &counter; + Err(JsString::from("closure error").into()) + }); + let result = invoke_result(&callback, 0); + drop(callback); + + result.is_err() && DROPS.load(Ordering::Relaxed) == 1 +} + +#[js_sys] +fn closure_once_lifecycle() -> bool { + DROPS.store(0, Ordering::Relaxed); + let counter = DropCounter; + let callback = closure!(dyn FnOnce(), move || drop(counter)); + invoke_unit(&callback); + let dropped_after_call = DROPS.load(Ordering::Relaxed) == 1; + drop(callback); + + dropped_after_call && DROPS.load(Ordering::Relaxed) == 1 +} + +#[js_sys] +fn closure_finalization() -> Closure { + DROPS.store(0, Ordering::Relaxed); + let counter = DropCounter; + + closure!(dyn Fn(), move || { + let _ = &counter; + }) +} + +#[js_sys] +fn closure_finalization_drops() -> u32 { + DROPS.load(Ordering::Relaxed) +} diff --git a/client/js-sys/src/closure/closure.gen.rs b/client/js-sys/src/closure/closure.gen.rs new file mode 100644 index 00000000..f65995f0 --- /dev/null +++ b/client/js-sys/src/closure/closure.gen.rs @@ -0,0 +1,39 @@ +//! This file was generated by `js-sys-bindgen`. + +#![allow(warnings)] + +use crate::{js_bindgen, r#macro}; +use crate::JsValue; + +pub(super) fn closure_unref(callback: &JsValue) { + js_bindgen::unsafe_global_wat! { + "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "closure_unref", shim = + "js_sys.closure_unref", inputs = [("arg0", & JsValue)],), + } + + js_bindgen::import_js! { + module = "js_sys", + name = "closure_unref", + required_embeds = [("js_sys", "closure.unref"), r#macro::js_input_embed::<&JsValue>()], + "{}", + interpolate r#macro::js_import!( + direct_open = "", direct_call = "this.#jsEmbed.js_sys['closure.unref']", indirect_call = + "this.#jsEmbed.js_sys['closure.unref'](arg0_0)", inputs = [("arg0", & JsValue)], + ), + } + + unsafe extern "C" { + #[link_name = "js_sys.closure_unref"] + fn closure_unref( + arg0_0: r#macro::InputSlot1<&JsValue>, + arg0_1: r#macro::InputSlot2<&JsValue>, + arg0_2: r#macro::InputSlot3<&JsValue>, + arg0_3: r#macro::InputSlot4<&JsValue>, + ); + } + + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(callback); + unsafe { closure_unref(arg0_0, arg0_1, arg0_2, arg0_3) } + }; +} diff --git a/client/js-sys/src/closure/closure.js-sys.rs b/client/js-sys/src/closure/closure.js-sys.rs new file mode 100644 index 00000000..205aee96 --- /dev/null +++ b/client/js-sys/src/closure/closure.js-sys.rs @@ -0,0 +1,7 @@ +use crate::JsValue; + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "closure.unref")] + pub(super) fn closure_unref(callback: &JsValue); +} diff --git a/client/js-sys/src/closure/mod.rs b/client/js-sys/src/closure/mod.rs new file mode 100644 index 00000000..02a9e1e7 --- /dev/null +++ b/client/js-sys/src/closure/mod.rs @@ -0,0 +1,351 @@ +#[rustfmt::skip] +#[path = "closure.gen.rs"] +mod closure; + +use alloc::boxed::Box; +use core::marker::PhantomData; +use core::mem::{self, ManuallyDrop}; +use core::ptr; + +use crate::hazard::{IntoJS, IntoJsConv}; +use crate::{JsValue, r#macro}; + +/// Type-erased information stored at the start of every closure allocation. +#[doc(hidden)] +#[repr(C)] +pub struct ClosureHeader { + drop: unsafe fn(*mut Self), +} + +// Both `repr(C)` prefixes start with `ClosureHeader`. This lets JavaScript +// retain one thin pointer while Rust recovers the signature and callback types. +#[repr(C)] +struct ClosurePrefix { + header: ClosureHeader, + call_shim: C, +} + +#[repr(C)] +struct ClosureState { + prefix: ClosurePrefix, + callback: F, +} + +impl ClosureHeader { + #[must_use] + const fn new(drop: unsafe fn(*mut Self)) -> Self { + Self { drop } + } + + unsafe fn release(pointer: *mut Self) { + // SAFETY: The caller guarantees that `pointer` identifies a live header. + // Read the function pointer before it releases the containing allocation. + let drop = unsafe { (*pointer).drop }; + // SAFETY: The same caller guarantee satisfies the stored drop function. + unsafe { drop(pointer) }; + } + + /// Reads the call shim stored after this header. + /// + /// # Safety + /// + /// `pointer` must come from [`ClosureAllocation::new`], and `C` must be the + /// call shim type used to create that allocation. + #[inline] + pub unsafe fn call_shim(pointer: *mut Self) -> C { + let pointer = pointer.cast::>(); + // SAFETY: The caller guarantees the allocation and `C` match. + unsafe { ptr::read(&raw const (*pointer).call_shim) } + } + + /// Returns the captured callback stored after this header. + /// + /// # Safety + /// + /// `pointer` must come from [`ClosureAllocation::new`], and `F` and `C` + /// must be the types used to create that allocation. The caller must + /// uphold the aliasing rules appropriate for `F`. + #[inline] + pub unsafe fn callback(pointer: *mut Self) -> *mut F { + let pointer = pointer.cast::>(); + // SAFETY: The caller guarantees the allocation, `F`, and `C` match. + unsafe { &raw mut (*pointer).callback } + } +} + +/// A closure allocation guarded until ownership reaches JavaScript. +#[doc(hidden)] +pub struct ClosureAllocation(*mut ClosureHeader); + +impl ClosureAllocation { + #[must_use] + pub fn new(callback: F, call_shim: C) -> Self { + unsafe fn drop(pointer: *mut ClosureHeader) { + // SAFETY: This function is stored only in the matching allocation. + unsafe { + mem::drop(Box::from_raw(pointer.cast::>())); + } + } + + let state = Box::new(ClosureState { + prefix: ClosurePrefix { + header: ClosureHeader::new(drop::), + call_shim, + }, + callback, + }); + Self(Box::into_raw(state).cast()) + } + + #[must_use] + pub fn data(&self) -> usize { + self.0.expose_provenance() + } + + /// Leaves this allocation under JavaScript ownership. + pub fn forget(self) { + mem::forget(self); + } +} + +impl Drop for ClosureAllocation { + fn drop(&mut self) { + // SAFETY: This guard uniquely owns the live allocation until transferred. + unsafe { ClosureHeader::release(self.0) }; + } +} + +#[unsafe(export_name = "__export_closure_drop")] +extern "C" fn closure_drop( + data_0: r#macro::FromJsSlot1, + data_1: r#macro::FromJsSlot2, + data_2: r#macro::FromJsSlot3, + data_3: r#macro::FromJsSlot4, +) { + let data = r#macro::join_from_js::(data_0, data_1, data_2, data_3); + if data == 0 { + return; + } + + // SAFETY: JavaScript owns one live reference to the allocation until it + // invokes this function, and clears `state.data` before doing so. + unsafe { + ClosureHeader::release(ptr::with_exposed_provenance_mut::(data)); + } +} + +js_bindgen::unsafe_global_wat! { + "{}", + interpolate r#macro::wat_export!( + "__export_closure_drop", + "closure_drop", + (("data", usize)), + ), +} + +js_bindgen::export_js! { + module = "js_sys", + name = "closure_drop", + required_embeds = [ + r#macro::js_from_embed::(), + ], + "{}", + interpolate r#macro::js_export!( + "closure_drop", + (("data", usize)), + ), +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "closure.unref", + "(callback) => callback.unref()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "closure.finalization", + "typeof FinalizationRegistry === 'undefined'", + " ? {{ register: () => {{}}, unregister: () => {{}} }}", + " : new FinalizationRegistry(state => {{", + // The `unref` function can outlive its callback. Clear the pointer before + // releasing it so a later call cannot release the allocation twice. + " const data = state.data", + " state.data = 0", + " if (data) this.#jsExports.closure_drop(data)", + " }})", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "closure.own", + required_embeds = [("js_sys", "closure.finalization")], + "(callback, state) => {{", + " callback.unref = () => {{", + " state.references -= 1", + " if (state.references === 0) {{", + " const data = state.data", + " state.data = 0", + " this.#jsEmbed.js_sys['closure.finalization'].unregister(state)", + " if (data) this.#jsExports.closure_drop(data)", + " }}", + " }}", + " this.#jsEmbed.js_sys['closure.finalization'].register(", + " callback, state, state", + " )", + " return callback", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "closure.make", + required_embeds = [("js_sys", "closure.own")], + "(data, call) => {{", + " const state = {{ data, references: 1 }}", + " const callback = (...args) => {{", + " if (!state.data) {{", + " throw new Error('closure invoked after being dropped')", + " }}", + " state.references += 1", + " try {{", + " return call(state.data, ...args)", + " }} finally {{", + " callback.unref()", + " }}", + " }}", + " return this.#jsEmbed.js_sys['closure.own'](callback, state)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "closure.make_mut", + required_embeds = [("js_sys", "closure.own")], + "(data, call) => {{", + " const state = {{ data, references: 1 }}", + " const callback = (...args) => {{", + " if (!state.data) {{", + " throw new Error('closure invoked recursively or after being dropped')", + " }}", + " state.references += 1", + " const data = state.data", + " state.data = 0", + " try {{", + " return call(data, ...args)", + " }} finally {{", + " state.data = data", + " callback.unref()", + " }}", + " }}", + " return this.#jsEmbed.js_sys['closure.own'](callback, state)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "closure.make_once", + required_embeds = [("js_sys", "closure.own")], + "(data, call) => {{", + " const state = {{ data, references: 1, called: false }}", + " const callback = (...args) => {{", + " if (!state.data) {{", + " throw new Error('closure invoked recursively or after being dropped')", + " }}", + " if (state.called) {{", + " throw new Error('FnOnce called more than once')", + " }}", + " state.called = true", + " state.references += 1", + " const data = state.data", + " state.data = 0", + " try {{", + " return call(data, ...args)", + " }} finally {{", + " state.data = data", + " callback.unref()", + " }}", + " }}", + " return this.#jsEmbed.js_sys['closure.own'](callback, state)", + "}}", +); + +/// An owned Rust closure exposed as a JavaScript function. +#[repr(transparent)] +pub struct Closure { + value: JsValue, + _type: PhantomData>, +} + +impl Closure { + #[must_use] + pub fn as_js_value(&self) -> &JsValue { + &self.value + } + + #[doc(hidden)] + #[must_use] + pub fn from_js_value(value: JsValue) -> Self { + Self { + value, + _type: PhantomData, + } + } + + /// Transfers this closure to JavaScript ownership. + /// + /// When supported by the JavaScript runtime, the captured Rust values are + /// released after the JavaScript function becomes unreachable. Otherwise, + /// the Rust allocation remains alive. + #[must_use] + pub fn into_js_value(self) -> JsValue { + let this = ManuallyDrop::new(self); + // SAFETY: `this` will not run `Closure::drop`, and `value` is moved out + // exactly once into the returned owner. + unsafe { ptr::read(&raw const this.value) } + } + + /// Leaves this closure under JavaScript ownership permanently. + /// + /// Prefer [`Closure::into_js_value`] when the JavaScript function can be + /// retained as a [`JsValue`]. + pub fn forget(self) { + mem::forget(self); + } +} + +impl AsRef for Closure { + fn as_ref(&self) -> &JsValue { + self.as_js_value() + } +} + +// SAFETY: This delegates to the borrowed conversion of the underlying +// `JsValue`; ownership of the callback remains with `Closure`. +unsafe impl<'a, T: ?Sized> IntoJS for &'a Closure { + const JS_CONV: Option = <&'a JsValue as IntoJS>::JS_CONV; + + type Abi = <&'a JsValue as IntoJS>::Abi; + + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(&self.value) + } +} + +// SAFETY: The owned `JsValue` is transferred to JavaScript. The callback's +// finalization registry owns the corresponding Rust closure allocation. +unsafe impl IntoJS for Closure { + const JS_CONV: Option = ::JS_CONV; + + type Abi = ::Abi; + + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(self.into_js_value()) + } +} + +impl Drop for Closure { + fn drop(&mut self) { + closure::closure_unref(&self.value); + } +} diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index f7b93f7e..9a89cd96 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -11,6 +11,7 @@ extern crate alloc; mod util; mod array; mod bigint; +mod closure; mod exception; mod externref; pub mod hazard; @@ -26,10 +27,11 @@ mod value; pub use js_bindgen; #[cfg(feature = "macro")] -pub use js_sys_macro::js_sys; +pub use js_sys_macro::{closure, js_sys}; pub use crate::array::{JsArray, TryFromJsArrayError}; pub use crate::bigint::JsBigInt; +pub use crate::closure::Closure; pub use crate::number::JsNumber; pub use crate::panic::{UnwrapThrowExt, panic}; pub use crate::string::JsString; diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index 5d8ee6fc..37023bd6 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -6,11 +6,15 @@ mod text; mod wat; mod wat_import; +pub use alloc::boxed::Box; + pub use abi::*; pub use result::*; pub use text::*; pub use wat::*; +// Closure macro runtime. +pub use crate::closure::{ClosureAllocation, ClosureHeader}; // Text rendering. pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; // JavaScript export shims. diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs index bc2f772e..f7ef54eb 100644 --- a/client/js-sys/src/macro/export.rs +++ b/client/js-sys/src/macro/export.rs @@ -276,12 +276,12 @@ macro_rules! js_export_output_expression { #[macro_export] macro_rules! js_export { ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - $($crate::r#macro::validate_from_js::<$input>();)* const PARAMETERS: &::core::primitive::str = $crate::r#macro::js_export_parameters!($(($par, $input)),*); const ARGUMENTS: &::core::primitive::str = $crate::r#macro::js_export_arguments!($(($par, $input)),*); + $($crate::r#macro::validate_from_js::<$input>();)* $crate::r#macro::const_concat!( "(", PARAMETERS, @@ -293,8 +293,6 @@ macro_rules! js_export { ) }}; ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - $($crate::r#macro::validate_from_js::<$input>();)* - $crate::r#macro::validate_return_into_js::<$output>(); const PARAMETERS: &::core::primitive::str = $crate::r#macro::js_export_parameters!($(($par, $input)),*); const ARGUMENTS: &::core::primitive::str = @@ -333,6 +331,8 @@ macro_rules! js_export { "" }; + $($crate::r#macro::validate_from_js::<$input>();)* + $crate::r#macro::validate_return_into_js::<$output>(); $crate::r#macro::const_concat!( "(", PARAMETERS, diff --git a/host/cli-lib/src/js/imports.mjs b/host/cli-lib/src/js/imports.mjs index cf036a5f..7e7a7fec 100644 --- a/host/cli-lib/src/js/imports.mjs +++ b/host/cli-lib/src/js/imports.mjs @@ -7,6 +7,8 @@ export class JsBindgen { // @ts-expect-error: Used in placeholder. // eslint-disable-next-line no-unused-private-class-members, @typescript-eslint/no-explicit-any #jsEmbed; + // @ts-expect-error: Used by generated closure factories. + #jsExports; // @ts-expect-error: Used in placeholder. // eslint-disable-next-line no-unused-private-class-members #memory; @@ -60,8 +62,8 @@ export class JsBindgen { this.#finished = true; // Export wrappers generated by `js-sys` use this stable binding. const wasmExports = instance.exports; - const jsExports = JBG_PLACEHOLDER_JS_EXPORT; - const exports = Object.assign(Object.create(null), wasmExports, jsExports); + this.#jsExports = JBG_PLACEHOLDER_JS_EXPORT; + const exports = Object.assign(Object.create(null), wasmExports, this.#jsExports); return { instance, exports, diff --git a/host/cli-lib/src/js/imports.mts b/host/cli-lib/src/js/imports.mts index d33b07e9..52888220 100644 --- a/host/cli-lib/src/js/imports.mts +++ b/host/cli-lib/src/js/imports.mts @@ -18,6 +18,8 @@ export class JsBindgen { // @ts-expect-error: Used in placeholder. // eslint-disable-next-line no-unused-private-class-members, @typescript-eslint/no-explicit-any #jsEmbed: Record> + // @ts-expect-error: Used by generated closure factories. + #jsExports: WebAssembly.Instance["exports"] // @ts-expect-error: Used in placeholder. // eslint-disable-next-line no-unused-private-class-members #memory: WebAssembly.Memory @@ -81,11 +83,11 @@ export class JsBindgen { // Export wrappers generated by `js-sys` use this stable binding. const wasmExports = instance.exports - const jsExports = JBG_PLACEHOLDER_JS_EXPORT + this.#jsExports = JBG_PLACEHOLDER_JS_EXPORT const exports = Object.assign( Object.create(null) as WebAssembly.Instance["exports"], wasmExports, - jsExports + this.#jsExports ) return { instance, diff --git a/host/dev/src/client/e2e.rs b/host/dev/src/client/e2e.rs index 5e292964..1bc6ee6f 100644 --- a/host/dev/src/client/e2e.rs +++ b/host/dev/src/client/e2e.rs @@ -104,6 +104,9 @@ impl E2e { command.args(["run", "--allow-read"]); } Engine::NodeJs => { + if example.name == "closure" { + command.arg("--expose-gc"); + } command.args(node_js_arg); } Engine::Bun => { diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs new file mode 100644 index 00000000..4148ffcf --- /dev/null +++ b/host/js-sys-bindgen/src/closure.rs @@ -0,0 +1,292 @@ +use std::env; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote_spanned}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::{ + Error, Expr, Path, PathArguments, ReturnType, Token, TraitBound, TraitBoundModifier, Type, + TypeParamBound, TypeTraitObject, parse_quote_spanned, +}; + +mod keyword { + syn::custom_keyword!(js_sys); +} + +pub fn closure(input: TokenStream, id: usize) -> Result { + let crate_name = env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found"); + closure_with_crate_name(input, id, &crate_name) +} + +fn closure_with_crate_name( + input: TokenStream, + id: usize, + crate_name: &str, +) -> Result { + let ClosureInput { + js_sys, + trait_object, + expression, + } = syn::parse2(input)?; + let signature = Signature::parse(&trait_object)?; + let span = trait_object.span(); + let js_sys = js_sys.unwrap_or_else(|| parse_quote_spanned!(span=> ::js_sys)); + let symbol_id = format!("{}_{}", crate_name.replace('-', "_"), id); + let call_ident = format_ident!("closure_call_{symbol_id}", span = span); + let factory_ident = format_ident!("closure_new_{symbol_id}", span = span); + let factory_name = syn::LitStr::new(&format!("closure.new.{symbol_id}"), span); + let factory_embed = syn::LitStr::new(signature.kind.factory_embed(), span); + let closure = signature.closure_type(&trait_object); + let factory_js = syn::LitStr::new( + &format!( + "(data) => this.#jsEmbed.js_sys['{}'](data, this.#jsExports['{call_ident}'])", + signature.kind.factory_embed(), + ), + span, + ); + let inputs: Vec<_> = signature.inputs.iter().collect(); + let arguments: Vec<_> = inputs + .iter() + .enumerate() + .map(|(index, ty)| format_ident!("arg{index}", span = ty.span())) + .collect(); + let output = &signature.output; + let output_decl = if signature.returns_unit { + TokenStream::new() + } else { + quote_spanned!(output.span()=> -> #output) + }; + let call_shim_type = format_ident!("ClosureCallShim{id}", span = span); + let call_impl = format_ident!("closure_call_impl_{symbol_id}", span = span); + let allocate = format_ident!("closure_alloc_{symbol_id}", span = span); + let closure_bound = if signature.kind == ClosureKind::Shared { + quote_spanned!(span=> ::core::ops::Fn(#(#inputs),*) -> #output) + } else { + quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*) -> #output) + }; + let call_body = if signature.kind == ClosureKind::Shared { + quote_spanned! {span=> + let callback = unsafe { + &*#js_sys::r#macro::ClosureHeader::callback::(pointer) + }; + callback(#(#arguments),*) + } + } else { + quote_spanned! {span=> + let callback = unsafe { + &mut *#js_sys::r#macro::ClosureHeader::callback::(pointer) + }; + callback(#(#arguments),*) + } + }; + let expression = if signature.kind == ClosureKind::Once { + quote_spanned! {expression.span()=> + { + let mut callback = ::core::option::Option::Some(#expression); + move |#(#arguments),*| { + let callback = ::core::option::Option::take(&mut callback) + .expect("FnOnce called more than once"); + callback(#(#arguments),*) + } + } + } + } else { + quote_spanned!(expression.span()=> #expression) + }; + + Ok(quote_spanned! {span=> + { + type #call_shim_type = unsafe fn( + *mut #js_sys::r#macro::ClosureHeader, + #(#inputs),* + ) #output_decl; + + #[allow(clippy::undocumented_unsafe_blocks)] + unsafe fn #call_impl( + pointer: *mut #js_sys::r#macro::ClosureHeader, + #(#arguments: #inputs),* + ) #output_decl + where + F: #closure_bound, + { + #call_body + } + + fn #allocate( + callback: F, + ) -> #js_sys::r#macro::ClosureAllocation + where + F: #closure_bound + 'static, + { + #js_sys::r#macro::ClosureAllocation::new( + callback, + #call_impl:: as #call_shim_type, + ) + } + + #[#js_sys::js_sys(js_sys = #js_sys)] + #[allow(clippy::undocumented_unsafe_blocks)] + fn #call_ident( + data: ::core::primitive::usize, + #(#arguments: #inputs),* + ) #output_decl { + let pointer = ::core::ptr::with_exposed_provenance_mut(data); + let call_shim = unsafe { + #js_sys::r#macro::ClosureHeader::call_shim::<#call_shim_type>(pointer) + }; + unsafe { call_shim(pointer.cast(), #(#arguments),*) } + } + + #js_sys::js_bindgen::embed_js! { + module = #crate_name, + name = #factory_name, + required_embeds = [("js_sys", #factory_embed)], + #factory_js, + } + + #[#js_sys::js_sys(js_sys = #js_sys)] + extern "js-sys" { + #[js_sys(js_embed = #factory_name)] + fn #factory_ident( + data: ::core::primitive::usize, + ) -> #js_sys::JsValue; + } + + let allocation = #allocate(#expression); + let value = #factory_ident(allocation.data()); + allocation.forget(); + #js_sys::Closure::<#closure>::from_js_value(value) + } + }) +} + +struct ClosureInput { + js_sys: Option, + trait_object: TypeTraitObject, + expression: Expr, +} + +impl Parse for ClosureInput { + fn parse(input: ParseStream<'_>) -> syn::Result { + let js_sys = if input.peek(keyword::js_sys) && input.peek2(Token![=]) { + input.parse::()?; + input.parse::()?; + let path = input.parse()?; + input.parse::()?; + Some(path) + } else { + None + }; + let trait_object = input.parse()?; + input.parse::()?; + let expression = input.parse()?; + + if input.is_empty() { + Ok(Self { + js_sys, + trait_object, + expression, + }) + } else { + Err(input.error("unexpected tokens after closure expression")) + } + } +} + +struct Signature { + kind: ClosureKind, + inputs: Punctuated, + output: Type, + returns_unit: bool, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum ClosureKind { + Shared, + Mutable, + Once, +} + +impl ClosureKind { + fn parse(bound: &syn::Ident) -> Result { + match bound.to_string().as_str() { + "Fn" => Ok(Self::Shared), + "FnMut" => Ok(Self::Mutable), + "FnOnce" => Ok(Self::Once), + _ => Err(Error::new_spanned( + bound, + "expected `Fn`, `FnMut`, or `FnOnce`", + )), + } + } + + fn factory_embed(self) -> &'static str { + match self { + Self::Shared => "closure.make", + Self::Mutable => "closure.make_mut", + Self::Once => "closure.make_once", + } + } +} + +impl Signature { + fn parse(trait_object: &TypeTraitObject) -> Result { + if trait_object.bounds.len() != 1 { + return Err(Error::new_spanned( + trait_object, + "expected exactly one closure trait", + )); + } + + let Some(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path, + })) = trait_object.bounds.first() + else { + return Err(Error::new_spanned( + trait_object, + "expected `dyn Fn(...)`, `dyn FnMut(...)`, or `dyn FnOnce(...)`", + )); + }; + let Some(segment) = path.segments.last() else { + return Err(Error::new_spanned(path, "expected a closure trait")); + }; + let kind = ClosureKind::parse(&segment.ident)?; + + let PathArguments::Parenthesized(arguments) = &segment.arguments else { + return Err(Error::new_spanned( + &segment.arguments, + "expected parenthesized closure arguments", + )); + }; + let output: Type = match &arguments.output { + ReturnType::Default => parse_quote_spanned!(trait_object.span()=> ()), + ReturnType::Type(_, output) => *output.clone(), + }; + let returns_unit = matches!(&output, Type::Tuple(tuple) if tuple.elems.is_empty()); + + Ok(Self { + kind, + inputs: arguments.inputs.clone(), + output, + returns_unit, + }) + } + + fn closure_type(&self, trait_object: &TypeTraitObject) -> TypeTraitObject { + let mut closure = trait_object.clone(); + if self.kind == ClosureKind::Once { + let Some(TypeParamBound::Trait(bound)) = closure.bounds.first_mut() else { + unreachable!("validated closure trait"); + }; + let Some(segment) = bound.path.segments.last_mut() else { + unreachable!("validated closure trait path"); + }; + segment.ident = format_ident!("FnMut", span = segment.ident.span()); + } + closure + } +} diff --git a/host/js-sys-bindgen/src/lib.rs b/host/js-sys-bindgen/src/lib.rs index 2bd68b1e..f161613e 100644 --- a/host/js-sys-bindgen/src/lib.rs +++ b/host/js-sys-bindgen/src/lib.rs @@ -1,4 +1,6 @@ #[cfg(feature = "macro")] +mod closure; +#[cfg(feature = "macro")] mod export; #[cfg(feature = "file")] mod file; @@ -16,6 +18,8 @@ pub use proc_macro2; pub use quote; pub use syn; +#[cfg(feature = "macro")] +pub use crate::closure::closure; #[cfg(feature = "file")] pub use crate::file::file; pub use crate::function::{Function, FunctionJsOutput}; diff --git a/host/js-sys-macro/src/lib.rs b/host/js-sys-macro/src/lib.rs index e6c12fd9..069b62c0 100644 --- a/host/js-sys-macro/src/lib.rs +++ b/host/js-sys-macro/src/lib.rs @@ -1,5 +1,17 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use js_sys_bindgen::syn::Error; use proc_macro::TokenStream; +static CLOSURE_ID: AtomicUsize = AtomicUsize::new(0); + +#[proc_macro] +pub fn closure(input: TokenStream) -> TokenStream { + js_sys_bindgen::closure(input.into(), CLOSURE_ID.fetch_add(1, Ordering::Relaxed)) + .unwrap_or_else(Error::into_compile_error) + .into() +} + #[proc_macro_attribute] pub fn js_sys(attr: TokenStream, item: TokenStream) -> TokenStream { js_sys_bindgen::r#macro(attr.into(), item.into(), None) diff --git a/host/macro/src/tests/import_js.rs b/host/macro/src/tests/import_js.rs index 2276f271..2fe8a42d 100644 --- a/host/macro/src/tests/import_js.rs +++ b/host/macro/src/tests/import_js.rs @@ -306,6 +306,19 @@ fn required_embeds_multiple() { }); } +#[test] +fn required_embeds_function_trait() { + crate::import_js_internal(quote! { + module = "foo", + name = "bar", + required_embeds = [ + js_input_embed::<&Closure i32>>(), + ], + "", + }) + .unwrap(); +} + #[test] fn required_embeds_empty() { let output = crate::import_js_internal(quote! { diff --git a/host/macro/src/util.rs b/host/macro/src/util.rs index 002e2efd..a7ac7d8b 100644 --- a/host/macro/src/util.rs +++ b/host/macro/src/util.rs @@ -550,15 +550,20 @@ fn parse_angular( let mut angular: TokenStream = iter::once(TokenTree::from(opening)).collect(); let mut opened = 1; + let mut previous_joint_hyphen = false; for tok in &mut stream { span.end = tok.span(); match &tok { - TokenTree::Punct(p) if p.as_char() == '>' => opened -= 1, + TokenTree::Punct(p) if p.as_char() == '>' && !previous_joint_hyphen => opened -= 1, TokenTree::Punct(p) if p.as_char() == '<' => opened += 1, _ => (), } + previous_joint_hyphen = matches!( + &tok, + TokenTree::Punct(p) if p.as_char() == '-' && p.spacing() == Spacing::Joint + ); angular.extend(iter::once(tok)); From 9abf1236e1ad1e965640cdba467ba178717f017b Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:04:52 +0800 Subject: [PATCH 06/21] Expand js_sys binding generation --- benchmarks/Cargo.lock | 7 + benchmarks/js-bindgen/Cargo.toml | 2 +- client/e2e/Cargo.toml | 2 +- client/js-sys/Cargo.toml | 5 +- client/js-sys/build.rs | 66 -- client/js-sys/src/array/array.gen.rs | 346 -------- client/js-sys/src/array/array.js-sys.rs | 37 - client/js-sys/src/bigint/bigint.gen.rs | 31 - client/js-sys/src/bigint/mod.rs | 5 - .../src/{array/mod.rs => builtins/array.rs} | 93 +- .../bigint.js-sys.rs => builtins/bigint.rs} | 2 +- client/js-sys/src/builtins/error.rs | 79 ++ client/js-sys/src/builtins/mod.rs | 13 + .../number.js-sys.rs => builtins/number.rs} | 2 +- client/js-sys/src/builtins/object.rs | 18 + .../src/{string/mod.rs => builtins/string.rs} | 70 +- client/js-sys/src/closure/closure.gen.rs | 39 - client/js-sys/src/closure/closure.js-sys.rs | 7 - client/js-sys/src/hazard.rs | 4 +- client/js-sys/src/lib.rs | 28 +- client/js-sys/src/macro.rs | 4 +- client/js-sys/src/macro/import.rs | 380 ++++++++ client/js-sys/src/macro/import/js.rs | 506 +++++++++++ client/js-sys/src/macro/import/wat.rs | 755 ++++++++++++++++ client/js-sys/src/macro/import/writer.rs | 102 +++ client/js-sys/src/macro/js_import.rs | 12 +- client/js-sys/src/macro/result.rs | 4 +- client/js-sys/src/macro/text.rs | 119 +-- client/js-sys/src/number/mod.rs | 5 - client/js-sys/src/number/number.gen.rs | 35 - .../{closure/mod.rs => runtime/closure.rs} | 55 +- client/js-sys/src/{ => runtime}/exception.rs | 4 +- client/js-sys/src/{ => runtime}/externref.rs | 2 +- client/js-sys/src/runtime/mod.rs | 11 + client/js-sys/src/{ => runtime}/panic.rs | 0 .../src/{value/mod.rs => runtime/value.rs} | 16 +- client/js-sys/src/string/string.gen.rs | 287 ------ client/js-sys/src/string/string.js-sys.rs | 26 - client/js-sys/src/value/value.gen.rs | 54 -- client/js-sys/src/value/value.js-sys.rs | 8 - client/test/Cargo.toml | 2 +- client/wabii/src/lib.rs | 12 +- client/wabii/src/random.64.wat | 4 + client/wabii/src/random.wat | 4 + client/wabii/src/stdio.64.wat | 4 + client/wabii/src/stdio.wat | 4 + client/wabii/src/time.wat | 4 + client/web-sys/src/console.gen.rs | 135 +-- host/dev/src/codegen.rs | 18 +- host/js-sys-bindgen/Cargo.toml | 4 +- host/js-sys-bindgen/src/closure.rs | 120 ++- host/js-sys-bindgen/src/export.rs | 32 +- host/js-sys-bindgen/src/file.rs | 9 +- host/js-sys-bindgen/src/function.rs | 820 +++++++++--------- host/js-sys-bindgen/src/function/js.rs | 145 ++++ host/js-sys-bindgen/src/function/options.rs | 167 ++++ host/js-sys-bindgen/src/hygiene.rs | 125 +-- host/js-sys-bindgen/src/lib.rs | 10 - host/js-sys-bindgen/src/macro.rs | 306 +++++-- host/js-sys-bindgen/src/tests/closure.rs | 57 ++ .../src/tests/macro/function.rs | 607 +++++++++---- host/js-sys-bindgen/src/tests/macro/member.rs | 457 +++++++--- host/js-sys-bindgen/src/tests/macro/mod.rs | 33 +- host/js-sys-bindgen/src/tests/macro/type.rs | 102 +++ host/js-sys-bindgen/src/tests/mod.rs | 2 +- host/js-sys-bindgen/src/tests/type.rs | 79 +- host/js-sys-bindgen/src/type.rs | 150 +++- host/js-sys-bindgen/src/web_idl.rs | 3 +- host/js-sys-macro/Cargo.toml | 2 +- host/js-sys-macro/src/lib.rs | 8 +- host/ld-shared/src/lib.rs | 101 ++- host/ld/src/post.rs | 4 +- host/ld/src/pre.rs | 6 +- host/macro/src/custom_section.rs | 164 ++-- host/macro/src/lib.rs | 20 +- host/macro/src/tests/global_wat.rs | 147 +++- host/macro/src/tests/import_js.rs | 48 +- web/playground/Cargo.toml | 4 +- web/playground/src/main.rs | 4 + 79 files changed, 4936 insertions(+), 2228 deletions(-) delete mode 100644 client/js-sys/build.rs delete mode 100644 client/js-sys/src/array/array.gen.rs delete mode 100644 client/js-sys/src/array/array.js-sys.rs delete mode 100644 client/js-sys/src/bigint/bigint.gen.rs delete mode 100644 client/js-sys/src/bigint/mod.rs rename client/js-sys/src/{array/mod.rs => builtins/array.rs} (74%) rename client/js-sys/src/{bigint/bigint.js-sys.rs => builtins/bigint.rs} (54%) create mode 100644 client/js-sys/src/builtins/error.rs create mode 100644 client/js-sys/src/builtins/mod.rs rename client/js-sys/src/{number/number.js-sys.rs => builtins/number.rs} (59%) create mode 100644 client/js-sys/src/builtins/object.rs rename client/js-sys/src/{string/mod.rs => builtins/string.rs} (67%) delete mode 100644 client/js-sys/src/closure/closure.gen.rs delete mode 100644 client/js-sys/src/closure/closure.js-sys.rs create mode 100644 client/js-sys/src/macro/import.rs create mode 100644 client/js-sys/src/macro/import/js.rs create mode 100644 client/js-sys/src/macro/import/wat.rs create mode 100644 client/js-sys/src/macro/import/writer.rs delete mode 100644 client/js-sys/src/number/mod.rs delete mode 100644 client/js-sys/src/number/number.gen.rs rename client/js-sys/src/{closure/mod.rs => runtime/closure.rs} (90%) rename client/js-sys/src/{ => runtime}/exception.rs (98%) rename client/js-sys/src/{ => runtime}/externref.rs (99%) create mode 100644 client/js-sys/src/runtime/mod.rs rename client/js-sys/src/{ => runtime}/panic.rs (100%) rename client/js-sys/src/{value/mod.rs => runtime/value.rs} (96%) delete mode 100644 client/js-sys/src/string/string.gen.rs delete mode 100644 client/js-sys/src/string/string.js-sys.rs delete mode 100644 client/js-sys/src/value/value.gen.rs delete mode 100644 client/js-sys/src/value/value.js-sys.rs create mode 100644 host/js-sys-bindgen/src/function/js.rs create mode 100644 host/js-sys-bindgen/src/function/options.rs create mode 100644 host/js-sys-bindgen/src/tests/closure.rs diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock index a38b7956..774ec0ac 100644 --- a/benchmarks/Cargo.lock +++ b/benchmarks/Cargo.lock @@ -77,6 +77,7 @@ dependencies = [ "proc-macro2", "quote", "syn", + "xxhash-rust", ] [[package]] @@ -185,3 +186,9 @@ checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" diff --git a/benchmarks/js-bindgen/Cargo.toml b/benchmarks/js-bindgen/Cargo.toml index 9baaefbf..17c18faa 100644 --- a/benchmarks/js-bindgen/Cargo.toml +++ b/benchmarks/js-bindgen/Cargo.toml @@ -8,4 +8,4 @@ publish = { workspace = true } crate-type = ["cdylib"] [dependencies] -js-sys = { path = "../../client/js-sys", features = ["macro"] } +js-sys = { path = "../../client/js-sys" } diff --git a/client/e2e/Cargo.toml b/client/e2e/Cargo.toml index 5847a46d..b50a2c9b 100644 --- a/client/e2e/Cargo.toml +++ b/client/e2e/Cargo.toml @@ -6,7 +6,7 @@ license = { workspace = true } publish = false [dev-dependencies] -js-sys = { workspace = true, features = ["macro"] } +js-sys = { workspace = true } [lints] workspace = true diff --git a/client/js-sys/Cargo.toml b/client/js-sys/Cargo.toml index 3ed0d20b..508d7690 100644 --- a/client/js-sys/Cargo.toml +++ b/client/js-sys/Cargo.toml @@ -12,15 +12,12 @@ test = false [dependencies] js-bindgen = { workspace = true } -js-sys-macro = { workspace = true, optional = true } +js-sys-macro = { workspace = true } [dev-dependencies] js-bindgen-test = { workspace = true } paste = { workspace = true } web-sys = { workspace = true } -[features] -macro = ["dep:js-sys-macro"] - [lints] workspace = true diff --git a/client/js-sys/build.rs b/client/js-sys/build.rs deleted file mode 100644 index d058f7a8..00000000 --- a/client/js-sys/build.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! This file is not shipped to Crates.io, but it is present when depending on -//! `js-sys` via `git` or `path`. - -use std::io::ErrorKind; -use std::path::Path; -use std::process::Command; -use std::{env, fs, panic, process}; - -fn main() { - if option_env!("JBG_DEV").is_none_or(|value| value != "1") - || option_env!("CI").is_some_and(|value| value == "true") - { - return; - } - - if search_dir(&env::current_dir().unwrap(), false) { - let status = Command::new("cargo") - .env_remove("CARGO_ENCODED_RUSTFLAGS") - .current_dir("../../host") - .arg("+stable") - .arg("run") - .args(["-p", "cargo-js-sys"]) - .arg("--") - .arg("-q") - .arg("js-sys") - .args(["--manifest-path", "../client/js-sys/Cargo.toml"]) - .status() - .unwrap(); - - if !status.success() { - process::exit(status.code().unwrap_or(1)) - } - } -} - -fn search_dir(dir: &Path, mut any: bool) -> bool { - for entry in fs::read_dir(dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - - if path.is_file() && path.as_os_str().as_encoded_bytes().ends_with(b".js-sys.rs") { - println!("cargo::rerun-if-changed={}", path.display()); - - if !any { - let r#gen = path.with_extension("").with_extension("gen.rs"); - - match fs::metadata(r#gen) { - Ok(meta) => { - let gen_mtime = meta.modified().unwrap(); - let js_sys_mtime = fs::metadata(&path).unwrap().modified().unwrap(); - - if gen_mtime < js_sys_mtime { - any = true; - } - } - Err(error) if error.kind() == ErrorKind::NotFound => any = true, - Err(error) => panic::panic_any(error), - } - } - } else if path.is_dir() { - any |= search_dir(&path, any); - } - } - - any -} diff --git a/client/js-sys/src/array/array.gen.rs b/client/js-sys/src/array/array.gen.rs deleted file mode 100644 index a3748d59..00000000 --- a/client/js-sys/src/array/array.gen.rs +++ /dev/null @@ -1,346 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use core::marker::PhantomData; -use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{IntoJS, JsCast}; -use crate::util::{PtrConst, PtrLength, PtrMut}; - -#[repr(transparent)] -pub struct JsArray { - value: JsValue, - _type: PhantomData, -} - -impl AsRef for JsArray { - fn as_ref(&self) -> &JsValue { - &self.value - } -} - -impl From> for JsValue { - fn from(value: JsArray) -> Self { - value.value - } -} - -unsafe impl JsCast for JsArray {} - -unsafe impl IntoJS for JsArray { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } -} - -impl JsArray { - pub fn length(self: &JsArray) -> u32 { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "length", shim = - "js_sys.length", inputs = [("arg0", & JsValue)], output = u32,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "length", - required_embeds = [ - r#macro::js_input_embed::<&JsValue>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = r#macro::js_function!("(", ") => ", ("arg0", & JsValue)), direct_call - = "arg0_0.length", indirect_call = "arg0_0.length", inputs = [("arg0", & JsValue)], - output = u32, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.length"] - fn length( - arg0_0: r#macro::InputSlot1<&JsValue>, - arg0_1: r#macro::InputSlot2<&JsValue>, - arg0_2: r#macro::InputSlot3<&JsValue>, - arg0_3: r#macro::InputSlot4<&JsValue>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { - r#macro::split_input_as::<&JsValue>(self) - }; - unsafe { length(arg0_0, arg0_1, arg0_2, arg0_3) } - }) - } -} - -pub(super) unsafe fn array_js_value_decode( - array: PtrConst, - len: PtrLength, -) -> JsArray { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_js_value_decode", - shim = "js_sys.array_js_value_decode", inputs = [("arg0", PtrConst < JsValue >), ("arg1", - PtrLength < JsValue >)], output = JsArray < JsValue >,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "array_js_value_decode", - required_embeds = [ - ("js_sys", "array.js_value.decode"), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_output_embed::>(), - r#macro::js_result_embed::>(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['array.js_value.decode']", - indirect_call = "this.#jsEmbed.js_sys['array.js_value.decode'](arg0_0, arg1_0)", inputs - = [("arg0", PtrConst < JsValue >), ("arg1", PtrLength < JsValue >)], output = JsArray < - JsValue >, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.array_js_value_decode"] - fn array_js_value_decode( - arg0_0: r#macro::InputSlot1>, - arg0_1: r#macro::InputSlot2>, - arg0_2: r#macro::InputSlot3>, - arg0_3: r#macro::InputSlot4>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - ) -> r#macro::OutputRet>; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::>(array); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(len); - unsafe { - array_js_value_decode(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) - } - }) -} - -pub(super) unsafe fn array_js_value_encode( - array: &JsArray, - array_ptr: PtrMut, - array_len: PtrLength, - externref_ptr: PtrConst, - externref_len: i32, -) -> bool { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_js_value_encode", - shim = "js_sys.array_js_value_encode", inputs = [("arg0", & JsArray), ("arg1", PtrMut < - JsValue >), ("arg2", PtrLength < JsValue >), ("arg3", PtrConst < i32 >), ("arg4", i32)], - output = bool,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "array_js_value_encode", - required_embeds = [ - ("js_sys", "array.js_value.encode"), - r#macro::js_input_embed::<&JsArray>(), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['array.js_value.encode']", - indirect_call = - "this.#jsEmbed.js_sys['array.js_value.encode'](arg0_0, arg1_0, arg2_0, arg3_0, arg4_0)", - inputs = [("arg0", & JsArray), ("arg1", PtrMut < JsValue >), ("arg2", PtrLength < - JsValue >), ("arg3", PtrConst < i32 >), ("arg4", i32)], output = bool, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.array_js_value_encode"] - fn array_js_value_encode( - arg0_0: r#macro::InputSlot1<&JsArray>, - arg0_1: r#macro::InputSlot2<&JsArray>, - arg0_2: r#macro::InputSlot3<&JsArray>, - arg0_3: r#macro::InputSlot4<&JsArray>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - arg2_0: r#macro::InputSlot1>, - arg2_1: r#macro::InputSlot2>, - arg2_2: r#macro::InputSlot3>, - arg2_3: r#macro::InputSlot4>, - arg3_0: r#macro::InputSlot1>, - arg3_1: r#macro::InputSlot2>, - arg3_2: r#macro::InputSlot3>, - arg3_3: r#macro::InputSlot4>, - arg4_0: r#macro::InputSlot1, - arg4_1: r#macro::InputSlot2, - arg4_2: r#macro::InputSlot3, - arg4_3: r#macro::InputSlot4, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsArray>(array); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(array_ptr); - let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::< - PtrLength, - >(array_len); - let (arg3_0, arg3_1, arg3_2, arg3_3) = r#macro::split_input::>(externref_ptr); - let (arg4_0, arg4_1, arg4_2, arg4_3) = r#macro::split_input::(externref_len); - unsafe { - array_js_value_encode( - arg0_0, - arg0_1, - arg0_2, - arg0_3, - arg1_0, - arg1_1, - arg1_2, - arg1_3, - arg2_0, - arg2_1, - arg2_2, - arg2_3, - arg3_0, - arg3_1, - arg3_2, - arg3_3, - arg4_0, - arg4_1, - arg4_2, - arg4_3, - ) - } - }) -} - -pub(super) unsafe fn array_u32_decode(array: PtrConst, len: PtrLength) -> JsArray { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_decode", shim - = "js_sys.array_u32_decode", inputs = [("arg0", PtrConst < u32 >), ("arg1", PtrLength < u32 - >)], output = JsArray < u32 >,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "array_u32_decode", - required_embeds = [ - ("js_sys", "view.getUint32"), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_output_embed::>(), - r#macro::js_result_embed::>(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['view.getUint32']", indirect_call - = "this.#jsEmbed.js_sys['view.getUint32'](arg0_0, arg1_0)", inputs = [("arg0", PtrConst - < u32 >), ("arg1", PtrLength < u32 >)], output = JsArray < u32 >, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.array_u32_decode"] - fn array_u32_decode( - arg0_0: r#macro::InputSlot1>, - arg0_1: r#macro::InputSlot2>, - arg0_2: r#macro::InputSlot3>, - arg0_3: r#macro::InputSlot4>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - ) -> r#macro::OutputRet>; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::>(array); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(len); - unsafe { array_u32_decode(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } - }) -} - -pub(super) unsafe fn array_u32_encode( - array: &JsArray, - ptr: PtrMut, - len: PtrLength, -) -> bool { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "array_u32_encode", shim - = "js_sys.array_u32_encode", inputs = [("arg0", & JsArray < u32 >), ("arg1", PtrMut < u32 - >), ("arg2", PtrLength < u32 >)], output = bool,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "array_u32_encode", - required_embeds = [ - ("js_sys", "array.u32.encode"), - r#macro::js_input_embed::<&JsArray>(), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['array.u32.encode']", - indirect_call = "this.#jsEmbed.js_sys['array.u32.encode'](arg0_0, arg1_0, arg2_0)", - inputs = [("arg0", & JsArray < u32 >), ("arg1", PtrMut < u32 >), ("arg2", PtrLength < - u32 >)], output = bool, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.array_u32_encode"] - fn array_u32_encode( - arg0_0: r#macro::InputSlot1<&JsArray>, - arg0_1: r#macro::InputSlot2<&JsArray>, - arg0_2: r#macro::InputSlot3<&JsArray>, - arg0_3: r#macro::InputSlot4<&JsArray>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - arg2_0: r#macro::InputSlot1>, - arg2_1: r#macro::InputSlot2>, - arg2_2: r#macro::InputSlot3>, - arg2_3: r#macro::InputSlot4>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsArray>(array); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(ptr); - let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::>(len); - unsafe { - array_u32_encode( - arg0_0, - arg0_1, - arg0_2, - arg0_3, - arg1_0, - arg1_1, - arg1_2, - arg1_3, - arg2_0, - arg2_1, - arg2_2, - arg2_3, - ) - } - }) -} diff --git a/client/js-sys/src/array/array.js-sys.rs b/client/js-sys/src/array/array.js-sys.rs deleted file mode 100644 index 3ce568db..00000000 --- a/client/js-sys/src/array/array.js-sys.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::util::{PtrConst, PtrLength, PtrMut}; - -#[js_sys] -extern "js-sys" { - pub type JsArray; - - #[js_sys(property)] - pub fn length(self: &JsArray) -> u32; - - #[js_sys(js_embed = "array.js_value.decode")] - pub(super) unsafe fn array_js_value_decode( - array: PtrConst, - len: PtrLength, - ) -> JsArray; - - #[js_sys(js_embed = "array.js_value.encode")] - pub(super) unsafe fn array_js_value_encode( - array: &JsArray, - array_ptr: PtrMut, - array_len: PtrLength, - externref_ptr: PtrConst, - externref_len: i32, - ) -> bool; - - #[js_sys(js_embed = "view.getUint32")] - pub(super) unsafe fn array_u32_decode( - array: PtrConst, - len: PtrLength, - ) -> JsArray; - - #[js_sys(js_embed = "array.u32.encode")] - pub(super) unsafe fn array_u32_encode( - array: &JsArray, - ptr: PtrMut, - len: PtrLength, - ) -> bool; -} diff --git a/client/js-sys/src/bigint/bigint.gen.rs b/client/js-sys/src/bigint/bigint.gen.rs deleted file mode 100644 index e1e47c2b..00000000 --- a/client/js-sys/src/bigint/bigint.gen.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use crate::JsValue; -use crate::hazard::{IntoJS, JsCast}; - -#[repr(transparent)] -pub struct JsBigInt(JsValue); - -impl AsRef for JsBigInt { - fn as_ref(&self) -> &JsValue { - &self.0 - } -} - -impl From for JsValue { - fn from(value: JsBigInt) -> Self { - value.0 - } -} - -unsafe impl JsCast for JsBigInt {} - -unsafe impl IntoJS for JsBigInt { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } -} diff --git a/client/js-sys/src/bigint/mod.rs b/client/js-sys/src/bigint/mod.rs deleted file mode 100644 index 2fc8c829..00000000 --- a/client/js-sys/src/bigint/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[rustfmt::skip] -#[path ="bigint.gen.rs"] -mod bigint; - -pub use self::bigint::JsBigInt; diff --git a/client/js-sys/src/array/mod.rs b/client/js-sys/src/builtins/array.rs similarity index 74% rename from client/js-sys/src/array/mod.rs rename to client/js-sys/src/builtins/array.rs index 02f27e1e..5b0be780 100644 --- a/client/js-sys/src/array/mod.rs +++ b/client/js-sys/src/builtins/array.rs @@ -1,16 +1,79 @@ -#[rustfmt::skip] -#[path ="array.gen.rs"] -mod array; - use core::error::Error; use core::fmt::{self, Display, Formatter}; use core::mem::MaybeUninit; use core::ptr; -pub use self::array::JsArray; +use crate::JsValue; use crate::hazard::{IntoJS, IntoJsConv, JsCast}; +use crate::runtime::externref; use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; -use crate::{JsValue, externref}; + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + pub type JsArray; + + #[js_sys(getter)] + #[must_use] + pub fn length(self: &JsArray) -> u32; + + #[js_sys(js_embed = "array.js_value.decode")] + // SAFETY: The pointer and length must describe a valid `JsValue` slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn array_js_value_decode( + array: PtrConst, + len: PtrLength, + ) -> JsArray; + + #[js_sys(js_embed = "array.js_value.encode")] + // SAFETY: Every pointer and length pair must describe its matching output slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn array_js_value_encode( + array: &JsArray, + array_ptr: PtrMut, + array_len: PtrLength, + externref_ptr: PtrConst, + externref_len: i32, + ) -> bool; + + #[js_sys(js_embed = "view.getUint32")] + // SAFETY: The pointer and length must describe a valid `u32` slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn array_u32_decode(array: PtrConst, len: PtrLength) -> JsArray; + + #[js_sys(js_embed = "array.u32.encode")] + // SAFETY: The pointer and length must describe a valid `u32` output slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn array_u32_encode(array: &JsArray, ptr: PtrMut, len: PtrLength) + -> bool; +} impl JsArray { #[must_use] @@ -67,7 +130,7 @@ impl JsArray { // SAFETY: Parameters are correct. let result = unsafe { - array::array_js_value_encode( + array_js_value_encode( self.as_any(), PtrMut::new(slice), PtrLength::new(slice), @@ -93,7 +156,7 @@ impl JsArray { // SAFETY: Parameters are correct. let result = unsafe { - array::array_js_value_encode( + array_js_value_encode( self.as_any(), PtrMut::from_uninit_slice(js_slice), PtrLength::from_uninit_slice(js_slice), @@ -118,7 +181,7 @@ impl JsArray { // SAFETY: Parameters are correct. let result = unsafe { - array::array_js_value_encode( + array_js_value_encode( self.as_any(), PtrMut::from_uninit_array(js_array), PtrLength::from_uninit_array(js_array), @@ -180,8 +243,7 @@ impl From<&[T]> for JsArray { fn from(value: &[T]) -> Self { let slice = JsValue::from_slice(value); // SAFETY: Parameters are correct. - let result = - unsafe { array::array_js_value_decode(PtrConst::new(slice), PtrLength::new(slice)) }; + let result = unsafe { array_js_value_decode(PtrConst::new(slice), PtrLength::new(slice)) }; Self::unchecked_from(result.into()) } @@ -205,8 +267,7 @@ unsafe impl IntoJS for &[T] { impl JsArray { pub fn to_slice(&self, slice: &mut [u32]) -> Result<(), TryFromJsArrayError> { // SAFETY: Parameters are correct. - let result = - unsafe { array::array_u32_encode(self, PtrMut::new(slice), PtrLength::new(slice)) }; + let result = unsafe { array_u32_encode(self, PtrMut::new(slice), PtrLength::new(slice)) }; if result { Ok(()) @@ -221,7 +282,7 @@ impl JsArray { ) -> Result<&'slice mut [u32], TryFromJsArrayError> { // SAFETY: Parameters are correct. let result = unsafe { - array::array_u32_encode( + array_u32_encode( self, PtrMut::from_uninit_slice(slice), PtrLength::from_uninit_slice(slice), @@ -242,7 +303,7 @@ impl JsArray { // SAFETY: Parameters are correct. let result = unsafe { - array::array_u32_encode( + array_u32_encode( self, PtrMut::from_uninit_array(&mut array), PtrLength::from_uninit_array(&array), @@ -273,7 +334,7 @@ js_bindgen::embed_js!( impl From<&[u32]> for JsArray { fn from(value: &[u32]) -> Self { // SAFETY: Parameters are correct. - unsafe { array::array_u32_decode(PtrConst::new(value), PtrLength::new(value)) } + unsafe { array_u32_decode(PtrConst::new(value), PtrLength::new(value)) } } } diff --git a/client/js-sys/src/bigint/bigint.js-sys.rs b/client/js-sys/src/builtins/bigint.rs similarity index 54% rename from client/js-sys/src/bigint/bigint.js-sys.rs rename to client/js-sys/src/builtins/bigint.rs index eadff82e..40d2e235 100644 --- a/client/js-sys/src/bigint/bigint.js-sys.rs +++ b/client/js-sys/src/builtins/bigint.rs @@ -1,4 +1,4 @@ -#[js_sys] +#[crate::js_sys(js_sys = crate)] extern "js-sys" { pub type JsBigInt; } diff --git a/client/js-sys/src/builtins/error.rs b/client/js-sys/src/builtins/error.rs new file mode 100644 index 00000000..4e893e9e --- /dev/null +++ b/client/js-sys/src/builtins/error.rs @@ -0,0 +1,79 @@ +use super::object::Object; +use crate::hazard::JsCast; +use crate::{JsString, JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error#cause) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type ErrorOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) + #[must_use] + #[js_sys(getter)] + pub fn get_cause(self: &ErrorOptions) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) + #[js_sys(setter)] + pub fn set_cause(self: &ErrorOptions, cause: &JsValue); +} + +impl ErrorOptions { + /// Construct a new `ErrorOptions` dictionary with the given `cause`. + #[must_use] + pub fn new(cause: &JsValue) -> Self { + let ret: Self = JsCast::unchecked_from(Object::new().into()); + ret.set_cause(cause); + ret + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type Error; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error) + #[must_use] + #[js_sys(constructor)] + pub fn new(message: &str) -> Error; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_options(message: &str, options: &ErrorOptions) -> Error; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) + #[must_use] + #[js_sys(getter)] + pub fn cause(self: &Error) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) + #[js_sys(setter)] + pub fn set_cause(self: &Error, cause: &JsValue); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message) + #[must_use] + #[js_sys(getter)] + pub fn message(self: &Error) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message) + #[js_sys(setter)] + pub fn set_message(self: &Error, message: &str); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/name) + #[must_use] + #[js_sys(getter)] + pub fn name(self: &Error) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/name) + #[js_sys(setter)] + pub fn set_name(self: &Error, name: &str); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Error) -> JsString; +} diff --git a/client/js-sys/src/builtins/mod.rs b/client/js-sys/src/builtins/mod.rs new file mode 100644 index 00000000..99b2e486 --- /dev/null +++ b/client/js-sys/src/builtins/mod.rs @@ -0,0 +1,13 @@ +mod array; +mod bigint; +mod error; +mod number; +mod object; +mod string; + +pub use array::{JsArray, TryFromJsArrayError}; +pub use bigint::JsBigInt; +pub use error::{Error, ErrorOptions}; +pub use number::JsNumber; +pub use object::Object; +pub use string::JsString; diff --git a/client/js-sys/src/number/number.js-sys.rs b/client/js-sys/src/builtins/number.rs similarity index 59% rename from client/js-sys/src/number/number.js-sys.rs rename to client/js-sys/src/builtins/number.rs index 3cf3c262..c192336d 100644 --- a/client/js-sys/src/number/number.js-sys.rs +++ b/client/js-sys/src/builtins/number.rs @@ -1,4 +1,4 @@ -#[js_sys] +#[crate::js_sys(js_sys = crate)] extern "js-sys" { pub type JsNumber; } diff --git a/client/js-sys/src/builtins/object.rs b/client/js-sys/src/builtins/object.rs new file mode 100644 index 00000000..326b2c17 --- /dev/null +++ b/client/js-sys/src/builtins/object.rs @@ -0,0 +1,18 @@ +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[derive(Clone, Debug)] + pub type Object; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Object; +} + +impl Default for Object { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/string/mod.rs b/client/js-sys/src/builtins/string.rs similarity index 67% rename from client/js-sys/src/string/mod.rs rename to client/js-sys/src/builtins/string.rs index 62f6a181..fd158eb3 100644 --- a/client/js-sys/src/string/mod.rs +++ b/client/js-sys/src/builtins/string.rs @@ -1,19 +1,69 @@ -#[rustfmt::skip] -#[path ="string.gen.rs"] -mod string; - use alloc::string::String; use alloc::vec::Vec; use core::fmt::{self, Display, Formatter}; -pub use self::string::JsString; +use js_sys_macro::js_sys; + +use super::object::Object; use crate::JsValue; use crate::util::{PtrConst, PtrLength, PtrMut}; +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_name = "String", extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type JsString; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_name = "String")] + fn string_constructor(value: &JsValue) -> JsString; + + #[js_sys(js_embed = "string.eq")] + // SAFETY: The pointer and length must describe a valid UTF-8 byte slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrLength) -> bool; + + #[js_sys(js_embed = "string.decode")] + // SAFETY: The pointer and length must describe a valid UTF-8 byte slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString; + + #[js_sys(js_embed = "string.utf8_length")] + fn string_utf8_length(string: &JsString) -> f64; + + #[js_sys(js_embed = "string.encode")] + // SAFETY: The pointer and length must describe a valid output byte slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn string_encode(string: &JsString, array: PtrMut, len: PtrLength); +} + impl JsString { #[must_use] pub fn new(value: &JsValue) -> Self { - string::string_constructor(value) + string_constructor(value) } } @@ -37,7 +87,7 @@ impl PartialEq<&str> for JsString { // SAFETY: Parameters are correct. unsafe { - string::string_eq( + string_eq( self, PtrConst::new(other.as_bytes()), PtrLength::new(other.as_bytes()), @@ -56,7 +106,7 @@ impl From<&str> for JsString { fn from(value: &str) -> Self { // SAFETY: Parameters are correct. unsafe { - string::string_decode( + string_decode( PtrConst::new(value.as_bytes()), PtrLength::new(value.as_bytes()), ) @@ -106,7 +156,7 @@ impl From<&JsString> for String { "}}", ); - let len = string::string_utf8_length(value); + let len = string_utf8_length(value); #[cfg(target_arch = "wasm32")] assert!( len < f64::from(u32::MAX), @@ -122,7 +172,7 @@ impl From<&JsString> for String { let mut vec = Vec::with_capacity(len); // SAFETY: Parameters are correct. unsafe { - string::string_encode( + string_encode( value, PtrMut::new(&mut vec), PtrLength::from_uninit_slice(vec.spare_capacity_mut()), diff --git a/client/js-sys/src/closure/closure.gen.rs b/client/js-sys/src/closure/closure.gen.rs deleted file mode 100644 index f65995f0..00000000 --- a/client/js-sys/src/closure/closure.gen.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use crate::{js_bindgen, r#macro}; -use crate::JsValue; - -pub(super) fn closure_unref(callback: &JsValue) { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "closure_unref", shim = - "js_sys.closure_unref", inputs = [("arg0", & JsValue)],), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "closure_unref", - required_embeds = [("js_sys", "closure.unref"), r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['closure.unref']", indirect_call = - "this.#jsEmbed.js_sys['closure.unref'](arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.closure_unref"] - fn closure_unref( - arg0_0: r#macro::InputSlot1<&JsValue>, - arg0_1: r#macro::InputSlot2<&JsValue>, - arg0_2: r#macro::InputSlot3<&JsValue>, - arg0_3: r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(callback); - unsafe { closure_unref(arg0_0, arg0_1, arg0_2, arg0_3) } - }; -} diff --git a/client/js-sys/src/closure/closure.js-sys.rs b/client/js-sys/src/closure/closure.js-sys.rs deleted file mode 100644 index 205aee96..00000000 --- a/client/js-sys/src/closure/closure.js-sys.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::JsValue; - -#[js_sys] -extern "js-sys" { - #[js_sys(js_embed = "closure.unref")] - pub(super) fn closure_unref(callback: &JsValue); -} diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 46dee466..69fa5d01 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -2,7 +2,7 @@ use core::mem::{ManuallyDrop, MaybeUninit}; use core::ptr; use crate::JsValue; -use crate::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; +use crate::runtime::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; // Conversion `metadata`. @@ -609,7 +609,7 @@ where type Abi = T::Abi; fn from_return_abi(raw: MaybeUninit>) -> Self { - if let Some(error) = crate::exception::take() { + if let Some(error) = crate::runtime::exception::take() { #[cfg(not(target_feature = "exception-handling"))] if ::MODE.is_direct() { // SAFETY: A direct Wasm return is always initialized. On the diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index 9a89cd96..c00b25c6 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -9,33 +9,25 @@ extern crate alloc; #[macro_use] mod util; -mod array; -mod bigint; -mod closure; -mod exception; -mod externref; + +// JavaScript standard built-in objects. +mod builtins; +// `Runtime` support for Rust and JavaScript `interop`. +mod runtime; + pub mod hazard; // Implementations for passing Rust standard types across the JavaScript // boundary. mod interop; #[doc(hidden)] pub mod r#macro; -mod number; -mod panic; -mod string; -mod value; +pub use builtins::{ + Error, ErrorOptions, JsArray, JsBigInt, JsNumber, JsString, Object, TryFromJsArrayError, +}; pub use js_bindgen; -#[cfg(feature = "macro")] pub use js_sys_macro::{closure, js_sys}; - -pub use crate::array::{JsArray, TryFromJsArrayError}; -pub use crate::bigint::JsBigInt; -pub use crate::closure::Closure; -pub use crate::number::JsNumber; -pub use crate::panic::{UnwrapThrowExt, panic}; -pub use crate::string::JsString; -pub use crate::value::JsValue; +pub use runtime::{Closure, ClosureAllocation, ClosureHeader, JsValue, UnwrapThrowExt, panic}; #[cfg(not(target_feature = "reference-types"))] compile_error!("`js-sys` requires the `reference-types` target feature"); diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index 37023bd6..67b4296f 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -1,5 +1,6 @@ mod abi; mod export; +mod import; mod js_import; mod result; mod text; @@ -9,12 +10,11 @@ mod wat_import; pub use alloc::boxed::Box; pub use abi::*; +pub use import::*; pub use result::*; pub use text::*; pub use wat::*; -// Closure macro runtime. -pub use crate::closure::{ClosureAllocation, ClosureHeader}; // Text rendering. pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; // JavaScript export shims. diff --git a/client/js-sys/src/macro/import.rs b/client/js-sys/src/macro/import.rs new file mode 100644 index 00000000..3d67a69f --- /dev/null +++ b/client/js-sys/src/macro/import.rs @@ -0,0 +1,380 @@ +mod js; +mod wat; +mod writer; + +use core::marker::PhantomData; + +use writer::Writer; + +use super::{ + WatSlot, into_js_wat_slots, js_input_template, js_output_has_conversion, js_output_sret, + js_output_templates, js_result_catch, js_result_try, return_from_js_is_direct, + return_from_js_wat_slots, validate_into_js, validate_return_from_js, wat_result_catch, + wat_result_default, wat_result_imports, wat_result_locals, wat_result_try, +}; +use crate::hazard::{IntoJS, ReturnFromJS}; + +/// All target-dependent metadata needed to render one imported argument. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ImportInput { + name: &'static str, + ty: &'static ImportInputType, +} + +#[derive(Clone, Copy)] +struct ImportInputType { + slots: [WatSlot; 4], + js_template: &'static str, + has_js_conversion: bool, + wat_capacity: WatInputCapacity, +} + +/// All target-dependent metadata needed to render one imported result. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ImportOutput { + direct: bool, + slots: [WatSlot; 4], + pointer: WatSlot, + has_js_conversion: bool, + js_templates: [&'static str; 4], + js_sret: &'static str, + js_try: &'static str, + js_catch: &'static str, + wat_result_imports: &'static str, + wat_result_locals: &'static str, + wat_result_try: &'static str, + wat_result_catch: &'static str, + wat_result_default: &'static str, + wat_capacity: usize, +} + +/// JavaScript-specific parts of an import descriptor. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ImportJs { + pub direct_wrapper: bool, + pub direct_call: &'static str, + pub indirect_call: &'static str, + pub required_embeds: &'static [(&'static str, &'static str)], +} + +/// A semantic description of one JavaScript import. +/// +/// Rendering is deliberately centralized in ordinary `const fn`s. Generated +/// bindings construct this value once instead of expanding a tree of string +/// concatenation macros and materializing every intermediate fragment. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ImportDescriptor { + module: &'static str, + import: &'static str, + shim: &'static str, + inputs: &'static [ImportInput], + output: Option<&'static ImportOutput>, + js: Option, + wat_capacity: usize, + js_capacity: usize, +} + +/// One framed sequence of length-prefixed custom-section records. +/// +/// `capacity` lets multiple padded fragments be concatenated in one custom +/// section. `used` excludes the zero-filled tail of `records`. +#[doc(hidden)] +#[repr(C)] +pub struct ImportSection { + capacity: [u8; 4], + used: [u8; 4], + records: [u8; CAPACITY], +} + +struct ImportInputMetadata(PhantomData); + +#[derive(Clone, Copy)] +struct WatInputCapacity { + fixed: usize, + name_uses: usize, +} + +impl ImportInputMetadata { + const VALUE: ImportInputType = { + validate_into_js::(); + let slots = into_js_wat_slots::(); + + ImportInputType { + slots, + js_template: js_input_template::(), + has_js_conversion: T::JS_CONV.is_some(), + wat_capacity: wat::input_capacity(&slots), + } + }; +} + +struct ImportOutputMetadata(PhantomData); + +impl ImportOutputMetadata { + const VALUE: ImportOutput = { + validate_return_from_js::(); + let slots = return_from_js_wat_slots::(); + let direct = return_from_js_is_direct::(); + let mut output = ImportOutput { + direct, + slots, + pointer: into_js_wat_slots::>()[0], + has_js_conversion: js_output_has_conversion::(), + js_templates: js_output_templates::(), + js_sret: js_output_sret::(), + js_try: js_result_try::(), + js_catch: js_result_catch::(direct), + wat_result_imports: wat_result_imports::(), + wat_result_locals: wat_result_locals::(), + wat_result_try: wat_result_try::(), + wat_result_catch: wat_result_catch::(), + wat_result_default: wat_result_default::(), + wat_capacity: 0, + }; + + output.wat_capacity = wat::output_capacity(&output); + output + }; +} + +#[doc(hidden)] +#[must_use] +pub const fn import_input(name: &'static str) -> ImportInput { + ImportInput { + name, + ty: &ImportInputMetadata::::VALUE, + } +} + +#[doc(hidden)] +#[must_use] +pub const fn import_output() -> &'static ImportOutput { + &ImportOutputMetadata::::VALUE +} + +impl ImportDescriptor { + #[doc(hidden)] + #[must_use] + pub const fn new( + module: &'static str, + import: &'static str, + shim: &'static str, + inputs: &'static [ImportInput], + output: Option<&'static ImportOutput>, + js: Option, + ) -> Self { + let mut descriptor = Self { + module, + import, + shim, + inputs, + output, + js, + wat_capacity: 0, + js_capacity: 0, + }; + + descriptor.wat_capacity = wat::descriptor_capacity(&descriptor); + descriptor.js_capacity = js::descriptor_capacity(&descriptor); + descriptor + } + + #[must_use] + const fn needs_js_shim(&self) -> bool { + let mut input = 0; + + while input < self.inputs.len() { + if self.inputs[input].ty.has_js_conversion { + return true; + } + input += 1; + } + + match self.output { + Some(output) => output.has_js_conversion || catches_result_in_js_from_output(output), + None => false, + } + } +} + +/// Returns a safe upper bound for [`import_wat`]. +/// +/// This follows the renderer without scanning declaration contents, so it is +/// suitable for sizing a padded, single-pass section. +#[doc(hidden)] +#[must_use] +pub const fn import_wat_capacity(imports: &[ImportDescriptor]) -> usize { + let mut capacity = Capacity::new(); + let mut import = 0; + + if !imports.is_empty() { + capacity.add(4); + capacity.add(imports.len() - 1); + } + + while import < imports.len() { + capacity.add(imports[import].wat_capacity); + import += 1; + } + + capacity.get() +} + +#[doc(hidden)] +#[must_use] +pub const fn import_wat( + imports: &[ImportDescriptor], +) -> ImportSection { + let mut writer = Writer::::new(); + write_wat(&mut writer, imports); + + ImportSection::new(writer) +} + +/// Returns a safe upper bound for [`import_js`]. +/// +/// This follows the renderer without scanning template contents, so it is +/// suitable for sizing a padded, single-pass section. +#[doc(hidden)] +#[must_use] +pub const fn import_js_capacity(imports: &[ImportDescriptor]) -> usize { + let mut capacity = Capacity::new(); + let mut import = 0; + + while import < imports.len() { + if imports[import].js.is_some() { + capacity.add(4); + capacity.add(imports[import].js_capacity); + } + import += 1; + } + + capacity.get() +} + +#[doc(hidden)] +#[must_use] +pub const fn import_js( + imports: &[ImportDescriptor], +) -> ImportSection { + let mut writer = Writer::::new(); + write_js(&mut writer, imports); + + ImportSection::new(writer) +} + +const fn write_wat(writer: &mut Writer, imports: &[ImportDescriptor]) { + if imports.is_empty() { + return; + } + + let header = writer.len(); + writer.write_u32(0); + let start = writer.len(); + let mut import = 0; + + while import < imports.len() { + if import != 0 { + writer.write_byte(b'\n'); + } + + imports[import].write_wat_boundary_import(writer); + import += 1; + } + + wat::write_wat_support_imports(writer, imports); + + import = 0; + while import < imports.len() { + imports[import].write_wat_shim(writer); + import += 1; + } + + let record_len = writer.len() - start; + writer.set_u32(header, record_len); +} + +const fn write_js(writer: &mut Writer, imports: &[ImportDescriptor]) { + let mut import = 0; + + while import < imports.len() { + if imports[import].js.is_some() { + let header = writer.len(); + writer.write_u32(0); + let start = writer.len(); + imports[import].write_js_record(writer); + let record_len = writer.len() - start; + writer.set_u32(header, record_len); + } + import += 1; + } +} + +#[must_use] +const fn catches_result_in_js_from_output(output: &ImportOutput) -> bool { + !output.js_try.is_empty() +} + +pub(super) struct Capacity(usize); + +impl Capacity { + pub const fn new() -> Self { + Self(0) + } + + pub const fn add(&mut self, additional: usize) { + self.0 = match self.0.checked_add(additional) { + Some(capacity) => capacity, + None => panic!("import section capacity overflows usize"), + }; + } + + pub const fn add_str(&mut self, value: &str) { + self.add(value.len()); + } + + pub const fn add_repeated_str(&mut self, value: &str, repetitions: usize) { + let Some(additional) = value.len().checked_mul(repetitions) else { + panic!("import section capacity overflows usize"); + }; + self.add(additional); + } + + pub const fn add_wat_lines(&mut self, value: &str) { + if !value.is_empty() { + // The separators already present in `value` become the leading + // newlines of all but its first line. + self.add(value.len()); + self.add(1); + } + } + + pub const fn get(self) -> usize { + self.0 + } +} + +impl ImportSection { + const fn new(writer: Writer) -> Self { + assert!(CAPACITY <= u32::MAX as usize); + let used = writer.len(); + assert!(used <= u32::MAX as usize); + + Self { + capacity: u32_bytes(CAPACITY), + used: u32_bytes(used), + records: writer.finish_padded(), + } + } +} + +const fn u32_bytes(value: usize) -> [u8; 4] { + assert!(value <= u32::MAX as usize); + let bytes = value.to_le_bytes(); + + [bytes[0], bytes[1], bytes[2], bytes[3]] +} diff --git a/client/js-sys/src/macro/import/js.rs b/client/js-sys/src/macro/import/js.rs new file mode 100644 index 00000000..9b751951 --- /dev/null +++ b/client/js-sys/src/macro/import/js.rs @@ -0,0 +1,506 @@ +use super::{Capacity, ImportDescriptor, ImportInput, ImportJs, ImportOutput, Writer}; + +pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize { + let Some(js) = descriptor.js else { + return 0; + }; + let mut capacity = Capacity::new(); + + capacity.add(2); + capacity.add_str(descriptor.module); + capacity.add(2); + capacity.add_str(descriptor.import); + capacity.add(1); + + let mut embed = 0; + while embed < js.required_embeds.len() { + let (module, name) = js.required_embeds[embed]; + + capacity.add(2); + capacity.add_str(module); + capacity.add(2); + capacity.add_str(name); + embed += 1; + } + + let wrapped = descriptor.needs_js_shim(); + + if wrapped { + capacity.add(1); + + if let Some(output) = descriptor.output + && !output.direct + { + capacity.add_str("$retptr"); + + if !descriptor.inputs.is_empty() { + capacity.add_str(", "); + } + } + + add_input_parameters_capacity(&mut capacity, descriptor.inputs); + capacity.add_str(") => {\n"); + } else if js.direct_wrapper { + capacity.add(1); + add_input_parameters_capacity(&mut capacity, descriptor.inputs); + capacity.add_str(") => "); + } + + let mut input = 0; + while input < descriptor.inputs.len() { + add_input_conversion_capacity(&mut capacity, &descriptor.inputs[input]); + input += 1; + } + + match descriptor.output { + Some(output) => add_output_capacity(&mut capacity, output, js, wrapped), + None => { + if wrapped { + capacity.add_str(js.indirect_call); + capacity.add_str("\n}"); + } else { + capacity.add_str(js.direct_call); + } + } + } + + capacity.get() +} + +const fn add_input_parameters_capacity(capacity: &mut Capacity, inputs: &[ImportInput]) { + let mut input = 0; + + while input < inputs.len() { + if input != 0 { + capacity.add_str(", "); + } + + let descriptor = &inputs[input]; + add_slot_name_capacity(capacity, descriptor.name); + + let mut slot = 1; + while slot < descriptor.ty.slots.len() { + if !descriptor.ty.slots[slot].abi.is_empty() { + capacity.add_str(", "); + add_slot_name_capacity(capacity, descriptor.name); + } + slot += 1; + } + + input += 1; + } +} + +const fn add_input_conversion_capacity(capacity: &mut Capacity, input: &ImportInput) { + if !input.ty.has_js_conversion { + return; + } + + capacity.add_str(" "); + add_slot_name_capacity(capacity, input.name); + capacity.add_str(" = "); + add_template_capacity(capacity, input.ty.js_template, "", Some(input.name)); + capacity.add(1); +} + +const fn add_output_capacity( + capacity: &mut Capacity, + output: &ImportOutput, + js: ImportJs, + wrapped: bool, +) { + let convert_direct = output.direct && output.has_js_conversion; + let catches_result = !output.js_try.is_empty(); + let call = if wrapped { + js.indirect_call + } else { + js.direct_call + }; + let indent = if catches_result { " " } else { " " }; + + capacity.add_str(output.js_try); + + if convert_direct { + capacity.add_str(indent); + capacity.add_str("const $ret = "); + } else if output.direct { + if catches_result { + capacity.add_str(" return "); + } else if wrapped { + capacity.add_str(" return "); + } + } else { + capacity.add_str(indent); + capacity.add_str("const $ret = "); + } + + if output.direct && !convert_direct { + add_template_capacity(capacity, output.js_templates[0], call, None); + } else { + capacity.add_str(call); + } + + if convert_direct { + capacity.add(1); + capacity.add_str(indent); + capacity.add_str("return "); + add_template_capacity(capacity, output.js_templates[0], "$ret", None); + } + + if !output.direct { + capacity.add(1); + capacity.add_str(indent); + capacity.add_str(output.js_sret); + capacity.add(1); + add_template_capacity(capacity, output.js_templates[0], "$ret", None); + + let mut slot = 1; + while slot < output.js_templates.len() { + capacity.add_str(", "); + add_template_capacity(capacity, output.js_templates[slot], "$ret", None); + slot += 1; + } + + capacity.add_str(", $retptr)"); + } + + if catches_result { + capacity.add_str(output.js_catch); + } else if wrapped { + capacity.add_str("\n}"); + } +} + +const fn add_slot_name_capacity(capacity: &mut Capacity, name: &str) { + capacity.add_str(name); + capacity.add(2); +} + +const fn add_template_capacity( + capacity: &mut Capacity, + template: &str, + value: &str, + slots: Option<&str>, +) { + let mut maximum_replacement = value.len(); + + if let Some(name) = slots { + let mut slot_len = name.len(); + slot_len = match slot_len.checked_add(2) { + Some(slot_len) => slot_len, + None => panic!("import section capacity overflows usize"), + }; + + if slot_len > maximum_replacement { + maximum_replacement = slot_len; + } + } + + // Literal bytes contribute at most `template.len()`. Every recognized + // placeholder is six bytes long, so at most `len / 6` are replaced. + capacity.add_str(template); + let Some(replacements) = + (template.len() / PLACEHOLDERS[0].len()).checked_mul(maximum_replacement) + else { + panic!("import section capacity overflows usize"); + }; + capacity.add(replacements); +} + +impl ImportDescriptor { + pub(super) const fn write_js_record(&self, writer: &mut Writer) { + let Some(js) = self.js else { + return; + }; + + writer.write_u16(self.module.len()); + writer.write_str(self.module); + writer.write_u16(self.import.len()); + writer.write_str(self.import); + + assert!(js.required_embeds.len() <= u8::MAX as usize); + writer.write_byte(js.required_embeds.len().to_le_bytes()[0]); + + let mut embed = 0; + while embed < js.required_embeds.len() { + let (module, name) = js.required_embeds[embed]; + + writer.write_u16(module.len()); + writer.write_str(module); + writer.write_u16(name.len()); + writer.write_str(name); + embed += 1; + } + + self.write_js(writer, js); + } + + const fn write_js(&self, writer: &mut Writer, js: ImportJs) { + let wrapped = self.needs_js_shim(); + + if wrapped { + writer.write_byte(b'('); + + if let Some(output) = self.output + && !output.direct + { + writer.write_str("$retptr"); + + if !self.inputs.is_empty() { + writer.write_str(", "); + } + } + + write_input_parameters(writer, self.inputs); + writer.write_str(") => {\n"); + } else if js.direct_wrapper { + writer.write_byte(b'('); + write_input_parameters(writer, self.inputs); + writer.write_str(") => "); + } + + let mut input = 0; + while input < self.inputs.len() { + write_input_conversion(writer, &self.inputs[input]); + input += 1; + } + + match self.output { + Some(output) => write_output(writer, output, js, wrapped), + None => { + if wrapped { + writer.write_str(js.indirect_call); + writer.write_str("\n}"); + } else { + writer.write_str(js.direct_call); + } + } + } + } +} + +const fn write_input_parameters( + writer: &mut Writer, + inputs: &[ImportInput], +) { + let mut input = 0; + + while input < inputs.len() { + if input != 0 { + writer.write_str(", "); + } + + write_input_parameter(writer, &inputs[input]); + input += 1; + } +} + +const fn write_input_parameter(writer: &mut Writer, input: &ImportInput) { + write_slot_name(writer, input.name, 0); + + let mut slot = 1; + while slot < input.ty.slots.len() { + if !input.ty.slots[slot].abi.is_empty() { + writer.write_str(", "); + write_slot_name(writer, input.name, slot); + } + + slot += 1; + } +} + +const fn write_input_conversion(writer: &mut Writer, input: &ImportInput) { + if !input.ty.has_js_conversion { + return; + } + + writer.write_str(" "); + write_slot_name(writer, input.name, 0); + writer.write_str(" = "); + write_template(writer, input.ty.js_template, "", Some(input.name)); + writer.write_byte(b'\n'); +} + +const fn write_output( + writer: &mut Writer, + output: &ImportOutput, + js: ImportJs, + wrapped: bool, +) { + let convert_direct = output.direct && output.has_js_conversion; + let catches_result = !output.js_try.is_empty(); + let call = if wrapped { + js.indirect_call + } else { + js.direct_call + }; + let template_value = if output.direct && !convert_direct { + call + } else { + "$ret" + }; + let indent = if catches_result { " " } else { " " }; + + writer.write_str(output.js_try); + + if convert_direct { + writer.write_str(indent); + writer.write_str("const $ret = "); + } else if output.direct { + if catches_result { + writer.write_str(" return "); + } else if wrapped { + writer.write_str(" return "); + } + } else { + writer.write_str(indent); + writer.write_str("const $ret = "); + } + + if output.direct && !convert_direct { + write_template(writer, output.js_templates[0], template_value, None); + } else { + writer.write_str(call); + } + + if convert_direct { + writer.write_byte(b'\n'); + writer.write_str(indent); + writer.write_str("return "); + write_template(writer, output.js_templates[0], "$ret", None); + } + + if !output.direct { + writer.write_byte(b'\n'); + writer.write_str(indent); + writer.write_str(output.js_sret); + writer.write_byte(b'('); + write_template(writer, output.js_templates[0], "$ret", None); + + let mut slot = 1; + while slot < output.js_templates.len() { + if template_len(output.js_templates[slot], "$ret", None) != 0 { + writer.write_str(", "); + write_template(writer, output.js_templates[slot], "$ret", None); + } + + slot += 1; + } + + writer.write_str(", $retptr)"); + } + + if catches_result { + writer.write_str(output.js_catch); + } else if wrapped { + writer.write_str("\n}"); + } +} + +const fn write_slot_name(writer: &mut Writer, name: &str, slot: usize) { + assert!(slot < 4); + writer.write_str(name); + writer.write_byte(b'_'); + writer.write_byte(b"0123"[slot]); +} + +const PLACEHOLDERS: [&[u8]; 5] = [b"$value", b"$slot1", b"$slot2", b"$slot3", b"$slot4"]; + +const fn write_template( + writer: &mut Writer, + template: &str, + value: &str, + slots: Option<&str>, +) { + let bytes = template.as_bytes(); + let mut input = 0; + + while input < bytes.len() { + let placeholder = template_placeholder(bytes, input); + + if placeholder == 0 { + writer.write_str(value); + input += PLACEHOLDERS[placeholder].len(); + } else if placeholder < PLACEHOLDERS.len() { + if let Some(name) = slots { + write_slot_name(writer, name, placeholder - 1); + } + + input += PLACEHOLDERS[placeholder].len(); + } else { + let start = input; + input += 1; + + while input < bytes.len() && bytes[input] != b'$' { + input += 1; + } + + writer.write_str_range(template, start, input); + } + } +} + +const fn template_len(template: &str, value: &str, slots: Option<&str>) -> usize { + let bytes = template.as_bytes(); + let mut input = 0; + let mut output = 0; + + while input < bytes.len() { + let placeholder = template_placeholder(bytes, input); + + if placeholder == 0 { + output += value.len(); + input += PLACEHOLDERS[placeholder].len(); + } else if placeholder < PLACEHOLDERS.len() { + if let Some(name) = slots { + output += name.len() + 2; + } + + input += PLACEHOLDERS[placeholder].len(); + } else { + let start = input; + input += 1; + + while input < bytes.len() && bytes[input] != b'$' { + input += 1; + } + + output += input - start; + } + } + + output +} + +const fn template_placeholder(template: &[u8], index: usize) -> usize { + if template[index] != b'$' { + return PLACEHOLDERS.len(); + } + + let mut placeholder = 0; + while placeholder < PLACEHOLDERS.len() { + let candidate = PLACEHOLDERS[placeholder]; + + if index + candidate.len() <= template.len() { + let mut byte = 0; + let mut matches = true; + + while byte < candidate.len() { + if template[index + byte] != candidate[byte] { + matches = false; + break; + } + + byte += 1; + } + + if matches { + return placeholder; + } + } + + placeholder += 1; + } + + PLACEHOLDERS.len() +} diff --git a/client/js-sys/src/macro/import/wat.rs b/client/js-sys/src/macro/import/wat.rs new file mode 100644 index 00000000..5909b3ea --- /dev/null +++ b/client/js-sys/src/macro/import/wat.rs @@ -0,0 +1,755 @@ +use super::{Capacity, ImportDescriptor, ImportOutput, WatInputCapacity, Writer}; +use crate::r#macro::WatSlot; + +pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize { + let mut capacity = Capacity::new(); + + capacity.add_str("(import \""); + capacity.add_str(descriptor.module); + capacity.add_str("\" \""); + capacity.add_str(descriptor.import); + capacity.add_str("\" (func $"); + add_import_name_capacity(&mut capacity, descriptor); + capacity.add_str(" (@sym (name \""); + add_import_name_capacity(&mut capacity, descriptor); + capacity.add_str("\"))"); + + if !input_types_are_empty(descriptor.inputs) { + capacity.add_str(" (param "); + capacity.add(descriptor.inputs.len() - 1); + capacity.add(1); + } + + capacity.add_str("))"); + capacity.add_str("\n(func $"); + capacity.add_str(descriptor.shim); + capacity.add_str(" (@sym)"); + + let mut input = 0; + while input < descriptor.inputs.len() { + let argument = &descriptor.inputs[input]; + capacity.add(argument.ty.wat_capacity.fixed); + capacity.add_repeated_str(argument.name, argument.ty.wat_capacity.name_uses); + input += 1; + } + + if let Some(output) = descriptor.output { + capacity.add(output.wat_capacity); + } + + capacity.add_str("\n call $"); + add_import_name_capacity(&mut capacity, descriptor); + capacity.add_str(" (@reloc)"); + capacity.add_str("\n)"); + capacity.get() +} + +pub(super) const fn input_capacity(slots: &[WatSlot; 4]) -> WatInputCapacity { + let mut capacity = Capacity::new(); + let mut name_uses = 0; + let mut slot = 0; + let mut wrote_type = false; + let mut wrote_get = false; + + while slot < slots.len() { + let descriptor = &slots[slot]; + + if !descriptor.boundary.is_empty() { + if wrote_type { + capacity.add(1); + } + capacity.add_str(descriptor.boundary); + wrote_type = true; + } + + if !descriptor.abi.is_empty() { + capacity.add_str(" (param $"); + capacity.add_str(slot_suffix(slot)); + capacity.add_str(descriptor.abi); + capacity.add(1); + name_uses += 1; + + if wrote_get { + capacity.add(1); + } + capacity.add_str(" local.get $"); + capacity.add_str(get_suffix(slot)); + add_conversion_capacity(&mut capacity, descriptor.conv); + name_uses += 1; + wrote_get = true; + } + + capacity.add_wat_lines(descriptor.imports); + capacity.add_wat_lines(descriptor.locals); + slot += 1; + } + + // `write_wat` starts every input's local-get sequence on a new line, + // including a zero-slot input. + capacity.add(1); + WatInputCapacity { + fixed: capacity.get(), + name_uses, + } +} + +pub(super) const fn output_capacity(output: &ImportOutput) -> usize { + let mut capacity = Capacity::new(); + + if output.direct { + capacity.add_str(" (result "); + capacity.add_str(output.slots[0].boundary); + capacity.add(1); + capacity.add_str(" (result "); + capacity.add_str(output.slots[0].abi); + capacity.add(1); + capacity.add_wat_lines(output.slots[0].imports); + capacity.add_wat_lines(output.slots[0].locals); + + if !output.slots[0].conv.is_empty() { + capacity.add_str("\n "); + capacity.add_str(output.slots[0].conv); + } + } else { + capacity.add_str(" (param $retptr "); + capacity.add_str(output.pointer.boundary); + capacity.add(1); + capacity.add_str(" (param $retptr "); + capacity.add_str(output.pointer.abi); + capacity.add(1); + capacity.add_wat_lines(output.pointer.locals); + capacity.add_str("\n local.get $retptr"); + add_conversion_capacity(&mut capacity, output.pointer.conv); + } + + capacity.add_wat_lines(output.wat_result_imports); + capacity.add_wat_lines(output.wat_result_locals); + capacity.add_str(output.wat_result_try); + capacity.add_str(output.wat_result_catch); + capacity.add_str(output.wat_result_default); + capacity.get() +} + +const fn add_import_name_capacity(capacity: &mut Capacity, descriptor: &ImportDescriptor) { + capacity.add_str(descriptor.module); + capacity.add_str(".import."); + capacity.add_str(descriptor.import); +} + +const fn add_conversion_capacity(capacity: &mut Capacity, conversion: &str) { + if !conversion.is_empty() { + capacity.add_str("\n "); + capacity.add_str(conversion); + } +} + +pub(super) const fn write_wat_support_imports( + writer: &mut Writer, + descriptors: &[ImportDescriptor], +) { + let mut descriptor_index = 0; + let mut source_index = 0; + + while descriptor_index < descriptors.len() { + let descriptor = &descriptors[descriptor_index]; + let source_count = descriptor.unique_source_count(); + + while source_index < source_count && descriptor.unique_source(true, source_index).is_empty() + { + source_index += 1; + } + + if source_index < source_count { + break; + } + + descriptor_index += 1; + source_index = 0; + } + + if descriptor_index == descriptors.len() { + return; + } + + let mut seen = [SeenLine::EMPTY; 32]; + let mut seen_len = 0; + + while descriptor_index < descriptors.len() { + let descriptor = &descriptors[descriptor_index]; + let source_count = descriptor.unique_source_count(); + + while source_index < source_count { + let source = descriptor.unique_source(true, source_index); + let bytes = source.as_bytes(); + let mut line_start = 0; + + while line_start < bytes.len() { + let line_end = line_end(source, line_start); + + if line_end != line_start { + let mut was_seen = false; + let mut seen_index = 0; + + while seen_index < seen_len { + let candidate = seen[seen_index]; + + if lines_equal( + source, + line_start, + line_end, + candidate.source, + candidate.start, + candidate.end, + ) { + was_seen = true; + break; + } + + seen_index += 1; + } + + if !was_seen && seen_len == seen.len() { + was_seen = previous_line_was_seen( + descriptors, + descriptor_index, + source_index, + line_start, + line_end, + ); + } + + if !was_seen { + writer.write_byte(b'\n'); + writer.write_str_range(source, line_start, line_end); + + if seen_len < seen.len() { + seen[seen_len] = SeenLine { + source, + start: line_start, + end: line_end, + }; + seen_len += 1; + } + } + } + + line_start = line_end + 1; + } + + source_index += 1; + } + + descriptor_index += 1; + source_index = 0; + } +} + +const fn previous_line_was_seen( + descriptors: &[ImportDescriptor], + descriptor_index: usize, + source_index: usize, + line_start: usize, + current_line_end: usize, +) -> bool { + let value = descriptors[descriptor_index].unique_source(true, source_index); + let mut candidate_descriptor_index = 0; + + while candidate_descriptor_index <= descriptor_index { + let descriptor = &descriptors[candidate_descriptor_index]; + let source_count = if candidate_descriptor_index == descriptor_index { + source_index + 1 + } else { + descriptor.unique_source_count() + }; + let mut candidate_source_index = 0; + + while candidate_source_index < source_count { + let candidate = descriptor.unique_source(true, candidate_source_index); + let limit = if candidate_descriptor_index == descriptor_index + && candidate_source_index == source_index + { + line_start + } else { + candidate.len() + }; + let mut candidate_start = 0; + + while candidate_start < limit { + let candidate_end = line_end(candidate, candidate_start); + + if candidate_end != candidate_start + && lines_equal( + value, + line_start, + current_line_end, + candidate, + candidate_start, + candidate_end, + ) { + return true; + } + + candidate_start = candidate_end + 1; + } + + candidate_source_index += 1; + } + + candidate_descriptor_index += 1; + } + + false +} + +impl ImportDescriptor { + pub(super) const fn write_wat_boundary_import( + &self, + writer: &mut Writer, + ) { + writer.write_str("(import \""); + writer.write_str(self.module); + writer.write_str("\" \""); + writer.write_str(self.import); + writer.write_str("\" (func $"); + self.write_import_name(writer); + writer.write_str(" (@sym (name \""); + self.write_import_name(writer); + writer.write_str("\"))"); + + if let Some(output) = self.output + && !output.direct + { + writer.write_str(" (param $retptr "); + writer.write_str(output.pointer.boundary); + writer.write_byte(b')'); + } + + if !input_types_are_empty(self.inputs) { + writer.write_str(" (param "); + write_input_types(writer, self.inputs); + writer.write_byte(b')'); + } + + if let Some(output) = self.output + && output.direct + { + writer.write_str(" (result "); + writer.write_str(output.slots[0].boundary); + writer.write_byte(b')'); + } + + writer.write_str("))"); + } + + pub(super) const fn write_wat_shim(&self, writer: &mut Writer) { + writer.write_str("\n(func $"); + writer.write_str(self.shim); + writer.write_str(" (@sym)"); + + if let Some(output) = self.output + && !output.direct + { + writer.write_str(" (param $retptr "); + writer.write_str(output.pointer.abi); + writer.write_byte(b')'); + } + + let mut input = 0; + while input < self.inputs.len() { + write_input_params( + writer, + self.inputs[input].name, + &self.inputs[input].ty.slots, + ); + input += 1; + } + + if let Some(output) = self.output + && output.direct + { + writer.write_str(" (result "); + writer.write_str(output.slots[0].abi); + writer.write_byte(b')'); + } + + self.write_unique_lines(writer, false); + + if let Some(output) = self.output { + writer.write_str(output.wat_result_try); + + if !output.direct { + writer.write_str("\n local.get $retptr"); + write_conversion(writer, output.pointer.conv); + } + } + + input = 0; + while input < self.inputs.len() { + writer.write_byte(b'\n'); + write_input_gets( + writer, + self.inputs[input].name, + &self.inputs[input].ty.slots, + ); + input += 1; + } + + writer.write_str("\n call $"); + self.write_import_name(writer); + writer.write_str(" (@reloc)"); + + if let Some(output) = self.output { + if output.direct && !output.slots[0].conv.is_empty() { + writer.write_str("\n "); + writer.write_str(output.slots[0].conv); + } + + writer.write_str(output.wat_result_catch); + writer.write_str(output.wat_result_default); + } + + writer.write_str("\n)"); + } + + const fn write_import_name(&self, writer: &mut Writer) { + writer.write_str(self.module); + writer.write_str(".import."); + writer.write_str(self.import); + } + + /// Writes the newline-delimited union used by the old `wat_imports!` and + /// `wat_locals!` macros. A line is emitted only at its first occurrence, + /// preserving both the original source order and its leading newline. + const fn write_unique_lines(&self, writer: &mut Writer, imports: bool) { + let source_count = self.unique_source_count(); + let mut source_index = 0; + + while source_index < source_count && self.unique_source(imports, source_index).is_empty() { + source_index += 1; + } + + if source_index == source_count { + return; + } + + let mut seen = [SeenLine::EMPTY; 8]; + let mut seen_len = 0; + + while source_index < source_count { + let source = self.unique_source(imports, source_index); + let bytes = source.as_bytes(); + let mut line_start = 0; + + while line_start < bytes.len() { + let line_end = line_end(source, line_start); + + if line_end != line_start { + let mut was_seen = false; + let mut seen_index = 0; + + while seen_index < seen_len { + let candidate = seen[seen_index]; + + if lines_equal( + source, + line_start, + line_end, + candidate.source, + candidate.start, + candidate.end, + ) { + was_seen = true; + break; + } + + seen_index += 1; + } + + if !was_seen && seen_len == seen.len() { + was_seen = self.line_was_seen(imports, source_index, line_start, line_end); + } + + if !was_seen { + writer.write_byte(b'\n'); + writer.write_str_range(source, line_start, line_end); + + if seen_len < seen.len() { + seen[seen_len] = SeenLine { + source, + start: line_start, + end: line_end, + }; + seen_len += 1; + } + } + } + + line_start = line_end + 1; + } + + source_index += 1; + } + } + + const fn unique_source_count(&self) -> usize { + self.inputs.len() * 4 + if self.output.is_some() { 2 } else { 0 } + } + + const fn unique_source(&self, imports: bool, index: usize) -> &'static str { + let input_sources = self.inputs.len() * 4; + + if index < input_sources { + let slot = &self.inputs[index / 4].ty.slots[index % 4]; + return if imports { slot.imports } else { slot.locals }; + } + + let Some(output) = self.output else { + return ""; + }; + + match index - input_sources { + 0 => output_source(output, imports), + 1 => { + if imports { + output.wat_result_imports + } else { + output.wat_result_locals + } + } + _ => "", + } + } + + const fn line_was_seen( + &self, + imports: bool, + source_index: usize, + line_start: usize, + current_line_end: usize, + ) -> bool { + let value = self.unique_source(imports, source_index); + let mut candidate_source_index = 0; + + while candidate_source_index <= source_index { + let candidate = self.unique_source(imports, candidate_source_index); + let limit = if candidate_source_index == source_index { + line_start + } else { + candidate.len() + }; + let mut candidate_start = 0; + + while candidate_start < limit { + let candidate_end = line_end(candidate, candidate_start); + + if candidate_end != candidate_start + && lines_equal( + value, + line_start, + current_line_end, + candidate, + candidate_start, + candidate_end, + ) { + return true; + } + + candidate_start = candidate_end + 1; + } + + candidate_source_index += 1; + } + + false + } +} + +#[derive(Clone, Copy)] +struct SeenLine { + source: &'static str, + start: usize, + end: usize, +} + +impl SeenLine { + const EMPTY: Self = Self { + source: "", + start: 0, + end: 0, + }; +} + +const fn output_source(output: &ImportOutput, imports: bool) -> &'static str { + if imports { + if output.direct { + output.slots[0].imports + } else { + "" + } + } else if output.direct { + output.slots[0].locals + } else { + output.pointer.locals + } +} + +const fn input_types_are_empty(inputs: &[super::ImportInput]) -> bool { + if inputs.is_empty() { + return true; + } + + // `wat_input!(import types; ...)` unconditionally inserts one space between + // arguments, so two or more arguments always produce a non-empty fragment. + if inputs.len() > 1 { + return false; + } + + let slots = &inputs[0].ty.slots; + slots[0].boundary.is_empty() + && slots[1].boundary.is_empty() + && slots[2].boundary.is_empty() + && slots[3].boundary.is_empty() +} + +const fn write_input_types( + writer: &mut Writer, + inputs: &[super::ImportInput], +) { + let mut input = 0; + + while input < inputs.len() { + if input != 0 { + writer.write_byte(b' '); + } + write_slot_types(writer, &inputs[input].ty.slots); + input += 1; + } +} + +const fn write_slot_types(writer: &mut Writer, slots: &[WatSlot; 4]) { + let mut slot = 0; + let mut wrote_type = false; + + while slot < slots.len() { + let r#type = slots[slot].boundary; + + if !r#type.is_empty() { + if wrote_type { + writer.write_byte(b' '); + } + writer.write_str(r#type); + wrote_type = true; + } + + slot += 1; + } +} + +const fn write_input_params( + writer: &mut Writer, + name: &str, + slots: &[WatSlot; 4], +) { + let mut slot = 0; + + while slot < slots.len() { + if !slots[slot].abi.is_empty() { + writer.write_str(" (param $"); + writer.write_str(name); + writer.write_str(slot_suffix(slot)); + writer.write_str(slots[slot].abi); + writer.write_byte(b')'); + } + + slot += 1; + } +} + +const fn write_input_gets( + writer: &mut Writer, + name: &str, + slots: &[WatSlot; 4], +) { + let mut slot = 0; + let mut wrote_get = false; + + while slot < slots.len() { + if !slots[slot].abi.is_empty() { + if wrote_get { + writer.write_byte(b'\n'); + } + + writer.write_str(" local.get $"); + writer.write_str(name); + writer.write_str(get_suffix(slot)); + write_conversion(writer, slots[slot].conv); + wrote_get = true; + } + + slot += 1; + } +} + +const fn write_conversion(writer: &mut Writer, conversion: &str) { + if !conversion.is_empty() { + writer.write_str("\n "); + writer.write_str(conversion); + } +} + +const fn slot_suffix(slot: usize) -> &'static str { + match slot { + 0 => "_0 ", + 1 => "_1 ", + 2 => "_2 ", + 3 => "_3 ", + _ => panic!("a Wasm ABI has exactly four slots"), + } +} + +const fn get_suffix(slot: usize) -> &'static str { + match slot { + 0 => "_0", + 1 => "_1", + 2 => "_2", + 3 => "_3", + _ => panic!("a Wasm ABI has exactly four slots"), + } +} + +const fn line_end(value: &str, start: usize) -> usize { + let bytes = value.as_bytes(); + let mut end = start; + + while end < bytes.len() && bytes[end] != b'\n' { + end += 1; + } + + end +} + +const fn lines_equal( + left: &str, + left_start: usize, + left_end: usize, + right: &str, + right_start: usize, + right_end: usize, +) -> bool { + if left_end - left_start != right_end - right_start { + return false; + } + + let left = left.as_bytes(); + let right = right.as_bytes(); + let mut offset = 0; + + while left_start + offset < left_end { + if left[left_start + offset] != right[right_start + offset] { + return false; + } + offset += 1; + } + + true +} diff --git a/client/js-sys/src/macro/import/writer.rs b/client/js-sys/src/macro/import/writer.rs new file mode 100644 index 00000000..d854b8e8 --- /dev/null +++ b/client/js-sys/src/macro/import/writer.rs @@ -0,0 +1,102 @@ +/// A const writer that renders directly into a custom section allocation. +pub(super) struct Writer { + bytes: [u8; LEN], + position: usize, +} + +impl Writer { + pub const fn new() -> Self { + Self { + bytes: [0; LEN], + position: 0, + } + } + + pub const fn len(&self) -> usize { + self.position + } + + pub const fn write_byte(&mut self, value: u8) { + if LEN != 0 { + assert!(self.position < LEN); + self.bytes[self.position] = value; + } + + self.position += 1; + } + + pub const fn write_str(&mut self, value: &str) { + let bytes = value.as_bytes(); + + if LEN != 0 { + assert!(bytes.len() <= LEN - self.position); + + // SAFETY: The assertion above proves that both ranges are valid and + // a string borrowed by the descriptor cannot overlap the output. + unsafe { + core::ptr::copy_nonoverlapping( + bytes.as_ptr(), + self.bytes.as_mut_ptr().add(self.position), + bytes.len(), + ); + } + } + + self.position += bytes.len(); + } + + pub const fn write_str_range(&mut self, value: &str, start: usize, end: usize) { + assert!(start <= end && end <= value.len()); + let len = end - start; + + if LEN != 0 { + assert!(len <= LEN - self.position); + + // SAFETY: Both assertions prove the source and destination ranges + // are valid, and they belong to different allocations. + unsafe { + core::ptr::copy_nonoverlapping( + value.as_ptr().add(start), + self.bytes.as_mut_ptr().add(self.position), + len, + ); + } + } + + self.position += len; + } + + pub const fn write_u16(&mut self, value: usize) { + assert!(value <= u16::MAX as usize); + let bytes = value.to_le_bytes(); + self.write_byte(bytes[0]); + self.write_byte(bytes[1]); + } + + pub const fn write_u32(&mut self, value: usize) { + assert!(value <= u32::MAX as usize); + let bytes = value.to_le_bytes(); + self.write_byte(bytes[0]); + self.write_byte(bytes[1]); + self.write_byte(bytes[2]); + self.write_byte(bytes[3]); + } + + pub const fn set_u32(&mut self, offset: usize, value: usize) { + assert!(value <= u32::MAX as usize); + + if LEN != 0 { + assert!(offset <= LEN - 4); + let bytes = value.to_le_bytes(); + self.bytes[offset] = bytes[0]; + self.bytes[offset + 1] = bytes[1]; + self.bytes[offset + 2] = bytes[2]; + self.bytes[offset + 3] = bytes[3]; + } + } + + pub const fn finish_padded(self) -> [u8; LEN] { + assert!(self.position <= LEN); + self.bytes + } +} diff --git a/client/js-sys/src/macro/js_import.rs b/client/js-sys/src/macro/js_import.rs index 4fbcbc3b..9ab815ec 100644 --- a/client/js-sys/src/macro/js_import.rs +++ b/client/js-sys/src/macro/js_import.rs @@ -3,7 +3,7 @@ #[macro_export] macro_rules! js_import { ( - direct_open = $direct_open:expr, + direct_wrapper = $direct_wrapper:expr, direct_call = $direct_call:expr, indirect_call = $indirect_call:expr, inputs = [$(($par:literal, $input:ty)),* $(,)?], @@ -12,8 +12,10 @@ macro_rules! js_import { $crate::r#macro::js_needs_shim!(($($input),*)); const OPEN: &::core::primitive::str = if WRAPPED { $crate::r#macro::js_function!("(", ") => {\n", $(($par, $input)),*) + } else if $direct_wrapper { + $crate::r#macro::js_function!("(", ") => ", $(($par, $input)),*) } else { - $direct_open + "" }; const BODY: &::core::primitive::str = if WRAPPED { $crate::r#macro::const_concat!($indirect_call, "\n}") @@ -28,7 +30,7 @@ macro_rules! js_import { ) }}; ( - direct_open = $direct_open:expr, + direct_wrapper = $direct_wrapper:expr, direct_call = $direct_call:expr, indirect_call = $indirect_call:expr, inputs = [$(($par:literal, $input:ty)),* $(,)?], @@ -43,8 +45,10 @@ macro_rules! js_import { ($output), $(($par, $input)),* ) + } else if $direct_wrapper { + $crate::r#macro::js_function!("(", ") => ", $(($par, $input)),*) } else { - $direct_open + "" }; $crate::r#macro::const_concat!( diff --git a/client/js-sys/src/macro/result.rs b/client/js-sys/src/macro/result.rs index 38db99e0..5fd2d29c 100644 --- a/client/js-sys/src/macro/result.rs +++ b/client/js-sys/src/macro/result.rs @@ -1,8 +1,8 @@ +use crate::hazard::ReturnFromJS; #[cfg(target_feature = "exception-handling")] -use crate::externref::{ +use crate::runtime::externref::{ WAT_INDEX_LOCAL, WAT_INSERT_CONV, WAT_NEXT_IMPORT, WAT_TABLE_IMPORT, WAT_VALUE_LOCAL, }; -use crate::hazard::ReturnFromJS; #[cfg(not(target_feature = "exception-handling"))] const DIRECT_CATCH: &str = " diff --git a/client/js-sys/src/macro/text.rs b/client/js-sys/src/macro/text.rs index fae18c33..769e383a 100644 --- a/client/js-sys/src/macro/text.rs +++ b/client/js-sys/src/macro/text.rs @@ -38,26 +38,8 @@ macro_rules! const_concat { ($($value:expr),* $(,)?) => {{ const VALUES: &[&::core::primitive::str] = &[$($value),*]; const LEN: ::core::primitive::usize = $crate::r#macro::const_concat_len(VALUES); - const VALUE: [::core::primitive::u8; LEN] = { - let mut value = [0; LEN]; - let mut index = 0; - let mut value_index = 0; - - while value_index < VALUES.len() { - let mut local_index = 0; - let bytes = ::core::primitive::str::as_bytes(VALUES[value_index]); - - while local_index < bytes.len() { - value[index] = bytes[local_index]; - index += 1; - local_index += 1; - } - - value_index += 1; - } - - value - }; + const VALUE: [::core::primitive::u8; LEN] = + $crate::r#macro::render_concat::(VALUES); // SAFETY: Joining valid strings keeps the result valid. unsafe { ::core::str::from_utf8_unchecked(&VALUE) } @@ -73,35 +55,8 @@ macro_rules! const_concat_if { ]; const LEN: ::core::primitive::usize = $crate::r#macro::const_concat_if_len(GROUPS); - const VALUE: [::core::primitive::u8; LEN] = { - let mut value = [0; LEN]; - let mut index = 0; - let mut group_index = 0; - - while group_index < GROUPS.len() { - if GROUPS[group_index].0 { - let values = GROUPS[group_index].1; - let mut value_index = 0; - - while value_index < values.len() { - let bytes = ::core::primitive::str::as_bytes(values[value_index]); - let mut byte_index = 0; - - while byte_index < bytes.len() { - value[index] = bytes[byte_index]; - index += 1; - byte_index += 1; - } - - value_index += 1; - } - } - - group_index += 1; - } - - value - }; + const VALUE: [::core::primitive::u8; LEN] = + $crate::r#macro::render_concat_if::(GROUPS); // SAFETY: Joining valid strings keeps the result valid. unsafe { ::core::str::from_utf8_unchecked(&VALUE) } @@ -166,6 +121,63 @@ pub const fn const_concat_if_len(groups: &[(bool, &[&str])]) -> usize { len } +#[must_use] +pub const fn render_concat(values: &[&str]) -> [u8; LEN] { + let mut output = [0; LEN]; + let mut offset = 0; + let mut index = 0; + + while index < values.len() { + offset = append_str(&mut output, offset, values[index]); + index += 1; + } + + output +} + +#[must_use] +pub const fn render_concat_if(groups: &[(bool, &[&str])]) -> [u8; LEN] { + let mut output = [0; LEN]; + let mut offset = 0; + let mut group_index = 0; + + while group_index < groups.len() { + if groups[group_index].0 { + let values = groups[group_index].1; + let mut value_index = 0; + + while value_index < values.len() { + offset = append_str(&mut output, offset, values[value_index]); + value_index += 1; + } + } + + group_index += 1; + } + + output +} + +const fn append_str(output: &mut [u8; LEN], offset: usize, value: &str) -> usize { + let bytes = value.as_bytes(); + let Some(end) = offset.checked_add(bytes.len()) else { + panic!("string append overflows usize"); + }; + assert!(end <= LEN); + + // SAFETY: `end <= LEN` proves that the destination range is in bounds. + // The source is a valid string slice and cannot overlap the output array. + unsafe { + core::ptr::copy_nonoverlapping( + bytes.as_ptr(), + output.as_mut_ptr().add(offset), + bytes.len(), + ); + } + + end +} + const JS_TEMPLATE_PLACEHOLDERS: [&str; 5] = ["$value", "$slot1", "$slot2", "$slot3", "$slot4"]; const fn js_template_placeholder(template: &[u8], index: usize) -> usize { @@ -237,14 +249,7 @@ pub const fn render_js_template( let placeholder = js_template_placeholder(template, input); if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - let replacement = replacements[placeholder].as_bytes(); - let mut byte = 0; - - while byte < replacement.len() { - rendered[output] = replacement[byte]; - output += 1; - byte += 1; - } + output = append_str(&mut rendered, output, replacements[placeholder]); input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); } else { diff --git a/client/js-sys/src/number/mod.rs b/client/js-sys/src/number/mod.rs deleted file mode 100644 index 0ffba725..00000000 --- a/client/js-sys/src/number/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[rustfmt::skip] -#[path ="number.gen.rs"] -mod number; - -pub use self::number::JsNumber; diff --git a/client/js-sys/src/number/number.gen.rs b/client/js-sys/src/number/number.gen.rs deleted file mode 100644 index 56ff968f..00000000 --- a/client/js-sys/src/number/number.gen.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use core::marker::PhantomData; -use crate::JsValue; -use crate::hazard::{IntoJS, JsCast}; - -#[repr(transparent)] -pub struct JsNumber { - value: JsValue, - _type: PhantomData, -} - -impl AsRef for JsNumber { - fn as_ref(&self) -> &JsValue { - &self.value - } -} - -impl From> for JsValue { - fn from(value: JsNumber) -> Self { - value.value - } -} - -unsafe impl JsCast for JsNumber {} - -unsafe impl IntoJS for JsNumber { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } -} diff --git a/client/js-sys/src/closure/mod.rs b/client/js-sys/src/runtime/closure.rs similarity index 90% rename from client/js-sys/src/closure/mod.rs rename to client/js-sys/src/runtime/closure.rs index 02a9e1e7..5166ed0b 100644 --- a/client/js-sys/src/closure/mod.rs +++ b/client/js-sys/src/runtime/closure.rs @@ -1,14 +1,16 @@ -#[rustfmt::skip] -#[path = "closure.gen.rs"] -mod closure; - use alloc::boxed::Box; use core::marker::PhantomData; use core::mem::{self, ManuallyDrop}; use core::ptr; +use crate::JsValue; use crate::hazard::{IntoJS, IntoJsConv}; -use crate::{JsValue, r#macro}; + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "closure.unref")] + fn closure_unref(callback: &JsValue); +} /// Type-erased information stored at the start of every closure allocation. #[doc(hidden)] @@ -115,14 +117,8 @@ impl Drop for ClosureAllocation { } } -#[unsafe(export_name = "__export_closure_drop")] -extern "C" fn closure_drop( - data_0: r#macro::FromJsSlot1, - data_1: r#macro::FromJsSlot2, - data_2: r#macro::FromJsSlot3, - data_3: r#macro::FromJsSlot4, -) { - let data = r#macro::join_from_js::(data_0, data_1, data_2, data_3); +#[crate::js_sys(js_sys = crate)] +fn closure_drop(data: usize) { if data == 0 { return; } @@ -134,28 +130,6 @@ extern "C" fn closure_drop( } } -js_bindgen::unsafe_global_wat! { - "{}", - interpolate r#macro::wat_export!( - "__export_closure_drop", - "closure_drop", - (("data", usize)), - ), -} - -js_bindgen::export_js! { - module = "js_sys", - name = "closure_drop", - required_embeds = [ - r#macro::js_from_embed::(), - ], - "{}", - interpolate r#macro::js_export!( - "closure_drop", - (("data", usize)), - ), -} - js_bindgen::embed_js!( module = "js_sys", name = "closure.unref", @@ -283,9 +257,14 @@ impl Closure { &self.value } + /// # Safety + /// + /// `value` must be a callback produced by the matching `js-sys` closure + /// factory. In particular, it must carry the `unref` method used by + /// [`Closure::drop`]. #[doc(hidden)] #[must_use] - pub fn from_js_value(value: JsValue) -> Self { + pub unsafe fn from_js_value(value: JsValue) -> Self { Self { value, _type: PhantomData, @@ -294,7 +273,7 @@ impl Closure { /// Transfers this closure to JavaScript ownership. /// - /// When supported by the JavaScript runtime, the captured Rust values are + /// When supported by the JavaScript `runtime`, the captured Rust values are /// released after the JavaScript function becomes unreachable. Otherwise, /// the Rust allocation remains alive. #[must_use] @@ -346,6 +325,6 @@ unsafe impl IntoJS for Closure { impl Drop for Closure { fn drop(&mut self) { - closure::closure_unref(&self.value); + closure_unref(&self.value); } } diff --git a/client/js-sys/src/exception.rs b/client/js-sys/src/runtime/exception.rs similarity index 98% rename from client/js-sys/src/exception.rs rename to client/js-sys/src/runtime/exception.rs index 0e2faacb..6080e31c 100644 --- a/client/js-sys/src/exception.rs +++ b/client/js-sys/src/runtime/exception.rs @@ -1,8 +1,8 @@ use core::cell::Cell; -use crate::JsValue; #[cfg(not(target_feature = "exception-handling"))] -use crate::externref; +use super::externref; +use crate::JsValue; #[cfg(target_feature = "exception-handling")] js_bindgen::import_js!( diff --git a/client/js-sys/src/externref.rs b/client/js-sys/src/runtime/externref.rs similarity index 99% rename from client/js-sys/src/externref.rs rename to client/js-sys/src/runtime/externref.rs index 5c9149b2..30fcff8e 100644 --- a/client/js-sys/src/externref.rs +++ b/client/js-sys/src/runtime/externref.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use core::cell::RefCell; -use crate::panic::panic; +use super::panic::panic; use crate::util::PtrConst; pub(crate) const WAT_TABLE_IMPORT: &str = "(import \"js_sys\" \"externref.table\" (table \ diff --git a/client/js-sys/src/runtime/mod.rs b/client/js-sys/src/runtime/mod.rs new file mode 100644 index 00000000..3f9867fd --- /dev/null +++ b/client/js-sys/src/runtime/mod.rs @@ -0,0 +1,11 @@ +pub(crate) mod closure; +pub(crate) mod exception; +pub(crate) mod externref; +mod panic; +mod value; + +pub use self::closure::Closure; +#[doc(hidden)] +pub use self::closure::{ClosureAllocation, ClosureHeader}; +pub use self::panic::{UnwrapThrowExt, panic}; +pub use self::value::JsValue; diff --git a/client/js-sys/src/panic.rs b/client/js-sys/src/runtime/panic.rs similarity index 100% rename from client/js-sys/src/panic.rs rename to client/js-sys/src/runtime/panic.rs diff --git a/client/js-sys/src/value/mod.rs b/client/js-sys/src/runtime/value.rs similarity index 96% rename from client/js-sys/src/value/mod.rs rename to client/js-sys/src/runtime/value.rs index f83a2045..a0d5b897 100644 --- a/client/js-sys/src/value/mod.rs +++ b/client/js-sys/src/runtime/value.rs @@ -1,12 +1,8 @@ -#[rustfmt::skip] -#[path ="value.gen.rs"] -mod value; - use core::marker::PhantomData; use core::mem::{ManuallyDrop, MaybeUninit}; use core::slice; -use crate::externref::{ +use super::externref::{ WAT_GET_CONV, WAT_INDEX_LOCAL, WAT_INSERT_CONV, WAT_INSERT_IMPORTS, WAT_INSERT_LOCALS, WAT_OPTIONAL_INSERT_CONV, WAT_TABLE_IMPORT, WAT_TAKE_CONV, WAT_TAKE_IMPORTS, release, }; @@ -22,6 +18,12 @@ pub struct JsValue { _local: PhantomData<*const ()>, } +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "js_value.partial_eq")] + fn js_value_partial_eq(value1: &JsValue, value2: &JsValue) -> bool; +} + /// The Wasm `ABI` carrier for an owned `externref` table index. #[doc(hidden)] #[repr(transparent)] @@ -276,6 +278,8 @@ impl PartialEq for JsValue { "(value1, value2) => value1 === value2", ); - value::js_value_partial_eq(self, other) + js_value_partial_eq(self, other) } } + +impl Eq for JsValue {} diff --git a/client/js-sys/src/string/string.gen.rs b/client/js-sys/src/string/string.gen.rs deleted file mode 100644 index 14938d3d..00000000 --- a/client/js-sys/src/string/string.gen.rs +++ /dev/null @@ -1,287 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{IntoJS, JsCast}; -use crate::util::{PtrConst, PtrLength, PtrMut}; - -#[derive(Clone, Debug)] -#[repr(transparent)] -pub struct JsString(JsValue); - -impl AsRef for JsString { - fn as_ref(&self) -> &JsValue { - &self.0 - } -} - -impl From for JsValue { - fn from(value: JsString) -> Self { - value.0 - } -} - -unsafe impl JsCast for JsString {} - -unsafe impl IntoJS for JsString { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } -} - -pub(super) fn string_constructor(value: &JsValue) -> JsString { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_constructor", - shim = "js_sys.string_constructor", inputs = [("arg0", & JsValue)], output = JsString,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "string_constructor", - required_embeds = [ - r#macro::js_input_embed::<&JsValue>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "globalThis.String", indirect_call = - "globalThis.String(arg0_0)", inputs = [("arg0", & JsValue)], output = JsString, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_constructor"] - fn string_constructor( - arg0_0: r#macro::InputSlot1<&JsValue>, - arg0_1: r#macro::InputSlot2<&JsValue>, - arg0_2: r#macro::InputSlot3<&JsValue>, - arg0_3: r#macro::InputSlot4<&JsValue>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(value); - unsafe { string_constructor(arg0_0, arg0_1, arg0_2, arg0_3) } - }) -} - -pub(super) unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrLength) -> bool { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_eq", shim = - "js_sys.string_eq", inputs = [("arg0", & JsString), ("arg1", PtrConst < u8 >), ("arg2", - PtrLength < u8 >)], output = bool,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "string_eq", - required_embeds = [ - ("js_sys", "string.eq"), - r#macro::js_input_embed::<&JsString>(), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.eq']", indirect_call = - "this.#jsEmbed.js_sys['string.eq'](arg0_0, arg1_0, arg2_0)", inputs = [("arg0", & - JsString), ("arg1", PtrConst < u8 >), ("arg2", PtrLength < u8 >)], output = bool, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_eq"] - fn string_eq( - arg0_0: r#macro::InputSlot1<&JsString>, - arg0_1: r#macro::InputSlot2<&JsString>, - arg0_2: r#macro::InputSlot3<&JsString>, - arg0_3: r#macro::InputSlot4<&JsString>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - arg2_0: r#macro::InputSlot1>, - arg2_1: r#macro::InputSlot2>, - arg2_2: r#macro::InputSlot3>, - arg2_3: r#macro::InputSlot4>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsString>(string); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(array); - let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::>(len); - unsafe { - string_eq( - arg0_0, - arg0_1, - arg0_2, - arg0_3, - arg1_0, - arg1_1, - arg1_2, - arg1_3, - arg2_0, - arg2_1, - arg2_2, - arg2_3, - ) - } - }) -} - -pub(super) unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_decode", shim = - "js_sys.string_decode", inputs = [("arg0", PtrConst < u8 >), ("arg1", PtrLength < u8 >)], - output = JsString,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "string_decode", - required_embeds = [ - ("js_sys", "string.decode"), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.decode']", indirect_call = - "this.#jsEmbed.js_sys['string.decode'](arg0_0, arg1_0)", inputs = [("arg0", PtrConst < - u8 >), ("arg1", PtrLength < u8 >)], output = JsString, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_decode"] - fn string_decode( - arg0_0: r#macro::InputSlot1>, - arg0_1: r#macro::InputSlot2>, - arg0_2: r#macro::InputSlot3>, - arg0_3: r#macro::InputSlot4>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::>(array); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(len); - unsafe { string_decode(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } - }) -} - -pub(super) fn string_utf8_length(string: &JsString) -> f64 { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_utf8_length", - shim = "js_sys.string_utf8_length", inputs = [("arg0", & JsString)], output = f64,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "string_utf8_length", - required_embeds = [ - ("js_sys", "string.utf8_length"), - r#macro::js_input_embed::<&JsString>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.utf8_length']", - indirect_call = "this.#jsEmbed.js_sys['string.utf8_length'](arg0_0)", inputs = [("arg0", - & JsString)], output = f64, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_utf8_length"] - fn string_utf8_length( - arg0_0: r#macro::InputSlot1<&JsString>, - arg0_1: r#macro::InputSlot2<&JsString>, - arg0_2: r#macro::InputSlot3<&JsString>, - arg0_3: r#macro::InputSlot4<&JsString>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsString>(string); - unsafe { string_utf8_length(arg0_0, arg0_1, arg0_2, arg0_3) } - }) -} - -pub(super) unsafe fn string_encode(string: &JsString, array: PtrMut, len: PtrLength) { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "string_encode", shim = - "js_sys.string_encode", inputs = [("arg0", & JsString), ("arg1", PtrMut < u8 >), ("arg2", - PtrLength < u8 >)],), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "string_encode", - required_embeds = [ - ("js_sys", "string.encode"), - r#macro::js_input_embed::<&JsString>(), - r#macro::js_input_embed::>(), - r#macro::js_input_embed::>(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['string.encode']", indirect_call = - "this.#jsEmbed.js_sys['string.encode'](arg0_0, arg1_0, arg2_0)", inputs = [("arg0", & - JsString), ("arg1", PtrMut < u8 >), ("arg2", PtrLength < u8 >)], - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_encode"] - fn string_encode( - arg0_0: r#macro::InputSlot1<&JsString>, - arg0_1: r#macro::InputSlot2<&JsString>, - arg0_2: r#macro::InputSlot3<&JsString>, - arg0_3: r#macro::InputSlot4<&JsString>, - arg1_0: r#macro::InputSlot1>, - arg1_1: r#macro::InputSlot2>, - arg1_2: r#macro::InputSlot3>, - arg1_3: r#macro::InputSlot4>, - arg2_0: r#macro::InputSlot1>, - arg2_1: r#macro::InputSlot2>, - arg2_2: r#macro::InputSlot3>, - arg2_3: r#macro::InputSlot4>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsString>(string); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::>(array); - let (arg2_0, arg2_1, arg2_2, arg2_3) = r#macro::split_input::>(len); - unsafe { - string_encode( - arg0_0, - arg0_1, - arg0_2, - arg0_3, - arg1_0, - arg1_1, - arg1_2, - arg1_3, - arg2_0, - arg2_1, - arg2_2, - arg2_3, - ) - } - }; -} diff --git a/client/js-sys/src/string/string.js-sys.rs b/client/js-sys/src/string/string.js-sys.rs deleted file mode 100644 index 26f75f0b..00000000 --- a/client/js-sys/src/string/string.js-sys.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::util::{PtrConst, PtrLength, PtrMut}; - -#[js_sys] -extern "js-sys" { - #[derive(Clone, Debug)] - pub type JsString; - - #[js_sys(js_name = "String")] - pub(super) fn string_constructor(value: &JsValue) -> JsString; - - #[js_sys(js_embed = "string.eq")] - pub(super) unsafe fn string_eq( - string: &JsString, - array: PtrConst, - len: PtrLength, - ) -> bool; - - #[js_sys(js_embed = "string.decode")] - pub(super) unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString; - - #[js_sys(js_embed = "string.utf8_length")] - pub(super) fn string_utf8_length(string: &JsString) -> f64; - - #[js_sys(js_embed = "string.encode")] - pub(super) unsafe fn string_encode(string: &JsString, array: PtrMut, len: PtrLength); -} diff --git a/client/js-sys/src/value/value.gen.rs b/client/js-sys/src/value/value.gen.rs deleted file mode 100644 index 4315962e..00000000 --- a/client/js-sys/src/value/value.gen.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use crate::{js_bindgen, r#macro}; -use super::JsValue; -use crate::util::PtrLength; - -pub(super) fn js_value_partial_eq(value1: &JsValue, value2: &JsValue) -> bool { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "js_sys", import = "js_value_partial_eq", - shim = "js_sys.js_value_partial_eq", inputs = [("arg0", & JsValue), ("arg1", & JsValue)], - output = bool,), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "js_value_partial_eq", - required_embeds = [ - ("js_sys", "js_value.partial_eq"), - r#macro::js_input_embed::<&JsValue>(), - r#macro::js_output_embed::(), - r#macro::js_result_embed::(), - ], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.js_sys['js_value.partial_eq']", - indirect_call = "this.#jsEmbed.js_sys['js_value.partial_eq'](arg0_0, arg1_0)", inputs = - [("arg0", & JsValue), ("arg1", & JsValue)], output = bool, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.js_value_partial_eq"] - fn js_value_partial_eq( - arg0_0: r#macro::InputSlot1<&JsValue>, - arg0_1: r#macro::InputSlot2<&JsValue>, - arg0_2: r#macro::InputSlot3<&JsValue>, - arg0_3: r#macro::InputSlot4<&JsValue>, - arg1_0: r#macro::InputSlot1<&JsValue>, - arg1_1: r#macro::InputSlot2<&JsValue>, - arg1_2: r#macro::InputSlot3<&JsValue>, - arg1_3: r#macro::InputSlot4<&JsValue>, - ) -> r#macro::OutputRet; - } - - r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(value1); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::<&JsValue>(value2); - unsafe { - js_value_partial_eq(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) - } - }) -} diff --git a/client/js-sys/src/value/value.js-sys.rs b/client/js-sys/src/value/value.js-sys.rs deleted file mode 100644 index 02549c38..00000000 --- a/client/js-sys/src/value/value.js-sys.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::JsValue; -use crate::util::PtrLength; - -#[js_sys] -extern "js-sys" { - #[js_sys(js_embed = "js_value.partial_eq")] - pub(super) fn js_value_partial_eq(value1: &JsValue, value2: &JsValue) -> bool; -} diff --git a/client/test/Cargo.toml b/client/test/Cargo.toml index bd30282f..2aeb4de4 100644 --- a/client/test/Cargo.toml +++ b/client/test/Cargo.toml @@ -12,7 +12,7 @@ test = false [target.'cfg(all(target_family = "wasm", any(target_os = "none", target_os = "unknown")))'.dependencies] js-bindgen-test-macro = { workspace = true } -js-sys = { workspace = true, features = ["macro"] } +js-sys = { workspace = true } [lints] workspace = true diff --git a/client/wabii/src/lib.rs b/client/wabii/src/lib.rs index 3c231162..a69e7ac5 100644 --- a/client/wabii/src/lib.rs +++ b/client/wabii/src/lib.rs @@ -5,12 +5,22 @@ macro_rules! include_wat { #[expect(unused, reason = "link_section")] const _: () = { const WAT: &[u8] = include_bytes!($path); + const USED: ::core::primitive::usize = WAT.len() + 4; #[repr(C)] - struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; N]); + struct Layout( + [::core::primitive::u8; 4], + [::core::primitive::u8; 4], + [::core::primitive::u8; 4], + [::core::primitive::u8; N], + ); #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout<{ WAT.len() }> = Layout( + #[expect(clippy::cast_possible_truncation, reason = "link_section")] + ::core::primitive::u32::to_le_bytes(USED as ::core::primitive::u32), + #[expect(clippy::cast_possible_truncation, reason = "link_section")] + ::core::primitive::u32::to_le_bytes(USED as ::core::primitive::u32), #[expect(clippy::cast_possible_truncation, reason = "link_section")] ::core::primitive::u32::to_le_bytes(WAT.len() as ::core::primitive::u32), *include_bytes!($path), diff --git a/client/wabii/src/random.64.wat b/client/wabii/src/random.64.wat index 46f2be1d..2ad5ef35 100644 --- a/client/wabii/src/random.64.wat +++ b/client/wabii/src/random.64.wat @@ -2,6 +2,10 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" + ;; block capacity and used length: 393 + "\89\01\00\00" + "\89\01\00\00" + ;; wabii:random.atomics_fill ;; record length: 224 "\e0\00\00\00" diff --git a/client/wabii/src/random.wat b/client/wabii/src/random.wat index d29a8e52..14a56d9f 100644 --- a/client/wabii/src/random.wat +++ b/client/wabii/src/random.wat @@ -2,6 +2,10 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" + ;; block capacity and used length: 313 + "\39\01\00\00" + "\39\01\00\00" + ;; wabii:random.atomics_fill ;; record length: 184 "\b8\00\00\00" diff --git a/client/wabii/src/stdio.64.wat b/client/wabii/src/stdio.64.wat index b59e32e1..86bc0934 100644 --- a/client/wabii/src/stdio.64.wat +++ b/client/wabii/src/stdio.64.wat @@ -2,6 +2,10 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" + ;; block capacity and used length: 252 + "\fc\00\00\00" + "\fc\00\00\00" + ;; wabii:stdio.stdout ;; record length: 121 "\79\00\00\00" diff --git a/client/wabii/src/stdio.wat b/client/wabii/src/stdio.wat index 4b6bb78f..075df05a 100644 --- a/client/wabii/src/stdio.wat +++ b/client/wabii/src/stdio.wat @@ -2,6 +2,10 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" + ;; block capacity and used length: 252 + "\fc\00\00\00" + "\fc\00\00\00" + ;; wabii:stdio.stdout ;; record length: 121 "\79\00\00\00" diff --git a/client/wabii/src/time.wat b/client/wabii/src/time.wat index bee92347..fd0eadc0 100644 --- a/client/wabii/src/time.wat +++ b/client/wabii/src/time.wat @@ -2,6 +2,10 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" + ;; block capacity and used length: 278 + "\16\01\00\00" + "\16\01\00\00" + ;; wabii:time.performance_now ;; record length: 64 "\40\00\00\00" diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs index 88caf3ee..3b2bc23a 100644 --- a/client/web-sys/src/console.gen.rs +++ b/client/web-sys/src/console.gen.rs @@ -2,26 +2,11 @@ #![allow(warnings)] -use js_sys::{js_bindgen, r#macro}; +use js_sys::r#macro; use js_sys::JsValue; use js_sys::hazard::JsCast; pub fn log0() { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log0", shim = - "web_sys.console.log0", inputs = [],), - } - - js_bindgen::import_js! { - module = "web_sys", - name = "console.log0", - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "globalThis.console.log", indirect_call = - "globalThis.console.log()", inputs = [], - ), - } - unsafe extern "C" { #[link_name = "web_sys.console.log0"] fn log0(); @@ -31,22 +16,6 @@ pub fn log0() { } pub fn log(data: &[T]) { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log", shim = - "web_sys.console.log", inputs = [("arg0", & [JsValue])],), - } - - js_bindgen::import_js! { - module = "web_sys", - name = "console.log", - required_embeds = [r#macro::js_input_embed::<&[JsValue]>()], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "globalThis.console.log", indirect_call = - "globalThis.console.log(arg0_0)", inputs = [("arg0", & [JsValue])], - ), - } - unsafe extern "C" { #[link_name = "web_sys.console.log"] fn log( @@ -66,23 +35,6 @@ pub fn log(data: &[T]) { } pub fn log2(data1: &JsValue, data2: &JsValue) { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.log2", shim = - "web_sys.console.log2", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), - } - - js_bindgen::import_js! { - module = "web_sys", - name = "console.log2", - required_embeds = [r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "globalThis.console.log", indirect_call = - "globalThis.console.log(arg0_0, arg1_0)", inputs = [("arg0", & JsValue), ("arg1", & - JsValue)], - ), - } - unsafe extern "C" { #[link_name = "web_sys.console.log2"] fn log2( @@ -105,22 +57,6 @@ pub fn log2(data1: &JsValue, data2: &JsValue) { } pub fn error(data: &JsValue) { - js_bindgen::unsafe_global_wat! { - "{}", interpolate r#macro::wat_import!(module = "web_sys", import = "console.error", shim = - "web_sys.console.error", inputs = [("arg0", & JsValue)],), - } - - js_bindgen::import_js! { - module = "web_sys", - name = "console.error", - required_embeds = [r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate r#macro::js_import!( - direct_open = "", direct_call = "globalThis.console.error", indirect_call = - "globalThis.console.error(arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "web_sys.console.error"] fn error( @@ -136,3 +72,72 @@ pub fn error(data: &JsValue) { unsafe { error(arg0_0, arg0_1, arg0_2, arg0_3) } }; } +const _: () = { + const IMPORTS: &[r#macro::ImportDescriptor] = &[ + r#macro::ImportDescriptor::new( + "web_sys", + "console.log0", + "web_sys.console.log0", + &[], + ::core::option::Option::None, + ::core::option::Option::Some(r#macro::ImportJs { + direct_wrapper: true, + direct_call: "globalThis.console.log()", + indirect_call: "globalThis.console.log()", + required_embeds: &[], + }), + ), + r#macro::ImportDescriptor::new( + "web_sys", + "console.log", + "web_sys.console.log", + &[r#macro::import_input::<&[JsValue]>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(r#macro::ImportJs { + direct_wrapper: true, + direct_call: "globalThis.console.log(arg0_0)", + indirect_call: "globalThis.console.log(arg0_0)", + required_embeds: &[r#macro::js_input_embed::<&[JsValue]>()], + }), + ), + r#macro::ImportDescriptor::new( + "web_sys", + "console.log2", + "web_sys.console.log2", + &[r#macro::import_input::<&JsValue>("arg0"), r#macro::import_input::<&JsValue>("arg1")], + ::core::option::Option::None, + ::core::option::Option::Some(r#macro::ImportJs { + direct_wrapper: true, + direct_call: "globalThis.console.log(arg0_0, arg1_0)", + indirect_call: "globalThis.console.log(arg0_0, arg1_0)", + required_embeds: &[r#macro::js_input_embed::<&JsValue>()], + }), + ), + r#macro::ImportDescriptor::new( + "web_sys", + "console.error", + "web_sys.console.error", + &[r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(r#macro::ImportJs { + direct_wrapper: true, + direct_call: "globalThis.console.error(arg0_0)", + indirect_call: "globalThis.console.error(arg0_0)", + required_embeds: &[r#macro::js_input_embed::<&JsValue>()], + }), + ), + ]; + const WAT_CAPACITY: ::core::primitive::usize = r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: r#macro::ImportSection = r#macro::import_wat::< + WAT_CAPACITY, + >(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: r#macro::ImportSection = r#macro::import_js::< + JS_CAPACITY, + >(IMPORTS); +}; diff --git a/host/dev/src/codegen.rs b/host/dev/src/codegen.rs index 054e3858..7db181d3 100644 --- a/host/dev/src/codegen.rs +++ b/host/dev/src/codegen.rs @@ -68,19 +68,31 @@ fn generate_wat(spec: &Spec) -> String { `.\n;; Do not edit by hand.\n\n", ); - format_section(&mut output, "js_bindgen.import", &spec.imports); - format_section(&mut output, "js_bindgen.embed", &spec.embeds); + format_section(&mut output, "js_bindgen.import", &spec.imports, true); + format_section(&mut output, "js_bindgen.embed", &spec.embeds, false); output } -fn format_section(output: &mut String, section: &str, entries: &[JsEntry]) { +fn format_section(output: &mut String, section: &str, entries: &[JsEntry], framed: bool) { if entries.is_empty() { return; } writeln!(output, "(@custom {section:?}").unwrap(); + if framed { + let used = entries.iter().fold(0u32, |used, entry| { + used.checked_add(4) + .and_then(|used| used.checked_add(record_len(entry))) + .expect("JS section is too large") + }); + writeln!(output, " ;; block capacity and used length: {used}").unwrap(); + write_binary_string(output, &used.to_le_bytes()); + write_binary_string(output, &used.to_le_bytes()); + writeln!(output).unwrap(); + } + for entry in entries { format_entry(output, entry); } diff --git a/host/js-sys-bindgen/Cargo.toml b/host/js-sys-bindgen/Cargo.toml index d33df660..6b08013f 100644 --- a/host/js-sys-bindgen/Cargo.toml +++ b/host/js-sys-bindgen/Cargo.toml @@ -24,6 +24,7 @@ syn = { workspace = true, features = [ "printing", ] } weedle2 = { workspace = true, optional = true } +xxhash-rust = { workspace = true } [dev-dependencies] anyhow = { workspace = true } @@ -37,8 +38,7 @@ tempfile = { workspace = true } wasmparser = { workspace = true } [features] -file = ["macro"] -macro = [] +file = [] web-idl = ["dep:weedle2"] [lints] diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index 4148ffcf..7bd440e5 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -1,7 +1,7 @@ use std::env; use proc_macro2::TokenStream; -use quote::{format_ident, quote_spanned}; +use quote::{ToTokens, format_ident, quote_spanned}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; @@ -9,21 +9,27 @@ use syn::{ Error, Expr, Path, PathArguments, ReturnType, Token, TraitBound, TraitBoundModifier, Type, TypeParamBound, TypeTraitObject, parse_quote_spanned, }; +use xxhash_rust::xxh3::xxh3_128; mod keyword { syn::custom_keyword!(js_sys); } -pub fn closure(input: TokenStream, id: usize) -> Result { +pub fn closure(input: TokenStream) -> Result { let crate_name = env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found"); - closure_with_crate_name(input, id, &crate_name) + let package_name = env::var("CARGO_PKG_NAME").expect("`CARGO_PKG_NAME` not found"); + let package_version = env::var("CARGO_PKG_VERSION").expect("`CARGO_PKG_VERSION` not found"); + + closure_with(input, &crate_name, &package_name, &package_version) } -fn closure_with_crate_name( +pub(crate) fn closure_with( input: TokenStream, - id: usize, crate_name: &str, + package_name: &str, + package_version: &str, ) -> Result { + let input_text = input.to_string(); let ClosureInput { js_sys, trait_object, @@ -32,7 +38,16 @@ fn closure_with_crate_name( let signature = Signature::parse(&trait_object)?; let span = trait_object.span(); let js_sys = js_sys.unwrap_or_else(|| parse_quote_spanned!(span=> ::js_sys)); - let symbol_id = format!("{}_{}", crate_name.replace('-', "_"), id); + // The package identity is part of the descriptor, so the hash is deterministic + // and does not depend on macro expansion order or parallel compilation. + let symbol_id = closure_symbol_hash( + crate_name, + package_name, + package_version, + &input_text, + &trait_object, + &expression, + ); let call_ident = format_ident!("closure_call_{symbol_id}", span = span); let factory_ident = format_ident!("closure_new_{symbol_id}", span = span); let factory_name = syn::LitStr::new(&format!("closure.new.{symbol_id}"), span); @@ -51,31 +66,35 @@ fn closure_with_crate_name( .enumerate() .map(|(index, ty)| format_ident!("arg{index}", span = ty.span())) .collect(); - let output = &signature.output; - let output_decl = if signature.returns_unit { - TokenStream::new() - } else { - quote_spanned!(output.span()=> -> #output) - }; - let call_shim_type = format_ident!("ClosureCallShim{id}", span = span); - let call_impl = format_ident!("closure_call_impl_{symbol_id}", span = span); - let allocate = format_ident!("closure_alloc_{symbol_id}", span = span); + let output = signature.output.as_ref(); + let output_decl = output.map_or_else( + TokenStream::new, + |output| quote_spanned!(output.span()=> -> #output), + ); let closure_bound = if signature.kind == ClosureKind::Shared { - quote_spanned!(span=> ::core::ops::Fn(#(#inputs),*) -> #output) + if let Some(output) = output { + quote_spanned!(span=> ::core::ops::Fn(#(#inputs),*) -> #output) + } else { + quote_spanned!(span=> ::core::ops::Fn(#(#inputs),*)) + } } else { - quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*) -> #output) + if let Some(output) = output { + quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*) -> #output) + } else { + quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*)) + } }; let call_body = if signature.kind == ClosureKind::Shared { quote_spanned! {span=> let callback = unsafe { - &*#js_sys::r#macro::ClosureHeader::callback::(pointer) + &*#js_sys::ClosureHeader::callback::(pointer) }; callback(#(#arguments),*) } } else { quote_spanned! {span=> let callback = unsafe { - &mut *#js_sys::r#macro::ClosureHeader::callback::(pointer) + &mut *#js_sys::ClosureHeader::callback::(pointer) }; callback(#(#arguments),*) } @@ -97,14 +116,14 @@ fn closure_with_crate_name( Ok(quote_spanned! {span=> { - type #call_shim_type = unsafe fn( - *mut #js_sys::r#macro::ClosureHeader, + type CallShim = unsafe fn( + *mut #js_sys::ClosureHeader, #(#inputs),* ) #output_decl; #[allow(clippy::undocumented_unsafe_blocks)] - unsafe fn #call_impl( - pointer: *mut #js_sys::r#macro::ClosureHeader, + unsafe fn call_impl( + pointer: *mut #js_sys::ClosureHeader, #(#arguments: #inputs),* ) #output_decl where @@ -113,15 +132,15 @@ fn closure_with_crate_name( #call_body } - fn #allocate( + fn allocate( callback: F, - ) -> #js_sys::r#macro::ClosureAllocation + ) -> #js_sys::ClosureAllocation where F: #closure_bound + 'static, { - #js_sys::r#macro::ClosureAllocation::new( + #js_sys::ClosureAllocation::new( callback, - #call_impl:: as #call_shim_type, + call_impl:: as CallShim, ) } @@ -133,7 +152,7 @@ fn closure_with_crate_name( ) #output_decl { let pointer = ::core::ptr::with_exposed_provenance_mut(data); let call_shim = unsafe { - #js_sys::r#macro::ClosureHeader::call_shim::<#call_shim_type>(pointer) + #js_sys::ClosureHeader::call_shim::(pointer) }; unsafe { call_shim(pointer.cast(), #(#arguments),*) } } @@ -153,14 +172,39 @@ fn closure_with_crate_name( ) -> #js_sys::JsValue; } - let allocation = #allocate(#expression); + let allocation = allocate(#expression); let value = #factory_ident(allocation.data()); allocation.forget(); - #js_sys::Closure::<#closure>::from_js_value(value) + // SAFETY: `value` is created by the matching closure factory above. + unsafe { #js_sys::Closure::<#closure>::from_js_value(value) } } }) } +fn closure_symbol_hash( + crate_name: &str, + package_name: &str, + package_version: &str, + input: &str, + trait_object: &TypeTraitObject, + expression: &Expr, +) -> String { + let mut descriptor = String::from("closure-v1\0"); + for value in [ + crate_name, + package_name, + package_version, + input, + &trait_object.to_token_stream().to_string(), + &expression.to_token_stream().to_string(), + ] { + descriptor.push_str(value); + descriptor.push('\0'); + } + + format!("{:032x}", xxh3_128(descriptor.as_bytes())) +} + struct ClosureInput { js_sys: Option, trait_object: TypeTraitObject, @@ -169,6 +213,8 @@ struct ClosureInput { impl Parse for ClosureInput { fn parse(input: ParseStream<'_>) -> syn::Result { + // The optional leading `js_sys = path` controls macro hygiene. It is not + // part of the closure trait object or the captured expression. let js_sys = if input.peek(keyword::js_sys) && input.peek2(Token![=]) { input.parse::()?; input.parse::()?; @@ -197,8 +243,7 @@ impl Parse for ClosureInput { struct Signature { kind: ClosureKind, inputs: Punctuated, - output: Type, - returns_unit: bool, + output: Option, } #[derive(Clone, Copy, Eq, PartialEq)] @@ -262,17 +307,18 @@ impl Signature { "expected parenthesized closure arguments", )); }; - let output: Type = match &arguments.output { - ReturnType::Default => parse_quote_spanned!(trait_object.span()=> ()), - ReturnType::Type(_, output) => *output.clone(), + let output = match &arguments.output { + ReturnType::Default => None, + ReturnType::Type(_, output) if matches!(&**output, Type::Tuple(tuple) if tuple.elems.is_empty()) => { + None + } + ReturnType::Type(_, output) => Some(*output.clone()), }; - let returns_unit = matches!(&output, Type::Tuple(tuple) if tuple.elems.is_empty()); Ok(Self { kind, inputs: arguments.inputs.clone(), output, - returns_unit, }) } diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 53d09aaa..5ab3d686 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -16,6 +16,8 @@ pub(crate) fn r#macro( meta::parser(|meta| { if meta.path.is_ident("js_sys") { + // On an exported Rust function, `js_sys` only overrides the crate + // path used by generated support code. if js_sys.is_some() { Err(meta.error("duplicate `js_sys` argument")) } else { @@ -32,8 +34,8 @@ pub(crate) fn r#macro( let span = function.span(); let js_sys: Path = js_sys.unwrap_or_else(|| parse_quote!(::js_sys)); - let js_bindgen: Path = parse_quote!(#js_sys::js_bindgen); - let r#macro: Path = parse_quote!(#js_sys::r#macro); + let js_bindgen_path: Path = parse_quote!(#js_sys::js_bindgen); + let macro_path: Path = parse_quote!(#js_sys::r#macro); let ident = &function.sig.ident; let export_name_value = ident.unraw().to_string(); let export_name = LitStr::new(&export_name_value, ident.span()); @@ -88,7 +90,7 @@ pub(crate) fn r#macro( let slot_alias = format_ident!("FromJsSlot{slot}", span = input.span()); raw_inputs.push(quote_spanned! {input.span()=> - #slot_ident: #r#macro::#slot_alias<#js_ty> + #slot_ident: #macro_path::#slot_alias<#js_ty> }); slots.push(slot_ident); } @@ -98,17 +100,17 @@ pub(crate) fn r#macro( let ty = &reference.elem; join_inputs.push(quote_spanned! {input.span()=> - let #anchor = #r#macro::join_from_js::<#js_ty>(#(#slots),*); + let #anchor = #macro_path::join_from_js::<#js_ty>(#(#slots),*); let #argument = ::core::borrow::Borrow::<#ty>::borrow(&#anchor); }); } else { join_inputs.push(quote_spanned! {input.span()=> - let #argument = #r#macro::join_from_js::<#js_ty>(#(#slots),*); + let #argument = #macro_path::join_from_js::<#js_ty>(#(#slots),*); }); } codegen_inputs.push(quote_spanned! {input.span()=> (#parameter, #js_ty) }); - required_embeds.push(quote_spanned!(input.span()=> #r#macro::js_from_embed::<#js_ty>())); + required_embeds.push(quote_spanned!(input.span()=> #macro_path::js_from_embed::<#js_ty>())); arguments.push(argument); } @@ -117,7 +119,7 @@ pub(crate) fn r#macro( } else { quote_spanned!(span=> #ident(#(#arguments),*)) }; - let raw_name = LitStr::new(&format!("__export_{export_name_value}"), ident.span()); + let raw_export_name = LitStr::new(&format!("__export_{export_name_value}"), ident.span()); let (raw_output, output_argument) = if let Some(output_ty) = output_ty { ( quote_spanned! {output_ty.span()=> @@ -133,7 +135,7 @@ pub(crate) fn r#macro( let raw_body = if output_ty.is_some() { quote_spanned! {span=> #(#join_inputs)* - #r#macro::return_to_js(#call) + #macro_path::return_to_js(#call) } } else { quote_spanned! {span=> @@ -143,38 +145,38 @@ pub(crate) fn r#macro( }; if let Some(output_ty) = output_ty { required_embeds - .push(quote_spanned!(output_ty.span()=> #r#macro::js_return_embed::<#output_ty>())); + .push(quote_spanned!(output_ty.span()=> #macro_path::js_return_embed::<#output_ty>())); } Ok(quote_spanned! {span=> #function const _: () = { - #[unsafe(export_name = #raw_name)] + #[unsafe(export_name = #raw_export_name)] extern "C" fn export_raw( #(#raw_inputs),* ) #raw_output { #raw_body } - #js_bindgen::unsafe_global_wat! { + #js_bindgen_path::unsafe_global_wat! { "{}", - interpolate #r#macro::wat_export!( - #raw_name, + interpolate #macro_path::wat_export!( + #raw_export_name, #export_name, (#(#codegen_inputs),*) #output_argument, ), } - #js_bindgen::export_js! { + #js_bindgen_path::export_js! { module = #crate_name, name = #export_name, required_embeds = [ #(#required_embeds),* ], "{}", - interpolate #r#macro::js_export!( + interpolate #macro_path::js_export!( #export_name, (#(#codegen_inputs),*) #output_argument, diff --git a/host/js-sys-bindgen/src/file.rs b/host/js-sys-bindgen/src/file.rs index 47682d37..b2b92826 100644 --- a/host/js-sys-bindgen/src/file.rs +++ b/host/js-sys-bindgen/src/file.rs @@ -3,7 +3,7 @@ use std::mem; use proc_macro2::TokenStream; use syn::{Error, File, Item, ItemMod, Meta, Path, Result, parse_quote}; -use crate::ImportManager; +use crate::hygiene::ImportManager; use crate::r#macro::{self, ErrorStack}; pub fn file(input: &str, crate_: &str, js_sys: Option) -> Result { @@ -64,8 +64,11 @@ fn process_items( } }; - match r#macro::internal(attr, foreign_mod, Some(crate_), Some(imports)) { - Ok(mut items) => output.append(&mut items), + match r#macro::expand_file(attr, foreign_mod, crate_, imports) { + Ok(items) => match items.into_items() { + Ok(mut items) => output.append(&mut items), + Err(e) => error.push(e), + }, Err((_, e)) => { error.push(e); } diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index bf426377..2dc9facd 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -1,78 +1,50 @@ +use std::collections::HashMap; use std::mem; use std::ops::DerefMut; use std::string::ToString; use itertools::Itertools; use proc_macro2::{Span, TokenStream}; -use quote::{ToTokens, quote, quote_spanned}; +use quote::{quote, quote_spanned}; use syn::spanned::Spanned; use syn::{ - Attribute, Error, FnArg, ForeignItemFn, GenericArgument, GenericParam, Generics, Ident, Item, - ItemFn, ItemImpl, Pat, PatIdent, PatType, Path, PathArguments, Receiver, Result, ReturnType, - Signature, Stmt, Token, Type, TypePath, TypeReference, parse_quote, parse_quote_spanned, + Attribute, Error, FnArg, ForeignItemFn, GenericArgument, GenericParam, Generics, Ident, Pat, + PatIdent, PatType, Path, PathArguments, Receiver, Result, ReturnType, Signature, Token, Type, + TypePath, TypeReference, parse_quote, }; -use crate::Hygiene; +use crate::hygiene::Hygiene; -pub enum Function { - Fn(ItemFn), - Impl(ItemImpl), -} +mod js; +mod options; + +use self::js::ForeignItem; +use self::options::FunctionOptions; -#[derive(Eq, PartialEq)] -pub enum FunctionJsOutput { - Generate { - js_name: Option, - property: bool, - }, - Embed(String), - Import, +pub(crate) struct FunctionImport { + pub(crate) cfg_attrs: Vec, + pub(crate) descriptor: TokenStream, + pub(crate) needs_js_section: bool, + pub(crate) macro_path: Path, } -struct State<'a> { - crate_: &'a str, - namespace: Option<&'a str>, - js_bindgen: Path, - r#macro: Path, - import_name: String, - foreign_name: String, +struct FunctionPlan { inputs: Vec, output_ty: Option, impl_generic_params: TokenStream, - r#type: OutputType, - span: Span, + binding: ForeignItem, } struct InputArg { abi_type: Type, rust_name: Ident, - wat_name: syn::LitStr, + descriptor_name: syn::LitStr, slot_names: [Ident; 4], - type_override: bool, -} - -enum OutputType { - Generate { - js_name: Option, - member: Option, - }, - Embed(String), - Import, -} - -struct Member { - self_ty: Path, - r#type: MemberType, -} - -enum MemberType { - Method, - Getter, - Setter, + uses_abi_override: bool, } impl InputArg { - fn new(index: usize, abi_type: Type, rust_name: Ident, type_override: bool) -> Self { + fn new(index: usize, abi_type: Type, rust_name: Ident, uses_abi_override: bool) -> Self { let span = Span::mixed_site(); let base = format!("arg{index}"); let slot_name = |slot| Ident::new(&format!("{base}_{slot}"), span); @@ -80,188 +52,193 @@ impl InputArg { Self { abi_type, rust_name, - wat_name: syn::LitStr::new(&base, span), + descriptor_name: syn::LitStr::new(&base, span), slot_names: [slot_name(0), slot_name(1), slot_name(2), slot_name(3)], - type_override, + uses_abi_override, } } } -impl Function { - pub fn new( - hygiene: &mut Hygiene<'_>, - js_output: FunctionJsOutput, - namespace: Option<&str>, - crate_: &str, - item: ForeignItemFn, - ) -> Result { - if let Some(constness) = item.sig.constness { - return Err(Error::new_spanned( - constness, - "`const` functions are not supported", - )); - } +pub(crate) fn expand( + hygiene: &mut Hygiene<'_>, + namespace: Option<&str>, + crate_: &str, + js_names: &HashMap, + item: ForeignItemFn, +) -> Result<(TokenStream, FunctionImport)> { + if let Some(constness) = item.sig.constness { + return Err(Error::new_spanned( + constness, + "`const` functions are not supported", + )); + } - if let Some(asyncness) = item.sig.asyncness { - return Err(Error::new_spanned( - asyncness, - "`async` functions are not supported", - )); - } + if let Some(asyncness) = item.sig.asyncness { + return Err(Error::new_spanned( + asyncness, + "`async` functions are not supported", + )); + } - if let Some(variadic) = &item.sig.variadic { - return Err(Error::new_spanned( - variadic, - "variadic functions are not supported", - )); - } + if let Some(variadic) = &item.sig.variadic { + return Err(Error::new_spanned( + variadic, + "variadic functions are not supported", + )); + } - let span = item.span(); - let ForeignItemFn { - attrs, - vis, - mut sig, - .. - } = item; - - let state = State::parse( - crate_, js_output, namespace, hygiene, &attrs, &mut sig, span, - )?; - let wat = state.wat(); - let js = state.js(); - let State { - r#macro, - foreign_name, - inputs, - output_ty, - impl_generic_params, - r#type, + let span = item.span(); + let ForeignItemFn { + mut attrs, + vis, + mut sig, + .. + } = item; + let cfg_attrs: Vec<_> = attrs + .iter() + .filter(|attr| { + let path = attr.path(); + path.is_ident("cfg") || path.is_ident("cfg_attr") + }) + .cloned() + .collect(); + + let options = FunctionOptions::parse(&mut attrs, &sig.ident)?; + + let plan = FunctionPlan::parse( + hygiene, &mut sig, options, namespace, &cfg_attrs, js_names, span, + )?; + let import_name = plan.binding.import_name(namespace, &sig.ident); + let link_name = format!("{crate_}.{import_name}"); + let macro_path = hygiene.r#macro(&cfg_attrs, span); + let import = plan.import_descriptor( + ¯o_path, + crate_, + &import_name, + &link_name, + &cfg_attrs, + span, + ); + let FunctionPlan { + inputs, + output_ty, + impl_generic_params, + binding, + .. + } = plan; + let ident = &sig.ident; + let split_inputs = inputs.iter().map(|input| { + let InputArg { + abi_type, + rust_name, + slot_names: [slot1, slot2, slot3, slot4], + uses_abi_override, .. - } = state; - let ident = &sig.ident; - let split_inputs = inputs.iter().map(|input| { - let InputArg { - abi_type, - rust_name, - slot_names: [slot1, slot2, slot3, slot4], - type_override, - .. - } = input; - let split_input = if *type_override { - quote_spanned!(span=> unsafe { - #r#macro::split_input_as::<#abi_type>(#rust_name) - }) - } else { - quote_spanned!(span=> #r#macro::split_input::<#abi_type>(#rust_name)) - }; - - quote_spanned! {span=> - let (#slot1, #slot2, #slot3, #slot4) = #split_input; - } - }); - let foreign_input_names: Vec<_> = inputs.iter().flat_map(|arg| &arg.slot_names).collect(); - let foreign_input_tys: Vec<_> = inputs - .iter() - .flat_map(|arg| { - let ty = &arg.abi_type; - - [ - quote_spanned!(span=> #r#macro::InputSlot1<#ty>), - quote_spanned!(span=> #r#macro::InputSlot2<#ty>), - quote_spanned!(span=> #r#macro::InputSlot3<#ty>), - quote_spanned!(span=> #r#macro::InputSlot4<#ty>), - ] + } = input; + let split_input = if *uses_abi_override { + quote_spanned!(span=> unsafe { + #macro_path::split_input_as::<#abi_type>(#rust_name) }) - .collect(); - let foreign_output = output_ty.as_ref().map_or_else( - TokenStream::new, - |ty| quote_spanned!(span=> -> #r#macro::OutputRet<#ty>), - ); - - let foreign_call = quote_spanned! {span=> { - #(#split_inputs)* - unsafe { #ident(#(#foreign_input_names),*) } - }}; - let foreign_call = if output_ty.is_some() { - quote_spanned!(span=> #r#macro::join_output(#foreign_call)) } else { - quote_spanned!(span=> #foreign_call;) + quote_spanned!(span=> #macro_path::split_input::<#abi_type>(#rust_name)) }; - let item_fn = parse_quote_spanned! {span=> - #(#attrs)* - #vis #sig { - #wat - - #js - - unsafe extern "C" { - #[link_name = #foreign_name] - fn #ident(#(#foreign_input_names: #foreign_input_tys),*) #foreign_output; - } - - #foreign_call - } - }; - - if let Some(Member { self_ty, .. }) = r#type.member() { - Ok(Self::Impl(parse_quote_spanned! {span=> - impl #impl_generic_params #self_ty { - #item_fn - } - })) - } else { - Ok(Self::Fn(item_fn)) + quote_spanned! {span=> + let (#slot1, #slot2, #slot3, #slot4) = #split_input; } - } -} + }); + let foreign_input_names: Vec<_> = inputs.iter().flat_map(|arg| &arg.slot_names).collect(); + let foreign_input_tys: Vec<_> = inputs + .iter() + .flat_map(|arg| { + let ty = &arg.abi_type; + + [ + quote_spanned!(span=> #macro_path::InputSlot1<#ty>), + quote_spanned!(span=> #macro_path::InputSlot2<#ty>), + quote_spanned!(span=> #macro_path::InputSlot3<#ty>), + quote_spanned!(span=> #macro_path::InputSlot4<#ty>), + ] + }) + .collect(); + let foreign_output = output_ty.as_ref().map_or_else( + TokenStream::new, + |ty| quote_spanned!(span=> -> #macro_path::OutputRet<#ty>), + ); + + let foreign_call = quote_spanned! {span=> { + #(#split_inputs)* + unsafe { #ident(#(#foreign_input_names),*) } + }}; + let foreign_call = if output_ty.is_some() { + quote_spanned!(span=> #macro_path::join_output(#foreign_call)) + } else { + quote_spanned!(span=> #foreign_call;) + }; + + let item_fn = quote_spanned! {span=> + #(#attrs)* + #vis #sig { + unsafe extern "C" { + #[link_name = #link_name] + fn #ident(#(#foreign_input_names: #foreign_input_tys),*) #foreign_output; + } -impl From for Item { - fn from(value: Function) -> Self { - match value { - Function::Fn(item) => item.into(), - Function::Impl(item) => item.into(), + #foreign_call } - } -} + }; -impl ToTokens for Function { - fn to_tokens(&self, tokens: &mut TokenStream) { - match self { - Self::Fn(item) => item.to_tokens(tokens), - Self::Impl(item) => item.to_tokens(tokens), + let item = if let Some(owner) = binding.owner() { + quote_spanned! {span=> + impl #impl_generic_params #owner { + #item_fn + } } - } -} + } else { + item_fn + }; -impl Default for FunctionJsOutput { - fn default() -> Self { - Self::Generate { - js_name: None, - property: false, - } - } + Ok((item, import)) } -impl<'a> State<'a> { +impl FunctionPlan { fn parse( - crate_: &'a str, - js_output: FunctionJsOutput, - namespace: Option<&'a str>, - hygiene: &'a mut Hygiene<'_>, - outer_attrs: &'a [Attribute], + hygiene: &mut Hygiene<'_>, sig: &mut Signature, + options: FunctionOptions, + namespace: Option<&str>, + cfg_attrs: &[Attribute], + js_names: &HashMap, span: Span, ) -> Result { - let import_name = if let Some(namespace) = namespace { - format!("{namespace}.{}", sig.ident) - } else { - sig.ident.to_string() + let external_implementation = options.import || options.embed.is_some(); + let (inputs, self_ty) = + Self::parse_inputs(hygiene, sig, cfg_attrs, span, external_implementation)?; + let binding = + Self::resolve_binding(options, sig, self_ty, namespace, js_names, &inputs, span)?; + let output_ty = match &sig.output { + ReturnType::Default => None, + ReturnType::Type(_, ty) => Some(*ty.clone()), }; - let foreign_name = format!("{crate_}.{import_name}"); - let mut self_ty = None; + let impl_generic_params = Self::impl_generic_params(&binding, &mut sig.generics); + Ok(Self { + inputs, + output_ty, + impl_generic_params, + binding, + }) + } + + fn parse_inputs( + hygiene: &mut Hygiene<'_>, + sig: &mut Signature, + cfg_attrs: &[Attribute], + span: Span, + external_implementation: bool, + ) -> Result<(Vec, Option)> { + let mut self_ty = None; let inputs = sig .inputs .iter_mut() @@ -277,17 +254,17 @@ impl<'a> State<'a> { }) = pat.deref_mut() && inner_attrs.is_empty() { - let mut r#type = None; + let mut abi_override = None; - if let Some(attr) = attrs - .extract_if(.., |attr| attr.path().is_ident("js_sys")) - .next() - { + // `#[js_sys(type = T)]` applies to this parameter only. It + // overrides the `ABI` conversion while preserving the public + // Rust signature. + for attr in attrs.extract_if(.., |attr| attr.path().is_ident("js_sys")) { attr.parse_nested_meta(|meta| { if meta.path.is_ident("type") { meta.input.parse::()?; - if r#type.replace(meta.input.parse::()?).is_some() { + if abi_override.replace(meta.input.parse::()?).is_some() { Err(meta.error("duplicate attribute")) } else { Ok(()) @@ -298,10 +275,15 @@ impl<'a> State<'a> { })?; } - let type_override = r#type.is_some(); - let r#type = r#type.unwrap_or_else(|| *ty.clone()); + let uses_abi_override = abi_override.is_some(); + let abi_type = abi_override.unwrap_or_else(|| *ty.clone()); - Ok(InputArg::new(index, r#type, ident.clone(), type_override)) + Ok(InputArg::new( + index, + abi_type, + ident.clone(), + uses_abi_override, + )) } else if let FnArg::Receiver(Receiver { attrs, reference: None, @@ -318,7 +300,7 @@ impl<'a> State<'a> { }) = ty.deref_mut() && let Type::Path(TypePath { qself: None, path }) = elem.deref_mut() { - if !matches!(&js_output, FunctionJsOutput::Generate { .. }) { + if external_implementation { return Err(Error::new_spanned( path, "`self` is not supported with `js_import` and `js_embed`", @@ -326,7 +308,7 @@ impl<'a> State<'a> { } self_ty = Some(path.clone()); - let js_value = hygiene.js_value(outer_attrs, span); + let js_value = hygiene.js_value(cfg_attrs, span); Ok(InputArg::new( index, parse_quote! { #and_token #js_value }, @@ -339,73 +321,180 @@ impl<'a> State<'a> { }) .collect::>>()?; - let r#type = match js_output { - FunctionJsOutput::Generate { js_name, property } => { - let member = if let Some(self_ty) = self_ty { - let r#type = if property { - match (sig.inputs.len(), &sig.output) { - (1, ReturnType::Type(..)) => MemberType::Getter, - (2, ReturnType::Default) => MemberType::Setter, - _ => { - return Err(Error::new( - span, - "`property` requires a getter or setter signature", - )); - } - } - } else { - MemberType::Method - }; + Ok((inputs, self_ty)) + } - Some(Member { self_ty, r#type }) - } else { - if property { - return Err(Error::new(span, "`property` requires `self` parameter")); - } + fn resolve_binding( + options: FunctionOptions, + sig: &Signature, + self_ty: Option, + namespace: Option<&str>, + js_names: &HashMap, + inputs: &[InputArg], + span: Span, + ) -> Result { + let FunctionOptions { + js_name, + static_of, + variadic, + constructor, + getter, + setter, + embed, + import, + } = options; + + if import { + return Ok(ForeignItem::Import); + } + if let Some(embed) = embed { + return Ok(ForeignItem::Embed(embed)); + } + + let js_inputs: Vec<_> = inputs + .iter() + .map(|input| input.slot_names[0].to_string()) + .collect(); + let argument_count = sig.inputs.len() - usize::from(self_ty.is_some()); + + if constructor && self_ty.is_some() { + return Err(Error::new( + span, + "`constructor` cannot be used with a `self` parameter", + )); + } + if self_ty.is_some() && static_of.is_some() { + return Err(Error::new( + span, + "`static_of` cannot be used with a `self` parameter", + )); + } + if variadic && argument_count == 0 { + return Err(Error::new( + span, + "`variadic` requires at least one argument", + )); + } + + if constructor { + // `constructor` applies to this foreign function. Its return type + // selects the Rust `impl` owner and JavaScript invokes it with `new`. + let owner = Self::constructor_type(&sig.output)?; + let name = js_name.unwrap_or_else(|| Self::type_js_name(&owner, js_names)); + let path = ForeignItem::global_path(namespace, &name); - None - }; + return Ok(ForeignItem::constructor(owner, &path, variadic, &js_inputs)); + } - OutputType::Generate { js_name, member } + let name = getter + .as_ref() + .or(setter.as_ref()) + .cloned() + .or(js_name) + .unwrap_or_else(|| sig.ident.to_string()); + let (owner, path, receiver) = + Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); + + if getter.is_some() { + if argument_count != 0 || !matches!(&sig.output, ReturnType::Type(..)) { + return Err(Error::new( + span, + "`getter` requires no arguments and a return value", + )); } - FunctionJsOutput::Embed(embed) => OutputType::Embed(embed), - FunctionJsOutput::Import => OutputType::Import, - }; - let output_ty = match &sig.output { - ReturnType::Default => None, - ReturnType::Type(_, ty) => Some(*ty.clone()), + Ok(ForeignItem::getter(owner, path)) + } else if setter.is_some() { + if argument_count != 1 || !matches!(&sig.output, ReturnType::Default) { + return Err(Error::new( + span, + "`setter` requires one argument and no return value", + )); + } + + Ok(ForeignItem::setter(owner, &path, receiver, &js_inputs)) + } else { + Ok(ForeignItem::call( + owner, path, receiver, variadic, namespace, &js_inputs, + )) + } + } + + fn member_path( + static_of: Option, + self_ty: Option, + name: &str, + namespace: Option<&str>, + js_names: &HashMap, + inputs: &[String], + ) -> (Option, String, bool) { + // `static_of` attaches the declaration to a type, an explicit `self` + // parameter makes it an instance member, and neither means a global. + if let Some(owner) = static_of { + let type_name = Self::type_js_name(&owner, js_names); + let path = ForeignItem::global_path(namespace, &format!("{type_name}.{name}")); + + (Some(owner), path, false) + } else if let Some(owner) = self_ty { + (Some(owner), format!("{}.{name}", inputs[0]), true) + } else { + (None, ForeignItem::global_path(namespace, name), false) + } + } + + fn constructor_type(output: &ReturnType) -> Result { + let ReturnType::Type(_, output) = output else { + return Err(Error::new_spanned( + output, + "`constructor` requires a return type", + )); }; - let impl_generic_params = Self::impl_generic_params(&r#type, &mut sig.generics); + Self::constructor_type_from(output) + } - let js_bindgen = hygiene.js_bindgen(outer_attrs, span); - let r#macro = hygiene.r#macro(outer_attrs, span); + fn type_js_name(owner: &Path, js_names: &HashMap) -> String { + let rust_name = owner + .segments + .last() + .expect("a type path always contains a segment") + .ident + .to_string(); - Ok(Self { - crate_, - namespace, - js_bindgen, - r#macro, - import_name, - foreign_name, - inputs, - output_ty, - impl_generic_params, - r#type, - span, - }) + js_names.get(&rust_name).cloned().unwrap_or(rust_name) + } + + fn constructor_type_from(output: &Type) -> Result { + let Type::Path(TypePath { qself: None, path }) = output else { + return Err(Error::new_spanned( + output, + "`constructor` requires a path return type", + )); + }; + let segment = path + .segments + .last() + .expect("a type path always contains a segment"); + + if segment.ident == "Result" + && let PathArguments::AngleBracketed(arguments) = &segment.arguments + && let Some(GenericArgument::Type(output)) = arguments.args.first() + { + return Self::constructor_type_from(output); + } + + Ok(path.clone()) } // Extract type generics from signature that are part of `impl `. - fn impl_generic_params(r#type: &OutputType, generics: &mut Generics) -> TokenStream { - if let Some(member) = r#type.member() { + fn impl_generic_params(binding: &ForeignItem, generics: &mut Generics) -> TokenStream { + if let Some(owner) = binding.owner() { let mut fn_generic_params: Vec<_> = mem::take(&mut generics.params).into_iter().collect(); let impl_generic_params: Vec<_> = fn_generic_params .extract_if(.., |param| { - for path in &member.self_ty.segments { + for path in &owner.segments { if let PathArguments::AngleBracketed(args) = &path.arguments { for arg in &args.args { match (&*param, arg) { @@ -453,83 +542,28 @@ impl<'a> State<'a> { } } - fn wat(&self) -> Stmt { + fn import_descriptor( + &self, + macro_path: &Path, + crate_: &str, + import_name: &str, + link_name: &str, + cfg_attrs: &[Attribute], + span: Span, + ) -> FunctionImport { let Self { - crate_, - js_bindgen, - r#macro, - import_name, - foreign_name, inputs, output_ty, - span, + binding, .. } = self; - let inputs = inputs.iter().map(|input| { - let name = &input.wat_name; + let input_descriptors = inputs.iter().map(|input| { + let name = &input.descriptor_name; let ty = &input.abi_type; - quote_spanned!(*span=> (#name, #ty)) + quote_spanned!(span=> #macro_path::import_input::<#ty>(#name)) }); - let output = output_ty.iter(); - - parse_quote_spanned! {*span=> - #js_bindgen::unsafe_global_wat! { - "{}", - interpolate #r#macro::wat_import!( - module = #crate_, - import = #import_name, - shim = #foreign_name, - inputs = [#(#inputs),*], - #(output = #output,)* - ), - } - } - } - - fn js(&self) -> Option { - let Self { - crate_, - js_bindgen, - r#macro, - import_name, - inputs, - output_ty, - r#type, - span, - .. - } = self; let input_tys: Vec<_> = inputs.iter().map(|input| &input.abi_type).collect(); - let input_names: Vec<_> = inputs.iter().map(|input| &input.wat_name).collect(); - let input_value_names: Vec<_> = inputs - .iter() - .map(|input| input.slot_names[0].to_string()) - .collect(); - let output_tys: Vec<_> = output_ty.iter().collect(); - - let js_path = match r#type { - OutputType::Generate { js_name, member } => { - let base = if member.is_some() { - input_value_names[0].as_str() - } else { - "globalThis" - }; - - if let Some(js_name) = js_name { - if let Some(namespace) = self.namespace { - format!("{base}.{namespace}.{js_name}") - } else { - format!("{base}.{js_name}") - } - } else { - format!("{base}.{import_name}") - } - } - OutputType::Embed(name) => { - format!("this.#jsEmbed.{crate_}['{name}']") - } - OutputType::Import => return None, - }; let mut unique_inputs = Vec::new(); @@ -541,83 +575,81 @@ impl<'a> State<'a> { let mut required_embeds = Vec::new(); - if let OutputType::Embed(name) = &r#type { - required_embeds.push(quote_spanned!(*span=> (#crate_, #name))); + if let ForeignItem::Embed(name) = binding { + required_embeds.push(quote_spanned!(span=> (#crate_, #name))); } for ty in &unique_inputs { - required_embeds.push(quote_spanned!(*span=> #r#macro::js_input_embed::<#ty>())); + required_embeds.push(quote_spanned!(span=> #macro_path::js_input_embed::<#ty>())); } - for &ty in &output_tys { - required_embeds.push(quote_spanned!(*span=> #r#macro::js_output_embed::<#ty>())); - required_embeds.push(quote_spanned!(*span=> #r#macro::js_result_embed::<#ty>())); + if let Some(ty) = output_ty { + required_embeds.push(quote_spanned!(span=> #macro_path::js_output_embed::<#ty>())); + required_embeds.push(quote_spanned!(span=> #macro_path::js_result_embed::<#ty>())); } - let required_embeds = if required_embeds.is_empty() { - [].as_slice() - } else { - &[quote_spanned!(*span=> required_embeds = [#(#required_embeds),*])] + let js = match binding { + ForeignItem::Generate { + direct_wrapper, + direct_call, + indirect_call, + .. + } => Some(quote_spanned! {span=> + #macro_path::ImportJs { + direct_wrapper: #direct_wrapper, + direct_call: #direct_call, + indirect_call: #indirect_call, + required_embeds: &[#(#required_embeds),*], + } + }), + ForeignItem::Embed(name) => { + let path = format!("this.#jsEmbed.{crate_}['{name}']"); + let arguments = inputs + .iter() + .map(|input| input.slot_names[0].to_string()) + .join(", "); + let indirect_call = format!("{path}({arguments})"); + + Some(quote_spanned! {span=> + #macro_path::ImportJs { + direct_wrapper: false, + direct_call: #path, + indirect_call: #indirect_call, + required_embeds: &[#(#required_embeds),*], + } + }) + } + ForeignItem::Import => None, }; - - let input_names_joined = input_value_names.iter().join(", "); - let call_input_names_joined = if r#type.member().is_some() { - input_value_names.iter().skip(1).join(", ") + let js_value = if let Some(js) = &js { + quote_spanned!(span=> ::core::option::Option::Some(#js)) } else { - input_names_joined.clone() + quote_spanned!(span=> ::core::option::Option::None) }; - let js_inputs: Vec<_> = input_names - .iter() - .zip(input_tys.iter()) - .map(|(name, ty)| quote_spanned!(*span=> (#name, #ty))) - .collect(); - let direct_fn_open = if r#type.member().is_none() { - quote_spanned!(*span=> "") - } else { - quote_spanned!(*span=> - #r#macro::js_function!("(", ") => ", #(#js_inputs),*) + let output = if let Some(output) = output_ty { + quote_spanned!(span=> + ::core::option::Option::Some(#macro_path::import_output::<#output>()) ) - }; - let direct_js_call = if let Some(member) = r#type.member() { - match member.r#type { - MemberType::Method => format!("{js_path}({call_input_names_joined})"), - MemberType::Getter => js_path.clone(), - MemberType::Setter => format!("{js_path} = {call_input_names_joined}"), - } } else { - js_path.clone() + quote_spanned!(span=> ::core::option::Option::None) }; - let indirect_js_call = if r#type.member().is_some() { - direct_js_call.clone() - } else { - format!("{js_path}({input_names_joined})") + let needs_js_section = js.is_some(); + let descriptor = quote_spanned! {span=> + #macro_path::ImportDescriptor::new( + #crate_, + #import_name, + #link_name, + &[#(#input_descriptors),*], + #output, + #js_value, + ) }; - let output = output_ty.iter(); - - Some(parse_quote_spanned! {*span=> - #js_bindgen::import_js! { - module = #crate_, - name = #import_name, - #(#required_embeds,)* - "{}", - interpolate #r#macro::js_import!( - direct_open = #direct_fn_open, - direct_call = #direct_js_call, - indirect_call = #indirect_js_call, - inputs = [#(#js_inputs),*], - #(output = #output,)* - ), - } - }) - } -} -impl OutputType { - fn member(&self) -> Option<&Member> { - if let Self::Generate { member, .. } = self { - member.as_ref() - } else { - None + FunctionImport { + cfg_attrs: cfg_attrs.to_vec(), + descriptor, + needs_js_section, + macro_path: macro_path.clone(), } } } diff --git a/host/js-sys-bindgen/src/function/js.rs b/host/js-sys-bindgen/src/function/js.rs new file mode 100644 index 00000000..622d2ea6 --- /dev/null +++ b/host/js-sys-bindgen/src/function/js.rs @@ -0,0 +1,145 @@ +use itertools::Itertools; +use syn::{Ident, Path}; + +/// The final JavaScript binding selected for a foreign function. +pub(super) enum ForeignItem { + Generate { + /// Rust type receiving the generated method, if this is not a free + /// function. + owner: Option, + /// Whether the direct conversion path must wrap `direct_call` in a + /// function. + direct_wrapper: bool, + /// Function reference or expression used by the direct conversion path. + direct_call: String, + /// Expression used when argument or result conversion requires a + /// wrapper. + indirect_call: String, + }, + Embed(String), + Import, +} + +impl ForeignItem { + pub(super) fn owner(&self) -> Option<&Path> { + let Self::Generate { owner, .. } = self else { + return None; + }; + + owner.as_ref() + } + + pub(super) fn import_name(&self, namespace: Option<&str>, rust_name: &Ident) -> String { + let name = if self.owner().is_some() { + format!("{}.{}", self.owner_name(), rust_name) + } else { + rust_name.to_string() + }; + + if let Some(namespace) = namespace { + format!("{namespace}.{name}") + } else { + name + } + } + + pub(super) fn call( + owner: Option, + path: String, + receiver: bool, + variadic: bool, + namespace: Option<&str>, + inputs: &[String], + ) -> Self { + let arguments = Self::arguments(inputs, receiver, variadic); + let indirect_call = format!("{path}({arguments})"); + + // Only a bare global function can be passed directly. Calls through a + // `namespace`, instance, or static member need a wrapper to preserve their + // receiver; `variadic` calls need one to emit the spread expression. + let direct_wrapper = namespace.is_some() || owner.is_some() || variadic; + let direct_call = if direct_wrapper { + indirect_call.clone() + } else { + path + }; + + Self::Generate { + owner, + direct_wrapper, + direct_call, + indirect_call, + } + } + + pub(super) fn constructor(owner: Path, path: &str, variadic: bool, inputs: &[String]) -> Self { + let arguments = Self::arguments(inputs, false, variadic); + let call = format!("new {path}({arguments})"); + + Self::Generate { + owner: Some(owner), + direct_wrapper: true, + direct_call: call.clone(), + indirect_call: call, + } + } + + pub(super) fn getter(owner: Option, path: String) -> Self { + Self::Generate { + owner, + direct_wrapper: true, + direct_call: path.clone(), + indirect_call: path, + } + } + + pub(super) fn setter( + owner: Option, + path: &str, + receiver: bool, + inputs: &[String], + ) -> Self { + let arguments = Self::arguments(inputs, receiver, false); + let call = format!("{path} = {arguments}"); + + Self::Generate { + owner, + direct_wrapper: true, + direct_call: call.clone(), + indirect_call: call, + } + } + + pub(super) fn global_path(namespace: Option<&str>, name: &str) -> String { + if let Some(namespace) = namespace { + format!("globalThis.{namespace}.{name}") + } else { + format!("globalThis.{name}") + } + } + + fn owner_name(&self) -> &Ident { + self.owner() + .and_then(|owner| owner.segments.last()) + .map(|segment| &segment.ident) + .expect("static and instance bindings always have an owner") + } + + fn arguments(inputs: &[String], receiver: bool, variadic: bool) -> String { + let inputs = if receiver { &inputs[1..] } else { inputs }; + + if variadic { + let (last, inputs) = inputs + .split_last() + .expect("variadic bindings always have an argument"); + + if inputs.is_empty() { + format!("...{last}") + } else { + format!("{}, ...{last}", inputs.iter().join(", ")) + } + } else { + inputs.iter().join(", ") + } + } +} diff --git a/host/js-sys-bindgen/src/function/options.rs b/host/js-sys-bindgen/src/function/options.rs new file mode 100644 index 00000000..0a757f7b --- /dev/null +++ b/host/js-sys-bindgen/src/function/options.rs @@ -0,0 +1,167 @@ +use syn::{Attribute, Error, Ident, LitStr, Path, Result}; + +/// Options read from `#[js_sys(...)]` on a foreign function declaration. +#[derive(Default)] +pub(super) struct FunctionOptions { + /// Overrides the JavaScript function or constructor name. + pub(super) js_name: Option, + /// Makes the foreign function a static member of this Rust type. + pub(super) static_of: Option, + /// Spreads the foreign function's last argument at the JavaScript call + /// site. + pub(super) variadic: bool, + /// Generates a JavaScript `new` expression for the foreign function. + pub(super) constructor: bool, + /// Reads this JavaScript property instead of calling a function. + pub(super) getter: Option, + /// Writes this JavaScript property instead of calling a function. + pub(super) setter: Option, + /// Uses a named JavaScript implementation embedded by the current crate. + pub(super) embed: Option, + /// Leaves the JavaScript implementation to the import object. + pub(super) import: bool, +} + +impl FunctionOptions { + pub(super) fn parse(attrs: &mut Vec, rust_name: &Ident) -> Result { + let mut options = Self::default(); + + for attr in attrs.extract_if(.., |attr| attr.path().is_ident("js_sys")) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("js_name") { + let name = meta.value()?.parse::()?.value(); + + if options.js_name.replace(name).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } + } else if meta.path.is_ident("static_of") { + let owner = meta.value()?.parse()?; + + if options.static_of.replace(owner).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } + } else if meta.path.is_ident("variadic") { + parse_flag(&meta, "variadic", &mut options.variadic) + } else if meta.path.is_ident("constructor") { + parse_flag(&meta, "constructor", &mut options.constructor) + } else if meta.path.is_ident("getter") { + let name = if meta.input.is_empty() { + rust_name.to_string() + } else { + meta.value()?.parse::()?.value() + }; + + if options.getter.replace(name).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } + } else if meta.path.is_ident("setter") { + let name = if meta.input.is_empty() { + infer_setter_property(rust_name)? + } else { + meta.value()?.parse::()?.value() + }; + + if options.setter.replace(name).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } + } else if meta.path.is_ident("js_embed") { + let name = meta.value()?.parse::()?.value(); + + if options.embed.replace(name).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } + } else if meta.path.is_ident("js_import") { + parse_flag(&meta, "js_import", &mut options.import) + } else { + Err(meta.error("unsupported attribute")) + } + })?; + } + + options.validate(rust_name)?; + Ok(options) + } + + fn validate(&self, rust_name: &Ident) -> Result<()> { + let source_count = usize::from(self.import) + usize::from(self.embed.is_some()); + let operation_count = usize::from(self.constructor) + + usize::from(self.getter.is_some()) + + usize::from(self.setter.is_some()); + let has_property = self.getter.is_some() || self.setter.is_some(); + let has_binding_options = self.js_name.is_some() + || self.static_of.is_some() + || operation_count != 0 + || self.variadic; + + if source_count > 1 || source_count == 1 && has_binding_options { + return Err(Error::new_spanned( + rust_name, + "`js_import` and `js_embed` cannot be combined with JavaScript binding options", + )); + } + if operation_count > 1 { + return Err(Error::new_spanned( + rust_name, + "`constructor`, `getter`, and `setter` are mutually exclusive", + )); + } + if self.constructor && self.static_of.is_some() { + return Err(Error::new_spanned( + rust_name, + "`constructor` cannot be combined with `static_of`", + )); + } + if has_property && self.js_name.is_some() { + return Err(Error::new_spanned( + rust_name, + "`js_name` cannot be combined with `getter` or `setter`; specify the field on the \ + property operation", + )); + } + if has_property && self.variadic { + return Err(Error::new_spanned( + rust_name, + "`variadic` cannot be combined with `getter` or `setter`", + )); + } + + Ok(()) + } +} + +fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, name: &str, value: &mut bool) -> Result<()> { + if !meta.input.is_empty() { + return Err(meta.error(format!("`{name}` supports no values"))); + } + if *value { + return Err(meta.error("duplicate attribute")); + } + + *value = true; + Ok(()) +} + +fn infer_setter_property(ident: &Ident) -> Result { + let name = ident.to_string(); + let Some(property) = name + .strip_prefix("set_") + .filter(|property| !property.is_empty()) + else { + return Err(Error::new_spanned( + ident, + "`setter` cannot infer a field name; use `setter = \"field\"`", + )); + }; + + Ok(property.to_owned()) +} diff --git a/host/js-sys-bindgen/src/hygiene.rs b/host/js-sys-bindgen/src/hygiene.rs index 0b3d27cd..aac54a54 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -1,91 +1,107 @@ -use std::borrow::Cow; - +#[cfg(any(feature = "file", feature = "web-idl", test))] use foldhash::fast::FixedState; +#[cfg(any(feature = "file", feature = "web-idl", test))] use hashbrown::{HashMap, HashSet}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; -use syn::{Attribute, Ident, ItemUse, Path, parse_quote, parse_quote_spanned}; - -pub enum Hygiene<'a> { +use syn::{Attribute, Ident, Path, parse_quote_spanned}; +#[cfg(any(feature = "file", feature = "web-idl", test))] +use syn::{ItemUse, parse_quote}; + +pub(crate) enum Hygiene<'a> { + /// File generation mode: emit short paths and record the required `use` + /// items. + #[cfg(any(feature = "file", feature = "web-idl", test))] Imports(&'a mut ImportManager), - Hygiene { js_sys: Option<&'a Path> }, + /// Procedural macro mode: emit paths qualified through the selected crate. + Qualified { js_sys: Option<&'a Path> }, } +#[cfg_attr( + not(any(feature = "file", feature = "web-idl", test)), + expect( + unused_variables, + reason = "attributes are only consumed by source-generation hygiene" + ) +)] impl Hygiene<'_> { pub(crate) fn js_value(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.js_sys_push(attrs, parse_quote_spanned!(span=> JsValue)); - parse_quote_spanned!(span=> JsValue) - } - Hygiene::Hygiene { js_sys } => Self::with_js_sys(*js_sys, "e!(JsValue), span), - } - } - - pub(crate) fn js_bindgen(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.js_sys_push(attrs, parse_quote_spanned!(span=> js_bindgen)); - parse_quote_spanned!(span=> js_bindgen) - } - Hygiene::Hygiene { js_sys } => Self::with_js_sys(*js_sys, "e!(js_bindgen), span), - } + self.js_sys_item(attrs, &parse_quote_spanned!(span=> JsValue), span) } pub(crate) fn js_cast(&mut self, attrs: &[Attribute], span: Span) -> Path { - match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> JsCast)); - parse_quote_spanned!(span=> JsCast) - } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::JsCast), span) - } - } + self.hazard_item(attrs, &parse_quote_spanned!(span=> JsCast), span) } pub(crate) fn js_into(&mut self, attrs: &[Attribute], span: Span) -> Path { + self.hazard_item(attrs, &parse_quote_spanned!(span=> IntoJS), span) + } + + pub(crate) fn r#macro(&mut self, attrs: &[Attribute], span: Span) -> Path { + self.js_sys_item(attrs, &parse_quote_spanned!(span=> r#macro), span) + } + + fn js_sys_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { + #[cfg(any(feature = "file", feature = "web-idl", test))] Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> IntoJS)); - parse_quote_spanned!(span=> IntoJS) + imports.js_sys_push(attrs, ident.clone()); + parse_quote_spanned!(span=> #ident) } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::IntoJS), span) + Hygiene::Qualified { js_sys } => { + Self::with_js_sys(*js_sys, &ident.to_token_stream(), span) } } } - pub(crate) fn r#macro(&mut self, attrs: &[Attribute], span: Span) -> Path { + fn hazard_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { + #[cfg(any(feature = "file", feature = "web-idl", test))] Hygiene::Imports(imports) => { - imports.js_sys_push(attrs, parse_quote_spanned!(span=> r#macro)); - parse_quote_spanned!(span=> r#macro) + imports.hazard_push(attrs, ident.clone()); + parse_quote_spanned!(span=> #ident) + } + Hygiene::Qualified { js_sys } => { + Self::with_js_sys(*js_sys, "e!(hazard::#ident), span) } - Hygiene::Hygiene { js_sys } => Self::with_js_sys(*js_sys, "e!(r#macro), span), } } pub(crate) fn as_ref(&mut self, span: Span) -> Path { match self { + #[cfg(any(feature = "file", feature = "web-idl", test))] Hygiene::Imports(_) => { parse_quote_spanned!(span=> AsRef) } - Hygiene::Hygiene { .. } => { + Hygiene::Qualified { .. } => { parse_quote_spanned!(span=> ::core::convert::AsRef) } } } + pub(crate) fn deref(&mut self, attrs: &[Attribute], span: Span) -> Path { + match self { + #[cfg(any(feature = "file", feature = "web-idl", test))] + Hygiene::Imports(imports) => { + imports.deref.insert(attrs.to_vec()); + parse_quote_spanned!(span=> Deref) + } + Hygiene::Qualified { .. } => { + parse_quote_spanned!(span=> ::core::ops::Deref) + } + } + } + pub(crate) fn phantom_data(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { + #[cfg(any(feature = "file", feature = "web-idl", test))] Hygiene::Imports(imports) => { imports .phantom_data .get_or_insert_with(attrs, <[_]>::to_vec); parse_quote_spanned!(span=> PhantomData) } - Hygiene::Hygiene { .. } => { + Hygiene::Qualified { .. } => { parse_quote_spanned!(span=> ::core::marker::PhantomData) } } @@ -93,29 +109,32 @@ impl Hygiene<'_> { pub(crate) fn from(&mut self, span: Span) -> Path { match self { + #[cfg(any(feature = "file", feature = "web-idl", test))] Hygiene::Imports(_) => { parse_quote_spanned!(span=> From) } - Hygiene::Hygiene { .. } => { + Hygiene::Qualified { .. } => { parse_quote_spanned!(span=> ::core::convert::From) } } } fn with_js_sys(js_sys: Option<&Path>, path: &TokenStream, span: Span) -> Path { - let js_sys = js_sys.map_or_else( - || Cow::Owned(parse_quote_spanned!(span=> ::js_sys)), - Cow::Borrowed, - ); - - parse_quote_spanned!(span=> #js_sys::#path) + if let Some(js_sys) = js_sys { + parse_quote_spanned!(span=> #js_sys::#path) + } else { + parse_quote_spanned!(span=> ::js_sys::#path) + } } } +#[cfg(any(feature = "file", feature = "web-idl", test))] type FixedHashMap = HashMap; +#[cfg(any(feature = "file", feature = "web-idl", test))] type FixedHashSet = HashSet; -pub struct ImportManager { +#[cfg(any(feature = "file", feature = "web-idl", test))] +pub(crate) struct ImportManager { js_sys: Path, deref: FixedHashSet>, phantom_data: FixedHashSet>, @@ -123,9 +142,10 @@ pub struct ImportManager { hazard_imports: FixedHashMap, FixedHashSet>, } +#[cfg(any(feature = "file", feature = "web-idl", test))] impl ImportManager { #[must_use] - pub fn new(js_sys: Option) -> Self { + pub(crate) fn new(js_sys: Option) -> Self { Self { js_sys: js_sys.unwrap_or_else(|| parse_quote! { js_sys }), deref: FixedHashSet::default(), @@ -135,7 +155,7 @@ impl ImportManager { } } - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.phantom_data .iter() .map(|attr| { @@ -187,6 +207,7 @@ impl ImportManager { } } +#[cfg(any(feature = "file", feature = "web-idl", test))] impl ToTokens for ImportManager { fn to_tokens(&self, tokens: &mut TokenStream) { for item_use in self.iter() { diff --git a/host/js-sys-bindgen/src/lib.rs b/host/js-sys-bindgen/src/lib.rs index f161613e..ba4d63e9 100644 --- a/host/js-sys-bindgen/src/lib.rs +++ b/host/js-sys-bindgen/src/lib.rs @@ -1,12 +1,9 @@ -#[cfg(feature = "macro")] mod closure; -#[cfg(feature = "macro")] mod export; #[cfg(feature = "file")] mod file; mod function; mod hygiene; -#[cfg(feature = "macro")] mod r#macro; #[cfg(test)] mod tests; @@ -14,18 +11,11 @@ mod r#type; #[cfg(feature = "web-idl")] mod web_idl; -pub use proc_macro2; -pub use quote; pub use syn; -#[cfg(feature = "macro")] pub use crate::closure::closure; #[cfg(feature = "file")] pub use crate::file::file; -pub use crate::function::{Function, FunctionJsOutput}; -pub use crate::hygiene::{Hygiene, ImportManager}; -#[cfg(feature = "macro")] pub use crate::r#macro::r#macro; -pub use crate::r#type::Type; #[cfg(feature = "web-idl")] pub use crate::web_idl::web_idl; diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index 2f693bee..df54ec84 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -1,32 +1,38 @@ +use std::collections::{HashMap, VecDeque}; use std::env; use proc_macro2::TokenStream; use quote::ToTokens; +#[cfg(any(feature = "file", test))] +use syn::File; use syn::parse::Parser; -use syn::{Error, ForeignItem, Item, ItemForeignMod, LitStr, Path, meta}; +use syn::{Attribute, Error, ForeignItem, Item, ItemForeignMod, LitStr, Path, meta}; -use crate::{Function, FunctionJsOutput, Hygiene, ImportManager, Type}; +use crate::function::{FunctionImport, expand}; +use crate::hygiene::Hygiene; +#[cfg(feature = "file")] +use crate::hygiene::ImportManager; +use crate::r#type::{Type, TypeOptions}; -pub fn r#macro( - attr: TokenStream, - item: TokenStream, - imports: Option<&mut ImportManager>, -) -> Result { +pub fn r#macro(attr: TokenStream, item: TokenStream) -> Result { match syn::parse2(item).map_err(Error::into_compile_error)? { - Item::ForeignMod(foreign_mod) => internal(attr, foreign_mod, None, imports) - .map(|items| items.into_iter().map(Item::into_token_stream).collect()) - .map_err(|(output, error)| { - let error = error.into_compile_error(); - - if let Some(output) = output { - let mut output: TokenStream = - output.into_iter().map(Item::into_token_stream).collect(); - output.extend(error); - output - } else { - error - } - }), + Item::ForeignMod(foreign_mod) => { + let crate_name = env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found"); + + expand_proc_macro(attr, foreign_mod, &crate_name) + .map(GeneratedItems::into_token_stream) + .map_err(|(output, error)| { + let error = error.into_compile_error(); + + if let Some(output) = output { + let mut output = output.into_token_stream(); + output.extend(error); + output + } else { + error + } + }) + } Item::Fn(function) => { crate::export::r#macro(attr, &function, None).map_err(Error::into_compile_error) } @@ -36,20 +42,64 @@ pub fn r#macro( } } -pub(crate) fn internal( +#[cfg(feature = "file")] +pub(crate) fn expand_file( attr: TokenStream, - mut foreign_mod: ItemForeignMod, - crate_: Option<&str>, - imports: Option<&mut ImportManager>, -) -> Result, (Option>, Error)> { - let mut error = ErrorStack::new(); + foreign_mod: ItemForeignMod, + crate_name: &str, + imports: &mut ImportManager, +) -> Result, Error)> { + let (_, namespace, error) = parse_block_options(attr, false); + expand_foreign_mod( + foreign_mod, + crate_name, + namespace.as_deref(), + Hygiene::Imports(imports), + error, + ) +} + +#[cfg(test)] +pub(crate) fn expand_for_test( + attr: TokenStream, + foreign_mod: ItemForeignMod, + crate_name: &str, +) -> Result, Error)> { + expand_proc_macro(attr, foreign_mod, crate_name) +} + +fn expand_proc_macro( + attr: TokenStream, + foreign_mod: ItemForeignMod, + crate_name: &str, +) -> Result, Error)> { + let (js_sys, namespace, error) = parse_block_options(attr, true); + + expand_foreign_mod( + foreign_mod, + crate_name, + namespace.as_deref(), + Hygiene::Qualified { + js_sys: js_sys.as_ref(), + }, + error, + ) +} + +fn parse_block_options( + attr: TokenStream, + allow_js_sys_path: bool, +) -> (Option, Option, ErrorStack) { + let mut error = ErrorStack::new(); let mut js_sys: Option = None; let mut namespace: Option = None; if let Err(e) = meta::parser(|meta| { if meta.path.is_ident("js_sys") { - if imports.is_some() { + // The block-level `js_sys` option selects the crate path used by + // every generated item in this foreign module. + if !allow_js_sys_path { Err(meta.error("`js_sys` attribute only allowed with proc-macro hygiene")) } else if js_sys.is_some() { Err(meta.error("duplicate attribute")) @@ -58,6 +108,8 @@ pub(crate) fn internal( Ok(()) } } else if meta.path.is_ident("namespace") { + // The block-level `namespace` prefixes every generated JavaScript + // global path and import symbol in this foreign module. if namespace.is_some() { Err(meta.error("duplicate attribute")) } else { @@ -73,14 +125,16 @@ pub(crate) fn internal( error.push(e); } - let mut hygiene = if let Some(imports) = imports { - Hygiene::Imports(imports) - } else { - Hygiene::Hygiene { - js_sys: js_sys.as_ref(), - } - }; + (js_sys, namespace, error) +} +fn expand_foreign_mod( + mut foreign_mod: ItemForeignMod, + crate_name: &str, + namespace: Option<&str>, + mut hygiene: Hygiene<'_>, + mut error: ErrorStack, +) -> Result, Error)> { for attr in foreign_mod .attrs .extract_if(.., |attr| attr.path().is_ident("js_sys")) @@ -91,13 +145,16 @@ pub(crate) fn internal( )); } - let mut output = Vec::new(); + let mut output = GeneratedItems::default(); + let mut function_imports = Vec::new(); + let mut type_options = VecDeque::new(); + let mut js_names = HashMap::new(); if foreign_mod .abi .name .as_ref() - .is_some_and(|value| value.value() != "js-sys") + .is_none_or(|value| value.value() != "js-sys") { error.push(Error::new_spanned( &foreign_mod.abi.name, @@ -105,71 +162,36 @@ pub(crate) fn internal( )); } + for item in &mut foreign_mod.items { + if let ForeignItem::Type(item) = item { + let options = TypeOptions::parse(item, |e| error.push(e)); + let rust_name = item.ident.to_string(); + let js_name = options.js_name.clone().unwrap_or_else(|| rust_name.clone()); + + js_names.insert(rust_name.clone(), js_name); + type_options.push_back(options); + } + } + for item in foreign_mod.items { match item { - ForeignItem::Fn(mut item) => { - let mut js_output = FunctionJsOutput::default(); - - for attr in item - .attrs - .extract_if(.., |attr| attr.path().is_ident("js_sys")) - { - if let Err(e) = attr.parse_nested_meta(|meta| { - let FunctionJsOutput::Generate { js_name, property } = &mut js_output - else { - return Err(meta.error("found duplicate/incompatible attribute")); - }; - - if meta.path.is_ident("js_name") { - *js_name = Some(meta.value()?.parse::()?.value()); - Ok(()) - } else if meta.path.is_ident("js_import") { - if meta.input.is_empty() { - js_output = FunctionJsOutput::Import; - Ok(()) - } else { - Err(meta.error("`js_import` supports no values")) - } - } else if meta.path.is_ident("js_embed") { - js_output = - FunctionJsOutput::Embed(meta.value()?.parse::()?.value()); - Ok(()) - } else if meta.path.is_ident("property") { - if *property { - return Err(meta.error("duplicate attribute")); - } - - *property = true; - Ok(()) - } else { - Err(meta.error("unsupported attribute")) - } - }) { - error.push(e); + ForeignItem::Fn(item) => { + match expand(&mut hygiene, namespace, crate_name, &js_names, item) { + Ok((function, import)) => { + output.push(&function); + function_imports.push(import); } - } - - let crate_ = if let Some(crate_) = crate_ { - crate_ - } else { - &env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found") - }; - - match Function::new(&mut hygiene, js_output, namespace.as_deref(), crate_, item) { - Ok(function) => output.push(function.into()), Err(e) => error.push(e), } } - ForeignItem::Type(mut item) => { - if let Some(attr) = item - .attrs - .extract_if(.., |attr| attr.path().is_ident("js_sys")) - .next() - { - error.push(Error::new_spanned(attr, "unsupported attribute")); - } + ForeignItem::Type(item) => { + let options = type_options + .pop_front() + .expect("all foreign types were parsed in the first pass"); - output.extend(Type::new(&mut hygiene, item)); + for item in Type::with_extends(&mut hygiene, item, &options.extends) { + output.push(&item); + } } item => { error.push(Error::new_spanned( @@ -180,6 +202,8 @@ pub(crate) fn internal( } } + output.extend(render_import_groups(function_imports)); + if let Some(error) = error.resolve() { Err((Some(output), error)) } else { @@ -187,6 +211,98 @@ pub(crate) fn internal( } } +#[derive(Debug, Default)] +pub(crate) struct GeneratedItems(TokenStream); + +impl GeneratedItems { + fn push(&mut self, item: &impl ToTokens) { + item.to_tokens(&mut self.0); + } + + fn extend(&mut self, items: TokenStream) { + self.0.extend(items); + } + + pub(crate) fn into_token_stream(self) -> TokenStream { + self.0 + } + + #[cfg(any(feature = "file", test))] + pub(crate) fn into_items(self) -> Result, Error> { + Ok(syn::parse2::(self.0)?.items) + } +} + +struct ImportGroup { + cfg_attrs: Vec, + descriptors: Vec, + needs_js_section: bool, + macro_path: Path, +} + +fn render_import_groups(imports: Vec) -> TokenStream { + let mut groups: Vec = Vec::new(); + + for import in imports { + // One section static may cover every import with the same conditional + // compilation boundary. Keeping the original attributes on a containing + // item also preserves arbitrary `cfg_attr` expansions. + if let Some(group) = groups + .iter_mut() + .find(|group| group.cfg_attrs == import.cfg_attrs) + { + group.descriptors.push(import.descriptor); + group.needs_js_section |= import.needs_js_section; + } else { + groups.push(ImportGroup { + cfg_attrs: import.cfg_attrs, + descriptors: vec![import.descriptor], + needs_js_section: import.needs_js_section, + macro_path: import.macro_path, + }); + } + } + + groups + .into_iter() + .fold(TokenStream::new(), |mut output, group| { + let ImportGroup { + cfg_attrs, + descriptors, + needs_js_section, + macro_path, + } = group; + let js = needs_js_section.then(|| { + quote::quote! { + const JS_CAPACITY: ::core::primitive::usize = + #macro_path::import_js_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: #macro_path::ImportSection = + #macro_path::import_js::(IMPORTS); + } + }); + + output.extend(quote::quote! { + #(#cfg_attrs)* + const _: () = { + const IMPORTS: &[#macro_path::ImportDescriptor] = &[#(#descriptors),*]; + const WAT_CAPACITY: ::core::primitive::usize = + #macro_path::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: #macro_path::ImportSection = + #macro_path::import_wat::(IMPORTS); + + #js + }; + }); + output + }) +} + pub(crate) struct ErrorStack(Option); impl ErrorStack { diff --git a/host/js-sys-bindgen/src/tests/closure.rs b/host/js-sys-bindgen/src/tests/closure.rs new file mode 100644 index 00000000..6ed205f1 --- /dev/null +++ b/host/js-sys-bindgen/src/tests/closure.rs @@ -0,0 +1,57 @@ +use proc_macro2::TokenStream; +use quote::quote; + +fn expand(input: TokenStream) -> String { + crate::closure::closure_with(input, "test-crate", "test-package", "1.2.3") + .unwrap() + .to_string() +} + +#[test] +fn package_version_disambiguates_symbols() { + let first = crate::closure::closure_with( + quote!(dyn Fn(), || {}), + "test-crate", + "test-package", + "1.2.3", + ) + .unwrap() + .to_string(); + let second = crate::closure::closure_with( + quote!(dyn Fn(), || {}), + "test-crate", + "test-package", + "2.0.0", + ) + .unwrap() + .to_string(); + + assert_ne!(first, second); +} + +#[test] +fn expansion_is_deterministic() { + let input = quote!(dyn FnMut(i32) -> i32, move |value| value + 1); + assert_eq!(expand(input.clone()), expand(input)); +} + +#[test] +fn invalid_trait_object() { + let error = crate::closure::closure_with( + quote!(dyn Clone, || {}), + "test-crate", + "test-package", + "1.2.3", + ) + .unwrap_err(); + assert_eq!(error.to_string(), "expected `Fn`, `FnMut`, or `FnOnce`"); + + let error = crate::closure::closure_with( + quote!(dyn Fn() + Send, || {}), + "test-crate", + "test-package", + "1.2.3", + ) + .unwrap_err(); + assert_eq!(error.to_string(), "expected exactly one closure trait"); +} diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index 0b30ec22..e437480f 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -9,22 +9,6 @@ fn basic() { }, { pub fn log(data: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [("arg0", & JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "log", - required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.log", indirect_call = - "globalThis.log(arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "test_crate.log"] fn log( @@ -41,6 +25,35 @@ fn basic() { unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.log", + indirect_call: "globalThis.log(arg0_0)", + required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) @@ -66,22 +79,6 @@ fn namespace() { }, { pub fn log(data: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = - "console.log", shim = "test_crate.console.log", inputs = [("arg0", & JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "console.log", - required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.console.log", indirect_call = - "globalThis.console.log(arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "test_crate.console.log"] fn log( @@ -98,6 +95,35 @@ fn namespace() { unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "console.log", + "test_crate.console.log", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: true, + direct_call: "globalThis.console.log(arg0_0)", + indirect_call: "globalThis.console.log(arg0_0)", + required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"console.log\" (func $test_crate.import.console.log (@sym (name \ \"test_crate.import.console.log\")) (param externref))) @@ -108,7 +134,7 @@ fn namespace() { table.get $js_sys.import.externref.table (@reloc) call $test_crate.import.console.log (@reloc) )", - "globalThis.console.log", + "(arg0_0) => globalThis.console.log(arg0_0)", ); } @@ -123,22 +149,6 @@ fn js_sys() { }, { pub fn log(data: &JsValue) { - js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [("arg0", & JsValue)],), - } - - js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "log", - required_embeds = [js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.log", indirect_call = - "globalThis.log(arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "test_crate.log"] fn log( @@ -155,6 +165,35 @@ fn js_sys() { unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + const _: () = { + const IMPORTS: &[js_sys::r#macro::ImportDescriptor] = + &[js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.log", + indirect_call: "globalThis.log(arg0_0)", + required_embeds: &[js_sys::r#macro::js_input_embed::<&JsValue>()], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: js_sys::r#macro::ImportSection = + js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: js_sys::r#macro::ImportSection = + js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) @@ -180,22 +219,6 @@ fn two_parameters() { }, { pub fn log(data1: &JsValue, data2: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [("arg0", & JsValue), ("arg1", & JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "log", - required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.log", indirect_call = - "globalThis.log(arg0_0, arg1_0)", inputs = [("arg0", & JsValue), ("arg1", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "test_crate.log"] fn log( @@ -222,6 +245,38 @@ fn two_parameters() { } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[ + ::js_sys::r#macro::import_input::<&JsValue>("arg0"), + ::js_sys::r#macro::import_input::<&JsValue>("arg1"), + ], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.log", + indirect_call: "globalThis.log(arg0_0, arg1_0)", + required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref externref))) @@ -239,31 +294,113 @@ fn two_parameters() { } #[test] -fn empty() { +fn groups_functions_and_shared_wat_imports() { test!( {}, { extern "js-sys" { - pub fn log(); + #[js_sys(js_import)] + pub fn first(value: &JsValue); + + #[js_sys(js_import)] + pub fn second(value: &JsValue); } }, { - pub fn log() { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [],), + pub fn first(value: &JsValue) { + unsafe extern "C" { + #[link_name = "test_crate.first"] + fn first( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "log", - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.log", indirect_call = "globalThis.log()", - inputs = [], - ), + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(value); + unsafe { first(arg0_0, arg0_1, arg0_2, arg0_3) } + }; + } + + pub fn second(value: &JsValue) { + unsafe extern "C" { + #[link_name = "test_crate.second"] + fn second( + arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, + arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, + arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, + arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, + ); } + { + let (arg0_0, arg0_1, arg0_2, arg0_3) = + ::js_sys::r#macro::split_input::<&JsValue>(value); + unsafe { second(arg0_0, arg0_1, arg0_2, arg0_3) } + }; + } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = &[ + ::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "first", + "test_crate.first", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::None, + ), + ::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "second", + "test_crate.second", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::None, + ), + ]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + }; + }, + "(import \"test_crate\" \"first\" (func $test_crate.import.first (@sym (name \ + \"test_crate.import.first\")) (param externref))) + (import \"test_crate\" \"second\" (func $test_crate.import.second (@sym (name \ + \"test_crate.import.second\")) (param externref))) + (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ + \"js_sys.externref.table\")) 2 externref)) + (func $test_crate.first (@sym) (param $arg0_0 i32) + local.get $arg0_0 + table.get $js_sys.import.externref.table (@reloc) + call $test_crate.import.first (@reloc) + ) + (func $test_crate.second (@sym) (param $arg0_0 i32) + local.get $arg0_0 + table.get $js_sys.import.externref.table (@reloc) + call $test_crate.import.second (@reloc) + )", + None, + ); +} + +#[test] +fn empty() { + test!( + {}, + { + extern "js-sys" { + pub fn log(); + } + }, + { + pub fn log() { unsafe extern "C" { #[link_name = "test_crate.log"] fn log(); @@ -273,6 +410,35 @@ fn empty() { unsafe { log() } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.log", + indirect_call: "globalThis.log()", + required_embeds: &[], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")))) @@ -295,22 +461,6 @@ fn js_name() { }, { pub fn logx(data: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "logx", - shim = "test_crate.logx", inputs = [("arg0", & JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "logx", - required_embeds = [::js_sys::r#macro::js_input_embed::<&JsValue>()], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.log", indirect_call = - "globalThis.log(arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "test_crate.logx"] fn logx( @@ -327,6 +477,35 @@ fn js_name() { unsafe { logx(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "logx", + "test_crate.logx", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.log", + indirect_call: "globalThis.log(arg0_0)", + required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"logx\" (func $test_crate.import.logx (@sym (name \ \"test_crate.import.logx\")) (param externref))) @@ -353,11 +532,6 @@ fn js_import() { }, { pub fn log(data: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [("arg0", & JsValue)],), - } - unsafe extern "C" { #[link_name = "test_crate.log"] fn log( @@ -374,6 +548,24 @@ fn js_import() { unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::None, + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) @@ -400,25 +592,6 @@ fn js_embed() { }, { pub fn log(data: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [("arg0", & JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "log", - required_embeds = [ - ("test_crate", "embed"), - ::js_sys::r#macro::js_input_embed::<&JsValue>(), - ], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "this.#jsEmbed.test_crate['embed']", indirect_call = - "this.#jsEmbed.test_crate['embed'](arg0_0)", inputs = [("arg0", & JsValue)], - ), - } - unsafe extern "C" { #[link_name = "test_crate.log"] fn log( @@ -435,6 +608,38 @@ fn js_embed() { unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "this.#jsEmbed.test_crate['embed']", + indirect_call: "this.#jsEmbed.test_crate['embed'](arg0_0)", + required_embeds: &[ + ("test_crate", "embed"), + ::js_sys::r#macro::js_input_embed::<&JsValue>(), + ], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")) (param externref))) @@ -460,25 +665,6 @@ fn r#return() { }, { pub fn is_nan() -> JsValue { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "is_nan", - shim = "test_crate.is_nan", inputs = [], output = JsValue,), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "is_nan", - required_embeds = [ - ::js_sys::r#macro::js_output_embed::(), - ::js_sys::r#macro::js_result_embed::(), - ], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.is_nan", indirect_call = - "globalThis.is_nan()", inputs = [], output = JsValue, - ), - } - unsafe extern "C" { #[link_name = "test_crate.is_nan"] fn is_nan() -> ::js_sys::r#macro::OutputRet; @@ -486,6 +672,38 @@ fn r#return() { ::js_sys::r#macro::join_output({ unsafe { is_nan() } }) } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "is_nan", + "test_crate.is_nan", + &[], + ::core::option::Option::Some(::js_sys::r#macro::import_output::()), + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.is_nan", + indirect_call: "globalThis.is_nan()", + required_embeds: &[ + ::js_sys::r#macro::js_output_embed::(), + ::js_sys::r#macro::js_result_embed::(), + ], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"is_nan\" (func $test_crate.import.is_nan (@sym (name \ \"test_crate.import.is_nan\")) (result externref))) @@ -520,21 +738,6 @@ fn cfg() { { #[cfg(all())] pub fn log() { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "log", - shim = "test_crate.log", inputs = [],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "log", - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = "", direct_call = "globalThis.log", indirect_call = "globalThis.log()", - inputs = [], - ), - } - unsafe extern "C" { #[link_name = "test_crate.log"] fn log(); @@ -544,6 +747,36 @@ fn cfg() { unsafe { log() } }; } + + #[cfg(all())] + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "log", + "test_crate.log", + &[], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: false, + direct_call: "globalThis.log", + indirect_call: "globalThis.log()", + required_embeds: &[], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ \"test_crate.import.log\")))) @@ -553,3 +786,71 @@ fn cfg() { "globalThis.log", ); } + +#[test] +fn preserves_successful_functions_after_an_error() { + let input = syn::parse_quote! { + extern "js-sys" { + pub fn good(value: i32) -> i32; + pub async fn bad(); + } + }; + let (Some(output), error) = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap_err() + else { + panic!("expected the successful function to be preserved"); + }; + + let mut output = output.into_token_stream(); + output.extend(error.into_compile_error()); + let output = output.to_string(); + + assert!(output.contains("pub fn good")); + assert!(output.contains("\"test_crate.good\"")); + assert_eq!(output.matches("ImportDescriptor :: new").count(), 1); + assert!(!output.contains("fn bad")); + assert!(output.contains("compile_error")); + assert!(output.contains("`async` functions are not supported")); +} + +#[test] +fn requires_js_sys_abi() { + let input = syn::parse_quote! { + extern { + pub fn log(); + } + }; + + assert_eq!(super::macro_error(input), "expected `js-sys` ABI"); +} + +#[test] +fn incompatible_binding_options_are_rejected() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(js_name = "renamed", js_import)] + pub fn log(); + } + }; + + assert_eq!( + super::macro_error(input), + "`js_import` and `js_embed` cannot be combined with JavaScript binding options" + ); +} + +#[test] +fn duplicate_parameter_abi_override_is_rejected() { + let input = syn::parse_quote! { + extern "js-sys" { + pub fn log( + #[js_sys(type = i32)] + #[js_sys(type = u32)] + value: i32, + ); + } + }; + + assert_eq!(super::macro_error(input), "duplicate attribute"); +} diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index da7a7fff..c41a3479 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -1,3 +1,26 @@ +fn generated_items(input: syn::ItemForeignMod) -> Vec { + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap() +} + +fn generated_rust(input: syn::ItemForeignMod) -> String { + let output = generated_items(input); + prettyplease::unparse(&syn::File { + shebang: None, + attrs: Vec::new(), + items: output, + }) +} + +fn generated_js(input: syn::ItemForeignMod) -> String { + let output = generated_rust(input); + let dir = tempfile::tempdir().unwrap(); + let (_, js, _) = super::inner(dir.path(), &output).unwrap(); + js.unwrap() +} + #[test] fn method() { test!( @@ -10,25 +33,8 @@ fn method() { { impl JsTest { pub fn test(self: &JsTest) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "test", - required_embeds = [::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>()], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & - ::js_sys::JsValue)), direct_call = "arg0_0.test()", indirect_call = "arg0_0.test()", - inputs = [("arg0", & ::js_sys::JsValue)], - ), - } - unsafe extern "C" { - #[link_name = "test_crate.test"] + #[link_name = "test_crate.JsTest.test"] fn test( arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, @@ -45,15 +51,48 @@ fn method() { }; } } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "JsTest.test", + "test_crate.JsTest.test", + &[::js_sys::r#macro::import_input::<&::js_sys::JsValue>( + "arg0", + )], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: true, + direct_call: "arg0_0.test()", + indirect_call: "arg0_0.test()", + required_embeds: &[::js_sys::r#macro::js_input_embed::< + &::js_sys::JsValue, + >()], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ - \"test_crate.import.test\")) (param externref))) + "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ + \"test_crate.import.JsTest.test\")) (param externref))) (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.test (@sym) (param $arg0_0 i32) + (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) local.get $arg0_0 table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.test (@reloc) + call $test_crate.import.JsTest.test (@reloc) )", "(arg0_0) => arg0_0.test()", ); @@ -71,30 +110,8 @@ fn method_par() { { impl JsTest { pub fn test(self: &JsTest, par1: &JsValue, par2: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & - JsValue), ("arg2", & JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "test", - required_embeds = [ - ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), - ::js_sys::r#macro::js_input_embed::<&JsValue>(), - ], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & - ::js_sys::JsValue), ("arg1", & JsValue), ("arg2", & JsValue)), direct_call = - "arg0_0.test(arg1_0, arg2_0)", indirect_call = "arg0_0.test(arg1_0, arg2_0)", inputs - = [("arg0", & ::js_sys::JsValue), ("arg1", & JsValue), ("arg2", & JsValue)], - ), - } - unsafe extern "C" { - #[link_name = "test_crate.test"] + #[link_name = "test_crate.JsTest.test"] fn test( arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, @@ -128,61 +145,243 @@ fn method_par() { }; } } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "JsTest.test", + "test_crate.JsTest.test", + &[ + ::js_sys::r#macro::import_input::<&::js_sys::JsValue>("arg0"), + ::js_sys::r#macro::import_input::<&JsValue>("arg1"), + ::js_sys::r#macro::import_input::<&JsValue>("arg2"), + ], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: true, + direct_call: "arg0_0.test(arg1_0, arg2_0)", + indirect_call: "arg0_0.test(arg1_0, arg2_0)", + required_embeds: &[ + ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), + ::js_sys::r#macro::js_input_embed::<&JsValue>(), + ], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ - \"test_crate.import.test\")) (param externref externref externref))) + "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ + \"test_crate.import.JsTest.test\")) (param externref externref externref))) (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) (param $arg2_0 i32) + (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) (param $arg2_0 i32) local.get $arg0_0 table.get $js_sys.import.externref.table (@reloc) local.get $arg1_0 table.get $js_sys.import.externref.table (@reloc) local.get $arg2_0 table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.test (@reloc) + call $test_crate.import.JsTest.test (@reloc) )", "(arg0_0, arg1_0, arg2_0) => arg0_0.test(arg1_0, arg2_0)", ); } +#[test] +fn variadic() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(variadic)] + pub fn push(self: &JsTest, first: &JsValue, rest: &[JsValue]); + } + }; + + assert_eq!( + generated_js(input), + "(arg0_0, arg1_0, arg2_0, arg2_1) => {\n arg2_0 = \ + this.#jsEmbed.js_sys['array.js_value.decode'](arg2_0, arg2_1)\narg0_0.push(arg1_0, \ + ...arg2_0)\n}" + ); +} + +#[test] +fn global_variadic() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(variadic)] + pub fn call(values: &JsArray); + } + }; + + assert_eq!( + generated_js(input), + "(arg0_0) => globalThis.call(...arg0_0)" + ); +} + +#[test] +fn type_js_name() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(constructor)] + pub fn new() -> RustType; + + #[js_sys(static_of = RustType, js_name = "create")] + pub fn create(); + + #[js_sys(js_name = "JavaScriptType")] + pub type RustType; + } + }; + + let output = generated_rust(input); + assert!(output.contains(r#"direct_call: "new globalThis.JavaScriptType()""#)); + assert!(output.contains(r#"direct_call: "globalThis.JavaScriptType.create()""#)); +} + +#[test] +fn constructor_uses_return_owner() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(constructor)] + pub fn new() -> Result; + + #[js_sys(js_name = "JavaScriptType")] + pub type RustType; + } + }; + + let output = generated_items(input); + let output = prettyplease::unparse(&syn::File { + shebang: None, + attrs: Vec::new(), + items: output, + }); + assert!(output.contains(r#"direct_call: "new globalThis.JavaScriptType()""#)); +} + +#[test] +fn static_properties_use_the_type_js_name() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(static_of = RustType, getter = "value")] + pub fn value() -> i32; + + #[js_sys(static_of = RustType, setter = "value")] + pub fn set_value(value: i32); + + #[js_sys(js_name = "JavaScriptType")] + pub type RustType; + } + }; + + let output = generated_rust(input); + assert!(output.contains(r#"direct_call: "globalThis.JavaScriptType.value""#)); + assert!(output.contains(r#"direct_call: "globalThis.JavaScriptType.value = arg0_0""#)); +} + +#[test] +fn variadic_requires_an_argument() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(variadic)] + pub fn call(); + } + }; + + assert_eq!( + super::macro_error(input), + "`variadic` requires at least one argument" + ); +} + +#[test] +fn named_getter() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(getter = "value")] + pub fn renamed(self: &JsTest) -> i32; + } + }; + + let output = generated_rust(input); + assert!(output.contains(r#"direct_call: "arg0_0.value""#)); + assert!(output.contains(r#"indirect_call: "arg0_0.value""#)); +} + +#[test] +fn named_setter() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(setter = "value")] + pub fn renamed(self: &JsTest, value: i32); + } + }; + + let output = generated_rust(input); + assert!(output.contains(r#"direct_call: "arg0_0.value = arg1_0""#)); + assert!(output.contains(r#"indirect_call: "arg0_0.value = arg1_0""#)); +} + +#[test] +fn inferred_setter() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(setter)] + pub fn set_value(self: &JsTest, value: i32); + } + }; + + let output = generated_rust(input); + assert!(output.contains(r#"direct_call: "arg0_0.value = arg1_0""#)); + assert!(output.contains(r#"indirect_call: "arg0_0.value = arg1_0""#)); +} + +#[test] +fn setter_requires_a_field_name() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(setter)] + pub fn update(self: &JsTest, value: i32); + } + }; + let (_, error) = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap_err(); + + assert_eq!( + error.to_string(), + "`setter` cannot infer a field name; use `setter = \"field\"`" + ); +} + #[test] fn getter() { test!( {}, { extern "js-sys" { - #[js_sys(property)] + #[js_sys(getter)] pub fn test(self: &JsTest) -> JsValue; } }, { impl JsTest { pub fn test(self: &JsTest) -> JsValue { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue)], output = - JsValue,), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "test", - required_embeds = [ - ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), - ::js_sys::r#macro::js_output_embed::(), - ::js_sys::r#macro::js_result_embed::(), - ], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & - ::js_sys::JsValue)), direct_call = "arg0_0.test", indirect_call = "arg0_0.test", - inputs = [("arg0", & ::js_sys::JsValue)], output = JsValue, - ), - } - unsafe extern "C" { - #[link_name = "test_crate.test"] + #[link_name = "test_crate.JsTest.test"] fn test( arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, @@ -199,18 +398,53 @@ fn getter() { }) } } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "JsTest.test", + "test_crate.JsTest.test", + &[::js_sys::r#macro::import_input::<&::js_sys::JsValue>( + "arg0", + )], + ::core::option::Option::Some(::js_sys::r#macro::import_output::()), + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: true, + direct_call: "arg0_0.test", + indirect_call: "arg0_0.test", + required_embeds: &[ + ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), + ::js_sys::r#macro::js_output_embed::(), + ::js_sys::r#macro::js_result_embed::(), + ], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ - \"test_crate.import.test\")) (param externref) (result externref))) + "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ + \"test_crate.import.JsTest.test\")) (param externref) (result externref))) (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ \"js_sys.externref.table\")) 2 externref)) (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) - (func $test_crate.test (@sym) (param $arg0_0 i32) (result i32) + (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) (result i32) (local $js_sys.externref.value externref) (local $js_sys.externref.index i32) local.get $arg0_0 table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.test (@reloc) + call $test_crate.import.JsTest.test (@reloc) local.set $js_sys.externref.value call $js_sys.externref.next (@reloc) local.tee $js_sys.externref.index @@ -228,37 +462,15 @@ fn setter() { {}, { extern "js-sys" { - #[js_sys(property)] + #[js_sys(setter = "test")] pub fn test(self: &JsTest, value: &JsValue); } }, { impl JsTest { pub fn test(self: &JsTest, value: &JsValue) { - ::js_sys::js_bindgen::unsafe_global_wat! { - "{}", interpolate::js_sys::r#macro::wat_import!(module = "test_crate", import = "test", - shim = "test_crate.test", inputs = [("arg0", & ::js_sys::JsValue), ("arg1", & - JsValue)],), - } - - ::js_sys::js_bindgen::import_js! { - module = "test_crate", - name = "test", - required_embeds = [ - ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), - ::js_sys::r#macro::js_input_embed::<&JsValue>(), - ], - "{}", - interpolate ::js_sys::r#macro::js_import!( - direct_open = ::js_sys::r#macro::js_function!("(", ") => ", ("arg0", & - ::js_sys::JsValue), ("arg1", & JsValue)), direct_call = "arg0_0.test = arg1_0", - indirect_call = "arg0_0.test = arg1_0", inputs = [("arg0", & ::js_sys::JsValue), - ("arg1", & JsValue)], - ), - } - unsafe extern "C" { - #[link_name = "test_crate.test"] + #[link_name = "test_crate.JsTest.test"] fn test( arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, @@ -285,17 +497,52 @@ fn setter() { }; } } + const _: () = { + const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = + &[::js_sys::r#macro::ImportDescriptor::new( + "test_crate", + "JsTest.test", + "test_crate.JsTest.test", + &[ + ::js_sys::r#macro::import_input::<&::js_sys::JsValue>("arg0"), + ::js_sys::r#macro::import_input::<&JsValue>("arg1"), + ], + ::core::option::Option::None, + ::core::option::Option::Some(::js_sys::r#macro::ImportJs { + direct_wrapper: true, + direct_call: "arg0_0.test = arg1_0", + indirect_call: "arg0_0.test = arg1_0", + required_embeds: &[ + ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), + ::js_sys::r#macro::js_input_embed::<&JsValue>(), + ], + }), + )]; + const WAT_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_wat_capacity(IMPORTS); + + #[used] + #[unsafe(link_section = "js_bindgen.wat")] + static WAT_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_wat::(IMPORTS); + const JS_CAPACITY: ::core::primitive::usize = + ::js_sys::r#macro::import_js_capacity(IMPORTS); + #[used] + #[unsafe(link_section = "js_bindgen.import")] + static JS_SECTION: ::js_sys::r#macro::ImportSection = + ::js_sys::r#macro::import_js::(IMPORTS); + }; }, - "(import \"test_crate\" \"test\" (func $test_crate.import.test (@sym (name \ - \"test_crate.import.test\")) (param externref externref))) + "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ + \"test_crate.import.JsTest.test\")) (param externref externref))) (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) + (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) local.get $arg0_0 table.get $js_sys.import.externref.table (@reloc) local.get $arg1_0 table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.test (@reloc) + call $test_crate.import.JsTest.test (@reloc) )", "(arg0_0, arg1_0) => arg0_0.test = arg1_0", ); diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index ae2ff9cb..244af0ee 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -6,9 +6,10 @@ use std::{env, fs}; use anyhow::{Context, Result, anyhow, bail, ensure}; use cargo_metadata::{Artifact, CompilerMessage, Message, Target}; use itertools::Itertools; -use js_bindgen_ld_shared::{JsBindgenJsSectionParser, JsBindgenWatSectionParser}; +use js_bindgen_ld_shared::{ + IMPORT_SECTION, JsBindgenJsSectionParser, JsBindgenWatSectionParser, WAT_SECTION, +}; use proc_macro2::TokenStream; -use quote::ToTokens; use syn::parse_quote; use wasmparser::{Parser, Payload}; @@ -32,8 +33,10 @@ macro_rules! test { let input = quote! $input; let foreign_mod = syn::parse2(input.clone()).unwrap(); - let output = - r#macro::internal(attr.clone(), foreign_mod, Some("test_crate"), None).unwrap(); + let output = r#macro::expand_for_test(attr.clone(), foreign_mod, "test_crate") + .unwrap() + .into_items() + .unwrap(); let output = prettyplease::unparse(&File { shebang: None, attrs: Vec::new(), @@ -77,6 +80,12 @@ mod function; mod member; mod r#type; +fn macro_error(input: syn::ItemForeignMod) -> String { + let (_, error) = r#macro::expand_for_test(TokenStream::new(), input, "test_crate").unwrap_err(); + + error.to_string() +} + fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Option)> { let js_sys = env::current_dir()? .parent() @@ -97,17 +106,13 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Op ); fs::write(tmp.join("Cargo.toml"), cargo_toml)?; - let js_test = r#macro::internal( + let js_test = r#macro::expand_for_test( TokenStream::new(), parse_quote! { extern "js-sys" { pub type JsTest; } }, - Some("test_crate"), - None, + "test_crate", ) - .unwrap(); - let js_test: TokenStream = js_test.into_iter().fold(TokenStream::new(), |mut acc, x| { - x.to_tokens(&mut acc); - acc - }); + .unwrap() + .into_token_stream(); let src = tmp.join("src"); fs::create_dir(&src)?; @@ -211,7 +216,7 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Op let payload = payload?; match payload { - Payload::CustomSection(c) if c.name() == "js_bindgen.wat" => { + Payload::CustomSection(c) if c.name() == WAT_SECTION => { let wat = JsBindgenWatSectionParser::new(&c) .exactly_one() .map_err(|wats| { @@ -224,7 +229,7 @@ fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Op wat_output = Some(wat.to_owned()); js_bindgen_ld_shared::wat_to_object(false, wat).unwrap(); } - Payload::CustomSection(c) if c.name() == "js_bindgen.import" => { + Payload::CustomSection(c) if c.name() == IMPORT_SECTION => { let mut parser = JsBindgenJsSectionParser::new(&c); let import = parser.next().unwrap(); diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index 957d5822..0365e5d8 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -81,6 +81,72 @@ fn generic() { ); } +#[test] +fn multiple_generic_kinds() { + let input = syn::parse_quote! { + extern "js-sys" { + pub type Generic<'a, T, U, const N: usize>; + } + }; + let output = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap(); + let output = prettyplease::unparse(&syn::File { + shebang: None, + attrs: Vec::new(), + items: output, + }); + let dir = tempfile::tempdir().unwrap(); + + super::inner(dir.path(), &output).unwrap(); +} + +#[test] +fn cfg_attr_only_applies_to_the_declared_type() { + let input = syn::parse_quote! { + extern "js-sys" { + #[cfg_attr(all(), derive(Clone))] + pub type JsString; + } + }; + let output = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap(); + + assert_eq!(output.len(), 5); + for (index, item) in output.into_iter().enumerate() { + let attrs = match item { + syn::Item::Struct(item) => item.attrs, + syn::Item::Impl(item) => item.attrs, + item => panic!("unexpected generated item: {item:?}"), + }; + let has_cfg_attr = attrs.iter().any(|attr| attr.path().is_ident("cfg_attr")); + + assert_eq!(has_cfg_attr, index == 0); + } +} + +#[test] +fn duplicate_type_names_do_not_panic_in_the_macro() { + let input = syn::parse_quote! { + extern "js-sys" { + pub type Duplicate; + pub type Duplicate; + } + }; + let output = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap(); + + assert_eq!(output.len(), 10); +} + #[test] fn default() { test!( @@ -166,3 +232,39 @@ fn r#trait() { None, ); } + +#[test] +fn extends() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(extends = JsTest)] + #[js_sys(extends = JsArray)] + pub type Child; + } + }; + let output = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap(); + let mut output = prettyplease::unparse(&syn::File { + shebang: None, + attrs: Vec::new(), + items: output, + }); + output.push_str( + r" +fn assert_extends(value: &Child) { + let _: &JsTest = value; + let _: &JsArray = ::core::convert::AsRef::::as_ref(value); +} + +fn into_parent(value: Child) -> JsArray { + value.into() +} +", + ); + + let dir = tempfile::tempdir().unwrap(); + super::inner(dir.path(), &output).unwrap(); +} diff --git a/host/js-sys-bindgen/src/tests/mod.rs b/host/js-sys-bindgen/src/tests/mod.rs index a9150b6d..a2c4180b 100644 --- a/host/js-sys-bindgen/src/tests/mod.rs +++ b/host/js-sys-bindgen/src/tests/mod.rs @@ -7,7 +7,7 @@ macro_rules! test { }; } -#[cfg(feature = "macro")] +mod closure; mod r#macro; mod r#type; #[cfg(feature = "web-idl")] diff --git a/host/js-sys-bindgen/src/tests/type.rs b/host/js-sys-bindgen/src/tests/type.rs index aa6bcde6..7834dc3f 100644 --- a/host/js-sys-bindgen/src/tests/type.rs +++ b/host/js-sys-bindgen/src/tests/type.rs @@ -1,6 +1,7 @@ use syn::parse_quote; -use crate::{Hygiene, ImportManager, Type}; +use crate::hygiene::{Hygiene, ImportManager}; +use crate::r#type::Type; #[test] fn basic() { @@ -103,3 +104,79 @@ fn generic() { }, ); } + +#[test] +fn extends() { + let mut imports = ImportManager::new(None); + let items = Type::with_extends( + &mut Hygiene::Imports(&mut imports), + parse_quote!( + type Test; + ), + &[parse_quote!(Base)], + ); + + test!( + { + #imports + + #items + }, + { + use core::ops::Deref; + use js_sys::JsValue; + use js_sys::hazard::{IntoJS, JsCast}; + + #[repr(transparent)] + struct Test(JsValue); + + impl AsRef for Test { + fn as_ref(&self) -> &JsValue { + &self.0 + } + } + + impl From for JsValue { + fn from(value: Test) -> Self { + value.0 + } + } + + unsafe impl JsCast for Test {} + + unsafe impl IntoJS for Test { + type Abi = ::Abi; + + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) + } + } + + impl Deref for Test { + type Target = Base; + + #[inline] + fn deref(&self) -> &Self::Target { + >::as_ref(self) + } + } + + impl AsRef for Test { + #[inline] + fn as_ref(&self) -> &Base { + ::unchecked_from_ref( + >::as_ref(self), + ) + } + } + + impl From for Base { + #[inline] + fn from(value: Test) -> Self { + ::unchecked_from(JsValue::from(value)) + } + } + + }, + ); +} diff --git a/host/js-sys-bindgen/src/type.rs b/host/js-sys-bindgen/src/type.rs index 72c4549e..15101ef6 100644 --- a/host/js-sys-bindgen/src/type.rs +++ b/host/js-sys-bindgen/src/type.rs @@ -1,20 +1,74 @@ -use std::array; - use proc_macro2::TokenStream; -use quote::{ToTokens, quote_spanned}; +use quote::{ToTokens, quote, quote_spanned}; use syn::spanned::Spanned; -use syn::{Fields, ForeignItemType, Item, ItemImpl, ItemStruct, Token, parse_quote_spanned}; +use syn::{ + Error, Fields, ForeignItemType, Item, ItemImpl, ItemStruct, LitStr, Path, Token, + parse_quote_spanned, +}; + +use crate::hygiene::Hygiene; + +pub(crate) struct Type { + r#struct: ItemStruct, + impls: Vec, +} -use crate::Hygiene; +#[derive(Default)] +pub(crate) struct TypeOptions { + /// JavaScript type name used by constructors and static members in this + /// block. + pub(crate) js_name: Option, + /// JavaScript parent types. The first parent is also the `Deref` target. + pub(crate) extends: Vec, +} + +impl TypeOptions { + pub(crate) fn parse(item: &mut ForeignItemType, mut on_error: impl FnMut(Error)) -> Self { + let mut options = Self::default(); + + // Type-level `#[js_sys(...)]` attributes describe the foreign type itself; + // function binding options are parsed separately. + for attr in item + .attrs + .extract_if(.., |attr| attr.path().is_ident("js_sys")) + { + if let Err(error) = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("js_name") { + let js_name = meta.value()?.parse::()?.value(); + + if options.js_name.replace(js_name).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } + } else if meta.path.is_ident("extends") { + options.extends.push(meta.value()?.parse()?); + Ok(()) + } else { + Err(meta.error("unsupported attribute")) + } + }) { + on_error(error); + } + } -pub struct Type { - pub r#struct: ItemStruct, - pub impls: [ItemImpl; 4], + options + } } impl Type { + #[cfg(any(feature = "web-idl", test))] + #[must_use] + pub(crate) fn new(hygiene: &mut Hygiene<'_>, item: ForeignItemType) -> Self { + Self::with_extends(hygiene, item, &[]) + } + #[must_use] - pub fn new(hygiene: &mut Hygiene<'_>, item: ForeignItemType) -> Self { + pub(crate) fn with_extends( + hygiene: &mut Hygiene<'_>, + item: ForeignItemType, + extends: &[Path], + ) -> Self { let span = item.span(); let ForeignItemType { attrs, @@ -45,12 +99,32 @@ impl Type { ) } else { let phantom_data = hygiene.phantom_data(&cfgs, span); + let marker_types: Vec<_> = generics + .params + .iter() + .filter_map(|param| match param { + syn::GenericParam::Lifetime(param) => { + let lifetime = ¶m.lifetime; + Some(quote_spanned!(span=> &#lifetime ())) + } + syn::GenericParam::Type(param) => { + let ident = ¶m.ident; + Some(quote_spanned!(span=> #ident)) + } + syn::GenericParam::Const(_) => None, + }) + .collect(); + let marker_type = match marker_types.as_slice() { + [] => quote!(()), + [ty] => quote!(#ty), + types => quote!((#(#types,)*)), + }; ( Fields::Named(parse_quote_spanned! {span=> { value: #js_value, - _type: #phantom_data #gen_type, + _type: #phantom_data<#marker_type>, } }), None, @@ -58,7 +132,7 @@ impl Type { ) }; - let impls = [ + let mut impls = vec![ parse_quote_spanned! {span=> #(#cfgs)* impl #gen_impl #as_ref<#js_value> for #ident #gen_type #gen_where { @@ -91,6 +165,45 @@ impl Type { }, ]; + if let Some(parent) = extends.first() { + let deref = hygiene.deref(&cfgs, span); + + impls.push(parse_quote_spanned! {span=> + #(#cfgs)* + impl #gen_impl #deref for #ident #gen_type #gen_where { + type Target = #parent; + + #[inline] + fn deref(&self) -> &Self::Target { + <#ident #gen_type as #as_ref<#parent>>::as_ref(self) + } + } + }); + } + + for parent in extends { + impls.push(parse_quote_spanned! {span=> + #(#cfgs)* + impl #gen_impl #as_ref<#parent> for #ident #gen_type #gen_where { + #[inline] + fn as_ref(&self) -> &#parent { + <#parent as #js_cast>::unchecked_from_ref( + <#ident #gen_type as #as_ref<#js_value>>::as_ref(self), + ) + } + } + }); + impls.push(parse_quote_spanned! {span=> + #(#cfgs)* + impl #gen_impl #from<#ident #gen_type> for #parent #gen_where { + #[inline] + fn from(value: #ident #gen_type) -> Self { + <#parent as #js_cast>::unchecked_from(#js_value::from(value)) + } + } + }); + } + item_attrs.append(&mut cfgs); item_attrs.push(parse_quote_spanned! {span=>#[repr(transparent)]}); @@ -110,18 +223,13 @@ impl Type { impl IntoIterator for Type { type Item = Item; - type IntoIter = array::IntoIter; + type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - let [impl_1, impl_2, impl_3, impl_4] = self.impls; - [ - Item::from(self.r#struct), - impl_1.into(), - impl_2.into(), - impl_3.into(), - impl_4.into(), - ] - .into_iter() + let mut items = Vec::with_capacity(self.impls.len() + 1); + items.push(Item::from(self.r#struct)); + items.extend(self.impls.into_iter().map(Item::from)); + items.into_iter() } } diff --git a/host/js-sys-bindgen/src/web_idl.rs b/host/js-sys-bindgen/src/web_idl.rs index 0bd985fc..c0b37fa5 100644 --- a/host/js-sys-bindgen/src/web_idl.rs +++ b/host/js-sys-bindgen/src/web_idl.rs @@ -3,7 +3,8 @@ use syn::{Attribute, File, Ident, Item, Path, Visibility, parse_quote}; use weedle::common::Docstring; use weedle::{Definition, Err, Error, InterfaceDefinition}; -use crate::{Hygiene, ImportManager, Type}; +use crate::hygiene::{Hygiene, ImportManager}; +use crate::r#type::Type; pub fn web_idl<'i>( web_idl: &'i str, diff --git a/host/js-sys-macro/Cargo.toml b/host/js-sys-macro/Cargo.toml index 2e203f94..14d8be81 100644 --- a/host/js-sys-macro/Cargo.toml +++ b/host/js-sys-macro/Cargo.toml @@ -13,7 +13,7 @@ proc-macro = true test = false [dependencies] -js-sys-bindgen = { workspace = true, features = ["macro"] } +js-sys-bindgen = { workspace = true } proc-macro2 = { workspace = true, features = ["proc-macro"] } [lints] diff --git a/host/js-sys-macro/src/lib.rs b/host/js-sys-macro/src/lib.rs index 069b62c0..34f596df 100644 --- a/host/js-sys-macro/src/lib.rs +++ b/host/js-sys-macro/src/lib.rs @@ -1,20 +1,16 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; - use js_sys_bindgen::syn::Error; use proc_macro::TokenStream; -static CLOSURE_ID: AtomicUsize = AtomicUsize::new(0); - #[proc_macro] pub fn closure(input: TokenStream) -> TokenStream { - js_sys_bindgen::closure(input.into(), CLOSURE_ID.fetch_add(1, Ordering::Relaxed)) + js_sys_bindgen::closure(input.into()) .unwrap_or_else(Error::into_compile_error) .into() } #[proc_macro_attribute] pub fn js_sys(attr: TokenStream, item: TokenStream) -> TokenStream { - js_sys_bindgen::r#macro(attr.into(), item.into(), None) + js_sys_bindgen::r#macro(attr.into(), item.into()) .unwrap_or_else(|e| e) .into() } diff --git a/host/ld-shared/src/lib.rs b/host/ld-shared/src/lib.rs index 9a2c2843..1dc5cb73 100644 --- a/host/ld-shared/src/lib.rs +++ b/host/ld-shared/src/lib.rs @@ -9,6 +9,9 @@ use object::read::archive::ArchiveFile; use rwat::ParseOptions; use wasmparser::CustomSectionReader; +pub const WAT_SECTION: &str = "js_bindgen.wat"; +pub const IMPORT_SECTION: &str = "js_bindgen.import"; + /// Creates a relocatable Wasm object from the WAT input. pub fn wat_to_object(wasm64: bool, wat: &str) -> rwat::Result> { // `wasm-ld` requires a `(memory i64)` in every object file if the requested @@ -117,7 +120,7 @@ pub struct JsBindgenWatSectionParser<'cs>(CustomSectionParser<'cs>); impl<'cs> JsBindgenWatSectionParser<'cs> { #[must_use] pub fn new(custom_section: &CustomSectionReader<'cs>) -> Self { - Self(CustomSectionParser::new(custom_section)) + Self(CustomSectionParser::new(custom_section, true)) } } @@ -163,7 +166,10 @@ pub struct JsRequiredEmbed<'cs> { impl<'cs> JsBindgenJsSectionParser<'cs> { #[must_use] pub fn new(custom_section: &CustomSectionReader<'cs>) -> Self { - Self(CustomSectionParser::new(custom_section)) + Self(CustomSectionParser::new( + custom_section, + custom_section.name() == IMPORT_SECTION, + )) } } @@ -247,14 +253,71 @@ impl<'cs> Iterator for JsBindgenJsSectionParser<'cs> { #[derive(Clone)] struct CustomSectionParser<'cs> { name: &'cs str, - data: &'cs [u8], + data: SectionData<'cs>, +} + +#[derive(Clone)] +enum SectionData<'cs> { + Records(&'cs [u8]), + Framed { + blocks: &'cs [u8], + records: &'cs [u8], + }, } impl<'cs> CustomSectionParser<'cs> { - fn new(custom_section: &CustomSectionReader<'cs>) -> Self { + fn new(custom_section: &CustomSectionReader<'cs>, framed: bool) -> Self { + let data = if framed { + // Linkers concatenate statics assigned to the same custom section. + // Each block carries its allocated and initialized lengths, followed + // by length-prefixed records and any trailing padding. + SectionData::Framed { + blocks: custom_section.data(), + records: &[], + } + } else { + SectionData::Records(custom_section.data()) + }; + Self { name: custom_section.name(), - data: custom_section.data(), + data, + } + } + + fn next_record(&mut self) -> Option<&'cs [u8]> { + let name = self.name; + + match &mut self.data { + SectionData::Records(records) => take_record(name, records), + SectionData::Framed { blocks, records } => loop { + if !records.is_empty() { + return take_record(name, records); + } + if blocks.is_empty() { + return None; + } + + let header = blocks.split_off(..8).unwrap_or_else(|| { + panic!("found incomplete block header in custom section `{name}`") + }); + let capacity = u32::from_le_bytes(header[..4].try_into().unwrap()) as usize; + let used = u32::from_le_bytes(header[4..].try_into().unwrap()) as usize; + + assert!( + used <= capacity, + "block uses {used} bytes but has capacity {capacity} in custom section \ + `{name}`" + ); + + let block = blocks.split_off(..capacity).unwrap_or_else(|| { + panic!( + "block has capacity {capacity}, but not enough bytes remain in custom \ + section `{name}`" + ) + }); + *records = &block[..used]; + }, } } } @@ -263,21 +326,21 @@ impl<'cs> Iterator for CustomSectionParser<'cs> { type Item = &'cs [u8]; fn next(&mut self) -> Option { - if let Some(length) = self.data.split_off(..4) { - let length = u32::from_le_bytes(length.try_into().unwrap()) as usize; + self.next_record() + } +} - let data = self.data.split_off(..length).unwrap_or_else(|| { - panic!("invalid length encoding in custom section `{}`", self.name) - }); +fn take_record<'data>(name: &str, data: &mut &'data [u8]) -> Option<&'data [u8]> { + if let Some(length) = data.split_off(..4) { + let length = u32::from_le_bytes(length.try_into().unwrap()) as usize; - Some(data) - } else if self.data.is_empty() { - None - } else { - panic!( - "found left over bytes in custom section `{}`: {:?}", - self.name, self.data - ); - } + Some( + data.split_off(..length) + .unwrap_or_else(|| panic!("invalid length encoding in custom section `{name}`")), + ) + } else if data.is_empty() { + None + } else { + panic!("found left over bytes in custom section `{name}`: {data:?}"); } } diff --git a/host/ld/src/post.rs b/host/ld/src/post.rs index 67b97f2e..d5e54577 100644 --- a/host/ld/src/post.rs +++ b/host/ld/src/post.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result, bail}; use js_bindgen_cli_lib::{JS_OUTPUT_SECTION, MainMemory}; +use js_bindgen_ld_shared::{IMPORT_SECTION, WAT_SECTION}; use js_bindgen_shared::{IS_COMPAT_SECTION, IS_TEST_SECTION}; use wasm_encoder::{ CustomSection, EntityType, ExportSection, ImportSection, Module, ProducersField, @@ -73,8 +74,7 @@ pub fn processing( export_section.append_to(&mut wasm_output); } // Don't write back our own custom sections. - Payload::CustomSection(c) if c.name() == "js_bindgen.wat" => (), - Payload::CustomSection(c) if c.name() == "js_bindgen.import" => (), + Payload::CustomSection(c) if matches!(c.name(), WAT_SECTION | IMPORT_SECTION) => {} Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => (), Payload::CustomSection(c) if c.name() == "js_bindgen.export" => (), // Register ourselves in the producer section. diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index a853c5ab..dbdfd5d5 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -5,7 +5,7 @@ use std::time::SystemTime; use anyhow::Result; use js_bindgen_cli_lib::MainMemory; -use js_bindgen_ld_shared::JsBindgenWatSectionParser; +use js_bindgen_ld_shared::{IMPORT_SECTION, JsBindgenWatSectionParser, WAT_SECTION}; use js_bindgen_shared::ReadFile; use wasmparser::{Parser, Payload}; @@ -120,7 +120,7 @@ fn process_object( // We are only interested in reading custom sections with our name. match &payload { - Payload::CustomSection(c) if c.name() == "js_bindgen.wat" => { + Payload::CustomSection(c) if c.name() == WAT_SECTION => { for wat in JsBindgenWatSectionParser::new(c) { file_counter += 1; let wasm_path = @@ -175,7 +175,7 @@ fn process_object( } } // Extract all JS imports. - Payload::CustomSection(c) if c.name() == "js_bindgen.import" => { + Payload::CustomSection(c) if c.name() == IMPORT_SECTION => { js_store.add_js_imports(c)?; } // Extract all JS embeds. diff --git a/host/macro/src/custom_section.rs b/host/macro/src/custom_section.rs index 90378571..220d8233 100644 --- a/host/macro/src/custom_section.rs +++ b/host/macro/src/custom_section.rs @@ -738,71 +738,69 @@ impl CustomSection { .flatten() } - /// ```"not rust" - /// #[repr(C)] - /// struct Layout([u8; 4], #([u8; LEN_]),*); - /// ``` - fn output_layout(&self) -> impl Iterator { + /// When `framed` is true, the block capacity and used length precede the + /// record length. Otherwise, the layout contains only the record length. + fn output_layout(&self, framed: bool) -> impl Iterator { let span = Span::mixed_site(); + let header_fields = if framed { 3 } else { 1 }; - // ``` - // [u8; 4], #([u8; LEN_]),* - // ``` - let tys = [ - group( - Delimiter::Bracket, - path(["core", "primitive", "u8"], span).chain([ - Punct::new(';', Spacing::Alone).into(), - Literal::usize_unsuffixed(4).into(), - ]), - ), - Punct::new(',', Spacing::Alone).into(), - ] - .into_iter() - .chain(self.flattened_values().flat_map(move |value| { - value - .cfg_iter() - .chain([ + let tys = (0..header_fields) + .flat_map(move |_| { + [ group( Delimiter::Bracket, path(["core", "primitive", "u8"], span).chain([ Punct::new(';', Spacing::Alone).into(), - match value.kind { - FlattenedValueKind::Bytes(bytes) => { - Literal::usize_unsuffixed(bytes.len()).into() - } - FlattenedValueKind::Const | FlattenedValueKind::Interpolate => { - ident(&format!("LEN_{}", value.name)) - } - FlattenedValueKind::InterpolateWithLength => { - Literal::usize_unsuffixed(2).into() - } - FlattenedValueKind::TupleCount => { - Literal::usize_unsuffixed(1).into() - } - }, + Literal::usize_unsuffixed(4).into(), ]), ), Punct::new(',', Spacing::Alone).into(), - ]) - .chain( - matches!(value.kind, FlattenedValueKind::InterpolateWithLength) - .then(|| { - value.cfg_iter().chain([ - group( - Delimiter::Bracket, - path(["core", "primitive", "u8"], span).chain([ - Punct::new(';', Spacing::Alone).into(), - ident(&format!("LEN_{}", value.name)), - ]), - ), - Punct::new(',', Spacing::Alone).into(), - ]) - }) - .into_iter() - .flatten(), - ) - })); + ] + }) + .chain(self.flattened_values().flat_map(move |value| { + value + .cfg_iter() + .chain([ + group( + Delimiter::Bracket, + path(["core", "primitive", "u8"], span).chain([ + Punct::new(';', Spacing::Alone).into(), + match value.kind { + FlattenedValueKind::Bytes(bytes) => { + Literal::usize_unsuffixed(bytes.len()).into() + } + FlattenedValueKind::Const | FlattenedValueKind::Interpolate => { + ident(&format!("LEN_{}", value.name)) + } + FlattenedValueKind::InterpolateWithLength => { + Literal::usize_unsuffixed(2).into() + } + FlattenedValueKind::TupleCount => { + Literal::usize_unsuffixed(1).into() + } + }, + ]), + ), + Punct::new(',', Spacing::Alone).into(), + ]) + .chain( + matches!(value.kind, FlattenedValueKind::InterpolateWithLength) + .then(|| { + value.cfg_iter().chain([ + group( + Delimiter::Bracket, + path(["core", "primitive", "u8"], span).chain([ + Punct::new(';', Spacing::Alone).into(), + ident(&format!("LEN_{}", value.name)), + ]), + ), + Punct::new(',', Spacing::Alone).into(), + ]) + }) + .into_iter() + .flatten(), + ) + })); // ``` // #[repr(C)] @@ -825,11 +823,7 @@ impl CustomSection { .into_iter() } - /// ```"not rust" - /// #[link_section = name] - /// static CUSTOM_SECTION: Layout = Layout(...(u32::to_le_bytes(LEN), #(ARR_),*)); - /// ``` - fn output_custom_section(&self, name: &str) -> impl Iterator { + fn output_custom_section(&self, name: &str, framed: bool) -> impl Iterator { let span = Span::mixed_site(); // ``` @@ -853,12 +847,23 @@ impl CustomSection { ), ]; - // ``` - // (u32::to_le_bytes(LEN), #(ARR_),*) - // ``` let values = group( Delimiter::Parenthesis, - path(["core", "primitive", "u32", "to_le_bytes"], span) + (0..if framed { 2 } else { 0 }) + .flat_map(move |_| { + path(["core", "primitive", "u32", "to_le_bytes"], span).chain([ + group( + Delimiter::Parenthesis, + [ + ident("LEN"), + Punct::new('+', Spacing::Alone).into(), + Literal::u32_unsuffixed(4).into(), + ], + ), + Punct::new(',', Spacing::Alone).into(), + ]) + }) + .chain(path(["core", "primitive", "u32", "to_le_bytes"], span)) .chain([ group(Delimiter::Parenthesis, iter::once(ident("LEN"))), Punct::new(',', Spacing::Alone).into(), @@ -901,22 +906,7 @@ impl CustomSection { link_section.into_iter().chain(custom_section) } - /// ```"not rust" - /// const _: () = { - /// const LEN: u32 = { - /// let mut len: usize = 0; - /// #(len += LEN_;)* - /// len as _ - /// }; - /// - /// #[repr(C)] - /// struct Layout([u8; 4], #([u8; LEN_]),*); - /// - /// #[link_section = name] - /// static CUSTOM_SECTION: Layout = Layout(u32::to_le_bytes(LEN), #(ARR_),*); - /// }; - /// ``` - pub fn output(self, name: &str) -> TokenStream { + fn output_inner(self, name: &str, framed: bool) -> TokenStream { r#const( "_", iter::once(group(Delimiter::Parenthesis, iter::empty())), @@ -925,12 +915,20 @@ impl CustomSection { self.output_values() .chain(self.output_len()) .chain(self.output_tuple_count()) - .chain(self.output_layout()) - .chain(self.output_custom_section(name)), + .chain(self.output_layout(framed)) + .chain(self.output_custom_section(name, framed)), )), ) .collect() } + + pub fn output(self, name: &str) -> TokenStream { + self.output_inner(name, false) + } + + pub fn output_framed(self, name: &str) -> TokenStream { + self.output_inner(name, true) + } } impl Bytes { diff --git a/host/macro/src/lib.rs b/host/macro/src/lib.rs index 76cc09f6..98edf1e1 100644 --- a/host/macro/src/lib.rs +++ b/host/macro/src/lib.rs @@ -30,7 +30,7 @@ fn global_wat_internal(input: TokenStream) -> Result { let mut custom_section = CustomSection::new(); parse_string_arguments(&mut input, Span::mixed_site(), &mut custom_section)?; - Ok(custom_section.output("js_bindgen.wat")) + Ok(custom_section.output_framed("js_bindgen.wat")) } #[proc_macro] @@ -43,7 +43,7 @@ pub fn embed_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream { } fn embed_js_internal(input: TokenStream) -> Result { - js_internal(input, "js_bindgen.embed") + js_internal(input, "js_bindgen.embed", false) } #[proc_macro] @@ -58,7 +58,7 @@ pub fn import_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream } fn import_js_internal(input: TokenStream) -> Result { - js_internal(input, "js_bindgen.import") + js_internal(input, "js_bindgen.import", true) } #[proc_macro] @@ -73,10 +73,14 @@ pub fn export_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream } fn export_js_internal(input: TokenStream) -> Result { - js_internal(input, "js_bindgen.export") + js_internal(input, "js_bindgen.export", false) } -fn js_internal(input: TokenStream, section: &str) -> Result { +fn js_internal( + input: TokenStream, + section: &str, + framed: bool, +) -> Result { let mut input = input.into_iter().peekable(); let mut custom_section = CustomSection::new(); @@ -87,7 +91,11 @@ fn js_internal(input: TokenStream, section: &str) -> Result> 96) as u32; @@ -14,6 +17,7 @@ fn main() { let uuid = format!("{g1:08x}-{g2:04x}-{g3:04x}-{g4:04x}-{g5:012x}"); let elapsed = ins.elapsed(); + println!("JS {}", err.to_string()); println!("result: {uuid}, cost: {elapsed:?}"); } From 7d95ea190cc4513fe4e46d480445313be187255b Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:05:07 +0800 Subject: [PATCH 07/21] Use an indirect table for Closure --- benchmarks/js-bindgen/src/lib.rs | 13 +- benchmarks/wasm-bindgen/src/lib.rs | 13 +- client/e2e/examples/closure.rs | 69 +++++++ client/js-sys/src/macro.rs | 6 + client/js-sys/src/macro/closure.rs | 270 +++++++++++++++++++++++++ client/js-sys/src/runtime/closure.rs | 47 +++-- client/js-sys/src/runtime/exception.rs | 8 +- host/js-sys-bindgen/src/closure.rs | 106 ++++++---- host/js-sys-bindgen/src/export.rs | 196 +++++++++++------- host/ld-shared/src/lib.rs | 2 +- host/ld/src/js.rs | 155 ++++++++------ host/ld/src/pre.rs | 8 + 12 files changed, 686 insertions(+), 207 deletions(-) create mode 100644 client/js-sys/src/macro/closure.rs diff --git a/benchmarks/js-bindgen/src/lib.rs b/benchmarks/js-bindgen/src/lib.rs index fb076185..4571cec5 100644 --- a/benchmarks/js-bindgen/src/lib.rs +++ b/benchmarks/js-bindgen/src/lib.rs @@ -1,4 +1,5 @@ -use core::{array, hint::black_box}; +use core::array; +use core::hint::black_box; use js_sys::{Closure, JsValue, closure, js_sys}; @@ -105,11 +106,16 @@ extern "js-sys" { #[js_sys(js_embed = "invoke_closure")] fn invoke_closure_raw(callback: &Closure i32>, value: i32) -> i32; + + #[js_sys(js_embed = "invoke_closure")] + fn invoke_closure_u128_raw(callback: &Closure u128>, value: u128) -> u128; } std::thread_local! { static CALLBACK: Closure i32> = closure!(dyn FnMut(i32) -> i32, |value| value); + static CALLBACK_U128: Closure u128> = + closure!(dyn FnMut(u128) -> u128, |value| value); } #[js_sys] @@ -117,6 +123,11 @@ fn bench_closure_call(value: i32) -> i32 { CALLBACK.with(|callback| invoke_closure_raw(callback, value)) } +#[js_sys] +fn bench_closure_call_u128(value: u128) -> u128 { + CALLBACK_U128.with(|callback| invoke_closure_u128_raw(callback, value)) +} + #[js_sys] fn bench_export_bool() -> bool { true diff --git a/benchmarks/wasm-bindgen/src/lib.rs b/benchmarks/wasm-bindgen/src/lib.rs index 0938076c..2b469e2d 100644 --- a/benchmarks/wasm-bindgen/src/lib.rs +++ b/benchmarks/wasm-bindgen/src/lib.rs @@ -1,4 +1,5 @@ -use core::{array, hint::black_box}; +use core::array; +use core::hint::black_box; use wasm_bindgen::prelude::*; @@ -92,11 +93,16 @@ extern "C" { extern "C" { #[wasm_bindgen(js_name = invoke_closure)] fn invoke_closure_raw(callback: &Closure i32>, value: i32) -> i32; + + #[wasm_bindgen(js_name = invoke_closure)] + fn invoke_closure_u128_raw(callback: &Closure u128>, value: u128) -> u128; } std::thread_local! { static CALLBACK: Closure i32> = Closure::new(|value| value); + static CALLBACK_U128: Closure u128> = + Closure::new(|value| value); } #[wasm_bindgen] @@ -104,6 +110,11 @@ pub fn bench_closure_call(value: i32) -> i32 { CALLBACK.with(|callback| invoke_closure_raw(callback, value)) } +#[wasm_bindgen] +pub fn bench_closure_call_u128(value: u128) -> u128 { + CALLBACK_U128.with(|callback| invoke_closure_u128_raw(callback, value)) +} + #[wasm_bindgen] pub fn bench_export_bool() -> bool { true diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs index 9a7f48b6..c6013336 100644 --- a/client/e2e/examples/closure.rs +++ b/client/e2e/examples/closure.rs @@ -1,12 +1,16 @@ #[rustfmt::skip] fn main() { // ;; exports["closure_i32"](20) === 43 + // ;; exports["closure_same_signature"](20) === 4280 + // ;; exports["closure_macro_repetition"](20) === 84 // ;; exports["closure_u128"](1n << 96n) === (1n << 96n) + 1n // ;; (() => { const value = {}; return exports["closure_js_value"](value) === value })() + // ;; exports["closure_js_string_ref"]("closure") === "closure" // ;; exports["closure_result"](41, false) === 42 // ;; (() => { try { exports["closure_result"](41, true); return false } catch (error) { return error === "closure error" } })() // ;; exports["closure_lifecycle"]() // ;; exports["closure_fn_reentrant"](20) === 22 + // ;; exports["closure_unref_during_call"]() // ;; exports["closure_owned"](20) === 21 // ;; exports["closure_owned_lifecycle"]() // ;; exports["closure_once"](20) === 21 @@ -28,6 +32,12 @@ static DROPS: AtomicU32 = AtomicU32::new(0); struct DropCounter; +macro_rules! repeated_closures { + ($($offset:expr),+ $(,)?) => { + ($(closure!(dyn FnMut(i32) -> i32, move |value| value + $offset)),+) + }; +} + impl Drop for DropCounter { fn drop(&mut self) { DROPS.fetch_add(1, Ordering::Relaxed); @@ -99,6 +109,18 @@ js_sys::js_bindgen::embed_js!( "}}", ); +js_sys::js_bindgen::embed_js!( + module = "closure", + name = "release.twice", + required_embeds = [("closure", "storage")], + "() => {{", + " const callback = this.#jsEmbed.closure.storage.callback", + " callback.unref()", + " callback.unref()", + " return true", + "}}", +); + #[js_sys] extern "js-sys" { #[js_sys(js_embed = "invoke.twice")] @@ -113,6 +135,12 @@ extern "js-sys" { value: JsValue, ) -> JsValue; + #[js_sys(js_embed = "invoke")] + fn invoke_js_string_ref( + callback: &Closure JsString>, + value: &JsString, + ) -> JsString; + #[js_sys(js_embed = "invoke")] fn invoke_result( callback: &Closure Result>, @@ -154,6 +182,9 @@ extern "js-sys" { #[js_sys(js_embed = "release")] fn release() -> bool; + + #[js_sys(js_embed = "release.twice")] + fn release_twice() -> bool; } #[js_sys] @@ -167,6 +198,21 @@ fn closure_i32(value: i32) -> i32 { invoke_i32_twice(&callback, value) } +#[js_sys] +fn closure_same_signature(value: i32) -> i32 { + let first = closure!(dyn FnMut(i32) -> i32, |value| value + 1); + let second = closure!(dyn FnMut(i32) -> i32, |value| value * 2); + + invoke_i32_twice(&first, value) * 100 + invoke_i32_twice(&second, value) +} + +#[js_sys] +fn closure_macro_repetition(value: i32) -> i32 { + let (first, second) = repeated_closures!(1, 1); + + invoke_i32_twice(&first, value) + invoke_i32_twice(&second, value) +} + #[js_sys] fn closure_u128(value: u128) -> u128 { let callback = closure!(dyn FnMut(u128) -> u128, move |value| value + 1); @@ -181,6 +227,13 @@ fn closure_js_value(value: JsValue) -> JsValue { invoke_js_value(&callback, value) } +#[js_sys] +fn closure_js_string_ref(value: &JsString) -> JsString { + let callback = closure!(dyn FnMut(&JsString) -> JsString, JsString::clone); + + invoke_js_string_ref(&callback, value) +} + #[js_sys] fn closure_result(value: i32, error: bool) -> Result { let callback = closure!(dyn FnMut(i32) -> Result, move |value| { @@ -221,6 +274,22 @@ fn closure_fn_reentrant(value: i32) -> i32 { invoke_saved(value) } +#[js_sys] +fn closure_unref_during_call() -> bool { + DROPS.store(0, Ordering::Relaxed); + let counter = DropCounter; + let callback = closure!(dyn Fn(i32) -> i32, move |value| { + let _ = &counter; + let released = release_twice(); + let alive = DROPS.load(Ordering::Relaxed) == 0; + + if released && alive { value + 1 } else { 0 } + }); + save_fn(&callback); + + invoke_saved(41) == 42 && DROPS.load(Ordering::Relaxed) == 1 +} + #[js_sys] fn closure_owned(value: i32) -> i32 { let callback = closure!(dyn FnMut(i32) -> i32, move |value| value + 1); diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index 67b4296f..e2ac4258 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -1,4 +1,5 @@ mod abi; +mod closure; mod export; mod import; mod js_import; @@ -27,6 +28,11 @@ pub use crate::{ js_function, js_import, js_indirect_function, js_input_parameters, js_needs_shim, js_output, js_parameter, }; +// WAT closure shims. +pub use crate::{ + wat_closure, wat_closure_call, wat_closure_direct, wat_closure_indirect, + wat_closure_table_import, +}; // WAT export shims. pub use crate::{wat_export, wat_export_direct, wat_export_indirect}; // WAT import shims. diff --git a/client/js-sys/src/macro/closure.rs b/client/js-sys/src/macro/closure.rs new file mode 100644 index 00000000..97294e26 --- /dev/null +++ b/client/js-sys/src/macro/closure.rs @@ -0,0 +1,270 @@ +// WAT shims for Rust closures called from JavaScript. + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_closure_table_import { + () => {{ + const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); + + $crate::r#macro::const_concat!( + "\n(import \"env\" \"__indirect_function_table\" ", + "(table $js_sys.closure.table ", + "(@sym (name \"__indirect_function_table\")) ", + POINTER, + " 0 funcref))" + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_closure_call { + ($call_shim:ty $(,)?) => {{ + const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); + type StoredCallShim = $call_shim; + const OFFSET_VALUE: ::core::primitive::usize = + $crate::ClosureHeader::call_shim_offset::(); + const OFFSET: &::core::primitive::str = $crate::r#macro::const_integer_str!(OFFSET_VALUE); + + $crate::r#macro::const_concat!( + " local.get $js_sys.closure.data\n ", + POINTER, + ".load offset=", + OFFSET, + "\n call_indirect $js_sys.closure.table (type $js_sys.closure.call) (@reloc)" + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_closure_direct { + ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*) $(,)?) => {{ + const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); + const DATA: $crate::r#macro::WatSlot = + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>()[0]; + + $crate::r#macro::const_concat!( + $crate::r#macro::wat_imports!( + slots = [ + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + ], + extras = [], + ), + $crate::r#macro::wat_closure_table_import!(), + "\n(type $js_sys.closure.call (func (param ", + POINTER, + ")", + $($crate::r#macro::wat_input!(export raw_param; $input),)* + "))\n", + "(func $export (@sym (name \"", + $export, + "\")) (param $data ", + DATA.boundary, + ")", + $($crate::r#macro::wat_input!(export params; $par, $input),)* + " (local $js_sys.closure.data ", + POINTER, + ")", + $crate::r#macro::wat_locals!( + slots = [ + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + ], + extras = [], + ), + "\n local.get $data", + $crate::r#macro::wat_conv_prefix(DATA.conv), + DATA.conv, + "\n local.set $js_sys.closure.data\n", + " local.get $js_sys.closure.data\n", + $($crate::r#macro::wat_input!(export gets; $par, $input),)* + $crate::r#macro::wat_closure_call!($call_shim), + "\n)" + ) + }}; + ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); + const DATA: $crate::r#macro::WatSlot = + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>()[0]; + const OUTPUT: $crate::r#macro::WatSlot = + $crate::r#macro::return_into_js_wat_slots::<$output>()[0]; + + $crate::r#macro::const_concat!( + $crate::r#macro::wat_imports!( + slots = [ + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), + $crate::r#macro::wat_closure_table_import!(), + "\n(type $js_sys.closure.call (func (param ", + POINTER, + ")", + $($crate::r#macro::wat_input!(export raw_param; $input),)* + " (result ", + OUTPUT.abi, + ")))\n", + "(func $export (@sym (name \"", + $export, + "\")) (param $data ", + DATA.boundary, + ")", + $($crate::r#macro::wat_input!(export params; $par, $input),)* + " (result ", + OUTPUT.boundary, + ") (local $js_sys.closure.data ", + POINTER, + ")", + $crate::r#macro::wat_locals!( + slots = [ + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), + "\n local.get $data", + $crate::r#macro::wat_conv_prefix(DATA.conv), + DATA.conv, + "\n local.set $js_sys.closure.data\n", + " local.get $js_sys.closure.data\n", + $($crate::r#macro::wat_input!(export gets; $par, $input),)* + $crate::r#macro::wat_closure_call!($call_shim), + $crate::r#macro::wat_conv_prefix(OUTPUT.conv), + OUTPUT.conv, + "\n)" + ) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! wat_closure_indirect { + ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); + const DATA: $crate::r#macro::WatSlot = + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>()[0]; + const SIZE: &::core::primitive::str = $crate::r#macro::const_integer_str!( + $crate::r#macro::export_output_frame_size::<$output>() + ); + const RESULT_TYPES: &::core::primitive::str = + $crate::r#macro::wat_slots!( + types, + $crate::r#macro::return_into_js_wat_slots::<$output>(), + boundary, + ); + + $crate::r#macro::const_concat!( + $crate::r#macro::wat_imports!( + slots = [ + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), + $crate::r#macro::wat_closure_table_import!(), + "\n(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut ", + POINTER, + ")))\n", + "(type $js_sys.closure.call (func (param ", + POINTER, + ") (param ", + POINTER, + ")", + $($crate::r#macro::wat_input!(export raw_param; $input),)* + "))\n", + "(func $export (@sym (name \"", + $export, + "\")) (param $data ", + DATA.boundary, + ")", + $($crate::r#macro::wat_input!(export params; $par, $input),)* + " (result ", + RESULT_TYPES, + ")\n (local $retptr ", + POINTER, + ")\n (local $js_sys.closure.data ", + POINTER, + ")", + $crate::r#macro::wat_locals!( + slots = [ + $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), + $($crate::r#macro::from_js_wat_slots::<$input>(),)* + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ], + extras = [], + ), + "\n local.get $data", + $crate::r#macro::wat_conv_prefix(DATA.conv), + DATA.conv, + "\n local.set $js_sys.closure.data\n", + " global.get $__stack_pointer\n ", + POINTER, + ".const ", + SIZE, + "\n ", + POINTER, + ".sub\n local.tee $retptr\n global.set $__stack_pointer\n", + " local.get $retptr\n", + " local.get $js_sys.closure.data\n", + $($crate::r#macro::wat_input!(export gets; $par, $input),)* + $crate::r#macro::wat_closure_call!($call_shim), + "\n", + $crate::r#macro::wat_slots!( + loads, + $output, + $crate::r#macro::return_into_js_wat_slots::<$output>(), + ), + " local.get $retptr\n ", + POINTER, + ".const ", + SIZE, + "\n ", + POINTER, + ".add\n global.set $__stack_pointer\n)" + ) + }}; +} + +/// Generates a closure dispatcher that calls the raw shim stored in its +/// allocation. +#[doc(hidden)] +#[macro_export] +macro_rules! wat_closure { + ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*) $(,)?) => {{ + $crate::r#macro::validate_from_js::<::core::primitive::usize>(); + $($crate::r#macro::validate_from_js::<$input>();)* + + $crate::r#macro::wat_closure_direct!( + $export, + $call_shim, + ($(($par, $input)),*), + ) + }}; + ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + $crate::r#macro::validate_from_js::<::core::primitive::usize>(); + $($crate::r#macro::validate_from_js::<$input>();)* + $crate::r#macro::validate_return_into_js::<$output>(); + + if $crate::r#macro::return_into_js_is_direct::<$output>() { + $crate::r#macro::wat_closure_direct!( + $export, + $call_shim, + ($(($par, $input)),*), + $output, + ) + } else { + $crate::r#macro::wat_closure_indirect!( + $export, + $call_shim, + ($(($par, $input)),*), + $output, + ) + } + }}; +} diff --git a/client/js-sys/src/runtime/closure.rs b/client/js-sys/src/runtime/closure.rs index 5166ed0b..e3a9aca4 100644 --- a/client/js-sys/src/runtime/closure.rs +++ b/client/js-sys/src/runtime/closure.rs @@ -39,6 +39,15 @@ impl ClosureHeader { Self { drop } } + /// Returns the byte offset of the call shim in a closure allocation. + #[doc(hidden)] + #[must_use] + pub const fn call_shim_offset() -> usize { + // This is the byte offset used to load `call_shim` from the allocation; + // the loaded field value, not this offset, is the function table index. + core::mem::offset_of!(ClosurePrefix, call_shim) + } + unsafe fn release(pointer: *mut Self) { // SAFETY: The caller guarantees that `pointer` identifies a live header. // Read the function pointer before it releases the containing allocation. @@ -47,19 +56,6 @@ impl ClosureHeader { unsafe { drop(pointer) }; } - /// Reads the call shim stored after this header. - /// - /// # Safety - /// - /// `pointer` must come from [`ClosureAllocation::new`], and `C` must be the - /// call shim type used to create that allocation. - #[inline] - pub unsafe fn call_shim(pointer: *mut Self) -> C { - let pointer = pointer.cast::>(); - // SAFETY: The caller guarantees the allocation and `C` match. - unsafe { ptr::read(&raw const (*pointer).call_shim) } - } - /// Returns the captured callback stored after this header. /// /// # Safety @@ -155,7 +151,8 @@ js_bindgen::embed_js!( name = "closure.own", required_embeds = [("js_sys", "closure.finalization")], "(callback, state) => {{", - " callback.unref = () => {{", + " let owned = true", + " const release = () => {{", " state.references -= 1", " if (state.references === 0) {{", " const data = state.data", @@ -164,10 +161,15 @@ js_bindgen::embed_js!( " if (data) this.#jsExports.closure_drop(data)", " }}", " }}", + " callback.unref = () => {{", + " if (!owned) return", + " owned = false", + " release()", + " }}", " this.#jsEmbed.js_sys['closure.finalization'].register(", " callback, state, state", " )", - " return callback", + " return release", "}}", ); @@ -185,10 +187,11 @@ js_bindgen::embed_js!( " try {{", " return call(state.data, ...args)", " }} finally {{", - " callback.unref()", + " release()", " }}", " }}", - " return this.#jsEmbed.js_sys['closure.own'](callback, state)", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", "}}", ); @@ -209,10 +212,11 @@ js_bindgen::embed_js!( " return call(data, ...args)", " }} finally {{", " state.data = data", - " callback.unref()", + " release()", " }}", " }}", - " return this.#jsEmbed.js_sys['closure.own'](callback, state)", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", "}}", ); @@ -237,10 +241,11 @@ js_bindgen::embed_js!( " return call(data, ...args)", " }} finally {{", " state.data = data", - " callback.unref()", + " release()", " }}", " }}", - " return this.#jsEmbed.js_sys['closure.own'](callback, state)", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", "}}", ); diff --git a/client/js-sys/src/runtime/exception.rs b/client/js-sys/src/runtime/exception.rs index 6080e31c..e5ae6d03 100644 --- a/client/js-sys/src/runtime/exception.rs +++ b/client/js-sys/src/runtime/exception.rs @@ -4,6 +4,10 @@ use core::cell::Cell; use super::externref; use crate::JsValue; +thread_local! { + static EXCEPTION: Cell = const { Cell::new(0) }; +} + #[cfg(target_feature = "exception-handling")] js_bindgen::import_js!( module = "js_sys", @@ -11,10 +15,6 @@ js_bindgen::import_js!( "WebAssembly.JSTag", ); -thread_local! { - static EXCEPTION: Cell = const { Cell::new(0) }; -} - fn set(index: i32) { EXCEPTION.with(|exception| { debug_assert_eq!(exception.get(), 0); diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index 7bd440e5..d5149873 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -11,6 +11,8 @@ use syn::{ }; use xxhash_rust::xxh3::xxh3_128; +use crate::export::{ExportAbi, lower_abi}; + mod keyword { syn::custom_keyword!(js_sys); } @@ -29,7 +31,6 @@ pub(crate) fn closure_with( package_name: &str, package_version: &str, ) -> Result { - let input_text = input.to_string(); let ClosureInput { js_sys, trait_object, @@ -40,36 +41,37 @@ pub(crate) fn closure_with( let js_sys = js_sys.unwrap_or_else(|| parse_quote_spanned!(span=> ::js_sys)); // The package identity is part of the descriptor, so the hash is deterministic // and does not depend on macro expansion order or parallel compilation. - let symbol_id = closure_symbol_hash( - crate_name, - package_name, - package_version, - &input_text, - &trait_object, - &expression, - ); - let call_ident = format_ident!("closure_call_{symbol_id}", span = span); + let symbol_id = closure_symbol_hash(crate_name, package_name, package_version, &trait_object); + let call_name_value = format!("closure_call_{symbol_id}"); + let call_name = syn::LitStr::new(&call_name_value, span); let factory_ident = format_ident!("closure_new_{symbol_id}", span = span); let factory_name = syn::LitStr::new(&format!("closure.new.{symbol_id}"), span); let factory_embed = syn::LitStr::new(signature.kind.factory_embed(), span); let closure = signature.closure_type(&trait_object); let factory_js = syn::LitStr::new( &format!( - "(data) => this.#jsEmbed.js_sys['{}'](data, this.#jsExports['{call_ident}'])", + "(data) => this.#jsEmbed.js_sys['{}'](data, this.#jsExports['{call_name_value}'])", signature.kind.factory_embed(), ), span, ); let inputs: Vec<_> = signature.inputs.iter().collect(); - let arguments: Vec<_> = inputs - .iter() - .enumerate() - .map(|(index, ty)| format_ident!("arg{index}", span = ty.span())) - .collect(); let output = signature.output.as_ref(); - let output_decl = output.map_or_else( - TokenStream::new, - |output| quote_spanned!(output.span()=> -> #output), + let ExportAbi { + raw_types, + raw_inputs, + join_inputs, + arguments, + codegen_inputs, + mut required_embeds, + raw_output, + output_argument, + } = lower_abi(inputs.iter().copied(), output, &js_sys)?; + let mut js_codegen_inputs = vec![quote_spanned!(span=> ("data", ::core::primitive::usize))]; + js_codegen_inputs.extend(codegen_inputs.iter().cloned()); + required_embeds.insert( + 0, + quote_spanned!(span=> #js_sys::r#macro::js_from_embed::<::core::primitive::usize>()), ); let closure_bound = if signature.kind == ClosureKind::Shared { if let Some(output) = output { @@ -84,7 +86,7 @@ pub(crate) fn closure_with( quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*)) } }; - let call_body = if signature.kind == ClosureKind::Shared { + let callback_call = if signature.kind == ClosureKind::Shared { quote_spanned! {span=> let callback = unsafe { &*#js_sys::ClosureHeader::callback::(pointer) @@ -99,6 +101,19 @@ pub(crate) fn closure_with( callback(#(#arguments),*) } }; + let call_body = if output.is_some() { + quote_spanned! {span=> + #(#join_inputs)* + #js_sys::r#macro::return_to_js({ + #callback_call + }) + } + } else { + quote_spanned! {span=> + #(#join_inputs)* + #callback_call; + } + }; let expression = if signature.kind == ClosureKind::Once { quote_spanned! {expression.span()=> { @@ -116,16 +131,16 @@ pub(crate) fn closure_with( Ok(quote_spanned! {span=> { - type CallShim = unsafe fn( + type CallShim = unsafe extern "C" fn( *mut #js_sys::ClosureHeader, - #(#inputs),* - ) #output_decl; + #(#raw_types),* + ) #raw_output; #[allow(clippy::undocumented_unsafe_blocks)] - unsafe fn call_impl( + unsafe extern "C" fn call_raw( pointer: *mut #js_sys::ClosureHeader, - #(#arguments: #inputs),* - ) #output_decl + #(#raw_inputs),* + ) #raw_output where F: #closure_bound, { @@ -140,21 +155,32 @@ pub(crate) fn closure_with( { #js_sys::ClosureAllocation::new( callback, - call_impl:: as CallShim, + call_raw:: as CallShim, ) } - #[#js_sys::js_sys(js_sys = #js_sys)] - #[allow(clippy::undocumented_unsafe_blocks)] - fn #call_ident( - data: ::core::primitive::usize, - #(#arguments: #inputs),* - ) #output_decl { - let pointer = ::core::ptr::with_exposed_provenance_mut(data); - let call_shim = unsafe { - #js_sys::ClosureHeader::call_shim::(pointer) - }; - unsafe { call_shim(pointer.cast(), #(#arguments),*) } + #js_sys::js_bindgen::unsafe_global_wat! { + "{}", + interpolate #js_sys::r#macro::wat_closure!( + #call_name, + CallShim, + (#(#codegen_inputs),*) + #output_argument, + ), + } + + #js_sys::js_bindgen::export_js! { + module = #crate_name, + name = #call_name, + required_embeds = [ + #(#required_embeds),* + ], + "{}", + interpolate #js_sys::r#macro::js_export!( + #call_name, + (#(#js_codegen_inputs),*) + #output_argument, + ), } #js_sys::js_bindgen::embed_js! { @@ -185,18 +211,14 @@ fn closure_symbol_hash( crate_name: &str, package_name: &str, package_version: &str, - input: &str, trait_object: &TypeTraitObject, - expression: &Expr, ) -> String { let mut descriptor = String::from("closure-v1\0"); for value in [ crate_name, package_name, package_version, - input, &trait_object.to_token_stream().to_string(), - &expression.to_token_stream().to_string(), ] { descriptor.push_str(value); descriptor.push('\0'); diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 5ab3d686..0f68fd71 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -51,68 +51,22 @@ pub(crate) fn r#macro( ReturnType::Type(_, ty) => Some(ty.as_ref()), ReturnType::Default => None, }; - let mut raw_inputs = Vec::new(); - let mut join_inputs = Vec::new(); - let mut arguments = Vec::new(); - let mut codegen_inputs = Vec::new(); - let mut required_embeds = Vec::new(); - - for (index, input) in function.sig.inputs.iter().enumerate() { + let inputs = function.sig.inputs.iter().map(|input| { let FnArg::Typed(input) = input else { unreachable!(); }; - let ty = &input.ty; - let argument = format_ident!("arg{index}", span = input.span()); - let parameter = LitStr::new(&argument.to_string(), input.span()); - let reference = match ty.as_ref() { - Type::Reference(reference) if reference.mutability.is_some() => { - return Err(Error::new_spanned( - reference, - "mutable references are not supported", - )); - } - Type::Reference(reference) => Some(reference), - _ => None, - }; - let js_ty = reference.map_or_else( - || quote_spanned!(input.span()=> #ty), - |reference| { - let ty = &reference.elem; - quote_spanned! {input.span()=> - <#ty as #js_sys::hazard::RefFromJS>::Anchor - } - }, - ); - let mut slots = Vec::new(); - - for slot in 1_usize..=4 { - let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = input.span()); - let slot_alias = format_ident!("FromJsSlot{slot}", span = input.span()); - - raw_inputs.push(quote_spanned! {input.span()=> - #slot_ident: #macro_path::#slot_alias<#js_ty> - }); - slots.push(slot_ident); - } - - if let Some(reference) = reference { - let anchor = format_ident!("arg{index}_anchor", span = input.span()); - let ty = &reference.elem; - - join_inputs.push(quote_spanned! {input.span()=> - let #anchor = #macro_path::join_from_js::<#js_ty>(#(#slots),*); - let #argument = ::core::borrow::Borrow::<#ty>::borrow(&#anchor); - }); - } else { - join_inputs.push(quote_spanned! {input.span()=> - let #argument = #macro_path::join_from_js::<#js_ty>(#(#slots),*); - }); - } - - codegen_inputs.push(quote_spanned! {input.span()=> (#parameter, #js_ty) }); - required_embeds.push(quote_spanned!(input.span()=> #macro_path::js_from_embed::<#js_ty>())); - arguments.push(argument); - } + input.ty.as_ref() + }); + let ExportAbi { + raw_inputs, + join_inputs, + arguments, + codegen_inputs, + required_embeds, + raw_output, + output_argument, + .. + } = lower_abi(inputs, output_ty, &js_sys)?; let call = if function.sig.unsafety.is_some() { quote_spanned!(span=> unsafe { #ident(#(#arguments),*) }) @@ -120,18 +74,6 @@ pub(crate) fn r#macro( quote_spanned!(span=> #ident(#(#arguments),*)) }; let raw_export_name = LitStr::new(&format!("__export_{export_name_value}"), ident.span()); - let (raw_output, output_argument) = if let Some(output_ty) = output_ty { - ( - quote_spanned! {output_ty.span()=> - -> #js_sys::hazard::WasmRet< - <#output_ty as #js_sys::hazard::ReturnIntoJS>::Abi - > - }, - quote_spanned!(output_ty.span()=> , #output_ty), - ) - } else { - (TokenStream::new(), TokenStream::new()) - }; let raw_body = if output_ty.is_some() { quote_spanned! {span=> #(#join_inputs)* @@ -143,10 +85,6 @@ pub(crate) fn r#macro( #call; } }; - if let Some(output_ty) = output_ty { - required_embeds - .push(quote_spanned!(output_ty.span()=> #macro_path::js_return_embed::<#output_ty>())); - } Ok(quote_spanned! {span=> #function @@ -186,6 +124,114 @@ pub(crate) fn r#macro( }) } +pub(crate) struct ExportAbi { + pub raw_types: Vec, + pub raw_inputs: Vec, + pub join_inputs: Vec, + pub arguments: Vec, + pub codegen_inputs: Vec, + pub required_embeds: Vec, + pub raw_output: TokenStream, + pub output_argument: TokenStream, +} + +pub(crate) fn lower_abi<'a>( + inputs: impl IntoIterator, + output: Option<&Type>, + js_sys: &Path, +) -> Result { + let mut raw_types = Vec::new(); + let mut raw_inputs = Vec::new(); + let mut join_inputs = Vec::new(); + let mut arguments = Vec::new(); + let mut codegen_inputs = Vec::new(); + let mut required_embeds = Vec::new(); + + for (index, ty) in inputs.into_iter().enumerate() { + let span = ty.span(); + let argument = format_ident!("arg{index}", span = span); + let parameter = LitStr::new(&argument.to_string(), span); + let reference = match ty { + Type::Reference(reference) if reference.mutability.is_some() => { + return Err(Error::new_spanned( + reference, + "mutable references are not supported", + )); + } + Type::Reference(reference) => Some(reference), + _ => None, + }; + let js_ty = reference.map_or_else( + || quote_spanned!(span=> #ty), + |reference| { + let ty = &reference.elem; + quote_spanned! {span=> + <#ty as #js_sys::hazard::RefFromJS>::Anchor + } + }, + ); + let mut slots = Vec::new(); + + for slot in 1_usize..=4 { + let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = span); + let slot_alias = format_ident!("FromJsSlot{slot}", span = span); + let raw_type = quote_spanned!(span=> #js_sys::r#macro::#slot_alias<#js_ty>); + + raw_types.push(raw_type.clone()); + raw_inputs.push(quote_spanned!(span=> #slot_ident: #raw_type)); + slots.push(slot_ident); + } + + if let Some(reference) = reference { + let anchor = format_ident!("arg{index}_anchor", span = span); + let ty = &reference.elem; + + join_inputs.push(quote_spanned! {span=> + let #anchor = #js_sys::r#macro::join_from_js::<#js_ty>(#(#slots),*); + let #argument = ::core::borrow::Borrow::<#ty>::borrow(&#anchor); + }); + } else { + join_inputs.push(quote_spanned! {span=> + let #argument = #js_sys::r#macro::join_from_js::<#js_ty>(#(#slots),*); + }); + } + + codegen_inputs.push(quote_spanned!(span=> (#parameter, #js_ty))); + required_embeds.push(quote_spanned!(span=> #js_sys::r#macro::js_from_embed::<#js_ty>())); + arguments.push(argument); + } + + let (raw_output, output_argument) = output.map_or_else( + || (TokenStream::new(), TokenStream::new()), + |output| { + ( + quote_spanned! {output.span()=> + -> #js_sys::hazard::WasmRet< + <#output as #js_sys::hazard::ReturnIntoJS>::Abi + > + }, + quote_spanned!(output.span()=> , #output), + ) + }, + ); + + if let Some(output) = output { + required_embeds + .push(quote_spanned!(output.span()=> #js_sys::r#macro::js_return_embed::<#output>())); + } + + Ok(ExportAbi { + raw_types, + raw_inputs, + join_inputs, + arguments, + codegen_inputs, + required_embeds, + raw_output, + output_argument, + }) +} + fn validate(function: &ItemFn) -> Result<(), Error> { let sig = &function.sig; diff --git a/host/ld-shared/src/lib.rs b/host/ld-shared/src/lib.rs index 1dc5cb73..3797a92e 100644 --- a/host/ld-shared/src/lib.rs +++ b/host/ld-shared/src/lib.rs @@ -268,7 +268,7 @@ enum SectionData<'cs> { impl<'cs> CustomSectionParser<'cs> { fn new(custom_section: &CustomSectionReader<'cs>, framed: bool) -> Self { let data = if framed { - // Linkers concatenate statics assigned to the same custom section. + // `Linkers` concatenate statics assigned to the same custom section. // Each block carries its allocated and initialized lengths, followed // by length-prefixed records and any trailing padding. SectionData::Framed { diff --git a/host/ld/src/js.rs b/host/ld/src/js.rs index 5f1d96c5..31f6fc40 100644 --- a/host/ld/src/js.rs +++ b/host/ld/src/js.rs @@ -11,30 +11,40 @@ type FixedHashMap = HashMap; pub struct JsStore { import: FixedHashMap>, expected_import: HashMap>, + // Keep canonical definitions after resolution so later records can be + // checked for equality. provided_import: HashMap>, embed: FixedHashMap>, expected_embed: HashMap>, provided_embed: HashMap>, - export: FixedHashMap, - export_module: FixedHashMap, + provided_export: FixedHashMap, } +#[derive(Clone, Debug, PartialEq, Eq)] struct JsWithEmbeds { js: String, embeds: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] struct JsEmbed { module: String, name: String, } +#[derive(Debug, PartialEq, Eq)] +struct JsExport { + module: String, + binding: JsWithEmbeds, +} + impl JsStore { pub fn add_import(&mut self, import: Import<'_>) -> Result<()> { if let Some(js) = self .provided_import - .get_mut(import.module) - .and_then(|names| names.remove(import.name)) + .get(import.module) + .and_then(|names| names.get(import.name)) + .cloned() { self.import .entry(import.module.to_owned()) @@ -62,6 +72,27 @@ impl JsStore { pub fn add_js_imports(&mut self, custom_section: &CustomSectionReader<'_>) -> Result<()> { for import in JsBindgenJsSectionParser::new(custom_section) { + let binding = JsWithEmbeds { + js: import.js.to_owned(), + embeds: import.embeds.into_iter().map(JsEmbed::from).collect(), + }; + let definitions = self.provided_import.entry_ref(import.module).or_default(); + + if let Some(previous) = definitions.get(import.name) { + if previous != &binding { + bail!( + "found multiple JS imports for `{}:{}`\n\tJS Import 1:\n{:?}\n\tJS Import \ + 2:\n{:?}", + import.module, + import.name, + previous, + binding + ); + } + } else { + definitions.insert(import.name.to_owned(), binding.clone()); + } + if self .expected_import .get_mut(import.module) @@ -70,30 +101,11 @@ impl JsStore { self.import .entry_ref(import.module) .or_default() - .insert(import.name.to_owned(), import.js.to_owned()); + .insert(import.name.to_owned(), binding.js.clone()); - for embed in import.embeds { - self.require_js_embed(embed.into()); + for embed in binding.embeds { + self.require_js_embed(embed); } - } else if let Err(error) = self - .provided_import - .entry_ref(import.module) - .or_default() - .try_insert( - import.name.to_owned(), - JsWithEmbeds { - js: import.js.to_owned(), - embeds: import.embeds.into_iter().map(JsEmbed::from).collect(), - }, - ) { - bail!( - "found multiple JS imports for `{}:{}`\n\tJS Import 1:\n{:?}\n\tJS Import \ - 2:\n{:?}", - import.module, - error.entry.key(), - error.entry.get().js, - import.js - ); } } @@ -102,6 +114,27 @@ impl JsStore { pub fn add_js_embeds(&mut self, custom_section: &CustomSectionReader<'_>) -> Result<()> { for embed in JsBindgenJsSectionParser::new(custom_section) { + let binding = JsWithEmbeds { + js: embed.js.to_owned(), + embeds: embed.embeds.into_iter().map(JsEmbed::from).collect(), + }; + let definitions = self.provided_embed.entry_ref(embed.module).or_default(); + + if let Some(previous) = definitions.get(embed.name) { + if previous != &binding { + bail!( + "found multiple JS embeds for `{}:{}`\n\tJS Embed 1:\n{:?}\n\tJS Embed \ + 2:\n{:?}", + embed.module, + embed.name, + previous, + binding + ); + } + } else { + definitions.insert(embed.name.to_owned(), binding.clone()); + } + if self .expected_embed .get_mut(embed.module) @@ -110,29 +143,11 @@ impl JsStore { self.embed .entry_ref(embed.module) .or_default() - .insert(embed.name.to_owned(), embed.js.to_owned()); + .insert(embed.name.to_owned(), binding.js.clone()); - for required_embed in embed.embeds { - self.require_js_embed(required_embed.into()); + for required_embed in binding.embeds { + self.require_js_embed(required_embed); } - } else if let Err(error) = self - .provided_embed - .entry_ref(embed.module) - .or_default() - .try_insert( - embed.name.to_owned(), - JsWithEmbeds { - js: embed.js.to_owned(), - embeds: embed.embeds.into_iter().map(JsEmbed::from).collect(), - }, - ) { - bail!( - "found multiple JS embeds for `{}:{}`\n\tJS Embed 1:\n{}\n\tJS Embed 2:\n{}", - embed.module, - error.entry.key(), - error.entry.get().js, - embed.js - ); } } @@ -146,28 +161,39 @@ impl JsStore { let mut names = Vec::new(); for export in JsBindgenJsSectionParser::new(custom_section) { - if let Some(previous) = self.export.get(export.name) { - let previous_module = &self.export_module[export.name]; + let binding = JsWithEmbeds { + js: export.js.to_owned(), + embeds: export.embeds.into_iter().map(JsEmbed::from).collect(), + }; + let definition = JsExport { + module: export.module.to_owned(), + binding, + }; + + if let Some(previous) = self.provided_export.get(export.name) { + if previous == &definition { + continue; + } + bail!( "found multiple JS exports named `{}` from `{}` and `{}`\n JS Export \ - 1:\n{}\n JS Export 2:\n{}", + 1:\n{:?}\n JS Export 2:\n{:?}", export.name, - previous_module, + previous.module, export.module, - previous, - export.js, + previous.binding, + definition.binding, ); } - self.export - .insert(export.name.to_owned(), export.js.to_owned()); - self.export_module - .insert(export.name.to_owned(), export.module.to_owned()); names.push(export.name.to_owned()); - for embed in export.embeds { - self.require_js_embed(embed.into()); + for embed in definition.binding.embeds.iter().cloned() { + self.require_js_embed(embed); } + + self.provided_export + .insert(export.name.to_owned(), definition); } Ok(names) @@ -181,8 +207,9 @@ impl JsStore { { if let Some(js) = self .provided_embed - .get_mut(&embed.module) - .and_then(|names| names.remove(&embed.name)) + .get(&embed.module) + .and_then(|names| names.get(&embed.name)) + .cloned() { self.embed .entry_ref(&embed.module) @@ -216,7 +243,11 @@ impl JsStore { main_memory, js_import: self.import, js_embed: self.embed, - js_export: self.export, + js_export: self + .provided_export + .into_iter() + .map(|(name, export)| (name, export.binding.js)) + .collect(), } } } diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index dbdfd5d5..a1a020f6 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::ffi::{OsStr, OsString}; use std::fs; use std::path::Path; @@ -58,6 +59,7 @@ pub fn processing<'a>(args: &'a Arguments<'a>) -> PreOutput<'a> { let main_memory = main_memory(arch, args, &mut add_args); let mut js_store = JsStore::default(); + let mut seen_wat = HashSet::new(); let mut is_test = false; // Extract embedded WAT from object files. @@ -69,6 +71,7 @@ pub fn processing<'a>(args: &'a Arguments<'a>) -> PreOutput<'a> { js_bindgen_ld_shared::ld_input_parser(input, |path, data, object_mtime| { process_object( &mut js_store, + &mut seen_wat, matches!(arch, Arch::Wasm64), &mut add_args, path, @@ -100,6 +103,7 @@ fn is_libtest(input: &OsStr) -> bool { /// them and passes them to the linker. fn process_object( js_store: &mut JsStore, + seen_wat: &mut HashSet, wasm64: bool, add_args: &mut Vec, archive_path: &Path, @@ -123,6 +127,9 @@ fn process_object( Payload::CustomSection(c) if c.name() == WAT_SECTION => { for wat in JsBindgenWatSectionParser::new(c) { file_counter += 1; + if !seen_wat.insert(wat.to_owned()) { + continue; + } let wasm_path = archive_path.with_added_extension(format!("wasm.{file_counter}.o")); // The cache is shared by concurrent linker processes. Hold the lock through @@ -163,6 +170,7 @@ fn process_object( process_object( js_store, + seen_wat, wasm64, &mut Vec::new(), &wasm_path, From d941dfad7a9c79d001f158625b6c966284813801 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:05:07 +0800 Subject: [PATCH 08/21] Add JsFuture --- benchmarks/Cargo.lock | 61 ++++- benchmarks/bench.mjs | 103 ++++++--- benchmarks/js-bindgen/src/lib.rs | 69 +++++- benchmarks/wasm-bindgen/Cargo.toml | 2 + benchmarks/wasm-bindgen/src/lib.rs | 65 ++++++ client/e2e/examples/future.rs | 100 +++++++++ client/js-sys/src/builtins/error.rs | 4 +- client/js-sys/src/builtins/function.rs | 54 +++++ client/js-sys/src/builtins/mod.rs | 4 + client/js-sys/src/builtins/promise.rs | 85 +++++++ client/js-sys/src/lib.rs | 8 +- client/js-sys/src/runtime/future/mod.rs | 189 ++++++++++++++++ client/js-sys/src/runtime/future/queue.rs | 97 ++++++++ .../js-sys/src/runtime/future/task/atomic.rs | 210 ++++++++++++++++++ client/js-sys/src/runtime/future/task/mod.rs | 38 ++++ .../js-sys/src/runtime/future/task/single.rs | 97 ++++++++ client/js-sys/src/runtime/mod.rs | 2 + 17 files changed, 1148 insertions(+), 40 deletions(-) create mode 100644 client/e2e/examples/future.rs create mode 100644 client/js-sys/src/builtins/function.rs create mode 100644 client/js-sys/src/builtins/promise.rs create mode 100644 client/js-sys/src/runtime/future/mod.rs create mode 100644 client/js-sys/src/runtime/future/queue.rs create mode 100644 client/js-sys/src/runtime/future/task/atomic.rs create mode 100644 client/js-sys/src/runtime/future/task/mod.rs create mode 100644 client/js-sys/src/runtime/future/task/single.rs diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock index 774ec0ac..ff57fbd3 100644 --- a/benchmarks/Cargo.lock +++ b/benchmarks/Cargo.lock @@ -26,6 +26,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -52,7 +76,7 @@ dependencies = [ name = "js-bindgen-benchmark" version = "0.0.0" dependencies = [ - "js-sys", + "js-sys 0.0.0", ] [[package]] @@ -67,6 +91,17 @@ dependencies = [ "js-sys-macro", ] +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "js-sys-bindgen" version = "0.1.0" @@ -94,6 +129,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -118,6 +159,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "syn" version = "2.0.119" @@ -152,6 +199,18 @@ dependencies = [ name = "wasm-bindgen-benchmark" version = "0.0.0" dependencies = [ + "js-sys 0.3.103", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys 0.3.103", "wasm-bindgen", ] diff --git a/benchmarks/bench.mjs b/benchmarks/bench.mjs index f2d3c72b..9effc00b 100644 --- a/benchmarks/bench.mjs +++ b/benchmarks/bench.mjs @@ -192,14 +192,15 @@ function compareBenchmarks(left, right) { // Wasm functions expose their arity but not their parameter types. Start with // Number and retry the parameter that rejected it as BigInt. Parameters that // never coerce the probe are reference values. -function inferArguments(exportName, call, allowThrow = false) { +async function inferArguments(exportName, call, allowFailure = false) { const kinds = Array(call.length).fill("number") while (true) { const coerced = Array(call.length).fill(false) let lastCoerced = -1 + let asynchronous = false let result - let throws = false + let fails = false const probes = kinds.map((kind, index) => ({ [Symbol.toPrimitive]() { coerced[index] = true @@ -220,13 +221,29 @@ function inferArguments(exportName, call, allowThrow = false) { continue } - if (!allowThrow) { + if (!allowFailure) { throw new Error(`cannot infer parameters for ${exportName}`, { cause: error, }) } - throws = true + fails = true + } + + if (!fails && typeof result?.then === "function") { + asynchronous = true + + try { + result = await result + } catch (error) { + if (!allowFailure) { + throw new Error(`cannot infer result for ${exportName}`, { + cause: error, + }) + } + + fails = true + } } let bigintIndex = 0 @@ -244,8 +261,9 @@ function inferArguments(exportName, call, allowThrow = false) { return 42 }), kinds: kinds.map((kind, index) => (coerced[index] ? kind : "reference")), + asynchronous, result, - throws, + fails, } } } @@ -260,19 +278,20 @@ async function discoverBenchmarks(implementation) { let benchmarkId = 0 -function createBenchmark(call, inputs, throws) { +function createBenchmark(call, inputs, fails, asynchronous) { const parameterCount = inputs.length const id = benchmarkId++ const parameters = Array.from({ length: parameterCount }, (_, index) => `arg${index}`) const invocation = `call(${parameters.join(", ")})` - const measuredCall = throws + const await_ = asynchronous ? "await " : "" + const measuredCall = fails ? ` try { - result = ${invocation}; + result = ${await_}${invocation}; } catch (error) { result = error; }` - : `result = ${invocation};` + : `result = ${await_}${invocation};` const setup = parameters .map( (_, index) => ` @@ -294,7 +313,7 @@ function createBenchmark(call, inputs, throws) { let result; yield {${setup} - bench(${parameters.join(", ")}) { + ${asynchronous ? "async " : ""}bench(${parameters.join(", ")}) { ${measuredCall} }, }; @@ -312,17 +331,6 @@ async function runWorker() { throw new Error(`unknown benchmark implementation: ${workerImplementation}`) } - const rawExports = await loadImplementation(implementation) - const rawCall = rawExports.raw[workerBenchmark] - - if (typeof rawCall !== "function") { - throw new Error(`missing Wasm export: ${implementation.name}:${workerBenchmark}`) - } - - const raw = inferArguments(workerBenchmark, rawCall) - - // Probe wrappers on a separate instance. Some raw ABIs transfer owned table - // indices, so probing them must not perturb the instance being measured. const wrappedExports = await loadImplementation(implementation) const wrappedCall = wrappedExports.wrapped[workerBenchmark] @@ -330,17 +338,32 @@ async function runWorker() { throw new Error(`missing JS export: ${implementation.name}:${workerBenchmark}`) } - const wrapped = inferArguments(workerBenchmark, wrappedCall, true) + const wrapped = await inferArguments(workerBenchmark, wrappedCall, true) const returnsReference = (typeof wrapped.result === "object" && wrapped.result !== null) || typeof wrapped.result === "function" - const useWrapper = - wrapped.throws || - wrapped.kinds.includes("reference") || - (Array.isArray(raw.result) && returnsReference) - const call = useWrapper ? wrappedCall : rawCall - const { inputs, kinds, throws } = useWrapper ? wrapped : raw - bench(implementation.name, createBenchmark(call, inputs, throws)) + let useWrapper = + wrapped.asynchronous || wrapped.fails || wrapped.kinds.includes("reference") + let raw + + if (!useWrapper) { + const rawExports = await loadImplementation(implementation) + const rawCall = rawExports.raw[workerBenchmark] + + if (typeof rawCall !== "function") { + throw new Error(`missing Wasm export: ${implementation.name}:${workerBenchmark}`) + } + + raw = await inferArguments(workerBenchmark, rawCall) + useWrapper = Array.isArray(raw.result) && returnsReference + } + + // Argument inference runs user code and can initialize queues or perturb + // owned table slots. Measure a fresh instance after all probing is complete. + const measuredExports = await loadImplementation(implementation) + const call = (useWrapper ? measuredExports.wrapped : measuredExports.raw)[workerBenchmark] + const { asynchronous, fails, inputs, kinds } = useWrapper ? wrapped : raw + bench(implementation.name, createBenchmark(call, inputs, fails, asynchronous)) const result = await run({ format: "quiet", throw: true }) const trial = result.benchmarks[0] @@ -361,9 +384,10 @@ async function runWorker() { version: result.context.version, }, implementation: implementation.name, + asynchronous, + fails, kinds, stats, - throws, }) ) } @@ -514,27 +538,38 @@ async function runCoordinator() { let printedContext = false const comparisons = [] for (const exportName of selectedBenchmarks) { + let expectedAsynchronous let expectedKinds - let expectedThrows + let expectedFailure const results = [] for (const implementation of implementations) { const result = runCase(implementation, exportName) + if ( + expectedAsynchronous !== undefined && + expectedAsynchronous !== result.asynchronous + ) { + throw new Error( + `async behavior mismatch for ${exportName}: ${expectedAsynchronous} != ${result.asynchronous}` + ) + } + if (expectedKinds && expectedKinds.join() !== result.kinds.join()) { throw new Error( `parameter ABI mismatch for ${exportName}: ${expectedKinds.join()} != ${result.kinds.join()}` ) } - if (expectedThrows !== undefined && expectedThrows !== result.throws) { + if (expectedFailure !== undefined && expectedFailure !== result.fails) { throw new Error( - `exception behavior mismatch for ${exportName}: ${expectedThrows} != ${result.throws}` + `failure behavior mismatch for ${exportName}: ${expectedFailure} != ${result.fails}` ) } + expectedAsynchronous = result.asynchronous expectedKinds = result.kinds - expectedThrows = result.throws + expectedFailure = result.fails results.push(result) if (!printedContext) { diff --git a/benchmarks/js-bindgen/src/lib.rs b/benchmarks/js-bindgen/src/lib.rs index 4571cec5..b9092654 100644 --- a/benchmarks/js-bindgen/src/lib.rs +++ b/benchmarks/js-bindgen/src/lib.rs @@ -1,7 +1,10 @@ use core::array; +use core::future::Future; use core::hint::black_box; +use core::pin::Pin; +use core::task::{Context, Poll}; -use js_sys::{Closure, JsValue, closure, js_sys}; +use js_sys::{Closure, JsFuture, JsValue, Promise, closure, future_to_promise, js_sys}; js_sys::js_bindgen::embed_js!( module = "js_bindgen_benchmark", @@ -27,6 +30,16 @@ js_sys::js_bindgen::embed_js!( "(callback, value) => callback(value)", ); +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_benchmark", + name = "pending_promise", + "() => {{", + " const {{ promise, resolve }} = Promise.withResolvers()", + " globalThis.queueMicrotask(resolve)", + " return promise", + "}}", +); + #[js_sys] extern "js-sys" { #[js_sys(js_embed = "identity")] @@ -109,6 +122,9 @@ extern "js-sys" { #[js_sys(js_embed = "invoke_closure")] fn invoke_closure_u128_raw(callback: &Closure u128>, value: u128) -> u128; + + #[js_sys(js_embed = "pending_promise")] + fn pending_promise() -> Promise; } std::thread_local! { @@ -128,6 +144,57 @@ fn bench_closure_call_u128(value: u128) -> u128 { CALLBACK_U128.with(|callback| invoke_closure_u128_raw(callback, value)) } +#[js_sys] +fn bench_future_to_promise_ready() -> Promise { + future_to_promise(async { Ok(JsValue::UNDEFINED) }) +} + +struct YieldOnce(bool); + +impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if self.0 { + Poll::Ready(()) + } else { + self.0 = true; + context.waker().wake_by_ref(); + Poll::Pending + } + } +} + +#[js_sys] +fn bench_future_to_promise_pending() -> Promise { + future_to_promise(async { + YieldOnce(false).await; + Ok(JsValue::UNDEFINED) + }) +} + +#[js_sys] +fn bench_future_to_promise_err() -> Promise { + future_to_promise(async { Err(JsValue::UNDEFINED) }) +} + +#[js_sys] +fn bench_promise_future_roundtrip_ready() -> Promise { + let promise = Promise::resolve(&JsValue::UNDEFINED); + future_to_promise(JsFuture::from(promise)) +} + +#[js_sys] +fn bench_promise_future_roundtrip_pending() -> Promise { + future_to_promise(JsFuture::from(pending_promise())) +} + +#[js_sys] +fn bench_promise_future_roundtrip_err() -> Promise { + let promise = Promise::reject(&JsValue::UNDEFINED); + future_to_promise(JsFuture::from(promise)) +} + #[js_sys] fn bench_export_bool() -> bool { true diff --git a/benchmarks/wasm-bindgen/Cargo.toml b/benchmarks/wasm-bindgen/Cargo.toml index bcc142e4..8f8dddf0 100644 --- a/benchmarks/wasm-bindgen/Cargo.toml +++ b/benchmarks/wasm-bindgen/Cargo.toml @@ -8,4 +8,6 @@ publish = { workspace = true } crate-type = ["cdylib"] [dependencies] +js-sys = "=0.3.103" wasm-bindgen = "=0.2.126" +wasm-bindgen-futures = "=0.4.76" diff --git a/benchmarks/wasm-bindgen/src/lib.rs b/benchmarks/wasm-bindgen/src/lib.rs index 2b469e2d..37acc598 100644 --- a/benchmarks/wasm-bindgen/src/lib.rs +++ b/benchmarks/wasm-bindgen/src/lib.rs @@ -1,7 +1,12 @@ use core::array; +use core::future::Future; use core::hint::black_box; +use core::pin::Pin; +use core::task::{Context, Poll}; +use js_sys::Promise; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::{JsFuture, future_to_promise}; #[wasm_bindgen(inline_js = "export function identity(value) { return value; }")] extern "C" { @@ -98,6 +103,15 @@ extern "C" { fn invoke_closure_u128_raw(callback: &Closure u128>, value: u128) -> u128; } +#[wasm_bindgen(inline_js = "export function pending_promise() { + const { promise, resolve } = Promise.withResolvers(); + globalThis.queueMicrotask(resolve); + return promise; + }")] +extern "C" { + fn pending_promise() -> Promise; +} + std::thread_local! { static CALLBACK: Closure i32> = Closure::new(|value| value); @@ -115,6 +129,57 @@ pub fn bench_closure_call_u128(value: u128) -> u128 { CALLBACK_U128.with(|callback| invoke_closure_u128_raw(callback, value)) } +#[wasm_bindgen] +pub fn bench_future_to_promise_ready() -> Promise { + future_to_promise(async { Ok(JsValue::UNDEFINED) }) +} + +struct YieldOnce(bool); + +impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if self.0 { + Poll::Ready(()) + } else { + self.0 = true; + context.waker().wake_by_ref(); + Poll::Pending + } + } +} + +#[wasm_bindgen] +pub fn bench_future_to_promise_pending() -> Promise { + future_to_promise(async { + YieldOnce(false).await; + Ok(JsValue::UNDEFINED) + }) +} + +#[wasm_bindgen] +pub fn bench_future_to_promise_err() -> Promise { + future_to_promise(async { Err(JsValue::UNDEFINED) }) +} + +#[wasm_bindgen] +pub fn bench_promise_future_roundtrip_ready() -> Promise { + let promise = Promise::resolve(&JsValue::UNDEFINED); + future_to_promise(JsFuture::from(promise)) +} + +#[wasm_bindgen] +pub fn bench_promise_future_roundtrip_pending() -> Promise { + future_to_promise(JsFuture::from(pending_promise())) +} + +#[wasm_bindgen] +pub fn bench_promise_future_roundtrip_err() -> Promise { + let promise = Promise::reject(&JsValue::UNDEFINED); + future_to_promise(JsFuture::from(promise)) +} + #[wasm_bindgen] pub fn bench_export_bool() -> bool { true diff --git a/client/e2e/examples/future.rs b/client/e2e/examples/future.rs new file mode 100644 index 00000000..17058d7c --- /dev/null +++ b/client/e2e/examples/future.rs @@ -0,0 +1,100 @@ +#[rustfmt::skip] +fn main() { + // ;; await (async () => { const value = {}; return await exports["future_to_promise_ok"](value) === value })() + // ;; await (async () => { const value = {}; try { await exports["future_to_promise_err"](value); return false } catch (error) { return error === value } })() + // ;; await (async () => { const value = {}; return await exports["promise_to_future"](Promise.resolve(value)) === value })() + // ;; await exports["typed_promise_to_future"](Promise.resolve("typed")) === "typed" + // ;; await (async () => { const value = {}; try { await exports["promise_to_future"](Promise.reject(value)); return false } catch (error) { return error === value } })() + // ;; await (async () => { const value = {}; return await exports["shared_promise"](Promise.resolve(value)) === value })() + // ;; await (async () => { exports["spawn_local_start"](); if (exports["spawn_local_done"]()) return false; await Promise.resolve(); return exports["spawn_local_done"]() })() + // ;; await (async () => { const value = {}; return await exports["self_wake"](value) === value })() + // ;; await (async () => { const { promise, resolve } = Promise.withResolvers(); exports["drop_js_future"](promise); resolve(); await Promise.resolve(); return true })() +} + +use core::future::Future; +use core::pin::Pin; +use core::sync::atomic::{AtomicBool, Ordering}; +use core::task::{Context, Poll}; + +use js_sys::{JsFuture, JsString, JsValue, Promise, future_to_promise, js_sys, spawn_local}; + +static SPAWN_LOCAL_DONE: AtomicBool = AtomicBool::new(false); + +struct YieldOnce(bool); + +impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if self.0 { + Poll::Ready(()) + } else { + self.0 = true; + context.waker().wake_by_ref(); + Poll::Pending + } + } +} + +#[js_sys] +fn future_to_promise_ok(value: JsValue) -> Promise { + future_to_promise(async move { Ok(value) }) +} + +#[js_sys] +fn future_to_promise_err(error: JsValue) -> Promise { + future_to_promise(async move { Err(error) }) +} + +#[js_sys] +fn promise_to_future(promise: Promise) -> Promise { + future_to_promise(async move { promise.await }) +} + +#[js_sys] +fn typed_promise_to_future(promise: Promise) -> Promise { + future_to_promise(async move { promise.await }) +} + +#[js_sys] +fn shared_promise(promise: Promise) -> Promise { + let first = JsFuture::from(promise.clone()); + let second = JsFuture::from(promise); + + future_to_promise(async move { + let first = first.await?; + let second = second.await?; + + if first == second { + Ok(first) + } else { + Err(JsValue::NULL) + } + }) +} + +#[js_sys] +fn spawn_local_start() { + SPAWN_LOCAL_DONE.store(false, Ordering::Relaxed); + spawn_local(async { + SPAWN_LOCAL_DONE.store(true, Ordering::Relaxed); + }); +} + +#[js_sys] +fn spawn_local_done() -> bool { + SPAWN_LOCAL_DONE.load(Ordering::Relaxed) +} + +#[js_sys] +fn self_wake(value: JsValue) -> Promise { + future_to_promise(async move { + YieldOnce(false).await; + Ok(value) + }) +} + +#[js_sys] +fn drop_js_future(promise: Promise) { + drop(JsFuture::from(promise)); +} diff --git a/client/js-sys/src/builtins/error.rs b/client/js-sys/src/builtins/error.rs index 4e893e9e..83a73085 100644 --- a/client/js-sys/src/builtins/error.rs +++ b/client/js-sys/src/builtins/error.rs @@ -6,7 +6,7 @@ use crate::{JsString, JsValue, js_sys}; extern "js-sys" { /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error#cause) #[js_sys(extends = Object)] - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive(Clone, Debug)] pub type ErrorOptions; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) @@ -32,7 +32,7 @@ impl ErrorOptions { #[js_sys(js_sys = crate)] extern "js-sys" { #[js_sys(extends = Object)] - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive(Clone, Debug)] pub type Error; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error) diff --git a/client/js-sys/src/builtins/function.rs b/client/js-sys/src/builtins/function.rs new file mode 100644 index 00000000..dc302de9 --- /dev/null +++ b/client/js-sys/src/builtins/function.rs @@ -0,0 +1,54 @@ +use super::object::Object; +use crate::{JsString, JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) + #[js_sys(extends = Object)] + #[derive(Clone, Debug)] + pub type Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) + #[js_sys(constructor)] + pub fn new_no_args(body: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) + #[js_sys(constructor)] + pub fn new_with_args(args: &str, body: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) + pub fn apply(self: &Function, this_arg: &JsValue, args: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) + #[must_use] + #[js_sys(variadic)] + pub fn bind(self: &Function, this_arg: &JsValue, args: &[JsValue]) -> Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) + #[js_sys(variadic)] + pub fn call(self: &Function, this_arg: &JsValue, args: &[JsValue]) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length) + #[must_use] + #[js_sys(getter)] + pub fn length(self: &Function) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name) + #[must_use] + #[js_sys(getter)] + pub fn name(self: &Function) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/prototype) + #[must_use] + #[js_sys(getter)] + pub fn prototype(self: &Function) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/prototype) + #[js_sys(setter)] + pub fn set_prototype(self: &Function, prototype: &JsValue); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Function) -> JsString; +} diff --git a/client/js-sys/src/builtins/mod.rs b/client/js-sys/src/builtins/mod.rs index 99b2e486..f839741a 100644 --- a/client/js-sys/src/builtins/mod.rs +++ b/client/js-sys/src/builtins/mod.rs @@ -1,13 +1,17 @@ mod array; mod bigint; mod error; +mod function; mod number; mod object; +mod promise; mod string; pub use array::{JsArray, TryFromJsArrayError}; pub use bigint::JsBigInt; pub use error::{Error, ErrorOptions}; +pub use function::Function; pub use number::JsNumber; pub use object::Object; +pub use promise::{Promise, PromiseWithResolvers}; pub use string::JsString; diff --git a/client/js-sys/src/builtins/promise.rs b/client/js-sys/src/builtins/promise.rs new file mode 100644 index 00000000..05ec47a1 --- /dev/null +++ b/client/js-sys/src/builtins/promise.rs @@ -0,0 +1,85 @@ +use super::function::Function; +use super::object::Object; +use crate::{Closure, JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) + #[must_use] + #[js_sys(extends = Object)] + #[derive(Clone, Debug)] + pub type Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) + #[must_use] + #[js_sys(extends = Object)] + #[derive(Clone, Debug)] + pub type PromiseWithResolvers; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) + #[js_sys(constructor)] + pub fn new(executor: &Closure) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) + #[js_sys(static_of = Promise)] + pub fn all(iterable: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) + #[js_sys(static_of = Promise, js_name = "allSettled")] + pub fn all_settled(iterable: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) + #[js_sys(static_of = Promise)] + pub fn any(iterable: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) + #[js_sys(static_of = Promise)] + pub fn race(iterable: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject) + #[js_sys(static_of = Promise)] + pub fn reject(reason: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) + #[js_sys(static_of = Promise)] + pub fn resolve(value: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try) + #[js_sys(static_of = Promise, js_name = "try", variadic)] + pub fn try_(callback: &Function, args: &[JsValue]) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) + #[js_sys(static_of = Promise, js_name = "withResolvers")] + pub fn with_resolvers() -> PromiseWithResolvers; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch) + pub fn catch(self: &Promise, handler: Closure JsValue>) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally) + pub fn finally(self: &Promise, callback: Closure) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) + pub fn then(self: &Promise, callback: Closure JsValue>) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) + #[js_sys(js_name = "then")] + pub fn then_with_reject( + self: &Promise, + resolve: Closure JsValue>, + reject: Closure JsValue>, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) + #[js_sys(getter)] + pub fn promise(self: &PromiseWithResolvers) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) + #[must_use] + #[js_sys(getter)] + pub fn resolve(self: &PromiseWithResolvers) -> Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) + #[must_use] + #[js_sys(getter)] + pub fn reject(self: &PromiseWithResolvers) -> Function; +} diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index c00b25c6..2e83a9a1 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -23,11 +23,15 @@ mod interop; pub mod r#macro; pub use builtins::{ - Error, ErrorOptions, JsArray, JsBigInt, JsNumber, JsString, Object, TryFromJsArrayError, + Error, ErrorOptions, Function, JsArray, JsBigInt, JsNumber, JsString, Object, Promise, + PromiseWithResolvers, TryFromJsArrayError, }; pub use js_bindgen; pub use js_sys_macro::{closure, js_sys}; -pub use runtime::{Closure, ClosureAllocation, ClosureHeader, JsValue, UnwrapThrowExt, panic}; +pub use runtime::{ + Closure, ClosureAllocation, ClosureHeader, JsFuture, JsValue, UnwrapThrowExt, + future_to_promise, panic, spawn_local, +}; #[cfg(not(target_feature = "reference-types"))] compile_error!("`js-sys` requires the `reference-types` target feature"); diff --git a/client/js-sys/src/runtime/future/mod.rs b/client/js-sys/src/runtime/future/mod.rs new file mode 100644 index 00000000..e2838d78 --- /dev/null +++ b/client/js-sys/src/runtime/future/mod.rs @@ -0,0 +1,189 @@ +//! Bridges JavaScript promises and Rust futures. + +mod queue; +mod task; + +use alloc::rc::{Rc, Weak}; +use core::cell::RefCell; +use core::future::{Future, IntoFuture}; +use core::pin::Pin; +use core::task::{Context, Poll, Waker}; +use core::{fmt, mem}; + +use crate::hazard::JsCast; +use crate::{Closure, JsValue, Promise, PromiseWithResolvers}; + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.observe", + "(promise, callback) => {{", + " promise.then(", + " value => {{", + " try {{ callback(true, value) }} finally {{ callback.unref() }}", + " }},", + " error => {{", + " try {{ callback(false, error) }} finally {{ callback.unref() }}", + " }},", + " )", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.settle", + "(resolvers, resolved, value) => {{", + " resolvers[resolved ? 'resolve' : 'reject'](value)", + "}}", +); + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "future.observe")] + fn observe(promise: &JsValue, callback: Closure); + + #[js_sys(js_embed = "future.settle")] + fn settle(resolvers: PromiseWithResolvers, resolved: bool, value: JsValue); +} + +enum State { + Pending { + waker: Option, + // Keep the `Promise` and its reaction callbacks alive while Rust waits. + _promise: JsValue, + }, + Ready(Result), + Done, +} + +impl State { + fn finish(state: &Weak>, result: Result) { + let Some(state) = state.upgrade() else { + return; + }; + + let waker = { + let mut state = state.borrow_mut(); + let Self::Pending { waker, .. } = &mut *state else { + return; + }; + let waker = waker.take(); + *state = Self::Ready(result); + waker + }; + + if let Some(waker) = waker { + waker.wake(); + } + } +} + +/// A Rust [`Future`] backed by a JavaScript [`Promise`]. +/// +/// Fulfillment produces `T`; rejection produces [`JsValue`]. +#[must_use = "futures do nothing unless polled or awaited"] +pub struct JsFuture { + state: Rc>>, +} + +impl fmt::Debug for JsFuture { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("JsFuture { .. }") + } +} + +impl From> for JsFuture { + fn from(promise: Promise) -> Self { + let promise = as AsRef>::as_ref(&promise); + let state = Rc::new(RefCell::new(State::Pending { + waker: None, + _promise: promise.clone(), + })); + let callback_state = Rc::downgrade(&state); + let callback = crate::closure!( + js_sys = crate, + dyn Fn(bool, JsValue), + move |resolved, value| { + let result = if resolved { + Ok(T::unchecked_from(value)) + } else { + Err(value) + }; + State::finish(&callback_state, result); + } + ); + + observe(promise, callback); + + Self { state } + } +} + +impl Future for JsFuture { + type Output = Result; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + let mut state = self.state.borrow_mut(); + + match &mut *state { + State::Pending { waker, .. } => { + if waker + .as_ref() + .is_none_or(|waker| !waker.will_wake(context.waker())) + { + *waker = Some(context.waker().clone()); + } + + Poll::Pending + } + State::Ready(_) => { + let State::Ready(result) = mem::replace(&mut *state, State::Done) else { + unreachable!(); + }; + Poll::Ready(result) + } + State::Done => panic!("`JsFuture` polled after completion"), + } + } +} + +impl IntoFuture for Promise { + type Output = Result; + type IntoFuture = JsFuture; + + fn into_future(self) -> Self::IntoFuture { + self.into() + } +} + +/// Runs a future on the current JavaScript thread. +/// +/// The first poll always runs on the next `microtask`. +#[inline] +pub fn spawn_local(future: impl Future + 'static) { + task::spawn(future); +} + +/// Converts a Rust future into a JavaScript [`Promise`]. +/// +/// `Ok` fulfills the promise and `Err` rejects it. +pub fn future_to_promise( + future: impl Future> + 'static, +) -> Promise +where + T: JsCast + Into + 'static, +{ + let resolvers = Promise::with_resolvers(); + // This function is the only producer of the `resolver` object's successful + // value. + let promise = Promise::::unchecked_from(resolvers.promise().into()); + + spawn_local(async move { + let (resolved, value) = match future.await { + Ok(value) => (true, value.into()), + Err(error) => (false, error), + }; + settle(resolvers, resolved, value); + }); + + promise +} diff --git a/client/js-sys/src/runtime/future/queue.rs b/client/js-sys/src/runtime/future/queue.rs new file mode 100644 index 00000000..a1decc39 --- /dev/null +++ b/client/js-sys/src/runtime/future/queue.rs @@ -0,0 +1,97 @@ +use alloc::collections::VecDeque; +use alloc::rc::Rc; +use core::cell::{Cell, RefCell}; + +use super::task::Task; + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.schedule", + "() => globalThis.queueMicrotask(() => this.#jsExports.future_poll())", +); + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[expect( + clippy::unnecessary_operation, + reason = "the generated wrapper calls a side-effect-only import" + )] + #[js_sys(js_embed = "future.schedule")] + fn schedule(); +} + +struct Queue { + tasks: RefCell>>, + scheduled: Cell, +} + +impl Queue { + const fn new() -> Self { + Self { + tasks: RefCell::new(VecDeque::new()), + scheduled: Cell::new(false), + } + } + + fn push(&self, task: Rc) -> bool { + self.tasks.borrow_mut().push_back(task); + !self.scheduled.replace(true) + } + + fn pop(&self) -> Option> { + self.tasks.borrow_mut().pop_front() + } + + fn begin_tick(&self) -> usize { + self.scheduled.set(false); + self.tasks.borrow().len() + } + + fn reschedule(&self) -> bool { + !self.tasks.borrow().is_empty() && !self.scheduled.replace(true) + } +} + +thread_local! { + static QUEUE: Queue = const { Queue::new() }; +} + +pub(super) fn push(task: Rc) { + if QUEUE.with(|queue| queue.push(task)) { + schedule(); + } +} + +struct RescheduleOnUnwind(bool); + +impl RescheduleOnUnwind { + fn new() -> Self { + Self(true) + } + + fn disarm(mut self) { + self.0 = false; + } +} + +impl Drop for RescheduleOnUnwind { + fn drop(&mut self) { + if self.0 && QUEUE.with(Queue::reschedule) { + schedule(); + } + } +} + +#[crate::js_sys(js_sys = crate)] +fn future_poll() { + let guard = RescheduleOnUnwind::new(); + + for _ in 0..QUEUE.with(Queue::begin_tick) { + let Some(task) = QUEUE.with(Queue::pop) else { + break; + }; + task.run(); + } + + guard.disarm(); +} diff --git a/client/js-sys/src/runtime/future/task/atomic.rs b/client/js-sys/src/runtime/future/task/atomic.rs new file mode 100644 index 00000000..22796531 --- /dev/null +++ b/client/js-sys/src/runtime/future/task/atomic.rs @@ -0,0 +1,210 @@ +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::sync::Arc; +use core::cell::RefCell; +use core::future::Future; +use core::mem::ManuallyDrop; +use core::pin::Pin; +use core::sync::atomic::{AtomicI32, Ordering}; +use core::task::{Context, RawWaker, RawWakerVTable, Waker}; + +use super::ClearOnUnwind; +use crate::Closure; +use crate::runtime::future::queue; +use crate::util::PtrConst; + +const SLEEPING: i32 = 0; +const AWAKE: i32 = 1; + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.atomic.state", + "({{ buffer: undefined, view: undefined, waits: new Map() }})", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.atomic.wait", + required_embeds = [("js_sys", "future.atomic.state")], + "(state, awake, resume) => {{", + " const atomic = this.#jsEmbed.js_sys['future.atomic.state']", + " if (awake) {{", + " globalThis.queueMicrotask(resume)", + " return", + " }}", + " const buffer = this.#memory.buffer", + " if (typeof SharedArrayBuffer === 'undefined'", + " || !(buffer instanceof SharedArrayBuffer)) {{", + " atomic.waits.set(state, resume)", + " return", + " }}", + " if (typeof Atomics.waitAsync !== 'function') {{", + " throw new Error('Wasm atomics futures require Atomics.waitAsync')", + " }}", + " if (buffer !== atomic.buffer) {{", + " atomic.buffer = buffer", + " atomic.view = new Int32Array(buffer)", + " }}", + " const result = Atomics.waitAsync(", + " atomic.view,", + " state / Int32Array.BYTES_PER_ELEMENT,", + " 0,", + " )", + " if (result.async) result.value.then(resume)", + " else globalThis.queueMicrotask(resume)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.atomic.notify", + required_embeds = [("js_sys", "future.atomic.state")], + "state => {{", + " const atomic = this.#jsEmbed.js_sys['future.atomic.state']", + " const buffer = this.#memory.buffer", + " if (typeof SharedArrayBuffer !== 'undefined'", + " && buffer instanceof SharedArrayBuffer) {{", + " if (buffer !== atomic.buffer) {{", + " atomic.buffer = buffer", + " atomic.view = new Int32Array(buffer)", + " }}", + " Atomics.notify(", + " atomic.view,", + " state / Int32Array.BYTES_PER_ELEMENT,", + " 1,", + " )", + " return", + " }}", + " const waits = atomic.waits", + " const resume = waits.get(state)", + " if (resume === undefined) return", + " waits.delete(state)", + " globalThis.queueMicrotask(resume)", + "}}", +); + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "future.atomic.wait")] + fn wait(state: PtrConst, awake: bool, resume: &Closure); + + #[js_sys(js_embed = "future.atomic.notify")] + fn notify(state: PtrConst); +} + +struct Wake { + state: AtomicI32, +} + +impl Wake { + fn new() -> Arc { + Arc::new(Self { + state: AtomicI32::new(AWAKE), + }) + } + + fn wake_by_ref(&self) { + if self.state.swap(AWAKE, Ordering::SeqCst) == AWAKE { + return; + } + + notify(PtrConst::new(core::slice::from_ref(&self.state))); + } + + unsafe fn raw_waker(this: Arc) -> RawWaker { + unsafe fn clone(pointer: *const ()) -> RawWaker { + // SAFETY: Every pointer in this table comes from `Arc::into_raw`. + let wake = ManuallyDrop::new(unsafe { Arc::from_raw(pointer.cast::()) }); + // SAFETY: The clone becomes the ownership represented by the new + // `RawWaker`. + unsafe { Wake::raw_waker(Arc::clone(&wake)) } + } + + unsafe fn wake(pointer: *const ()) { + // SAFETY: `wake` consumes the ownership represented by this `Waker`. + let wake = unsafe { Arc::from_raw(pointer.cast::()) }; + wake.wake_by_ref(); + } + + unsafe fn wake_by_ref(pointer: *const ()) { + // SAFETY: `wake_by_ref` borrows the ownership represented by this + // `Waker`. + let wake = ManuallyDrop::new(unsafe { Arc::from_raw(pointer.cast::()) }); + wake.wake_by_ref(); + } + + unsafe fn drop(pointer: *const ()) { + // SAFETY: `drop` consumes the ownership represented by this `Waker`. + core::mem::drop(unsafe { Arc::from_raw(pointer.cast::()) }); + } + + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + + RawWaker::new(Arc::into_raw(this).cast(), &VTABLE) + } +} + +struct TaskState { + future: Pin>>, + waker: Waker, + resume: Closure, +} + +pub(in crate::runtime::future) struct Task { + state: RefCell>, + wake: Arc, +} + +impl Task { + pub(super) fn spawn(future: impl Future + 'static) { + let wake = Wake::new(); + // SAFETY: The raw `Waker` owns the cloned, thread-safe `Arc`. + let waker = unsafe { Waker::from_raw(Wake::raw_waker(Arc::clone(&wake))) }; + let task = Rc::new(Self { + state: RefCell::new(None), + wake, + }); + let resumed_task = Rc::clone(&task); + let resume = crate::closure!(js_sys = crate, dyn FnMut(), move || { + // A delayed notification from the preceding wait may arrive after a + // new wait starts. Normalize the state before polling in either case. + resumed_task.wake.wake_by_ref(); + resumed_task.run(); + }); + *task.state.borrow_mut() = Some(TaskState { + future: Box::pin(future), + waker, + resume, + }); + queue::push(task); + } + + fn wait(&self, resume: &Closure) { + wait( + PtrConst::new(core::slice::from_ref(&self.wake.state)), + self.wake.state.load(Ordering::SeqCst) == AWAKE, + resume, + ); + } + + pub(in crate::runtime::future) fn run(self: &Rc) { + let guard = ClearOnUnwind::new(&self.state); + let mut slot = self.state.borrow_mut(); + let Some(task_state) = slot.as_mut() else { + guard.disarm(); + return; + }; + + let previous = self.wake.state.swap(SLEEPING, Ordering::SeqCst); + debug_assert_eq!(previous, AWAKE); + let mut context = Context::from_waker(&task_state.waker); + + if task_state.future.as_mut().poll(&mut context).is_ready() { + *slot = None; + } else { + self.wait(&task_state.resume); + } + + guard.disarm(); + } +} diff --git a/client/js-sys/src/runtime/future/task/mod.rs b/client/js-sys/src/runtime/future/task/mod.rs new file mode 100644 index 00000000..f2b9cde2 --- /dev/null +++ b/client/js-sys/src/runtime/future/task/mod.rs @@ -0,0 +1,38 @@ +use core::cell::RefCell; + +#[cfg(target_feature = "atomics")] +mod atomic; +#[cfg(not(target_feature = "atomics"))] +mod single; + +#[cfg(target_feature = "atomics")] +pub(super) use atomic::Task; +#[cfg(not(target_feature = "atomics"))] +pub(super) use single::Task; + +pub(super) fn spawn(future: impl core::future::Future + 'static) { + Task::spawn(future); +} + +struct ClearOnUnwind<'a, T> { + value: &'a RefCell>, + armed: bool, +} + +impl<'a, T> ClearOnUnwind<'a, T> { + fn new(value: &'a RefCell>) -> Self { + Self { value, armed: true } + } + + fn disarm(mut self) { + self.armed = false; + } +} + +impl Drop for ClearOnUnwind<'_, T> { + fn drop(&mut self) { + if self.armed { + *self.value.borrow_mut() = None; + } + } +} diff --git a/client/js-sys/src/runtime/future/task/single.rs b/client/js-sys/src/runtime/future/task/single.rs new file mode 100644 index 00000000..60b3c334 --- /dev/null +++ b/client/js-sys/src/runtime/future/task/single.rs @@ -0,0 +1,97 @@ +use alloc::boxed::Box; +use alloc::rc::Rc; +use core::cell::{Cell, RefCell}; +use core::future::Future; +use core::mem::ManuallyDrop; +use core::pin::Pin; +use core::task::{Context, RawWaker, RawWakerVTable, Waker}; + +use super::ClearOnUnwind; +use crate::runtime::future::queue; + +struct TaskState { + future: Pin>>, + waker: Waker, +} + +pub(in crate::runtime::future) struct Task { + state: RefCell>, + queued: Cell, +} + +impl Task { + pub(super) fn spawn(future: impl Future + 'static) { + let task = Rc::new(Self { + state: RefCell::new(None), + queued: Cell::new(true), + }); + // SAFETY: This target has no Wasm `atomics`, so its `Waker` cannot cross + // threads. The raw `Waker` owns this cloned `Rc`. + let waker = unsafe { Waker::from_raw(Self::raw_waker(Rc::clone(&task))) }; + *task.state.borrow_mut() = Some(TaskState { + future: Box::pin(future), + waker, + }); + queue::push(task); + } + + fn wake(task: Rc) { + if !task.queued.replace(true) { + queue::push(task); + } + } + + fn wake_by_ref(task: &Rc) { + if !task.queued.replace(true) { + queue::push(Rc::clone(task)); + } + } + + unsafe fn raw_waker(task: Rc) -> RawWaker { + unsafe fn clone(pointer: *const ()) -> RawWaker { + // SAFETY: Every pointer in this table comes from `Rc::into_raw`. + let task = ManuallyDrop::new(unsafe { Rc::from_raw(pointer.cast::()) }); + // SAFETY: The clone becomes the ownership represented by the new + // `RawWaker`. + unsafe { Task::raw_waker(Rc::clone(&task)) } + } + + unsafe fn wake(pointer: *const ()) { + // SAFETY: `wake` consumes the ownership represented by this `Waker`. + Task::wake(unsafe { Rc::from_raw(pointer.cast::()) }); + } + + unsafe fn wake_by_ref(pointer: *const ()) { + // SAFETY: `wake_by_ref` borrows the ownership represented by this + // `Waker`. + let task = ManuallyDrop::new(unsafe { Rc::from_raw(pointer.cast::()) }); + Task::wake_by_ref(&task); + } + + unsafe fn drop(pointer: *const ()) { + // SAFETY: `drop` consumes the ownership represented by this `Waker`. + core::mem::drop(unsafe { Rc::from_raw(pointer.cast::()) }); + } + + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + + RawWaker::new(Rc::into_raw(task).cast(), &VTABLE) + } + + pub(in crate::runtime::future) fn run(&self) { + let guard = ClearOnUnwind::new(&self.state); + let mut slot = self.state.borrow_mut(); + let Some(task_state) = slot.as_mut() else { + guard.disarm(); + return; + }; + + self.queued.set(false); + let mut context = Context::from_waker(&task_state.waker); + if task_state.future.as_mut().poll(&mut context).is_ready() { + *slot = None; + } + + guard.disarm(); + } +} diff --git a/client/js-sys/src/runtime/mod.rs b/client/js-sys/src/runtime/mod.rs index 3f9867fd..a44b9f04 100644 --- a/client/js-sys/src/runtime/mod.rs +++ b/client/js-sys/src/runtime/mod.rs @@ -1,11 +1,13 @@ pub(crate) mod closure; pub(crate) mod exception; pub(crate) mod externref; +mod future; mod panic; mod value; pub use self::closure::Closure; #[doc(hidden)] pub use self::closure::{ClosureAllocation, ClosureHeader}; +pub use self::future::{JsFuture, future_to_promise, spawn_local}; pub use self::panic::{UnwrapThrowExt, panic}; pub use self::value::JsValue; From 36b2823df70e94f2651fc670f58d2f7a295d9dc5 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:05:07 +0800 Subject: [PATCH 09/21] Add async test support --- client/js-sys/tests/future.rs | 36 ++++++++++ client/test/src/unknown.rs | 71 ++++++++++++++++++- host/js-sys-bindgen/src/export.rs | 21 ++++-- host/js-sys-bindgen/src/tests/macro/export.rs | 34 ++++++++- host/runner/src/js/shared/shared.mjs | 2 +- host/runner/src/js/shared/shared.mts | 4 +- host/test-macro/src/lib.rs | 38 +++++++--- 7 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 client/js-sys/tests/future.rs diff --git a/client/js-sys/tests/future.rs b/client/js-sys/tests/future.rs new file mode 100644 index 00000000..010c5427 --- /dev/null +++ b/client/js-sys/tests/future.rs @@ -0,0 +1,36 @@ +use js_bindgen_test::test; +use js_sys::{JsValue, Promise}; + +#[test] +async fn promise() { + let value = Promise::resolve(&JsValue::NULL).await.unwrap(); + + assert_eq!(value, JsValue::NULL); +} + +#[test] +#[should_panic(expected = "async panic")] +async fn should_panic() { + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + panic!("async panic"); +} + +mod first { + use js_bindgen_test::test; + use js_sys::{JsValue, Promise}; + + #[test] + async fn same_name() { + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + } +} + +mod second { + use js_bindgen_test::test; + use js_sys::{JsValue, Promise}; + + #[test] + async fn same_name() { + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + } +} diff --git a/client/test/src/unknown.rs b/client/test/src/unknown.rs index 57e40cd0..722428fa 100644 --- a/client/test/src/unknown.rs +++ b/client/test/src/unknown.rs @@ -1,8 +1,26 @@ +#[doc(hidden)] +pub extern crate js_sys; + +use core::future::Future; +use core::pin::Pin; +use core::task::{Context, Poll}; use std::panic::{self, PanicHookInfo}; use std::sync::Once; pub use js_bindgen_test_macro::test; -use js_sys::{JsString, js_sys}; +use js_sys::{Closure, JsString, JsValue, Promise, js_sys}; + +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_test", + name = "call", + "callback => {{", + " try {{", + " callback()", + " }} catch (error) {{", + " return error ?? new Error('nullish exception')", + " }}", + "}}", +); #[js_sys] extern "js-sys" { @@ -11,6 +29,9 @@ extern "js-sys" { #[js_sys(js_import)] fn set_payload(payload: &JsString); + + #[js_sys(js_embed = "call")] + fn call(callback: &Closure) -> Option; } #[doc(hidden)] @@ -39,3 +60,51 @@ pub fn set_panic_hook() { })); }); } + +struct AsyncTest { + future: F, +} + +struct PollState<'future, 'context, F> { + future: Pin<&'future mut F>, + context: &'future mut Context<'context>, + output: Option>, +} + +impl + 'static> Future for AsyncTest { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + // SAFETY: `future` remains pinned with its containing `AsyncTest`. + let future = unsafe { Pin::new_unchecked(&mut self.as_mut().get_unchecked_mut().future) }; + let mut state = PollState { + future, + context, + output: None, + }; + let state = (&raw mut state).cast::<()>(); + let callback = js_sys::closure!(dyn FnMut(), move || { + // SAFETY: `call` calls this closure synchronously and does not retain + // it, so `state` still points to the live stack allocation above. + let state = unsafe { &mut *state.cast::>() }; + state.output = Some(state.future.as_mut().poll(state.context)); + }); + let error = call(&callback); + // SAFETY: `call` has returned without retaining `callback`, and the + // stack allocation remains live until this function returns. + let output = unsafe { &mut *state.cast::>() }.output; + + match (error, output) { + (None, Some(Poll::Ready(()))) => Poll::Ready(Ok(JsValue::UNDEFINED)), + (None, Some(Poll::Pending)) => Poll::Pending, + (Some(error), None) => Poll::Ready(Err(error)), + _ => unreachable!("invalid async test poll state"), + } + } +} + +#[doc(hidden)] +pub fn async_test(future: impl Future + 'static) -> Promise { + set_panic_hook(); + js_sys::future_to_promise(AsyncTest { future }) +} diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 0f68fd71..994ec4e7 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -5,7 +5,7 @@ use quote::{format_ident, quote_spanned}; use syn::ext::IdentExt; use syn::parse::Parser; use syn::spanned::Spanned; -use syn::{Error, FnArg, ItemFn, LitStr, Path, ReturnType, Type, meta, parse_quote}; +use syn::{Error, Expr, FnArg, ItemFn, LitStr, Path, ReturnType, Type, meta, parse_quote}; pub(crate) fn r#macro( attr: TokenStream, @@ -13,6 +13,7 @@ pub(crate) fn r#macro( crate_: Option<&str>, ) -> Result { let mut js_sys: Option = None; + let mut js_name: Option = None; meta::parser(|meta| { if meta.path.is_ident("js_sys") { @@ -24,6 +25,13 @@ pub(crate) fn r#macro( js_sys = Some(meta.value()?.parse()?); Ok(()) } + } else if meta.path.is_ident("js_name") { + if js_name.is_some() { + Err(meta.error("duplicate `js_name` argument")) + } else { + js_name = Some(meta.value()?.parse()?); + Ok(()) + } } else { Err(meta.error("unsupported attribute")) } @@ -37,8 +45,13 @@ pub(crate) fn r#macro( let js_bindgen_path: Path = parse_quote!(#js_sys::js_bindgen); let macro_path: Path = parse_quote!(#js_sys::r#macro); let ident = &function.sig.ident; - let export_name_value = ident.unraw().to_string(); - let export_name = LitStr::new(&export_name_value, ident.span()); + let export_name = js_name.map_or_else( + || { + let name = LitStr::new(&ident.unraw().to_string(), ident.span()); + quote_spanned!(ident.span()=> #name) + }, + |name| quote_spanned!(name.span()=> #name), + ); let crate_name = crate_.map_or_else( || env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found"), str::to_owned, @@ -73,7 +86,7 @@ pub(crate) fn r#macro( } else { quote_spanned!(span=> #ident(#(#arguments),*)) }; - let raw_export_name = LitStr::new(&format!("__export_{export_name_value}"), ident.span()); + let raw_export_name = quote_spanned!(ident.span()=> ::core::concat!("__export_", #export_name)); let raw_body = if output_ty.is_some() { quote_spanned! {span=> #(#join_inputs)* diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs index d781a374..23fae0c2 100644 --- a/host/js-sys-bindgen/src/tests/macro/export.rs +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -3,8 +3,12 @@ use quote::quote; use syn::File; fn expand(function: &TokenStream) -> (String, String) { + expand_with_attr(&TokenStream::new(), function) +} + +fn expand_with_attr(attr: &TokenStream, function: &TokenStream) -> (String, String) { let function = syn::parse2(quote! { #function }).unwrap(); - let output = crate::export::r#macro(TokenStream::new(), &function, Some("test_crate")).unwrap(); + let output = crate::export::r#macro(attr.clone(), &function, Some("test_crate")).unwrap(); let output = prettyplease::unparse(&syn::parse2::(output).unwrap()); let dir = tempfile::tempdir().unwrap(); let (wat, js_import, js_export) = super::inner(dir.path(), &output).unwrap(); @@ -13,6 +17,34 @@ fn expand(function: &TokenStream) -> (String, String) { (wat.unwrap(), js_export.unwrap()) } +#[test] +fn js_name_expression() { + let (wat, js) = expand_with_attr( + "e!(js_name = concat!("module", "::answer")), + "e! { + fn answer() -> u32 { + 42 + } + }, + ); + + inline_snap::inline_snap!( + wat, + r#" +(import "env" "raw" (func $raw (@sym (name "__export_module::answer")) (result i32))) +(func $export (@sym (name "module::answer")) (result i32) + call $raw (@reloc) +)"# + ); + assert_eq!( + js, + r"() => { + const ret = wasmExports['module::answer']() + return ret >>> 0 +}" + ); +} + #[test] fn borrowed_return_is_rejected() { let function = syn::parse2(quote! { diff --git a/host/runner/src/js/shared/shared.mjs b/host/runner/src/js/shared/shared.mjs index 1a88d2d1..338653ac 100644 --- a/host/runner/src/js/shared/shared.mjs +++ b/host/runner/src/js/shared/shared.mjs @@ -176,7 +176,7 @@ export async function run(module, jsBindgenCtor, report) { } interceptFlag = true; try { - testFn(); + await testFn(); result = { success: true }; } catch (error) { diff --git a/host/runner/src/js/shared/shared.mts b/host/runner/src/js/shared/shared.mts index 01b364e0..4b831378 100644 --- a/host/runner/src/js/shared/shared.mts +++ b/host/runner/src/js/shared/shared.mts @@ -230,7 +230,7 @@ export async function run( continue } - const testFn = state.instance.exports[test.importName] as () => void + const testFn = state.instance.exports[test.importName] as () => void | Promise let result: { success: true } | { success: false; stack: string; message: string } if (test.shouldPanic) { @@ -244,7 +244,7 @@ export async function run( interceptFlag = true try { - testFn() + await testFn() result = { success: true } } catch (error) { result = { diff --git a/host/test-macro/src/lib.rs b/host/test-macro/src/lib.rs index a17aa18b..69c3fcaf 100644 --- a/host/test-macro/src/lib.rs +++ b/host/test-macro/src/lib.rs @@ -115,9 +115,7 @@ fn test_internal(attr: TokenStream, item: TokenStream) -> Result { return Err(Error::new_spanned(constness, "`const` test not supported")); } - if let Some(asyncness) = function.sig.asyncness { - return Err(Error::new_spanned(asyncness, "`async` test not supported")); - } + let is_async = function.sig.asyncness.is_some(); if !function.sig.inputs.is_empty() { return Err(Error::new_spanned( @@ -142,6 +140,31 @@ fn test_internal(attr: TokenStream, item: TokenStream) -> Result { let foreign_test = quote! { ::core::concat!(::core::module_path!(), "::", ::core::stringify!(#ident)) }; + let export = if is_async { + quote! { + #[cfg(test)] + const _: () = { + #[#crate_::js_sys::js_sys( + js_sys = #crate_::js_sys, + js_name = #foreign_test, + )] + fn __jbg_test() -> #crate_::js_sys::Promise { + #crate_::async_test(#ident()) + } + }; + } + } else { + quote! { + #[cfg(test)] + const _: () = { + #[unsafe(export_name = #foreign_test)] + extern "C" fn __jbg_test() { + #crate_::set_panic_hook(); + #ident(); + } + }; + } + }; Ok(quote! { #function @@ -169,14 +192,7 @@ fn test_internal(attr: TokenStream, item: TokenStream) -> Result { static CUSTOM_SECTION: Layout = Layout(LEN_ARR, DATA, TEST_ARR); }; - #[cfg(test)] - const _: () = { - #[unsafe(export_name = #foreign_test)] - extern "C" fn __jbg_test() { - #crate_::set_panic_hook(); - #ident(); - } - }; + #export }) } From d5919412f8e85a9d197f3ae57e0f56533f8583fd Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:05:07 +0800 Subject: [PATCH 10/21] Add JSPI --- client/e2e/examples/jspi.rs | 88 +++++++++++ client/js-sys/src/hazard.rs | 1 - client/js-sys/src/lib.rs | 2 +- client/js-sys/src/macro.rs | 2 +- client/js-sys/src/macro/abi.rs | 10 ++ client/js-sys/src/macro/export.rs | 141 ++++++++++++++---- client/js-sys/src/macro/import.rs | 60 +++++++- client/js-sys/src/macro/import/js.rs | 49 +++++- .../js-sys/src/runtime/future/jspi/atomic.rs | 104 +++++++++++++ client/js-sys/src/runtime/future/jspi/mod.rs | 41 +++++ .../js-sys/src/runtime/future/jspi/single.rs | 114 ++++++++++++++ client/js-sys/src/runtime/future/mod.rs | 3 + .../js-sys/src/runtime/future/task/atomic.rs | 4 +- client/js-sys/src/runtime/mod.rs | 10 +- client/js-sys/src/util.rs | 6 + host/dev/src/client/e2e.rs | 2 +- host/js-sys-bindgen/src/export.rs | 17 ++- host/js-sys-bindgen/src/function.rs | 16 +- host/js-sys-bindgen/src/function/options.rs | 14 +- host/js-sys-bindgen/src/tests/macro/export.rs | 120 +++++++++++++++ .../src/tests/macro/function.rs | 89 +++++++++++ host/runner/src/js/shared/shared.mjs | 13 +- host/runner/src/js/shared/shared.mts | 22 ++- web/playground/src/main.rs | 22 +++ 24 files changed, 887 insertions(+), 63 deletions(-) create mode 100644 client/e2e/examples/jspi.rs create mode 100644 client/js-sys/src/runtime/future/jspi/atomic.rs create mode 100644 client/js-sys/src/runtime/future/jspi/mod.rs create mode 100644 client/js-sys/src/runtime/future/jspi/single.rs diff --git a/client/e2e/examples/jspi.rs b/client/e2e/examples/jspi.rs new file mode 100644 index 00000000..f4d875ed --- /dev/null +++ b/client/e2e/examples/jspi.rs @@ -0,0 +1,88 @@ +#[rustfmt::skip] +fn main() { + // ;; await exports["jspi_block_on"]() === "resolved" + // ;; await exports["jspi_u32"](0xffff_ffff) === 0xffff_ffff + // ;; await exports["jspi_u128"](1n << 96n) === 1n << 96n + // ;; typeof exports["jspi_result"] !== "function" || await exports["jspi_result"](42) === 42 + // ;; typeof exports["jspi_result"] !== "function" || await (async () => { try { await exports["jspi_result"](-1); return false } catch (error) { return error === -1 } })() + // ;; typeof exports["jspi_result"] !== "function" || await (async () => { const [first, second] = await Promise.allSettled([exports["jspi_result"](-1), exports["jspi_result"](-2)]); return first.reason === -1 && second.reason === -2 })() + // ;; await (async () => { const value = { answer: 42 }; return await exports["jspi_js_value"](value) === value })() +} + +use core::future::Future; +use core::pin::Pin; +use core::task::{Context, Poll}; + +use js_sys::{JsString, JsValue, Promise, block_on, js_sys}; + +js_sys::js_bindgen::embed_js!( + module = "jspi", + name = "jspi.resolve", + "value => Promise.resolve(value)", +); + +js_sys::js_bindgen::embed_js!( + module = "jspi", + name = "jspi.result", + "value => value >= 0 ? Promise.resolve(value) : Promise.reject(value)", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "jspi.resolve", suspending)] + fn suspend_u32(value: u32) -> u32; + + #[js_sys(js_embed = "jspi.resolve", suspending)] + fn suspend_u128(value: u128) -> u128; + + #[cfg(target_feature = "exception-handling")] + #[js_sys(js_embed = "jspi.result", suspending)] + fn suspend_result(value: i32) -> Result; +} + +struct YieldOnce(bool); + +impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if self.0 { + Poll::Ready(()) + } else { + self.0 = true; + context.waker().wake_by_ref(); + Poll::Pending + } + } +} + +#[js_sys(promising)] +fn jspi_block_on() -> JsString { + block_on(async { + YieldOnce(false).await; + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + JsString::from("resolved") + }) +} + +#[js_sys(promising)] +fn jspi_u32(value: u32) -> u32 { + suspend_u32(value) +} + +#[js_sys(promising)] +fn jspi_u128(value: u128) -> u128 { + suspend_u128(value) +} + +#[cfg(target_feature = "exception-handling")] +#[js_sys(promising)] +fn jspi_result(value: i32) -> Result { + suspend_result(value) +} + +#[js_sys(promising)] +fn jspi_js_value(value: JsValue) -> JsValue { + value +} diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 69fa5d01..6515caab 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -5,7 +5,6 @@ use crate::JsValue; use crate::runtime::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; // Conversion `metadata`. - #[derive(Clone, Copy)] pub struct WatConv { pub imports: Option<&'static str>, diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index 2e83a9a1..6f4ae1b9 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -29,7 +29,7 @@ pub use builtins::{ pub use js_bindgen; pub use js_sys_macro::{closure, js_sys}; pub use runtime::{ - Closure, ClosureAllocation, ClosureHeader, JsFuture, JsValue, UnwrapThrowExt, + Closure, ClosureAllocation, ClosureHeader, JsFuture, JsValue, UnwrapThrowExt, block_on, future_to_promise, panic, spawn_local, }; diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index e2ac4258..834d7afd 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -21,7 +21,7 @@ pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; // JavaScript export shims. pub use crate::{ js_export, js_export_arguments, js_export_input_arguments, js_export_output_expression, - js_export_parameters, + js_export_parameters, js_export_promising, js_export_promising_then, js_export_result_throw, }; // JavaScript import shims. pub use crate::{ diff --git a/client/js-sys/src/macro/abi.rs b/client/js-sys/src/macro/abi.rs index 07aba83a..dd5969b3 100644 --- a/client/js-sys/src/macro/abi.rs +++ b/client/js-sys/src/macro/abi.rs @@ -370,6 +370,11 @@ pub const fn js_return_embed() -> (&'static str, &'static str) }) } +#[must_use] +pub const fn js_return_has_conversion() -> bool { + T::JS_CONV.conversion().is_some() +} + #[must_use] pub const fn js_output_embed() -> (&'static str, &'static str) { js_embed(match T::JS_CONV.conversion() { @@ -386,6 +391,11 @@ pub const fn js_from_embed() -> (&'static str, &'static str) { }) } +#[must_use] +pub const fn js_from_has_conversion() -> bool { + T::JS_CONV.is_some() +} + #[must_use] pub const fn js_result_embed() -> (&'static str, &'static str) { if T::JS_CONV.is_result() { diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs index f7ef54eb..9fa8ab57 100644 --- a/client/js-sys/src/macro/export.rs +++ b/client/js-sys/src/macro/export.rs @@ -271,6 +271,42 @@ macro_rules! js_export_output_expression { }}; } +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_result_throw { + ($indent:literal, $ty:ty $(,)?) => {{ + const SLOTS: [$crate::r#macro::WatSlot; 4] = + $crate::r#macro::return_into_js_wat_slots::<$ty>(); + const ERROR_DISCRIMINANT: &::core::primitive::str = if SLOTS[0].abi.is_empty() { + "ret[0]" + } else if SLOTS[1].abi.is_empty() { + "ret[1]" + } else { + "ret[2]" + }; + const ERROR: &::core::primitive::str = if SLOTS[0].abi.is_empty() { + "ret[1]" + } else if SLOTS[1].abi.is_empty() { + "ret[2]" + } else { + "ret[3]" + }; + + if $crate::r#macro::return_into_js_is_result::<$ty>() { + $crate::r#macro::const_concat!( + $indent, + "if (", + ERROR_DISCRIMINANT, + " !== 0) throw ", + ERROR, + "\n", + ) + } else { + "" + } + }}; +} + /// Generates the complete JavaScript wrapper for one Rust export. #[doc(hidden)] #[macro_export] @@ -299,37 +335,8 @@ macro_rules! js_export { $crate::r#macro::js_export_arguments!($(($par, $input)),*); const OUTPUT: &::core::primitive::str = $crate::r#macro::js_export_output_expression!($output); - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::return_into_js_wat_slots::<$output>(); - const ERROR_DISCRIMINANT: &::core::primitive::str = - if SLOTS[0].abi.is_empty() { - "ret[0]" - } else if SLOTS[1].abi.is_empty() { - "ret[1]" - } else { - "ret[2]" - }; - const ERROR: &::core::primitive::str = - if SLOTS[0].abi.is_empty() { - "ret[1]" - } else if SLOTS[1].abi.is_empty() { - "ret[2]" - } else { - "ret[3]" - }; - const THROW: &::core::primitive::str = if $crate::r#macro::return_into_js_is_result::< - $output, - >() { - $crate::r#macro::const_concat!( - " if (", - ERROR_DISCRIMINANT, - " !== 0) throw ", - ERROR, - "\n", - ) - } else { - "" - }; + const THROW: &::core::primitive::str = + $crate::r#macro::js_export_result_throw!(" ", $output); $($crate::r#macro::validate_from_js::<$input>();)* $crate::r#macro::validate_return_into_js::<$output>(); @@ -348,3 +355,75 @@ macro_rules! js_export { ) }}; } + +/// Generates handling for the value produced by a `promising` export. +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_promising_then { + () => { + "" + }; + ($output:ty) => {{ + const OUTPUT: &::core::primitive::str = + $crate::r#macro::js_export_output_expression!($output); + const POSTPROCESS: ::core::primitive::bool = + $crate::r#macro::js_return_has_conversion::<$output>() + || $crate::r#macro::return_into_js_is_result::<$output>(); + const THROW: &::core::primitive::str = + $crate::r#macro::js_export_result_throw!(" ", $output); + + if POSTPROCESS { + $crate::r#macro::const_concat!( + ".then(ret => {\n", + THROW, + " return ", + OUTPUT, + "\n })", + ) + } else { + "" + } + }}; +} + +/// Generates a JavaScript wrapper for a Wasm export marked as `promising`. +#[doc(hidden)] +#[macro_export] +macro_rules! js_export_promising { + ( + $export:expr, + ($(($par:literal, $input:ty)),*) + $(, $output:ty)? + $(,)? + ) => {{ + const PARAMETERS: &::core::primitive::str = + $crate::r#macro::js_export_parameters!($(($par, $input)),*); + const ARGUMENTS: &::core::primitive::str = + $crate::r#macro::js_export_arguments!($(($par, $input)),*); + const THEN: &::core::primitive::str = + $crate::r#macro::js_export_promising_then!($($output)?); + const PASSTHROUGH: ::core::primitive::bool = THEN.is_empty() + $(&& !$crate::r#macro::js_from_has_conversion::<$input>())*; + const RAW: &::core::primitive::str = $crate::r#macro::const_concat!( + "WebAssembly.promising(wasmExports['", + $export, + "'])", + ); + const WRAPPED: &::core::primitive::str = $crate::r#macro::const_concat!( + "(() => {\n const $promising = ", + RAW, + "\n return (", + PARAMETERS, + ") => $promising(", + ARGUMENTS, + ")", + THEN, + "\n})()", + ); + + $($crate::r#macro::validate_from_js::<$input>();)* + $($crate::r#macro::validate_return_into_js::<$output>();)? + + if PASSTHROUGH { RAW } else { WRAPPED } + }}; +} diff --git a/client/js-sys/src/macro/import.rs b/client/js-sys/src/macro/import.rs index 3d67a69f..1457bbda 100644 --- a/client/js-sys/src/macro/import.rs +++ b/client/js-sys/src/macro/import.rs @@ -14,7 +14,7 @@ use super::{ }; use crate::hazard::{IntoJS, ReturnFromJS}; -/// All target-dependent metadata needed to render one imported argument. +/// All target-dependent information needed to render one imported argument. #[doc(hidden)] #[derive(Clone, Copy)] pub struct ImportInput { @@ -30,7 +30,7 @@ struct ImportInputType { wat_capacity: WatInputCapacity, } -/// All target-dependent metadata needed to render one imported result. +/// All target-dependent information needed to render one imported result. #[doc(hidden)] #[derive(Clone, Copy)] pub struct ImportOutput { @@ -74,6 +74,7 @@ pub struct ImportDescriptor { inputs: &'static [ImportInput], output: Option<&'static ImportOutput>, js: Option, + suspending: bool, wat_capacity: usize, js_capacity: usize, } @@ -166,6 +167,41 @@ impl ImportDescriptor { inputs: &'static [ImportInput], output: Option<&'static ImportOutput>, js: Option, + ) -> Self { + Self::build(module, import, shim, inputs, output, js, false) + } + + #[doc(hidden)] + #[must_use] + pub const fn new_suspending( + module: &'static str, + import: &'static str, + shim: &'static str, + inputs: &'static [ImportInput], + output: Option<&'static ImportOutput>, + js: Option, + ) -> Self { + assert!( + js.is_some(), + "suspending imports require a generated JavaScript binding", + ); + if let Some(output) = output { + assert!( + !catches_result_in_js_from_output(output), + "suspending Result imports require the Wasm exception-handling target feature", + ); + } + Self::build(module, import, shim, inputs, output, js, true) + } + + const fn build( + module: &'static str, + import: &'static str, + shim: &'static str, + inputs: &'static [ImportInput], + output: Option<&'static ImportOutput>, + js: Option, + suspending: bool, ) -> Self { let mut descriptor = Self { module, @@ -174,6 +210,7 @@ impl ImportDescriptor { inputs, output, js, + suspending, wat_capacity: 0, js_capacity: 0, }; @@ -199,12 +236,23 @@ impl ImportDescriptor { None => false, } } + + const fn awaits_suspending_output(&self) -> bool { + if !self.suspending { + return false; + } + + match self.output { + Some(output) => output.has_js_conversion || catches_result_in_js_from_output(output), + None => false, + } + } } /// Returns a safe upper bound for [`import_wat`]. /// -/// This follows the renderer without scanning declaration contents, so it is -/// suitable for sizing a padded, single-pass section. +/// This follows the rendering logic without scanning declaration contents, so +/// it is suitable for sizing a padded, single-pass section. #[doc(hidden)] #[must_use] pub const fn import_wat_capacity(imports: &[ImportDescriptor]) -> usize { @@ -237,8 +285,8 @@ pub const fn import_wat( /// Returns a safe upper bound for [`import_js`]. /// -/// This follows the renderer without scanning template contents, so it is -/// suitable for sizing a padded, single-pass section. +/// This follows the rendering logic without scanning template contents, so it +/// is suitable for sizing a padded, single-pass section. #[doc(hidden)] #[must_use] pub const fn import_js_capacity(imports: &[ImportDescriptor]) -> usize { diff --git a/client/js-sys/src/macro/import/js.rs b/client/js-sys/src/macro/import/js.rs index 9b751951..2d23660d 100644 --- a/client/js-sys/src/macro/import/js.rs +++ b/client/js-sys/src/macro/import/js.rs @@ -23,7 +23,18 @@ pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize embed += 1; } + if descriptor.suspending { + capacity.add_str("new WebAssembly.Suspending("); + } + let wrapped = descriptor.needs_js_shim(); + let await_output = descriptor.awaits_suspending_output(); + + if await_output { + capacity.add_str("async "); + capacity.add_str("await ("); + capacity.add(1); + } if wrapped { capacity.add(1); @@ -53,9 +64,14 @@ pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize } match descriptor.output { - Some(output) => add_output_capacity(&mut capacity, output, js, wrapped), + Some(output) => { + add_output_capacity(&mut capacity, output, js, wrapped); + } None => { if wrapped { + if descriptor.suspending { + capacity.add_str(" return "); + } capacity.add_str(js.indirect_call); capacity.add_str("\n}"); } else { @@ -64,6 +80,10 @@ pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize } } + if descriptor.suspending { + capacity.add(1); + } + capacity.get() } @@ -237,8 +257,16 @@ impl ImportDescriptor { const fn write_js(&self, writer: &mut Writer, js: ImportJs) { let wrapped = self.needs_js_shim(); + let await_output = self.awaits_suspending_output(); + + if self.suspending { + writer.write_str("new WebAssembly.Suspending("); + } if wrapped { + if await_output { + writer.write_str("async "); + } writer.write_byte(b'('); if let Some(output) = self.output @@ -266,9 +294,12 @@ impl ImportDescriptor { } match self.output { - Some(output) => write_output(writer, output, js, wrapped), + Some(output) => write_output(writer, output, js, wrapped, await_output), None => { if wrapped { + if self.suspending { + writer.write_str(" return "); + } writer.write_str(js.indirect_call); writer.write_str("\n}"); } else { @@ -276,6 +307,10 @@ impl ImportDescriptor { } } } + + if self.suspending { + writer.write_byte(b')'); + } } } @@ -326,6 +361,7 @@ const fn write_output( output: &ImportOutput, js: ImportJs, wrapped: bool, + await_output: bool, ) { let convert_direct = output.direct && output.has_js_conversion; let catches_result = !output.js_try.is_empty(); @@ -358,10 +394,19 @@ const fn write_output( } if output.direct && !convert_direct { + if await_output { + writer.write_str("await ("); + } write_template(writer, output.js_templates[0], template_value, None); } else { + if await_output { + writer.write_str("await ("); + } writer.write_str(call); } + if await_output { + writer.write_byte(b')'); + } if convert_direct { writer.write_byte(b'\n'); diff --git a/client/js-sys/src/runtime/future/jspi/atomic.rs b/client/js-sys/src/runtime/future/jspi/atomic.rs new file mode 100644 index 00000000..d9529a29 --- /dev/null +++ b/client/js-sys/src/runtime/future/jspi/atomic.rs @@ -0,0 +1,104 @@ +use alloc::sync::Arc; +use alloc::task::Wake; +use core::sync::atomic::{AtomicI32, Ordering}; +use core::task::Waker; + +use super::{AWAKE, POLLING, WAITING}; +use crate::util::PtrConst; + +js_bindgen::embed_js!(module = "js_sys", name = "future.jspi.waits", "new Map()"); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.jspi.suspend", + required_embeds = [("js_sys", "future.jspi.waits")], + "state => {{", + " const buffer = this.#memory.buffer", + " const signal = new Int32Array(buffer, state, 1)", + " if (typeof SharedArrayBuffer !== 'undefined'", + " && buffer instanceof SharedArrayBuffer) {{", + " if (typeof Atomics.waitAsync !== 'function') {{", + " throw new Error('shared-memory JSPI requires Atomics.waitAsync')", + " }}", + " const result = Atomics.waitAsync(signal, 0, 1)", + " return result.async ? result.value : undefined", + " }}", + " if (signal[0] !== 1) return", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " return new Promise(resolve => waits.set(state, resolve))", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.jspi.notify", + required_embeds = [("js_sys", "future.jspi.waits")], + "state => {{", + " const buffer = this.#memory.buffer", + " if (typeof SharedArrayBuffer !== 'undefined'", + " && buffer instanceof SharedArrayBuffer) {{", + " Atomics.notify(new Int32Array(buffer, state, 1), 0, 1)", + " return", + " }}", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " const resolve = waits.get(state)", + " if (resolve === undefined) return", + " waits.delete(state)", + " resolve()", + "}}", +); + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "future.jspi.suspend", suspending)] + fn jspi_suspend(state: PtrConst); + + #[js_sys(js_embed = "future.jspi.notify")] + fn jspi_notify(state: PtrConst); +} + +pub(super) struct Signal { + state: AtomicI32, +} + +impl Signal { + fn notify(&self) { + if self.state.swap(AWAKE, Ordering::SeqCst) == WAITING { + jspi_notify(PtrConst::from_ref(&self.state)); + } + } + + pub(super) fn new() -> Arc { + Arc::new(Self { + state: AtomicI32::new(AWAKE), + }) + } + + pub(super) fn waker(self: &Arc) -> Waker { + Waker::from(Arc::clone(self)) + } + + pub(super) fn begin_poll(&self) { + self.state.store(POLLING, Ordering::SeqCst); + } + + pub(super) fn begin_wait(&self) -> bool { + self.state + .compare_exchange(POLLING, WAITING, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + } + + pub(super) fn suspend(&self) { + jspi_suspend(PtrConst::from_ref(&self.state)); + } +} + +impl Wake for Signal { + fn wake(self: Arc) { + self.notify(); + } + + fn wake_by_ref(self: &Arc) { + self.notify(); + } +} diff --git a/client/js-sys/src/runtime/future/jspi/mod.rs b/client/js-sys/src/runtime/future/jspi/mod.rs new file mode 100644 index 00000000..97fe25c5 --- /dev/null +++ b/client/js-sys/src/runtime/future/jspi/mod.rs @@ -0,0 +1,41 @@ +use core::future::{Future, IntoFuture}; +use core::task::{Context, Poll}; + +#[cfg(target_feature = "atomics")] +mod atomic; +#[cfg(not(target_feature = "atomics"))] +mod single; + +#[cfg(target_feature = "atomics")] +use atomic::Signal; +#[cfg(not(target_feature = "atomics"))] +use single::Signal; + +const POLLING: i32 = 0; +const WAITING: i32 = 1; +const AWAKE: i32 = 2; + +/// Runs a future to completion by suspending the current Wasm stack with +/// `JSPI`. +/// +/// The dynamic call into Wasm must enter through an export using +/// `#[js_sys(promising)]`. The `js-bindgen` runner marks binary entry points +/// automatically. +pub fn block_on(future: F) -> F::Output { + let mut future = core::pin::pin!(future.into_future()); + let signal = Signal::new(); + let waker = signal.waker(); + let mut context = Context::from_waker(&waker); + + loop { + signal.begin_poll(); + + if let Poll::Ready(output) = future.as_mut().poll(&mut context) { + return output; + } + + if signal.begin_wait() { + signal.suspend(); + } + } +} diff --git a/client/js-sys/src/runtime/future/jspi/single.rs b/client/js-sys/src/runtime/future/jspi/single.rs new file mode 100644 index 00000000..3eb111d7 --- /dev/null +++ b/client/js-sys/src/runtime/future/jspi/single.rs @@ -0,0 +1,114 @@ +use alloc::rc::Rc; +use core::cell::Cell; +use core::mem::ManuallyDrop; +use core::task::{RawWaker, RawWakerVTable, Waker}; + +use super::{AWAKE, POLLING, WAITING}; +use crate::util::PtrConst; + +js_bindgen::embed_js!(module = "js_sys", name = "future.jspi.waits", "new Map()"); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.jspi.suspend", + required_embeds = [("js_sys", "future.jspi.waits")], + "state => {{", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " return new Promise(resolve => waits.set(state, resolve))", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "future.jspi.notify", + required_embeds = [("js_sys", "future.jspi.waits")], + "state => {{", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " const resolve = waits.get(state)", + " if (resolve === undefined) return", + " waits.delete(state)", + " resolve()", + "}}", +); + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "future.jspi.suspend", suspending)] + fn jspi_suspend(state: PtrConst>); + + #[js_sys(js_embed = "future.jspi.notify")] + fn jspi_notify(state: PtrConst>); +} + +pub(super) struct Signal { + state: Cell, +} + +impl Signal { + fn notify(&self) { + if self.state.replace(AWAKE) == WAITING { + jspi_notify(PtrConst::from_ref(&self.state)); + } + } + + unsafe fn raw_waker(this: Rc) -> RawWaker { + unsafe fn clone(pointer: *const ()) -> RawWaker { + // SAFETY: Every pointer in this table comes from `Rc::into_raw`. + let signal = ManuallyDrop::new(unsafe { Rc::from_raw(pointer.cast::()) }); + // SAFETY: The clone becomes the ownership represented by the new + // `RawWaker`. + unsafe { Signal::raw_waker(Rc::clone(&signal)) } + } + + unsafe fn wake(pointer: *const ()) { + // SAFETY: `wake` consumes the ownership represented by this `Waker`. + let signal = unsafe { Rc::from_raw(pointer.cast::()) }; + signal.notify(); + } + + unsafe fn wake_by_ref(pointer: *const ()) { + // SAFETY: `wake_by_ref` borrows the ownership represented by this + // `Waker`. + let signal = ManuallyDrop::new(unsafe { Rc::from_raw(pointer.cast::()) }); + signal.notify(); + } + + unsafe fn drop(pointer: *const ()) { + // SAFETY: `drop` consumes the ownership represented by this `Waker`. + core::mem::drop(unsafe { Rc::from_raw(pointer.cast::()) }); + } + + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + + RawWaker::new(Rc::into_raw(this).cast(), &VTABLE) + } + + pub(super) fn new() -> Rc { + Rc::new(Self { + state: Cell::new(AWAKE), + }) + } + + pub(super) fn waker(self: &Rc) -> Waker { + // SAFETY: The raw `Waker` owns this cloned `Rc`. This implementation is + // only compiled for targets without Wasm `atomics`, so it cannot cross + // threads. + unsafe { Waker::from_raw(Self::raw_waker(Rc::clone(self))) } + } + + pub(super) fn begin_poll(&self) { + self.state.set(POLLING); + } + + pub(super) fn begin_wait(&self) -> bool { + if self.state.get() != POLLING { + return false; + } + self.state.set(WAITING); + true + } + + pub(super) fn suspend(&self) { + jspi_suspend(PtrConst::from_ref(&self.state)); + } +} diff --git a/client/js-sys/src/runtime/future/mod.rs b/client/js-sys/src/runtime/future/mod.rs index e2838d78..64ae2b9f 100644 --- a/client/js-sys/src/runtime/future/mod.rs +++ b/client/js-sys/src/runtime/future/mod.rs @@ -1,5 +1,6 @@ //! Bridges JavaScript promises and Rust futures. +mod jspi; mod queue; mod task; @@ -10,6 +11,8 @@ use core::pin::Pin; use core::task::{Context, Poll, Waker}; use core::{fmt, mem}; +pub use jspi::block_on; + use crate::hazard::JsCast; use crate::{Closure, JsValue, Promise, PromiseWithResolvers}; diff --git a/client/js-sys/src/runtime/future/task/atomic.rs b/client/js-sys/src/runtime/future/task/atomic.rs index 22796531..97c98281 100644 --- a/client/js-sys/src/runtime/future/task/atomic.rs +++ b/client/js-sys/src/runtime/future/task/atomic.rs @@ -108,7 +108,7 @@ impl Wake { return; } - notify(PtrConst::new(core::slice::from_ref(&self.state))); + notify(PtrConst::from_ref(&self.state)); } unsafe fn raw_waker(this: Arc) -> RawWaker { @@ -181,7 +181,7 @@ impl Task { fn wait(&self, resume: &Closure) { wait( - PtrConst::new(core::slice::from_ref(&self.wake.state)), + PtrConst::from_ref(&self.wake.state), self.wake.state.load(Ordering::SeqCst) == AWAKE, resume, ); diff --git a/client/js-sys/src/runtime/mod.rs b/client/js-sys/src/runtime/mod.rs index a44b9f04..65589a2e 100644 --- a/client/js-sys/src/runtime/mod.rs +++ b/client/js-sys/src/runtime/mod.rs @@ -5,9 +5,9 @@ mod future; mod panic; mod value; -pub use self::closure::Closure; +pub use closure::Closure; #[doc(hidden)] -pub use self::closure::{ClosureAllocation, ClosureHeader}; -pub use self::future::{JsFuture, future_to_promise, spawn_local}; -pub use self::panic::{UnwrapThrowExt, panic}; -pub use self::value::JsValue; +pub use closure::{ClosureAllocation, ClosureHeader}; +pub use future::{JsFuture, block_on, future_to_promise, spawn_local}; +pub use panic::{UnwrapThrowExt, panic}; +pub use value::JsValue; diff --git a/client/js-sys/src/util.rs b/client/js-sys/src/util.rs index 1fedffd2..d4fdbb1e 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -107,6 +107,12 @@ impl PtrConst { ptr: value.as_ptr(), } } + + pub(crate) fn from_ref(value: &T) -> Self { + Self { + ptr: core::ptr::from_ref(value), + } + } } // SAFETY: `PtrConst` is transparent over a native Wasm pointer. On `wasm64`, diff --git a/host/dev/src/client/e2e.rs b/host/dev/src/client/e2e.rs index 1bc6ee6f..6cbff86a 100644 --- a/host/dev/src/client/e2e.rs +++ b/host/dev/src/client/e2e.rs @@ -204,7 +204,7 @@ impl Example { }; let mut script = format!( "import {{ JsBindgen }} from './{}.mjs'\n\n{read}\nconst module = await \ - WebAssembly.compile(bytes)\nconst {{ exports }} = await new \ + WebAssembly.compile(bytes)\nconst {{ instance, exports }} = await new \ JsBindgen(module).instantiate()\n\nfunction assert(value, expression) {{\n if \ (!value) throw new Error(`assertion failed: ${{expression}}`)\n}}\n", self.name diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 994ec4e7..4ea310cf 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -14,6 +14,7 @@ pub(crate) fn r#macro( ) -> Result { let mut js_sys: Option = None; let mut js_name: Option = None; + let mut promising = false; meta::parser(|meta| { if meta.path.is_ident("js_sys") { @@ -32,6 +33,15 @@ pub(crate) fn r#macro( js_name = Some(meta.value()?.parse()?); Ok(()) } + } else if meta.path.is_ident("promising") { + if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) { + Err(meta.error("`promising` supports no values")) + } else if promising { + Err(meta.error("duplicate `promising` argument")) + } else { + promising = true; + Ok(()) + } } else { Err(meta.error("unsupported attribute")) } @@ -98,6 +108,11 @@ pub(crate) fn r#macro( #call; } }; + let js_export = if promising { + quote_spanned!(span=> #macro_path::js_export_promising!) + } else { + quote_spanned!(span=> #macro_path::js_export!) + }; Ok(quote_spanned! {span=> #function @@ -127,7 +142,7 @@ pub(crate) fn r#macro( #(#required_embeds),* ], "{}", - interpolate #macro_path::js_export!( + interpolate #js_export( #export_name, (#(#codegen_inputs),*) #output_argument, diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index 2dc9facd..6369b7a0 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -18,8 +18,8 @@ use crate::hygiene::Hygiene; mod js; mod options; -use self::js::ForeignItem; -use self::options::FunctionOptions; +use js::ForeignItem; +use options::FunctionOptions; pub(crate) struct FunctionImport { pub(crate) cfg_attrs: Vec, @@ -33,6 +33,7 @@ struct FunctionPlan { output_ty: Option, impl_generic_params: TokenStream, binding: ForeignItem, + suspending: bool, } struct InputArg { @@ -211,6 +212,7 @@ impl FunctionPlan { js_names: &HashMap, span: Span, ) -> Result { + let suspending = options.suspending; let external_implementation = options.import || options.embed.is_some(); let (inputs, self_ty) = Self::parse_inputs(hygiene, sig, cfg_attrs, span, external_implementation)?; @@ -228,6 +230,7 @@ impl FunctionPlan { output_ty, impl_generic_params, binding, + suspending, }) } @@ -342,6 +345,7 @@ impl FunctionPlan { setter, embed, import, + suspending: _, } = options; if import { @@ -555,6 +559,7 @@ impl FunctionPlan { inputs, output_ty, binding, + suspending, .. } = self; let input_descriptors = inputs.iter().map(|input| { @@ -634,8 +639,13 @@ impl FunctionPlan { quote_spanned!(span=> ::core::option::Option::None) }; let needs_js_section = js.is_some(); + let descriptor_constructor = if *suspending { + quote_spanned!(span=> #macro_path::ImportDescriptor::new_suspending) + } else { + quote_spanned!(span=> #macro_path::ImportDescriptor::new) + }; let descriptor = quote_spanned! {span=> - #macro_path::ImportDescriptor::new( + #descriptor_constructor( #crate_, #import_name, #link_name, diff --git a/host/js-sys-bindgen/src/function/options.rs b/host/js-sys-bindgen/src/function/options.rs index 0a757f7b..35ebf54e 100644 --- a/host/js-sys-bindgen/src/function/options.rs +++ b/host/js-sys-bindgen/src/function/options.rs @@ -20,6 +20,9 @@ pub(super) struct FunctionOptions { pub(super) embed: Option, /// Leaves the JavaScript implementation to the import object. pub(super) import: bool, + /// Allows a Promise returned by the JavaScript implementation to suspend + /// the current Wasm stack. + pub(super) suspending: bool, } impl FunctionOptions { @@ -82,6 +85,8 @@ impl FunctionOptions { } } else if meta.path.is_ident("js_import") { parse_flag(&meta, "js_import", &mut options.import) + } else if meta.path.is_ident("suspending") { + parse_flag(&meta, "suspending", &mut options.suspending) } else { Err(meta.error("unsupported attribute")) } @@ -109,6 +114,13 @@ impl FunctionOptions { "`js_import` and `js_embed` cannot be combined with JavaScript binding options", )); } + if self.import && self.suspending { + return Err(Error::new_spanned( + rust_name, + "`suspending` cannot be combined with `js_import`; provide a \ + `WebAssembly.Suspending` import directly", + )); + } if operation_count > 1 { return Err(Error::new_spanned( rust_name, @@ -140,7 +152,7 @@ impl FunctionOptions { } fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, name: &str, value: &mut bool) -> Result<()> { - if !meta.input.is_empty() { + if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) { return Err(meta.error(format!("`{name}` supports no values"))); } if *value { diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs index 23fae0c2..11eab0ae 100644 --- a/host/js-sys-bindgen/src/tests/macro/export.rs +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -45,6 +45,126 @@ fn js_name_expression() { ); } +#[test] +fn promising_direct() { + let (_, js) = expand_with_attr( + "e!(promising), + "e! { + fn echo(value: i32) -> i32 { + value + } + }, + ); + + assert_eq!(js, "WebAssembly.promising(wasmExports['echo'])"); +} + +#[test] +fn promising_without_output_converts_inputs() { + let (_, js) = expand_with_attr( + "e!(promising), + "e! { + fn notify(value: u128) { + let _ = value; + } + }, + ); + + assert_eq!( + js, + "(() => {\n const $promising = WebAssembly.promising(wasmExports['notify'])\n \ + return (arg0) => $promising(arg0, arg0 >> 64n)\n})()", + ); +} + +#[test] +fn promising_postprocesses_fulfilled_values() { + let (_, js) = expand_with_attr( + "e!(promising), + "e! { + fn echo(value: u32) -> u32 { + value + } + }, + ); + + assert_eq!( + js, + "(() => {\n const $promising = WebAssembly.promising(wasmExports['echo'])\n return \ + (arg0) => $promising(arg0).then(ret => {\n return ret >>> 0\n })\n})()", + ); +} + +#[test] +fn promising_externref_uses_passthrough() { + let (_, js) = expand_with_attr( + "e!(promising), + "e! { + fn echo(value: JsValue) -> JsValue { + value + } + }, + ); + + assert_eq!(js, "WebAssembly.promising(wasmExports['echo'])"); +} + +#[test] +fn promising_converts_multivalue_results() { + let (_, js) = expand_with_attr( + "e!(promising), + "e! { + fn echo(value: u128) -> u128 { + value + } + }, + ); + + assert_eq!( + js, + "(() => {\n const $promising = WebAssembly.promising(wasmExports['echo'])\n return \ + (arg0) => $promising(arg0, arg0 >> 64n).then(ret => {\n return \ + this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1])\n })\n})()", + ); +} + +#[test] +fn promising_turns_result_errors_into_rejections() { + let (_, js) = expand_with_attr( + "e!(promising), + "e! { + fn checked(value: i32) -> Result { + Ok(value) + } + }, + ); + + assert_eq!( + js, + "(() => {\n const $promising = WebAssembly.promising(wasmExports['checked'])\n \ + return (arg0) => $promising(arg0).then(ret => {\n if (ret[1] !== 0) throw \ + ret[2]\n return ret[0]\n })\n})()", + ); +} + +#[test] +fn promising_attribute_is_a_flag() { + let function = syn::parse2(quote! { + fn answer() -> i32 { + 42 + } + }) + .unwrap(); + + let error = crate::export::r#macro(quote!(promising = true), &function, Some("test_crate")) + .unwrap_err(); + assert_eq!(error.to_string(), "`promising` supports no values"); + + let error = crate::export::r#macro(quote!(promising, promising), &function, Some("test_crate")) + .unwrap_err(); + assert_eq!(error.to_string(), "duplicate `promising` argument"); +} + #[test] fn borrowed_return_is_rejected() { let function = syn::parse2(quote! { diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index e437480f..8c47e19b 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -654,6 +654,51 @@ fn js_embed() { ); } +#[test] +fn suspending_direct() { + let js = generated_js(syn::parse_quote! { + extern "js-sys" { + #[js_sys(suspending)] + pub fn wait(value: i32) -> i32; + } + }); + + assert_eq!(js, "new WebAssembly.Suspending(globalThis.wait)"); +} + +#[test] +fn suspending_converts_inputs_before_calling_javascript() { + let js = generated_js(syn::parse_quote! { + extern "js-sys" { + #[js_sys(suspending)] + pub fn wait(value: u32) -> u32; + } + }); + + assert_eq!( + js, + "new WebAssembly.Suspending((arg0_0) => {\n arg0_0 = arg0_0 >>> 0\n return \ + globalThis.wait(arg0_0)\n})", + ); +} + +#[test] +fn suspending_converts_fulfilled_indirect_results() { + let js = generated_js(syn::parse_quote! { + extern "js-sys" { + #[js_sys(suspending)] + pub fn wait() -> u128; + } + }); + + assert_eq!( + js, + "new WebAssembly.Suspending(async ($retptr) => {\n const $ret = await \ + (globalThis.wait())\n this.#jsEmbed.js_sys['numeric.128.encode']($ret, $ret >> 64n, \ + $retptr)\n})", + ); +} + #[test] fn r#return() { test!( @@ -840,6 +885,34 @@ fn incompatible_binding_options_are_rejected() { ); } +#[test] +fn suspending_requires_a_generated_binding() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(js_import, suspending)] + pub fn wait(); + } + }; + + assert_eq!( + super::macro_error(input), + "`suspending` cannot be combined with `js_import`; provide a `WebAssembly.Suspending` \ + import directly", + ); +} + +#[test] +fn duplicate_suspending_is_rejected() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(suspending, suspending)] + pub fn wait(); + } + }; + + assert_eq!(super::macro_error(input), "duplicate attribute"); +} + #[test] fn duplicate_parameter_abi_override_is_rejected() { let input = syn::parse_quote! { @@ -854,3 +927,19 @@ fn duplicate_parameter_abi_override_is_rejected() { assert_eq!(super::macro_error(input), "duplicate attribute"); } +fn generated_js(input: syn::ItemForeignMod) -> String { + let output = + crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap(); + let output = prettyplease::unparse(&syn::File { + shebang: None, + attrs: Vec::new(), + items: output, + }); + let dir = tempfile::tempdir().unwrap(); + let (_, js, _) = super::inner(dir.path(), &output).unwrap(); + + js.unwrap() +} diff --git a/host/runner/src/js/shared/shared.mjs b/host/runner/src/js/shared/shared.mjs index 338653ac..f8b3d021 100644 --- a/host/runner/src/js/shared/shared.mjs +++ b/host/runner/src/js/shared/shared.mjs @@ -1,4 +1,7 @@ import runData from "../run-data.json" with { type: "json" }; +function usesJspi(module) { + return WebAssembly.Module.imports(module).some(item => item.module === "js_sys" && item.name === "jspi_suspend"); +} function mainMemory(module, name, importObject) { const value = importObject[module]?.[name]; if (!(value instanceof WebAssembly.Memory)) { @@ -46,6 +49,7 @@ function mainArgs(memory, values, wasm64) { } } export async function run(module, jsBindgenCtor, report) { + const jspi = usesJspi(module); let interceptFlag = false; const interceptStore = []; const newLineText = { text: "\n", color: 0 /* Color.Default */ }; @@ -109,18 +113,19 @@ export async function run(module, jsBindgenCtor, report) { return 1 /* Status.Abnormal */; } const memory = mainMemory(runData.memory.module, runData.memory.name, state.importObject); + const mainExports = jspi ? state.instance.instance.exports : state.instance.exports; interceptFlag = true; let status; try { if (runData.wasm64) { const { argc, argv } = mainArgs(memory, runData.args, true); - const main = state.instance.exports["main"]; - status = main(argc, argv); + const main = mainExports["main"]; + status = jspi ? await WebAssembly.promising(main)(argc, argv) : main(argc, argv); } else { const { argc, argv } = mainArgs(memory, runData.args, false); - const main = state.instance.exports["main"]; - status = main(argc, argv); + const main = mainExports["main"]; + status = jspi ? await WebAssembly.promising(main)(argc, argv) : main(argc, argv); } } catch (error) { diff --git a/host/runner/src/js/shared/shared.mts b/host/runner/src/js/shared/shared.mts index 4b831378..ba3f0417 100644 --- a/host/runner/src/js/shared/shared.mts +++ b/host/runner/src/js/shared/shared.mts @@ -24,6 +24,18 @@ export const enum Status { type MainArgs32 = { argc: number; argv: number } type MainArgs64 = { argc: number; argv: bigint } +type WasmFunction = (...args: Args) => Result +type Jspi = typeof WebAssembly & { + promising( + fn: WasmFunction + ): WasmFunction> +} + +function usesJspi(module: WebAssembly.Module): boolean { + return WebAssembly.Module.imports(module).some( + item => item.module === "js_sys" && item.name === "jspi_suspend" + ) +} function mainMemory( module: string, @@ -95,6 +107,7 @@ export async function run( jsBindgenCtor: typeof JsBindgen, report: (stream: Stream, text: StyledText[]) => void ): Promise { + const jspi = usesJspi(module) let interceptFlag = false const interceptStore: string[] = [] const newLineText = { text: "\n", color: Color.Default } @@ -166,6 +179,7 @@ export async function run( } const memory = mainMemory(runData.memory.module, runData.memory.name, state.importObject) + const mainExports = jspi ? state.instance.instance.exports : state.instance.exports interceptFlag = true let status: number @@ -173,12 +187,12 @@ export async function run( try { if (runData.wasm64) { const { argc, argv } = mainArgs(memory, runData.args, true) - const main = state.instance.exports["main"] as (argc: number, argv: bigint) => number - status = main(argc, argv) + const main = mainExports["main"] as (argc: number, argv: bigint) => number + status = jspi ? await (WebAssembly as Jspi).promising(main)(argc, argv) : main(argc, argv) } else { const { argc, argv } = mainArgs(memory, runData.args, false) - const main = state.instance.exports["main"] as (argc: number, argv: number) => number - status = main(argc, argv) + const main = mainExports["main"] as (argc: number, argv: number) => number + status = jspi ? await (WebAssembly as Jspi).promising(main)(argc, argv) : main(argc, argv) } } catch (error) { const message = state.panicMessage ?? (error as Error).message diff --git a/web/playground/src/main.rs b/web/playground/src/main.rs index 06f748ae..68e4e5de 100644 --- a/web/playground/src/main.rs +++ b/web/playground/src/main.rs @@ -23,6 +23,8 @@ fn main() { #[cfg(test)] mod tests { + use js_sys::{JsValue, Promise, block_on}; + #[test] #[should_panic] fn test1() { @@ -37,4 +39,24 @@ mod tests { #[test] fn test3() {} + + #[test] + fn jspi_block_on() { + let value = String::from("resolved"); + let output = block_on(async { + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + value.as_str() + }); + + assert_eq!(output, "resolved"); + } + + #[test] + #[should_panic(expected = "JSPI panic")] + fn jspi_should_panic() { + block_on(async { + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + panic!("JSPI panic"); + }); + } } From da979a559205c53b2c4d3f64b1a1fc857420a99b Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:06:14 +0800 Subject: [PATCH 11/21] Remove obsolete js_sys lowering paths --- client/js-sys/src/macro.rs | 11 - client/js-sys/src/macro/abi.rs | 81 +----- client/js-sys/src/macro/export.rs | 70 +++-- client/js-sys/src/macro/import/wat.rs | 4 +- client/js-sys/src/macro/js_import.rs | 258 ------------------ client/js-sys/src/macro/result.rs | 13 - client/js-sys/src/macro/wat.rs | 47 +--- client/js-sys/src/macro/wat_import.rs | 147 ---------- host/js-sys-bindgen/src/tests/macro/export.rs | 33 +-- 9 files changed, 74 insertions(+), 590 deletions(-) delete mode 100644 client/js-sys/src/macro/js_import.rs delete mode 100644 client/js-sys/src/macro/wat_import.rs diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index 834d7afd..d8ebe6d4 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -2,13 +2,9 @@ mod abi; mod closure; mod export; mod import; -mod js_import; mod result; mod text; mod wat; -mod wat_import; - -pub use alloc::boxed::Box; pub use abi::*; pub use import::*; @@ -23,11 +19,6 @@ pub use crate::{ js_export, js_export_arguments, js_export_input_arguments, js_export_output_expression, js_export_parameters, js_export_promising, js_export_promising_then, js_export_result_throw, }; -// JavaScript import shims. -pub use crate::{ - js_function, js_import, js_indirect_function, js_input_parameters, js_needs_shim, js_output, - js_parameter, -}; // WAT closure shims. pub use crate::{ wat_closure, wat_closure_call, wat_closure_direct, wat_closure_indirect, @@ -35,7 +26,5 @@ pub use crate::{ }; // WAT export shims. pub use crate::{wat_export, wat_export_direct, wat_export_indirect}; -// WAT import shims. -pub use crate::{wat_import, wat_import_output}; // Shared WAT helpers. pub use crate::{wat_imports, wat_input, wat_locals, wat_slots, wat_unique_list}; diff --git a/client/js-sys/src/macro/abi.rs b/client/js-sys/src/macro/abi.rs index dd5969b3..9fd76505 100644 --- a/client/js-sys/src/macro/abi.rs +++ b/client/js-sys/src/macro/abi.rs @@ -2,7 +2,7 @@ use crate::hazard::{ FromJS, IntoJS, ReturnAbi, ReturnFromJS, ReturnIntoJS, Slot, WasmAbi, WasmRet, WatConv, }; -// Rust ABI shims used by generated import and export functions. +// Rust `ABI` shims used by generated import and export functions. pub type InputSlot1 = <::Abi as WasmAbi>::Slot1; pub type InputSlot2 = <::Abi as WasmAbi>::Slot2; @@ -51,13 +51,13 @@ pub fn return_to_js(value: T) -> WasmRet { } /// Lowers a value through a different [`IntoJS`] implementation with the same -/// ABI. This is reserved for generated `#[js_sys(type = ...)]` overrides, where -/// `T` must also describe the value's WAT and JavaScript conversions. +/// `ABI`. This is reserved for generated `#[js_sys(type = ...)]` overrides, +/// where `T` must also describe the value's WAT and JavaScript conversions. /// /// # Safety /// -/// The value's lowering must have the semantics expected by `T`; sharing an ABI -/// alone does not make two [`IntoJS`] implementations interchangeable. +/// The value's lowering must have the semantics expected by `T`; sharing an +/// `ABI` alone does not make two [`IntoJS`] implementations interchangeable. #[must_use] #[inline] pub unsafe fn split_input_as( @@ -72,7 +72,7 @@ pub fn join_output(value: OutputRet) -> T { T::from_return_abi(value) } -// Compile-time validation of conversion metadata. +// Compile-time validation of conversion `metadata`. #[must_use] pub const fn into_js_is_multislot() -> bool { @@ -140,13 +140,13 @@ pub const fn validate_return_from_js() { ); } -// WAT metadata shared by import and export shims. +// WAT `metadata` shared by import and export shims. /// The `WAT` representation of one `ABI` slot at a JavaScript boundary. #[doc(hidden)] #[derive(Clone, Copy)] pub struct WatSlot { - /// The carrier type in the Rust function ABI. + /// The carrier type in the Rust function `ABI`. pub abi: &'static str, /// The type visible at the JavaScript boundary. pub boundary: &'static str, @@ -238,69 +238,6 @@ pub const fn wat_direct() -> &'static str { } } -#[must_use] -pub const fn wat_indirect_type() -> &'static str { - if return_from_js_is_direct::() { - "" - } else { - crate::util::WAT_PTR_TYPE - } -} - -#[must_use] -pub const fn wat_indirect_import_type() -> &'static str { - if return_from_js_is_direct::() { - "" - } else { - into_js_wat_slots::>()[0].boundary - } -} - -#[must_use] -pub const fn wat_indirect_conv() -> &'static str { - if return_from_js_is_direct::() { - "" - } else { - into_js_wat_slots::>()[0].conv - } -} - -#[must_use] -pub const fn wat_output_imports() -> &'static str { - if return_from_js_is_direct::() { - return_from_js_wat_slots::()[0].imports - } else { - "" - } -} - -#[must_use] -pub const fn wat_output_locals() -> &'static str { - if return_from_js_is_direct::() { - return_from_js_wat_slots::()[0].locals - } else { - into_js_wat_slots::>()[0].locals - } -} - -#[must_use] -pub const fn wat_output_import_type() -> &'static str { - if return_from_js_is_direct::() { - return_from_js_wat_slots::()[0].boundary - } else { - "" - } -} - -#[must_use] -pub const fn wat_output_conv() -> &'static str { - if return_from_js_is_direct::() { - return_from_js_wat_slots::()[0].conv - } else { - "" - } -} - #[must_use] pub const fn return_from_js_is_direct() -> bool { ::MODE.is_direct() @@ -352,7 +289,7 @@ pub const fn wat_pointer_type() -> &'static str { crate::util::WAT_PTR_TYPE } -// JavaScript conversion metadata. +// JavaScript conversion `metadata`. #[must_use] pub const fn js_input_embed() -> (&'static str, &'static str) { diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs index 9fa8ab57..1001522a 100644 --- a/client/js-sys/src/macro/export.rs +++ b/client/js-sys/src/macro/export.rs @@ -316,17 +316,29 @@ macro_rules! js_export { $crate::r#macro::js_export_parameters!($(($par, $input)),*); const ARGUMENTS: &::core::primitive::str = $crate::r#macro::js_export_arguments!($(($par, $input)),*); + const PASSTHROUGH: ::core::primitive::bool = true + $(&& !$crate::r#macro::js_from_has_conversion::<$input>())*; + const RAW: &::core::primitive::str = $crate::r#macro::const_concat!( + "wasmExports['", + $export, + "']", + ); $($crate::r#macro::validate_from_js::<$input>();)* - $crate::r#macro::const_concat!( - "(", - PARAMETERS, - ") => {\n wasmExports['", - $export, - "'](", - ARGUMENTS, - ")\n}" - ) + + if PASSTHROUGH { + RAW + } else { + $crate::r#macro::const_concat!( + "(", + PARAMETERS, + ") => {\n ", + RAW, + "(", + ARGUMENTS, + ")\n}" + ) + } }}; ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ const PARAMETERS: &::core::primitive::str = @@ -337,22 +349,36 @@ macro_rules! js_export { $crate::r#macro::js_export_output_expression!($output); const THROW: &::core::primitive::str = $crate::r#macro::js_export_result_throw!(" ", $output); + const PASSTHROUGH: ::core::primitive::bool = + !$crate::r#macro::js_return_has_conversion::<$output>() + && !$crate::r#macro::return_into_js_is_result::<$output>() + $(&& !$crate::r#macro::js_from_has_conversion::<$input>())*; + const RAW: &::core::primitive::str = $crate::r#macro::const_concat!( + "wasmExports['", + $export, + "']", + ); $($crate::r#macro::validate_from_js::<$input>();)* $crate::r#macro::validate_return_into_js::<$output>(); - $crate::r#macro::const_concat!( - "(", - PARAMETERS, - ") => {\n const ret = wasmExports['", - $export, - "'](", - ARGUMENTS, - ")\n", - THROW, - " return ", - OUTPUT, - "\n}" - ) + + if PASSTHROUGH { + RAW + } else { + $crate::r#macro::const_concat!( + "(", + PARAMETERS, + ") => {\n const ret = ", + RAW, + "(", + ARGUMENTS, + ")\n", + THROW, + " return ", + OUTPUT, + "\n}" + ) + } }}; } diff --git a/client/js-sys/src/macro/import/wat.rs b/client/js-sys/src/macro/import/wat.rs index 5909b3ea..26ed685f 100644 --- a/client/js-sys/src/macro/import/wat.rs +++ b/client/js-sys/src/macro/import/wat.rs @@ -598,8 +598,8 @@ const fn input_types_are_empty(inputs: &[super::ImportInput]) -> bool { return true; } - // `wat_input!(import types; ...)` unconditionally inserts one space between - // arguments, so two or more arguments always produce a non-empty fragment. + // Input groups are separated by a space, so two or more arguments always + // produce a non-empty fragment. if inputs.len() > 1 { return false; } diff --git a/client/js-sys/src/macro/js_import.rs b/client/js-sys/src/macro/js_import.rs deleted file mode 100644 index 9ab815ec..00000000 --- a/client/js-sys/src/macro/js_import.rs +++ /dev/null @@ -1,258 +0,0 @@ -/// Generates the complete JavaScript shim for one import. -#[doc(hidden)] -#[macro_export] -macro_rules! js_import { - ( - direct_wrapper = $direct_wrapper:expr, - direct_call = $direct_call:expr, - indirect_call = $indirect_call:expr, - inputs = [$(($par:literal, $input:ty)),* $(,)?], - ) => {{ - const WRAPPED: ::core::primitive::bool = - $crate::r#macro::js_needs_shim!(($($input),*)); - const OPEN: &::core::primitive::str = if WRAPPED { - $crate::r#macro::js_function!("(", ") => {\n", $(($par, $input)),*) - } else if $direct_wrapper { - $crate::r#macro::js_function!("(", ") => ", $(($par, $input)),*) - } else { - "" - }; - const BODY: &::core::primitive::str = if WRAPPED { - $crate::r#macro::const_concat!($indirect_call, "\n}") - } else { - $direct_call - }; - - $crate::r#macro::const_concat!( - OPEN, - $($crate::r#macro::js_parameter!($par, $input),)* - BODY - ) - }}; - ( - direct_wrapper = $direct_wrapper:expr, - direct_call = $direct_call:expr, - indirect_call = $indirect_call:expr, - inputs = [$(($par:literal, $input:ty)),* $(,)?], - output = $output:ty, - ) => {{ - const WRAPPED: ::core::primitive::bool = - $crate::r#macro::js_needs_shim!(($($input),*), $output); - const OPEN: &::core::primitive::str = if WRAPPED { - $crate::r#macro::js_indirect_function!( - "(", - ") => {\n", - ($output), - $(($par, $input)),* - ) - } else if $direct_wrapper { - $crate::r#macro::js_function!("(", ") => ", $(($par, $input)),*) - } else { - "" - }; - - $crate::r#macro::const_concat!( - OPEN, - $($crate::r#macro::js_parameter!($par, $input),)* - $crate::r#macro::js_output!( - WRAPPED, - " return ", - $direct_call, - $indirect_call, - $output, - ) - ) - }}; -} - -// Shim selection. - -#[doc(hidden)] -#[macro_export] -macro_rules! js_needs_shim { - (($($input:ty),*) $(, $output:ty)? $(,)?) => {{ - 'outer: { - $( - $crate::r#macro::validate_into_js::<$input>(); - - if ::core::option::Option::is_some( - &<$input as $crate::hazard::IntoJS>::JS_CONV, - ) { - break 'outer true; - } - )* - - $( - $crate::r#macro::validate_return_from_js::<$output>(); - - if $crate::r#macro::catches_result_in_js::<$output>() - || ::core::option::Option::is_some( - &<$output as $crate::hazard::ReturnFromJS>::JS_CONV.conversion(), - ) - { - break 'outer true; - } - )? - - false - } - }}; -} - -// Input rendering. - -#[doc(hidden)] -#[macro_export] -macro_rules! js_input_parameters { - ($par:literal, $ty:ty $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::into_js_wat_slots::<$ty>(); - - $crate::r#macro::const_concat_if!( - true => [$par, "_0"], - !SLOTS[1].abi.is_empty() => [", ", $par, "_1"], - !SLOTS[2].abi.is_empty() => [", ", $par, "_2"], - !SLOTS[3].abi.is_empty() => [", ", $par, "_3"], - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_function { - ($pre:literal, $post:literal $(,)?) => { - $crate::r#macro::const_concat!($pre, $post) - }; - ($pre:literal, $post:literal, ($par:literal, $ty:ty) $(, ($rest_par:literal, $rest_ty:ty))* $(,)?) => { - $crate::r#macro::const_concat!( - $pre, - $crate::r#macro::js_input_parameters!($par, $ty), - $(", ", $crate::r#macro::js_input_parameters!($rest_par, $rest_ty),)* - $post - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_indirect_function { - ($pre:literal, $post:literal, (), $(($par:literal, $ty:ty)),* $(,)?) => { - $crate::r#macro::js_function!($pre, $post, $(($par, $ty)),*) - }; - ($pre:literal, $post:literal, ($output:ty), $(($par:literal, $ty:ty)),* $(,)?) => {{ - const PARAMETERS: &::core::primitive::str = - $crate::r#macro::js_function!("", "", $(($par, $ty)),*); - const INDIRECT: ::core::primitive::bool = - !$crate::r#macro::return_from_js_is_direct::<$output>(); - const RETURN: &::core::primitive::str = if INDIRECT { "$retptr" } else { "" }; - const SEPARATOR: &::core::primitive::str = - if INDIRECT && !PARAMETERS.is_empty() { ", " } else { "" }; - - $crate::r#macro::const_concat!($pre, RETURN, SEPARATOR, PARAMETERS, $post) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_parameter { - ($par:literal, $ty:ty $(,)?) => {{ - const HAS_CONV: ::core::primitive::bool = - ::core::option::Option::is_some(&<$ty as $crate::hazard::IntoJS>::JS_CONV); - const TEMPLATE: &::core::primitive::str = $crate::r#macro::js_input_template::<$ty>(); - const SLOTS: [&::core::primitive::str; 4] = [ - $crate::r#macro::const_concat!($par, "_0"), - $crate::r#macro::const_concat!($par, "_1"), - $crate::r#macro::const_concat!($par, "_2"), - $crate::r#macro::const_concat!($par, "_3"), - ]; - const CONV: &::core::primitive::str = $crate::r#macro::js_template!( - TEMPLATE, - slots = SLOTS, - ); - - $crate::r#macro::const_concat_if!( - HAS_CONV => [" ", $par, "_0 = ", CONV, "\n"], - ) - }}; -} - -// Return rendering. - -#[doc(hidden)] -#[macro_export] -macro_rules! js_output { - ($wrapped:expr, $start:literal, $direct_call:literal, $indirect_call:literal, $output:ty $(,)?) => {{ - const OUTPUT_WRAPPED: ::core::primitive::bool = $wrapped; - const DIRECT_RETURN: ::core::primitive::bool = - $crate::r#macro::return_from_js_is_direct::<$output>(); - const CONVERT_DIRECT: ::core::primitive::bool = - DIRECT_RETURN && $crate::r#macro::js_output_has_conversion::<$output>(); - const CATCH_RESULT: ::core::primitive::bool = - $crate::r#macro::catches_result_in_js::<$output>(); - const CALL: &::core::primitive::str = if OUTPUT_WRAPPED { - $indirect_call - } else { - $direct_call - }; - const TEMPLATES: [&::core::primitive::str; 4] = - $crate::r#macro::js_output_templates::<$output>(); - const TEMPLATE_VALUE: &::core::primitive::str = if DIRECT_RETURN && !CONVERT_DIRECT { - CALL - } else { - "$ret" - }; - const SLOTS: [&::core::primitive::str; 4] = [ - $crate::r#macro::js_template!(TEMPLATES[0], value = TEMPLATE_VALUE), - $crate::r#macro::js_template!(TEMPLATES[1], value = TEMPLATE_VALUE), - $crate::r#macro::js_template!(TEMPLATES[2], value = TEMPLATE_VALUE), - $crate::r#macro::js_template!(TEMPLATES[3], value = TEMPLATE_VALUE), - ]; - const SRET: &::core::primitive::str = $crate::r#macro::js_output_sret::<$output>(); - const INDENT: &::core::primitive::str = if CATCH_RESULT { " " } else { " " }; - const VALUE_START: &::core::primitive::str = if CONVERT_DIRECT { - $crate::r#macro::const_concat!(INDENT, "const $ret = ") - } else if DIRECT_RETURN { - if CATCH_RESULT { - " return " - } else if OUTPUT_WRAPPED { - $start - } else { - "" - } - } else { - $crate::r#macro::const_concat!(INDENT, "const $ret = ") - }; - const OUTPUT_VALUE: &::core::primitive::str = if DIRECT_RETURN && !CONVERT_DIRECT { - SLOTS[0] - } else { - CALL - }; - const DIRECT_CONVERSION: &::core::primitive::str = $crate::r#macro::const_concat_if!( - CONVERT_DIRECT => ["\n", INDENT, "return ", SLOTS[0]], - ); - const SRET_CALL: &::core::primitive::str = $crate::r#macro::const_concat_if!( - !DIRECT_RETURN => ["\n", INDENT, SRET, "(", SLOTS[0]], - !DIRECT_RETURN && !SLOTS[1].is_empty() => [", ", SLOTS[1]], - !DIRECT_RETURN && !SLOTS[2].is_empty() => [", ", SLOTS[2]], - !DIRECT_RETURN && !SLOTS[3].is_empty() => [", ", SLOTS[3]], - !DIRECT_RETURN => [", $retptr)"], - ); - const TRY: &::core::primitive::str = $crate::r#macro::js_result_try::<$output>(); - const END: &::core::primitive::str = if CATCH_RESULT { - $crate::r#macro::js_result_catch::<$output>(DIRECT_RETURN) - } else if OUTPUT_WRAPPED { - "\n}" - } else { - "" - }; - - $crate::r#macro::const_concat!( - TRY, - VALUE_START, - OUTPUT_VALUE, - DIRECT_CONVERSION, - SRET_CALL, - END - ) - }}; -} diff --git a/client/js-sys/src/macro/result.rs b/client/js-sys/src/macro/result.rs index 5fd2d29c..8e8c3124 100644 --- a/client/js-sys/src/macro/result.rs +++ b/client/js-sys/src/macro/result.rs @@ -49,19 +49,6 @@ const WAT_CATCH: &str = crate::const_concat!( "\n call $js_sys.exception.store (@reloc)", ); -#[must_use] -pub const fn catches_result_in_js() -> bool { - #[cfg(target_feature = "exception-handling")] - { - false - } - - #[cfg(not(target_feature = "exception-handling"))] - { - crate::r#macro::return_from_js_is_result::() - } -} - #[must_use] pub const fn js_result_try() -> &'static str { #[cfg(target_feature = "exception-handling")] diff --git a/client/js-sys/src/macro/wat.rs b/client/js-sys/src/macro/wat.rs index 197ea0d4..78f72022 100644 --- a/client/js-sys/src/macro/wat.rs +++ b/client/js-sys/src/macro/wat.rs @@ -253,16 +253,6 @@ macro_rules! wat_slots { !SLOTS[3].abi.is_empty() => [" (param $", $par, "_3 ", SLOTS[3].$field, ")"], ) }}; - (import_gets, $par:literal, $slots:expr $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => ["", " local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv], - !SLOTS[1].abi.is_empty() => ["\n", " local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv], - !SLOTS[2].abi.is_empty() => ["\n", " local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv], - !SLOTS[3].abi.is_empty() => ["\n", " local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv], - ) - }}; (export_gets, $par:literal, $slots:expr $(,)?) => {{ const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; @@ -297,45 +287,10 @@ macro_rules! wat_slots { }}; } -/// Renders input fragments for JavaScript imports and Rust exports. +/// Renders input fragments for Rust exports. #[doc(hidden)] #[macro_export] macro_rules! wat_input { - (import types;) => { - "" - }; - (import types; $first:ty $(, $rest:ty)* $(,)?) => { - $crate::r#macro::const_concat!( - $crate::r#macro::wat_slots!( - types, - $crate::r#macro::into_js_wat_slots::<$first>(), - boundary, - ), - $( - " ", - $crate::r#macro::wat_slots!( - types, - $crate::r#macro::into_js_wat_slots::<$rest>(), - boundary, - ), - )* - ) - }; - (import params; $par:literal, $ty:ty $(,)?) => { - $crate::r#macro::wat_slots!( - params, - $par, - $crate::r#macro::into_js_wat_slots::<$ty>(), - abi, - ) - }; - (import gets; $par:literal, $ty:ty $(,)?) => { - $crate::r#macro::wat_slots!( - import_gets, - $par, - $crate::r#macro::into_js_wat_slots::<$ty>(), - ) - }; (export raw_param; $ty:ty $(,)?) => { $crate::r#macro::wat_slots!( grouped_param, diff --git a/client/js-sys/src/macro/wat_import.rs b/client/js-sys/src/macro/wat_import.rs deleted file mode 100644 index f6e775b6..00000000 --- a/client/js-sys/src/macro/wat_import.rs +++ /dev/null @@ -1,147 +0,0 @@ -/// Generates the complete WAT shim for one JavaScript import. -#[doc(hidden)] -#[macro_export] -macro_rules! wat_import { - ( - module = $crate_name:expr, - import = $import_name:expr, - shim = $foreign_name:expr, - inputs = [$(($par:literal, $input:ty)),* $(,)?], - $(output = $output:ty,)? - ) => {{ - const INPUT_TYPES: &::core::primitive::str = - $crate::r#macro::wat_input!(import types; $($input),*); - const INPUT_PARAM: &::core::primitive::str = $crate::r#macro::const_concat_if!( - !INPUT_TYPES.is_empty() => [" (param ", INPUT_TYPES, ")"], - ); - - $crate::r#macro::const_concat!( - "(import \"", - $crate_name, - "\" \"", - $import_name, - "\" (func $", - $crate_name, - ".import.", - $import_name, - " (@sym (name \"", - $crate_name, - ".import.", - $import_name, - "\"))", - $($crate::r#macro::wat_import_output!(import_param, $output),)? - INPUT_PARAM, - $($crate::r#macro::wat_import_output!(import_result, $output),)? - "))", - $crate::r#macro::wat_imports!( - slots = [ - $($crate::r#macro::into_js_wat_slots::<$input>(),)* - ], - extras = [ - $($crate::r#macro::wat_output_imports::<$output>(),)? - $($crate::r#macro::wat_result_imports::<$output>(),)? - ], - ), - "\n(func $", - $foreign_name, - " (@sym)", - $($crate::r#macro::wat_import_output!(shim_param, $output),)? - $($crate::r#macro::wat_input!(import params; $par, $input),)* - $($crate::r#macro::wat_import_output!(shim_result, $output),)? - $crate::r#macro::wat_locals!( - slots = [ - $($crate::r#macro::into_js_wat_slots::<$input>(),)* - ], - extras = [ - $($crate::r#macro::wat_output_locals::<$output>(),)? - $($crate::r#macro::wat_result_locals::<$output>(),)? - ], - ), - $($crate::r#macro::wat_result_try::<$output>(),)? - $($crate::r#macro::wat_import_output!(shim_retptr, $output),)? - $( - "\n", - $crate::r#macro::wat_input!(import gets; $par, $input), - )* - "\n call $", - $crate_name, - ".import.", - $import_name, - " (@reloc)", - $($crate::r#macro::wat_import_output!(shim_convert, $output),)? - $($crate::r#macro::wat_result_catch::<$output>(),)? - $($crate::r#macro::wat_result_default::<$output>(),)? - "\n)" - ) - }}; -} - -/// Renders the direct or indirect output fragments of an import shim. -#[doc(hidden)] -#[macro_export] -macro_rules! wat_import_output { - (import_param, $ty:ty $(,)?) => { - if $crate::r#macro::return_from_js_is_direct::<$ty>() { - "" - } else { - $crate::r#macro::const_concat!( - " (param $retptr ", - $crate::r#macro::wat_indirect_import_type::<$ty>(), - ")" - ) - } - }; - (import_result, $ty:ty $(,)?) => { - if $crate::r#macro::return_from_js_is_direct::<$ty>() { - $crate::r#macro::const_concat!( - " (result ", - $crate::r#macro::wat_output_import_type::<$ty>(), - ")" - ) - } else { - "" - } - }; - (shim_param, $ty:ty $(,)?) => { - if $crate::r#macro::return_from_js_is_direct::<$ty>() { - "" - } else { - $crate::r#macro::const_concat!( - " (param $retptr ", - $crate::r#macro::wat_indirect_type::<$ty>(), - ")" - ) - } - }; - (shim_result, $ty:ty $(,)?) => { - if $crate::r#macro::return_from_js_is_direct::<$ty>() { - $crate::r#macro::const_concat!(" (result ", $crate::r#macro::wat_direct::<$ty>(), ")") - } else { - "" - } - }; - (shim_retptr, $ty:ty $(,)?) => {{ - if $crate::r#macro::return_from_js_is_direct::<$ty>() { - "" - } else { - const CONV: &::core::primitive::str = $crate::r#macro::wat_indirect_conv::<$ty>(); - - $crate::r#macro::const_concat!( - "\n local.get $retptr", - $crate::r#macro::wat_conv_prefix(CONV), - CONV - ) - } - }}; - (shim_convert, $ty:ty $(,)?) => { - if !$crate::r#macro::return_from_js_is_direct::<$ty>() { - "" - } else if !$crate::r#macro::wat_output_conv::<$ty>().is_empty() { - const CONV: &::core::primitive::str = $crate::r#macro::wat_output_conv::<$ty>(); - - $crate::r#macro::const_concat!("\n ", CONV) - } else { - "" - } - }; -} diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs index 11eab0ae..7a89143a 100644 --- a/host/js-sys-bindgen/src/tests/macro/export.rs +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -205,6 +205,17 @@ fn direct() { ); } +#[test] +fn scalar_passthrough() { + let (_, js) = expand("e! { + fn echo(value: i32) -> i32 { + value + } + }); + + assert_eq!(js, "wasmExports['echo']"); +} + #[test] fn wat_slot_conversions() { let (wat, js) = expand("e! { @@ -233,12 +244,7 @@ fn wat_slot_conversions() { call $raw (@reloc) )" ); - assert_eq!( - js, - r"(arg0) => { - wasmExports['drop_value'](arg0) -}" - ); + assert_eq!(js, "wasmExports['drop_value']"); let (wat, js) = expand("e! { pub fn undefined() -> Option { @@ -267,13 +273,7 @@ fn wat_slot_conversions() { end )" ); - assert_eq!( - js, - r"() => { - const ret = wasmExports['undefined']() - return ret -}" - ); + assert_eq!(js, "wasmExports['undefined']"); } #[test] @@ -567,10 +567,5 @@ fn no_return_value() { call $raw (@reloc) )"# ); - assert_eq!( - js, - r"(arg0) => { - wasmExports['nothing'](arg0) -}" - ); + assert_eq!(js, "wasmExports['nothing']"); } From 5b96889c720a8fa5ef547ebd067f34013c2f1647 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:06:20 +0800 Subject: [PATCH 12/21] Refine js_sys runtime values --- client/js-sys/src/builtins/array.rs | 73 +++++++++++-------- client/js-sys/src/builtins/bigint.rs | 1 + client/js-sys/src/builtins/number.rs | 1 + client/js-sys/src/builtins/string.rs | 9 ++- client/js-sys/src/runtime/externref.rs | 29 +++++++- client/js-sys/src/runtime/future/queue.rs | 2 +- .../js-sys/src/runtime/future/task/atomic.rs | 46 +++--------- client/js-sys/src/runtime/panic.rs | 8 +- client/js-sys/src/runtime/value.rs | 10 +-- client/js-sys/tests/array.rs | 25 ++++++- client/js-sys/tests/value.rs | 16 +++- 11 files changed, 134 insertions(+), 86 deletions(-) diff --git a/client/js-sys/src/builtins/array.rs b/client/js-sys/src/builtins/array.rs index 5b0be780..c0cbfa46 100644 --- a/client/js-sys/src/builtins/array.rs +++ b/client/js-sys/src/builtins/array.rs @@ -3,6 +3,7 @@ use core::fmt::{self, Display, Formatter}; use core::mem::MaybeUninit; use core::ptr; +use super::Object; use crate::JsValue; use crate::hazard::{IntoJS, IntoJsConv, JsCast}; use crate::runtime::externref; @@ -10,6 +11,7 @@ use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; #[crate::js_sys(js_sys = crate)] extern "js-sys" { + #[js_sys(js_name = "Array", extends = Object)] pub type JsArray; #[js_sys(getter)] @@ -47,7 +49,8 @@ extern "js-sys" { array_len: PtrLength, externref_ptr: PtrConst, externref_len: i32, - ) -> bool; + write_output: bool, + ) -> Result; #[js_sys(js_embed = "view.getUint32")] // SAFETY: The pointer and length must describe a valid `u32` slice. @@ -117,7 +120,7 @@ pub struct TryFromJsArrayError; impl Display for TryFromJsArrayError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("length did not match") + f.write_str("failed to copy array") } } @@ -125,22 +128,26 @@ impl Error for TryFromJsArrayError {} impl JsArray { pub fn to_slice(&self, slice: &mut [T]) -> Result<(), TryFromJsArrayError> { - let slice = JsValue::from_slice_mut(slice); let slots = externref::reserve_slots(slice.len()); - // SAFETY: Parameters are correct. - let result = unsafe { - array_js_value_encode( - self.as_any(), - PtrMut::new(slice), - PtrLength::new(slice), - slots.ptr(), - slots.len(), - ) + let result = { + let js_slice = JsValue::from_slice_mut(slice); + // SAFETY: Parameters are correct. `write_output` is false, so JavaScript + // does not write through the destination pointer. + unsafe { + array_js_value_encode( + self.as_any(), + PtrMut::new(js_slice), + PtrLength::new(js_slice), + slots.ptr(), + slots.len(), + false, + ) + } }; - if result { - slots.commit(); + if matches!(result, Ok(true)) { + slots.replace(slice); Ok(()) } else { Err(TryFromJsArrayError) @@ -162,10 +169,11 @@ impl JsArray { PtrLength::from_uninit_slice(js_slice), slots.ptr(), slots.len(), + true, ) }; - if result { + if matches!(result, Ok(true)) { slots.commit(); // SAFETY: Correctly initialized in JS. Ok(unsafe { assume_init_mut(slice) }) @@ -187,10 +195,11 @@ impl JsArray { PtrLength::from_uninit_array(js_array), slots.ptr(), slots.len(), + true, ) }; - if result { + if matches!(result, Ok(true)) { slots.commit(); // SAFETY: Correctly initialized in JS. Ok(unsafe { array.assume_init() }) @@ -204,7 +213,7 @@ js_bindgen::embed_js!( module = "js_sys", name = "array.js_value.encode", required_embeds = [("js_sys", "view.getInt32"), ("js_sys", "view.setInt32")], - "(array, arrPtr, arrLen, refPtr, refLen) => {{", + "(array, arrPtr, arrLen, refPtr, refLen, writeOutput) => {{", " if (array.length !== arrLen) return false", "", " const table = this.#jsEmbed.js_sys['externref.table']", @@ -212,15 +221,19 @@ js_bindgen::embed_js!( " refPtr,", " refLen,", " )", - " const elemIndices = new Array(arrLen)", - "", - " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", - " const elemIndex = refIndices[arrayIndex]", - " table.set(elemIndex, array[arrayIndex])", - " elemIndices[arrayIndex] = elemIndex", + " if (writeOutput) {{", + " const elemIndices = new Array(arrLen)", + " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", + " const elemIndex = refIndices[arrayIndex]", + " table.set(elemIndex, array[arrayIndex])", + " elemIndices[arrayIndex] = elemIndex", + " }}", + " this.#jsEmbed.js_sys['view.setInt32'](arrPtr, elemIndices)", + " }} else {{", + " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", + " table.set(refIndices[arrayIndex], array[arrayIndex])", + " }}", " }}", - "", - " this.#jsEmbed.js_sys['view.setInt32'](arrPtr, elemIndices)", " return true", "}}", ); @@ -231,9 +244,10 @@ js_bindgen::embed_js!( required_embeds = [("js_sys", "view.getInt32")], "(ptr, len) => {{", " const array = new Array(len)", + " const table = this.#jsEmbed.js_sys['externref.table']", " const refIndices = this.#jsEmbed.js_sys['view.getInt32'](ptr, len)", " for (let arrayIndex = 0; arrayIndex < len; arrayIndex++) {{", - " array[arrayIndex] = this.#jsEmbed.js_sys['externref.table'].get(refIndices[arrayIndex])", + " array[arrayIndex] = table.get(refIndices[arrayIndex])", " }}", " return array", "}}", @@ -297,8 +311,7 @@ impl JsArray { } } - #[must_use] - pub fn to_array(&self) -> Option<[u32; N]> { + pub fn to_array(&self) -> Result<[u32; N], TryFromJsArrayError> { let mut array: MaybeUninit<[u32; N]> = MaybeUninit::uninit(); // SAFETY: Parameters are correct. @@ -312,9 +325,9 @@ impl JsArray { if result { // SAFETY: Correctly initialized in JS. - Some(unsafe { array.assume_init() }) + Ok(unsafe { array.assume_init() }) } else { - None + Err(TryFromJsArrayError) } } } diff --git a/client/js-sys/src/builtins/bigint.rs b/client/js-sys/src/builtins/bigint.rs index 40d2e235..29b050a0 100644 --- a/client/js-sys/src/builtins/bigint.rs +++ b/client/js-sys/src/builtins/bigint.rs @@ -1,4 +1,5 @@ #[crate::js_sys(js_sys = crate)] extern "js-sys" { + #[js_sys(js_name = "BigInt")] pub type JsBigInt; } diff --git a/client/js-sys/src/builtins/number.rs b/client/js-sys/src/builtins/number.rs index c192336d..104a2e4c 100644 --- a/client/js-sys/src/builtins/number.rs +++ b/client/js-sys/src/builtins/number.rs @@ -1,4 +1,5 @@ #[crate::js_sys(js_sys = crate)] extern "js-sys" { + #[js_sys(js_name = "Number")] pub type JsNumber; } diff --git a/client/js-sys/src/builtins/string.rs b/client/js-sys/src/builtins/string.rs index fd158eb3..66451cba 100644 --- a/client/js-sys/src/builtins/string.rs +++ b/client/js-sys/src/builtins/string.rs @@ -11,10 +11,12 @@ use crate::util::{PtrConst, PtrLength, PtrMut}; #[js_sys(js_sys = crate)] extern "js-sys" { #[js_sys(js_name = "String", extends = Object)] - #[derive(Debug, Clone, PartialEq, Eq)] + #[derive(Debug, Clone, PartialEq)] pub type JsString; } +impl Eq for JsString {} + #[js_sys(js_sys = crate)] extern "js-sys" { #[js_sys(js_name = "String")] @@ -196,9 +198,10 @@ js_bindgen::embed_js!( " if (this.#memory.buffer instanceof ArrayBuffer)", " return true", "", - " const array = new WebAssembly.Memory({{ initial: 0, maximum: 0, shared: true }})", " try {{", - " new TextDecoder().decode(array)", + " const view = new Uint8Array(this.#memory.buffer, 0, 0)", + " new TextDecoder().decode(view)", + " new TextEncoder().encodeInto('', view)", " return true", " }} catch {{", " return false", diff --git a/client/js-sys/src/runtime/externref.rs b/client/js-sys/src/runtime/externref.rs index 30fcff8e..a97b20cd 100644 --- a/client/js-sys/src/runtime/externref.rs +++ b/client/js-sys/src/runtime/externref.rs @@ -1,7 +1,10 @@ use alloc::vec::Vec; use core::cell::RefCell; +use core::mem; use super::panic::panic; +use crate::JsValue; +use crate::hazard::JsCast; use crate::util::PtrConst; pub(crate) const WAT_TABLE_IMPORT: &str = "(import \"js_sys\" \"externref.table\" (table \ @@ -96,6 +99,7 @@ struct Slab { data: Vec, head: usize, base: usize, + table_len: usize, } impl Slab { @@ -104,6 +108,7 @@ impl Slab { data: Vec::new(), head: 0, base: 0, + table_len: 0, } } @@ -118,7 +123,7 @@ impl Slab { let slot = self.head; if slot == self.data.len() { let len = self.data.len(); - if len == self.data.capacity() { + if len == self.table_len { let additional = len.max(128); let first = grow(index_to_abi(additional)); if first == -1 { @@ -128,16 +133,17 @@ impl Slab { let first = index_from_abi(first); if self.base == 0 { self.base = first; - } else if self.base + self.data.len() != first { + } else if self.base + self.table_len != first { panic("non-contiguous `externref` table growth"); } if self.data.try_reserve_exact(additional).is_err() { panic("`externref` slab allocation failure"); } + self.table_len += additional; } - if self.data.len() >= self.data.capacity() { + if self.data.len() >= self.table_len { panic("`externref` slab capacity mismatch"); } self.data.push(slot + 1); @@ -195,6 +201,22 @@ impl ReservedSlots { pub(crate) fn commit(mut self) { self.committed = true; } + + /// Moves the values stored in the reserved slots into an initialized slice, + /// dropping every replaced value through its Rust type. + pub(crate) fn replace(mut self, destination: &mut [T]) { + assert_eq!(self.slots.len(), destination.len()); + + // Popping in reverse preserves the original slot order without shifting + // the vector on every replacement. + for value in destination.iter_mut().rev() { + let index = self.slots.pop().unwrap(); + let old = mem::replace(value, T::unchecked_from(JsValue::new(index))); + drop(old); + } + + self.committed = true; + } } impl Drop for ReservedSlots { @@ -202,6 +224,7 @@ impl Drop for ReservedSlots { if !self.committed { let mut slab = EXTERNREF_SLAB.0.borrow_mut(); for &index in &self.slots { + remove(index); slab.dealloc(index_from_abi(index)); } } diff --git a/client/js-sys/src/runtime/future/queue.rs b/client/js-sys/src/runtime/future/queue.rs index a1decc39..c41d8766 100644 --- a/client/js-sys/src/runtime/future/queue.rs +++ b/client/js-sys/src/runtime/future/queue.rs @@ -7,7 +7,7 @@ use super::task::Task; js_bindgen::embed_js!( module = "js_sys", name = "future.schedule", - "() => globalThis.queueMicrotask(() => this.#jsExports.future_poll())", + "() => globalThis.queueMicrotask(this.#jsExports.future_poll)", ); #[crate::js_sys(js_sys = crate)] diff --git a/client/js-sys/src/runtime/future/task/atomic.rs b/client/js-sys/src/runtime/future/task/atomic.rs index 97c98281..92b52d10 100644 --- a/client/js-sys/src/runtime/future/task/atomic.rs +++ b/client/js-sys/src/runtime/future/task/atomic.rs @@ -3,10 +3,9 @@ use alloc::rc::Rc; use alloc::sync::Arc; use core::cell::RefCell; use core::future::Future; -use core::mem::ManuallyDrop; use core::pin::Pin; use core::sync::atomic::{AtomicI32, Ordering}; -use core::task::{Context, RawWaker, RawWakerVTable, Waker}; +use core::task::{Context, Waker}; use super::ClearOnUnwind; use crate::Closure; @@ -103,44 +102,22 @@ impl Wake { }) } - fn wake_by_ref(&self) { + fn signal(&self) { if self.state.swap(AWAKE, Ordering::SeqCst) == AWAKE { return; } notify(PtrConst::from_ref(&self.state)); } +} - unsafe fn raw_waker(this: Arc) -> RawWaker { - unsafe fn clone(pointer: *const ()) -> RawWaker { - // SAFETY: Every pointer in this table comes from `Arc::into_raw`. - let wake = ManuallyDrop::new(unsafe { Arc::from_raw(pointer.cast::()) }); - // SAFETY: The clone becomes the ownership represented by the new - // `RawWaker`. - unsafe { Wake::raw_waker(Arc::clone(&wake)) } - } - - unsafe fn wake(pointer: *const ()) { - // SAFETY: `wake` consumes the ownership represented by this `Waker`. - let wake = unsafe { Arc::from_raw(pointer.cast::()) }; - wake.wake_by_ref(); - } - - unsafe fn wake_by_ref(pointer: *const ()) { - // SAFETY: `wake_by_ref` borrows the ownership represented by this - // `Waker`. - let wake = ManuallyDrop::new(unsafe { Arc::from_raw(pointer.cast::()) }); - wake.wake_by_ref(); - } - - unsafe fn drop(pointer: *const ()) { - // SAFETY: `drop` consumes the ownership represented by this `Waker`. - core::mem::drop(unsafe { Arc::from_raw(pointer.cast::()) }); - } - - const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); +impl alloc::task::Wake for Wake { + fn wake(self: Arc) { + self.signal(); + } - RawWaker::new(Arc::into_raw(this).cast(), &VTABLE) + fn wake_by_ref(self: &Arc) { + self.signal(); } } @@ -158,8 +135,7 @@ pub(in crate::runtime::future) struct Task { impl Task { pub(super) fn spawn(future: impl Future + 'static) { let wake = Wake::new(); - // SAFETY: The raw `Waker` owns the cloned, thread-safe `Arc`. - let waker = unsafe { Waker::from_raw(Wake::raw_waker(Arc::clone(&wake))) }; + let waker = Waker::from(Arc::clone(&wake)); let task = Rc::new(Self { state: RefCell::new(None), wake, @@ -168,7 +144,7 @@ impl Task { let resume = crate::closure!(js_sys = crate, dyn FnMut(), move || { // A delayed notification from the preceding wait may arrive after a // new wait starts. Normalize the state before polling in either case. - resumed_task.wake.wake_by_ref(); + resumed_task.wake.signal(); resumed_task.run(); }); *task.state.borrow_mut() = Some(TaskState { diff --git a/client/js-sys/src/runtime/panic.rs b/client/js-sys/src/runtime/panic.rs index e07db521..b9fda854 100644 --- a/client/js-sys/src/runtime/panic.rs +++ b/client/js-sys/src/runtime/panic.rs @@ -1,5 +1,3 @@ -#[cfg(not(debug_assertions))] -use alloc::format; #[cfg(all(not(debug_assertions), target_arch = "wasm32"))] use core::arch::wasm32 as wasm; #[cfg(all(not(debug_assertions), target_arch = "wasm64"))] @@ -58,16 +56,14 @@ impl UnwrapThrowExt for Result { fn expect_throw(self, message: &str) -> T { match self { Ok(value) => value, - Err(error) => panic(&format!("{message}: {error:?}")), + Err(_) => panic(message), } } fn unwrap_throw(self) -> T { match self { Ok(value) => value, - Err(error) => panic(&format!( - "called `Result::unwrap()` on an `Err` value: {error:?}" - )), + Err(_) => panic("called `Result::unwrap()` on an `Err` value"), } } } diff --git a/client/js-sys/src/runtime/value.rs b/client/js-sys/src/runtime/value.rs index a0d5b897..9ece6803 100644 --- a/client/js-sys/src/runtime/value.rs +++ b/client/js-sys/src/runtime/value.rs @@ -115,19 +115,19 @@ impl JsValue { } } - pub fn from_slice(slice: &[T]) -> &[Self] { + pub(crate) fn from_slice(slice: &[T]) -> &[Self] { let ptr: *const Self = slice.as_ptr().cast(); // SAFETY: `JsCast` assumes that `T` is `#[transparent]` over a `JsValue`. unsafe { slice::from_raw_parts(ptr, slice.len()) } } - pub fn from_slice_mut(slice: &mut [T]) -> &mut [Self] { + pub(crate) fn from_slice_mut(slice: &mut [T]) -> &mut [Self] { let ptr: *mut Self = slice.as_mut_ptr().cast(); // SAFETY: `JsCast` assumes that `T` is `#[transparent]` over a `JsValue`. unsafe { slice::from_raw_parts_mut(ptr, slice.len()) } } - pub fn from_uninit_slice_mut( + pub(crate) fn from_uninit_slice_mut( slice: &mut [MaybeUninit], ) -> &mut [MaybeUninit] { let ptr: *mut MaybeUninit = slice.as_mut_ptr().cast(); @@ -177,7 +177,7 @@ impl Clone for JsValue { impl Drop for JsValue { #[inline] fn drop(&mut self) { - if self.index > 1 { + if u32::from_ne_bytes(self.index.to_ne_bytes()) >= 2 { release(self.index); } } @@ -281,5 +281,3 @@ impl PartialEq for JsValue { js_value_partial_eq(self, other) } } - -impl Eq for JsValue {} diff --git a/client/js-sys/tests/array.rs b/client/js-sys/tests/array.rs index 897c8bc3..94f6c5f3 100644 --- a/client/js-sys/tests/array.rs +++ b/client/js-sys/tests/array.rs @@ -1,9 +1,19 @@ use core::array; use js_bindgen_test::test; -use js_sys::{JsArray, JsValue, js_sys}; +use js_sys::{JsArray, JsString, JsValue, js_sys}; js_bindgen::embed_js!(module = "array", name = "test", "(value) => value"); +js_bindgen::embed_js!( + module = "array", + name = "throwing", + "(value, len) => new Proxy(new Array(len).fill(value), {{", + " get(target, property) {{", + " if (property === '1') throw new Error('boom')", + " return target[property]", + " }}", + "}})", +); #[test] fn js_value() { @@ -11,6 +21,9 @@ fn js_value() { extern "js-sys" { #[js_sys(js_embed = "test")] fn js(value: &[JsValue]) -> JsArray; + + #[js_sys(js_embed = "throwing")] + fn throwing(value: &JsValue, len: u32) -> JsArray; } let rust_array = [JsValue::UNDEFINED; 42]; @@ -23,6 +36,16 @@ fn js_value() { let mut wrong_length = [JsValue::UNDEFINED; 41]; assert!(js_array.to_slice(&mut wrong_length).is_err()); + let previous = JsString::from("previous"); + let previous: JsValue = previous.into(); + let mut destination: [JsValue; 42] = array::from_fn(|_| previous.clone()); + let throwing = throwing(&JsValue::NULL, 42); + assert!(throwing.to_slice(&mut destination).is_err()); + assert!(destination.iter().all(|value| value == &previous)); + + js_array.to_slice(&mut destination).unwrap(); + assert_eq!(rust_array, destination); + let returned_array: [JsValue; 42] = js_array.to_array().unwrap(); assert_eq!(rust_array, returned_array); diff --git a/client/js-sys/tests/value.rs b/client/js-sys/tests/value.rs index 2a104404..0a2cc050 100644 --- a/client/js-sys/tests/value.rs +++ b/client/js-sys/tests/value.rs @@ -1,5 +1,13 @@ use js_bindgen_test::test; -use js_sys::{JsString, JsValue}; +use js_sys::{JsString, JsValue, js_sys}; + +js_bindgen::embed_js!(module = "value", name = "nan", "() => NaN"); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "nan")] + fn nan() -> JsValue; +} #[test] fn undefined() { @@ -26,6 +34,12 @@ fn clone() { assert_eq!(value, "Hello, World!"); } +#[test] +fn strict_equality_is_not_reflexive() { + let value = nan(); + assert!(!PartialEq::eq(&value, &value)); +} + #[test] fn many_live_values() { let value = JsString::from("Hello, World!"); From 980d5bc6ff1dfa479fa540e08c19d3d77fc69315 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:06:29 +0800 Subject: [PATCH 13/21] Keep atomic waits alive --- host/dev/src/client/e2e.rs | 11 ++++++++--- host/runner/src/js/bun/bun.mjs | 5 +++-- host/runner/src/js/bun/bun.mts | 5 +++-- host/runner/src/js/deno/deno.mjs | 5 +++-- host/runner/src/js/deno/deno.mts | 5 +++-- host/runner/src/js/node-js/node-js.mjs | 5 +++-- host/runner/src/js/node-js/node-js.mts | 5 +++-- host/runner/src/js/shared/shared-terminal.mjs | 7 +++++++ host/runner/src/js/shared/shared-terminal.mts | 8 ++++++++ 9 files changed, 41 insertions(+), 15 deletions(-) diff --git a/host/dev/src/client/e2e.rs b/host/dev/src/client/e2e.rs index 6cbff86a..faaa2322 100644 --- a/host/dev/src/client/e2e.rs +++ b/host/dev/src/client/e2e.rs @@ -202,21 +202,26 @@ impl Example { self.name ), }; + // A pending `Atomics.waitAsync` does not keep a command-line event loop + // alive: https://github.com/denoland/deno/issues/15358. Keep the whole + // example alive instead of adding work to each Future suspension. let mut script = format!( "import {{ JsBindgen }} from './{}.mjs'\n\n{read}\nconst module = await \ WebAssembly.compile(bytes)\nconst {{ instance, exports }} = await new \ JsBindgen(module).instantiate()\n\nfunction assert(value, expression) {{\n if \ - (!value) throw new Error(`assertion failed: ${{expression}}`)\n}}\n", + (!value) throw new Error(`assertion failed: ${{expression}}`)\n}}\n\nconst timer = \ + globalThis.setInterval(() => {{}}, 0x7fffffff)\ntry {{\n", self.name ); for test in &self.tests { - script.push_str("\nassert("); + script.push_str(" assert("); script.push_str(test); script.push_str(", "); write!(script, "{test:?}").unwrap(); - script.push_str(")\n"); + script.push_str(")\n\n"); } + script.push_str("} finally {\n globalThis.clearInterval(timer)\n}\n"); script } diff --git a/host/runner/src/js/bun/bun.mjs b/host/runner/src/js/bun/bun.mjs index 1c9a6541..9ded48b6 100644 --- a/host/runner/src/js/bun/bun.mjs +++ b/host/runner/src/js/bun/bun.mjs @@ -1,5 +1,5 @@ import { run } from "../shared/shared.mjs"; -import { colorText } from "../shared/shared-terminal.mjs"; +import { colorText, keepAlive } from "../shared/shared-terminal.mjs"; import { JsBindgen } from "../imports.mjs"; const wasmFile = Bun.file(new URL("../wasm.wasm", import.meta.url)); const wasmResponse = new Response(wasmFile, { @@ -7,12 +7,13 @@ const wasmResponse = new Response(wasmFile, { }); const module = await WebAssembly.compileStreaming(wasmResponse); let pendingWrite = Promise.resolve(); -const status = await run(module, JsBindgen, (stream, text) => { +const runPromise = run(module, JsBindgen, (stream, text) => { const output = colorText(text); const destination = stream === 0 /* Stream.Stdout */ ? Bun.stdout : Bun.stderr; pendingWrite = pendingWrite.then(async () => { await Bun.write(destination, output); }); }); +const status = await keepAlive(runPromise); await pendingWrite; process.exit(status); diff --git a/host/runner/src/js/bun/bun.mts b/host/runner/src/js/bun/bun.mts index 8834edaf..0cada130 100644 --- a/host/runner/src/js/bun/bun.mts +++ b/host/runner/src/js/bun/bun.mts @@ -1,5 +1,5 @@ import { Stream, run } from "../shared/shared.mjs" -import { colorText } from "../shared/shared-terminal.mjs" +import { colorText, keepAlive } from "../shared/shared-terminal.mjs" import { JsBindgen } from "../imports.mts" const wasmFile = Bun.file(new URL("../wasm.wasm", import.meta.url)) @@ -9,7 +9,7 @@ const wasmResponse = new Response(wasmFile, { const module = await WebAssembly.compileStreaming(wasmResponse) let pendingWrite = Promise.resolve() -const status = await run(module, JsBindgen, (stream, text) => { +const runPromise = run(module, JsBindgen, (stream, text) => { const output = colorText(text) const destination = stream === Stream.Stdout ? Bun.stdout : Bun.stderr @@ -17,6 +17,7 @@ const status = await run(module, JsBindgen, (stream, text) => { await Bun.write(destination, output) }) }) +const status = await keepAlive(runPromise) await pendingWrite process.exit(status) diff --git a/host/runner/src/js/deno/deno.mjs b/host/runner/src/js/deno/deno.mjs index d294486f..558c8219 100644 --- a/host/runner/src/js/deno/deno.mjs +++ b/host/runner/src/js/deno/deno.mjs @@ -1,8 +1,8 @@ import { run } from "../shared/shared.mjs"; -import { colorText } from "../shared/shared-terminal.mjs"; +import { colorText, keepAlive } from "../shared/shared-terminal.mjs"; import { JsBindgen } from "../imports.mjs"; const module = await WebAssembly.compileStreaming(fetch(new URL("../wasm.wasm", import.meta.url))); -const status = await run(module, JsBindgen, (stream, text) => { +const runPromise = run(module, JsBindgen, (stream, text) => { function printSync(input, to) { let bytesWritten = 0; const bytes = new TextEncoder().encode(input); @@ -19,4 +19,5 @@ const status = await run(module, JsBindgen, (stream, text) => { printSync(output, Deno.stderr); } }); +const status = await keepAlive(runPromise); Deno.exit(status); diff --git a/host/runner/src/js/deno/deno.mts b/host/runner/src/js/deno/deno.mts index 2a0b9400..4e020ec2 100644 --- a/host/runner/src/js/deno/deno.mts +++ b/host/runner/src/js/deno/deno.mts @@ -1,10 +1,10 @@ import { run, Stream } from "../shared/shared.mjs" -import { colorText } from "../shared/shared-terminal.mjs" +import { colorText, keepAlive } from "../shared/shared-terminal.mjs" import { JsBindgen } from "../imports.mts" const module = await WebAssembly.compileStreaming(fetch(new URL("../wasm.wasm", import.meta.url))) -const status = await run(module, JsBindgen, (stream, text) => { +const runPromise = run(module, JsBindgen, (stream, text) => { function printSync(input: string, to: typeof Deno.stdout | typeof Deno.stderr) { let bytesWritten = 0 const bytes = new TextEncoder().encode(input) @@ -24,5 +24,6 @@ const status = await run(module, JsBindgen, (stream, text) => { printSync(output, Deno.stderr) } }) +const status = await keepAlive(runPromise) Deno.exit(status) diff --git a/host/runner/src/js/node-js/node-js.mjs b/host/runner/src/js/node-js/node-js.mjs index 19c458e9..6f92ddcd 100644 --- a/host/runner/src/js/node-js/node-js.mjs +++ b/host/runner/src/js/node-js/node-js.mjs @@ -1,6 +1,6 @@ import { open } from "node:fs/promises"; import { run } from "../shared/shared.mjs"; -import { colorText } from "../shared/shared-terminal.mjs"; +import { colorText, keepAlive } from "../shared/shared-terminal.mjs"; import { JsBindgen } from "../imports.mjs"; const wasmFile = await open(new URL("../wasm.wasm", import.meta.url)); const wasmResponse = new Response( @@ -9,7 +9,7 @@ wasmFile.createReadStream(), { headers: { "Content-Type": "application/wasm" }, }); const module = await WebAssembly.compileStreaming(wasmResponse); -const status = await run(module, JsBindgen, (stream, text) => { +const runPromise = run(module, JsBindgen, (stream, text) => { const output = colorText(text); switch (stream) { case 0 /* Stream.Stdout */: @@ -19,4 +19,5 @@ const status = await run(module, JsBindgen, (stream, text) => { process.stderr.write(output); } }); +const status = await keepAlive(runPromise); process.exit(status); diff --git a/host/runner/src/js/node-js/node-js.mts b/host/runner/src/js/node-js/node-js.mts index 061c3cc3..5d7df61b 100644 --- a/host/runner/src/js/node-js/node-js.mts +++ b/host/runner/src/js/node-js/node-js.mts @@ -1,6 +1,6 @@ import { open } from "node:fs/promises" import { Stream, run } from "../shared/shared.mjs" -import { colorText } from "../shared/shared-terminal.mjs" +import { colorText, keepAlive } from "../shared/shared-terminal.mjs" import { JsBindgen } from "../imports.mts" const wasmFile = await open(new URL("../wasm.wasm", import.meta.url)) @@ -13,7 +13,7 @@ const wasmResponse = new Response( ) const module = await WebAssembly.compileStreaming(wasmResponse) -const status = await run(module, JsBindgen, (stream, text) => { +const runPromise = run(module, JsBindgen, (stream, text) => { const output = colorText(text) switch (stream) { @@ -24,5 +24,6 @@ const status = await run(module, JsBindgen, (stream, text) => { process.stderr.write(output) } }) +const status = await keepAlive(runPromise) process.exit(status) diff --git a/host/runner/src/js/shared/shared-terminal.mjs b/host/runner/src/js/shared/shared-terminal.mjs index 0481d4a2..c7a0b28a 100644 --- a/host/runner/src/js/shared/shared-terminal.mjs +++ b/host/runner/src/js/shared/shared-terminal.mjs @@ -1,3 +1,10 @@ +export function keepAlive(promise) { + // A pending `Atomics.waitAsync` does not keep a command-line event loop + // alive: https://github.com/denoland/deno/issues/15358. Keep one timer active + // until the complete test or binary run settles. + const timer = globalThis.setInterval(() => undefined, 0x7fffffff); + return promise.finally(() => globalThis.clearInterval(timer)); +} export function colorText(text) { const green = "\u001b[32m"; const yellow = "\u001b[33m"; diff --git a/host/runner/src/js/shared/shared-terminal.mts b/host/runner/src/js/shared/shared-terminal.mts index efd1eb2d..ce8729ec 100644 --- a/host/runner/src/js/shared/shared-terminal.mts +++ b/host/runner/src/js/shared/shared-terminal.mts @@ -1,5 +1,13 @@ import { Color, type StyledText } from "./shared.mts" +export function keepAlive(promise: Promise): Promise { + // A pending `Atomics.waitAsync` does not keep a command-line event loop + // alive: https://github.com/denoland/deno/issues/15358. Keep one timer active + // until the complete test or binary run settles. + const timer = globalThis.setInterval(() => undefined, 0x7fffffff) + return promise.finally(() => globalThis.clearInterval(timer)) +} + export function colorText(text: StyledText[]): string { const green = "\u001b[32m" const yellow = "\u001b[33m" From 2fd20854a84368509199ab4cf689d7d2867be003 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:06:29 +0800 Subject: [PATCH 14/21] Add JavaScript built-in objects --- benchmarks/Cargo.lock | 30 - benchmarks/README.md | 10 + benchmarks/bench.mjs | 27 +- benchmarks/js-bindgen/src/lib.rs | 161 ++- benchmarks/wasm-bindgen/src/lib.rs | 161 ++- client/e2e/examples/closure.rs | 28 +- client/e2e/examples/primitive.rs | 14 +- client/e2e/examples/string.rs | 48 +- client/e2e/examples/vec.rs | 75 + client/js-sys/src/builtins/array.rs | 882 +++++++----- client/js-sys/src/builtins/array_buffer.rs | 153 ++ .../src/builtins/async_disposable_stack.rs | 72 + client/js-sys/src/builtins/atomics.rs | 680 +++++++++ client/js-sys/src/builtins/bigint.rs | 59 +- client/js-sys/src/builtins/boolean.rs | 38 + client/js-sys/src/builtins/data_view.rs | 284 ++++ client/js-sys/src/builtins/date.rs | 530 +++++++ .../js-sys/src/builtins/disposable_stack.rs | 71 + .../js-sys/src/builtins/dynamic_function.rs | 176 +++ client/js-sys/src/builtins/error.rs | 140 +- .../src/builtins/finalization_registry.rs | 36 + client/js-sys/src/builtins/generator.rs | 110 ++ client/js-sys/src/builtins/global.rs | 59 + client/js-sys/src/builtins/intl/collator.rs | 326 +++++ .../src/builtins/intl/date_time_format.rs | 883 ++++++++++++ .../js-sys/src/builtins/intl/display_names.rs | 290 ++++ .../src/builtins/intl/duration_format.rs | 562 ++++++++ .../js-sys/src/builtins/intl/list_format.rs | 229 +++ client/js-sys/src/builtins/intl/locale.rs | 489 +++++++ client/js-sys/src/builtins/intl/mod.rs | 147 ++ .../js-sys/src/builtins/intl/number_format.rs | 1029 +++++++++++++ .../js-sys/src/builtins/intl/plural_rules.rs | 533 +++++++ .../src/builtins/intl/relative_time_format.rs | 301 ++++ client/js-sys/src/builtins/intl/segmenter.rs | 217 +++ client/js-sys/src/builtins/iterator.rs | 752 ++++++++++ client/js-sys/src/builtins/json.rs | 43 + client/js-sys/src/builtins/map.rs | 163 +++ client/js-sys/src/builtins/math.rs | 258 ++++ client/js-sys/src/builtins/mod.rs | 81 +- client/js-sys/src/builtins/number.rs | 132 +- client/js-sys/src/builtins/object.rs | 236 ++- client/js-sys/src/builtins/proxy.rs | 33 + client/js-sys/src/builtins/reflect.rs | 143 ++ client/js-sys/src/builtins/regexp.rs | 270 ++++ client/js-sys/src/builtins/set.rs | 161 +++ client/js-sys/src/builtins/string.rs | 574 +++++--- client/js-sys/src/builtins/symbol.rs | 142 ++ .../js-sys/src/builtins/temporal/duration.rs | 268 ++++ .../js-sys/src/builtins/temporal/instant.rs | 119 ++ client/js-sys/src/builtins/temporal/mod.rs | 23 + client/js-sys/src/builtins/temporal/now.rs | 62 + .../src/builtins/temporal/plain_date.rs | 245 ++++ .../src/builtins/temporal/plain_date_time.rs | 380 +++++ .../src/builtins/temporal/plain_month_day.rs | 112 ++ .../src/builtins/temporal/plain_time.rs | 192 +++ .../src/builtins/temporal/plain_year_month.rs | 199 +++ .../src/builtins/temporal/zoned_date_time.rs | 337 +++++ client/js-sys/src/builtins/typed_array.rs | 1278 +++++++++++++++++ client/js-sys/src/builtins/uint8_array.rs | 177 +++ client/js-sys/src/builtins/weak_map.rs | 99 ++ client/js-sys/src/builtins/weak_ref.rs | 39 + client/js-sys/src/builtins/weak_set.rs | 67 + .../src/builtins/webassembly/address.rs | 28 + .../js-sys/src/builtins/webassembly/error.rs | 47 + .../src/builtins/webassembly/exception.rs | 127 ++ .../js-sys/src/builtins/webassembly/global.rs | 134 ++ .../src/builtins/webassembly/instance.rs | 23 + .../js-sys/src/builtins/webassembly/jspi.rs | 16 + .../js-sys/src/builtins/webassembly/memory.rs | 118 ++ client/js-sys/src/builtins/webassembly/mod.rs | 33 + .../js-sys/src/builtins/webassembly/module.rs | 112 ++ .../src/builtins/webassembly/namespace.rs | 188 +++ .../js-sys/src/builtins/webassembly/table.rs | 192 +++ client/js-sys/src/hazard.rs | 32 +- client/js-sys/src/interop/js/array.rs | 570 ++++++++ .../js-sys/src/interop/js/async_iterator.rs | 126 ++ client/js-sys/src/interop/js/iterator.rs | 325 +++++ client/js-sys/src/interop/js/mod.rs | 10 + client/js-sys/src/interop/js/string.rs | 134 ++ client/js-sys/src/interop/js/typed_array.rs | 722 ++++++++++ client/js-sys/src/interop/mod.rs | 8 + client/js-sys/src/interop/primitive.rs | 381 ++--- client/js-sys/src/interop/slice.rs | 260 ++++ client/js-sys/src/interop/string.rs | 461 +++++- client/js-sys/src/interop/vec.rs | 597 ++++++++ client/js-sys/src/lib.rs | 23 +- client/js-sys/src/macro.rs | 7 +- client/js-sys/src/macro/abi.rs | 58 +- client/js-sys/src/macro/export.rs | 323 +---- client/js-sys/src/macro/export/js.rs | 430 ++++++ client/js-sys/src/macro/import.rs | 19 +- client/js-sys/src/macro/import/js.rs | 221 +-- client/js-sys/src/macro/import/wat.rs | 54 +- client/js-sys/src/macro/result.rs | 4 +- client/js-sys/src/macro/text.rs | 29 +- client/js-sys/src/macro/wat.rs | 4 +- .../js-sys/src/macro/{import => }/writer.rs | 2 +- client/js-sys/src/runtime/allocator.rs | 79 + client/js-sys/src/runtime/closure.rs | 174 ++- client/js-sys/src/runtime/externref.rs | 55 +- .../js-sys/src/runtime/future/jspi/atomic.rs | 48 +- .../js-sys/src/runtime/future/jspi/single.rs | 14 +- client/js-sys/src/runtime/future/mod.rs | 18 +- .../js-sys/src/runtime/future/task/atomic.rs | 90 +- client/js-sys/src/runtime/mod.rs | 1 + client/js-sys/src/util.rs | 167 ++- client/js-sys/tests/array.rs | 316 +++- client/js-sys/tests/future.rs | 45 +- client/js-sys/tests/hazard.rs | 19 +- client/js-sys/tests/iterator.rs | 603 ++++++++ client/js-sys/tests/numeric.rs | 32 +- client/js-sys/tests/optional.rs | 16 +- client/js-sys/tests/string.rs | 104 +- client/js-sys/tests/typed_array.rs | 221 +++ client/js-sys/tests/value.rs | 33 +- client/js-sys/tests/vec.rs | 219 +++ client/web-sys/src/console.gen.rs | 2 +- host/cli-lib/src/js/imports.mjs | 5 +- host/cli-lib/src/js/imports.mts | 7 +- host/js-sys-bindgen/Cargo.toml | 13 +- host/js-sys-bindgen/src/function.rs | 190 ++- host/js-sys-bindgen/src/function/js.rs | 50 +- host/js-sys-bindgen/src/function/options.rs | 136 +- host/js-sys-bindgen/src/hygiene.rs | 32 +- host/js-sys-bindgen/src/macro.rs | 2 +- host/js-sys-bindgen/src/tests/closure.rs | 11 +- host/js-sys-bindgen/src/tests/macro/export.rs | 554 +------ .../src/tests/macro/function.rs | 1020 ++----------- host/js-sys-bindgen/src/tests/macro/member.rs | 554 +------ host/js-sys-bindgen/src/tests/macro/mod.rs | 60 - host/js-sys-bindgen/src/tests/macro/type.rs | 261 +--- host/js-sys-bindgen/src/tests/mod.rs | 2 +- host/js-sys-bindgen/src/tests/type.rs | 182 --- host/js-sys-bindgen/src/type.rs | 2 +- host/shared/src/web_driver/mod.rs | 9 +- web/playground/src/main.rs | 6 +- 136 files changed, 22558 insertions(+), 4047 deletions(-) create mode 100644 client/e2e/examples/vec.rs create mode 100644 client/js-sys/src/builtins/array_buffer.rs create mode 100644 client/js-sys/src/builtins/async_disposable_stack.rs create mode 100644 client/js-sys/src/builtins/atomics.rs create mode 100644 client/js-sys/src/builtins/boolean.rs create mode 100644 client/js-sys/src/builtins/data_view.rs create mode 100644 client/js-sys/src/builtins/date.rs create mode 100644 client/js-sys/src/builtins/disposable_stack.rs create mode 100644 client/js-sys/src/builtins/dynamic_function.rs create mode 100644 client/js-sys/src/builtins/finalization_registry.rs create mode 100644 client/js-sys/src/builtins/generator.rs create mode 100644 client/js-sys/src/builtins/global.rs create mode 100644 client/js-sys/src/builtins/intl/collator.rs create mode 100644 client/js-sys/src/builtins/intl/date_time_format.rs create mode 100644 client/js-sys/src/builtins/intl/display_names.rs create mode 100644 client/js-sys/src/builtins/intl/duration_format.rs create mode 100644 client/js-sys/src/builtins/intl/list_format.rs create mode 100644 client/js-sys/src/builtins/intl/locale.rs create mode 100644 client/js-sys/src/builtins/intl/mod.rs create mode 100644 client/js-sys/src/builtins/intl/number_format.rs create mode 100644 client/js-sys/src/builtins/intl/plural_rules.rs create mode 100644 client/js-sys/src/builtins/intl/relative_time_format.rs create mode 100644 client/js-sys/src/builtins/intl/segmenter.rs create mode 100644 client/js-sys/src/builtins/iterator.rs create mode 100644 client/js-sys/src/builtins/json.rs create mode 100644 client/js-sys/src/builtins/map.rs create mode 100644 client/js-sys/src/builtins/math.rs create mode 100644 client/js-sys/src/builtins/proxy.rs create mode 100644 client/js-sys/src/builtins/reflect.rs create mode 100644 client/js-sys/src/builtins/regexp.rs create mode 100644 client/js-sys/src/builtins/set.rs create mode 100644 client/js-sys/src/builtins/symbol.rs create mode 100644 client/js-sys/src/builtins/temporal/duration.rs create mode 100644 client/js-sys/src/builtins/temporal/instant.rs create mode 100644 client/js-sys/src/builtins/temporal/mod.rs create mode 100644 client/js-sys/src/builtins/temporal/now.rs create mode 100644 client/js-sys/src/builtins/temporal/plain_date.rs create mode 100644 client/js-sys/src/builtins/temporal/plain_date_time.rs create mode 100644 client/js-sys/src/builtins/temporal/plain_month_day.rs create mode 100644 client/js-sys/src/builtins/temporal/plain_time.rs create mode 100644 client/js-sys/src/builtins/temporal/plain_year_month.rs create mode 100644 client/js-sys/src/builtins/temporal/zoned_date_time.rs create mode 100644 client/js-sys/src/builtins/typed_array.rs create mode 100644 client/js-sys/src/builtins/uint8_array.rs create mode 100644 client/js-sys/src/builtins/weak_map.rs create mode 100644 client/js-sys/src/builtins/weak_ref.rs create mode 100644 client/js-sys/src/builtins/weak_set.rs create mode 100644 client/js-sys/src/builtins/webassembly/address.rs create mode 100644 client/js-sys/src/builtins/webassembly/error.rs create mode 100644 client/js-sys/src/builtins/webassembly/exception.rs create mode 100644 client/js-sys/src/builtins/webassembly/global.rs create mode 100644 client/js-sys/src/builtins/webassembly/instance.rs create mode 100644 client/js-sys/src/builtins/webassembly/jspi.rs create mode 100644 client/js-sys/src/builtins/webassembly/memory.rs create mode 100644 client/js-sys/src/builtins/webassembly/mod.rs create mode 100644 client/js-sys/src/builtins/webassembly/module.rs create mode 100644 client/js-sys/src/builtins/webassembly/namespace.rs create mode 100644 client/js-sys/src/builtins/webassembly/table.rs create mode 100644 client/js-sys/src/interop/js/array.rs create mode 100644 client/js-sys/src/interop/js/async_iterator.rs create mode 100644 client/js-sys/src/interop/js/iterator.rs create mode 100644 client/js-sys/src/interop/js/mod.rs create mode 100644 client/js-sys/src/interop/js/string.rs create mode 100644 client/js-sys/src/interop/js/typed_array.rs create mode 100644 client/js-sys/src/interop/slice.rs create mode 100644 client/js-sys/src/interop/vec.rs create mode 100644 client/js-sys/src/macro/export/js.rs rename client/js-sys/src/macro/{import => }/writer.rs (96%) create mode 100644 client/js-sys/src/runtime/allocator.rs create mode 100644 client/js-sys/tests/iterator.rs create mode 100644 client/js-sys/tests/typed_array.rs create mode 100644 client/js-sys/tests/vec.rs delete mode 100644 host/js-sys-bindgen/src/tests/type.rs diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock index ff57fbd3..73065ca8 100644 --- a/benchmarks/Cargo.lock +++ b/benchmarks/Cargo.lock @@ -14,18 +14,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "futures-core" version = "0.3.33" @@ -50,21 +38,6 @@ dependencies = [ "slab", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "itertools" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" -dependencies = [ - "either", -] - [[package]] name = "js-bindgen" version = "0.0.0" @@ -106,9 +79,6 @@ dependencies = [ name = "js-sys-bindgen" version = "0.1.0" dependencies = [ - "foldhash", - "hashbrown", - "itertools", "proc-macro2", "quote", "syn", diff --git a/benchmarks/README.md b/benchmarks/README.md index 93b54080..e16f37e7 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -7,6 +7,13 @@ npm install node bench.mjs ``` +Pass one or more case-insensitive substrings to run only matching benchmarks. Multiple filters are +combined with `OR`: + +```console +node bench.mjs vec_u8 option_i32 +``` + The comparison uses the in-tree `js-bindgen` and the exact published `wasm-bindgen` version pinned in `Cargo.toml`. The matching `wasm-bindgen` CLI must be available on `PATH`. @@ -14,6 +21,9 @@ Warmup, batching, sampling, and statistics are handled by `mitata`. Every implem benchmark runs in a fresh process. The parent process aggregates the results after both implementations finish, preventing one benchmark's JIT and GC state from affecting another. +The runner recreates the ignored `generated` directory on every invocation. Rust build artifacts +remain in `target` so subsequent filtered runs only rebuild changed inputs. + Benchmark functions are discovered from raw Wasm exports whose names start with `bench_`. To add a benchmark, export the same `bench_*` function from both Rust crates. The runner infers Number, BigInt, and reference parameters before measurement and gives every benchmark its own Wasm instance. diff --git a/benchmarks/bench.mjs b/benchmarks/bench.mjs index 9effc00b..9989d6a3 100644 --- a/benchmarks/bench.mjs +++ b/benchmarks/bench.mjs @@ -189,6 +189,11 @@ function compareBenchmarks(left, right) { return left.localeCompare(right) } +function benchmarkDifference(expected, actual) { + const actualSet = new Set(actual) + return expected.filter(name => !actualSet.has(name)) +} + // Wasm functions expose their arity but not their parameter types. Start with // Number and retry the parameter that rejected it as BigInt. Parameters that // never coerce the probe are reference values. @@ -376,12 +381,12 @@ async function runWorker() { const { debug: _, samples: __, ...stats } = measurement.stats process.stdout.write( JSON.stringify({ - context: { - arch: result.context.arch, - cpu: result.context.cpu, - exceptionHandling, - runtime: result.context.runtime, - version: result.context.version, + context: { + arch: result.context.arch, + cpu: result.context.cpu, + exceptionHandling, + runtime: result.context.runtime, + version: result.context.version, }, implementation: implementation.name, asynchronous, @@ -517,8 +522,16 @@ async function runCoordinator() { for (let index = 1; index < discoveredBenchmarks.length; index++) { if (benchmarks.join("\n") !== discoveredBenchmarks[index].join("\n")) { + const missing = benchmarkDifference(benchmarks, discoveredBenchmarks[index]) + const extra = benchmarkDifference(discoveredBenchmarks[index], benchmarks) throw new Error( - `${implementations[index].name} exports do not match ${implementations[0].name}` + [ + `${implementations[index].name} exports do not match ${implementations[0].name}`, + missing.length === 0 ? undefined : `missing: ${missing.join(", ")}`, + extra.length === 0 ? undefined : `extra: ${extra.join(", ")}`, + ] + .filter(Boolean) + .join("\n") ) } } diff --git a/benchmarks/js-bindgen/src/lib.rs b/benchmarks/js-bindgen/src/lib.rs index b9092654..1f41f1b1 100644 --- a/benchmarks/js-bindgen/src/lib.rs +++ b/benchmarks/js-bindgen/src/lib.rs @@ -4,7 +4,9 @@ use core::hint::black_box; use core::pin::Pin; use core::task::{Context, Poll}; -use js_sys::{Closure, JsFuture, JsValue, Promise, closure, future_to_promise, js_sys}; +use js_sys::{ + Closure, JsFuture, JsValue, Promise, Uint32Array, closure, future_to_promise, js_sys, +}; js_sys::js_bindgen::embed_js!( module = "js_bindgen_benchmark", @@ -24,6 +26,12 @@ js_sys::js_bindgen::embed_js!( "(value) => value.length", ); +js_sys::js_bindgen::embed_js!( + module = "js_bindgen_benchmark", + name = "string", + "() => 'js-bindgen benchmark'", +); + js_sys::js_bindgen::embed_js!( module = "js_bindgen_benchmark", name = "invoke_closure", @@ -34,9 +42,9 @@ js_sys::js_bindgen::embed_js!( module = "js_bindgen_benchmark", name = "pending_promise", "() => {{", - " const {{ promise, resolve }} = Promise.withResolvers()", - " globalThis.queueMicrotask(resolve)", - " return promise", + " const {{ promise, resolve }} = Promise.withResolvers()", + " globalThis.queueMicrotask(resolve)", + " return promise", "}}", ); @@ -108,12 +116,42 @@ extern "js-sys" { #[js_sys(js_embed = "identity")] fn import_js_value_raw(value: JsValue) -> JsValue; + #[js_sys(js_embed = "identity")] + fn import_vec_js_value_raw(value: Vec) -> Vec; + + #[js_sys(js_embed = "identity")] + fn import_vec_u32_raw(value: Vec) -> Vec; + + #[js_sys(js_embed = "identity")] + fn import_vec_u8_raw(value: Vec) -> Vec; + + #[js_sys(js_embed = "identity")] + fn import_vec_u64_raw(value: Vec) -> Vec; + + #[js_sys(js_embed = "identity")] + fn import_vec_f64_raw(value: Vec) -> Vec; + + #[js_sys(js_embed = "identity")] + fn import_vec_string_raw(value: Vec) -> Vec; + #[js_sys(js_embed = "length")] fn import_str_raw(value: &str) -> u32; + #[js_sys(js_embed = "length")] + fn import_string_length_raw(value: String) -> u32; + + #[js_sys(js_embed = "string")] + fn import_string_raw() -> String; + + #[js_sys(js_embed = "identity")] + fn import_string_roundtrip_raw(value: String) -> String; + #[js_sys(js_embed = "length")] fn import_u32_slice_raw(value: &[u32]) -> u32; + #[js_sys(js_embed = "length")] + fn import_u64_slice_raw(value: &[u64]) -> u32; + #[js_sys(js_embed = "length")] fn import_js_value_slice_raw(value: &[JsValue]) -> u32; @@ -132,8 +170,14 @@ std::thread_local! { closure!(dyn FnMut(i32) -> i32, |value| value); static CALLBACK_U128: Closure u128> = closure!(dyn FnMut(u128) -> u128, |value| value); + static UINT32_ARRAY: Uint32Array = Uint32Array::from(&UINT32_VALUES); } +const UINT32_VALUES: [u32; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; +const UINT8_VALUES: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; +const UINT64_VALUES: [u64; 8] = [1, 2, 3, 4, 5, 6, 7, u64::MAX]; +const FLOAT64_VALUES: [f64; 8] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + #[js_sys] fn bench_closure_call(value: i32) -> i32 { CALLBACK.with(|callback| invoke_closure_raw(callback, value)) @@ -348,6 +392,39 @@ fn bench_export_js_value_alloc(value: JsValue) -> i32 { 512 } +#[js_sys] +fn bench_export_vec_js_value(value: JsValue) -> Vec { + vec![value] +} + +#[js_sys] +fn bench_export_vec_u32() -> Vec { + black_box(UINT32_VALUES).to_vec() +} + +#[js_sys] +fn bench_export_vec_u8() -> Vec { + black_box(UINT8_VALUES).to_vec() +} + +#[js_sys] +fn bench_export_vec_u64() -> Vec { + black_box(UINT64_VALUES).to_vec() +} + +#[js_sys] +fn bench_export_vec_f64() -> Vec { + black_box(FLOAT64_VALUES).to_vec() +} + +#[js_sys] +fn bench_export_vec_string() -> Vec { + black_box(["js", "bindgen", "benchmark", "🦀"]) + .into_iter() + .map(String::from) + .collect() +} + #[js_sys] fn bench_import_i32(value: i32) -> i32 { import_i32_raw(value) @@ -488,17 +565,91 @@ fn bench_import_js_value(value: JsValue) -> JsValue { import_js_value_raw(value) } +#[js_sys] +fn bench_import_vec_js_value(value: JsValue) -> usize { + import_vec_js_value_raw(vec![value]).len() +} + +#[js_sys] +fn bench_import_vec_u32() -> usize { + import_vec_u32_raw(black_box(UINT32_VALUES).to_vec()).len() +} + +#[js_sys] +fn bench_import_vec_u8() -> usize { + import_vec_u8_raw(black_box(UINT8_VALUES).to_vec()).len() +} + +#[js_sys] +fn bench_import_vec_u64() -> usize { + import_vec_u64_raw(black_box(UINT64_VALUES).to_vec()).len() +} + +#[js_sys] +fn bench_import_vec_f64() -> usize { + import_vec_f64_raw(black_box(FLOAT64_VALUES).to_vec()).len() +} + +#[js_sys] +fn bench_import_vec_string() -> usize { + let values = black_box(["js", "bindgen", "benchmark", "🦀"]) + .into_iter() + .map(String::from) + .collect(); + import_vec_string_raw(values).len() +} + #[js_sys] fn bench_import_str() -> u32 { import_str_raw(black_box("js-bindgen benchmark")) } +#[js_sys] +fn bench_import_string_to_js() -> u32 { + import_string_length_raw(String::from(black_box("js-bindgen benchmark"))) +} + +#[js_sys] +fn bench_import_string_from_js() -> usize { + import_string_raw().len() +} + +#[js_sys] +fn bench_import_string_roundtrip() -> usize { + import_string_roundtrip_raw(String::from(black_box("js-bindgen benchmark"))).len() +} + #[js_sys] fn bench_import_u32_slice() -> u32 { - import_u32_slice_raw(black_box(&[1, 2, 3, 4, 5, 6, 7, 8])) + import_u32_slice_raw(black_box(&UINT32_VALUES)) +} + +#[js_sys] +fn bench_import_u64_slice() -> u32 { + import_u64_slice_raw(black_box(&UINT64_VALUES)) } #[js_sys] fn bench_import_js_value_slice(value: JsValue) -> u32 { import_js_value_slice_raw(core::slice::from_ref(&value)) } + +#[js_sys] +fn bench_typed_array_copy_to_u32() -> u32 { + let mut output = [0; UINT32_VALUES.len()]; + UINT32_ARRAY.with(|array| array.copy_to(&mut output).unwrap()); + black_box(output)[output.len() - 1] +} + +#[js_sys] +fn bench_typed_array_copy_from_u32() -> usize { + UINT32_ARRAY.with(|array| { + array.copy_from(black_box(&UINT32_VALUES)).unwrap(); + array.length() as usize + }) +} + +#[js_sys] +fn bench_typed_array_from_u32() -> usize { + Uint32Array::from(black_box(&UINT32_VALUES)).length() as usize +} diff --git a/benchmarks/wasm-bindgen/src/lib.rs b/benchmarks/wasm-bindgen/src/lib.rs index 37acc598..9cad7562 100644 --- a/benchmarks/wasm-bindgen/src/lib.rs +++ b/benchmarks/wasm-bindgen/src/lib.rs @@ -4,7 +4,7 @@ use core::hint::black_box; use core::pin::Pin; use core::task::{Context, Poll}; -use js_sys::Promise; +use js_sys::{Promise, Uint32Array}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::{JsFuture, future_to_promise}; @@ -63,6 +63,24 @@ extern "C" { #[wasm_bindgen(js_name = identity)] fn import_js_value_raw(value: JsValue) -> JsValue; + + #[wasm_bindgen(js_name = identity)] + fn import_vec_js_value_raw(value: Vec) -> Vec; + + #[wasm_bindgen(js_name = identity)] + fn import_vec_u32_raw(value: Vec) -> Vec; + + #[wasm_bindgen(js_name = identity)] + fn import_vec_u8_raw(value: Vec) -> Vec; + + #[wasm_bindgen(js_name = identity)] + fn import_vec_u64_raw(value: Vec) -> Vec; + + #[wasm_bindgen(js_name = identity)] + fn import_vec_f64_raw(value: Vec) -> Vec; + + #[wasm_bindgen(js_name = identity)] + fn import_vec_string_raw(value: Vec) -> Vec; } #[wasm_bindgen(inline_js = "export function throw_value(value) { throw value; }")] @@ -85,13 +103,31 @@ extern "C" { #[wasm_bindgen(js_name = length)] fn import_str_raw(value: &str) -> u32; + #[wasm_bindgen(js_name = length)] + fn import_string_length_raw(value: String) -> u32; + #[wasm_bindgen(js_name = length)] fn import_u32_slice_raw(value: &[u32]) -> u32; + #[wasm_bindgen(js_name = length)] + fn import_u64_slice_raw(value: &[u64]) -> u32; + #[wasm_bindgen(js_name = length)] fn import_js_value_slice_raw(value: &[JsValue]) -> u32; } +#[wasm_bindgen(inline_js = "export function string() { return 'js-bindgen benchmark'; }")] +extern "C" { + #[wasm_bindgen(js_name = string)] + fn import_string_raw() -> String; +} + +#[wasm_bindgen(inline_js = "export function string_identity(value) { return value; }")] +extern "C" { + #[wasm_bindgen(js_name = string_identity)] + fn import_string_roundtrip_raw(value: String) -> String; +} + #[wasm_bindgen( inline_js = "export function invoke_closure(callback, value) { return callback(value); }" )] @@ -104,10 +140,10 @@ extern "C" { } #[wasm_bindgen(inline_js = "export function pending_promise() { - const { promise, resolve } = Promise.withResolvers(); - globalThis.queueMicrotask(resolve); - return promise; - }")] + const { promise, resolve } = Promise.withResolvers(); + globalThis.queueMicrotask(resolve); + return promise; +}")] extern "C" { fn pending_promise() -> Promise; } @@ -117,8 +153,14 @@ std::thread_local! { Closure::new(|value| value); static CALLBACK_U128: Closure u128> = Closure::new(|value| value); + static UINT32_ARRAY: Uint32Array = Uint32Array::from(UINT32_VALUES.as_slice()); } +const UINT32_VALUES: [u32; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; +const UINT8_VALUES: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; +const UINT64_VALUES: [u64; 8] = [1, 2, 3, 4, 5, 6, 7, u64::MAX]; +const FLOAT64_VALUES: [f64; 8] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + #[wasm_bindgen] pub fn bench_closure_call(value: i32) -> i32 { CALLBACK.with(|callback| invoke_closure_raw(callback, value)) @@ -333,6 +375,39 @@ pub fn bench_export_js_value_alloc(value: JsValue) -> i32 { 512 } +#[wasm_bindgen] +pub fn bench_export_vec_js_value(value: JsValue) -> Vec { + vec![value] +} + +#[wasm_bindgen] +pub fn bench_export_vec_u32() -> Vec { + black_box(UINT32_VALUES).to_vec() +} + +#[wasm_bindgen] +pub fn bench_export_vec_u8() -> Vec { + black_box(UINT8_VALUES).to_vec() +} + +#[wasm_bindgen] +pub fn bench_export_vec_u64() -> Vec { + black_box(UINT64_VALUES).to_vec() +} + +#[wasm_bindgen] +pub fn bench_export_vec_f64() -> Vec { + black_box(FLOAT64_VALUES).to_vec() +} + +#[wasm_bindgen] +pub fn bench_export_vec_string() -> Vec { + black_box(["js", "bindgen", "benchmark", "🦀"]) + .into_iter() + .map(String::from) + .collect() +} + #[wasm_bindgen] pub fn bench_import_i32(value: i32) -> i32 { import_i32_raw(value) @@ -473,17 +548,91 @@ pub fn bench_import_js_value(value: JsValue) -> JsValue { import_js_value_raw(value) } +#[wasm_bindgen] +pub fn bench_import_vec_js_value(value: JsValue) -> usize { + import_vec_js_value_raw(vec![value]).len() +} + +#[wasm_bindgen] +pub fn bench_import_vec_u32() -> usize { + import_vec_u32_raw(black_box(UINT32_VALUES).to_vec()).len() +} + +#[wasm_bindgen] +pub fn bench_import_vec_u8() -> usize { + import_vec_u8_raw(black_box(UINT8_VALUES).to_vec()).len() +} + +#[wasm_bindgen] +pub fn bench_import_vec_u64() -> usize { + import_vec_u64_raw(black_box(UINT64_VALUES).to_vec()).len() +} + +#[wasm_bindgen] +pub fn bench_import_vec_f64() -> usize { + import_vec_f64_raw(black_box(FLOAT64_VALUES).to_vec()).len() +} + +#[wasm_bindgen] +pub fn bench_import_vec_string() -> usize { + let values = black_box(["js", "bindgen", "benchmark", "🦀"]) + .into_iter() + .map(String::from) + .collect(); + import_vec_string_raw(values).len() +} + #[wasm_bindgen] pub fn bench_import_str() -> u32 { import_str_raw(black_box("js-bindgen benchmark")) } +#[wasm_bindgen] +pub fn bench_import_string_to_js() -> u32 { + import_string_length_raw(String::from(black_box("js-bindgen benchmark"))) +} + +#[wasm_bindgen] +pub fn bench_import_string_from_js() -> usize { + import_string_raw().len() +} + +#[wasm_bindgen] +pub fn bench_import_string_roundtrip() -> usize { + import_string_roundtrip_raw(String::from(black_box("js-bindgen benchmark"))).len() +} + #[wasm_bindgen] pub fn bench_import_u32_slice() -> u32 { - import_u32_slice_raw(black_box(&[1, 2, 3, 4, 5, 6, 7, 8])) + import_u32_slice_raw(black_box(&UINT32_VALUES)) +} + +#[wasm_bindgen] +pub fn bench_import_u64_slice() -> u32 { + import_u64_slice_raw(black_box(&UINT64_VALUES)) } #[wasm_bindgen] pub fn bench_import_js_value_slice(value: JsValue) -> u32 { import_js_value_slice_raw(core::slice::from_ref(&value)) } + +#[wasm_bindgen] +pub fn bench_typed_array_copy_to_u32() -> u32 { + let mut output = [0; UINT32_VALUES.len()]; + UINT32_ARRAY.with(|array| array.copy_to(&mut output)); + black_box(output)[output.len() - 1] +} + +#[wasm_bindgen] +pub fn bench_typed_array_copy_from_u32() -> usize { + UINT32_ARRAY.with(|array| { + array.copy_from(black_box(&UINT32_VALUES)); + array.length() as usize + }) +} + +#[wasm_bindgen] +pub fn bench_typed_array_from_u32() -> usize { + Uint32Array::from(black_box(UINT32_VALUES.as_slice())).length() as usize +} diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs index c6013336..9f22e386 100644 --- a/client/e2e/examples/closure.rs +++ b/client/e2e/examples/closure.rs @@ -67,7 +67,7 @@ js_sys::js_bindgen::embed_js!( name = "save", required_embeds = [("closure", "storage")], "(callback) => {{", - " this.#jsEmbed.closure.storage.callback = callback", + " this.#jsEmbed.closure.storage.callback = callback", "}}", ); @@ -76,13 +76,13 @@ js_sys::js_bindgen::embed_js!( name = "is_invalid", required_embeds = [("closure", "storage")], "() => {{", - " try {{", - " this.#jsEmbed.closure.storage.callback()", - " return false", - " }} catch (error) {{", - " return error instanceof Error", - " && error.message === 'closure invoked recursively or after being dropped'", - " }}", + " try {{", + " this.#jsEmbed.closure.storage.callback()", + " return false", + " }} catch (error) {{", + " return error instanceof Error", + " && error.message === 'closure invoked recursively or after being dropped'", + " }}", "}}", ); @@ -104,8 +104,8 @@ js_sys::js_bindgen::embed_js!( name = "release", required_embeds = [("closure", "storage")], "() => {{", - " this.#jsEmbed.closure.storage.callback.unref()", - " return true", + " this.#jsEmbed.closure.storage.callback.unref()", + " return true", "}}", ); @@ -114,10 +114,10 @@ js_sys::js_bindgen::embed_js!( name = "release.twice", required_embeds = [("closure", "storage")], "() => {{", - " const callback = this.#jsEmbed.closure.storage.callback", - " callback.unref()", - " callback.unref()", - " return true", + " const callback = this.#jsEmbed.closure.storage.callback", + " callback.unref()", + " callback.unref()", + " return true", "}}", ); diff --git a/client/e2e/examples/primitive.rs b/client/e2e/examples/primitive.rs index a0d57797..50d0bfa9 100644 --- a/client/e2e/examples/primitive.rs +++ b/client/e2e/examples/primitive.rs @@ -75,7 +75,7 @@ js_sys::js_bindgen::embed_js!( module = "primitive", name = "result.unit", "(ok) => {{", - " if (!ok) throw 'unit error'", + " if (!ok) throw 'unit error'", "}}", ); @@ -83,10 +83,10 @@ js_sys::js_bindgen::embed_js!( module = "primitive", name = "result.i64", "(value) => {{", - " if (value === -2n) throw undefined", - " if (value === -3n) throw null", - " if (value < 0n) throw 'i64 error'", - " return value + 1n", + " if (value === -2n) throw undefined", + " if (value === -3n) throw null", + " if (value < 0n) throw 'i64 error'", + " return value + 1n", "}}", ); @@ -94,8 +94,8 @@ js_sys::js_bindgen::embed_js!( module = "primitive", name = "result.u128", "(value) => {{", - " if (value === (1n << 128n) - 1n) throw 'u128 error'", - " return value + 1n", + " if (value === (1n << 128n) - 1n) throw 'u128 error'", + " return value + 1n", "}}", ); diff --git a/client/e2e/examples/string.rs b/client/e2e/examples/string.rs index ff80cdfa..ec3b3935 100644 --- a/client/e2e/examples/string.rs +++ b/client/e2e/examples/string.rs @@ -14,20 +14,33 @@ fn main() { // ;; exports["roundtrip"]("a\0b", "a\0b") // ;; exports["roundtrip"]("\ud800", "\ufffd") // ;; (() => { const value = "js-bindgen 🦀 ".repeat(8_192); return exports["roundtrip"](value, value) })() + // ;; exports["owned_roundtrip"]("") === "" + // ;; exports["owned_roundtrip"]("Hello from JavaScript! 🦀") === "Hello from JavaScript! 🦀" + // ;; exports["owned_roundtrip"]("\ufeffleading byte-order mark") === "\ufeffleading byte-order mark" + // ;; exports["owned_roundtrip"]("\ud800") === "\ufffd" + // ;; (() => { const value = "owned 🦀 ".repeat(32_768); return exports["owned_roundtrip"](value) === value })() + // ;; exports["optional_owned_roundtrip"](undefined) === undefined + // ;; exports["optional_owned_roundtrip"](null) === undefined + // ;; exports["optional_owned_roundtrip"]("") === "" + // ;; exports["optional_owned_roundtrip"]("optional 🦀") === "optional 🦀" + // ;; exports["result_owned_string"](true) === "ok" + // ;; (() => { try { exports["result_owned_string"](false); return false } catch (error) { return error === "owned error" } })() // ;; exports["result_js_string"](true) === "ok" // ;; (() => { try { exports["result_js_string"](false); return false } catch (error) { return error === "error" } })() // ;; exports["import_result_js_string"]("ok") === "ok!" // ;; (() => { try { exports["import_result_js_string"]("error"); return false } catch (error) { return error === "string error" } })() + // ;; exports["import_result_string"]("ok") === "ok!" + // ;; (() => { try { exports["import_result_string"]("error"); return false } catch (error) { return error === "string error" } })() } -use js_sys::{JsArray, JsString, JsValue, js_sys}; +use js_sys::{Array, JsString, JsValue, js_sys}; js_sys::js_bindgen::embed_js!( module = "string", name = "result.js_string", "(value) => {{", - " if (value === 'error') throw 'string error'", - " return `${{value}}!`", + " if (value === 'error') throw 'string error'", + " return `${{value}}!`", "}}", ); @@ -35,6 +48,9 @@ js_sys::js_bindgen::embed_js!( extern "js-sys" { #[js_sys(js_embed = "result.js_string")] fn import_result_js_string_raw(value: JsString) -> Result; + + #[js_sys(js_embed = "result.js_string")] + fn import_result_string_raw(value: String) -> Result; } #[expect(clippy::cmp_owned, reason = "checked")] @@ -65,7 +81,7 @@ fn borrowed_js_value(value: &JsValue) -> bool { } #[js_sys] -fn borrowed_js_array(value: &JsArray) -> u32 { +fn borrowed_js_array(value: &Array) -> u32 { value.length() } @@ -84,6 +100,25 @@ fn roundtrip(value: JsString, expected: JsString) -> bool { JsString::from(rust_value.as_str()) == rust_expected } +#[js_sys] +fn owned_roundtrip(value: String) -> String { + value +} + +#[js_sys] +fn optional_owned_roundtrip(value: Option) -> Option { + value +} + +#[js_sys] +fn result_owned_string(ok: bool) -> Result { + if ok { + Ok(String::from("ok")) + } else { + Err(JsString::from("owned error")) + } +} + #[js_sys] fn result_js_string(ok: bool) -> Result { if ok { @@ -97,3 +132,8 @@ fn result_js_string(ok: bool) -> Result { fn import_result_js_string(value: JsString) -> Result { import_result_js_string_raw(value) } + +#[js_sys] +fn import_result_string(value: String) -> Result { + import_result_string_raw(value) +} diff --git a/client/e2e/examples/vec.rs b/client/e2e/examples/vec.rs new file mode 100644 index 00000000..62ef2e11 --- /dev/null +++ b/client/e2e/examples/vec.rs @@ -0,0 +1,75 @@ +#[rustfmt::skip] +fn main() { + // ;; (() => { const first = {}; const last = {}; const input = [first, null, last]; const result = exports["js_value_roundtrip"](input); return result !== input && Array.isArray(result) && result.length === 3 && result[0] === first && result[1] === null && result[2] === last })() + // ;; (() => { const result = exports["js_value_roundtrip"]([]); return Array.isArray(result) && result.length === 0 })() + // ;; (() => { try { exports["js_value_roundtrip"](new Uint32Array()); return false } catch (error) { return error instanceof TypeError } })() + // ;; (() => { const input = new Uint32Array([0, 1, 0xffffffff]); const result = exports["u32_roundtrip"](input); return result !== input && result instanceof Uint32Array && result.length === 3 && result[0] === 0 && result[1] === 1 && result[2] === 0xffffffff })() + // ;; (() => { const result = exports["u32_roundtrip"](new Uint32Array()); return result instanceof Uint32Array && result.length === 0 })() + // ;; (() => { try { exports["u32_roundtrip"]([]); return false } catch (error) { return error instanceof TypeError } })() + // ;; (() => { const result = exports["i8_roundtrip"](new Int8Array([-128, -1, 0, 127])); return result instanceof Int8Array && result.join() === '-128,-1,0,127' })() + // ;; (() => { const result = exports["u16_roundtrip"](new Uint16Array([0, 1, 0x8000, 0xffff])); return result instanceof Uint16Array && result.join() === '0,1,32768,65535' })() + // ;; (() => { const result = exports["i64_roundtrip"](new BigInt64Array([-(1n << 63n), -1n, 0n, (1n << 63n) - 1n])); return result instanceof BigInt64Array && result[0] === -(1n << 63n) && result[3] === (1n << 63n) - 1n })() + // ;; (() => { const result = exports["u64_roundtrip"](new BigUint64Array([0n, 1n, 18446744073709551615n])); return result instanceof BigUint64Array && result[2] === 18446744073709551615n })() + // ;; (() => { const result = exports["f32_roundtrip"](new Float32Array([Math.fround(1 / 3), -0, Infinity, NaN])); return result instanceof Float32Array && result[0] === Math.fround(1 / 3) && Object.is(result[1], -0) && result[2] === Infinity && Number.isNaN(result[3]) })() + // ;; (() => { const result = exports["f64_roundtrip"](new Float64Array([-1.25, 0, 1.25])); return result instanceof Float64Array && result.join() === '-1.25,0,1.25' })() + // ;; (() => { const bits = exports["pointer_width"](); const input = bits === 64 ? new BigUint64Array([0n, 0xffffffffffffffffn]) : new Uint32Array([0, 0xffffffff]); const result = exports["usize_roundtrip"](input); const Constructor = bits === 64 ? BigUint64Array : Uint32Array; return result !== input && result instanceof Constructor && result.length === 2 && result[0] === input[0] && result[1] === input[1] })() + // ;; (() => { const input = ['first', '', '第三个 🦀']; const result = exports["string_roundtrip"](input); return result !== input && Array.isArray(result) && result.join('|') === 'first||第三个 🦀' })() + // ;; (() => { try { exports["string_roundtrip"](['valid', 42]); return false } catch (error) { return error instanceof TypeError } })() +} + +use js_sys::{JsValue, js_sys}; + +#[js_sys] +fn js_value_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn u32_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn i8_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn u16_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn i64_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn u64_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn f32_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn f64_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn usize_roundtrip(value: Vec) -> Vec { + value +} + +#[js_sys] +fn pointer_width() -> u32 { + usize::BITS +} + +#[js_sys] +fn string_roundtrip(value: Vec) -> Vec { + value +} diff --git a/client/js-sys/src/builtins/array.rs b/client/js-sys/src/builtins/array.rs index c0cbfa46..73d3f858 100644 --- a/client/js-sys/src/builtins/array.rs +++ b/client/js-sys/src/builtins/array.rs @@ -1,373 +1,615 @@ -use core::error::Error; -use core::fmt::{self, Display, Formatter}; -use core::mem::MaybeUninit; -use core::ptr; +use core::fmt::{self, Formatter}; -use super::Object; +use super::{Function, Iterable, JsIterator, Number, Object, Promise}; use crate::JsValue; -use crate::hazard::{IntoJS, IntoJsConv, JsCast}; -use crate::runtime::externref; -use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; +use crate::hazard::JsCast; #[crate::js_sys(js_sys = crate)] extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) + /// + /// `T` is an unchecked marker for the intended element type; JavaScript + /// arrays remain dynamic and may contain holes or values of another type. #[js_sys(js_name = "Array", extends = Object)] - pub type JsArray; + pub type Array; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_length(length: u32) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array) + #[must_use] + #[js_sys(constructor, return_abi = Array)] + pub fn new_typed() -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array) + #[must_use] + #[js_sys(constructor, return_abi = Array)] + pub fn new_typed_with_length(length: u32) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) + #[js_sys(static_of = Array, js_name = "from")] + pub fn from_value(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) + #[js_sys(static_of = Array, js_name = "from")] + pub fn from_value_with_map(value: &JsValue, map: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) + #[js_sys(static_of = Array, js_name = "from")] + pub fn from_value_with_map_and_this( + value: &JsValue, + map: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync) + #[js_sys(static_of = Array, js_name = "fromAsync")] + pub fn from_async(value: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync) + #[js_sys(static_of = Array, js_name = "fromAsync")] + pub fn from_async_with_map(value: &JsValue, map: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync) + #[js_sys(static_of = Array, js_name = "fromAsync")] + pub fn from_async_with_map_and_this( + value: &JsValue, + map: &Function, + this: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) + #[must_use] + #[js_sys(static_of = Array, js_name = "isArray")] + pub fn is_array(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/of) + #[must_use] + #[js_sys(static_of = Array, variadic, return_abi = Array)] + pub fn of(#[js_sys(type = &[JsValue])] values: &[T]) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/constructor) + #[must_use] #[js_sys(getter)] + pub fn constructor(self: &Array) -> Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length) + #[must_use] + #[js_sys(getter)] + pub fn length(self: &Array) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length) + #[js_sys(setter)] + pub fn set_length(self: &Array, length: u32); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at) + #[must_use] + pub fn at(self: &Array, index: f64) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) + #[must_use] + #[js_sys(return_abi = Array)] + pub fn concat(self: &Array, #[js_sys(type = &JsValue)] value: &Array) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) + #[must_use] + #[js_sys(js_name = "concat", variadic, return_abi = Array)] + pub fn concat_many( + self: &Array, + #[js_sys(type = &[JsValue])] values: &[Array], + ) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin) + #[must_use] + #[js_sys(js_name = "copyWithin", return_abi = Array)] + pub fn copy_within(self: &Array, target: f64, start: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin) + #[must_use] + #[js_sys(js_name = "copyWithin", return_abi = Array)] + pub fn copy_within_range(self: &Array, target: f64, start: f64, end: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries) #[must_use] - pub fn length(self: &JsArray) -> u32; - - #[js_sys(js_embed = "array.js_value.decode")] - // SAFETY: The pointer and length must describe a valid `JsValue` slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn array_js_value_decode( - array: PtrConst, - len: PtrLength, - ) -> JsArray; - - #[js_sys(js_embed = "array.js_value.encode")] - // SAFETY: Every pointer and length pair must describe its matching output slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn array_js_value_encode( - array: &JsArray, - array_ptr: PtrMut, - array_len: PtrLength, - externref_ptr: PtrConst, - externref_len: i32, - write_output: bool, + pub fn entries(self: &Array) -> JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) + pub fn every(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) + #[js_sys(js_name = "every")] + pub fn every_with_this( + self: &Array, + callback: &Function, + this: &JsValue, ) -> Result; - #[js_sys(js_embed = "view.getUint32")] - // SAFETY: The pointer and length must describe a valid `u32` slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn array_u32_decode(array: PtrConst, len: PtrLength) -> JsArray; - - #[js_sys(js_embed = "array.u32.encode")] - // SAFETY: The pointer and length must describe a valid `u32` output slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn array_u32_encode(array: &JsArray, ptr: PtrMut, len: PtrLength) - -> bool; -} + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill) + #[must_use] + #[js_sys(return_abi = Array)] + pub fn fill(self: &Array, #[js_sys(type = &JsValue)] value: &T) -> Array; -impl JsArray { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill) #[must_use] - pub fn as_any(&self) -> &JsArray { - JsArray::unchecked_from_ref(self.as_ref()) - } + #[js_sys(js_name = "fill", return_abi = Array)] + pub fn fill_from( + self: &Array, + #[js_sys(type = &JsValue)] value: &T, + start: f64, + ) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill) + #[must_use] + #[js_sys(js_name = "fill", return_abi = Array)] + pub fn fill_range( + self: &Array, + #[js_sys(type = &JsValue)] value: &T, + start: f64, + end: f64, + ) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) + #[js_sys(return_abi = Result)] + pub fn filter(self: &Array, callback: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) + #[js_sys(js_name = "filter", return_abi = Result)] + pub fn filter_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) + pub fn find(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) + #[js_sys(js_name = "find")] + pub fn find_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) + #[js_sys(js_name = "findIndex")] + pub fn find_index(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) + #[js_sys(js_name = "findIndex")] + pub fn find_index_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) + #[js_sys(js_name = "findLast")] + pub fn find_last(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) + #[js_sys(js_name = "findLast")] + pub fn find_last_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex) + #[js_sys(js_name = "findLastIndex")] + pub fn find_last_index(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex) + #[js_sys(js_name = "findLastIndex")] + pub fn find_last_index_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) + #[must_use] + pub fn flat(self: &Array) -> Array; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) #[must_use] - pub fn into_any(self) -> JsArray { - JsArray::unchecked_from(self.into()) - } -} + #[js_sys(js_name = "flat")] + pub fn flat_with_depth(self: &Array, depth: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) + #[js_sys(js_name = "flatMap")] + pub fn flat_map(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) + #[js_sys(js_name = "flatMap")] + pub fn flat_map_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each(self: &Array, callback: &Function) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) + #[must_use] + pub fn includes(self: &Array, #[js_sys(type = &JsValue)] value: &T) -> bool; -impl From<&[T; N]> for JsArray -where - Self: for<'a> From<&'a [T]>, -{ - fn from(value: &[T; N]) -> Self { - value.as_slice().into() - } -} + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) + #[must_use] + #[js_sys(js_name = "includes")] + pub fn includes_from( + self: &Array, + #[js_sys(type = &JsValue)] value: &T, + from_index: f64, + ) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) + #[must_use] + #[js_sys(js_name = "indexOf")] + pub fn index_of(self: &Array, #[js_sys(type = &JsValue)] value: &T) -> f64; -// SAFETY: The array delegates to the slice implementation with the same -// element representation. -unsafe impl<'a, T, const N: usize> IntoJS for &'a [T; N] -where - &'a [T]: IntoJS, -{ - const JS_CONV: Option = <&[T] as IntoJS>::JS_CONV; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) + #[must_use] + #[js_sys(js_name = "indexOf")] + pub fn index_of_from( + self: &Array, + #[js_sys(type = &JsValue)] value: &T, + from_index: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join) + #[must_use] + pub fn join(self: &Array) -> crate::JsString; - type Abi = <&'a [T] as IntoJS>::Abi; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join) + #[must_use] + #[js_sys(js_name = "join")] + pub fn join_with(self: &Array, separator: &str) -> crate::JsString; - fn into_abi(self) -> Self::Abi { - self.as_slice().into_abi() - } -} + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/keys) + #[must_use] + pub fn keys(self: &Array) -> JsIterator>; -#[derive(Debug)] -#[non_exhaustive] -pub struct TryFromJsArrayError; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf) + #[must_use] + #[js_sys(js_name = "lastIndexOf")] + pub fn last_index_of(self: &Array, #[js_sys(type = &JsValue)] value: &T) -> f64; -impl Display for TryFromJsArrayError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("failed to copy array") - } -} + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf) + #[must_use] + #[js_sys(js_name = "lastIndexOf")] + pub fn last_index_of_from( + self: &Array, + #[js_sys(type = &JsValue)] value: &T, + from_index: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) + pub fn map(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) + #[js_sys(js_name = "map")] + pub fn map_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop) + #[must_use] + pub fn pop(self: &Array) -> JsValue; -impl Error for TryFromJsArrayError {} - -impl JsArray { - pub fn to_slice(&self, slice: &mut [T]) -> Result<(), TryFromJsArrayError> { - let slots = externref::reserve_slots(slice.len()); - - let result = { - let js_slice = JsValue::from_slice_mut(slice); - // SAFETY: Parameters are correct. `write_output` is false, so JavaScript - // does not write through the destination pointer. - unsafe { - array_js_value_encode( - self.as_any(), - PtrMut::new(js_slice), - PtrLength::new(js_slice), - slots.ptr(), - slots.len(), - false, - ) - } - }; - - if matches!(result, Ok(true)) { - slots.replace(slice); - Ok(()) - } else { - Err(TryFromJsArrayError) - } - } + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) + #[must_use] + pub fn push(self: &Array, #[js_sys(type = &JsValue)] value: &T) -> u32; - pub fn to_uninit_slice<'slice>( - &self, - slice: &'slice mut [MaybeUninit], - ) -> Result<&'slice mut [T], TryFromJsArrayError> { - let js_slice = JsValue::from_uninit_slice_mut(slice); - let slots = externref::reserve_slots(js_slice.len()); - - // SAFETY: Parameters are correct. - let result = unsafe { - array_js_value_encode( - self.as_any(), - PtrMut::from_uninit_slice(js_slice), - PtrLength::from_uninit_slice(js_slice), - slots.ptr(), - slots.len(), - true, - ) - }; - - if matches!(result, Ok(true)) { - slots.commit(); - // SAFETY: Correctly initialized in JS. - Ok(unsafe { assume_init_mut(slice) }) - } else { - Err(TryFromJsArrayError) - } - } + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) + #[must_use] + #[js_sys(js_name = "push", variadic)] + pub fn push_many(self: &Array, #[js_sys(type = &[JsValue])] values: &[T]) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + pub fn reduce(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + #[js_sys(js_name = "reduce")] + pub fn reduce_with_initial( + self: &Array, + callback: &Function, + initial: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight) + #[js_sys(js_name = "reduceRight")] + pub fn reduce_right(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight) + #[js_sys(js_name = "reduceRight")] + pub fn reduce_right_with_initial( + self: &Array, + callback: &Function, + initial: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse) + #[must_use] + #[js_sys(return_abi = Array)] + pub fn reverse(self: &Array) -> Array; - pub fn to_array(&self) -> Result<[T; N], TryFromJsArrayError> { - let mut array: MaybeUninit<[T; N]> = MaybeUninit::uninit(); - let slots = externref::reserve_slots(N); - let js_array = JsValue::from_mut_uninit_array(&mut array); - - // SAFETY: Parameters are correct. - let result = unsafe { - array_js_value_encode( - self.as_any(), - PtrMut::from_uninit_array(js_array), - PtrLength::from_uninit_array(js_array), - slots.ptr(), - slots.len(), - true, - ) - }; - - if matches!(result, Ok(true)) { - slots.commit(); - // SAFETY: Correctly initialized in JS. - Ok(unsafe { array.assume_init() }) - } else { - Err(TryFromJsArrayError) - } - } + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift) + #[must_use] + pub fn shift(self: &Array) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) + #[must_use] + #[js_sys(return_abi = Array)] + pub fn slice(self: &Array) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) + #[must_use] + #[js_sys(js_name = "slice", return_abi = Array)] + pub fn slice_from(self: &Array, start: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) + #[must_use] + #[js_sys(js_name = "slice", return_abi = Array)] + pub fn slice_range(self: &Array, start: f64, end: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) + pub fn some(self: &Array, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) + #[js_sys(js_name = "some")] + pub fn some_with_this( + self: &Array, + callback: &Function, + this: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) + #[must_use] + #[js_sys(return_abi = Array)] + pub fn sort(self: &Array) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) + #[js_sys(js_name = "sort", return_abi = Result)] + pub fn sort_by(self: &Array, callback: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) + #[must_use] + #[js_sys(return_abi = Array)] + pub fn splice(self: &Array, start: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) + #[must_use] + #[js_sys(js_name = "splice", return_abi = Array)] + pub fn splice_delete(self: &Array, start: f64, delete_count: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) + #[must_use] + #[js_sys(js_name = "splice", variadic, return_abi = Array)] + pub fn splice_many( + self: &Array, + start: f64, + delete_count: f64, + #[js_sys(type = &[JsValue])] values: &[T], + ) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toLocaleString) + #[must_use] + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &Array) -> crate::JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toLocaleString) + #[must_use] + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales(self: &Array, locales: &JsValue) -> crate::JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toLocaleString) + #[must_use] + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_options( + self: &Array, + locales: &JsValue, + options: &JsValue, + ) -> crate::JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toReversed) + #[must_use] + #[js_sys(js_name = "toReversed", return_abi = Array)] + pub fn to_reversed(self: &Array) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted) + #[must_use] + #[js_sys(js_name = "toSorted", return_abi = Array)] + pub fn to_sorted(self: &Array) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted) + #[js_sys(js_name = "toSorted", return_abi = Result)] + pub fn to_sorted_by(self: &Array, callback: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced) + #[must_use] + #[js_sys(js_name = "toSpliced", return_abi = Array)] + pub fn to_spliced(self: &Array, start: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced) + #[must_use] + #[js_sys(js_name = "toSpliced", return_abi = Array)] + pub fn to_spliced_delete(self: &Array, start: f64, delete_count: f64) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced) + #[must_use] + #[js_sys(js_name = "toSpliced", variadic, return_abi = Array)] + pub fn to_spliced_many( + self: &Array, + start: f64, + delete_count: f64, + #[js_sys(type = &[JsValue])] values: &[T], + ) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Array) -> crate::JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift) + #[must_use] + pub fn unshift(self: &Array, #[js_sys(type = &JsValue)] value: &T) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift) + #[must_use] + #[js_sys(js_name = "unshift", variadic)] + pub fn unshift_many( + self: &Array, + #[js_sys(type = &[JsValue])] values: &[T], + ) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values) + #[must_use] + #[js_sys(return_abi = JsIterator)] + pub fn values(self: &Array) -> JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/with) + #[js_sys(js_name = "with", return_abi = Result)] + pub fn with( + self: &Array, + index: f64, + #[js_sys(type = &JsValue)] value: &T, + ) -> Result, JsValue>; } -js_bindgen::embed_js!( - module = "js_sys", - name = "array.js_value.encode", - required_embeds = [("js_sys", "view.getInt32"), ("js_sys", "view.setInt32")], - "(array, arrPtr, arrLen, refPtr, refLen, writeOutput) => {{", - " if (array.length !== arrLen) return false", - "", - " const table = this.#jsEmbed.js_sys['externref.table']", - " const refIndices = this.#jsEmbed.js_sys['view.getInt32'](", - " refPtr,", - " refLen,", - " )", - " if (writeOutput) {{", - " const elemIndices = new Array(arrLen)", - " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", - " const elemIndex = refIndices[arrayIndex]", - " table.set(elemIndex, array[arrayIndex])", - " elemIndices[arrayIndex] = elemIndex", - " }}", - " this.#jsEmbed.js_sys['view.setInt32'](arrPtr, elemIndices)", - " }} else {{", - " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", - " table.set(refIndices[arrayIndex], array[arrayIndex])", - " }}", - " }}", - " return true", - "}}", -); +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[must_use] + #[js_sys(indexing_getter)] + pub fn get(self: &Array, index: u32) -> JsValue; -js_bindgen::embed_js!( - module = "js_sys", - name = "array.js_value.decode", - required_embeds = [("js_sys", "view.getInt32")], - "(ptr, len) => {{", - " const array = new Array(len)", - " const table = this.#jsEmbed.js_sys['externref.table']", - " const refIndices = this.#jsEmbed.js_sys['view.getInt32'](ptr, len)", - " for (let arrayIndex = 0; arrayIndex < len; arrayIndex++) {{", - " array[arrayIndex] = table.get(refIndices[arrayIndex])", - " }}", - " return array", - "}}", -); + #[must_use] + #[js_sys(indexing_getter, return_abi = JsValue)] + pub fn get_unchecked(self: &Array, index: u32) -> T; -impl From<&[T]> for JsArray { - fn from(value: &[T]) -> Self { - let slice = JsValue::from_slice(value); - // SAFETY: Parameters are correct. - let result = unsafe { array_js_value_decode(PtrConst::new(slice), PtrLength::new(slice)) }; + #[js_sys(indexing_setter)] + pub fn set(self: &Array, index: u32, #[js_sys(type = &JsValue)] value: &T); - Self::unchecked_from(result.into()) - } + #[js_sys(indexing_setter)] + pub fn try_set( + self: &Array, + index: u32, + #[js_sys(type = &JsValue)] value: &T, + ) -> Result<(), JsValue>; + + #[must_use] + #[js_sys(indexing_deleter)] + pub fn delete(self: &Array, index: u32) -> bool; + + #[js_sys(indexing_deleter)] + pub fn try_delete(self: &Array, index: u32) -> Result; } -// SAFETY: The two slots point to borrowed `JsValue` table indices, which the -// JavaScript decoder resolves before the import is called. -unsafe impl IntoJS for &[T] { - const JS_CONV: Option = Some( - IntoJsConv::new("this.#jsEmbed.js_sys['array.js_value.decode']($slot1, $slot2)") - .with_embed(("js_sys", "array.js_value.decode")), - ); +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "array.species")] + fn array_species() -> Function; + + #[js_sys(js_embed = "array.symbol_iterator")] + fn array_symbol_iterator(array: &Array) -> JsIterator; - type Abi = ExternSlice; + #[js_sys(js_embed = "array.symbol_unscopables")] + fn array_symbol_unscopables(array: &Array) -> JsValue; +} - fn into_abi(self) -> Self::Abi { - ExternSlice::new(JsValue::from_slice(self)) +impl Clone for Array { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) } } -impl JsArray { - pub fn to_slice(&self, slice: &mut [u32]) -> Result<(), TryFromJsArrayError> { - // SAFETY: Parameters are correct. - let result = unsafe { array_u32_encode(self, PtrMut::new(slice), PtrLength::new(slice)) }; +impl fmt::Debug for Array { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), f) + } +} - if result { - Ok(()) - } else { - Err(TryFromJsArrayError) - } +impl Default for Array { + fn default() -> Self { + Self::new_typed() } +} - pub fn to_uninit_slice<'slice>( - &self, - slice: &'slice mut [MaybeUninit], - ) -> Result<&'slice mut [u32], TryFromJsArrayError> { - // SAFETY: Parameters are correct. - let result = unsafe { - array_u32_encode( - self, - PtrMut::from_uninit_slice(slice), - PtrLength::from_uninit_slice(slice), - ) - }; - - if result { - // SAFETY: Correctly initialized in JS. - Ok(unsafe { assume_init_mut(slice) }) - } else { - Err(TryFromJsArrayError) - } +impl Array { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Symbol.iterator) + #[must_use] + pub fn symbol_iterator(&self) -> JsIterator { + JsIterator::unchecked_from(array_symbol_iterator(self.as_untyped()).into()) } - pub fn to_array(&self) -> Result<[u32; N], TryFromJsArrayError> { - let mut array: MaybeUninit<[u32; N]> = MaybeUninit::uninit(); - - // SAFETY: Parameters are correct. - let result = unsafe { - array_u32_encode( - self, - PtrMut::from_uninit_array(&mut array), - PtrLength::from_uninit_array(&array), - ) - }; - - if result { - // SAFETY: Correctly initialized in JS. - Ok(unsafe { array.assume_init() }) - } else { - Err(TryFromJsArrayError) - } + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Symbol.unscopables) + #[must_use] + pub fn symbol_unscopables(&self) -> JsValue { + array_symbol_unscopables(self.as_untyped()) } } -js_bindgen::embed_js!( - module = "js_sys", - name = "array.u32.encode", - required_embeds = [("js_sys", "view.setInt32")], - "(array, ptr, len) => {{", - " if (array.length !== len) return false", - "", - " this.#jsEmbed.js_sys['view.setInt32'](ptr, array)", - " return true", - "}}", -); +impl Array { + #[must_use] + pub fn as_untyped(&self) -> &Array { + Array::unchecked_from_ref(self.as_ref()) + } -impl From<&[u32]> for JsArray { - fn from(value: &[u32]) -> Self { - // SAFETY: Parameters are correct. - unsafe { array_u32_decode(PtrConst::new(value), PtrLength::new(value)) } + #[must_use] + pub fn into_untyped(self) -> Array { + Array::unchecked_from(self.into()) } } -// SAFETY: The two slots describe a borrowed `u32` slice, which JavaScript -// copies into an array before the import is called. -unsafe impl IntoJS for &[u32] { - const JS_CONV: Option = Some( - IntoJsConv::new("this.#jsEmbed.js_sys['view.getUint32']($slot1, $slot2)") - .with_embed(("js_sys", "view.getUint32")), - ); - - type Abi = ExternSlice; +impl Array { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) + pub fn from_iterable(value: &I) -> Result, JsValue> { + let array = Self::from_value(value.as_ref())?; + Ok(Array::unchecked_from(array.into())) + } - fn into_abi(self) -> Self::Abi { - ExternSlice::new(self) + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Symbol.species) + #[must_use] + pub fn species() -> Function { + array_species() } } -// MSRV: Stable on v1.93. -const unsafe fn assume_init_mut(slice: &mut [MaybeUninit]) -> &mut [T] { - // SAFETY: copied from Std. - unsafe { &mut *(ptr::from_mut::<[MaybeUninit]>(slice) as *mut [T]) } +impl Iterable for Array { + type Item = T; } + +js_bindgen::embed_js!( + module = "js_sys", + name = "array.symbol_iterator", + "(array) => array[Symbol.iterator]()", +); +js_bindgen::embed_js!( + module = "js_sys", + name = "array.species", + "() => Array[Symbol.species]", +); +js_bindgen::embed_js!( + module = "js_sys", + name = "array.symbol_unscopables", + "(array) => array[Symbol.unscopables]", +); diff --git a/client/js-sys/src/builtins/array_buffer.rs b/client/js-sys/src/builtins/array_buffer.rs new file mode 100644 index 00000000..b7e6abb9 --- /dev/null +++ b/client/js-sys/src/builtins/array_buffer.rs @@ -0,0 +1,153 @@ +use super::Object; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ArrayBufferOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer#maxbytelength) + #[must_use] + #[js_sys(getter = "maxByteLength")] + pub fn max_byte_length(self: &ArrayBufferOptions) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer#maxbytelength) + #[js_sys(setter = "maxByteLength")] + pub fn set_max_byte_length(self: &ArrayBufferOptions, max_byte_length: f64); +} + +impl ArrayBufferOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer#maxbytelength) + #[must_use] + pub fn new(max_byte_length: f64) -> Self { + let options = Self::unchecked_from(Object::new().into()); + options.set_max_byte_length(max_byte_length); + options + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) + #[js_sys(js_name = "ArrayBuffer", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ArrayBuffer; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer) + #[js_sys(constructor)] + pub fn new(byte_length: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer) + #[js_sys(constructor)] + pub fn new_with_options( + byte_length: f64, + options: &ArrayBufferOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/byteLength) + #[must_use] + #[js_sys(getter = "byteLength")] + pub fn byte_length(self: &ArrayBuffer) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/detached) + #[must_use] + #[js_sys(getter)] + pub fn detached(self: &ArrayBuffer) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/isView) + #[must_use] + #[js_sys(static_of = ArrayBuffer, js_name = "isView")] + pub fn is_view(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/maxByteLength) + #[must_use] + #[js_sys(getter = "maxByteLength")] + pub fn max_byte_length(self: &ArrayBuffer) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/resizable) + #[must_use] + #[js_sys(getter)] + pub fn resizable(self: &ArrayBuffer) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/resize) + pub fn resize(self: &ArrayBuffer, new_byte_length: f64) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/slice) + pub fn slice(self: &ArrayBuffer, begin: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/slice) + #[js_sys(js_name = "slice")] + pub fn slice_range(self: &ArrayBuffer, begin: f64, end: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer) + pub fn transfer(self: &ArrayBuffer) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer) + #[js_sys(js_name = "transfer")] + pub fn transfer_with_length( + self: &ArrayBuffer, + new_byte_length: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transferToFixedLength) + #[js_sys(js_name = "transferToFixedLength")] + pub fn transfer_to_fixed_length(self: &ArrayBuffer) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transferToFixedLength) + #[js_sys(js_name = "transferToFixedLength")] + pub fn transfer_to_fixed_length_with_length( + self: &ArrayBuffer, + new_byte_length: f64, + ) -> Result; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) + #[js_sys(js_name = "SharedArrayBuffer", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type SharedArrayBuffer; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer) + #[js_sys(constructor)] + pub fn new(byte_length: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer) + #[js_sys(constructor)] + pub fn new_with_options( + byte_length: f64, + options: &ArrayBufferOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/byteLength) + #[must_use] + #[js_sys(getter = "byteLength")] + pub fn byte_length(self: &SharedArrayBuffer) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/growable) + #[must_use] + #[js_sys(getter)] + pub fn growable(self: &SharedArrayBuffer) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/maxByteLength) + #[must_use] + #[js_sys(getter = "maxByteLength")] + pub fn max_byte_length(self: &SharedArrayBuffer) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/grow) + pub fn grow(self: &SharedArrayBuffer, new_byte_length: f64) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/slice) + pub fn slice(self: &SharedArrayBuffer, begin: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/slice) + #[js_sys(js_name = "slice")] + pub fn slice_range( + self: &SharedArrayBuffer, + begin: f64, + end: f64, + ) -> Result; +} diff --git a/client/js-sys/src/builtins/async_disposable_stack.rs b/client/js-sys/src/builtins/async_disposable_stack.rs new file mode 100644 index 00000000..d480a19b --- /dev/null +++ b/client/js-sys/src/builtins/async_disposable_stack.rs @@ -0,0 +1,72 @@ +use super::{Function, Object, Promise}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type AsyncDisposableStack; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/AsyncDisposableStack) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> AsyncDisposableStack; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/adopt) + #[js_sys(return_abi = Result)] + pub fn adopt( + self: &AsyncDisposableStack, + #[js_sys(type = &JsValue)] value: &T, + on_dispose_async: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/defer) + pub fn defer(self: &AsyncDisposableStack, on_dispose_async: &Function) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/disposeAsync) + #[js_sys(js_name = "disposeAsync")] + pub fn dispose_async(self: &AsyncDisposableStack) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/disposed) + #[must_use] + #[js_sys(getter)] + pub fn disposed(self: &AsyncDisposableStack) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/move) + #[js_sys(js_name = "move")] + pub fn move_(self: &AsyncDisposableStack) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/use) + #[js_sys(js_name = "use", return_abi = Result)] + pub fn use_( + self: &AsyncDisposableStack, + #[js_sys(type = &JsValue)] value: &T, + ) -> Result; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "async_disposable_stack.symbol_async_dispose")] + fn async_disposable_stack_symbol_async_dispose(stack: &AsyncDisposableStack) -> Promise; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_disposable_stack.symbol_async_dispose", + "(stack) => stack[Symbol.asyncDispose]()", +); + +impl AsyncDisposableStack { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack/Symbol.asyncDispose) + pub fn symbol_async_dispose(&self) -> Promise { + async_disposable_stack_symbol_async_dispose(self) + } +} + +impl Default for AsyncDisposableStack { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/atomics.rs b/client/js-sys/src/builtins/atomics.rs new file mode 100644 index 00000000..7b9a5a15 --- /dev/null +++ b/client/js-sys/src/builtins/atomics.rs @@ -0,0 +1,680 @@ +use crate::{ + BigInt64Array, BigUint64Array, Int8Array, Int16Array, Int32Array, JsString, JsValue, Object, + Uint8Array, Uint16Array, Uint32Array, js_sys, +}; + +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod Atomics { + use super::*; + + #[js_sys(js_sys = crate)] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type WaitAsyncResult; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + #[must_use] + #[js_sys(getter = "async")] + pub fn async_(self: &WaitAsyncResult) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + #[must_use] + #[js_sys(getter)] + pub fn value(self: &WaitAsyncResult) -> JsValue; + } + + mod sealed { + pub trait Sealed {} + } + + mod raw { + use super::*; + + macro_rules! operations { + ( + $value:ty, + add = $add:ident, + and = $and:ident, + compare_exchange = $compare_exchange:ident, + exchange = $exchange:ident, + load = $load:ident, + or = $or:ident, + store = $store:ident, + sub = $sub:ident, + xor = $xor:ident, + ) => { + #[js_sys(js_sys = crate, namespace = "Atomics")] + extern "js-sys" { + #[js_sys(js_name = "add")] + pub(super) fn $add( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "and")] + pub(super) fn $and( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "compareExchange")] + pub(super) fn $compare_exchange( + array: &JsValue, + index: f64, + expected: $value, + replacement: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "exchange")] + pub(super) fn $exchange( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "load")] + pub(super) fn $load(array: &JsValue, index: f64) -> Result<$value, JsValue>; + + #[js_sys(js_name = "or")] + pub(super) fn $or( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "store")] + pub(super) fn $store( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "sub")] + pub(super) fn $sub( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + + #[js_sys(js_name = "xor")] + pub(super) fn $xor( + array: &JsValue, + index: f64, + value: $value, + ) -> Result<$value, JsValue>; + } + }; + } + + operations!( + f64, + add = add_number, + and = and_number, + compare_exchange = compare_exchange_number, + exchange = exchange_number, + load = load_number, + or = or_number, + store = store_number, + sub = sub_number, + xor = xor_number, + ); + operations!( + i64, + add = add_i64, + and = and_i64, + compare_exchange = compare_exchange_i64, + exchange = exchange_i64, + load = load_i64, + or = or_i64, + store = store_i64, + sub = sub_i64, + xor = xor_i64, + ); + operations!( + u64, + add = add_u64, + and = and_u64, + compare_exchange = compare_exchange_u64, + exchange = exchange_u64, + load = load_u64, + or = or_u64, + store = store_u64, + sub = sub_u64, + xor = xor_u64, + ); + + #[js_sys(js_sys = crate, namespace = "Atomics")] + extern "js-sys" { + #[js_sys(js_name = "isLockFree")] + pub(super) fn is_lock_free(size: f64) -> bool; + + pub(super) fn notify(array: &Int32Array, index: f64) -> Result; + + #[js_sys(js_name = "notify")] + pub(super) fn notify_with_count( + array: &Int32Array, + index: f64, + count: f64, + ) -> Result; + + #[js_sys(js_name = "notify")] + pub(super) fn notify_bigint(array: &BigInt64Array, index: f64) -> Result; + + #[js_sys(js_name = "notify")] + pub(super) fn notify_bigint_with_count( + array: &BigInt64Array, + index: f64, + count: f64, + ) -> Result; + + pub(super) fn pause() -> Result<(), JsValue>; + + #[js_sys(js_name = "pause")] + pub(super) fn pause_with_hint(duration_hint: f64) -> Result<(), JsValue>; + + pub(super) fn wait( + array: &Int32Array, + index: f64, + value: i32, + ) -> Result; + + #[js_sys(js_name = "wait")] + pub(super) fn wait_with_timeout( + array: &Int32Array, + index: f64, + value: i32, + timeout: f64, + ) -> Result; + + #[js_sys(js_name = "wait")] + pub(super) fn wait_bigint( + array: &BigInt64Array, + index: f64, + value: i64, + ) -> Result; + + #[js_sys(js_name = "wait")] + pub(super) fn wait_bigint_with_timeout( + array: &BigInt64Array, + index: f64, + value: i64, + timeout: f64, + ) -> Result; + + #[js_sys(js_name = "waitAsync")] + pub(super) fn wait_async( + array: &Int32Array, + index: f64, + value: i32, + ) -> Result; + + #[js_sys(js_name = "waitAsync")] + pub(super) fn wait_async_with_timeout( + array: &Int32Array, + index: f64, + value: i32, + timeout: f64, + ) -> Result; + + #[js_sys(js_name = "waitAsync")] + pub(super) fn wait_async_bigint( + array: &BigInt64Array, + index: f64, + value: i64, + ) -> Result; + + #[js_sys(js_name = "waitAsync")] + pub(super) fn wait_async_bigint_with_timeout( + array: &BigInt64Array, + index: f64, + value: i64, + timeout: f64, + ) -> Result; + } + } + + #[doc(hidden)] + pub trait AtomicInteger: sealed::Sealed + AsRef { + type Value: Copy; + + fn atomic_add(&self, index: f64, value: Self::Value) -> Result; + fn atomic_and(&self, index: f64, value: Self::Value) -> Result; + fn atomic_compare_exchange( + &self, + index: f64, + expected: Self::Value, + replacement: Self::Value, + ) -> Result; + fn atomic_exchange(&self, index: f64, value: Self::Value) -> Result; + fn atomic_load(&self, index: f64) -> Result; + fn atomic_or(&self, index: f64, value: Self::Value) -> Result; + fn atomic_store(&self, index: f64, value: Self::Value) -> Result; + fn atomic_sub(&self, index: f64, value: Self::Value) -> Result; + fn atomic_xor(&self, index: f64, value: Self::Value) -> Result; + } + + macro_rules! impl_number { + ($array:ty, $value:ty, $($lint:path),+ $(,)?) => { + impl sealed::Sealed for $array {} + + #[expect( + $($lint),+, + reason = "JavaScript returns a value represented by the typed array element" + )] + impl AtomicInteger for $array { + type Value = $value; + + fn atomic_add( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::add_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + + fn atomic_and( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::and_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + + fn atomic_compare_exchange( + &self, + index: f64, + expected: Self::Value, + replacement: Self::Value, + ) -> Result { + raw::compare_exchange_number( + self.as_ref(), + index, + f64::from(expected), + f64::from(replacement), + ) + .map(|value| value as $value) + } + + fn atomic_exchange( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::exchange_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + + fn atomic_load(&self, index: f64) -> Result { + raw::load_number(self.as_ref(), index).map(|value| value as $value) + } + + fn atomic_or( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::or_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + + fn atomic_store( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::store_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + + fn atomic_sub( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::sub_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + + fn atomic_xor( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::xor_number(self.as_ref(), index, f64::from(value)) + .map(|value| value as $value) + } + } + }; + } + + macro_rules! impl_bigint { + ( + $array:ty, + $value:ty, + add = $add:ident, + and = $and:ident, + compare_exchange = $compare_exchange:ident, + exchange = $exchange:ident, + load = $load:ident, + or = $or:ident, + store = $store:ident, + sub = $sub:ident, + xor = $xor:ident, + ) => { + impl sealed::Sealed for $array {} + + impl AtomicInteger for $array { + type Value = $value; + + fn atomic_add( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$add(self.as_ref(), index, value) + } + + fn atomic_and( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$and(self.as_ref(), index, value) + } + + fn atomic_compare_exchange( + &self, + index: f64, + expected: Self::Value, + replacement: Self::Value, + ) -> Result { + raw::$compare_exchange(self.as_ref(), index, expected, replacement) + } + + fn atomic_exchange( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$exchange(self.as_ref(), index, value) + } + + fn atomic_load(&self, index: f64) -> Result { + raw::$load(self.as_ref(), index) + } + + fn atomic_or( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$or(self.as_ref(), index, value) + } + + fn atomic_store( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$store(self.as_ref(), index, value) + } + + fn atomic_sub( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$sub(self.as_ref(), index, value) + } + + fn atomic_xor( + &self, + index: f64, + value: Self::Value, + ) -> Result { + raw::$xor(self.as_ref(), index, value) + } + } + }; + } + + impl_number!(Int8Array, i8, clippy::cast_possible_truncation); + impl_number!( + Uint8Array, + u8, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + ); + impl_number!(Int16Array, i16, clippy::cast_possible_truncation); + impl_number!( + Uint16Array, + u16, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + ); + impl_number!(Int32Array, i32, clippy::cast_possible_truncation); + impl_number!( + Uint32Array, + u32, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + ); + impl_bigint!( + BigInt64Array, + i64, + add = add_i64, + and = and_i64, + compare_exchange = compare_exchange_i64, + exchange = exchange_i64, + load = load_i64, + or = or_i64, + store = store_i64, + sub = sub_i64, + xor = xor_i64, + ); + impl_bigint!( + BigUint64Array, + u64, + add = add_u64, + and = and_u64, + compare_exchange = compare_exchange_u64, + exchange = exchange_u64, + load = load_u64, + or = or_u64, + store = store_u64, + sub = sub_u64, + xor = xor_u64, + ); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/add) + pub fn add( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_add(index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/and) + pub fn and( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_and(index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/compareExchange) + pub fn compare_exchange( + array: &A, + index: f64, + expected: A::Value, + replacement: A::Value, + ) -> Result { + array.atomic_compare_exchange(index, expected, replacement) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/exchange) + pub fn exchange( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_exchange(index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/isLockFree) + #[must_use] + pub fn is_lock_free(size: f64) -> bool { + raw::is_lock_free(size) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/load) + pub fn load(array: &A, index: f64) -> Result { + array.atomic_load(index) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/notify) + pub fn notify(array: &Int32Array, index: f64) -> Result { + raw::notify(array, index) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/notify) + pub fn notify_with_count(array: &Int32Array, index: f64, count: f64) -> Result { + raw::notify_with_count(array, index, count) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/notify) + pub fn notify_bigint(array: &BigInt64Array, index: f64) -> Result { + raw::notify_bigint(array, index) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/notify) + pub fn notify_bigint_with_count( + array: &BigInt64Array, + index: f64, + count: f64, + ) -> Result { + raw::notify_bigint_with_count(array, index, count) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/or) + pub fn or( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_or(index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/pause) + pub fn pause() -> Result<(), JsValue> { + raw::pause() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/pause) + pub fn pause_with_hint(duration_hint: f64) -> Result<(), JsValue> { + raw::pause_with_hint(duration_hint) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/store) + pub fn store( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_store(index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait) + pub fn wait(array: &Int32Array, index: f64, value: i32) -> Result { + raw::wait(array, index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait) + pub fn wait_with_timeout( + array: &Int32Array, + index: f64, + value: i32, + timeout: f64, + ) -> Result { + raw::wait_with_timeout(array, index, value, timeout) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait) + pub fn wait_bigint(array: &BigInt64Array, index: f64, value: i64) -> Result { + raw::wait_bigint(array, index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait) + pub fn wait_bigint_with_timeout( + array: &BigInt64Array, + index: f64, + value: i64, + timeout: f64, + ) -> Result { + raw::wait_bigint_with_timeout(array, index, value, timeout) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + pub fn wait_async( + array: &Int32Array, + index: f64, + value: i32, + ) -> Result { + raw::wait_async(array, index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + pub fn wait_async_with_timeout( + array: &Int32Array, + index: f64, + value: i32, + timeout: f64, + ) -> Result { + raw::wait_async_with_timeout(array, index, value, timeout) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + pub fn wait_async_bigint( + array: &BigInt64Array, + index: f64, + value: i64, + ) -> Result { + raw::wait_async_bigint(array, index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync) + pub fn wait_async_bigint_with_timeout( + array: &BigInt64Array, + index: f64, + value: i64, + timeout: f64, + ) -> Result { + raw::wait_async_bigint_with_timeout(array, index, value, timeout) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/sub) + pub fn sub( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_sub(index, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/xor) + pub fn xor( + array: &A, + index: f64, + value: A::Value, + ) -> Result { + array.atomic_xor(index, value) + } +} diff --git a/client/js-sys/src/builtins/bigint.rs b/client/js-sys/src/builtins/bigint.rs index 29b050a0..ec8acccb 100644 --- a/client/js-sys/src/builtins/bigint.rs +++ b/client/js-sys/src/builtins/bigint.rs @@ -1,5 +1,62 @@ +use super::JsString; +use crate::JsValue; + #[crate::js_sys(js_sys = crate)] extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) #[js_sys(js_name = "BigInt")] - pub type JsBigInt; + #[derive(Clone, Debug, PartialEq)] + pub type BigInt; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt) + #[js_sys(js_name = "BigInt")] + fn bigint_constructor(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asIntN) + #[js_sys(static_of = BigInt, js_name = "asIntN")] + pub fn as_int_n(bits: f64, value: &BigInt) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asUintN) + #[js_sys(static_of = BigInt, js_name = "asUintN")] + pub fn as_uint_n(bits: f64, value: &BigInt) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toLocaleString) + #[must_use] + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &BigInt) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locale(self: &BigInt, locale: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_options( + self: &BigInt, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &BigInt) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_radix(self: &BigInt, radix: u8) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/valueOf) + #[must_use] + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &BigInt) -> BigInt; +} + +impl Eq for BigInt {} + +impl BigInt { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt) + pub fn new(value: &JsValue) -> Result { + bigint_constructor(value) + } } diff --git a/client/js-sys/src/builtins/boolean.rs b/client/js-sys/src/builtins/boolean.rs new file mode 100644 index 00000000..6c55d3b4 --- /dev/null +++ b/client/js-sys/src/builtins/boolean.rs @@ -0,0 +1,38 @@ +use super::Object; +use crate::{JsString, JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean) + #[js_sys(js_name = "Boolean", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Boolean; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/Boolean) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Boolean; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/Boolean) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_value(value: &JsValue) -> Boolean; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Boolean) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/valueOf) + #[must_use] + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Boolean) -> bool; +} + +impl Eq for Boolean {} + +impl Default for Boolean { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/data_view.rs b/client/js-sys/src/builtins/data_view.rs new file mode 100644 index 00000000..6e01d7ba --- /dev/null +++ b/client/js-sys/src/builtins/data_view.rs @@ -0,0 +1,284 @@ +use super::Object; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) + #[js_sys(js_name = "DataView", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DataView; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView) + #[js_sys(constructor)] + pub fn new(buffer: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView) + #[js_sys(constructor)] + pub fn new_with_offset(buffer: &JsValue, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView) + #[js_sys(constructor)] + pub fn new_with_offset_and_length( + buffer: &JsValue, + byte_offset: f64, + byte_length: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/buffer) + #[must_use] + #[js_sys(getter)] + pub fn buffer(self: &DataView) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/byteLength) + #[js_sys(getter = "byteLength")] + pub fn byte_length(self: &DataView) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/byteOffset) + #[js_sys(getter = "byteOffset")] + pub fn byte_offset(self: &DataView) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigInt64) + #[js_sys(js_name = "getBigInt64")] + pub fn get_big_int64(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigInt64) + #[js_sys(js_name = "getBigInt64")] + pub fn get_big_int64_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64) + #[js_sys(js_name = "getBigUint64")] + pub fn get_big_uint64(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64) + #[js_sys(js_name = "getBigUint64")] + pub fn get_big_uint64_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat16) + #[js_sys(js_name = "getFloat16")] + pub fn get_float16_as_f32(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat16) + #[js_sys(js_name = "getFloat16")] + pub fn get_float16_endian_as_f32( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat32) + #[js_sys(js_name = "getFloat32")] + pub fn get_float32(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat32) + #[js_sys(js_name = "getFloat32")] + pub fn get_float32_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat64) + #[js_sys(js_name = "getFloat64")] + pub fn get_float64(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat64) + #[js_sys(js_name = "getFloat64")] + pub fn get_float64_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt8) + #[js_sys(js_name = "getInt8")] + pub fn get_int8(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt16) + #[js_sys(js_name = "getInt16")] + pub fn get_int16(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt16) + #[js_sys(js_name = "getInt16")] + pub fn get_int16_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt32) + #[js_sys(js_name = "getInt32")] + pub fn get_int32(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt32) + #[js_sys(js_name = "getInt32")] + pub fn get_int32_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint8) + #[js_sys(js_name = "getUint8")] + pub fn get_uint8(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint16) + #[js_sys(js_name = "getUint16")] + pub fn get_uint16(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint16) + #[js_sys(js_name = "getUint16")] + pub fn get_uint16_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint32) + #[js_sys(js_name = "getUint32")] + pub fn get_uint32(self: &DataView, byte_offset: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint32) + #[js_sys(js_name = "getUint32")] + pub fn get_uint32_endian( + self: &DataView, + byte_offset: f64, + little_endian: bool, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigInt64) + #[js_sys(js_name = "setBigInt64")] + pub fn set_big_int64(self: &DataView, byte_offset: f64, value: i64) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigInt64) + #[js_sys(js_name = "setBigInt64")] + pub fn set_big_int64_endian( + self: &DataView, + byte_offset: f64, + value: i64, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64) + #[js_sys(js_name = "setBigUint64")] + pub fn set_big_uint64(self: &DataView, byte_offset: f64, value: u64) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64) + #[js_sys(js_name = "setBigUint64")] + pub fn set_big_uint64_endian( + self: &DataView, + byte_offset: f64, + value: u64, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat16) + #[js_sys(js_name = "setFloat16")] + pub fn set_float16_from_f32( + self: &DataView, + byte_offset: f64, + value: f32, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat16) + #[js_sys(js_name = "setFloat16")] + pub fn set_float16_endian_from_f32( + self: &DataView, + byte_offset: f64, + value: f32, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat32) + #[js_sys(js_name = "setFloat32")] + pub fn set_float32(self: &DataView, byte_offset: f64, value: f32) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat32) + #[js_sys(js_name = "setFloat32")] + pub fn set_float32_endian( + self: &DataView, + byte_offset: f64, + value: f32, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat64) + #[js_sys(js_name = "setFloat64")] + pub fn set_float64(self: &DataView, byte_offset: f64, value: f64) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat64) + #[js_sys(js_name = "setFloat64")] + pub fn set_float64_endian( + self: &DataView, + byte_offset: f64, + value: f64, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt8) + #[js_sys(js_name = "setInt8")] + pub fn set_int8(self: &DataView, byte_offset: f64, value: i8) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt16) + #[js_sys(js_name = "setInt16")] + pub fn set_int16(self: &DataView, byte_offset: f64, value: i16) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt16) + #[js_sys(js_name = "setInt16")] + pub fn set_int16_endian( + self: &DataView, + byte_offset: f64, + value: i16, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt32) + #[js_sys(js_name = "setInt32")] + pub fn set_int32(self: &DataView, byte_offset: f64, value: i32) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt32) + #[js_sys(js_name = "setInt32")] + pub fn set_int32_endian( + self: &DataView, + byte_offset: f64, + value: i32, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint8) + #[js_sys(js_name = "setUint8")] + pub fn set_uint8(self: &DataView, byte_offset: f64, value: u8) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint16) + #[js_sys(js_name = "setUint16")] + pub fn set_uint16(self: &DataView, byte_offset: f64, value: u16) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint16) + #[js_sys(js_name = "setUint16")] + pub fn set_uint16_endian( + self: &DataView, + byte_offset: f64, + value: u16, + little_endian: bool, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint32) + #[js_sys(js_name = "setUint32")] + pub fn set_uint32(self: &DataView, byte_offset: f64, value: u32) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint32) + #[js_sys(js_name = "setUint32")] + pub fn set_uint32_endian( + self: &DataView, + byte_offset: f64, + value: u32, + little_endian: bool, + ) -> Result<(), JsValue>; +} diff --git a/client/js-sys/src/builtins/date.rs b/client/js-sys/src/builtins/date.rs new file mode 100644 index 00000000..97cbdf7f --- /dev/null +++ b/client/js-sys/src/builtins/date.rs @@ -0,0 +1,530 @@ +use super::temporal::Temporal::Instant; +use super::{JsString, Object}; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[js_sys(constructor)] + pub fn new_with_value(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_milliseconds(milliseconds: f64) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_string(value: &str) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_date(value: &Date) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_year_month(year: f64, month: f64) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_year_month_day(year: f64, month: f64, day: f64) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour(year: f64, month: f64, day: f64, hour: f64) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute( + year: f64, + month: f64, + day: f64, + hour: f64, + minute: f64, + ) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute_second( + year: f64, + month: f64, + day: f64, + hour: f64, + minute: f64, + second: f64, + ) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute_second_millisecond( + year: f64, + month: f64, + day: f64, + hour: f64, + minute: f64, + second: f64, + millisecond: f64, + ) -> Date; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) + #[must_use] + #[js_sys(static_of = Date)] + pub fn now() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) + #[must_use] + #[js_sys(static_of = Date)] + pub fn parse(date: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC) + #[must_use] + #[js_sys(static_of = Date, js_name = "UTC")] + pub fn utc(year: f64, month: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC) + #[must_use] + #[js_sys(static_of = Date, js_name = "UTC")] + pub fn utc_with_day(year: f64, month: f64, day: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC) + #[must_use] + #[js_sys(static_of = Date, js_name = "UTC")] + pub fn utc_with_day_hour(year: f64, month: f64, day: f64, hour: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC) + #[must_use] + #[js_sys(static_of = Date, js_name = "UTC")] + pub fn utc_with_day_hour_minute(year: f64, month: f64, day: f64, hour: f64, minute: f64) + -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC) + #[must_use] + #[js_sys(static_of = Date, js_name = "UTC")] + pub fn utc_with_day_hour_minute_second( + year: f64, + month: f64, + day: f64, + hour: f64, + minute: f64, + second: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC) + #[must_use] + #[js_sys(static_of = Date, js_name = "UTC")] + pub fn utc_with_day_hour_minute_second_millisecond( + year: f64, + month: f64, + day: f64, + hour: f64, + minute: f64, + second: f64, + millisecond: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDate) + #[must_use] + #[js_sys(js_name = "getDate")] + pub fn get_date(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDay) + #[must_use] + #[js_sys(js_name = "getDay")] + pub fn get_day(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear) + #[must_use] + #[js_sys(js_name = "getFullYear")] + pub fn get_full_year(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getHours) + #[must_use] + #[js_sys(js_name = "getHours")] + pub fn get_hours(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMilliseconds) + #[must_use] + #[js_sys(js_name = "getMilliseconds")] + pub fn get_milliseconds(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMinutes) + #[must_use] + #[js_sys(js_name = "getMinutes")] + pub fn get_minutes(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMonth) + #[must_use] + #[js_sys(js_name = "getMonth")] + pub fn get_month(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getSeconds) + #[must_use] + #[js_sys(js_name = "getSeconds")] + pub fn get_seconds(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime) + #[must_use] + #[js_sys(js_name = "getTime")] + pub fn get_time(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset) + #[must_use] + #[js_sys(js_name = "getTimezoneOffset")] + pub fn get_timezone_offset(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCDate) + #[must_use] + #[js_sys(js_name = "getUTCDate")] + pub fn get_utc_date(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCDay) + #[must_use] + #[js_sys(js_name = "getUTCDay")] + pub fn get_utc_day(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCFullYear) + #[must_use] + #[js_sys(js_name = "getUTCFullYear")] + pub fn get_utc_full_year(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCHours) + #[must_use] + #[js_sys(js_name = "getUTCHours")] + pub fn get_utc_hours(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMilliseconds) + #[must_use] + #[js_sys(js_name = "getUTCMilliseconds")] + pub fn get_utc_milliseconds(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMinutes) + #[must_use] + #[js_sys(js_name = "getUTCMinutes")] + pub fn get_utc_minutes(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMonth) + #[must_use] + #[js_sys(js_name = "getUTCMonth")] + pub fn get_utc_month(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCSeconds) + #[must_use] + #[js_sys(js_name = "getUTCSeconds")] + pub fn get_utc_seconds(self: &Date) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate) + #[must_use] + #[js_sys(js_name = "setDate")] + pub fn set_date(self: &Date, date: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear) + #[must_use] + #[js_sys(js_name = "setFullYear")] + pub fn set_full_year(self: &Date, year: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear) + #[must_use] + #[js_sys(js_name = "setFullYear")] + pub fn set_full_year_with_month(self: &Date, year: f64, month: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear) + #[must_use] + #[js_sys(js_name = "setFullYear")] + pub fn set_full_year_with_month_date(self: &Date, year: f64, month: f64, date: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setHours) + #[must_use] + #[js_sys(js_name = "setHours")] + pub fn set_hours(self: &Date, hours: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setHours) + #[must_use] + #[js_sys(js_name = "setHours")] + pub fn set_hours_with_minutes(self: &Date, hours: f64, minutes: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setHours) + #[must_use] + #[js_sys(js_name = "setHours")] + pub fn set_hours_with_minutes_seconds( + self: &Date, + hours: f64, + minutes: f64, + seconds: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setHours) + #[must_use] + #[js_sys(js_name = "setHours")] + pub fn set_hours_with_minutes_seconds_milliseconds( + self: &Date, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMilliseconds) + #[must_use] + #[js_sys(js_name = "setMilliseconds")] + pub fn set_milliseconds(self: &Date, milliseconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMinutes) + #[must_use] + #[js_sys(js_name = "setMinutes")] + pub fn set_minutes(self: &Date, minutes: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMinutes) + #[must_use] + #[js_sys(js_name = "setMinutes")] + pub fn set_minutes_with_seconds(self: &Date, minutes: f64, seconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMinutes) + #[must_use] + #[js_sys(js_name = "setMinutes")] + pub fn set_minutes_with_seconds_milliseconds( + self: &Date, + minutes: f64, + seconds: f64, + milliseconds: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMonth) + #[must_use] + #[js_sys(js_name = "setMonth")] + pub fn set_month(self: &Date, month: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMonth) + #[must_use] + #[js_sys(js_name = "setMonth")] + pub fn set_month_with_date(self: &Date, month: f64, date: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setSeconds) + #[must_use] + #[js_sys(js_name = "setSeconds")] + pub fn set_seconds(self: &Date, seconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setSeconds) + #[must_use] + #[js_sys(js_name = "setSeconds")] + pub fn set_seconds_with_milliseconds(self: &Date, seconds: f64, milliseconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setTime) + #[must_use] + #[js_sys(js_name = "setTime")] + pub fn set_time(self: &Date, time: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCDate) + #[must_use] + #[js_sys(js_name = "setUTCDate")] + pub fn set_utc_date(self: &Date, date: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCFullYear) + #[must_use] + #[js_sys(js_name = "setUTCFullYear")] + pub fn set_utc_full_year(self: &Date, year: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCFullYear) + #[must_use] + #[js_sys(js_name = "setUTCFullYear")] + pub fn set_utc_full_year_with_month(self: &Date, year: f64, month: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCFullYear) + #[must_use] + #[js_sys(js_name = "setUTCFullYear")] + pub fn set_utc_full_year_with_month_date(self: &Date, year: f64, month: f64, date: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCHours) + #[must_use] + #[js_sys(js_name = "setUTCHours")] + pub fn set_utc_hours(self: &Date, hours: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCHours) + #[must_use] + #[js_sys(js_name = "setUTCHours")] + pub fn set_utc_hours_with_minutes(self: &Date, hours: f64, minutes: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCHours) + #[must_use] + #[js_sys(js_name = "setUTCHours")] + pub fn set_utc_hours_with_minutes_seconds( + self: &Date, + hours: f64, + minutes: f64, + seconds: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCHours) + #[must_use] + #[js_sys(js_name = "setUTCHours")] + pub fn set_utc_hours_with_minutes_seconds_milliseconds( + self: &Date, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMilliseconds) + #[must_use] + #[js_sys(js_name = "setUTCMilliseconds")] + pub fn set_utc_milliseconds(self: &Date, milliseconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMinutes) + #[must_use] + #[js_sys(js_name = "setUTCMinutes")] + pub fn set_utc_minutes(self: &Date, minutes: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMinutes) + #[must_use] + #[js_sys(js_name = "setUTCMinutes")] + pub fn set_utc_minutes_with_seconds(self: &Date, minutes: f64, seconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMinutes) + #[must_use] + #[js_sys(js_name = "setUTCMinutes")] + pub fn set_utc_minutes_with_seconds_milliseconds( + self: &Date, + minutes: f64, + seconds: f64, + milliseconds: f64, + ) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMonth) + #[must_use] + #[js_sys(js_name = "setUTCMonth")] + pub fn set_utc_month(self: &Date, month: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMonth) + #[must_use] + #[js_sys(js_name = "setUTCMonth")] + pub fn set_utc_month_with_date(self: &Date, month: f64, date: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCSeconds) + #[must_use] + #[js_sys(js_name = "setUTCSeconds")] + pub fn set_utc_seconds(self: &Date, seconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCSeconds) + #[must_use] + #[js_sys(js_name = "setUTCSeconds")] + pub fn set_utc_seconds_with_milliseconds(self: &Date, seconds: f64, milliseconds: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toDateString) + #[must_use] + #[js_sys(js_name = "toDateString")] + pub fn to_date_string(self: &Date) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + #[js_sys(js_name = "toISOString")] + pub fn to_iso_string(self: &Date) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &Date) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString) + #[js_sys(js_name = "toLocaleDateString")] + pub fn to_locale_date_string(self: &Date) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString) + #[js_sys(js_name = "toLocaleDateString")] + pub fn to_locale_date_string_with_locales( + self: &Date, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString) + #[js_sys(js_name = "toLocaleDateString")] + pub fn to_locale_date_string_with_locales_and_options( + self: &Date, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &Date) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &Date, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &Date, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString) + #[js_sys(js_name = "toLocaleTimeString")] + pub fn to_locale_time_string(self: &Date) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString) + #[js_sys(js_name = "toLocaleTimeString")] + pub fn to_locale_time_string_with_locales( + self: &Date, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString) + #[js_sys(js_name = "toLocaleTimeString")] + pub fn to_locale_time_string_with_locales_and_options( + self: &Date, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Date) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toTemporalInstant) + #[js_sys(js_name = "toTemporalInstant")] + pub fn to_temporal_instant(self: &Date) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toTimeString) + #[must_use] + #[js_sys(js_name = "toTimeString")] + pub fn to_time_string(self: &Date) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString) + #[must_use] + #[js_sys(js_name = "toUTCString")] + pub fn to_utc_string(self: &Date) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf) + #[must_use] + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Date) -> f64; +} + +impl Default for Date { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/disposable_stack.rs b/client/js-sys/src/builtins/disposable_stack.rs new file mode 100644 index 00000000..091aaafa --- /dev/null +++ b/client/js-sys/src/builtins/disposable_stack.rs @@ -0,0 +1,71 @@ +use super::{Function, Object}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DisposableStack; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/DisposableStack) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> DisposableStack; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/adopt) + #[js_sys(return_abi = Result)] + pub fn adopt( + self: &DisposableStack, + #[js_sys(type = &JsValue)] value: &T, + on_dispose: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/defer) + pub fn defer(self: &DisposableStack, on_dispose: &Function) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/dispose) + pub fn dispose(self: &DisposableStack) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/disposed) + #[must_use] + #[js_sys(getter)] + pub fn disposed(self: &DisposableStack) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/move) + #[js_sys(js_name = "move")] + pub fn move_(self: &DisposableStack) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/use) + #[js_sys(js_name = "use", return_abi = Result)] + pub fn use_( + self: &DisposableStack, + #[js_sys(type = &JsValue)] value: &T, + ) -> Result; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "disposable_stack.symbol_dispose")] + fn disposable_stack_symbol_dispose(stack: &DisposableStack) -> Result<(), JsValue>; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "disposable_stack.symbol_dispose", + "(stack) => stack[Symbol.dispose]()", +); + +impl DisposableStack { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack/Symbol.dispose) + pub fn symbol_dispose(&self) -> Result<(), JsValue> { + disposable_stack_symbol_dispose(self) + } +} + +impl Default for DisposableStack { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/dynamic_function.rs b/client/js-sys/src/builtins/dynamic_function.rs new file mode 100644 index 00000000..b303f288 --- /dev/null +++ b/client/js-sys/src/builtins/dynamic_function.rs @@ -0,0 +1,176 @@ +use core::fmt::{self, Formatter}; + +use super::{AsyncGenerator, Function, Generator, Object, Promise}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/GeneratorFunction) + #[js_sys(js_name = "GeneratorFunction", extends = Function, extends = Object)] + pub type GeneratorFunction; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) + #[js_sys(variadic, return_abi = Result)] + pub fn call( + self: &GeneratorFunction, + this_arg: &JsValue, + args: &[JsValue], + ) -> Result, JsValue>; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGeneratorFunction) + #[js_sys( + js_name = "AsyncGeneratorFunction", + extends = Function, + extends = Object + )] + pub type AsyncGeneratorFunction; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) + #[js_sys(variadic, return_abi = Result)] + pub fn call( + self: &AsyncGeneratorFunction, + this_arg: &JsValue, + args: &[JsValue], + ) -> Result, JsValue>; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction) + #[js_sys(js_name = "AsyncFunction", extends = Function, extends = Object)] + pub type AsyncFunction; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) + #[js_sys(variadic, return_abi = Result)] + pub fn call( + self: &AsyncFunction, + this_arg: &JsValue, + args: &[JsValue], + ) -> Result, JsValue>; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys( + js_embed = "generator_function.new", + return_abi = Result + )] + fn generator_function_new(body: &str) -> Result, JsValue>; + + #[js_sys( + js_embed = "generator_function.new", + return_abi = Result + )] + fn generator_function_new_with_args( + args: &str, + body: &str, + ) -> Result, JsValue>; + + #[js_sys( + js_embed = "async_generator_function.new", + return_abi = Result + )] + fn async_generator_function_new( + body: &str, + ) -> Result, JsValue>; + + #[js_sys( + js_embed = "async_generator_function.new", + return_abi = Result + )] + fn async_generator_function_new_with_args( + args: &str, + body: &str, + ) -> Result, JsValue>; + + #[js_sys( + js_embed = "async_function.new", + return_abi = Result + )] + fn async_function_new(body: &str) -> Result, JsValue>; + + #[js_sys( + js_embed = "async_function.new", + return_abi = Result + )] + fn async_function_new_with_args(args: &str, body: &str) + -> Result, JsValue>; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "generator_function.new", + "(...args) => new ((function* () {{}}).constructor)(...args)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_generator_function.new", + "(...args) => new ((async function* () {{}}).constructor)(...args)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_function.new", + "(...args) => new ((async function () {{}}).constructor)(...args)", +); + +impl GeneratorFunction { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/GeneratorFunction/GeneratorFunction) + pub fn new(body: &str) -> Result { + generator_function_new(body) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/GeneratorFunction/GeneratorFunction) + pub fn new_with_args(args: &str, body: &str) -> Result { + generator_function_new_with_args(args, body) + } +} + +impl AsyncGeneratorFunction { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGeneratorFunction/AsyncGeneratorFunction) + pub fn new(body: &str) -> Result { + async_generator_function_new(body) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGeneratorFunction/AsyncGeneratorFunction) + pub fn new_with_args(args: &str, body: &str) -> Result { + async_generator_function_new_with_args(args, body) + } +} + +impl AsyncFunction { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction/AsyncFunction) + pub fn new(body: &str) -> Result { + async_function_new(body) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction/AsyncFunction) + pub fn new_with_args(args: &str, body: &str) -> Result { + async_function_new_with_args(args, body) + } +} + +macro_rules! impl_wrapper { + ($type:ident<$($generic:ident),+>) => { + impl<$($generic),+> Clone for $type<$($generic),+> { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } + } + + impl<$($generic),+> fmt::Debug for $type<$($generic),+> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } + } + }; +} + +impl_wrapper!(GeneratorFunction); +impl_wrapper!(AsyncGeneratorFunction); +impl_wrapper!(AsyncFunction); diff --git a/client/js-sys/src/builtins/error.rs b/client/js-sys/src/builtins/error.rs index 83a73085..6f43e690 100644 --- a/client/js-sys/src/builtins/error.rs +++ b/client/js-sys/src/builtins/error.rs @@ -1,4 +1,4 @@ -use super::object::Object; +use super::{Array, Object}; use crate::hazard::JsCast; use crate::{JsString, JsValue, js_sys}; @@ -20,7 +20,7 @@ extern "js-sys" { } impl ErrorOptions { - /// Construct a new `ErrorOptions` dictionary with the given `cause`. + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) #[must_use] pub fn new(cause: &JsValue) -> Self { let ret: Self = JsCast::unchecked_from(Object::new().into()); @@ -31,6 +31,7 @@ impl ErrorOptions { #[js_sys(js_sys = crate)] extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) #[js_sys(extends = Object)] #[derive(Clone, Debug)] pub type Error; @@ -45,6 +46,11 @@ extern "js-sys" { #[js_sys(constructor)] pub fn new_with_options(message: &str, options: &ErrorOptions) -> Error; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError) + #[must_use] + #[js_sys(static_of = Error, js_name = "isError")] + pub fn is_error(value: &JsValue) -> bool; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) #[must_use] #[js_sys(getter)] @@ -77,3 +83,133 @@ extern "js-sys" { #[js_sys(js_name = "toString")] pub fn to_string(self: &Error) -> JsString; } + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) + #[js_sys(extends = Error, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type AggregateError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError/AggregateError) + #[must_use] + #[js_sys(constructor)] + pub fn new(errors: &[JsValue]) -> AggregateError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError/AggregateError) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_message(errors: &[JsValue], message: &str) -> AggregateError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError/AggregateError) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_options( + errors: &[JsValue], + message: &str, + options: &ErrorOptions, + ) -> AggregateError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError/errors) + #[must_use] + #[js_sys(getter = "errors")] + pub fn errors(self: &AggregateError) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError/errors) + #[js_sys(setter)] + pub fn set_errors(self: &AggregateError, errors: &Array); +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError) + #[js_sys(extends = Error, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type SuppressedError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError/SuppressedError) + #[must_use] + #[js_sys(constructor)] + pub fn new(error: &JsValue, suppressed: &JsValue) -> SuppressedError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError/SuppressedError) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_message( + error: &JsValue, + suppressed: &JsValue, + message: &str, + ) -> SuppressedError; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError/error) + #[must_use] + #[js_sys(getter)] + pub fn error(self: &SuppressedError) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError/error) + #[js_sys(setter)] + pub fn set_error(self: &SuppressedError, error: &JsValue); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError/suppressed) + #[must_use] + #[js_sys(getter)] + pub fn suppressed(self: &SuppressedError) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError/suppressed) + #[js_sys(setter)] + pub fn set_suppressed(self: &SuppressedError, suppressed: &JsValue); +} + +macro_rules! standard_error_types { + ($( + $type:ident = $js_name:literal { + type_doc = $type_doc:literal, + constructor_doc = $constructor_doc:literal, + } + )*) => {$( + #[js_sys(js_sys = crate)] + extern "js-sys" { + #[doc = $type_doc] + #[js_sys(js_name = $js_name, extends = Error, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type $type; + + #[doc = $constructor_doc] + #[must_use] + #[js_sys(constructor)] + pub fn new(message: &str) -> $type; + + #[doc = $constructor_doc] + #[must_use] + #[js_sys(constructor)] + pub fn new_with_options(message: &str, options: &ErrorOptions) -> $type; + } + )*}; +} + +standard_error_types! { + EvalError = "EvalError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError/EvalError)", + } + RangeError = "RangeError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError/RangeError)", + } + ReferenceError = "ReferenceError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError/ReferenceError)", + } + SyntaxError = "SyntaxError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError/SyntaxError)", + } + TypeError = "TypeError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError/TypeError)", + } + UriError = "URIError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError/URIError)", + } +} diff --git a/client/js-sys/src/builtins/finalization_registry.rs b/client/js-sys/src/builtins/finalization_registry.rs new file mode 100644 index 00000000..7f43ecbe --- /dev/null +++ b/client/js-sys/src/builtins/finalization_registry.rs @@ -0,0 +1,36 @@ +use super::{Function, Object}; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type FinalizationRegistry; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry/FinalizationRegistry) + #[js_sys(constructor)] + pub fn new(cleanup_callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry/register) + pub fn register( + self: &FinalizationRegistry, + target: &JsValue, + held_value: &JsValue, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry/register) + #[js_sys(js_name = "register")] + pub fn register_with_token( + self: &FinalizationRegistry, + target: &JsValue, + held_value: &JsValue, + unregister_token: &JsValue, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry/unregister) + pub fn unregister( + self: &FinalizationRegistry, + unregister_token: &JsValue, + ) -> Result; +} diff --git a/client/js-sys/src/builtins/generator.rs b/client/js-sys/src/builtins/generator.rs new file mode 100644 index 00000000..4ed7f407 --- /dev/null +++ b/client/js-sys/src/builtins/generator.rs @@ -0,0 +1,110 @@ +use core::fmt::{self, Formatter}; + +use super::{AsyncIterable, AsyncIterator, Iterable, IteratorResult, JsIterator, Object, Promise}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) + #[js_sys(extends = JsIterator, extends = Object)] + pub type Generator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next) + pub fn next(self: &Generator) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next) + #[js_sys(js_name = "next")] + pub fn next_with( + self: &Generator, + #[js_sys(type = &JsValue)] value: &N, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return) + #[js_sys(js_name = "return")] + pub fn return_(self: &Generator) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return) + #[js_sys(js_name = "return")] + pub fn return_with( + self: &Generator, + #[js_sys(type = &JsValue)] value: &R, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw) + #[js_sys(js_name = "throw")] + pub fn throw(self: &Generator) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw) + #[js_sys(js_name = "throw")] + pub fn throw_with( + self: &Generator, + error: &JsValue, + ) -> Result; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) + #[js_sys(extends = AsyncIterator, extends = Object)] + pub type AsyncGenerator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/next) + pub fn next(self: &AsyncGenerator) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/next) + #[js_sys(js_name = "next")] + pub fn next_with( + self: &AsyncGenerator, + #[js_sys(type = &JsValue)] value: &N, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/return) + #[js_sys(js_name = "return")] + pub fn return_(self: &AsyncGenerator) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/return) + #[js_sys(js_name = "return")] + pub fn return_with( + self: &AsyncGenerator, + #[js_sys(type = &JsValue)] value: &R, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/throw) + #[js_sys(js_name = "throw")] + pub fn throw(self: &AsyncGenerator) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/throw) + #[js_sys(js_name = "throw")] + pub fn throw_with( + self: &AsyncGenerator, + error: &JsValue, + ) -> Promise; +} + +macro_rules! impl_wrapper { + ($type:ident<$($generic:ident),+>) => { + impl<$($generic),+> Clone for $type<$($generic),+> { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } + } + + impl<$($generic),+> fmt::Debug for $type<$($generic),+> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } + } + }; +} + +impl_wrapper!(Generator); +impl_wrapper!(AsyncGenerator); + +impl Iterable for Generator { + type Item = Y; +} + +impl AsyncIterable for AsyncGenerator { + type Item = Y; +} diff --git a/client/js-sys/src/builtins/global.rs b/client/js-sys/src/builtins/global.rs new file mode 100644 index 00000000..bc3b49e5 --- /dev/null +++ b/client/js-sys/src/builtins/global.rs @@ -0,0 +1,59 @@ +use crate::{JsString, JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) + #[js_sys(js_name = "decodeURI")] + pub fn decode_uri(uri: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) + #[js_sys(js_name = "decodeURIComponent")] + pub fn decode_uri_component(component: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) + #[must_use] + #[js_sys(js_name = "encodeURI")] + pub fn encode_uri(uri: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) + #[must_use] + #[js_sys(js_name = "encodeURIComponent")] + pub fn encode_uri_component(component: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) + pub fn eval(source: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) + #[js_sys(js_name = "isFinite")] + pub fn is_finite(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) + #[js_sys(js_name = "isNaN")] + pub fn is_nan(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat) + #[must_use] + #[js_sys(js_name = "parseFloat")] + pub fn parse_float(value: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt) + #[must_use] + #[js_sys(js_name = "parseInt")] + pub fn parse_int(value: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt) + #[must_use] + #[js_sys(js_name = "parseInt")] + pub fn parse_int_with_radix(value: &str, radix: u8) -> f64; + +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) + #[must_use] + #[js_sys(js_embed = "global.this")] + pub fn global_this() -> JsValue; +} + +js_bindgen::embed_js!(module = "js_sys", name = "global.this", "() => globalThis"); diff --git a/client/js-sys/src/builtins/intl/collator.rs b/client/js-sys/src/builtins/intl/collator.rs new file mode 100644 index 00000000..d19205a2 --- /dev/null +++ b/client/js-sys/src/builtins/intl/collator.rs @@ -0,0 +1,326 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, Function, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#usage) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CollatorUsage { + Sort, + Search, +} + +impl CollatorUsage { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::Sort => "sort", + Self::Search => "search", + } + } + + pub(super) fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "sort" => Some(Self::Sort), + "search" => Some(Self::Search), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#sensitivity) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CollatorSensitivity { + Base, + Accent, + Case, + Variant, +} + +impl CollatorSensitivity { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::Base => "base", + Self::Accent => "accent", + Self::Case => "case", + Self::Variant => "variant", + } + } + + pub(super) fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "base" => Some(Self::Base), + "accent" => Some(Self::Accent), + "case" => Some(Self::Case), + "variant" => Some(Self::Variant), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#casefirst) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CollatorCaseFirst { + Upper, + Lower, + False, +} + +impl CollatorCaseFirst { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::Upper => "upper", + Self::Lower => "lower", + Self::False => "false", + } + } + + pub(super) fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "upper" => Some(Self::Upper), + "lower" => Some(Self::Lower), + "false" => Some(Self::False), + _ => None, + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator) + #[js_sys(js_name = "Collator", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Collator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type CollatorOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type CollatorResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Collator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &CollatorOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf) + #[js_sys(static_of = Collator, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf) + #[js_sys(static_of = Collator, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/compare) + #[must_use] + #[js_sys(getter)] + pub fn compare(self: &Collator) -> Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &Collator) -> CollatorResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &CollatorResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn usage(self: &CollatorResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn sensitivity(self: &CollatorResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "ignorePunctuation")] + pub fn ignore_punctuation(self: &CollatorResolvedOptions) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn collation(self: &CollatorResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn numeric(self: &CollatorResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "caseFirst")] + pub fn case_first(self: &CollatorResolvedOptions) -> Option; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &CollatorOptions) -> Option; + + #[js_sys(getter = "usage")] + fn usage_raw(self: &CollatorOptions) -> Option; + + #[js_sys(getter = "collation")] + fn collation_raw(self: &CollatorOptions) -> Option; + + #[js_sys(getter = "numeric")] + fn numeric_raw(self: &CollatorOptions) -> Option; + + #[js_sys(getter = "caseFirst")] + fn case_first_raw(self: &CollatorOptions) -> Option; + + #[js_sys(getter = "sensitivity")] + fn sensitivity_raw(self: &CollatorOptions) -> Option; + + #[js_sys(getter = "ignorePunctuation")] + fn ignore_punctuation_raw(self: &CollatorOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &CollatorOptions, value: &str); + + #[js_sys(setter = "usage")] + fn set_usage_raw(self: &CollatorOptions, value: &str); + + #[js_sys(setter = "collation")] + fn set_collation_raw(self: &CollatorOptions, value: &str); + + #[js_sys(setter = "numeric")] + fn set_numeric_raw(self: &CollatorOptions, value: bool); + + #[js_sys(setter = "caseFirst")] + fn set_case_first_raw(self: &CollatorOptions, value: &str); + + #[js_sys(setter = "sensitivity")] + fn set_sensitivity_raw(self: &CollatorOptions, value: &str); + + #[js_sys(setter = "ignorePunctuation")] + fn set_ignore_punctuation_raw(self: &CollatorOptions, value: bool); +} + +impl CollatorOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#usage) + #[must_use] + pub fn usage(&self) -> Option { + self.usage_raw() + .as_ref() + .and_then(CollatorUsage::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#usage) + pub fn set_usage(&self, value: CollatorUsage) { + self.set_usage_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#collation) + #[must_use] + pub fn collation(&self) -> Option { + self.collation_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#collation) + pub fn set_collation(&self, value: &str) { + self.set_collation_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#numeric) + #[must_use] + pub fn numeric(&self) -> Option { + self.numeric_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#numeric) + pub fn set_numeric(&self, value: bool) { + self.set_numeric_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#casefirst) + #[must_use] + pub fn case_first(&self) -> Option { + self.case_first_raw() + .as_ref() + .and_then(CollatorCaseFirst::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#casefirst) + pub fn set_case_first(&self, value: CollatorCaseFirst) { + self.set_case_first_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#sensitivity) + #[must_use] + pub fn sensitivity(&self) -> Option { + self.sensitivity_raw() + .as_ref() + .and_then(CollatorSensitivity::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#sensitivity) + pub fn set_sensitivity(&self, value: CollatorSensitivity) { + self.set_sensitivity_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#ignorepunctuation) + #[must_use] + pub fn ignore_punctuation(&self) -> Option { + self.ignore_punctuation_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#ignorepunctuation) + pub fn set_ignore_punctuation(&self, value: bool) { + self.set_ignore_punctuation_raw(value); + } +} + +impl Default for CollatorOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for Collator { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/intl/date_time_format.rs b/client/js-sys/src/builtins/intl/date_time_format.rs new file mode 100644 index 00000000..97cea3cc --- /dev/null +++ b/client/js-sys/src/builtins/intl/date_time_format.rs @@ -0,0 +1,883 @@ +use alloc::string::String; + +use super::locale::HourCycle; +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, Function, JsString, JsValue, Object, js_sys}; + +macro_rules! string_enum { + ( + $(#[$meta:meta])* + pub enum $name:ident { + $($variant:ident => $value:literal),+ $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum $name { + $($variant),+ + } + + impl $name { + const fn as_str(self) -> &'static str { + match self { + $(Self::$variant => $value),+ + } + } + + fn from_str(value: &str) -> Option { + match value { + $($value => Some(Self::$variant)),+, + _ => None, + } + } + } + }; +} + +macro_rules! readonly_string_enum { + ( + $(#[$meta:meta])* + pub enum $name:ident { + $($variant:ident => $value:literal),+ $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum $name { + $($variant),+ + } + + impl $name { + fn from_str(value: &str) -> Option { + match value { + $($value => Some(Self::$variant)),+, + _ => None, + } + } + } + }; +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#date-time_component_options) + pub enum DateTimeFormatTextStyle { + Long => "long", + Short => "short", + Narrow => "narrow", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#date-time_component_options) + pub enum DateTimeFormatNumericStyle { + Numeric => "numeric", + TwoDigit => "2-digit", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#month) + pub enum DateTimeFormatMonthStyle { + Numeric => "numeric", + TwoDigit => "2-digit", + Long => "long", + Short => "short", + Narrow => "narrow", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#formatmatcher) + pub enum DateTimeFormatMatcher { + Basic => "basic", + BestFit => "best fit", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#style_shortcuts) + pub enum DateTimeFormatStyle { + Full => "full", + Long => "long", + Medium => "medium", + Short => "short", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename) + pub enum DateTimeFormatTimeZoneName { + Long => "long", + Short => "short", + ShortOffset => "shortOffset", + LongOffset => "longOffset", + ShortGeneric => "shortGeneric", + LongGeneric => "longGeneric", + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#fractionalseconddigits) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DateTimeFormatFractionalSecondDigits { + One, + Two, + Three, +} + +impl DateTimeFormatFractionalSecondDigits { + const fn as_u8(self) -> u8 { + match self { + Self::One => 1, + Self::Two => 2, + Self::Three => 3, + } + } + + const fn from_u8(value: u8) -> Option { + match value { + 1 => Some(Self::One), + 2 => Some(Self::Two), + 3 => Some(Self::Three), + _ => None, + } + } +} + +readonly_string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#return_value) + pub enum DateTimeFormatPartType { + Weekday => "weekday", + Era => "era", + Year => "year", + Month => "month", + Day => "day", + DayPeriod => "dayPeriod", + Hour => "hour", + Minute => "minute", + Second => "second", + FractionalSecond => "fractionalSecond", + TimeZoneName => "timeZoneName", + Literal => "literal", + RelatedYear => "relatedYear", + YearName => "yearName", + Unknown => "unknown", + } +} + +readonly_string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRangeToParts#return_value) + pub enum DateTimeFormatRangeSource { + StartRange => "startRange", + EndRange => "endRange", + Shared => "shared", + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) + #[js_sys(js_name = "DateTimeFormat", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DateTimeFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DateTimeFormatOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DateTimeFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DateTimeFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRangeToParts#return_value) + #[js_sys(extends = DateTimeFormatPart)] + #[derive(Clone, Debug, PartialEq)] + pub type DateTimeRangeFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> DateTimeFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &DateTimeFormatOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/supportedLocalesOf) + #[js_sys(static_of = DateTimeFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/supportedLocalesOf) + #[js_sys(static_of = DateTimeFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format) + #[must_use] + #[js_sys(getter)] + pub fn format(self: &DateTimeFormat) -> Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts) + #[must_use] + #[js_sys(js_name = "formatToParts")] + pub fn format_to_parts(self: &DateTimeFormat) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts) + #[js_sys(js_name = "formatToParts")] + pub fn format_to_parts_with_date( + self: &DateTimeFormat, + date: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRange) + #[js_sys(js_name = "formatRange")] + pub fn format_range( + self: &DateTimeFormat, + start_date: &JsValue, + end_date: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRangeToParts) + #[js_sys(js_name = "formatRangeToParts")] + pub fn format_range_to_parts( + self: &DateTimeFormat, + start_date: &JsValue, + end_date: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &DateTimeFormat) -> DateTimeFormatResolvedOptions; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "calendar")] + fn calendar_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "calendar")] + fn set_calendar_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "numberingSystem")] + fn numbering_system_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "numberingSystem")] + fn set_numbering_system_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "hour12")] + fn hour_12_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "hour12")] + fn set_hour_12_raw(self: &DateTimeFormatOptions, value: bool); + + #[js_sys(getter = "hourCycle")] + fn hour_cycle_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "hourCycle")] + fn set_hour_cycle_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "timeZone")] + fn time_zone_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "timeZone")] + fn set_time_zone_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "weekday")] + fn weekday_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "weekday")] + fn set_weekday_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "era")] + fn era_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "era")] + fn set_era_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "year")] + fn year_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "year")] + fn set_year_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "month")] + fn month_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "month")] + fn set_month_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "day")] + fn day_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "day")] + fn set_day_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "dayPeriod")] + fn day_period_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "dayPeriod")] + fn set_day_period_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "hour")] + fn hour_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "hour")] + fn set_hour_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "minute")] + fn minute_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "minute")] + fn set_minute_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "second")] + fn second_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "second")] + fn set_second_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "fractionalSecondDigits")] + fn fractional_second_digits_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "fractionalSecondDigits")] + fn set_fractional_second_digits_raw(self: &DateTimeFormatOptions, value: u8); + + #[js_sys(getter = "timeZoneName")] + fn time_zone_name_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "timeZoneName")] + fn set_time_zone_name_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "formatMatcher")] + fn format_matcher_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "formatMatcher")] + fn set_format_matcher_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "dateStyle")] + fn date_style_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "dateStyle")] + fn set_date_style_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "timeStyle")] + fn time_style_raw(self: &DateTimeFormatOptions) -> Option; + + #[js_sys(setter = "timeStyle")] + fn set_time_style_raw(self: &DateTimeFormatOptions, value: &str); + + #[js_sys(getter = "locale")] + fn resolved_locale_raw(self: &DateTimeFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "calendar")] + fn resolved_calendar_raw(self: &DateTimeFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "numberingSystem")] + fn resolved_numbering_system_raw(self: &DateTimeFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "timeZone")] + fn resolved_time_zone_raw(self: &DateTimeFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "hourCycle")] + fn resolved_hour_cycle_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "hour12")] + fn resolved_hour_12_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "weekday")] + fn resolved_weekday_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "era")] + fn resolved_era_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "year")] + fn resolved_year_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "month")] + fn resolved_month_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "day")] + fn resolved_day_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "dayPeriod")] + fn resolved_day_period_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "hour")] + fn resolved_hour_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "minute")] + fn resolved_minute_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "second")] + fn resolved_second_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "fractionalSecondDigits")] + fn resolved_fractional_second_digits_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "timeZoneName")] + fn resolved_time_zone_name_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "dateStyle")] + fn resolved_date_style_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "timeStyle")] + fn resolved_time_style_raw(self: &DateTimeFormatResolvedOptions) -> Option; + + #[js_sys(getter = "type")] + fn part_type_raw(self: &DateTimeFormatPart) -> JsString; + + #[js_sys(getter = "value")] + fn part_value_raw(self: &DateTimeFormatPart) -> JsString; + + #[js_sys(getter = "source")] + fn range_source_raw(self: &DateTimeRangeFormatPart) -> JsString; +} + +fn parse_string_option(value: Option, parse: fn(&str) -> Option) -> Option { + value.and_then(|value| parse(&String::from(value))) +} + +impl DateTimeFormatOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#calendar) + #[must_use] + pub fn calendar(&self) -> Option { + self.calendar_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#calendar) + pub fn set_calendar(&self, value: &str) { + self.set_calendar_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#numberingsystem) + #[must_use] + pub fn numbering_system(&self) -> Option { + self.numbering_system_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#numberingsystem) + pub fn set_numbering_system(&self, value: &str) { + self.set_numbering_system_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#hour12) + #[must_use] + pub fn hour_12(&self) -> Option { + self.hour_12_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#hour12) + pub fn set_hour_12(&self, value: bool) { + self.set_hour_12_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#hourcycle) + #[must_use] + pub fn hour_cycle(&self) -> Option { + self.hour_cycle_raw() + .as_ref() + .and_then(HourCycle::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#hourcycle) + pub fn set_hour_cycle(&self, value: HourCycle) { + self.set_hour_cycle_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezone) + #[must_use] + pub fn time_zone(&self) -> Option { + self.time_zone_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezone) + pub fn set_time_zone(&self, value: &str) { + self.set_time_zone_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#weekday) + #[must_use] + pub fn weekday(&self) -> Option { + parse_string_option(self.weekday_raw(), DateTimeFormatTextStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#weekday) + pub fn set_weekday(&self, value: DateTimeFormatTextStyle) { + self.set_weekday_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#era) + #[must_use] + pub fn era(&self) -> Option { + parse_string_option(self.era_raw(), DateTimeFormatTextStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#era) + pub fn set_era(&self, value: DateTimeFormatTextStyle) { + self.set_era_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#year) + #[must_use] + pub fn year(&self) -> Option { + parse_string_option(self.year_raw(), DateTimeFormatNumericStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#year) + pub fn set_year(&self, value: DateTimeFormatNumericStyle) { + self.set_year_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#month) + #[must_use] + pub fn month(&self) -> Option { + parse_string_option(self.month_raw(), DateTimeFormatMonthStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#month) + pub fn set_month(&self, value: DateTimeFormatMonthStyle) { + self.set_month_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#day) + #[must_use] + pub fn day(&self) -> Option { + parse_string_option(self.day_raw(), DateTimeFormatNumericStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#day) + pub fn set_day(&self, value: DateTimeFormatNumericStyle) { + self.set_day_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#dayperiod) + #[must_use] + pub fn day_period(&self) -> Option { + parse_string_option(self.day_period_raw(), DateTimeFormatTextStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#dayperiod) + pub fn set_day_period(&self, value: DateTimeFormatTextStyle) { + self.set_day_period_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#hour) + #[must_use] + pub fn hour(&self) -> Option { + parse_string_option(self.hour_raw(), DateTimeFormatNumericStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#hour) + pub fn set_hour(&self, value: DateTimeFormatNumericStyle) { + self.set_hour_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#minute) + #[must_use] + pub fn minute(&self) -> Option { + parse_string_option(self.minute_raw(), DateTimeFormatNumericStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#minute) + pub fn set_minute(&self, value: DateTimeFormatNumericStyle) { + self.set_minute_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#second) + #[must_use] + pub fn second(&self) -> Option { + parse_string_option(self.second_raw(), DateTimeFormatNumericStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#second) + pub fn set_second(&self, value: DateTimeFormatNumericStyle) { + self.set_second_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#fractionalseconddigits) + #[must_use] + pub fn fractional_second_digits(&self) -> Option { + self.fractional_second_digits_raw() + .and_then(DateTimeFormatFractionalSecondDigits::from_u8) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#fractionalseconddigits) + pub fn set_fractional_second_digits(&self, value: DateTimeFormatFractionalSecondDigits) { + self.set_fractional_second_digits_raw(value.as_u8()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename) + #[must_use] + pub fn time_zone_name(&self) -> Option { + parse_string_option( + self.time_zone_name_raw(), + DateTimeFormatTimeZoneName::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename) + pub fn set_time_zone_name(&self, value: DateTimeFormatTimeZoneName) { + self.set_time_zone_name_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#formatmatcher) + #[must_use] + pub fn format_matcher(&self) -> Option { + parse_string_option(self.format_matcher_raw(), DateTimeFormatMatcher::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#formatmatcher) + pub fn set_format_matcher(&self, value: DateTimeFormatMatcher) { + self.set_format_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#datestyle) + #[must_use] + pub fn date_style(&self) -> Option { + parse_string_option(self.date_style_raw(), DateTimeFormatStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#datestyle) + pub fn set_date_style(&self, value: DateTimeFormatStyle) { + self.set_date_style_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timestyle) + #[must_use] + pub fn time_style(&self) -> Option { + parse_string_option(self.time_style_raw(), DateTimeFormatStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timestyle) + pub fn set_time_style(&self, value: DateTimeFormatStyle) { + self.set_time_style_raw(value.as_str()); + } +} + +impl Default for DateTimeFormatOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for DateTimeFormat { + fn default() -> Self { + Self::new() + } +} + +impl DateTimeFormatResolvedOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn locale(&self) -> JsString { + self.resolved_locale_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn calendar(&self) -> JsString { + self.resolved_calendar_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn numbering_system(&self) -> JsString { + self.resolved_numbering_system_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn time_zone(&self) -> JsString { + self.resolved_time_zone_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn hour_cycle(&self) -> Option { + self.resolved_hour_cycle_raw() + .as_ref() + .and_then(HourCycle::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn hour_12(&self) -> Option { + self.resolved_hour_12_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn weekday(&self) -> Option { + parse_string_option( + self.resolved_weekday_raw(), + DateTimeFormatTextStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn era(&self) -> Option { + parse_string_option(self.resolved_era_raw(), DateTimeFormatTextStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn year(&self) -> Option { + parse_string_option( + self.resolved_year_raw(), + DateTimeFormatNumericStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn month(&self) -> Option { + parse_string_option( + self.resolved_month_raw(), + DateTimeFormatMonthStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn day(&self) -> Option { + parse_string_option( + self.resolved_day_raw(), + DateTimeFormatNumericStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn day_period(&self) -> Option { + parse_string_option( + self.resolved_day_period_raw(), + DateTimeFormatTextStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn hour(&self) -> Option { + parse_string_option( + self.resolved_hour_raw(), + DateTimeFormatNumericStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn minute(&self) -> Option { + parse_string_option( + self.resolved_minute_raw(), + DateTimeFormatNumericStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn second(&self) -> Option { + parse_string_option( + self.resolved_second_raw(), + DateTimeFormatNumericStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn fractional_second_digits(&self) -> Option { + self.resolved_fractional_second_digits_raw() + .and_then(DateTimeFormatFractionalSecondDigits::from_u8) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn time_zone_name(&self) -> Option { + parse_string_option( + self.resolved_time_zone_name_raw(), + DateTimeFormatTimeZoneName::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn date_style(&self) -> Option { + parse_string_option( + self.resolved_date_style_raw(), + DateTimeFormatStyle::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#return_value) + #[must_use] + pub fn time_style(&self) -> Option { + parse_string_option( + self.resolved_time_style_raw(), + DateTimeFormatStyle::from_str, + ) + } +} + +impl DateTimeFormatPart { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#return_value) + #[must_use] + pub fn type_(&self) -> Option { + DateTimeFormatPartType::from_str(&String::from(self.part_type_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#return_value) + #[must_use] + pub fn value(&self) -> JsString { + self.part_value_raw() + } +} + +impl DateTimeRangeFormatPart { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatRangeToParts#return_value) + #[must_use] + pub fn source(&self) -> Option { + DateTimeFormatRangeSource::from_str(&String::from(self.range_source_raw())) + } +} diff --git a/client/js-sys/src/builtins/intl/display_names.rs b/client/js-sys/src/builtins/intl/display_names.rs new file mode 100644 index 00000000..34cfe686 --- /dev/null +++ b/client/js-sys/src/builtins/intl/display_names.rs @@ -0,0 +1,290 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#type) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DisplayNamesType { + Language, + Region, + Script, + Currency, + Calendar, + DateTimeField, +} + +impl DisplayNamesType { + const fn as_str(self) -> &'static str { + match self { + Self::Language => "language", + Self::Region => "region", + Self::Script => "script", + Self::Currency => "currency", + Self::Calendar => "calendar", + Self::DateTimeField => "dateTimeField", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "language" => Some(Self::Language), + "region" => Some(Self::Region), + "script" => Some(Self::Script), + "currency" => Some(Self::Currency), + "calendar" => Some(Self::Calendar), + "dateTimeField" => Some(Self::DateTimeField), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#style) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DisplayNamesStyle { + Long, + Short, + Narrow, +} + +impl DisplayNamesStyle { + const fn as_str(self) -> &'static str { + match self { + Self::Long => "long", + Self::Short => "short", + Self::Narrow => "narrow", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "long" => Some(Self::Long), + "short" => Some(Self::Short), + "narrow" => Some(Self::Narrow), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#fallback) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DisplayNamesFallback { + Code, + None, +} + +impl DisplayNamesFallback { + const fn as_str(self) -> &'static str { + match self { + Self::Code => "code", + Self::None => "none", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "code" => Some(Self::Code), + "none" => Some(Self::None), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#languagedisplay) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DisplayNamesLanguageDisplay { + Dialect, + Standard, +} + +impl DisplayNamesLanguageDisplay { + const fn as_str(self) -> &'static str { + match self { + Self::Dialect => "dialect", + Self::Standard => "standard", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "dialect" => Some(Self::Dialect), + "standard" => Some(Self::Standard), + _ => None, + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) + #[js_sys(js_name = "DisplayNames", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DisplayNames; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DisplayNamesOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DisplayNamesResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames) + #[js_sys(constructor)] + pub fn new(locales: &JsValue, options: &DisplayNamesOptions) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/supportedLocalesOf) + #[js_sys(static_of = DisplayNames, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/supportedLocalesOf) + #[js_sys(static_of = DisplayNames, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/of) + pub fn of(self: &DisplayNames, code: &str) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &DisplayNames) -> DisplayNamesResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &DisplayNamesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn style(self: &DisplayNamesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "type")] + pub fn display_names_type(self: &DisplayNamesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn fallback(self: &DisplayNamesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "languageDisplay")] + pub fn language_display(self: &DisplayNamesResolvedOptions) -> Option; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &DisplayNamesOptions) -> Option; + + #[js_sys(getter = "style")] + fn style_raw(self: &DisplayNamesOptions) -> Option; + + #[js_sys(getter = "type")] + fn type_raw(self: &DisplayNamesOptions) -> Option; + + #[js_sys(getter = "fallback")] + fn fallback_raw(self: &DisplayNamesOptions) -> Option; + + #[js_sys(getter = "languageDisplay")] + fn language_display_raw(self: &DisplayNamesOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &DisplayNamesOptions, value: &str); + + #[js_sys(setter = "style")] + fn set_style_raw(self: &DisplayNamesOptions, value: &str); + + #[js_sys(setter = "type")] + fn set_type_raw(self: &DisplayNamesOptions, value: &str); + + #[js_sys(setter = "fallback")] + fn set_fallback_raw(self: &DisplayNamesOptions, value: &str); + + #[js_sys(setter = "languageDisplay")] + fn set_language_display_raw(self: &DisplayNamesOptions, value: &str); +} + +impl DisplayNamesOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#options) + #[must_use] + pub fn new(display_names_type: DisplayNamesType) -> Self { + let options = Self::unchecked_from(Object::new().into()); + options.set_type(display_names_type); + options + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#style) + #[must_use] + pub fn style(&self) -> Option { + self.style_raw() + .as_ref() + .and_then(DisplayNamesStyle::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#style) + pub fn set_style(&self, value: DisplayNamesStyle) { + self.set_style_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#type) + #[must_use] + pub fn display_names_type(&self) -> Option { + self.type_raw() + .as_ref() + .and_then(DisplayNamesType::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#type) + pub fn set_type(&self, value: DisplayNamesType) { + self.set_type_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#fallback) + #[must_use] + pub fn fallback(&self) -> Option { + self.fallback_raw() + .as_ref() + .and_then(DisplayNamesFallback::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#fallback) + pub fn set_fallback(&self, value: DisplayNamesFallback) { + self.set_fallback_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#languagedisplay) + #[must_use] + pub fn language_display(&self) -> Option { + self.language_display_raw() + .as_ref() + .and_then(DisplayNamesLanguageDisplay::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#languagedisplay) + pub fn set_language_display(&self, value: DisplayNamesLanguageDisplay) { + self.set_language_display_raw(value.as_str()); + } +} diff --git a/client/js-sys/src/builtins/intl/duration_format.rs b/client/js-sys/src/builtins/intl/duration_format.rs new file mode 100644 index 00000000..03142ee6 --- /dev/null +++ b/client/js-sys/src/builtins/intl/duration_format.rs @@ -0,0 +1,562 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, js_sys}; + +macro_rules! string_enum { + ( + $(#[$meta:meta])* + pub enum $name:ident { + $($variant:ident => $value:literal),+ $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum $name { + $($variant),+ + } + + impl $name { + const fn as_str(self) -> &'static str { + match self { + $(Self::$variant => $value),+ + } + } + + fn from_str(value: &str) -> Option { + match value { + $($value => Some(Self::$variant)),+, + _ => None, + } + } + } + }; +} + +macro_rules! readonly_string_enum { + ( + $(#[$meta:meta])* + pub enum $name:ident { + $($variant:ident => $value:literal),+ $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum $name { + $($variant),+ + } + + impl $name { + fn from_str(value: &str) -> Option { + match value { + $($value => Some(Self::$variant)),+, + _ => None, + } + } + } + }; +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#style) + pub enum DurationFormatStyle { + Long => "long", + Short => "short", + Narrow => "narrow", + Digital => "digital", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#years) + pub enum DurationUnitStyle { + Long => "long", + Short => "short", + Narrow => "narrow", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#hours) + pub enum DurationTimeUnitStyle { + Long => "long", + Short => "short", + Narrow => "narrow", + Numeric => "numeric", + TwoDigit => "2-digit", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#milliseconds) + pub enum DurationSubsecondUnitStyle { + Long => "long", + Short => "short", + Narrow => "narrow", + Numeric => "numeric", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#yearsdisplay) + pub enum DurationUnitDisplay { + Always => "always", + Auto => "auto", + } +} + +readonly_string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts#return_value) + pub enum DurationFormatPartType { + Integer => "integer", + Group => "group", + Decimal => "decimal", + Fraction => "fraction", + Literal => "literal", + Unit => "unit", + MinusSign => "minusSign", + } +} + +readonly_string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts#return_value) + pub enum DurationUnit { + Year => "year", + Month => "month", + Week => "week", + Day => "day", + Hour => "hour", + Minute => "minute", + Second => "second", + Millisecond => "millisecond", + Microsecond => "microsecond", + Nanosecond => "nanosecond", + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat) + #[js_sys(js_name = "DurationFormat", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DurationFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DurationFormatOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DurationFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Duration; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type DurationFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> DurationFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &DurationFormatOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/supportedLocalesOf) + #[js_sys(static_of = DurationFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/supportedLocalesOf) + #[js_sys(static_of = DurationFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format) + pub fn format(self: &DurationFormat, duration: &Duration) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts) + #[js_sys(js_name = "formatToParts")] + pub fn format_to_parts( + self: &DurationFormat, + duration: &Duration, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &DurationFormat) -> DurationFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#numberingsystem) + #[must_use] + #[js_sys(getter = "numberingSystem")] + pub fn numbering_system(self: &DurationFormatOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#numberingsystem) + #[js_sys(setter = "numberingSystem")] + pub fn set_numbering_system(self: &DurationFormatOptions, value: &str); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#fractionaldigits) + #[must_use] + #[js_sys(getter = "fractionalDigits")] + pub fn fractional_digits(self: &DurationFormatOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#fractionaldigits) + #[js_sys(setter = "fractionalDigits")] + pub fn set_fractional_digits(self: &DurationFormatOptions, value: u8); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &DurationFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "numberingSystem")] + pub fn numbering_system(self: &DurationFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "fractionalDigits")] + pub fn fractional_digits(self: &DurationFormatResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn years(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_years(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn months(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_months(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn weeks(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_weeks(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn days(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_days(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn hours(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_hours(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn minutes(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_minutes(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn seconds(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_seconds(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn milliseconds(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_milliseconds(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn microseconds(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_microseconds(self: &Duration, value: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + #[js_sys(getter)] + pub fn nanoseconds(self: &Duration) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[js_sys(setter)] + pub fn set_nanoseconds(self: &Duration, value: f64); +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &DurationFormatOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &DurationFormatOptions, value: &str); + + #[js_sys(getter = "style")] + fn style_raw(self: &DurationFormatOptions) -> Option; + + #[js_sys(setter = "style")] + fn set_style_raw(self: &DurationFormatOptions, value: &str); + + #[js_sys(getter = "style")] + fn resolved_style_raw(self: &DurationFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "type")] + fn part_type_raw(self: &DurationFormatPart) -> JsString; + + #[js_sys(getter = "value")] + fn part_value_raw(self: &DurationFormatPart) -> JsString; + + #[js_sys(getter = "unit")] + fn part_unit_raw(self: &DurationFormatPart) -> Option; +} + +fn parse_string_option( + value: Option, + parse: impl FnOnce(&str) -> Option, +) -> Option { + let value = String::from(value?); + parse(&value) +} + +macro_rules! duration_unit_options { + ( + $( + $(#[$meta:meta])* + $get:ident, $set:ident, $get_raw:ident, $set_raw:ident, $resolved_raw:ident: + $ty:ty = $js_name:literal; + )+ + ) => { + #[js_sys(js_sys = crate)] + extern "js-sys" { + $( + #[js_sys(getter = $js_name)] + fn $get_raw(self: &DurationFormatOptions) -> Option; + + #[js_sys(setter = $js_name)] + fn $set_raw(self: &DurationFormatOptions, value: &str); + + #[js_sys(getter = $js_name)] + fn $resolved_raw(self: &DurationFormatResolvedOptions) -> JsString; + )+ + } + + impl DurationFormatOptions { + $( + $(#[$meta])* + #[must_use] + pub fn $get(&self) -> Option<$ty> { + parse_string_option(self.$get_raw(), <$ty>::from_str) + } + + $(#[$meta])* + pub fn $set(&self, value: $ty) { + self.$set_raw(value.as_str()); + } + )+ + } + + impl DurationFormatResolvedOptions { + $( + $(#[$meta])* + #[must_use] + pub fn $get(&self) -> Option<$ty> { + <$ty>::from_str(&String::from(self.$resolved_raw())) + } + )+ + } + }; +} + +duration_unit_options! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#years) + years, set_years, years_raw, set_years_raw, resolved_years_raw: + DurationUnitStyle = "years"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#yearsdisplay) + years_display, set_years_display, years_display_raw, set_years_display_raw, resolved_years_display_raw: + DurationUnitDisplay = "yearsDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#months) + months, set_months, months_raw, set_months_raw, resolved_months_raw: + DurationUnitStyle = "months"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#monthsdisplay) + months_display, set_months_display, months_display_raw, set_months_display_raw, resolved_months_display_raw: + DurationUnitDisplay = "monthsDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#weeks) + weeks, set_weeks, weeks_raw, set_weeks_raw, resolved_weeks_raw: + DurationUnitStyle = "weeks"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#weeksdisplay) + weeks_display, set_weeks_display, weeks_display_raw, set_weeks_display_raw, resolved_weeks_display_raw: + DurationUnitDisplay = "weeksDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#days) + days, set_days, days_raw, set_days_raw, resolved_days_raw: + DurationUnitStyle = "days"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#daysdisplay) + days_display, set_days_display, days_display_raw, set_days_display_raw, resolved_days_display_raw: + DurationUnitDisplay = "daysDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#hours) + hours, set_hours, hours_raw, set_hours_raw, resolved_hours_raw: + DurationTimeUnitStyle = "hours"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#hoursdisplay) + hours_display, set_hours_display, hours_display_raw, set_hours_display_raw, resolved_hours_display_raw: + DurationUnitDisplay = "hoursDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#minutes) + minutes, set_minutes, minutes_raw, set_minutes_raw, resolved_minutes_raw: + DurationTimeUnitStyle = "minutes"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#minutesdisplay) + minutes_display, set_minutes_display, minutes_display_raw, set_minutes_display_raw, resolved_minutes_display_raw: + DurationUnitDisplay = "minutesDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#seconds) + seconds, set_seconds, seconds_raw, set_seconds_raw, resolved_seconds_raw: + DurationTimeUnitStyle = "seconds"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#secondsdisplay) + seconds_display, set_seconds_display, seconds_display_raw, set_seconds_display_raw, resolved_seconds_display_raw: + DurationUnitDisplay = "secondsDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#milliseconds) + milliseconds, set_milliseconds, milliseconds_raw, set_milliseconds_raw, resolved_milliseconds_raw: + DurationSubsecondUnitStyle = "milliseconds"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#millisecondsdisplay) + milliseconds_display, set_milliseconds_display, milliseconds_display_raw, set_milliseconds_display_raw, resolved_milliseconds_display_raw: + DurationUnitDisplay = "millisecondsDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#microseconds) + microseconds, set_microseconds, microseconds_raw, set_microseconds_raw, resolved_microseconds_raw: + DurationSubsecondUnitStyle = "microseconds"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#microsecondsdisplay) + microseconds_display, set_microseconds_display, microseconds_display_raw, set_microseconds_display_raw, resolved_microseconds_display_raw: + DurationUnitDisplay = "microsecondsDisplay"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#nanoseconds) + nanoseconds, set_nanoseconds, nanoseconds_raw, set_nanoseconds_raw, resolved_nanoseconds_raw: + DurationSubsecondUnitStyle = "nanoseconds"; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#nanosecondsdisplay) + nanoseconds_display, set_nanoseconds_display, nanoseconds_display_raw, set_nanoseconds_display_raw, resolved_nanoseconds_display_raw: + DurationUnitDisplay = "nanosecondsDisplay"; +} + +impl DurationFormatOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#style) + #[must_use] + pub fn style(&self) -> Option { + parse_string_option(self.style_raw(), DurationFormatStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#style) + pub fn set_style(&self, value: DurationFormatStyle) { + self.set_style_raw(value.as_str()); + } +} + +impl DurationFormatResolvedOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/resolvedOptions#return_value) + #[must_use] + pub fn style(&self) -> Option { + DurationFormatStyle::from_str(&String::from(self.resolved_style_raw())) + } +} + +impl Duration { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#duration) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } +} + +impl DurationFormatPart { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts#return_value) + #[must_use] + pub fn type_(&self) -> Option { + DurationFormatPartType::from_str(&String::from(self.part_type_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts#return_value) + #[must_use] + pub fn value(&self) -> JsString { + self.part_value_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/formatToParts#return_value) + #[must_use] + pub fn unit(&self) -> Option { + parse_string_option(self.part_unit_raw(), DurationUnit::from_str) + } +} + +impl Default for DurationFormatOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for Duration { + fn default() -> Self { + Self::new() + } +} + +impl Default for DurationFormat { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/intl/list_format.rs b/client/js-sys/src/builtins/intl/list_format.rs new file mode 100644 index 00000000..98c7766b --- /dev/null +++ b/client/js-sys/src/builtins/intl/list_format.rs @@ -0,0 +1,229 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#style) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ListFormatStyle { + Long, + Short, + Narrow, +} + +impl ListFormatStyle { + const fn as_str(self) -> &'static str { + match self { + Self::Long => "long", + Self::Short => "short", + Self::Narrow => "narrow", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "long" => Some(Self::Long), + "short" => Some(Self::Short), + "narrow" => Some(Self::Narrow), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#type) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ListFormatType { + Conjunction, + Disjunction, + Unit, +} + +impl ListFormatType { + const fn as_str(self) -> &'static str { + match self { + Self::Conjunction => "conjunction", + Self::Disjunction => "disjunction", + Self::Unit => "unit", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "conjunction" => Some(Self::Conjunction), + "disjunction" => Some(Self::Disjunction), + "unit" => Some(Self::Unit), + _ => None, + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat) + #[js_sys(js_name = "ListFormat", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ListFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ListFormatOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ListFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/formatToParts#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ListFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> ListFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &ListFormatOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/supportedLocalesOf) + #[js_sys(static_of = ListFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/supportedLocalesOf) + #[js_sys(static_of = ListFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/format) + pub fn format(self: &ListFormat, list: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/formatToParts) + #[js_sys(js_name = "formatToParts")] + pub fn format_to_parts( + self: &ListFormat, + list: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &ListFormat) -> ListFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &ListFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "type")] + pub fn type_(self: &ListFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn style(self: &ListFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/formatToParts#return_value) + #[must_use] + #[js_sys(getter = "type")] + pub fn type_(self: &ListFormatPart) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/formatToParts#return_value) + #[must_use] + #[js_sys(getter)] + pub fn value(self: &ListFormatPart) -> JsString; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &ListFormatOptions) -> Option; + + #[js_sys(getter = "type")] + fn type_raw(self: &ListFormatOptions) -> Option; + + #[js_sys(getter = "style")] + fn style_raw(self: &ListFormatOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &ListFormatOptions, value: &str); + + #[js_sys(setter = "type")] + fn set_type_raw(self: &ListFormatOptions, value: &str); + + #[js_sys(setter = "style")] + fn set_style_raw(self: &ListFormatOptions, value: &str); +} + +impl ListFormatOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#type) + #[must_use] + pub fn type_(&self) -> Option { + self.type_raw() + .as_ref() + .and_then(ListFormatType::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#type) + pub fn set_type(&self, value: ListFormatType) { + self.set_type_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#style) + #[must_use] + pub fn style(&self) -> Option { + self.style_raw() + .as_ref() + .and_then(ListFormatStyle::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#style) + pub fn set_style(&self, value: ListFormatStyle) { + self.set_style_raw(value.as_str()); + } +} + +impl Default for ListFormatOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for ListFormat { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/intl/locale.rs b/client/js-sys/src/builtins/intl/locale.rs new file mode 100644 index 00000000..b5eb4353 --- /dev/null +++ b/client/js-sys/src/builtins/intl/locale.rs @@ -0,0 +1,489 @@ +use alloc::string::String; + +use super::collator::CollatorCaseFirst; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Number, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum HourCycle { + H11, + H12, + H23, + H24, +} + +impl HourCycle { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::H11 => "h11", + Self::H12 => "h12", + Self::H23 => "h23", + Self::H24 => "h24", + } + } + + pub(super) fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "h11" => Some(Self::H11), + "h12" => Some(Self::H12), + "h23" => Some(Self::H23), + "h24" => Some(Self::H24), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getTextInfo) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TextDirection { + LeftToRight, + RightToLeft, +} + +/// [`ECMA-402` proposal](https://tc39.es/proposal-intl-locale-info/#sec-intl.locale.prototype.firstdayofweek) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FirstDayOfWeek { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +impl FirstDayOfWeek { + const fn as_str(self) -> &'static str { + match self { + Self::Monday => "mon", + Self::Tuesday => "tue", + Self::Wednesday => "wed", + Self::Thursday => "thu", + Self::Friday => "fri", + Self::Saturday => "sat", + Self::Sunday => "sun", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "mon" => Some(Self::Monday), + "tue" => Some(Self::Tuesday), + "wed" => Some(Self::Wednesday), + "thu" => Some(Self::Thursday), + "fri" => Some(Self::Friday), + "sat" => Some(Self::Saturday), + "sun" => Some(Self::Sunday), + _ => None, + } + } +} + +impl TextDirection { + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "ltr" => Some(Self::LeftToRight), + "rtl" => Some(Self::RightToLeft), + _ => None, + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) + #[js_sys(js_name = "Locale", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Locale; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type LocaleOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type WeekInfo; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getTextInfo) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type TextInfo; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale) + #[js_sys(constructor)] + pub fn new(tag: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale) + #[js_sys(constructor)] + pub fn new_with_options(tag: &str, options: &LocaleOptions) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/baseName) + #[must_use] + #[js_sys(getter = "baseName")] + pub fn base_name(self: &Locale) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar) + #[must_use] + #[js_sys(getter)] + pub fn calendar(self: &Locale) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/collation) + #[must_use] + #[js_sys(getter)] + pub fn collation(self: &Locale) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/language) + #[must_use] + #[js_sys(getter)] + pub fn language(self: &Locale) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem) + #[must_use] + #[js_sys(getter = "numberingSystem")] + pub fn numbering_system(self: &Locale) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numeric) + #[must_use] + #[js_sys(getter)] + pub fn numeric(self: &Locale) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/region) + #[must_use] + #[js_sys(getter)] + pub fn region(self: &Locale) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/script) + #[must_use] + #[js_sys(getter)] + pub fn script(self: &Locale) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/variants) + #[must_use] + #[js_sys(getter)] + pub fn variants(self: &Locale) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getCalendars) + #[must_use] + #[js_sys(js_name = "getCalendars")] + pub fn get_calendars(self: &Locale) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getCollations) + #[must_use] + #[js_sys(js_name = "getCollations")] + pub fn get_collations(self: &Locale) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getHourCycles) + #[must_use] + #[js_sys(js_name = "getHourCycles")] + pub fn get_hour_cycles(self: &Locale) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getNumberingSystems) + #[must_use] + #[js_sys(js_name = "getNumberingSystems")] + pub fn get_numbering_systems(self: &Locale) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getTimeZones) + #[must_use] + #[js_sys(js_name = "getTimeZones")] + pub fn get_time_zones(self: &Locale) -> Option>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo) + #[js_sys(js_name = "getWeekInfo")] + pub fn get_week_info(self: &Locale) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getTextInfo) + #[js_sys(js_name = "getTextInfo")] + pub fn get_text_info(self: &Locale) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/maximize) + #[must_use] + pub fn maximize(self: &Locale) -> Locale; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/minimize) + #[must_use] + pub fn minimize(self: &Locale) -> Locale; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Locale) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo) + #[must_use] + #[js_sys(getter = "firstDay")] + pub fn first_day(self: &WeekInfo) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo) + #[must_use] + #[js_sys(getter)] + pub fn weekend(self: &WeekInfo) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo) + #[must_use] + #[js_sys(getter = "minimalDays")] + pub fn minimal_days(self: &WeekInfo) -> Option; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "caseFirst")] + fn case_first_raw(self: &Locale) -> Option; + + #[js_sys(getter = "hourCycle")] + fn hour_cycle_raw(self: &Locale) -> Option; + + #[js_sys(getter = "direction")] + fn direction_raw(self: &TextInfo) -> Option; + + #[js_sys(getter = "language")] + fn language_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "script")] + fn script_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "region")] + fn region_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "variants")] + fn variants_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "calendar")] + fn calendar_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "collation")] + fn collation_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "numberingSystem")] + fn numbering_system_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "caseFirst")] + fn option_case_first_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "hourCycle")] + fn option_hour_cycle_raw(self: &LocaleOptions) -> Option; + + #[js_sys(getter = "numeric")] + fn numeric_raw(self: &LocaleOptions) -> Option; + + #[js_sys(setter = "language")] + fn set_language_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "script")] + fn set_script_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "region")] + fn set_region_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "variants")] + fn set_variants_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "calendar")] + fn set_calendar_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "collation")] + fn set_collation_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "numberingSystem")] + fn set_numbering_system_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "caseFirst")] + fn set_case_first_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "hourCycle")] + fn set_hour_cycle_raw(self: &LocaleOptions, value: &str); + + #[js_sys(setter = "numeric")] + fn set_numeric_raw(self: &LocaleOptions, value: bool); +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "firstDayOfWeek")] + fn first_day_of_week_raw(self: &Locale) -> Option; + + #[js_sys(getter = "firstDayOfWeek")] + fn option_first_day_of_week_raw(self: &LocaleOptions) -> Option; + + #[js_sys(setter = "firstDayOfWeek")] + fn set_first_day_of_week_raw(self: &LocaleOptions, value: &str); +} + +impl Locale { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/caseFirst) + #[must_use] + pub fn case_first(&self) -> Option { + self.case_first_raw() + .as_ref() + .and_then(CollatorCaseFirst::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle) + #[must_use] + pub fn hour_cycle(&self) -> Option { + self.hour_cycle_raw() + .as_ref() + .and_then(HourCycle::from_js_string) + } + + /// [`ECMA-402` proposal](https://tc39.es/proposal-intl-locale-info/#sec-intl.locale.prototype.firstdayofweek) + #[must_use] + pub fn first_day_of_week(&self) -> Option { + self.first_day_of_week_raw() + .as_ref() + .and_then(FirstDayOfWeek::from_js_string) + } +} + +impl TextInfo { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getTextInfo) + #[must_use] + pub fn direction(&self) -> Option { + self.direction_raw() + .as_ref() + .and_then(TextDirection::from_js_string) + } +} + +impl LocaleOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#language) + #[must_use] + pub fn language(&self) -> Option { + self.language_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#language) + pub fn set_language(&self, value: &str) { + self.set_language_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#script) + #[must_use] + pub fn script(&self) -> Option { + self.script_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#script) + pub fn set_script(&self, value: &str) { + self.set_script_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#region) + #[must_use] + pub fn region(&self) -> Option { + self.region_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#region) + pub fn set_region(&self, value: &str) { + self.set_region_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#variants) + #[must_use] + pub fn variants(&self) -> Option { + self.variants_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#variants) + pub fn set_variants(&self, value: &str) { + self.set_variants_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#calendar) + #[must_use] + pub fn calendar(&self) -> Option { + self.calendar_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#calendar) + pub fn set_calendar(&self, value: &str) { + self.set_calendar_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#collation) + #[must_use] + pub fn collation(&self) -> Option { + self.collation_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#collation) + pub fn set_collation(&self, value: &str) { + self.set_collation_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#numberingsystem) + #[must_use] + pub fn numbering_system(&self) -> Option { + self.numbering_system_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#numberingsystem) + pub fn set_numbering_system(&self, value: &str) { + self.set_numbering_system_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#casefirst) + #[must_use] + pub fn case_first(&self) -> Option { + self.option_case_first_raw() + .as_ref() + .and_then(CollatorCaseFirst::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#hourcycle) + #[must_use] + pub fn hour_cycle(&self) -> Option { + self.option_hour_cycle_raw() + .as_ref() + .and_then(HourCycle::from_js_string) + } + + /// [`ECMA-402` proposal](https://tc39.es/proposal-intl-locale-info/#sec-intl-locale-constructor) + #[must_use] + pub fn first_day_of_week(&self) -> Option { + self.option_first_day_of_week_raw() + .as_ref() + .and_then(FirstDayOfWeek::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#numeric) + #[must_use] + pub fn numeric(&self) -> Option { + self.numeric_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#numeric) + pub fn set_numeric(&self, value: bool) { + self.set_numeric_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#casefirst) + pub fn set_case_first(&self, value: CollatorCaseFirst) { + self.set_case_first_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#hourcycle) + pub fn set_hour_cycle(&self, value: HourCycle) { + self.set_hour_cycle_raw(value.as_str()); + } + + /// [`ECMA-402` proposal](https://tc39.es/proposal-intl-locale-info/#sec-intl-locale-constructor) + pub fn set_first_day_of_week(&self, value: FirstDayOfWeek) { + self.set_first_day_of_week_raw(value.as_str()); + } +} + +impl Default for LocaleOptions { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/intl/mod.rs b/client/js-sys/src/builtins/intl/mod.rs new file mode 100644 index 00000000..ec33c274 --- /dev/null +++ b/client/js-sys/src/builtins/intl/mod.rs @@ -0,0 +1,147 @@ +mod collator; +mod date_time_format; +mod display_names; +mod duration_format; +mod list_format; +mod locale; +mod number_format; +mod plural_rules; +mod relative_time_format; +mod segmenter; + +use alloc::string::String; + +use crate::hazard::JsCast; +use crate::{JsString, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locale_identification_and_negotiation) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LocaleMatcher { + Lookup, + BestFit, +} + +impl LocaleMatcher { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::Lookup => "lookup", + Self::BestFit => "best fit", + } + } + + pub(super) fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "lookup" => Some(Self::Lookup), + "best fit" => Some(Self::BestFit), + _ => None, + } + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type LocaleMatcherOptions; + + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &LocaleMatcherOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &LocaleMatcherOptions, value: &str); +} + +impl LocaleMatcherOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf#options) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf#options) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } +} + +impl Default for LocaleMatcherOptions { + fn default() -> Self { + Self::new() + } +} + +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod Intl { + pub use super::collator::{ + Collator, CollatorCaseFirst, CollatorOptions, CollatorResolvedOptions, CollatorSensitivity, + CollatorUsage, + }; + pub use super::date_time_format::{ + DateTimeFormat, DateTimeFormatFractionalSecondDigits, DateTimeFormatMatcher, + DateTimeFormatMonthStyle, DateTimeFormatNumericStyle, DateTimeFormatOptions, + DateTimeFormatPart, DateTimeFormatPartType, DateTimeFormatRangeSource, + DateTimeFormatResolvedOptions, DateTimeFormatStyle, DateTimeFormatTextStyle, + DateTimeFormatTimeZoneName, DateTimeRangeFormatPart, + }; + pub use super::display_names::{ + DisplayNames, DisplayNamesFallback, DisplayNamesLanguageDisplay, DisplayNamesOptions, + DisplayNamesResolvedOptions, DisplayNamesStyle, DisplayNamesType, + }; + pub use super::duration_format::{ + Duration, DurationFormat, DurationFormatOptions, DurationFormatPart, + DurationFormatPartType, DurationFormatResolvedOptions, DurationFormatStyle, + DurationSubsecondUnitStyle, DurationTimeUnitStyle, DurationUnit, DurationUnitDisplay, + DurationUnitStyle, + }; + pub use super::list_format::{ + ListFormat, ListFormatOptions, ListFormatPart, ListFormatResolvedOptions, ListFormatStyle, + ListFormatType, + }; + pub use super::locale::{ + FirstDayOfWeek, HourCycle, Locale, LocaleOptions, TextDirection, TextInfo, WeekInfo, + }; + pub use super::number_format::{ + NumberFormat, NumberFormatCompactDisplay, NumberFormatCurrencyDisplay, + NumberFormatCurrencySign, NumberFormatNotation, NumberFormatOptions, NumberFormatPart, + NumberFormatPartType, NumberFormatRangeSource, NumberFormatResolvedOptions, + NumberFormatRoundingIncrement, NumberFormatRoundingMode, NumberFormatRoundingPriority, + NumberFormatSignDisplay, NumberFormatStyle, NumberFormatTrailingZeroDisplay, + NumberFormatUnitDisplay, NumberFormatUseGrouping, NumberRangeFormatPart, + }; + pub use super::plural_rules::{ + PluralRules, PluralRulesOptions, PluralRulesResolvedOptions, PluralRulesRoundingIncrement, + PluralRulesRoundingMode, PluralRulesRoundingPriority, PluralRulesTrailingZeroDisplay, + PluralRulesType, + }; + pub use super::relative_time_format::{ + RelativeTimeFormat, RelativeTimeFormatNumeric, RelativeTimeFormatOptions, + RelativeTimeFormatPart, RelativeTimeFormatResolvedOptions, RelativeTimeFormatStyle, + RelativeTimeUnit, + }; + pub use super::segmenter::{ + SegmentData, Segmenter, SegmenterGranularity, SegmenterOptions, SegmenterResolvedOptions, + Segments, + }; + pub use super::{LocaleMatcher, LocaleMatcherOptions}; + use crate::{Array, JsString, JsValue, js_sys}; + + #[js_sys(js_sys = crate, namespace = "Intl")] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/getCanonicalLocales) + #[js_sys(js_name = "getCanonicalLocales")] + pub fn get_canonical_locales(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf) + #[js_sys(js_name = "supportedValuesOf")] + pub fn supported_values_of(key: &str) -> Result, JsValue>; + } +} diff --git a/client/js-sys/src/builtins/intl/number_format.rs b/client/js-sys/src/builtins/intl/number_format.rs new file mode 100644 index 00000000..e2260224 --- /dev/null +++ b/client/js-sys/src/builtins/intl/number_format.rs @@ -0,0 +1,1029 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, Function, JsString, JsValue, Object, js_sys}; + +macro_rules! string_enum { + ( + $(#[$meta:meta])* + pub enum $name:ident { + $($variant:ident => $value:literal),+ $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum $name { + $($variant),+ + } + + impl $name { + const fn as_str(self) -> &'static str { + match self { + $(Self::$variant => $value),+ + } + } + + fn from_str(value: &str) -> Option { + match value { + $($value => Some(Self::$variant)),+, + _ => None, + } + } + } + }; +} + +macro_rules! readonly_string_enum { + ( + $(#[$meta:meta])* + pub enum $name:ident { + $($variant:ident => $value:literal),+ $(,)? + } + ) => { + $(#[$meta])* + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum $name { + $($variant),+ + } + + impl $name { + fn from_str(value: &str) -> Option { + match value { + $($value => Some(Self::$variant)),+, + _ => None, + } + } + } + }; +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#style) + pub enum NumberFormatStyle { + Decimal => "decimal", + Currency => "currency", + Percent => "percent", + Unit => "unit", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currencydisplay) + pub enum NumberFormatCurrencyDisplay { + Code => "code", + Symbol => "symbol", + NarrowSymbol => "narrowSymbol", + Name => "name", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currencysign) + pub enum NumberFormatCurrencySign { + Standard => "standard", + Accounting => "accounting", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#unitdisplay) + pub enum NumberFormatUnitDisplay { + Short => "short", + Narrow => "narrow", + Long => "long", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#notation) + pub enum NumberFormatNotation { + Standard => "standard", + Scientific => "scientific", + Engineering => "engineering", + Compact => "compact", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#compactdisplay) + pub enum NumberFormatCompactDisplay { + Short => "short", + Long => "long", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#signdisplay) + pub enum NumberFormatSignDisplay { + Auto => "auto", + Always => "always", + ExceptZero => "exceptZero", + Negative => "negative", + Never => "never", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode) + pub enum NumberFormatRoundingMode { + Ceil => "ceil", + Floor => "floor", + Expand => "expand", + Trunc => "trunc", + HalfCeil => "halfCeil", + HalfFloor => "halfFloor", + HalfExpand => "halfExpand", + HalfTrunc => "halfTrunc", + HalfEven => "halfEven", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingpriority) + pub enum NumberFormatRoundingPriority { + Auto => "auto", + MorePrecision => "morePrecision", + LessPrecision => "lessPrecision", + } +} + +string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#trailingzerodisplay) + pub enum NumberFormatTrailingZeroDisplay { + Auto => "auto", + StripIfInteger => "stripIfInteger", + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NumberFormatRoundingIncrement { + One, + Two, + Five, + Ten, + Twenty, + TwentyFive, + Fifty, + OneHundred, + TwoHundred, + TwoHundredFifty, + FiveHundred, + OneThousand, + TwoThousand, + TwoThousandFiveHundred, + FiveThousand, +} + +impl NumberFormatRoundingIncrement { + const fn as_u32(self) -> u32 { + match self { + Self::One => 1, + Self::Two => 2, + Self::Five => 5, + Self::Ten => 10, + Self::Twenty => 20, + Self::TwentyFive => 25, + Self::Fifty => 50, + Self::OneHundred => 100, + Self::TwoHundred => 200, + Self::TwoHundredFifty => 250, + Self::FiveHundred => 500, + Self::OneThousand => 1_000, + Self::TwoThousand => 2_000, + Self::TwoThousandFiveHundred => 2_500, + Self::FiveThousand => 5_000, + } + } + + fn from_u32(value: u32) -> Option { + match value { + 1 => Some(Self::One), + 2 => Some(Self::Two), + 5 => Some(Self::Five), + 10 => Some(Self::Ten), + 20 => Some(Self::Twenty), + 25 => Some(Self::TwentyFive), + 50 => Some(Self::Fifty), + 100 => Some(Self::OneHundred), + 200 => Some(Self::TwoHundred), + 250 => Some(Self::TwoHundredFifty), + 500 => Some(Self::FiveHundred), + 1_000 => Some(Self::OneThousand), + 2_000 => Some(Self::TwoThousand), + 2_500 => Some(Self::TwoThousandFiveHundred), + 5_000 => Some(Self::FiveThousand), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#usegrouping) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NumberFormatUseGrouping { + Always, + Auto, + Min2, + True, + False, +} + +impl NumberFormatUseGrouping { + fn from_str(value: &str) -> Option { + match value { + "always" => Some(Self::Always), + "auto" => Some(Self::Auto), + "min2" => Some(Self::Min2), + "true" => Some(Self::True), + "false" => Some(Self::False), + _ => None, + } + } +} + +readonly_string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts#return_value) + pub enum NumberFormatPartType { + ApproximatelySign => "approximatelySign", + Compact => "compact", + Currency => "currency", + Decimal => "decimal", + ExponentInteger => "exponentInteger", + ExponentMinusSign => "exponentMinusSign", + ExponentSeparator => "exponentSeparator", + Fraction => "fraction", + Group => "group", + Infinity => "infinity", + Integer => "integer", + Literal => "literal", + MinusSign => "minusSign", + Nan => "nan", + PercentSign => "percentSign", + PlusSign => "plusSign", + Unit => "unit", + Unknown => "unknown", + } +} + +readonly_string_enum! { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRangeToParts#return_value) + pub enum NumberFormatRangeSource { + StartRange => "startRange", + EndRange => "endRange", + Shared => "shared", + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) + #[js_sys(js_name = "NumberFormat", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type NumberFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type NumberFormatOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type NumberFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type NumberFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRangeToParts#return_value) + #[js_sys(extends = NumberFormatPart)] + #[derive(Clone, Debug, PartialEq)] + pub type NumberRangeFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> NumberFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &NumberFormatOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/supportedLocalesOf) + #[js_sys(static_of = NumberFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/supportedLocalesOf) + #[js_sys(static_of = NumberFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/format) + #[must_use] + #[js_sys(getter)] + pub fn format(self: &NumberFormat) -> Function; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts) + #[must_use] + #[js_sys(js_name = "formatToParts")] + pub fn format_to_parts(self: &NumberFormat) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts) + #[js_sys(js_name = "formatToParts")] + pub fn format_to_parts_with_value( + self: &NumberFormat, + value: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRange) + #[js_sys(js_name = "formatRange")] + pub fn format_range( + self: &NumberFormat, + start: &JsValue, + end: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRangeToParts) + #[js_sys(js_name = "formatRangeToParts")] + pub fn format_range_to_parts( + self: &NumberFormat, + start: &JsValue, + end: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &NumberFormat) -> NumberFormatResolvedOptions; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "numberingSystem")] + fn numbering_system_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "numberingSystem")] + fn set_numbering_system_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "style")] + fn style_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "style")] + fn set_style_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "currency")] + fn currency_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "currency")] + fn set_currency_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "currencyDisplay")] + fn currency_display_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "currencyDisplay")] + fn set_currency_display_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "currencySign")] + fn currency_sign_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "currencySign")] + fn set_currency_sign_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "unit")] + fn unit_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "unit")] + fn set_unit_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "unitDisplay")] + fn unit_display_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "unitDisplay")] + fn set_unit_display_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "minimumIntegerDigits")] + fn minimum_integer_digits_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "minimumIntegerDigits")] + fn set_minimum_integer_digits_raw(self: &NumberFormatOptions, value: u32); + + #[js_sys(getter = "minimumFractionDigits")] + fn minimum_fraction_digits_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "minimumFractionDigits")] + fn set_minimum_fraction_digits_raw(self: &NumberFormatOptions, value: u32); + + #[js_sys(getter = "maximumFractionDigits")] + fn maximum_fraction_digits_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "maximumFractionDigits")] + fn set_maximum_fraction_digits_raw(self: &NumberFormatOptions, value: u32); + + #[js_sys(getter = "minimumSignificantDigits")] + fn minimum_significant_digits_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "minimumSignificantDigits")] + fn set_minimum_significant_digits_raw(self: &NumberFormatOptions, value: u32); + + #[js_sys(getter = "maximumSignificantDigits")] + fn maximum_significant_digits_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "maximumSignificantDigits")] + fn set_maximum_significant_digits_raw(self: &NumberFormatOptions, value: u32); + + #[js_sys(getter = "roundingPriority")] + fn rounding_priority_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "roundingPriority")] + fn set_rounding_priority_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "roundingIncrement")] + fn rounding_increment_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "roundingIncrement")] + fn set_rounding_increment_raw(self: &NumberFormatOptions, value: u32); + + #[js_sys(getter = "roundingMode")] + fn rounding_mode_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "roundingMode")] + fn set_rounding_mode_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "trailingZeroDisplay")] + fn trailing_zero_display_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "trailingZeroDisplay")] + fn set_trailing_zero_display_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "notation")] + fn notation_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "notation")] + fn set_notation_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "compactDisplay")] + fn compact_display_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "compactDisplay")] + fn set_compact_display_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(js_embed = "intl.number_format.use_grouping")] + fn use_grouping_raw(options: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "useGrouping")] + fn set_use_grouping_string_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(setter = "useGrouping")] + fn set_use_grouping_bool_raw(self: &NumberFormatOptions, value: bool); + + #[js_sys(getter = "signDisplay")] + fn sign_display_raw(self: &NumberFormatOptions) -> Option; + + #[js_sys(setter = "signDisplay")] + fn set_sign_display_raw(self: &NumberFormatOptions, value: &str); + + #[js_sys(getter = "locale")] + fn locale_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "numberingSystem")] + fn resolved_numbering_system_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "style")] + fn resolved_style_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "currency")] + fn resolved_currency_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "currencyDisplay")] + fn resolved_currency_display_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "currencySign")] + fn resolved_currency_sign_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "unit")] + fn resolved_unit_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "unitDisplay")] + fn resolved_unit_display_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "minimumIntegerDigits")] + fn resolved_minimum_integer_digits_raw(self: &NumberFormatResolvedOptions) -> u32; + + #[js_sys(getter = "minimumFractionDigits")] + fn resolved_minimum_fraction_digits_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "maximumFractionDigits")] + fn resolved_maximum_fraction_digits_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "minimumSignificantDigits")] + fn resolved_minimum_significant_digits_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "maximumSignificantDigits")] + fn resolved_maximum_significant_digits_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(js_embed = "intl.number_format.use_grouping")] + fn resolved_use_grouping_raw(options: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "notation")] + fn resolved_notation_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "compactDisplay")] + fn resolved_compact_display_raw(self: &NumberFormatResolvedOptions) -> Option; + + #[js_sys(getter = "signDisplay")] + fn resolved_sign_display_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "roundingIncrement")] + fn resolved_rounding_increment_raw(self: &NumberFormatResolvedOptions) -> u32; + + #[js_sys(getter = "roundingMode")] + fn resolved_rounding_mode_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "roundingPriority")] + fn resolved_rounding_priority_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "trailingZeroDisplay")] + fn resolved_trailing_zero_display_raw(self: &NumberFormatResolvedOptions) -> JsString; + + #[js_sys(getter = "type")] + fn part_type_raw(self: &NumberFormatPart) -> JsString; + + #[js_sys(getter = "value")] + fn part_value_raw(self: &NumberFormatPart) -> JsString; + + #[js_sys(getter = "source")] + fn range_source_raw(self: &NumberRangeFormatPart) -> JsString; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "intl.number_format.use_grouping", + "value => {{ const grouping = value.useGrouping; return grouping === true ? 'true' : grouping \ + === false ? 'false' : typeof grouping === 'string' ? grouping : undefined }}", +); + +fn parse_string_option( + value: Option, + parse: impl FnOnce(&str) -> Option, +) -> Option { + let value = String::from(value?); + parse(&value) +} + +impl NumberFormatOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + parse_string_option(self.locale_matcher_raw(), |value| match value { + "lookup" => Some(LocaleMatcher::Lookup), + "best fit" => Some(LocaleMatcher::BestFit), + _ => None, + }) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#numberingsystem) + #[must_use] + pub fn numbering_system(&self) -> Option { + self.numbering_system_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#numberingsystem) + pub fn set_numbering_system(&self, value: &str) { + self.set_numbering_system_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#style) + #[must_use] + pub fn style(&self) -> Option { + parse_string_option(self.style_raw(), NumberFormatStyle::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#style) + pub fn set_style(&self, value: NumberFormatStyle) { + self.set_style_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currency) + #[must_use] + pub fn currency(&self) -> Option { + self.currency_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currency) + pub fn set_currency(&self, value: &str) { + self.set_currency_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currencydisplay) + #[must_use] + pub fn currency_display(&self) -> Option { + parse_string_option( + self.currency_display_raw(), + NumberFormatCurrencyDisplay::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currencydisplay) + pub fn set_currency_display(&self, value: NumberFormatCurrencyDisplay) { + self.set_currency_display_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currencysign) + #[must_use] + pub fn currency_sign(&self) -> Option { + parse_string_option(self.currency_sign_raw(), NumberFormatCurrencySign::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currencysign) + pub fn set_currency_sign(&self, value: NumberFormatCurrencySign) { + self.set_currency_sign_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#unit) + #[must_use] + pub fn unit(&self) -> Option { + self.unit_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#unit) + pub fn set_unit(&self, value: &str) { + self.set_unit_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#unitdisplay) + #[must_use] + pub fn unit_display(&self) -> Option { + parse_string_option(self.unit_display_raw(), NumberFormatUnitDisplay::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#unitdisplay) + pub fn set_unit_display(&self, value: NumberFormatUnitDisplay) { + self.set_unit_display_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumintegerdigits) + #[must_use] + pub fn minimum_integer_digits(&self) -> Option { + self.minimum_integer_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumintegerdigits) + pub fn set_minimum_integer_digits(&self, value: u32) { + self.set_minimum_integer_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits) + #[must_use] + pub fn minimum_fraction_digits(&self) -> Option { + self.minimum_fraction_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits) + pub fn set_minimum_fraction_digits(&self, value: u32) { + self.set_minimum_fraction_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#maximumfractiondigits) + #[must_use] + pub fn maximum_fraction_digits(&self) -> Option { + self.maximum_fraction_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#maximumfractiondigits) + pub fn set_maximum_fraction_digits(&self, value: u32) { + self.set_maximum_fraction_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumsignificantdigits) + #[must_use] + pub fn minimum_significant_digits(&self) -> Option { + self.minimum_significant_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumsignificantdigits) + pub fn set_minimum_significant_digits(&self, value: u32) { + self.set_minimum_significant_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#maximumsignificantdigits) + #[must_use] + pub fn maximum_significant_digits(&self) -> Option { + self.maximum_significant_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#maximumsignificantdigits) + pub fn set_maximum_significant_digits(&self, value: u32) { + self.set_maximum_significant_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingpriority) + #[must_use] + pub fn rounding_priority(&self) -> Option { + parse_string_option( + self.rounding_priority_raw(), + NumberFormatRoundingPriority::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingpriority) + pub fn set_rounding_priority(&self, value: NumberFormatRoundingPriority) { + self.set_rounding_priority_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement) + #[must_use] + pub fn rounding_increment(&self) -> Option { + self.rounding_increment_raw() + .and_then(NumberFormatRoundingIncrement::from_u32) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement) + pub fn set_rounding_increment(&self, value: NumberFormatRoundingIncrement) { + self.set_rounding_increment_raw(value.as_u32()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode) + #[must_use] + pub fn rounding_mode(&self) -> Option { + parse_string_option(self.rounding_mode_raw(), NumberFormatRoundingMode::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode) + pub fn set_rounding_mode(&self, value: NumberFormatRoundingMode) { + self.set_rounding_mode_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#trailingzerodisplay) + #[must_use] + pub fn trailing_zero_display(&self) -> Option { + parse_string_option( + self.trailing_zero_display_raw(), + NumberFormatTrailingZeroDisplay::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#trailingzerodisplay) + pub fn set_trailing_zero_display(&self, value: NumberFormatTrailingZeroDisplay) { + self.set_trailing_zero_display_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#notation) + #[must_use] + pub fn notation(&self) -> Option { + parse_string_option(self.notation_raw(), NumberFormatNotation::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#notation) + pub fn set_notation(&self, value: NumberFormatNotation) { + self.set_notation_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#compactdisplay) + #[must_use] + pub fn compact_display(&self) -> Option { + parse_string_option( + self.compact_display_raw(), + NumberFormatCompactDisplay::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#compactdisplay) + pub fn set_compact_display(&self, value: NumberFormatCompactDisplay) { + self.set_compact_display_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#usegrouping) + #[must_use] + pub fn use_grouping(&self) -> Option { + parse_string_option(use_grouping_raw(self), NumberFormatUseGrouping::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#usegrouping) + pub fn set_use_grouping(&self, value: NumberFormatUseGrouping) { + match value { + NumberFormatUseGrouping::Always => self.set_use_grouping_string_raw("always"), + NumberFormatUseGrouping::Auto => self.set_use_grouping_string_raw("auto"), + NumberFormatUseGrouping::Min2 => self.set_use_grouping_string_raw("min2"), + NumberFormatUseGrouping::True => self.set_use_grouping_bool_raw(true), + NumberFormatUseGrouping::False => self.set_use_grouping_bool_raw(false), + } + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#signdisplay) + #[must_use] + pub fn sign_display(&self) -> Option { + parse_string_option(self.sign_display_raw(), NumberFormatSignDisplay::from_str) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#signdisplay) + pub fn set_sign_display(&self, value: NumberFormatSignDisplay) { + self.set_sign_display_raw(value.as_str()); + } +} + +impl Default for NumberFormatOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for NumberFormat { + fn default() -> Self { + Self::new() + } +} + +impl NumberFormatResolvedOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn locale(&self) -> JsString { + self.locale_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn numbering_system(&self) -> JsString { + self.resolved_numbering_system_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn style(&self) -> Option { + NumberFormatStyle::from_str(&String::from(self.resolved_style_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn currency(&self) -> Option { + self.resolved_currency_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn currency_display(&self) -> Option { + parse_string_option( + self.resolved_currency_display_raw(), + NumberFormatCurrencyDisplay::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn currency_sign(&self) -> Option { + parse_string_option( + self.resolved_currency_sign_raw(), + NumberFormatCurrencySign::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn unit(&self) -> Option { + self.resolved_unit_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn unit_display(&self) -> Option { + parse_string_option( + self.resolved_unit_display_raw(), + NumberFormatUnitDisplay::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn minimum_integer_digits(&self) -> u32 { + self.resolved_minimum_integer_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn minimum_fraction_digits(&self) -> Option { + self.resolved_minimum_fraction_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn maximum_fraction_digits(&self) -> Option { + self.resolved_maximum_fraction_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn minimum_significant_digits(&self) -> Option { + self.resolved_minimum_significant_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn maximum_significant_digits(&self) -> Option { + self.resolved_maximum_significant_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn use_grouping(&self) -> Option { + parse_string_option( + resolved_use_grouping_raw(self), + NumberFormatUseGrouping::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn notation(&self) -> Option { + NumberFormatNotation::from_str(&String::from(self.resolved_notation_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn compact_display(&self) -> Option { + parse_string_option( + self.resolved_compact_display_raw(), + NumberFormatCompactDisplay::from_str, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn sign_display(&self) -> Option { + NumberFormatSignDisplay::from_str(&String::from(self.resolved_sign_display_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn rounding_increment(&self) -> Option { + NumberFormatRoundingIncrement::from_u32(self.resolved_rounding_increment_raw()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn rounding_mode(&self) -> Option { + NumberFormatRoundingMode::from_str(&String::from(self.resolved_rounding_mode_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn rounding_priority(&self) -> Option { + NumberFormatRoundingPriority::from_str(&String::from(self.resolved_rounding_priority_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions#return_value) + #[must_use] + pub fn trailing_zero_display(&self) -> Option { + NumberFormatTrailingZeroDisplay::from_str(&String::from( + self.resolved_trailing_zero_display_raw(), + )) + } +} + +impl NumberFormatPart { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts#return_value) + #[must_use] + pub fn type_(&self) -> Option { + NumberFormatPartType::from_str(&String::from(self.part_type_raw())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts#return_value) + #[must_use] + pub fn value(&self) -> JsString { + self.part_value_raw() + } +} + +impl NumberRangeFormatPart { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRangeToParts#return_value) + #[must_use] + pub fn source(&self) -> Option { + NumberFormatRangeSource::from_str(&String::from(self.range_source_raw())) + } +} diff --git a/client/js-sys/src/builtins/intl/plural_rules.rs b/client/js-sys/src/builtins/intl/plural_rules.rs new file mode 100644 index 00000000..dcca7122 --- /dev/null +++ b/client/js-sys/src/builtins/intl/plural_rules.rs @@ -0,0 +1,533 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#type) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PluralRulesType { + Cardinal, + Ordinal, +} + +impl PluralRulesType { + const fn as_str(self) -> &'static str { + match self { + Self::Cardinal => "cardinal", + Self::Ordinal => "ordinal", + } + } + + fn parse(value: &str) -> Option { + match value { + "cardinal" => Some(Self::Cardinal), + "ordinal" => Some(Self::Ordinal), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingpriority) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PluralRulesRoundingPriority { + Auto, + MorePrecision, + LessPrecision, +} + +impl PluralRulesRoundingPriority { + const fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::MorePrecision => "morePrecision", + Self::LessPrecision => "lessPrecision", + } + } + + fn parse(value: &str) -> Option { + match value { + "auto" => Some(Self::Auto), + "morePrecision" => Some(Self::MorePrecision), + "lessPrecision" => Some(Self::LessPrecision), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingincrement) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PluralRulesRoundingIncrement { + One, + Two, + Five, + Ten, + Twenty, + TwentyFive, + Fifty, + OneHundred, + TwoHundred, + TwoHundredFifty, + FiveHundred, + OneThousand, + TwoThousand, + TwoThousandFiveHundred, + FiveThousand, +} + +impl PluralRulesRoundingIncrement { + const fn as_u32(self) -> u32 { + match self { + Self::One => 1, + Self::Two => 2, + Self::Five => 5, + Self::Ten => 10, + Self::Twenty => 20, + Self::TwentyFive => 25, + Self::Fifty => 50, + Self::OneHundred => 100, + Self::TwoHundred => 200, + Self::TwoHundredFifty => 250, + Self::FiveHundred => 500, + Self::OneThousand => 1_000, + Self::TwoThousand => 2_000, + Self::TwoThousandFiveHundred => 2_500, + Self::FiveThousand => 5_000, + } + } + + const fn from_u32(value: u32) -> Option { + match value { + 1 => Some(Self::One), + 2 => Some(Self::Two), + 5 => Some(Self::Five), + 10 => Some(Self::Ten), + 20 => Some(Self::Twenty), + 25 => Some(Self::TwentyFive), + 50 => Some(Self::Fifty), + 100 => Some(Self::OneHundred), + 200 => Some(Self::TwoHundred), + 250 => Some(Self::TwoHundredFifty), + 500 => Some(Self::FiveHundred), + 1_000 => Some(Self::OneThousand), + 2_000 => Some(Self::TwoThousand), + 2_500 => Some(Self::TwoThousandFiveHundred), + 5_000 => Some(Self::FiveThousand), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingmode) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PluralRulesRoundingMode { + Ceil, + Floor, + Expand, + Trunc, + HalfCeil, + HalfFloor, + HalfExpand, + HalfTrunc, + HalfEven, +} + +impl PluralRulesRoundingMode { + const fn as_str(self) -> &'static str { + match self { + Self::Ceil => "ceil", + Self::Floor => "floor", + Self::Expand => "expand", + Self::Trunc => "trunc", + Self::HalfCeil => "halfCeil", + Self::HalfFloor => "halfFloor", + Self::HalfExpand => "halfExpand", + Self::HalfTrunc => "halfTrunc", + Self::HalfEven => "halfEven", + } + } + + fn parse(value: &str) -> Option { + match value { + "ceil" => Some(Self::Ceil), + "floor" => Some(Self::Floor), + "expand" => Some(Self::Expand), + "trunc" => Some(Self::Trunc), + "halfCeil" => Some(Self::HalfCeil), + "halfFloor" => Some(Self::HalfFloor), + "halfExpand" => Some(Self::HalfExpand), + "halfTrunc" => Some(Self::HalfTrunc), + "halfEven" => Some(Self::HalfEven), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#options) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PluralRulesTrailingZeroDisplay { + Auto, + StripIfInteger, +} + +impl PluralRulesTrailingZeroDisplay { + const fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::StripIfInteger => "stripIfInteger", + } + } + + fn parse(value: &str) -> Option { + match value { + "auto" => Some(Self::Auto), + "stripIfInteger" => Some(Self::StripIfInteger), + _ => None, + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) + #[js_sys(js_name = "PluralRules", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PluralRules; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PluralRulesOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PluralRulesResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> PluralRules; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &PluralRulesOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/supportedLocalesOf) + #[js_sys(static_of = PluralRules, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/supportedLocalesOf) + #[js_sys(static_of = PluralRules, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/select) + #[must_use] + pub fn select(self: &PluralRules, value: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/selectRange) + #[js_sys(js_name = "selectRange")] + pub fn select_range(self: &PluralRules, start: f64, end: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &PluralRules) -> PluralRulesResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &PluralRulesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "type")] + pub fn type_(self: &PluralRulesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "minimumIntegerDigits")] + pub fn minimum_integer_digits(self: &PluralRulesResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "minimumFractionDigits")] + pub fn minimum_fraction_digits(self: &PluralRulesResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "maximumFractionDigits")] + pub fn maximum_fraction_digits(self: &PluralRulesResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "minimumSignificantDigits")] + pub fn minimum_significant_digits(self: &PluralRulesResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "maximumSignificantDigits")] + pub fn maximum_significant_digits(self: &PluralRulesResolvedOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "pluralCategories")] + pub fn plural_categories(self: &PluralRulesResolvedOptions) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "roundingIncrement")] + pub fn rounding_increment(self: &PluralRulesResolvedOptions) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "roundingMode")] + pub fn rounding_mode(self: &PluralRulesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "roundingPriority")] + pub fn rounding_priority(self: &PluralRulesResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "trailingZeroDisplay")] + pub fn trailing_zero_display(self: &PluralRulesResolvedOptions) -> JsString; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &PluralRulesOptions, value: &str); + + #[js_sys(getter = "type")] + fn type_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "type")] + fn set_type_raw(self: &PluralRulesOptions, value: &str); + + #[js_sys(getter = "minimumIntegerDigits")] + fn minimum_integer_digits_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "minimumIntegerDigits")] + fn set_minimum_integer_digits_raw(self: &PluralRulesOptions, value: u32); + + #[js_sys(getter = "minimumFractionDigits")] + fn minimum_fraction_digits_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "minimumFractionDigits")] + fn set_minimum_fraction_digits_raw(self: &PluralRulesOptions, value: u32); + + #[js_sys(getter = "maximumFractionDigits")] + fn maximum_fraction_digits_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "maximumFractionDigits")] + fn set_maximum_fraction_digits_raw(self: &PluralRulesOptions, value: u32); + + #[js_sys(getter = "minimumSignificantDigits")] + fn minimum_significant_digits_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "minimumSignificantDigits")] + fn set_minimum_significant_digits_raw(self: &PluralRulesOptions, value: u32); + + #[js_sys(getter = "maximumSignificantDigits")] + fn maximum_significant_digits_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "maximumSignificantDigits")] + fn set_maximum_significant_digits_raw(self: &PluralRulesOptions, value: u32); + + #[js_sys(getter = "roundingPriority")] + fn rounding_priority_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "roundingPriority")] + fn set_rounding_priority_raw(self: &PluralRulesOptions, value: &str); + + #[js_sys(getter = "roundingIncrement")] + fn rounding_increment_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "roundingIncrement")] + fn set_rounding_increment_raw(self: &PluralRulesOptions, value: u32); + + #[js_sys(getter = "roundingMode")] + fn rounding_mode_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "roundingMode")] + fn set_rounding_mode_raw(self: &PluralRulesOptions, value: &str); + + #[js_sys(getter = "trailingZeroDisplay")] + fn trailing_zero_display_raw(self: &PluralRulesOptions) -> Option; + + #[js_sys(setter = "trailingZeroDisplay")] + fn set_trailing_zero_display_raw(self: &PluralRulesOptions, value: &str); +} + +fn parse_string_option( + value: Option, + parse: impl FnOnce(&str) -> Option, +) -> Option { + let value = String::from(value?); + parse(&value) +} + +impl PluralRulesOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#type) + #[must_use] + pub fn type_(&self) -> Option { + parse_string_option(self.type_raw(), PluralRulesType::parse) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#type) + pub fn set_type(&self, value: PluralRulesType) { + self.set_type_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#minimumintegerdigits) + #[must_use] + pub fn minimum_integer_digits(&self) -> Option { + self.minimum_integer_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#minimumintegerdigits) + pub fn set_minimum_integer_digits(&self, value: u32) { + self.set_minimum_integer_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#minimumfractiondigits) + #[must_use] + pub fn minimum_fraction_digits(&self) -> Option { + self.minimum_fraction_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#minimumfractiondigits) + pub fn set_minimum_fraction_digits(&self, value: u32) { + self.set_minimum_fraction_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#maximumfractiondigits) + #[must_use] + pub fn maximum_fraction_digits(&self) -> Option { + self.maximum_fraction_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#maximumfractiondigits) + pub fn set_maximum_fraction_digits(&self, value: u32) { + self.set_maximum_fraction_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#minimumsignificantdigits) + #[must_use] + pub fn minimum_significant_digits(&self) -> Option { + self.minimum_significant_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#minimumsignificantdigits) + pub fn set_minimum_significant_digits(&self, value: u32) { + self.set_minimum_significant_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#maximumsignificantdigits) + #[must_use] + pub fn maximum_significant_digits(&self) -> Option { + self.maximum_significant_digits_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#maximumsignificantdigits) + pub fn set_maximum_significant_digits(&self, value: u32) { + self.set_maximum_significant_digits_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingpriority) + #[must_use] + pub fn rounding_priority(&self) -> Option { + parse_string_option( + self.rounding_priority_raw(), + PluralRulesRoundingPriority::parse, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingpriority) + pub fn set_rounding_priority(&self, value: PluralRulesRoundingPriority) { + self.set_rounding_priority_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingincrement) + #[must_use] + pub fn rounding_increment(&self) -> Option { + self.rounding_increment_raw() + .and_then(PluralRulesRoundingIncrement::from_u32) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingincrement) + pub fn set_rounding_increment(&self, value: PluralRulesRoundingIncrement) { + self.set_rounding_increment_raw(value.as_u32()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingmode) + #[must_use] + pub fn rounding_mode(&self) -> Option { + parse_string_option(self.rounding_mode_raw(), PluralRulesRoundingMode::parse) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#roundingmode) + pub fn set_rounding_mode(&self, value: PluralRulesRoundingMode) { + self.set_rounding_mode_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#options) + #[must_use] + pub fn trailing_zero_display(&self) -> Option { + parse_string_option( + self.trailing_zero_display_raw(), + PluralRulesTrailingZeroDisplay::parse, + ) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#options) + pub fn set_trailing_zero_display(&self, value: PluralRulesTrailingZeroDisplay) { + self.set_trailing_zero_display_raw(value.as_str()); + } +} + +impl Default for PluralRulesOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for PluralRules { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/intl/relative_time_format.rs b/client/js-sys/src/builtins/intl/relative_time_format.rs new file mode 100644 index 00000000..f2d6ea87 --- /dev/null +++ b/client/js-sys/src/builtins/intl/relative_time_format.rs @@ -0,0 +1,301 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#style) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RelativeTimeFormatStyle { + Long, + Short, + Narrow, +} + +impl RelativeTimeFormatStyle { + const fn as_str(self) -> &'static str { + match self { + Self::Long => "long", + Self::Short => "short", + Self::Narrow => "narrow", + } + } + + fn parse(value: &str) -> Option { + match value { + "long" => Some(Self::Long), + "short" => Some(Self::Short), + "narrow" => Some(Self::Narrow), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#numeric) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RelativeTimeFormatNumeric { + Always, + Auto, +} + +impl RelativeTimeFormatNumeric { + const fn as_str(self) -> &'static str { + match self { + Self::Always => "always", + Self::Auto => "auto", + } + } + + fn parse(value: &str) -> Option { + match value { + "always" => Some(Self::Always), + "auto" => Some(Self::Auto), + _ => None, + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/format#unit) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RelativeTimeUnit { + Year, + Quarter, + Month, + Week, + Day, + Hour, + Minute, + Second, +} + +impl RelativeTimeUnit { + const fn as_str(self) -> &'static str { + match self { + Self::Year => "year", + Self::Quarter => "quarter", + Self::Month => "month", + Self::Week => "week", + Self::Day => "day", + Self::Hour => "hour", + Self::Minute => "minute", + Self::Second => "second", + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) + #[js_sys(js_name = "RelativeTimeFormat", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RelativeTimeFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RelativeTimeFormatOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RelativeTimeFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RelativeTimeFormatPart; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> RelativeTimeFormat; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &RelativeTimeFormatOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/supportedLocalesOf) + #[js_sys(static_of = RelativeTimeFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/supportedLocalesOf) + #[js_sys(static_of = RelativeTimeFormat, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &RelativeTimeFormat) -> RelativeTimeFormatResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &RelativeTimeFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn style(self: &RelativeTimeFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn numeric(self: &RelativeTimeFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter = "numberingSystem")] + pub fn numbering_system(self: &RelativeTimeFormatResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts#return_value) + #[must_use] + #[js_sys(getter = "type")] + pub fn type_(self: &RelativeTimeFormatPart) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts#return_value) + #[must_use] + #[js_sys(getter)] + pub fn value(self: &RelativeTimeFormatPart) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts#return_value) + #[must_use] + #[js_sys(getter)] + pub fn unit(self: &RelativeTimeFormatPart) -> Option; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_name = "format")] + fn format_raw(self: &RelativeTimeFormat, value: f64, unit: &str) -> Result; + + #[js_sys(js_name = "formatToParts")] + fn format_to_parts_raw( + self: &RelativeTimeFormat, + value: f64, + unit: &str, + ) -> Result, JsValue>; + + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &RelativeTimeFormatOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &RelativeTimeFormatOptions, value: &str); + + #[js_sys(getter = "numberingSystem")] + fn numbering_system_raw(self: &RelativeTimeFormatOptions) -> Option; + + #[js_sys(setter = "numberingSystem")] + fn set_numbering_system_raw(self: &RelativeTimeFormatOptions, value: &str); + + #[js_sys(getter = "style")] + fn style_raw(self: &RelativeTimeFormatOptions) -> Option; + + #[js_sys(setter = "style")] + fn set_style_raw(self: &RelativeTimeFormatOptions, value: &str); + + #[js_sys(getter = "numeric")] + fn numeric_raw(self: &RelativeTimeFormatOptions) -> Option; + + #[js_sys(setter = "numeric")] + fn set_numeric_raw(self: &RelativeTimeFormatOptions, value: &str); +} + +fn parse_string_option( + value: Option, + parse: impl FnOnce(&str) -> Option, +) -> Option { + let value = String::from(value?); + parse(&value) +} + +impl RelativeTimeFormatOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#numberingsystem) + #[must_use] + pub fn numbering_system(&self) -> Option { + self.numbering_system_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#numberingsystem) + pub fn set_numbering_system(&self, value: &str) { + self.set_numbering_system_raw(value); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#style) + #[must_use] + pub fn style(&self) -> Option { + parse_string_option(self.style_raw(), RelativeTimeFormatStyle::parse) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#style) + pub fn set_style(&self, value: RelativeTimeFormatStyle) { + self.set_style_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#numeric) + #[must_use] + pub fn numeric(&self) -> Option { + parse_string_option(self.numeric_raw(), RelativeTimeFormatNumeric::parse) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#numeric) + pub fn set_numeric(&self, value: RelativeTimeFormatNumeric) { + self.set_numeric_raw(value.as_str()); + } +} + +impl RelativeTimeFormat { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/format) + pub fn format(&self, value: f64, unit: RelativeTimeUnit) -> Result { + self.format_raw(value, unit.as_str()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts) + pub fn format_to_parts( + &self, + value: f64, + unit: RelativeTimeUnit, + ) -> Result, JsValue> { + self.format_to_parts_raw(value, unit.as_str()) + } +} + +impl Default for RelativeTimeFormatOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for RelativeTimeFormat { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/intl/segmenter.rs b/client/js-sys/src/builtins/intl/segmenter.rs new file mode 100644 index 00000000..ce7ff3e1 --- /dev/null +++ b/client/js-sys/src/builtins/intl/segmenter.rs @@ -0,0 +1,217 @@ +use alloc::string::String; + +use super::{LocaleMatcher, LocaleMatcherOptions}; +use crate::hazard::JsCast; +use crate::{Array, Iterable, JsIterator, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#granularity) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SegmenterGranularity { + Grapheme, + Word, + Sentence, +} + +impl SegmenterGranularity { + const fn as_str(self) -> &'static str { + match self { + Self::Grapheme => "grapheme", + Self::Word => "word", + Self::Sentence => "sentence", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "grapheme" => Some(Self::Grapheme), + "word" => Some(Self::Word), + "sentence" => Some(Self::Sentence), + _ => None, + } + } +} + +#[js_sys(js_sys = crate, namespace = "Intl")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) + #[js_sys(js_name = "Segmenter", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Segmenter; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type SegmenterOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/resolvedOptions#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type SegmenterResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Segments; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type SegmentData; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Segmenter; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter) + #[js_sys(constructor)] + pub fn new_with_locales(locales: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter) + #[js_sys(constructor)] + pub fn new_with_locales_and_options( + locales: &JsValue, + options: &SegmenterOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/supportedLocalesOf) + #[js_sys(static_of = Segmenter, js_name = "supportedLocalesOf")] + pub fn supported_locales_of(locales: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/supportedLocalesOf) + #[js_sys(static_of = Segmenter, js_name = "supportedLocalesOf")] + pub fn supported_locales_of_with_options( + locales: &JsValue, + options: &LocaleMatcherOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/resolvedOptions) + #[must_use] + #[js_sys(js_name = "resolvedOptions")] + pub fn resolved_options(self: &Segmenter) -> SegmenterResolvedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment) + #[must_use] + pub fn segment(self: &Segmenter, input: &str) -> Segments; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing) + #[must_use] + pub fn containing(self: &Segments) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing) + #[must_use] + #[js_sys(js_name = "containing")] + pub fn containing_at(self: &Segments, index: f64) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn locale(self: &SegmenterResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/resolvedOptions#return_value) + #[must_use] + #[js_sys(getter)] + pub fn granularity(self: &SegmenterResolvedOptions) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing#return_value) + #[must_use] + #[js_sys(getter)] + pub fn segment(self: &SegmentData) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing#return_value) + #[must_use] + #[js_sys(getter)] + pub fn index(self: &SegmentData) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing#return_value) + #[must_use] + #[js_sys(getter)] + pub fn input(self: &SegmentData) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/containing#return_value) + #[must_use] + #[js_sys(getter = "isWordLike")] + pub fn is_word_like(self: &SegmentData) -> Option; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "intl.segments.iterator")] + fn segments_symbol_iterator(segments: &Segments) -> JsIterator; + + #[js_sys(getter = "localeMatcher")] + fn locale_matcher_raw(self: &SegmenterOptions) -> Option; + + #[js_sys(getter = "granularity")] + fn granularity_raw(self: &SegmenterOptions) -> Option; + + #[js_sys(setter = "localeMatcher")] + fn set_locale_matcher_raw(self: &SegmenterOptions, value: &str); + + #[js_sys(setter = "granularity")] + fn set_granularity_raw(self: &SegmenterOptions, value: &str); +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "intl.segments.iterator", + "segments => segments[Symbol.iterator]()", +); + +impl SegmenterOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#localematcher) + #[must_use] + pub fn locale_matcher(&self) -> Option { + self.locale_matcher_raw() + .as_ref() + .and_then(LocaleMatcher::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#localematcher) + pub fn set_locale_matcher(&self, value: LocaleMatcher) { + self.set_locale_matcher_raw(value.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#granularity) + #[must_use] + pub fn granularity(&self) -> Option { + self.granularity_raw() + .as_ref() + .and_then(SegmenterGranularity::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#granularity) + pub fn set_granularity(&self, value: SegmenterGranularity) { + self.set_granularity_raw(value.as_str()); + } +} + +impl Default for SegmenterOptions { + fn default() -> Self { + Self::new() + } +} + +impl Default for Segmenter { + fn default() -> Self { + Self::new() + } +} + +impl Segments { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment/Segments/Symbol.iterator) + #[must_use] + pub fn symbol_iterator(&self) -> JsIterator { + segments_symbol_iterator(self) + } +} + +impl Iterable for Segments { + type Item = SegmentData; +} diff --git a/client/js-sys/src/builtins/iterator.rs b/client/js-sys/src/builtins/iterator.rs new file mode 100644 index 00000000..4551a6af --- /dev/null +++ b/client/js-sys/src/builtins/iterator.rs @@ -0,0 +1,752 @@ +use core::fmt::{self, Formatter}; + +use super::{Array, Function, JsString, Object, Promise}; +use crate::JsValue; +use crate::hazard::JsCast; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip#mode) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum IteratorZipMode { + Shortest, + Longest, + Strict, +} + +impl IteratorZipMode { + const fn as_str(self) -> &'static str { + match self { + Self::Shortest => "shortest", + Self::Longest => "longest", + Self::Strict => "strict", + } + } +} + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator) + #[js_sys(js_name = "Iterator", extends = Object)] + pub type JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator) + #[js_sys(extends = Object)] + pub type AsyncIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) + #[js_sys(extends = Object)] + #[derive(Clone, Debug)] + pub type IteratorResult; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type IteratorZipOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zipKeyed#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type IteratorZipKeyedOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/from) + #[js_sys(static_of = JsIterator, js_name = "from")] + pub fn from_value(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/concat) + #[js_sys(static_of = JsIterator, variadic)] + pub fn concat(iterables: &[JsValue]) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip) + #[js_sys(static_of = JsIterator)] + pub fn zip( + #[js_sys(type = &JsValue)] iterables: &I, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip) + #[js_sys( + static_of = JsIterator, + js_name = "zip" + )] + pub fn zip_with_options( + #[js_sys(type = &JsValue)] iterables: &I, + options: &IteratorZipOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zipKeyed) + #[js_sys( + static_of = JsIterator, + js_name = "zipKeyed" + )] + pub fn zip_keyed(iterables: &Object) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zipKeyed) + #[js_sys( + static_of = JsIterator, + js_name = "zipKeyed" + )] + pub fn zip_keyed_with_options( + iterables: &Object, + options: &IteratorZipKeyedOptions, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/drop) + #[js_sys(js_name = "drop", return_abi = Result)] + pub fn drop(self: &JsIterator, count: f64) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/every) + pub fn every(self: &JsIterator, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/filter) + #[js_sys(return_abi = Result)] + pub fn filter(self: &JsIterator, callback: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/find) + pub fn find(self: &JsIterator, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/flatMap) + #[js_sys(js_name = "flatMap")] + pub fn flat_map(self: &JsIterator, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each(self: &JsIterator, callback: &Function) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/includes) + pub fn includes( + self: &JsIterator, + #[js_sys(type = &JsValue)] search_element: &T, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/includes) + #[js_sys(js_name = "includes")] + pub fn includes_from( + self: &JsIterator, + #[js_sys(type = &JsValue)] search_element: &T, + from_index: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/join) + pub fn join(self: &JsIterator) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/join) + #[js_sys(js_name = "join")] + pub fn join_with_separator( + self: &JsIterator, + separator: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/map) + pub fn map(self: &JsIterator, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/reduce) + pub fn reduce(self: &JsIterator, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/reduce) + #[js_sys(js_name = "reduce")] + pub fn reduce_with_initial_value( + self: &JsIterator, + callback: &Function, + initial_value: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/some) + pub fn some(self: &JsIterator, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/take) + #[js_sys(return_abi = Result)] + pub fn take(self: &JsIterator, limit: f64) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/toArray) + #[js_sys(js_name = "toArray", return_abi = Result)] + pub fn to_array(self: &JsIterator) -> Result, JsValue>; +} + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(setter = "mode")] + fn set_zip_mode_raw(self: &IteratorZipOptions, mode: &str); + + #[js_sys(setter = "padding")] + fn set_zip_padding_raw(self: &IteratorZipOptions, padding: &JsValue); + + #[js_sys(setter = "mode")] + fn set_zip_keyed_mode_raw(self: &IteratorZipKeyedOptions, mode: &str); + + #[js_sys(setter = "padding")] + fn set_zip_keyed_padding_raw(self: &IteratorZipKeyedOptions, padding: &Object); +} + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "iterator.next")] + fn iterator_next( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + ) -> Result; + + #[js_sys(js_embed = "iterator.next.value")] + fn iterator_next_with_value( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + value: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "iterator.return")] + fn iterator_return( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + ) -> Result, JsValue>; + + #[js_sys(js_embed = "iterator.return.value")] + fn iterator_return_with_value( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + value: &JsValue, + ) -> Result, JsValue>; + + #[js_sys(js_embed = "iterator.throw")] + fn iterator_throw( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + value: &JsValue, + ) -> Result, JsValue>; + + #[js_sys(js_embed = "iterator.dispose")] + fn iterator_dispose( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + ) -> Result<(), JsValue>; + + #[js_sys(js_embed = "async_iterator.next")] + pub(crate) fn async_iterator_next( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + ) -> Result, JsValue>; + + #[js_sys(js_embed = "async_iterator.next.value")] + fn async_iterator_next_with_value( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + value: &JsValue, + ) -> Result, JsValue>; + + #[js_sys(js_embed = "async_iterator.return")] + fn async_iterator_return( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + ) -> Result>, JsValue>; + + #[js_sys(js_embed = "async_iterator.return.value")] + fn async_iterator_return_with_value( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + value: &JsValue, + ) -> Result>, JsValue>; + + #[js_sys(js_embed = "async_iterator.throw")] + fn async_iterator_throw( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + value: &JsValue, + ) -> Result>, JsValue>; + + #[js_sys(js_embed = "async_iterator.dispose")] + fn async_iterator_dispose( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + ) -> Result; + + #[js_sys(js_embed = "iterator_result.done")] + fn iterator_result_done(result: &IteratorResult) -> Result; + + #[js_sys(js_embed = "iterator_result.value")] + fn iterator_result_value(result: &IteratorResult) -> Result; +} + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "iterator.from")] + pub(crate) fn iterator_from(value: &JsValue) -> Result, JsValue>; + + #[js_sys(js_embed = "async_iterator.from")] + pub(crate) fn async_iterator_from(value: &JsValue) -> Result, JsValue>; +} + +macro_rules! impl_wrapper { + ($type:ident) => { + impl Clone for $type { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } + } + + impl fmt::Debug for $type { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } + } + }; +} + +impl_wrapper!(JsIterator); +impl_wrapper!(AsyncIterator); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.next", + "(iterator) => {{", + " const result = iterator.next()", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('iterator next method returned a non-object value')", + " return result", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.next.value", + "(iterator, value) => {{", + " const result = iterator.next(value)", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('iterator next method returned a non-object value')", + " return result", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.return", + "(iterator) => {{", + " const method = iterator.return", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('iterator return property is not callable')", + " const result = method.call(iterator)", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('iterator return method returned a non-object value')", + " return result", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.return.value", + "(iterator, value) => {{", + " const method = iterator.return", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('iterator return property is not callable')", + " const result = method.call(iterator, value)", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('iterator return method returned a non-object value')", + " return result", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.throw", + "(iterator, value) => {{", + " const method = iterator.throw", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('iterator throw property is not callable')", + " const result = method.call(iterator, value)", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('iterator throw method returned a non-object value')", + " return result", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.dispose", + "(iterator) => {{", + " const method = iterator[Symbol.dispose]", + " if (typeof method !== 'function')", + " throw new TypeError('iterator does not provide Symbol.dispose')", + " method.call(iterator)", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator_result.done", + "(result) => Boolean(result.done)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator_result.value", + "(result) => result.value", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.from", + "(value) => {{", + " if (value == null) return null", + " const method = value[Symbol.iterator]", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('Symbol.iterator property is not callable')", + " const iterator = method.call(value)", + " if (iterator == null || (typeof iterator !== 'object' && typeof iterator !== 'function'))", + " throw new TypeError('iterator method returned a non-object value')", + " return iterator", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.from", + "(value) => {{", + " if (value == null) return null", + " const asyncMethod = value[Symbol.asyncIterator]", + " if (asyncMethod != null) {{", + " if (typeof asyncMethod !== 'function')", + " throw new TypeError('Symbol.asyncIterator property is not callable')", + " const iterator = asyncMethod.call(value)", + " if (iterator == null || (typeof iterator !== 'object' && typeof iterator !== \ + 'function'))", + " throw new TypeError('async iterator method returned a non-object value')", + " return iterator", + " }}", + "", + " const syncMethod = value[Symbol.iterator]", + " if (syncMethod == null) return null", + " if (typeof syncMethod !== 'function')", + " throw new TypeError('Symbol.iterator property is not callable')", + " const iterator = syncMethod.call(value)", + " if (iterator == null || (typeof iterator !== 'object' && typeof iterator !== 'function'))", + " throw new TypeError('iterator method returned a non-object value')", + " const next = iterator.next", + " if (typeof next !== 'function')", + " throw new TypeError('iterator does not provide a next method')", + "", + " const reject = error => Promise.reject(error)", + " const close = reason => {{", + " try {{", + " const method = iterator.return", + " if (method != null) {{", + " if (typeof method !== 'function')", + " throw new TypeError('iterator return property is not callable')", + " const result = method.call(iterator)", + " if (result == null || (typeof result !== 'object' && typeof result !== \ + 'function'))", + " throw new TypeError('iterator return method returned a non-object value')", + " }}", + " }} catch {{}}", + " throw reason", + " }}", + " const continueWith = (result, closeOnRejection) => {{", + " try {{", + " if (result == null || (typeof result !== 'object' && typeof result !== \ + 'function'))", + " throw new TypeError('iterator method returned a non-object value')", + " const done = Boolean(result.done)", + " const value = result.value", + " const unwrap = value => ({{ done, value }})", + " return !done && closeOnRejection", + " ? Promise.resolve(value).then(unwrap, close)", + " : Promise.resolve(value).then(unwrap)", + " }} catch (error) {{", + " return reject(error)", + " }}", + " }}", + " return {{", + " next(value) {{", + " try {{", + " return continueWith(", + " arguments.length === 0 ? next.call(iterator) : next.call(iterator, \ + value),", + " true,", + " )", + " }} catch (error) {{", + " return reject(error)", + " }}", + " }},", + " return(value) {{", + " try {{", + " const method = iterator.return", + " if (method == null)", + " return Promise.resolve({{", + " done: true,", + " value: arguments.length === 0 ? undefined : value,", + " }})", + " if (typeof method !== 'function')", + " throw new TypeError('iterator return property is not callable')", + " return continueWith(", + " arguments.length === 0 ? method.call(iterator) : method.call(iterator, \ + value),", + " false,", + " )", + " }} catch (error) {{", + " return reject(error)", + " }}", + " }},", + " throw(value) {{", + " try {{", + " const method = iterator.throw", + " if (method == null) {{", + " const close = iterator.return", + " if (close != null) {{", + " if (typeof close !== 'function')", + " throw new TypeError('iterator return property is not callable')", + " const result = close.call(iterator)", + " if (result == null || (typeof result !== 'object' && typeof result \ + !== 'function'))", + " throw new TypeError('iterator return method returned a \ + non-object value')", + " }}", + " throw new TypeError('sync iterator does not provide a throw method')", + " }}", + " if (typeof method !== 'function')", + " throw new TypeError('iterator throw property is not callable')", + " return continueWith(", + " arguments.length === 0 ? method.call(iterator) : method.call(iterator, \ + value),", + " true,", + " )", + " }} catch (error) {{", + " return reject(error)", + " }}", + " }},", + " [Symbol.asyncIterator]() {{ return this }},", + " }}", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.next", + "(iterator) => Promise.resolve(iterator.next()).then(result => {{", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('async iterator next method returned a non-object value')", + " return result", + "}})", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.next.value", + "(iterator, value) => Promise.resolve(iterator.next(value)).then(result => {{", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('async iterator next method returned a non-object value')", + " return result", + "}})", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.return", + "(iterator) => {{", + " const method = iterator.return", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('async iterator return property is not callable')", + " return Promise.resolve(method.call(iterator)).then(result => {{", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('async iterator return method returned a non-object value')", + " return result", + " }})", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.return.value", + "(iterator, value) => {{", + " const method = iterator.return", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('async iterator return property is not callable')", + " return Promise.resolve(method.call(iterator, value)).then(result => {{", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('async iterator return method returned a non-object value')", + " return result", + " }})", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.throw", + "(iterator, value) => {{", + " const method = iterator.throw", + " if (method == null) return null", + " if (typeof method !== 'function')", + " throw new TypeError('async iterator throw property is not callable')", + " return Promise.resolve(method.call(iterator, value)).then(result => {{", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('async iterator throw method returned a non-object value')", + " return result", + " }})", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.dispose", + "(iterator) => {{", + " const method = iterator[Symbol.asyncDispose]", + " if (typeof method !== 'function')", + " throw new TypeError('async iterator does not provide Symbol.asyncDispose')", + " return Promise.resolve(method.call(iterator))", + "}}", +); + +/// A JavaScript type known to implement `Symbol.iterator`. +pub trait Iterable: JsCast + AsRef { + type Item: JsCast; +} + +/// A JavaScript type known to implement `Symbol.asyncIterator`. +pub trait AsyncIterable: JsCast + AsRef { + type Item: JsCast; +} + +impl IteratorZipOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip#mode) + pub fn set_mode(&self, mode: IteratorZipMode) { + self.set_zip_mode_raw(mode.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zip#padding) + pub fn set_padding(&self, padding: &I) { + self.set_zip_padding_raw(padding.as_ref()); + } +} + +impl Default for IteratorZipOptions { + fn default() -> Self { + Self::new() + } +} + +impl IteratorZipKeyedOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zipKeyed#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zipKeyed#mode) + pub fn set_mode(&self, mode: IteratorZipMode) { + self.set_zip_keyed_mode_raw(mode.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/zipKeyed#padding) + pub fn set_padding(&self, padding: &Object) { + self.set_zip_keyed_padding_raw(padding); + } +} + +impl Default for IteratorZipKeyedOptions { + fn default() -> Self { + Self::new() + } +} + +impl Iterable for JsIterator { + type Item = T; +} + +impl AsyncIterable for AsyncIterator { + type Item = T; +} + +impl JsIterator { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/from) + pub fn from_iterable>(value: &I) -> Result { + let iterator = JsIterator::from_value(value.as_ref())?; + Ok(Self::unchecked_from(iterator.into())) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_next_method) + pub fn next_result(&self) -> Result { + iterator_next(self) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_next_method) + pub fn next_result_with_value(&self, value: &JsValue) -> Result { + iterator_next_with_value(self, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_return_method) + pub fn return_result(&self) -> Result, JsValue> { + iterator_return(self) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_return_method) + pub fn return_result_with_value( + &self, + value: &JsValue, + ) -> Result, JsValue> { + iterator_return_with_value(self, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_throw_method) + pub fn throw_result(&self, value: &JsValue) -> Result, JsValue> { + iterator_throw(self, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/Symbol.dispose) + pub fn dispose(&self) -> Result<(), JsValue> { + iterator_dispose(self) + } +} + +impl AsyncIterator { + /// Creates an `async` iterator from an asynchronous or synchronous + /// `iterable`. + /// + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#description) + pub fn try_from_value(value: &JsValue) -> Result, JsValue> { + Ok(async_iterator_from(value)?.map(|iterator| Self::unchecked_from(iterator.into()))) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_next_method) + pub fn next_result(&self) -> Result, JsValue> { + async_iterator_next(self) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_next_method) + pub fn next_result_with_value( + &self, + value: &JsValue, + ) -> Result, JsValue> { + async_iterator_next_with_value(self, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_return_method) + pub fn return_result(&self) -> Result>, JsValue> { + async_iterator_return(self) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_return_method) + pub fn return_result_with_value( + &self, + value: &JsValue, + ) -> Result>, JsValue> { + async_iterator_return_with_value(self, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_throw_method) + pub fn throw_result( + &self, + value: &JsValue, + ) -> Result>, JsValue> { + async_iterator_throw(self, value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator/Symbol.asyncDispose) + pub fn dispose(&self) -> Result { + async_iterator_dispose(self) + } +} + +impl IteratorResult { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol) + pub fn done(&self) -> Result { + iterator_result_done(self) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol) + pub fn value(&self) -> Result { + iterator_result_value(self) + } +} diff --git a/client/js-sys/src/builtins/json.rs b/client/js-sys/src/builtins/json.rs new file mode 100644 index 00000000..8d188b48 --- /dev/null +++ b/client/js-sys/src/builtins/json.rs @@ -0,0 +1,43 @@ +use crate::{Function, JsString, JsValue, Object, js_sys}; + +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod JSON { + use super::*; + + #[js_sys(js_sys = crate, namespace = "JSON")] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) + pub fn parse(text: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) + #[js_sys(js_name = "parse")] + pub fn parse_with_reviver(text: &str, reviver: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) + pub fn stringify(value: &JsValue) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) + #[js_sys(js_name = "stringify")] + pub fn stringify_with_replacer( + value: &JsValue, + replacer: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) + #[js_sys(js_name = "stringify")] + pub fn stringify_with_replacer_and_space( + value: &JsValue, + replacer: &JsValue, + space: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/isRawJSON) + #[must_use] + #[js_sys(js_name = "isRawJSON")] + pub fn is_raw_json(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/rawJSON) + #[js_sys(js_name = "rawJSON")] + pub fn raw_json(text: &str) -> Result; + } +} diff --git a/client/js-sys/src/builtins/map.rs b/client/js-sys/src/builtins/map.rs new file mode 100644 index 00000000..c7919c2b --- /dev/null +++ b/client/js-sys/src/builtins/map.rs @@ -0,0 +1,163 @@ +use core::fmt::{self, Formatter}; + +use super::{Array, Function, Iterable, JsIterator, Object}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + #[js_sys(extends = Object)] + pub type Map; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Map; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) + #[must_use] + #[js_sys(constructor, return_abi = Map)] + pub fn new_typed() -> Map; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) + #[js_sys(constructor, return_abi = Result)] + pub fn new_from_iterable( + #[js_sys(type = &JsValue)] entries: &I, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy) + #[js_sys(static_of = Map, js_name = "groupBy")] + pub fn group_by(items: &JsValue, callback: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) + pub fn clear(self: &Map); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete) + #[must_use] + pub fn delete(self: &Map, #[js_sys(type = &JsValue)] key: &K) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) + #[must_use] + pub fn entries(self: &Map) -> JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each(self: &Map, callback: &Function) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each_with_this( + self: &Map, + callback: &Function, + this: &JsValue, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) + #[must_use] + pub fn get(self: &Map, #[js_sys(type = &JsValue)] key: &K) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) + #[must_use] + #[js_sys(js_name = "get", return_abi = Option)] + pub fn get_checked( + self: &Map, + #[js_sys(type = &JsValue)] key: &K, + ) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/getOrInsert) + #[must_use] + #[js_sys(js_name = "getOrInsert", return_abi = JsValue)] + pub fn get_or_insert( + self: &Map, + #[js_sys(type = &JsValue)] key: &K, + #[js_sys(type = &JsValue)] default_value: &V, + ) -> V; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/getOrInsertComputed) + #[js_sys( + js_name = "getOrInsertComputed", + return_abi = Result + )] + pub fn get_or_insert_computed( + self: &Map, + #[js_sys(type = &JsValue)] key: &K, + callback: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) + #[must_use] + pub fn has(self: &Map, #[js_sys(type = &JsValue)] key: &K) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys) + #[must_use] + #[js_sys(return_abi = JsIterator)] + pub fn keys(self: &Map) -> JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set) + #[must_use] + #[js_sys(return_abi = Map)] + pub fn set( + self: &Map, + #[js_sys(type = &JsValue)] key: &K, + #[js_sys(type = &JsValue)] value: &V, + ) -> Map; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size) + #[must_use] + #[js_sys(getter)] + pub fn size(self: &Map) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values) + #[must_use] + #[js_sys(return_abi = JsIterator)] + pub fn values(self: &Map) -> JsIterator; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "map.symbol_iterator")] + fn map_symbol_iterator(#[js_sys(type = &JsValue)] map: &Map) -> JsIterator; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "map.symbol_iterator", + "(map) => map[Symbol.iterator]()", +); + +impl Clone for Map { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } +} + +impl fmt::Debug for Map { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } +} + +impl PartialEq for Map { + fn eq(&self, other: &Self) -> bool { + >::as_ref(self) == >::as_ref(other) + } +} + +impl Default for Map { + fn default() -> Self { + Self::new_typed() + } +} + +impl Map { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Symbol.iterator) + #[must_use] + pub fn symbol_iterator(&self) -> JsIterator { + map_symbol_iterator(self) + } +} + +impl Iterable for Map { + type Item = Array; +} diff --git a/client/js-sys/src/builtins/math.rs b/client/js-sys/src/builtins/math.rs new file mode 100644 index 00000000..3c5cea7b --- /dev/null +++ b/client/js-sys/src/builtins/math.rs @@ -0,0 +1,258 @@ +use super::Iterable; +use crate::js_sys; +use crate::util::{PtrConst, PtrLength}; + +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod Math { + use super::*; + + #[js_sys(js_sys = crate, namespace = "Math")] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/E) + #[must_use] + #[js_sys(getter = "E")] + pub fn e() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/LN10) + #[must_use] + #[js_sys(getter = "LN10")] + pub fn ln_10() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/LN2) + #[must_use] + #[js_sys(getter = "LN2")] + pub fn ln_2() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/LOG10E) + #[must_use] + #[js_sys(getter = "LOG10E")] + pub fn log_10_e() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/LOG2E) + #[must_use] + #[js_sys(getter = "LOG2E")] + pub fn log_2_e() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/PI) + #[must_use] + #[js_sys(getter = "PI")] + pub fn pi() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/SQRT1_2) + #[must_use] + #[js_sys(getter = "SQRT1_2")] + pub fn sqrt_1_2() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/SQRT2) + #[must_use] + #[js_sys(getter = "SQRT2")] + pub fn sqrt_2() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs) + #[must_use] + pub fn abs(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos) + #[must_use] + pub fn acos(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh) + #[must_use] + pub fn acosh(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin) + #[must_use] + pub fn asin(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh) + #[must_use] + pub fn asinh(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan) + #[must_use] + pub fn atan(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2) + #[must_use] + pub fn atan2(y: f64, x: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh) + #[must_use] + pub fn atanh(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt) + #[must_use] + pub fn cbrt(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil) + #[must_use] + pub fn ceil(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32) + #[must_use] + pub fn clz32(value: u32) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos) + #[must_use] + pub fn cos(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh) + #[must_use] + pub fn cosh(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp) + #[must_use] + pub fn exp(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1) + #[must_use] + pub fn expm1(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/f16round) + #[must_use] + pub fn f16round(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor) + #[must_use] + pub fn floor(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround) + #[must_use] + pub fn fround(value: f64) -> f32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot) + #[must_use] + pub fn hypot(x: f64, y: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul) + #[must_use] + pub fn imul(left: i32, right: i32) -> i32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log) + #[must_use] + pub fn log(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p) + #[must_use] + pub fn log1p(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10) + #[must_use] + pub fn log10(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2) + #[must_use] + pub fn log2(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) + #[must_use] + pub fn max(x: f64, y: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min) + #[must_use] + pub fn min(x: f64, y: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow) + #[must_use] + pub fn pow(base: f64, exponent: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) + #[must_use] + pub fn random() -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round) + #[must_use] + pub fn round(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign) + #[must_use] + pub fn sign(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin) + #[must_use] + pub fn sin(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh) + #[must_use] + pub fn sinh(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt) + #[must_use] + pub fn sqrt(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sumPrecise) + #[js_sys(js_name = "sumPrecise")] + pub fn sum_precise( + #[js_sys(type = &crate::JsValue)] numbers: &I, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan) + #[must_use] + pub fn tan(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh) + #[must_use] + pub fn tanh(value: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc) + #[must_use] + pub fn trunc(value: f64) -> f64; + } + + #[js_sys(js_sys = crate)] + extern "js-sys" { + #[js_sys(js_embed = "math.hypot")] + unsafe fn hypot_many_raw(values: PtrConst, len: PtrLength) -> f64; + + #[js_sys(js_embed = "math.max")] + unsafe fn max_many_raw(values: PtrConst, len: PtrLength) -> f64; + + #[js_sys(js_embed = "math.min")] + unsafe fn min_many_raw(values: PtrConst, len: PtrLength) -> f64; + } + + js_bindgen::embed_js!( + module = "js_sys", + name = "math.hypot", + required_embeds = [("js_sys", "view.getFloat64")], + "(ptr, len) => Math.hypot(...this.#jsEmbed.js_sys['view.getFloat64'](ptr, len))", + ); + + js_bindgen::embed_js!( + module = "js_sys", + name = "math.max", + required_embeds = [("js_sys", "view.getFloat64")], + "(ptr, len) => Math.max(...this.#jsEmbed.js_sys['view.getFloat64'](ptr, len))", + ); + + js_bindgen::embed_js!( + module = "js_sys", + name = "math.min", + required_embeds = [("js_sys", "view.getFloat64")], + "(ptr, len) => Math.min(...this.#jsEmbed.js_sys['view.getFloat64'](ptr, len))", + ); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot) + #[must_use] + pub fn hypot_many(values: &[f64]) -> f64 { + // SAFETY: The pointer and length describe `values` for the duration of the + // call. + unsafe { hypot_many_raw(PtrConst::new(values), PtrLength::new(values)) } + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) + #[must_use] + pub fn max_many(values: &[f64]) -> f64 { + // SAFETY: The pointer and length describe `values` for the duration of the + // call. + unsafe { max_many_raw(PtrConst::new(values), PtrLength::new(values)) } + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min) + #[must_use] + pub fn min_many(values: &[f64]) -> f64 { + // SAFETY: The pointer and length describe `values` for the duration of the + // call. + unsafe { min_many_raw(PtrConst::new(values), PtrLength::new(values)) } + } +} diff --git a/client/js-sys/src/builtins/mod.rs b/client/js-sys/src/builtins/mod.rs index f839741a..bacf424a 100644 --- a/client/js-sys/src/builtins/mod.rs +++ b/client/js-sys/src/builtins/mod.rs @@ -1,17 +1,88 @@ mod array; +mod array_buffer; +mod async_disposable_stack; +mod atomics; mod bigint; +mod boolean; +mod data_view; +mod date; +mod disposable_stack; +mod dynamic_function; mod error; +mod finalization_registry; mod function; +mod generator; +mod global; +mod intl; +pub(crate) mod iterator; +mod json; +mod map; +mod math; mod number; mod object; mod promise; +mod proxy; +mod reflect; +mod regexp; +mod set; mod string; +mod symbol; +mod temporal; +mod typed_array; +mod uint8_array; +mod weak_map; +mod weak_ref; +mod weak_set; +mod webassembly; -pub use array::{JsArray, TryFromJsArrayError}; -pub use bigint::JsBigInt; -pub use error::{Error, ErrorOptions}; +pub use array::Array; +pub use array_buffer::{ArrayBuffer, ArrayBufferOptions, SharedArrayBuffer}; +pub use async_disposable_stack::AsyncDisposableStack; +pub use atomics::Atomics; +pub use bigint::BigInt; +pub use boolean::Boolean; +pub use data_view::DataView; +pub use date::Date; +pub use disposable_stack::DisposableStack; +pub use dynamic_function::{AsyncFunction, AsyncGeneratorFunction, GeneratorFunction}; +pub use error::{ + AggregateError, Error, ErrorOptions, EvalError, RangeError, ReferenceError, SuppressedError, + SyntaxError, TypeError, UriError, +}; +pub use finalization_registry::FinalizationRegistry; pub use function::Function; -pub use number::JsNumber; -pub use object::Object; +pub use generator::{AsyncGenerator, Generator}; +pub use global::{ + decode_uri, decode_uri_component, encode_uri, encode_uri_component, eval, global_this, + is_finite, is_nan, parse_float, parse_int, parse_int_with_radix, +}; +pub use intl::Intl; +pub use iterator::{ + AsyncIterable, AsyncIterator, Iterable, IteratorResult, IteratorZipKeyedOptions, + IteratorZipMode, IteratorZipOptions, JsIterator, +}; +pub use json::JSON; +pub use map::Map; +pub use math::Math; +pub use number::Number; +pub use object::{Object, PropertyDescriptor}; pub use promise::{Promise, PromiseWithResolvers}; +pub use proxy::{Proxy, ProxyRevocable}; +pub use reflect::Reflect; +pub use regexp::{RegExp, RegExpIndicesArray, RegExpMatchArray}; +pub use set::Set; pub use string::JsString; +pub use symbol::Symbol; +pub use temporal::Temporal; +pub use typed_array::{ + BigInt64Array, BigUint64Array, Float16Array, Float32Array, Float64Array, Int8Array, Int16Array, + Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array, +}; +pub use uint8_array::{ + Base64Alphabet, Base64DecodeOptions, Base64EncodeOptions, Base64LastChunkHandling, + Uint8ArraySetResult, +}; +pub use weak_map::WeakMap; +pub use weak_ref::WeakRef; +pub use weak_set::WeakSet; +pub use webassembly::WebAssembly; diff --git a/client/js-sys/src/builtins/number.rs b/client/js-sys/src/builtins/number.rs index 104a2e4c..8b72841a 100644 --- a/client/js-sys/src/builtins/number.rs +++ b/client/js-sys/src/builtins/number.rs @@ -1,5 +1,135 @@ +use super::JsString; +use crate::JsValue; + #[crate::js_sys(js_sys = crate)] extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) #[js_sys(js_name = "Number")] - pub type JsNumber; + #[derive(Clone, Debug, PartialEq)] + pub type Number; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/Number) + #[js_sys(js_name = "Number")] + fn number_constructor(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite) + #[must_use] + #[js_sys(static_of = Number, js_name = "isFinite")] + pub fn is_finite(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) + #[must_use] + #[js_sys(static_of = Number, js_name = "isInteger")] + pub fn is_integer(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) + #[must_use] + #[js_sys(static_of = Number, js_name = "isNaN")] + pub fn is_nan(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger) + #[must_use] + #[js_sys(static_of = Number, js_name = "isSafeInteger")] + pub fn is_safe_integer(value: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/parseFloat) + #[must_use] + #[js_sys(static_of = Number, js_name = "parseFloat")] + pub fn parse_float(value: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/parseInt) + #[must_use] + #[js_sys(static_of = Number, js_name = "parseInt")] + pub fn parse_int(value: &str, radix: u8) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential) + #[must_use] + #[js_sys(js_name = "toExponential")] + pub fn to_exponential(self: &Number) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential) + #[js_sys(js_name = "toExponential")] + pub fn to_exponential_with_digits( + self: &Number, + fraction_digits: u8, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) + #[must_use] + #[js_sys(js_name = "toFixed")] + pub fn to_fixed(self: &Number) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) + #[js_sys(js_name = "toFixed")] + pub fn to_fixed_with_digits(self: &Number, digits: u8) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString) + #[must_use] + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &Number) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locale( + self: &Number, + locale: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_options( + self: &Number, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision) + #[must_use] + #[js_sys(js_name = "toPrecision")] + pub fn to_precision(self: &Number) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision) + #[js_sys(js_name = "toPrecision")] + pub fn to_precision_with_digits( + self: &Number, + precision: u8, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Number) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_radix(self: &Number, radix: u8) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/valueOf) + #[must_use] + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Number) -> f64; +} + +impl Number { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/Number) + pub fn new(value: &JsValue) -> Result { + number_constructor(value) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON) + pub const EPSILON: f64 = f64::EPSILON; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) + pub const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_VALUE) + pub const MAX_VALUE: f64 = f64::MAX; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) + pub const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_VALUE) + pub const MIN_VALUE: f64 = f64::from_bits(1); + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/NaN) + pub const NAN: f64 = f64::NAN; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/NEGATIVE_INFINITY) + pub const NEGATIVE_INFINITY: f64 = f64::NEG_INFINITY; + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/POSITIVE_INFINITY) + pub const POSITIVE_INFINITY: f64 = f64::INFINITY; } diff --git a/client/js-sys/src/builtins/object.rs b/client/js-sys/src/builtins/object.rs index 326b2c17..44c88fab 100644 --- a/client/js-sys/src/builtins/object.rs +++ b/client/js-sys/src/builtins/object.rs @@ -1,14 +1,232 @@ -use crate::{JsValue, js_sys}; +use crate::hazard::JsCast; +use crate::{Array, Function, JsString, JsValue, Symbol, js_sys}; #[js_sys(js_sys = crate)] extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) #[derive(Clone, Debug)] pub type Object; - /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description) + #[derive(Clone, Debug)] + pub type PropertyDescriptor; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/Object) #[must_use] #[js_sys(constructor)] pub fn new() -> Object; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/Object) + #[must_use] + #[js_sys(constructor)] + pub fn new_with_value(value: &JsValue) -> Object; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + #[js_sys(static_of = Object)] + pub fn assign(target: &Object, source: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + #[js_sys(static_of = Object, js_name = "assign", variadic)] + pub fn assign_many( + target: &Object, + #[js_sys(type = &[JsValue])] sources: &[Object], + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) + #[js_sys(static_of = Object)] + pub fn create(prototype: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) + #[js_sys(static_of = Object, js_name = "create")] + pub fn create_with_properties( + prototype: &JsValue, + properties: &Object, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties) + #[js_sys(static_of = Object, js_name = "defineProperties")] + pub fn define_properties( + object: &Object, + properties: &Object, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) + #[js_sys(static_of = Object, js_name = "defineProperty")] + pub fn define_property( + object: &Object, + property: &JsValue, + descriptor: &PropertyDescriptor, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries) + #[js_sys(static_of = Object)] + pub fn entries(object: &Object) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) + #[js_sys(static_of = Object)] + pub fn freeze(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) + #[js_sys(static_of = Object, js_name = "fromEntries")] + pub fn from_entries(entries: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) + #[js_sys(static_of = Object, js_name = "getOwnPropertyDescriptor")] + pub fn get_own_property_descriptor( + object: &Object, + property: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors) + #[js_sys(static_of = Object, js_name = "getOwnPropertyDescriptors")] + pub fn get_own_property_descriptors( + object: &Object, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames) + #[js_sys( + static_of = Object, + js_name = "getOwnPropertyNames" + )] + pub fn get_own_property_names(object: &Object) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols) + #[js_sys( + static_of = Object, + js_name = "getOwnPropertySymbols" + )] + pub fn get_own_property_symbols(object: &Object) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf) + #[js_sys(static_of = Object, js_name = "getPrototypeOf")] + pub fn get_prototype_of(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) + #[js_sys(static_of = Object, js_name = "groupBy")] + pub fn group_by(items: &JsValue, callback: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn) + #[js_sys(static_of = Object, js_name = "hasOwn")] + pub fn has_own(object: &Object, property: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) + #[must_use] + #[js_sys(static_of = Object)] + pub fn is(left: &JsValue, right: &JsValue) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible) + #[js_sys(static_of = Object, js_name = "isExtensible")] + pub fn is_extensible(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen) + #[js_sys(static_of = Object, js_name = "isFrozen")] + pub fn is_frozen(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed) + #[js_sys(static_of = Object, js_name = "isSealed")] + pub fn is_sealed(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys) + #[js_sys(static_of = Object)] + pub fn keys(object: &Object) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions) + #[js_sys(static_of = Object, js_name = "preventExtensions")] + pub fn prevent_extensions(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) + #[js_sys(static_of = Object)] + pub fn seal(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) + #[js_sys(static_of = Object, js_name = "setPrototypeOf")] + pub fn set_prototype_of(object: &Object, prototype: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values) + #[js_sys(static_of = Object)] + pub fn values(object: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) + #[js_sys(js_name = "hasOwnProperty")] + pub fn has_own_property(self: &Object, property: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf) + #[js_sys(js_name = "isPrototypeOf")] + pub fn is_prototype_of(self: &Object, value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable) + #[js_sys(js_name = "propertyIsEnumerable")] + pub fn property_is_enumerable(self: &Object, property: &JsValue) + -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#configurable) + #[must_use] + #[js_sys(getter)] + pub fn configurable(self: &PropertyDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#configurable) + #[js_sys(setter)] + pub fn set_configurable(self: &PropertyDescriptor, value: bool); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#enumerable) + #[must_use] + #[js_sys(getter)] + pub fn enumerable(self: &PropertyDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#enumerable) + #[js_sys(setter)] + pub fn set_enumerable(self: &PropertyDescriptor, value: bool); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#get) + #[must_use] + #[js_sys(getter = "get")] + pub fn get(self: &PropertyDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#get) + #[js_sys(setter = "get")] + pub fn set_get(self: &PropertyDescriptor, value: &Function); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#set) + #[must_use] + #[js_sys(getter = "set")] + pub fn set(self: &PropertyDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#set) + #[js_sys(setter = "set")] + pub fn set_set(self: &PropertyDescriptor, value: &Function); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#value) + #[must_use] + #[js_sys(getter)] + pub fn value(self: &PropertyDescriptor) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#value) + #[js_sys(setter)] + pub fn set_value(self: &PropertyDescriptor, value: &JsValue); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#writable) + #[must_use] + #[js_sys(getter)] + pub fn writable(self: &PropertyDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#writable) + #[js_sys(setter)] + pub fn set_writable(self: &PropertyDescriptor, value: bool); } impl Default for Object { @@ -16,3 +234,17 @@ impl Default for Object { Self::new() } } + +impl PropertyDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } +} + +impl Default for PropertyDescriptor { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/proxy.rs b/client/js-sys/src/builtins/proxy.rs new file mode 100644 index 00000000..f951fda9 --- /dev/null +++ b/client/js-sys/src/builtins/proxy.rs @@ -0,0 +1,33 @@ +use super::{Function, Object}; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + #[js_sys(js_name = "Proxy", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Proxy; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ProxyRevocable; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) + #[js_sys(constructor)] + pub fn new(target: &JsValue, handler: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable) + #[js_sys(static_of = Proxy)] + pub fn revocable(target: &JsValue, handler: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable) + #[must_use] + #[js_sys(getter)] + pub fn proxy(self: &ProxyRevocable) -> Proxy; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable) + #[must_use] + #[js_sys(getter)] + pub fn revoke(self: &ProxyRevocable) -> Function; +} diff --git a/client/js-sys/src/builtins/reflect.rs b/client/js-sys/src/builtins/reflect.rs new file mode 100644 index 00000000..4ce9b1a8 --- /dev/null +++ b/client/js-sys/src/builtins/reflect.rs @@ -0,0 +1,143 @@ +use crate::{Array, Function, JsValue, js_sys}; + +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod Reflect { + use super::*; + + #[js_sys(js_sys = crate, namespace = "Reflect")] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply) + pub fn apply( + target: &Function, + this_argument: &JsValue, + arguments_list: &Array, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct) + pub fn construct(target: &Function, arguments_list: &Array) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct) + #[js_sys(js_name = "construct")] + pub fn construct_with_new_target( + target: &Function, + arguments_list: &Array, + new_target: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/defineProperty) + #[js_sys(js_name = "defineProperty")] + pub fn define_property( + target: &JsValue, + property_key: &JsValue, + attributes: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/defineProperty) + #[js_sys(js_name = "defineProperty")] + pub fn define_property_str( + target: &JsValue, + property_key: &str, + attributes: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/deleteProperty) + #[js_sys(js_name = "deleteProperty")] + pub fn delete_property(target: &JsValue, property_key: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/deleteProperty) + #[js_sys(js_name = "deleteProperty")] + pub fn delete_property_str(target: &JsValue, property_key: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get) + pub fn get(target: &JsValue, property_key: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get) + #[js_sys(js_name = "get")] + pub fn get_str(target: &JsValue, property_key: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get) + #[js_sys(js_name = "get")] + pub fn get_u32(target: &JsValue, property_key: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get) + #[js_sys(js_name = "get")] + pub fn get_with_receiver( + target: &JsValue, + property_key: &JsValue, + receiver: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor) + #[js_sys(js_name = "getOwnPropertyDescriptor")] + pub fn get_own_property_descriptor( + target: &JsValue, + property_key: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor) + #[js_sys(js_name = "getOwnPropertyDescriptor")] + pub fn get_own_property_descriptor_str( + target: &JsValue, + property_key: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getPrototypeOf) + #[js_sys(js_name = "getPrototypeOf")] + pub fn get_prototype_of(target: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has) + pub fn has(target: &JsValue, property_key: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has) + #[js_sys(js_name = "has")] + pub fn has_str(target: &JsValue, property_key: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/isExtensible) + #[js_sys(js_name = "isExtensible")] + pub fn is_extensible(target: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys) + #[js_sys(js_name = "ownKeys")] + pub fn own_keys(target: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/preventExtensions) + #[js_sys(js_name = "preventExtensions")] + pub fn prevent_extensions(target: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set) + pub fn set( + target: &JsValue, + property_key: &JsValue, + value: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set) + #[js_sys(js_name = "set")] + pub fn set_str( + target: &JsValue, + property_key: &str, + value: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set) + #[js_sys(js_name = "set")] + pub fn set_u32( + target: &JsValue, + property_key: u32, + value: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set) + #[js_sys(js_name = "set")] + pub fn set_with_receiver( + target: &JsValue, + property_key: &JsValue, + value: &JsValue, + receiver: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/setPrototypeOf) + #[js_sys(js_name = "setPrototypeOf")] + pub fn set_prototype_of(target: &JsValue, prototype: &JsValue) -> Result; + } +} diff --git a/client/js-sys/src/builtins/regexp.rs b/client/js-sys/src/builtins/regexp.rs new file mode 100644 index 00000000..3e6f50e4 --- /dev/null +++ b/client/js-sys/src/builtins/regexp.rs @@ -0,0 +1,270 @@ +use super::{Array, Function, JsIterator, JsString, Number, Object}; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) + #[js_sys(js_name = "RegExp", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RegExp; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) + #[js_sys(constructor)] + pub fn new(pattern: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) + #[js_sys(constructor)] + pub fn new_with_flags(pattern: &str, flags: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) + #[must_use] + #[js_sys(constructor)] + pub fn new_from_regexp(pattern: &RegExp) -> RegExp; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) + #[js_sys(constructor)] + pub fn new_from_regexp_with_flags(pattern: &RegExp, flags: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape) + #[must_use] + #[js_sys(static_of = RegExp)] + pub fn escape(input: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll) + #[must_use] + #[js_sys(getter = "dotAll")] + pub fn dot_all(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec) + #[must_use] + pub fn exec(self: &RegExp, input: &str) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags) + #[must_use] + #[js_sys(getter)] + pub fn flags(self: &RegExp) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global) + #[must_use] + #[js_sys(getter)] + pub fn global(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/hasIndices) + #[must_use] + #[js_sys(getter = "hasIndices")] + pub fn has_indices(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase) + #[must_use] + #[js_sys(getter = "ignoreCase")] + pub fn ignore_case(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex) + #[must_use] + #[js_sys(getter = "lastIndex")] + pub fn last_index(self: &RegExp) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex) + #[js_sys(setter = "lastIndex")] + pub fn set_last_index(self: &RegExp, index: f64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline) + #[must_use] + #[js_sys(getter)] + pub fn multiline(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source) + #[must_use] + #[js_sys(getter)] + pub fn source(self: &RegExp) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) + #[must_use] + #[js_sys(getter)] + pub fn sticky(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) + #[must_use] + pub fn test(self: &RegExp, input: &str) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &RegExp) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode) + #[must_use] + #[js_sys(getter)] + pub fn unicode(self: &RegExp) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets) + #[must_use] + #[js_sys(getter = "unicodeSets")] + pub fn unicode_sets(self: &RegExp) -> bool; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[js_sys(extends = Array, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RegExpMatchArray; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[must_use] + #[js_sys(getter)] + pub fn index(self: &RegExpMatchArray) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[must_use] + #[js_sys(getter)] + pub fn input(self: &RegExpMatchArray) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[must_use] + #[js_sys(getter = "groups")] + pub fn groups(self: &RegExpMatchArray) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[must_use] + #[js_sys(getter = "indices")] + pub fn indices(self: &RegExpMatchArray) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[must_use] + #[js_sys(getter)] + pub fn length(self: &RegExpMatchArray) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value) + #[must_use] + #[js_sys(indexing_getter)] + pub fn get(self: &RegExpMatchArray, index: u32) -> Option; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#indices) + #[js_sys(extends = Array, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type RegExpIndicesArray; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#indices) + #[must_use] + #[js_sys(getter = "groups")] + pub fn groups(self: &RegExpIndicesArray) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#indices) + #[must_use] + #[js_sys(getter)] + pub fn length(self: &RegExpIndicesArray) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#indices) + #[must_use] + #[js_sys(indexing_getter)] + pub fn get(self: &RegExpIndicesArray, index: u32) -> Option>; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "regexp.match")] + fn regexp_match(regexp: &RegExp, input: &str) -> Option; + + #[js_sys(js_embed = "regexp.match_all")] + fn regexp_match_all(regexp: &RegExp, input: &str) -> JsIterator; + + #[js_sys(js_embed = "regexp.replace")] + fn regexp_replace(regexp: &RegExp, input: &str, replacement: &str) -> JsString; + + #[js_sys(js_embed = "regexp.replace")] + fn regexp_replace_with_function( + regexp: &RegExp, + input: &str, + replacement: &Function, + ) -> Result; + + #[js_sys(js_embed = "regexp.search")] + fn regexp_search(regexp: &RegExp, input: &str) -> f64; + + #[js_sys(js_embed = "regexp.split")] + fn regexp_split(regexp: &RegExp, input: &str) -> Array; + + #[js_sys(js_embed = "regexp.split")] + fn regexp_split_with_limit(regexp: &RegExp, input: &str, limit: u32) -> Array; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "regexp.match", + "(regexp, input) => regexp[Symbol.match](input)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "regexp.match_all", + "(regexp, input) => regexp[Symbol.matchAll](input)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "regexp.replace", + "(regexp, input, replacement) => regexp[Symbol.replace](input, replacement)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "regexp.search", + "(regexp, input) => regexp[Symbol.search](input)", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "regexp.split", + "(regexp, input, limit) => regexp[Symbol.split](input, limit)", +); + +impl RegExp { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.match) + #[must_use] + pub fn match_(&self, input: &str) -> Option { + regexp_match(self, input) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.matchAll) + #[must_use] + pub fn match_all(&self, input: &str) -> JsIterator { + regexp_match_all(self, input) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.replace) + #[must_use] + pub fn replace(&self, input: &str, replacement: &str) -> JsString { + regexp_replace(self, input, replacement) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.replace) + pub fn replace_with_function( + &self, + input: &str, + replacement: &Function, + ) -> Result { + regexp_replace_with_function(self, input, replacement) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.search) + #[must_use] + pub fn search(&self, input: &str) -> f64 { + regexp_search(self, input) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.split) + #[must_use] + pub fn split(&self, input: &str) -> Array { + regexp_split(self, input) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/Symbol.split) + #[must_use] + pub fn split_with_limit(&self, input: &str, limit: u32) -> Array { + regexp_split_with_limit(self, input, limit) + } +} diff --git a/client/js-sys/src/builtins/set.rs b/client/js-sys/src/builtins/set.rs new file mode 100644 index 00000000..b9c60f6e --- /dev/null +++ b/client/js-sys/src/builtins/set.rs @@ -0,0 +1,161 @@ +use core::fmt::{self, Formatter}; + +use super::{Array, Function, Iterable, JsIterator, Object}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) + #[js_sys(extends = Object)] + pub type Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) + #[must_use] + #[js_sys(constructor, return_abi = Set)] + pub fn new_typed() -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) + #[js_sys(constructor, return_abi = Result)] + pub fn new_from_iterable>( + #[js_sys(type = &JsValue)] items: &I, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add) + #[must_use] + #[js_sys(return_abi = Set)] + pub fn add(self: &Set, #[js_sys(type = &JsValue)] value: &T) -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear) + pub fn clear(self: &Set); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete) + #[must_use] + pub fn delete(self: &Set, #[js_sys(type = &JsValue)] value: &T) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/difference) + #[must_use] + #[js_sys(return_abi = Set)] + pub fn difference(self: &Set, #[js_sys(type = &JsValue)] other: &Set) -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries) + #[must_use] + pub fn entries(self: &Set) -> JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each(self: &Set, callback: &Function) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/forEach) + #[js_sys(js_name = "forEach")] + pub fn for_each_with_this( + self: &Set, + callback: &Function, + this: &JsValue, + ) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) + #[must_use] + pub fn has(self: &Set, #[js_sys(type = &JsValue)] value: &T) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection) + #[must_use] + #[js_sys(return_abi = Set)] + pub fn intersection(self: &Set, #[js_sys(type = &JsValue)] other: &Set) -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isDisjointFrom) + #[must_use] + #[js_sys(js_name = "isDisjointFrom")] + pub fn is_disjoint_from(self: &Set, #[js_sys(type = &JsValue)] other: &Set) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSubsetOf) + #[must_use] + #[js_sys(js_name = "isSubsetOf")] + pub fn is_subset_of(self: &Set, #[js_sys(type = &JsValue)] other: &Set) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSupersetOf) + #[must_use] + #[js_sys(js_name = "isSupersetOf")] + pub fn is_superset_of(self: &Set, #[js_sys(type = &JsValue)] other: &Set) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/keys) + #[must_use] + #[js_sys(return_abi = JsIterator)] + pub fn keys(self: &Set) -> JsIterator; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/size) + #[must_use] + #[js_sys(getter)] + pub fn size(self: &Set) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference) + #[must_use] + #[js_sys(js_name = "symmetricDifference", return_abi = Set)] + pub fn symmetric_difference( + self: &Set, + #[js_sys(type = &JsValue)] other: &Set, + ) -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/union) + #[must_use] + #[js_sys(return_abi = Set)] + pub fn union(self: &Set, #[js_sys(type = &JsValue)] other: &Set) -> Set; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values) + #[must_use] + #[js_sys(return_abi = JsIterator)] + pub fn values(self: &Set) -> JsIterator; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "set.symbol_iterator", return_abi = JsIterator)] + fn set_symbol_iterator(#[js_sys(type = &JsValue)] set: &Set) -> JsIterator; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "set.symbol_iterator", + "(set) => set[Symbol.iterator]()", +); + +impl Clone for Set { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } +} + +impl fmt::Debug for Set { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } +} + +impl PartialEq for Set { + fn eq(&self, other: &Self) -> bool { + >::as_ref(self) == >::as_ref(other) + } +} + +impl Default for Set { + fn default() -> Self { + Self::new_typed() + } +} + +impl Set { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Symbol.iterator) + #[must_use] + pub fn symbol_iterator(&self) -> JsIterator { + set_symbol_iterator(self) + } +} + +impl Iterable for Set { + type Item = T; +} diff --git a/client/js-sys/src/builtins/string.rs b/client/js-sys/src/builtins/string.rs index 66451cba..86138163 100644 --- a/client/js-sys/src/builtins/string.rs +++ b/client/js-sys/src/builtins/string.rs @@ -1,18 +1,380 @@ -use alloc::string::String; -use alloc::vec::Vec; -use core::fmt::{self, Display, Formatter}; - use js_sys_macro::js_sys; -use super::object::Object; +use super::{Array, Function, Intl, Iterable, JsIterator, Object, RegExp, RegExpMatchArray}; use crate::JsValue; -use crate::util::{PtrConst, PtrLength, PtrMut}; #[js_sys(js_sys = crate)] extern "js-sys" { - #[js_sys(js_name = "String", extends = Object)] - #[derive(Debug, Clone, PartialEq)] + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) + #[js_sys(js_name = "String")] + #[derive(Clone, PartialEq)] pub type JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode) + #[must_use] + #[js_sys(static_of = JsString, js_name = "fromCharCode", variadic)] + pub fn from_char_code(char_codes: &[u32]) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint) + #[js_sys(static_of = JsString, js_name = "fromCodePoint", variadic)] + pub fn from_code_point(code_points: &[u32]) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) + #[js_sys(static_of = JsString, variadic)] + pub fn raw(call_site: &Object, substitutions: &[JsValue]) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length) + #[must_use] + #[js_sys(getter)] + pub fn length(self: &JsString) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at) + #[must_use] + pub fn at(self: &JsString, index: f64) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt) + #[must_use] + #[js_sys(js_name = "charAt")] + pub fn char_at(self: &JsString, index: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt) + #[must_use] + #[js_sys(js_name = "charCodeAt")] + pub fn char_code_at(self: &JsString, index: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt) + #[must_use] + #[js_sys(js_name = "codePointAt")] + pub fn code_point_at(self: &JsString, index: f64) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat) + #[must_use] + pub fn concat(self: &JsString, string: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat) + #[must_use] + #[js_sys(js_name = "concat", variadic)] + pub fn concat_many( + self: &JsString, + #[js_sys(type = &[JsValue])] strings: &[JsString], + ) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith) + #[must_use] + #[js_sys(js_name = "endsWith")] + pub fn ends_with(self: &JsString, search: &str) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith) + #[must_use] + #[js_sys(js_name = "endsWith")] + pub fn ends_with_at(self: &JsString, search: &str, end: f64) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes) + #[must_use] + pub fn includes(self: &JsString, search: &str) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes) + #[must_use] + #[js_sys(js_name = "includes")] + pub fn includes_from(self: &JsString, search: &str, position: f64) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf) + #[must_use] + #[js_sys(js_name = "indexOf")] + pub fn index_of(self: &JsString, search: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf) + #[must_use] + #[js_sys(js_name = "indexOf")] + pub fn index_of_from(self: &JsString, search: &str, position: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/isWellFormed) + #[must_use] + #[js_sys(js_name = "isWellFormed")] + pub fn is_well_formed(self: &JsString) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf) + #[must_use] + #[js_sys(js_name = "lastIndexOf")] + pub fn last_index_of(self: &JsString, search: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf) + #[must_use] + #[js_sys(js_name = "lastIndexOf")] + pub fn last_index_of_from(self: &JsString, search: &str, position: f64) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) + #[must_use] + #[js_sys(js_name = "localeCompare")] + pub fn locale_compare(self: &JsString, compare: &str) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) + #[js_sys(js_name = "localeCompare")] + pub fn locale_compare_with_locales( + self: &JsString, + compare: &str, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) + #[js_sys(js_name = "localeCompare")] + pub fn locale_compare_with_locales_and_options( + self: &JsString, + compare: &str, + locales: &JsValue, + options: &Intl::CollatorOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match) + #[must_use] + #[js_sys(js_name = "match")] + pub fn match_(self: &JsString, pattern: &RegExp) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match) + #[js_sys(js_name = "match")] + pub fn match_str(self: &JsString, pattern: &str) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll) + #[js_sys(js_name = "matchAll")] + pub fn match_all( + self: &JsString, + pattern: &RegExp, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll) + #[js_sys(js_name = "matchAll")] + pub fn match_all_str( + self: &JsString, + pattern: &str, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize) + #[must_use] + pub fn normalize(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize) + #[js_sys(js_name = "normalize")] + pub fn normalize_with_form(self: &JsString, form: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd) + #[must_use] + #[js_sys(js_name = "padEnd")] + pub fn pad_end(self: &JsString, target_length: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd) + #[must_use] + #[js_sys(js_name = "padEnd")] + pub fn pad_end_with_string(self: &JsString, target_length: f64, pad_string: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart) + #[must_use] + #[js_sys(js_name = "padStart")] + pub fn pad_start(self: &JsString, target_length: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart) + #[must_use] + #[js_sys(js_name = "padStart")] + pub fn pad_start_with_string(self: &JsString, target_length: f64, pad_string: &str) + -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat) + pub fn repeat(self: &JsString, count: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) + #[must_use] + pub fn replace(self: &JsString, pattern: &str, replacement: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) + #[must_use] + #[js_sys(js_name = "replace")] + pub fn replace_regexp(self: &JsString, pattern: &RegExp, replacement: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) + #[js_sys(js_name = "replace")] + pub fn replace_with_function( + self: &JsString, + pattern: &str, + replacement: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) + #[js_sys(js_name = "replace")] + pub fn replace_regexp_with_function( + self: &JsString, + pattern: &RegExp, + replacement: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) + #[must_use] + #[js_sys(js_name = "replaceAll")] + pub fn replace_all(self: &JsString, pattern: &str, replacement: &str) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) + #[js_sys(js_name = "replaceAll")] + pub fn replace_all_regexp( + self: &JsString, + pattern: &RegExp, + replacement: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) + #[js_sys(js_name = "replaceAll")] + pub fn replace_all_with_function( + self: &JsString, + pattern: &str, + replacement: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) + #[js_sys(js_name = "replaceAll")] + pub fn replace_all_regexp_with_function( + self: &JsString, + pattern: &RegExp, + replacement: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search) + #[must_use] + pub fn search(self: &JsString, pattern: &RegExp) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search) + #[js_sys(js_name = "search")] + pub fn search_str(self: &JsString, pattern: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice) + #[must_use] + pub fn slice(self: &JsString, start: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice) + #[must_use] + #[js_sys(js_name = "slice")] + pub fn slice_range(self: &JsString, start: f64, end: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) + #[must_use] + pub fn split(self: &JsString) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) + #[must_use] + #[js_sys(js_name = "split")] + pub fn split_with_separator(self: &JsString, separator: &str) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) + #[must_use] + #[js_sys(js_name = "split")] + pub fn split_with_separator_and_limit( + self: &JsString, + separator: &str, + limit: u32, + ) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) + #[must_use] + #[js_sys(js_name = "split")] + pub fn split_with_regexp(self: &JsString, separator: &RegExp) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) + #[must_use] + #[js_sys(js_name = "split")] + pub fn split_with_regexp_and_limit(self: &JsString, separator: &RegExp, limit: u32) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) + #[must_use] + #[js_sys(js_name = "startsWith")] + pub fn starts_with(self: &JsString, search: &str) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) + #[must_use] + #[js_sys(js_name = "startsWith")] + pub fn starts_with_at(self: &JsString, search: &str, position: f64) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring) + #[must_use] + pub fn substring(self: &JsString, start: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring) + #[must_use] + #[js_sys(js_name = "substring")] + pub fn substring_range(self: &JsString, start: f64, end: f64) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase) + #[must_use] + #[js_sys(js_name = "toLocaleLowerCase")] + pub fn to_locale_lower_case(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase) + #[js_sys(js_name = "toLocaleLowerCase")] + pub fn to_locale_lower_case_with_locale( + self: &JsString, + locale: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase) + #[js_sys(js_name = "toLocaleLowerCase")] + pub fn to_locale_lower_case_with_locales( + self: &JsString, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase) + #[must_use] + #[js_sys(js_name = "toLocaleUpperCase")] + pub fn to_locale_upper_case(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase) + #[js_sys(js_name = "toLocaleUpperCase")] + pub fn to_locale_upper_case_with_locale( + self: &JsString, + locale: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase) + #[js_sys(js_name = "toLocaleUpperCase")] + pub fn to_locale_upper_case_with_locales( + self: &JsString, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) + #[must_use] + #[js_sys(js_name = "toLowerCase")] + pub fn to_lower_case(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) + #[must_use] + #[js_sys(js_name = "toUpperCase")] + pub fn to_upper_case(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toWellFormed) + #[must_use] + #[js_sys(js_name = "toWellFormed")] + pub fn to_well_formed(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim) + #[must_use] + pub fn trim(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd) + #[must_use] + #[js_sys(js_name = "trimEnd")] + pub fn trim_end(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart) + #[must_use] + #[js_sys(js_name = "trimStart")] + pub fn trim_start(self: &JsString) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf) + #[must_use] + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &JsString) -> JsString; + } impl Eq for JsString {} @@ -20,191 +382,31 @@ impl Eq for JsString {} #[js_sys(js_sys = crate)] extern "js-sys" { #[js_sys(js_name = "String")] - fn string_constructor(value: &JsValue) -> JsString; - - #[js_sys(js_embed = "string.eq")] - // SAFETY: The pointer and length must describe a valid UTF-8 byte slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrLength) -> bool; - - #[js_sys(js_embed = "string.decode")] - // SAFETY: The pointer and length must describe a valid UTF-8 byte slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString; - - #[js_sys(js_embed = "string.utf8_length")] - fn string_utf8_length(string: &JsString) -> f64; - - #[js_sys(js_embed = "string.encode")] - // SAFETY: The pointer and length must describe a valid output byte slice. - #[expect( - clippy::allow_attributes, - reason = "the macro emits an unsafe ABI call" - )] - #[allow( - clippy::undocumented_unsafe_blocks, - reason = "the safety requirement is documented on this declaration" - )] - unsafe fn string_encode(string: &JsString, array: PtrMut, len: PtrLength); -} + fn string_constructor(value: &JsValue) -> Result; -impl JsString { - #[must_use] - pub fn new(value: &JsValue) -> Self { - string_constructor(value) - } + #[js_sys(js_embed = "string.iterator")] + fn string_iterator(value: &JsString) -> JsIterator; } -impl Display for JsString { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", String::from(self)) - } -} - -impl PartialEq<&str> for JsString { - fn eq(&self, other: &&str) -> bool { - js_bindgen::embed_js!( - module = "js_sys", - name = "string.eq", - required_embeds = [("js_sys", "string.decode")], - "(string, ptr, len) => {{", - " const other = this.#jsEmbed.js_sys['string.decode'](ptr, len)", - " return string === other", - "}}", - ); - - // SAFETY: Parameters are correct. - unsafe { - string_eq( - self, - PtrConst::new(other.as_bytes()), - PtrLength::new(other.as_bytes()), - ) - } - } -} +js_bindgen::embed_js!( + module = "js_sys", + name = "string.iterator", + "value => value[Symbol.iterator]()" +); -impl PartialEq for JsString { - fn eq(&self, other: &String) -> bool { - self.eq(&other.as_str()) +impl JsString { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String) + pub fn new(value: &JsValue) -> Result { + string_constructor(value) } -} -impl From<&str> for JsString { - fn from(value: &str) -> Self { - // SAFETY: Parameters are correct. - unsafe { - string_decode( - PtrConst::new(value.as_bytes()), - PtrLength::new(value.as_bytes()), - ) - } + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Symbol.iterator) + #[must_use] + pub fn iterator(&self) -> JsIterator { + string_iterator(self) } } -impl From<&JsString> for String { - fn from(value: &JsString) -> Self { - js_bindgen::embed_js!( - module = "js_sys", - name = "string.encoder", - "new TextEncoder()", - ); - - js_bindgen::embed_js!( - module = "js_sys", - name = "string.utf8_length", - required_embeds = [("js_sys", "string.encoder")], - "(string) => this.#jsEmbed.js_sys['string.encoder'].encode(string).length", - ); - - #[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] - js_bindgen::embed_js!( - module = "js_sys", - name = "string.encode", - required_embeds = [("js_sys", "string.encoder")], - "(string, ptr, len) => {{", - " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " this.#jsEmbed.js_sys['string.encoder'].encodeInto(string, view)", - "}}", - ); - - #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] - js_bindgen::embed_js!( - module = "js_sys", - name = "string.encode", - required_embeds = [("js_sys", "string.encoder"), ("js_sys", "string.sab")], - "(string, ptr, len) => {{", - " if (this.#jsEmbed.js_sys['string.sab']) {{", - " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " this.#jsEmbed.js_sys['string.encoder'].encodeInto(string, view)", - " }} else {{", - " const bytes = this.#jsEmbed.js_sys['string.encoder'].encode(string)", - " new Uint8Array(this.#memory.buffer).set(bytes, ptr)", - " }}", - "}}", - ); - - let len = string_utf8_length(value); - #[cfg(target_arch = "wasm32")] - assert!( - len < f64::from(u32::MAX), - "found string length bigger than `usize::MAX`" - ); - #[expect( - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - reason = "in practice this is memory constrained" - )] - let len = len as usize; - - let mut vec = Vec::with_capacity(len); - // SAFETY: Parameters are correct. - unsafe { - string_encode( - value, - PtrMut::new(&mut vec), - PtrLength::from_uninit_slice(vec.spare_capacity_mut()), - ); - } - - // SAFETY: `string.encode` initializes exactly `len` bytes with valid - // UTF-8 produced by `TextEncoder`. - unsafe { - vec.set_len(len); - Self::from_utf8_unchecked(vec) - } - } +impl Iterable for JsString { + type Item = Self; } - -#[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] -js_bindgen::embed_js!( - module = "js_sys", - name = "string.sab", - "(() => {{", - " if (this.#memory.buffer instanceof ArrayBuffer)", - " return true", - "", - " try {{", - " const view = new Uint8Array(this.#memory.buffer, 0, 0)", - " new TextDecoder().decode(view)", - " new TextEncoder().encodeInto('', view)", - " return true", - " }} catch {{", - " return false", - " }}", - "}})()", -); diff --git a/client/js-sys/src/builtins/symbol.rs b/client/js-sys/src/builtins/symbol.rs new file mode 100644 index 00000000..f7353858 --- /dev/null +++ b/client/js-sys/src/builtins/symbol.rs @@ -0,0 +1,142 @@ +use crate::{JsString, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) + #[derive(Clone, Debug, PartialEq)] + pub type Symbol; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_name = "Symbol")] + fn symbol() -> Symbol; + + #[js_sys(js_name = "Symbol")] + fn symbol_with_description(description: &str) -> Symbol; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncDispose) + #[must_use] + #[js_sys(static_of = Symbol, getter = "asyncDispose")] + pub fn async_dispose() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator) + #[must_use] + #[js_sys(static_of = Symbol, getter = "asyncIterator")] + pub fn async_iterator() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/dispose) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn dispose() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/hasInstance) + #[must_use] + #[js_sys(static_of = Symbol, getter = "hasInstance")] + pub fn has_instance() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable) + #[must_use] + #[js_sys(static_of = Symbol, getter = "isConcatSpreadable")] + pub fn is_concat_spreadable() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn iterator() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/match) + #[must_use] + #[js_sys(static_of = Symbol, getter = "match")] + pub fn match_() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/matchAll) + #[must_use] + #[js_sys(static_of = Symbol, getter = "matchAll")] + pub fn match_all() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/replace) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn replace() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/search) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn search() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/species) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn species() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/split) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn split() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive) + #[must_use] + #[js_sys(static_of = Symbol, getter = "toPrimitive")] + pub fn to_primitive() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag) + #[must_use] + #[js_sys(static_of = Symbol, getter = "toStringTag")] + pub fn to_string_tag() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables) + #[must_use] + #[js_sys(static_of = Symbol, getter)] + pub fn unscopables() -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for) + #[must_use] + #[js_sys(static_of = Symbol, js_name = "for")] + pub fn for_(key: &str) -> Symbol; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/keyFor) + #[must_use] + #[js_sys(static_of = Symbol, js_name = "keyFor")] + pub fn key_for(symbol: &Symbol) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/description) + #[must_use] + #[js_sys(getter)] + pub fn description(self: &Symbol) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_js_string(self: &Symbol) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/valueOf) + #[must_use] + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Symbol) -> Symbol; +} + +impl Eq for Symbol {} + +impl Symbol { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol) + #[must_use] + pub fn new() -> Self { + symbol() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol) + #[must_use] + pub fn new_with_description(description: &str) -> Self { + symbol_with_description(description) + } +} + +impl Default for Symbol { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/temporal/duration.rs b/client/js-sys/src/builtins/temporal/duration.rs new file mode 100644 index 00000000..e19b26fd --- /dev/null +++ b/client/js-sys/src/builtins/temporal/duration.rs @@ -0,0 +1,268 @@ +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) + #[js_sys(js_name = "Duration", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Duration; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> Duration; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years(years: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years_months(years: f64, months: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years_months_weeks( + years: f64, + months: f64, + weeks: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years_months_weeks_days( + years: f64, + months: f64, + weeks: f64, + days: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years_months_weeks_days_hours( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years_months_weeks_days_hours_minutes( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + minutes: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[js_sys(constructor)] + pub fn new_with_years_months_weeks_days_hours_minutes_seconds( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[expect( + clippy::too_many_arguments, + reason = "matches the JavaScript constructor" + )] + #[js_sys(constructor)] + pub fn new_with_years_months_weeks_days_hours_minutes_seconds_milliseconds( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[expect( + clippy::too_many_arguments, + reason = "matches the JavaScript constructor" + )] + #[js_sys(constructor)] + pub fn new_with_years_months_weeks_days_hours_minutes_seconds_milliseconds_microseconds( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + microseconds: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) + #[expect( + clippy::too_many_arguments, + reason = "matches the JavaScript constructor" + )] + #[js_sys(constructor)] + pub fn new_with_values( + years: f64, + months: f64, + weeks: f64, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + milliseconds: f64, + microseconds: f64, + nanoseconds: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/from) + #[js_sys(static_of = Duration)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/compare) + #[js_sys(static_of = Duration)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/compare) + #[js_sys(static_of = Duration, js_name = "compare")] + pub fn compare_with_options( + one: &JsValue, + two: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/years) + #[must_use] + #[js_sys(getter)] + pub fn years(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/months) + #[must_use] + #[js_sys(getter)] + pub fn months(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/weeks) + #[must_use] + #[js_sys(getter)] + pub fn weeks(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/days) + #[must_use] + #[js_sys(getter)] + pub fn days(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/hours) + #[must_use] + #[js_sys(getter)] + pub fn hours(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/minutes) + #[must_use] + #[js_sys(getter)] + pub fn minutes(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/seconds) + #[must_use] + #[js_sys(getter)] + pub fn seconds(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/milliseconds) + #[must_use] + #[js_sys(getter)] + pub fn milliseconds(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/microseconds) + #[must_use] + #[js_sys(getter)] + pub fn microseconds(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/nanoseconds) + #[must_use] + #[js_sys(getter)] + pub fn nanoseconds(self: &Duration) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/sign) + #[must_use] + #[js_sys(getter)] + pub fn sign(self: &Duration) -> i32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/blank) + #[must_use] + #[js_sys(getter)] + pub fn blank(self: &Duration) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/with) + pub fn with(self: &Duration, duration_like: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/negated) + #[must_use] + pub fn negated(self: &Duration) -> Duration; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/abs) + #[must_use] + pub fn abs(self: &Duration) -> Duration; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/add) + pub fn add(self: &Duration, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/subtract) + pub fn subtract(self: &Duration, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/round) + pub fn round(self: &Duration, round_to: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/total) + pub fn total(self: &Duration, total_of: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Duration) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options(self: &Duration, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &Duration) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &Duration, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &Duration, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &Duration) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Duration) -> Result; +} + +impl Default for Duration { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/temporal/instant.rs b/client/js-sys/src/builtins/temporal/instant.rs new file mode 100644 index 00000000..f93b12fb --- /dev/null +++ b/client/js-sys/src/builtins/temporal/instant.rs @@ -0,0 +1,119 @@ +use super::duration::Duration; +use super::zoned_date_time::ZonedDateTime; +use crate::{BigInt, JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) + #[js_sys(js_name = "Instant", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Instant; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/Instant) + #[js_sys(constructor)] + pub fn new(epoch_nanoseconds: &BigInt) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/from) + #[js_sys(static_of = Instant)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/fromEpochMilliseconds) + #[js_sys(static_of = Instant, js_name = "fromEpochMilliseconds")] + pub fn from_epoch_milliseconds(epoch_milliseconds: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/fromEpochNanoseconds) + #[js_sys(static_of = Instant, js_name = "fromEpochNanoseconds")] + pub fn from_epoch_nanoseconds(epoch_nanoseconds: &BigInt) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/compare) + #[js_sys(static_of = Instant)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/epochMilliseconds) + #[must_use] + #[js_sys(getter = "epochMilliseconds")] + pub fn epoch_milliseconds(self: &Instant) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/epochNanoseconds) + #[must_use] + #[js_sys(getter = "epochNanoseconds")] + pub fn epoch_nanoseconds(self: &Instant) -> BigInt; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/add) + pub fn add(self: &Instant, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/subtract) + pub fn subtract(self: &Instant, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/until) + pub fn until(self: &Instant, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/until) + #[js_sys(js_name = "until")] + pub fn until_with_options( + self: &Instant, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/since) + pub fn since(self: &Instant, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/since) + #[js_sys(js_name = "since")] + pub fn since_with_options( + self: &Instant, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/round) + pub fn round(self: &Instant, round_to: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/equals) + pub fn equals(self: &Instant, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &Instant) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options(self: &Instant, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &Instant) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &Instant, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &Instant, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &Instant) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toZonedDateTimeISO) + #[js_sys(js_name = "toZonedDateTimeISO")] + pub fn to_zoned_date_time_iso( + self: &Instant, + time_zone: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Instant) -> Result; +} diff --git a/client/js-sys/src/builtins/temporal/mod.rs b/client/js-sys/src/builtins/temporal/mod.rs new file mode 100644 index 00000000..ab64d01a --- /dev/null +++ b/client/js-sys/src/builtins/temporal/mod.rs @@ -0,0 +1,23 @@ +mod duration; +mod instant; +mod now; +mod plain_date; +mod plain_date_time; +mod plain_month_day; +mod plain_time; +mod plain_year_month; +mod zoned_date_time; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod Temporal { + pub use super::duration::Duration; + pub use super::instant::Instant; + pub use super::now::Now; + pub use super::plain_date::PlainDate; + pub use super::plain_date_time::PlainDateTime; + pub use super::plain_month_day::PlainMonthDay; + pub use super::plain_time::PlainTime; + pub use super::plain_year_month::PlainYearMonth; + pub use super::zoned_date_time::ZonedDateTime; +} diff --git a/client/js-sys/src/builtins/temporal/now.rs b/client/js-sys/src/builtins/temporal/now.rs new file mode 100644 index 00000000..63662413 --- /dev/null +++ b/client/js-sys/src/builtins/temporal/now.rs @@ -0,0 +1,62 @@ +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now) +#[expect(non_snake_case, reason = "matches the JavaScript namespace name")] +pub mod Now { + use super::super::instant::Instant; + use super::super::plain_date::PlainDate; + use super::super::plain_date_time::PlainDateTime; + use super::super::plain_time::PlainTime; + use super::super::zoned_date_time::ZonedDateTime; + use crate::{JsString, JsValue, js_sys}; + + #[js_sys(js_sys = crate, namespace = "Temporal.Now")] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/instant) + #[must_use] + pub fn instant() -> Instant; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainDateISO) + #[must_use] + #[js_sys(js_name = "plainDateISO")] + pub fn plain_date_iso() -> PlainDate; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainDateISO) + #[js_sys(js_name = "plainDateISO")] + pub fn plain_date_iso_with_time_zone(time_zone: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainDateTimeISO) + #[must_use] + #[js_sys(js_name = "plainDateTimeISO")] + pub fn plain_date_time_iso() -> PlainDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainDateTimeISO) + #[js_sys(js_name = "plainDateTimeISO")] + pub fn plain_date_time_iso_with_time_zone( + time_zone: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainTimeISO) + #[must_use] + #[js_sys(js_name = "plainTimeISO")] + pub fn plain_time_iso() -> PlainTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainTimeISO) + #[js_sys(js_name = "plainTimeISO")] + pub fn plain_time_iso_with_time_zone(time_zone: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/timeZoneId) + #[must_use] + #[js_sys(js_name = "timeZoneId")] + pub fn time_zone_id() -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/zonedDateTimeISO) + #[must_use] + #[js_sys(js_name = "zonedDateTimeISO")] + pub fn zoned_date_time_iso() -> ZonedDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/zonedDateTimeISO) + #[js_sys(js_name = "zonedDateTimeISO")] + pub fn zoned_date_time_iso_with_time_zone( + time_zone: &JsValue, + ) -> Result; + } +} diff --git a/client/js-sys/src/builtins/temporal/plain_date.rs b/client/js-sys/src/builtins/temporal/plain_date.rs new file mode 100644 index 00000000..35e9a4eb --- /dev/null +++ b/client/js-sys/src/builtins/temporal/plain_date.rs @@ -0,0 +1,245 @@ +use super::duration::Duration; +use super::plain_date_time::PlainDateTime; +use super::plain_month_day::PlainMonthDay; +use super::plain_year_month::PlainYearMonth; +use super::zoned_date_time::ZonedDateTime; +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate) + #[js_sys(js_name = "PlainDate", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PlainDate; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/PlainDate) + #[js_sys(constructor)] + pub fn new(iso_year: i32, iso_month: u32, iso_day: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/PlainDate) + #[js_sys(constructor)] + pub fn new_with_calendar( + iso_year: i32, + iso_month: u32, + iso_day: u32, + calendar: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/from) + #[js_sys(static_of = PlainDate)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/from) + #[js_sys(static_of = PlainDate, js_name = "from")] + pub fn from_with_options(item: &JsValue, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/compare) + #[js_sys(static_of = PlainDate)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/calendarId) + #[must_use] + #[js_sys(getter = "calendarId")] + pub fn calendar_id(self: &PlainDate) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/era) + #[must_use] + #[js_sys(getter)] + pub fn era(self: &PlainDate) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/eraYear) + #[must_use] + #[js_sys(getter = "eraYear")] + pub fn era_year(self: &PlainDate) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/year) + #[must_use] + #[js_sys(getter)] + pub fn year(self: &PlainDate) -> i32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/month) + #[must_use] + #[js_sys(getter)] + pub fn month(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/monthCode) + #[must_use] + #[js_sys(getter = "monthCode")] + pub fn month_code(self: &PlainDate) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/day) + #[must_use] + #[js_sys(getter)] + pub fn day(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/dayOfWeek) + #[must_use] + #[js_sys(getter = "dayOfWeek")] + pub fn day_of_week(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/dayOfYear) + #[must_use] + #[js_sys(getter = "dayOfYear")] + pub fn day_of_year(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/weekOfYear) + #[must_use] + #[js_sys(getter = "weekOfYear")] + pub fn week_of_year(self: &PlainDate) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/yearOfWeek) + #[must_use] + #[js_sys(getter = "yearOfWeek")] + pub fn year_of_week(self: &PlainDate) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/daysInWeek) + #[must_use] + #[js_sys(getter = "daysInWeek")] + pub fn days_in_week(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/daysInMonth) + #[must_use] + #[js_sys(getter = "daysInMonth")] + pub fn days_in_month(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/daysInYear) + #[must_use] + #[js_sys(getter = "daysInYear")] + pub fn days_in_year(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/monthsInYear) + #[must_use] + #[js_sys(getter = "monthsInYear")] + pub fn months_in_year(self: &PlainDate) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/inLeapYear) + #[must_use] + #[js_sys(getter = "inLeapYear")] + pub fn in_leap_year(self: &PlainDate) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/add) + pub fn add(self: &PlainDate, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/add) + #[js_sys(js_name = "add")] + pub fn add_with_options( + self: &PlainDate, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/subtract) + pub fn subtract(self: &PlainDate, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/subtract) + #[js_sys(js_name = "subtract")] + pub fn subtract_with_options( + self: &PlainDate, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/with) + pub fn with(self: &PlainDate, date_like: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/with) + #[js_sys(js_name = "with")] + pub fn with_options( + self: &PlainDate, + date_like: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/withCalendar) + #[js_sys(js_name = "withCalendar")] + pub fn with_calendar(self: &PlainDate, calendar: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/until) + pub fn until(self: &PlainDate, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/until) + #[js_sys(js_name = "until")] + pub fn until_with_options( + self: &PlainDate, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/since) + pub fn since(self: &PlainDate, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/since) + #[js_sys(js_name = "since")] + pub fn since_with_options( + self: &PlainDate, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/equals) + pub fn equals(self: &PlainDate, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toPlainDateTime) + #[must_use] + #[js_sys(js_name = "toPlainDateTime")] + pub fn to_plain_date_time(self: &PlainDate) -> PlainDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toPlainDateTime) + #[js_sys(js_name = "toPlainDateTime")] + pub fn to_plain_date_time_with_time( + self: &PlainDate, + time: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toZonedDateTime) + #[js_sys(js_name = "toZonedDateTime")] + pub fn to_zoned_date_time(self: &PlainDate, item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toPlainYearMonth) + #[must_use] + #[js_sys(js_name = "toPlainYearMonth")] + pub fn to_plain_year_month(self: &PlainDate) -> PlainYearMonth; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toPlainMonthDay) + #[must_use] + #[js_sys(js_name = "toPlainMonthDay")] + pub fn to_plain_month_day(self: &PlainDate) -> PlainMonthDay; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &PlainDate) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options(self: &PlainDate, options: &JsValue) + -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &PlainDate) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &PlainDate, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &PlainDate, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &PlainDate) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &PlainDate) -> Result; +} diff --git a/client/js-sys/src/builtins/temporal/plain_date_time.rs b/client/js-sys/src/builtins/temporal/plain_date_time.rs new file mode 100644 index 00000000..c46e12a5 --- /dev/null +++ b/client/js-sys/src/builtins/temporal/plain_date_time.rs @@ -0,0 +1,380 @@ +use super::duration::Duration; +use super::plain_date::PlainDate; +use super::plain_time::PlainTime; +use super::zoned_date_time::ZonedDateTime; +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime) + #[js_sys(js_name = "PlainDateTime", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PlainDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[js_sys(constructor)] + pub fn new(iso_year: i32, iso_month: u32, iso_day: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + minute: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute_second( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + minute: u32, + second: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute_second_millisecond( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[expect( + clippy::too_many_arguments, + reason = "matches the JavaScript constructor" + )] + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute_second_millisecond_microsecond( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + microsecond: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[expect( + clippy::too_many_arguments, + reason = "matches the JavaScript constructor" + )] + #[js_sys(constructor)] + pub fn new_with_year_month_day_hour_minute_second_millisecond_microsecond_nanosecond( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + microsecond: u32, + nanosecond: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) + #[expect( + clippy::too_many_arguments, + reason = "matches the JavaScript constructor" + )] + #[js_sys(constructor)] + pub fn new_with_values( + iso_year: i32, + iso_month: u32, + iso_day: u32, + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + microsecond: u32, + nanosecond: u32, + calendar: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/from) + #[js_sys(static_of = PlainDateTime)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/from) + #[js_sys(static_of = PlainDateTime, js_name = "from")] + pub fn from_with_options(item: &JsValue, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/compare) + #[js_sys(static_of = PlainDateTime)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/calendarId) + #[must_use] + #[js_sys(getter = "calendarId")] + pub fn calendar_id(self: &PlainDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/era) + #[must_use] + #[js_sys(getter)] + pub fn era(self: &PlainDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/eraYear) + #[must_use] + #[js_sys(getter = "eraYear")] + pub fn era_year(self: &PlainDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/year) + #[must_use] + #[js_sys(getter)] + pub fn year(self: &PlainDateTime) -> i32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/month) + #[must_use] + #[js_sys(getter)] + pub fn month(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/monthCode) + #[must_use] + #[js_sys(getter = "monthCode")] + pub fn month_code(self: &PlainDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/day) + #[must_use] + #[js_sys(getter)] + pub fn day(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/hour) + #[must_use] + #[js_sys(getter)] + pub fn hour(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/minute) + #[must_use] + #[js_sys(getter)] + pub fn minute(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/second) + #[must_use] + #[js_sys(getter)] + pub fn second(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/millisecond) + #[must_use] + #[js_sys(getter)] + pub fn millisecond(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/microsecond) + #[must_use] + #[js_sys(getter)] + pub fn microsecond(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/nanosecond) + #[must_use] + #[js_sys(getter)] + pub fn nanosecond(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/dayOfWeek) + #[must_use] + #[js_sys(getter = "dayOfWeek")] + pub fn day_of_week(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/dayOfYear) + #[must_use] + #[js_sys(getter = "dayOfYear")] + pub fn day_of_year(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/weekOfYear) + #[must_use] + #[js_sys(getter = "weekOfYear")] + pub fn week_of_year(self: &PlainDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/yearOfWeek) + #[must_use] + #[js_sys(getter = "yearOfWeek")] + pub fn year_of_week(self: &PlainDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/daysInWeek) + #[must_use] + #[js_sys(getter = "daysInWeek")] + pub fn days_in_week(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/daysInMonth) + #[must_use] + #[js_sys(getter = "daysInMonth")] + pub fn days_in_month(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/daysInYear) + #[must_use] + #[js_sys(getter = "daysInYear")] + pub fn days_in_year(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/monthsInYear) + #[must_use] + #[js_sys(getter = "monthsInYear")] + pub fn months_in_year(self: &PlainDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/inLeapYear) + #[must_use] + #[js_sys(getter = "inLeapYear")] + pub fn in_leap_year(self: &PlainDateTime) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/with) + pub fn with(self: &PlainDateTime, date_time_like: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/with) + #[js_sys(js_name = "with")] + pub fn with_options( + self: &PlainDateTime, + date_time_like: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/withPlainTime) + #[must_use] + #[js_sys(js_name = "withPlainTime")] + pub fn with_plain_time(self: &PlainDateTime) -> PlainDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/withPlainTime) + #[js_sys(js_name = "withPlainTime")] + pub fn with_plain_time_value( + self: &PlainDateTime, + plain_time: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/withCalendar) + #[js_sys(js_name = "withCalendar")] + pub fn with_calendar( + self: &PlainDateTime, + calendar: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/add) + pub fn add(self: &PlainDateTime, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/add) + #[js_sys(js_name = "add")] + pub fn add_with_options( + self: &PlainDateTime, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/subtract) + pub fn subtract(self: &PlainDateTime, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/subtract) + #[js_sys(js_name = "subtract")] + pub fn subtract_with_options( + self: &PlainDateTime, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/until) + pub fn until(self: &PlainDateTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/until) + #[js_sys(js_name = "until")] + pub fn until_with_options( + self: &PlainDateTime, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/since) + pub fn since(self: &PlainDateTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/since) + #[js_sys(js_name = "since")] + pub fn since_with_options( + self: &PlainDateTime, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/round) + pub fn round(self: &PlainDateTime, round_to: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/equals) + pub fn equals(self: &PlainDateTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toZonedDateTime) + #[js_sys(js_name = "toZonedDateTime")] + pub fn to_zoned_date_time( + self: &PlainDateTime, + time_zone: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toZonedDateTime) + #[js_sys(js_name = "toZonedDateTime")] + pub fn to_zoned_date_time_with_options( + self: &PlainDateTime, + time_zone: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toPlainDate) + #[must_use] + #[js_sys(js_name = "toPlainDate")] + pub fn to_plain_date(self: &PlainDateTime) -> PlainDate; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toPlainTime) + #[must_use] + #[js_sys(js_name = "toPlainTime")] + pub fn to_plain_time(self: &PlainDateTime) -> PlainTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &PlainDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options( + self: &PlainDateTime, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &PlainDateTime) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &PlainDateTime, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &PlainDateTime, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &PlainDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &PlainDateTime) -> Result; +} diff --git a/client/js-sys/src/builtins/temporal/plain_month_day.rs b/client/js-sys/src/builtins/temporal/plain_month_day.rs new file mode 100644 index 00000000..824dec45 --- /dev/null +++ b/client/js-sys/src/builtins/temporal/plain_month_day.rs @@ -0,0 +1,112 @@ +use super::plain_date::PlainDate; +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay) + #[js_sys(js_name = "PlainMonthDay", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PlainMonthDay; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/PlainMonthDay) + #[js_sys(constructor)] + pub fn new(iso_month: u32, iso_day: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/PlainMonthDay) + #[js_sys(constructor)] + pub fn new_with_calendar( + iso_month: u32, + iso_day: u32, + calendar: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/PlainMonthDay) + #[js_sys(constructor)] + pub fn new_with_calendar_and_reference_year( + iso_month: u32, + iso_day: u32, + calendar: &str, + reference_iso_year: i32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/from) + #[js_sys(static_of = PlainMonthDay)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/from) + #[js_sys(static_of = PlainMonthDay, js_name = "from")] + pub fn from_with_options(item: &JsValue, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/calendarId) + #[must_use] + #[js_sys(getter = "calendarId")] + pub fn calendar_id(self: &PlainMonthDay) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/monthCode) + #[must_use] + #[js_sys(getter = "monthCode")] + pub fn month_code(self: &PlainMonthDay) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/day) + #[must_use] + #[js_sys(getter)] + pub fn day(self: &PlainMonthDay) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/with) + pub fn with(self: &PlainMonthDay, month_day_like: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/with) + #[js_sys(js_name = "with")] + pub fn with_options( + self: &PlainMonthDay, + month_day_like: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/equals) + pub fn equals(self: &PlainMonthDay, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toPlainDate) + #[js_sys(js_name = "toPlainDate")] + pub fn to_plain_date(self: &PlainMonthDay, item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &PlainMonthDay) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options( + self: &PlainMonthDay, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &PlainMonthDay) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &PlainMonthDay, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &PlainMonthDay, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &PlainMonthDay) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &PlainMonthDay) -> Result; +} diff --git a/client/js-sys/src/builtins/temporal/plain_time.rs b/client/js-sys/src/builtins/temporal/plain_time.rs new file mode 100644 index 00000000..2dcfed2b --- /dev/null +++ b/client/js-sys/src/builtins/temporal/plain_time.rs @@ -0,0 +1,192 @@ +use super::duration::Duration; +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime) + #[js_sys(js_name = "PlainTime", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PlainTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> PlainTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[js_sys(constructor)] + pub fn new_with_hour(hour: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[js_sys(constructor)] + pub fn new_with_hour_minute(hour: u32, minute: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[js_sys(constructor)] + pub fn new_with_hour_minute_second( + hour: u32, + minute: u32, + second: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[js_sys(constructor)] + pub fn new_with_hour_minute_second_millisecond( + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[js_sys(constructor)] + pub fn new_with_hour_minute_second_millisecond_microsecond( + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + microsecond: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) + #[js_sys(constructor)] + pub fn new_with_values( + hour: u32, + minute: u32, + second: u32, + millisecond: u32, + microsecond: u32, + nanosecond: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/from) + #[js_sys(static_of = PlainTime)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/from) + #[js_sys(static_of = PlainTime, js_name = "from")] + pub fn from_with_options(item: &JsValue, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/compare) + #[js_sys(static_of = PlainTime)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/hour) + #[must_use] + #[js_sys(getter)] + pub fn hour(self: &PlainTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/minute) + #[must_use] + #[js_sys(getter)] + pub fn minute(self: &PlainTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/second) + #[must_use] + #[js_sys(getter)] + pub fn second(self: &PlainTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/millisecond) + #[must_use] + #[js_sys(getter)] + pub fn millisecond(self: &PlainTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/microsecond) + #[must_use] + #[js_sys(getter)] + pub fn microsecond(self: &PlainTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/nanosecond) + #[must_use] + #[js_sys(getter)] + pub fn nanosecond(self: &PlainTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/add) + pub fn add(self: &PlainTime, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/subtract) + pub fn subtract(self: &PlainTime, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/with) + pub fn with(self: &PlainTime, time_like: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/with) + #[js_sys(js_name = "with")] + pub fn with_options( + self: &PlainTime, + time_like: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/until) + pub fn until(self: &PlainTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/until) + #[js_sys(js_name = "until")] + pub fn until_with_options( + self: &PlainTime, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/since) + pub fn since(self: &PlainTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/since) + #[js_sys(js_name = "since")] + pub fn since_with_options( + self: &PlainTime, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/equals) + pub fn equals(self: &PlainTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/round) + pub fn round(self: &PlainTime, round_to: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &PlainTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options(self: &PlainTime, options: &JsValue) + -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &PlainTime) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &PlainTime, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &PlainTime, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &PlainTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &PlainTime) -> Result; +} + +impl Default for PlainTime { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/temporal/plain_year_month.rs b/client/js-sys/src/builtins/temporal/plain_year_month.rs new file mode 100644 index 00000000..81201130 --- /dev/null +++ b/client/js-sys/src/builtins/temporal/plain_year_month.rs @@ -0,0 +1,199 @@ +use super::duration::Duration; +use super::plain_date::PlainDate; +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth) + #[js_sys(js_name = "PlainYearMonth", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type PlainYearMonth; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/PlainYearMonth) + #[js_sys(constructor)] + pub fn new(iso_year: i32, iso_month: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/PlainYearMonth) + #[js_sys(constructor)] + pub fn new_with_calendar( + iso_year: i32, + iso_month: u32, + calendar: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/PlainYearMonth) + #[js_sys(constructor)] + pub fn new_with_calendar_and_reference_day( + iso_year: i32, + iso_month: u32, + calendar: &str, + reference_iso_day: u32, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/from) + #[js_sys(static_of = PlainYearMonth)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/from) + #[js_sys(static_of = PlainYearMonth, js_name = "from")] + pub fn from_with_options(item: &JsValue, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/compare) + #[js_sys(static_of = PlainYearMonth)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/calendarId) + #[must_use] + #[js_sys(getter = "calendarId")] + pub fn calendar_id(self: &PlainYearMonth) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/era) + #[must_use] + #[js_sys(getter)] + pub fn era(self: &PlainYearMonth) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/eraYear) + #[must_use] + #[js_sys(getter = "eraYear")] + pub fn era_year(self: &PlainYearMonth) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/year) + #[must_use] + #[js_sys(getter)] + pub fn year(self: &PlainYearMonth) -> i32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/month) + #[must_use] + #[js_sys(getter)] + pub fn month(self: &PlainYearMonth) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/monthCode) + #[must_use] + #[js_sys(getter = "monthCode")] + pub fn month_code(self: &PlainYearMonth) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/daysInYear) + #[must_use] + #[js_sys(getter = "daysInYear")] + pub fn days_in_year(self: &PlainYearMonth) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/daysInMonth) + #[must_use] + #[js_sys(getter = "daysInMonth")] + pub fn days_in_month(self: &PlainYearMonth) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/monthsInYear) + #[must_use] + #[js_sys(getter = "monthsInYear")] + pub fn months_in_year(self: &PlainYearMonth) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/inLeapYear) + #[must_use] + #[js_sys(getter = "inLeapYear")] + pub fn in_leap_year(self: &PlainYearMonth) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/with) + pub fn with( + self: &PlainYearMonth, + year_month_like: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/with) + #[js_sys(js_name = "with")] + pub fn with_options( + self: &PlainYearMonth, + year_month_like: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/add) + pub fn add(self: &PlainYearMonth, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/add) + #[js_sys(js_name = "add")] + pub fn add_with_options( + self: &PlainYearMonth, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/subtract) + pub fn subtract(self: &PlainYearMonth, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/subtract) + #[js_sys(js_name = "subtract")] + pub fn subtract_with_options( + self: &PlainYearMonth, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/until) + pub fn until(self: &PlainYearMonth, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/until) + #[js_sys(js_name = "until")] + pub fn until_with_options( + self: &PlainYearMonth, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/since) + pub fn since(self: &PlainYearMonth, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/since) + #[js_sys(js_name = "since")] + pub fn since_with_options( + self: &PlainYearMonth, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/equals) + pub fn equals(self: &PlainYearMonth, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toPlainDate) + #[js_sys(js_name = "toPlainDate")] + pub fn to_plain_date(self: &PlainYearMonth, item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &PlainYearMonth) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options( + self: &PlainYearMonth, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &PlainYearMonth) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &PlainYearMonth, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &PlainYearMonth, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &PlainYearMonth) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &PlainYearMonth) -> Result; +} diff --git a/client/js-sys/src/builtins/temporal/zoned_date_time.rs b/client/js-sys/src/builtins/temporal/zoned_date_time.rs new file mode 100644 index 00000000..e63a6394 --- /dev/null +++ b/client/js-sys/src/builtins/temporal/zoned_date_time.rs @@ -0,0 +1,337 @@ +use super::duration::Duration; +use super::instant::Instant; +use super::plain_date::PlainDate; +use super::plain_date_time::PlainDateTime; +use super::plain_time::PlainTime; +use crate::{BigInt, JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "Temporal")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) + #[js_sys(js_name = "ZonedDateTime", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ZonedDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/ZonedDateTime) + #[js_sys(constructor)] + pub fn new(epoch_nanoseconds: &BigInt, time_zone: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/ZonedDateTime) + #[js_sys(constructor)] + pub fn new_with_calendar( + epoch_nanoseconds: &BigInt, + time_zone: &str, + calendar: &str, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/from) + #[js_sys(static_of = ZonedDateTime)] + pub fn from(item: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/from) + #[js_sys(static_of = ZonedDateTime, js_name = "from")] + pub fn from_with_options(item: &JsValue, options: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/compare) + #[js_sys(static_of = ZonedDateTime)] + pub fn compare(one: &JsValue, two: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/calendarId) + #[must_use] + #[js_sys(getter = "calendarId")] + pub fn calendar_id(self: &ZonedDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/timeZoneId) + #[must_use] + #[js_sys(getter = "timeZoneId")] + pub fn time_zone_id(self: &ZonedDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/era) + #[must_use] + #[js_sys(getter)] + pub fn era(self: &ZonedDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/eraYear) + #[must_use] + #[js_sys(getter = "eraYear")] + pub fn era_year(self: &ZonedDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/year) + #[must_use] + #[js_sys(getter)] + pub fn year(self: &ZonedDateTime) -> i32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/month) + #[must_use] + #[js_sys(getter)] + pub fn month(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/monthCode) + #[must_use] + #[js_sys(getter = "monthCode")] + pub fn month_code(self: &ZonedDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/day) + #[must_use] + #[js_sys(getter)] + pub fn day(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/hour) + #[must_use] + #[js_sys(getter)] + pub fn hour(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/minute) + #[must_use] + #[js_sys(getter)] + pub fn minute(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/second) + #[must_use] + #[js_sys(getter)] + pub fn second(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/millisecond) + #[must_use] + #[js_sys(getter)] + pub fn millisecond(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/microsecond) + #[must_use] + #[js_sys(getter)] + pub fn microsecond(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/nanosecond) + #[must_use] + #[js_sys(getter)] + pub fn nanosecond(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/epochMilliseconds) + #[must_use] + #[js_sys(getter = "epochMilliseconds")] + pub fn epoch_milliseconds(self: &ZonedDateTime) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/epochNanoseconds) + #[must_use] + #[js_sys(getter = "epochNanoseconds")] + pub fn epoch_nanoseconds(self: &ZonedDateTime) -> BigInt; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/dayOfWeek) + #[must_use] + #[js_sys(getter = "dayOfWeek")] + pub fn day_of_week(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/dayOfYear) + #[must_use] + #[js_sys(getter = "dayOfYear")] + pub fn day_of_year(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/weekOfYear) + #[must_use] + #[js_sys(getter = "weekOfYear")] + pub fn week_of_year(self: &ZonedDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/yearOfWeek) + #[must_use] + #[js_sys(getter = "yearOfWeek")] + pub fn year_of_week(self: &ZonedDateTime) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/hoursInDay) + #[js_sys(getter = "hoursInDay")] + pub fn hours_in_day(self: &ZonedDateTime) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/daysInWeek) + #[must_use] + #[js_sys(getter = "daysInWeek")] + pub fn days_in_week(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/daysInMonth) + #[must_use] + #[js_sys(getter = "daysInMonth")] + pub fn days_in_month(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/daysInYear) + #[must_use] + #[js_sys(getter = "daysInYear")] + pub fn days_in_year(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/monthsInYear) + #[must_use] + #[js_sys(getter = "monthsInYear")] + pub fn months_in_year(self: &ZonedDateTime) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/inLeapYear) + #[must_use] + #[js_sys(getter = "inLeapYear")] + pub fn in_leap_year(self: &ZonedDateTime) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/offsetNanoseconds) + #[must_use] + #[js_sys(getter = "offsetNanoseconds")] + pub fn offset_nanoseconds(self: &ZonedDateTime) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/offset) + #[must_use] + #[js_sys(getter)] + pub fn offset(self: &ZonedDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/with) + pub fn with( + self: &ZonedDateTime, + zoned_date_time_like: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/with) + #[js_sys(js_name = "with")] + pub fn with_options( + self: &ZonedDateTime, + zoned_date_time_like: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/withPlainTime) + #[js_sys(js_name = "withPlainTime")] + pub fn with_plain_time(self: &ZonedDateTime) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/withPlainTime) + #[js_sys(js_name = "withPlainTime")] + pub fn with_plain_time_value( + self: &ZonedDateTime, + plain_time: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/withTimeZone) + #[js_sys(js_name = "withTimeZone")] + pub fn with_time_zone( + self: &ZonedDateTime, + time_zone: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/withCalendar) + #[js_sys(js_name = "withCalendar")] + pub fn with_calendar( + self: &ZonedDateTime, + calendar: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/add) + pub fn add(self: &ZonedDateTime, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/add) + #[js_sys(js_name = "add")] + pub fn add_with_options( + self: &ZonedDateTime, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/subtract) + pub fn subtract(self: &ZonedDateTime, duration: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/subtract) + #[js_sys(js_name = "subtract")] + pub fn subtract_with_options( + self: &ZonedDateTime, + duration: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/until) + pub fn until(self: &ZonedDateTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/until) + #[js_sys(js_name = "until")] + pub fn until_with_options( + self: &ZonedDateTime, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/since) + pub fn since(self: &ZonedDateTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/since) + #[js_sys(js_name = "since")] + pub fn since_with_options( + self: &ZonedDateTime, + other: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/round) + pub fn round(self: &ZonedDateTime, round_to: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/equals) + pub fn equals(self: &ZonedDateTime, other: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/startOfDay) + #[js_sys(js_name = "startOfDay")] + pub fn start_of_day(self: &ZonedDateTime) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/getTimeZoneTransition) + #[js_sys(js_name = "getTimeZoneTransition")] + pub fn get_time_zone_transition( + self: &ZonedDateTime, + direction: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toInstant) + #[must_use] + #[js_sys(js_name = "toInstant")] + pub fn to_instant(self: &ZonedDateTime) -> Instant; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toPlainDate) + #[must_use] + #[js_sys(js_name = "toPlainDate")] + pub fn to_plain_date(self: &ZonedDateTime) -> PlainDate; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toPlainTime) + #[must_use] + #[js_sys(js_name = "toPlainTime")] + pub fn to_plain_time(self: &ZonedDateTime) -> PlainTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toPlainDateTime) + #[must_use] + #[js_sys(js_name = "toPlainDateTime")] + pub fn to_plain_date_time(self: &ZonedDateTime) -> PlainDateTime; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toString) + #[must_use] + #[js_sys(js_name = "toString")] + pub fn to_string(self: &ZonedDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toString) + #[js_sys(js_name = "toString")] + pub fn to_string_with_options( + self: &ZonedDateTime, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string(self: &ZonedDateTime) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales( + self: &ZonedDateTime, + locales: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toLocaleString) + #[js_sys(js_name = "toLocaleString")] + pub fn to_locale_string_with_locales_and_options( + self: &ZonedDateTime, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/toJSON) + #[must_use] + #[js_sys(js_name = "toJSON")] + pub fn to_json(self: &ZonedDateTime) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &ZonedDateTime) -> Result; +} diff --git a/client/js-sys/src/builtins/typed_array.rs b/client/js-sys/src/builtins/typed_array.rs new file mode 100644 index 00000000..a96b2b02 --- /dev/null +++ b/client/js-sys/src/builtins/typed_array.rs @@ -0,0 +1,1278 @@ +use super::{Array, Function, Iterable, JsIterator, JsString, Number, Object}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys( + js_embed = "typed_array.slice", + return_abi = Result + )] + fn typed_array_slice(array: &JsValue) -> Result; + + #[js_sys( + js_embed = "typed_array.slice_from", + return_abi = Result + )] + fn typed_array_slice_from(array: &JsValue, begin: f64) -> Result; + + #[js_sys( + js_embed = "typed_array.slice_range", + return_abi = Result + )] + fn typed_array_slice_range( + array: &JsValue, + begin: f64, + end: f64, + ) -> Result; + + #[js_sys( + js_embed = "typed_array.subarray", + return_abi = Result + )] + fn typed_array_subarray(array: &JsValue) -> Result; + + #[js_sys( + js_embed = "typed_array.subarray_from", + return_abi = Result + )] + fn typed_array_subarray_from(array: &JsValue, begin: f64) -> Result; + + #[js_sys( + js_embed = "typed_array.subarray_range", + return_abi = Result + )] + fn typed_array_subarray_range( + array: &JsValue, + begin: f64, + end: f64, + ) -> Result; + + #[js_sys(js_embed = "typed_array.property.buffer")] + fn typed_array_buffer(array: &JsValue) -> JsValue; + + #[js_sys(js_embed = "typed_array.property.byte_length")] + fn typed_array_byte_length(array: &JsValue) -> f64; + + #[js_sys(js_embed = "typed_array.property.byte_offset")] + fn typed_array_byte_offset(array: &JsValue) -> f64; + + #[js_sys(js_embed = "typed_array.property.length")] + fn typed_array_length(array: &JsValue) -> f64; + + #[js_sys( + js_embed = "typed_array.copy_within", + return_abi = Result + )] + fn typed_array_copy_within( + array: &JsValue, + target: f64, + start: f64, + ) -> Result; + + #[js_sys( + js_embed = "typed_array.copy_within_range", + return_abi = Result + )] + fn typed_array_copy_within_range( + array: &JsValue, + target: f64, + start: f64, + end: f64, + ) -> Result; + + #[js_sys(js_embed = "typed_array.set")] + fn typed_array_set(array: &JsValue, source: &JsValue) -> Result<(), JsValue>; + + #[js_sys(js_embed = "typed_array.set_with_offset")] + fn typed_array_set_with_offset( + array: &JsValue, + source: &JsValue, + offset: f64, + ) -> Result<(), JsValue>; + + #[js_sys(js_embed = "typed_array.property.constructor")] + fn typed_array_constructor(array: &JsValue) -> Function; + + #[js_sys(js_embed = "typed_array.property.bytes_per_element")] + fn typed_array_bytes_per_element(array: &JsValue) -> u32; + + #[js_sys(js_embed = "typed_array.species")] + fn typed_array_species(constructor: &str) -> Result; + + #[js_sys(js_embed = "typed_array.to_string_tag")] + fn typed_array_to_string_tag(array: &JsValue) -> JsString; + + #[js_sys(js_embed = "typed_array.symbol_iterator")] + fn typed_array_symbol_iterator(array: &JsValue) -> Result; + + #[js_sys(js_embed = "typed_array.entries")] + fn typed_array_entries(array: &JsValue) -> Result, JsValue>; + + #[js_sys(js_embed = "typed_array.every")] + fn typed_array_every(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.every_this")] + fn typed_array_every_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result; + + #[js_sys( + js_embed = "typed_array.filter", + return_abi = Result + )] + fn typed_array_filter(array: &JsValue, callback: &Function) -> Result; + + #[js_sys( + js_embed = "typed_array.filter_this", + return_abi = Result + )] + fn typed_array_filter_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "typed_array.find_index")] + fn typed_array_find_index(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.find_index_this")] + fn typed_array_find_index_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "typed_array.find_last_index")] + fn typed_array_find_last_index(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.find_last_index_this")] + fn typed_array_find_last_index_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "typed_array.for_each")] + fn typed_array_for_each(array: &JsValue, callback: &Function) -> Result<(), JsValue>; + + #[js_sys(js_embed = "typed_array.for_each_this")] + fn typed_array_for_each_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result<(), JsValue>; + + #[js_sys(js_embed = "typed_array.join")] + fn typed_array_join(array: &JsValue) -> Result; + + #[js_sys(js_embed = "typed_array.join_separator")] + fn typed_array_join_with_separator( + array: &JsValue, + separator: &str, + ) -> Result; + + #[js_sys(js_embed = "typed_array.keys")] + fn typed_array_keys(array: &JsValue) -> Result>, JsValue>; + + #[js_sys( + js_embed = "typed_array.map", + return_abi = Result + )] + fn typed_array_map(array: &JsValue, callback: &Function) -> Result; + + #[js_sys( + js_embed = "typed_array.map_this", + return_abi = Result + )] + fn typed_array_map_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "typed_array.reduce")] + fn typed_array_reduce(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.reduce_initial")] + fn typed_array_reduce_with_initial( + array: &JsValue, + callback: &Function, + initial: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "typed_array.reduce_right")] + fn typed_array_reduce_right(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.reduce_right_initial")] + fn typed_array_reduce_right_with_initial( + array: &JsValue, + callback: &Function, + initial: &JsValue, + ) -> Result; + + #[js_sys( + js_embed = "typed_array.reverse", + return_abi = Result + )] + fn typed_array_reverse(array: &JsValue) -> Result; + + #[js_sys(js_embed = "typed_array.some")] + fn typed_array_some(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.some_this")] + fn typed_array_some_with_this( + array: &JsValue, + callback: &Function, + this: &JsValue, + ) -> Result; + + #[js_sys( + js_embed = "typed_array.sort", + return_abi = Result + )] + fn typed_array_sort(array: &JsValue) -> Result; + + #[js_sys( + js_embed = "typed_array.sort_by", + return_abi = Result + )] + fn typed_array_sort_by(array: &JsValue, callback: &Function) -> Result; + + #[js_sys(js_embed = "typed_array.to_locale_string")] + fn typed_array_to_locale_string(array: &JsValue) -> Result; + + #[js_sys(js_embed = "typed_array.to_locale_string_locales")] + fn typed_array_to_locale_string_with_locales( + array: &JsValue, + locales: &JsValue, + ) -> Result; + + #[js_sys(js_embed = "typed_array.to_locale_string_options")] + fn typed_array_to_locale_string_with_options( + array: &JsValue, + locales: &JsValue, + options: &JsValue, + ) -> Result; + + #[js_sys( + js_embed = "typed_array.to_reversed", + return_abi = Result + )] + fn typed_array_to_reversed(array: &JsValue) -> Result; + + #[js_sys( + js_embed = "typed_array.to_sorted", + return_abi = Result + )] + fn typed_array_to_sorted(array: &JsValue) -> Result; + + #[js_sys( + js_embed = "typed_array.to_sorted_by", + return_abi = Result + )] + fn typed_array_to_sorted_by( + array: &JsValue, + callback: &Function, + ) -> Result; + + #[js_sys(js_embed = "typed_array.to_string")] + fn typed_array_to_string(array: &JsValue) -> Result; + + #[js_sys(js_embed = "typed_array.values")] + fn typed_array_values(array: &JsValue) -> Result; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.brand", + "(() => {{", + " const prototype = Object.getPrototypeOf(Uint8Array.prototype)", + " const brand = Object.getOwnPropertyDescriptor(prototype, Symbol.toStringTag).get", + " return (source, result) => {{", + " const sourceBrand = brand.call(source)", + " if (sourceBrand === undefined || sourceBrand !== brand.call(result))", + " throw new TypeError('typed array species changed the element type')", + " return result", + " }}", + "}})()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.slice", + required_embeds = [("js_sys", "typed_array.brand")], + "(array) => this.#jsEmbed.js_sys['typed_array.brand'](array, array.slice())", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.slice_from", + required_embeds = [("js_sys", "typed_array.brand")], + "(array, begin) => this.#jsEmbed.js_sys['typed_array.brand'](", + " array, array.slice(begin)", + ")", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.slice_range", + required_embeds = [("js_sys", "typed_array.brand")], + "(array, begin, end) => this.#jsEmbed.js_sys['typed_array.brand'](", + " array, array.slice(begin, end)", + ")", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.subarray", + required_embeds = [("js_sys", "typed_array.brand")], + "(array) => this.#jsEmbed.js_sys['typed_array.brand'](array, array.subarray())", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.subarray_from", + required_embeds = [("js_sys", "typed_array.brand")], + "(array, begin) => this.#jsEmbed.js_sys['typed_array.brand'](", + " array, array.subarray(begin)", + ")", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.subarray_range", + required_embeds = [("js_sys", "typed_array.brand")], + "(array, begin, end) => this.#jsEmbed.js_sys['typed_array.brand'](", + " array, array.subarray(begin, end)", + ")", +); + +macro_rules! typed_array_embed { + ($name:tt, $source:tt) => { + js_bindgen::embed_js!(module = "js_sys", name = $name, $source); + }; +} + +macro_rules! typed_array_brand_embed { + ($name:tt, $source:tt) => { + js_bindgen::embed_js!( + module = "js_sys", + name = $name, + required_embeds = [("js_sys", "typed_array.brand")], + $source, + ); + }; +} + +typed_array_embed!("typed_array.property.buffer", "(array) => array.buffer"); +typed_array_embed!( + "typed_array.property.byte_length", + "(array) => array.byteLength" +); +typed_array_embed!( + "typed_array.property.byte_offset", + "(array) => array.byteOffset" +); +typed_array_embed!("typed_array.property.length", "(array) => array.length"); +typed_array_embed!( + "typed_array.copy_within", + "(array, target, start) => array.copyWithin(target, start)" +); +typed_array_embed!( + "typed_array.copy_within_range", + "(array, target, start, end) => array.copyWithin(target, start, end)" +); +typed_array_embed!("typed_array.set", "(array, source) => array.set(source)"); +typed_array_embed!( + "typed_array.set_with_offset", + "(array, source, offset) => array.set(source, offset)" +); +typed_array_embed!( + "typed_array.property.constructor", + "(array) => array.constructor" +); +typed_array_embed!( + "typed_array.property.bytes_per_element", + "(array) => array.BYTES_PER_ELEMENT" +); + +typed_array_embed!( + "typed_array.to_string_tag", + "(array) => array[Symbol.toStringTag]" +); +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.species", + "(constructor) => {{", + " const value = globalThis[constructor]", + " if (typeof value !== 'function')", + " throw new TypeError(`${{constructor}} is not available`)", + " const species = value[Symbol.species]", + " if (typeof species !== 'function')", + " throw new TypeError(`${{constructor}} does not provide Symbol.species`)", + " return species", + "}}", +); +typed_array_embed!( + "typed_array.symbol_iterator", + "(array) => array[Symbol.iterator]()" +); +typed_array_embed!("typed_array.entries", "(array) => array.entries()"); +typed_array_embed!( + "typed_array.every", + "(array, callback) => array.every(callback)" +); +typed_array_embed!( + "typed_array.every_this", + "(array, callback, thisArg) => array.every(callback, thisArg)" +); +typed_array_brand_embed!( + "typed_array.filter", + "(array, callback) => this.#jsEmbed.js_sys['typed_array.brand'](array, array.filter(callback))" +); +typed_array_brand_embed!( + "typed_array.filter_this", + "(array, callback, thisArg) => this.#jsEmbed.js_sys['typed_array.brand'](array, \ + array.filter(callback, thisArg))" +); +typed_array_embed!( + "typed_array.find_index", + "(array, callback) => array.findIndex(callback)" +); +typed_array_embed!( + "typed_array.find_index_this", + "(array, callback, thisArg) => array.findIndex(callback, thisArg)" +); +typed_array_embed!( + "typed_array.find_last_index", + "(array, callback) => array.findLastIndex(callback)" +); +typed_array_embed!( + "typed_array.find_last_index_this", + "(array, callback, thisArg) => array.findLastIndex(callback, thisArg)" +); +typed_array_embed!( + "typed_array.for_each", + "(array, callback) => array.forEach(callback)" +); +typed_array_embed!( + "typed_array.for_each_this", + "(array, callback, thisArg) => array.forEach(callback, thisArg)" +); +typed_array_embed!("typed_array.join", "(array) => array.join()"); +typed_array_embed!( + "typed_array.join_separator", + "(array, separator) => array.join(separator)" +); +typed_array_embed!("typed_array.keys", "(array) => array.keys()"); +typed_array_brand_embed!( + "typed_array.map", + "(array, callback) => this.#jsEmbed.js_sys['typed_array.brand'](array, array.map(callback))" +); +typed_array_brand_embed!( + "typed_array.map_this", + "(array, callback, thisArg) => this.#jsEmbed.js_sys['typed_array.brand'](array, \ + array.map(callback, thisArg))" +); +typed_array_embed!( + "typed_array.reduce", + "(array, callback) => array.reduce(callback)" +); +typed_array_embed!( + "typed_array.reduce_initial", + "(array, callback, initial) => array.reduce(callback, initial)" +); +typed_array_embed!( + "typed_array.reduce_right", + "(array, callback) => array.reduceRight(callback)" +); +typed_array_embed!( + "typed_array.reduce_right_initial", + "(array, callback, initial) => array.reduceRight(callback, initial)" +); +typed_array_embed!("typed_array.reverse", "(array) => array.reverse()"); +typed_array_embed!( + "typed_array.some", + "(array, callback) => array.some(callback)" +); +typed_array_embed!( + "typed_array.some_this", + "(array, callback, thisArg) => array.some(callback, thisArg)" +); +typed_array_embed!("typed_array.sort", "(array) => array.sort()"); +typed_array_embed!( + "typed_array.sort_by", + "(array, callback) => array.sort(callback)" +); +typed_array_embed!( + "typed_array.to_locale_string", + "(array) => array.toLocaleString()" +); +typed_array_embed!( + "typed_array.to_locale_string_locales", + "(array, locales) => array.toLocaleString(locales)" +); +typed_array_embed!( + "typed_array.to_locale_string_options", + "(array, locales, options) => array.toLocaleString(locales, options)" +); +typed_array_brand_embed!( + "typed_array.to_reversed", + "(array) => this.#jsEmbed.js_sys['typed_array.brand'](array, array.toReversed())" +); +typed_array_brand_embed!( + "typed_array.to_sorted", + "(array) => this.#jsEmbed.js_sys['typed_array.brand'](array, array.toSorted())" +); +typed_array_brand_embed!( + "typed_array.to_sorted_by", + "(array, callback) => this.#jsEmbed.js_sys['typed_array.brand'](array, \ + array.toSorted(callback))" +); +typed_array_embed!("typed_array.to_string", "(array) => array.toString()"); +typed_array_embed!("typed_array.values", "(array) => array.values()"); + +macro_rules! typed_array_stable_api { + (@normal $name:ident : $value:ty, constructor = $constructor:literal) => { + typed_array_stable_api! { + @impl $name: $value, + constructor = $constructor, + find = find, + find_with_this = find_with_this, + find_last = find_last, + find_last_with_this = find_last_with_this, + includes = includes, + includes_from = includes_from, + index_of = index_of, + index_of_from = index_of_from, + last_index_of = last_index_of, + last_index_of_from = last_index_of_from, + with = with, + } + }; + (@float16 $name:ident : $value:ty, constructor = $constructor:literal) => { + typed_array_stable_api! { + @impl $name: $value, + constructor = $constructor, + find = find_as_f32, + find_with_this = find_as_f32_with_this, + find_last = find_last_as_f32, + find_last_with_this = find_last_as_f32_with_this, + includes = includes_f32, + includes_from = includes_f32_from, + index_of = index_of_f32, + index_of_from = index_of_f32_from, + last_index_of = last_index_of_f32, + last_index_of_from = last_index_of_f32_from, + with = with_f32, + } + }; + ( + @impl $name:ident : $value:ty, + constructor = $constructor:literal, + find = $find:ident, + find_with_this = $find_with_this:ident, + find_last = $find_last:ident, + find_last_with_this = $find_last_with_this:ident, + includes = $includes:ident, + includes_from = $includes_from:ident, + index_of = $index_of:ident, + index_of_from = $index_of_from:ident, + last_index_of = $last_index_of:ident, + last_index_of_from = $last_index_of_from:ident, + with = $with:ident, + ) => { + #[js_sys(js_sys = crate)] + extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/from) + #[js_sys(static_of = $name, js_name = "from")] + pub fn from_value(value: &JsValue) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/from) + #[js_sys(static_of = $name, js_name = "from")] + pub fn from_value_with_map(value: &JsValue, map: &Function) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/from) + #[js_sys(static_of = $name, js_name = "from")] + pub fn from_value_with_map_and_this( + value: &JsValue, + map: &Function, + this: &JsValue, + ) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/of) + #[js_sys(static_of = $name, variadic)] + pub fn of(values: &[JsValue]) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/find) + #[js_sys(js_name = "find")] + pub fn $find(self: &$name, callback: &Function) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/find) + #[js_sys(js_name = "find")] + pub fn $find_with_this( + self: &$name, + callback: &Function, + this: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findLast) + #[js_sys(js_name = "findLast")] + pub fn $find_last(self: &$name, callback: &Function) + -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findLast) + #[js_sys(js_name = "findLast")] + pub fn $find_last_with_this( + self: &$name, + callback: &Function, + this: &JsValue, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/includes) + #[js_sys(js_name = "includes")] + pub fn $includes(self: &$name, value: $value) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/includes) + #[js_sys(js_name = "includes")] + pub fn $includes_from( + self: &$name, + value: $value, + from_index: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/indexOf) + #[js_sys(js_name = "indexOf")] + pub fn $index_of(self: &$name, value: $value) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/indexOf) + #[js_sys(js_name = "indexOf")] + pub fn $index_of_from( + self: &$name, + value: $value, + from_index: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/lastIndexOf) + #[js_sys(js_name = "lastIndexOf")] + pub fn $last_index_of(self: &$name, value: $value) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/lastIndexOf) + #[js_sys(js_name = "lastIndexOf")] + pub fn $last_index_of_from( + self: &$name, + value: $value, + from_index: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/with) + #[js_sys(js_name = "with")] + pub fn $with(self: &$name, index: f64, value: $value) -> Result<$name, JsValue>; + } + + impl $name { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/Symbol.species) + pub fn species() -> Result { + typed_array_species($constructor) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/constructor) + #[must_use] + #[inline] + pub fn constructor(&self) -> Function { + typed_array_constructor(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/BYTES_PER_ELEMENT) + #[must_use] + #[inline] + pub fn bytes_per_element(&self) -> u32 { + typed_array_bytes_per_element(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/buffer) + #[must_use] + #[inline] + pub fn buffer(&self) -> JsValue { + typed_array_buffer(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/byteLength) + #[must_use] + #[inline] + pub fn byte_length(&self) -> f64 { + typed_array_byte_length(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/byteOffset) + #[must_use] + #[inline] + pub fn byte_offset(&self) -> f64 { + typed_array_byte_offset(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/length) + #[must_use] + #[inline] + pub fn length(&self) -> f64 { + typed_array_length(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/copyWithin) + #[inline] + pub fn copy_within(&self, target: f64, start: f64) -> Result { + typed_array_copy_within(self.unchecked_as_ref(), target, start) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/copyWithin) + #[inline] + pub fn copy_within_range( + &self, + target: f64, + start: f64, + end: f64, + ) -> Result { + typed_array_copy_within_range(self.unchecked_as_ref(), target, start, end) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set) + #[inline] + pub fn set(&self, source: &JsValue) -> Result<(), JsValue> { + typed_array_set(self.unchecked_as_ref(), source) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set) + #[inline] + pub fn set_with_offset(&self, source: &JsValue, offset: f64) -> Result<(), JsValue> { + typed_array_set_with_offset(self.unchecked_as_ref(), source, offset) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/Symbol.toStringTag) + #[must_use] + pub fn symbol_to_string_tag(&self) -> JsString { + typed_array_to_string_tag(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/Symbol.iterator) + pub fn symbol_iterator(&self) -> Result { + typed_array_symbol_iterator(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/entries) + pub fn entries(&self) -> Result, JsValue> { + typed_array_entries(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/every) + pub fn every(&self, callback: &Function) -> Result { + typed_array_every(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/every) + pub fn every_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result { + typed_array_every_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/filter) + pub fn filter(&self, callback: &Function) -> Result { + typed_array_filter(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/filter) + pub fn filter_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result { + typed_array_filter_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findIndex) + pub fn find_index(&self, callback: &Function) -> Result { + typed_array_find_index(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findIndex) + pub fn find_index_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result { + typed_array_find_index_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findLastIndex) + pub fn find_last_index(&self, callback: &Function) -> Result { + typed_array_find_last_index(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findLastIndex) + pub fn find_last_index_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result { + typed_array_find_last_index_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/forEach) + pub fn for_each(&self, callback: &Function) -> Result<(), JsValue> { + typed_array_for_each(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/forEach) + pub fn for_each_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result<(), JsValue> { + typed_array_for_each_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/join) + pub fn join(&self) -> Result { + typed_array_join(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/join) + pub fn join_with_separator(&self, separator: &str) -> Result { + typed_array_join_with_separator(self.unchecked_as_ref(), separator) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/keys) + pub fn keys(&self) -> Result>, JsValue> { + typed_array_keys(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/map) + pub fn map(&self, callback: &Function) -> Result { + typed_array_map(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/map) + pub fn map_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result { + typed_array_map_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/reduce) + pub fn reduce(&self, callback: &Function) -> Result { + typed_array_reduce(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/reduce) + pub fn reduce_with_initial( + &self, + callback: &Function, + initial: &JsValue, + ) -> Result { + typed_array_reduce_with_initial(self.unchecked_as_ref(), callback, initial) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/reduceRight) + pub fn reduce_right(&self, callback: &Function) -> Result { + typed_array_reduce_right(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/reduceRight) + pub fn reduce_right_with_initial( + &self, + callback: &Function, + initial: &JsValue, + ) -> Result { + typed_array_reduce_right_with_initial(self.unchecked_as_ref(), callback, initial) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/reverse) + pub fn reverse(&self) -> Result { + typed_array_reverse(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/some) + pub fn some(&self, callback: &Function) -> Result { + typed_array_some(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/some) + pub fn some_with_this( + &self, + callback: &Function, + this: &JsValue, + ) -> Result { + typed_array_some_with_this(self.unchecked_as_ref(), callback, this) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/sort) + pub fn sort(&self) -> Result { + typed_array_sort(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/sort) + pub fn sort_by(&self, callback: &Function) -> Result { + typed_array_sort_by(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toLocaleString) + pub fn to_locale_string(&self) -> Result { + typed_array_to_locale_string(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toLocaleString) + pub fn to_locale_string_with_locales( + &self, + locales: &JsValue, + ) -> Result { + typed_array_to_locale_string_with_locales(self.unchecked_as_ref(), locales) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toLocaleString) + pub fn to_locale_string_with_options( + &self, + locales: &JsValue, + options: &JsValue, + ) -> Result { + typed_array_to_locale_string_with_options(self.unchecked_as_ref(), locales, options) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toReversed) + pub fn to_reversed(&self) -> Result { + typed_array_to_reversed(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toSorted) + pub fn to_sorted(&self) -> Result { + typed_array_to_sorted(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toSorted) + pub fn to_sorted_by(&self, callback: &Function) -> Result { + typed_array_to_sorted_by(self.unchecked_as_ref(), callback) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/toString) + pub fn to_string(&self) -> Result { + typed_array_to_string(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/values) + pub fn values(&self) -> Result { + typed_array_values(self.unchecked_as_ref()) + } + } + + impl Iterable for $name { + type Item = JsValue; + } + }; +} + +macro_rules! typed_array { + ( + $name:ident : $element:ty, + constructor = $constructor:literal, + mdn = $mdn:literal, + ) => { + #[js_sys(js_sys = crate)] + extern "js-sys" { + #[doc = "[`MDN` documentation]("] + #[doc = $mdn] + #[doc = ")"] + #[js_sys(js_name = $constructor, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type $name; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new(value: &JsValue) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new_with_length( + length: f64, + ) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new_with_byte_offset( + buffer: &JsValue, + byte_offset: f64, + ) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new_with_byte_offset_and_length( + buffer: &JsValue, + byte_offset: f64, + length: f64, + ) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/at) + pub fn at( + self: &$name, + index: f64, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill) + pub fn fill(self: &$name, value: $element) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill) + #[js_sys(js_name = "fill")] + pub fn fill_from( + self: &$name, + value: $element, + start: f64, + ) -> Result<$name, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill) + #[js_sys(js_name = "fill")] + pub fn fill_range( + self: &$name, + value: $element, + start: f64, + end: f64, + ) -> Result<$name, JsValue>; + } + + typed_array_stable_api!(@normal $name: $element, constructor = $constructor); + + #[js_sys(js_sys = crate)] + extern "js-sys" { + #[must_use] + #[js_sys(indexing_getter)] + pub fn get( + self: &$name, + index: f64, + ) -> Option<$element>; + + #[js_sys(indexing_setter)] + pub fn set_index( + self: &$name, + index: f64, + value: $element, + ); + } + + impl $name { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice) + #[inline] + pub fn slice(&self) -> Result { + typed_array_slice(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice) + #[inline] + pub fn slice_from(&self, begin: f64) -> Result { + typed_array_slice_from(self.unchecked_as_ref(), begin) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice) + #[inline] + pub fn slice_range(&self, begin: f64, end: f64) -> Result { + typed_array_slice_range(self.unchecked_as_ref(), begin, end) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray) + #[inline] + pub fn subarray(&self) -> Result { + typed_array_subarray(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray) + #[inline] + pub fn subarray_from(&self, begin: f64) -> Result { + typed_array_subarray_from(self.unchecked_as_ref(), begin) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray) + #[inline] + pub fn subarray_range(&self, begin: f64, end: f64) -> Result { + typed_array_subarray_range(self.unchecked_as_ref(), begin, end) + } + } + }; +} + +typed_array! { + Int8Array: i8, + constructor = "Int8Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array", +} + +typed_array! { + Uint8Array: u8, + constructor = "Uint8Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array", +} + +typed_array! { + Uint8ClampedArray: u8, + constructor = "Uint8ClampedArray", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray", +} + +typed_array! { + Int16Array: i16, + constructor = "Int16Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array", +} + +typed_array! { + Uint16Array: u16, + constructor = "Uint16Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array", +} + +// Stable Rust does not have `f16`. Scalar APIs therefore use `f32`, while +// bulk APIs preserve the raw `IEEE 754 binary16` representation in `u16` +// values. +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float16Array) + #[js_sys(js_name = "Float16Array", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Float16Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new(value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new_with_length(length: f64) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new_with_byte_offset( + buffer: &JsValue, + byte_offset: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) + #[js_sys(constructor)] + pub fn new_with_byte_offset_and_length( + buffer: &JsValue, + byte_offset: f64, + length: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/at) + #[js_sys(js_name = "at")] + pub fn at_as_f32(self: &Float16Array, index: f64) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill) + #[js_sys(js_name = "fill")] + pub fn fill_with_f32(self: &Float16Array, value: f32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill) + #[js_sys(js_name = "fill")] + pub fn fill_with_f32_from( + self: &Float16Array, + value: f32, + start: f64, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/fill) + #[js_sys(js_name = "fill")] + pub fn fill_with_f32_range( + self: &Float16Array, + value: f32, + start: f64, + end: f64, + ) -> Result; +} + +typed_array_stable_api!(@float16 Float16Array: f32, constructor = "Float16Array"); + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[must_use] + #[js_sys(indexing_getter)] + pub fn get_as_f32(self: &Float16Array, index: f64) -> Option; + + #[js_sys(indexing_setter)] + pub fn set_index_from_f32(self: &Float16Array, index: f64, value: f32); +} + +impl Float16Array { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice) + #[inline] + pub fn slice(&self) -> Result { + typed_array_slice(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice) + #[inline] + pub fn slice_from(&self, begin: f64) -> Result { + typed_array_slice_from(self.unchecked_as_ref(), begin) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice) + #[inline] + pub fn slice_range(&self, begin: f64, end: f64) -> Result { + typed_array_slice_range(self.unchecked_as_ref(), begin, end) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray) + #[inline] + pub fn subarray(&self) -> Result { + typed_array_subarray(self.unchecked_as_ref()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray) + #[inline] + pub fn subarray_from(&self, begin: f64) -> Result { + typed_array_subarray_from(self.unchecked_as_ref(), begin) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray) + #[inline] + pub fn subarray_range(&self, begin: f64, end: f64) -> Result { + typed_array_subarray_range(self.unchecked_as_ref(), begin, end) + } +} + +typed_array! { + Int32Array: i32, + constructor = "Int32Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array", +} + +typed_array! { + Uint32Array: u32, + constructor = "Uint32Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array", +} + +typed_array! { + Float32Array: f32, + constructor = "Float32Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array", +} + +typed_array! { + Float64Array: f64, + constructor = "Float64Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float64Array", +} + +typed_array! { + BigInt64Array: i64, + constructor = "BigInt64Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array", +} + +typed_array! { + BigUint64Array: u64, + constructor = "BigUint64Array", + mdn = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigUint64Array", +} diff --git a/client/js-sys/src/builtins/uint8_array.rs b/client/js-sys/src/builtins/uint8_array.rs new file mode 100644 index 00000000..874cfd16 --- /dev/null +++ b/client/js-sys/src/builtins/uint8_array.rs @@ -0,0 +1,177 @@ +use super::{Object, Uint8Array}; +use crate::hazard::JsCast; +use crate::{JsString, JsValue, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#alphabet) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Base64Alphabet { + Base64, + Base64Url, +} + +impl Base64Alphabet { + const fn as_str(self) -> &'static str { + match self { + Self::Base64 => "base64", + Self::Base64Url => "base64url", + } + } +} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#lastchunkhandling) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Base64LastChunkHandling { + Loose, + Strict, + StopBeforePartial, +} + +impl Base64LastChunkHandling { + const fn as_str(self) -> &'static str { + match self { + Self::Loose => "loose", + Self::Strict => "strict", + Self::StopBeforePartial => "stop-before-partial", + } + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Base64DecodeOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Base64EncodeOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromBase64#return_value) + /// + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromHex#return_value) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Uint8ArraySetResult; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromBase64#return_value) + #[must_use] + #[js_sys(getter = "read")] + pub fn read(self: &Uint8ArraySetResult) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromBase64#return_value) + #[must_use] + #[js_sys(getter = "written")] + pub fn written(self: &Uint8ArraySetResult) -> f64; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64) + #[js_sys(static_of = Uint8Array, js_name = "fromBase64")] + pub fn from_base64(string: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64) + #[js_sys(static_of = Uint8Array, js_name = "fromBase64")] + pub fn from_base64_with_options( + string: &str, + options: &Base64DecodeOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromHex) + #[js_sys(static_of = Uint8Array, js_name = "fromHex")] + pub fn from_hex(string: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromBase64) + #[js_sys(js_name = "setFromBase64")] + pub fn set_from_base64(self: &Uint8Array, string: &str) + -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromBase64) + #[js_sys(js_name = "setFromBase64")] + pub fn set_from_base64_with_options( + self: &Uint8Array, + string: &str, + options: &Base64DecodeOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/setFromHex) + #[js_sys(js_name = "setFromHex")] + pub fn set_from_hex(self: &Uint8Array, string: &str) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64) + #[js_sys(js_name = "toBase64")] + pub fn to_base64(self: &Uint8Array) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64) + #[js_sys(js_name = "toBase64")] + pub fn to_base64_with_options( + self: &Uint8Array, + options: &Base64EncodeOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toHex) + #[js_sys(js_name = "toHex")] + pub fn to_hex(self: &Uint8Array) -> Result; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(setter = "alphabet")] + fn set_decode_alphabet(self: &Base64DecodeOptions, alphabet: &str); + + #[js_sys(setter = "lastChunkHandling")] + fn set_last_chunk_handling_raw(self: &Base64DecodeOptions, handling: &str); + + #[js_sys(setter = "alphabet")] + fn set_encode_alphabet(self: &Base64EncodeOptions, alphabet: &str); + + #[js_sys(setter = "omitPadding")] + fn set_omit_padding_raw(self: &Base64EncodeOptions, omit_padding: bool); +} + +impl Base64DecodeOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#alphabet) + pub fn set_alphabet(&self, alphabet: Base64Alphabet) { + self.set_decode_alphabet(alphabet.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#lastchunkhandling) + pub fn set_last_chunk_handling(&self, handling: Base64LastChunkHandling) { + self.set_last_chunk_handling_raw(handling.as_str()); + } +} + +impl Default for Base64DecodeOptions { + fn default() -> Self { + Self::new() + } +} + +impl Base64EncodeOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64#alphabet) + pub fn set_alphabet(&self, alphabet: Base64Alphabet) { + self.set_encode_alphabet(alphabet.as_str()); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64#omitpadding) + pub fn set_omit_padding(&self, omit_padding: bool) { + self.set_omit_padding_raw(omit_padding); + } +} + +impl Default for Base64EncodeOptions { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/weak_map.rs b/client/js-sys/src/builtins/weak_map.rs new file mode 100644 index 00000000..a8db9afd --- /dev/null +++ b/client/js-sys/src/builtins/weak_map.rs @@ -0,0 +1,99 @@ +use core::fmt::{self, Formatter}; + +use super::{Function, Iterable, Object}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) + #[js_sys(extends = Object)] + pub type WeakMap; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/WeakMap) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> WeakMap; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/WeakMap) + #[must_use] + #[js_sys(constructor, return_abi = WeakMap)] + pub fn new_typed() -> WeakMap; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/WeakMap) + #[js_sys(constructor, return_abi = Result)] + pub fn new_from_iterable( + #[js_sys(type = &JsValue)] entries: &I, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/delete) + #[must_use] + pub fn delete(self: &WeakMap, #[js_sys(type = &JsValue)] key: &K) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/get) + #[must_use] + pub fn get(self: &WeakMap, #[js_sys(type = &JsValue)] key: &K) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/get) + #[must_use] + #[js_sys(js_name = "get", return_abi = Option)] + pub fn get_checked( + self: &WeakMap, + #[js_sys(type = &JsValue)] key: &K, + ) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/getOrInsert) + #[js_sys(js_name = "getOrInsert", return_abi = Result)] + pub fn get_or_insert( + self: &WeakMap, + #[js_sys(type = &JsValue)] key: &K, + #[js_sys(type = &JsValue)] default_value: &V, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/getOrInsertComputed) + #[js_sys( + js_name = "getOrInsertComputed", + return_abi = Result + )] + pub fn get_or_insert_computed( + self: &WeakMap, + #[js_sys(type = &JsValue)] key: &K, + callback: &Function, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/has) + #[must_use] + pub fn has(self: &WeakMap, #[js_sys(type = &JsValue)] key: &K) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/set) + #[js_sys(return_abi = Result)] + pub fn set( + self: &WeakMap, + #[js_sys(type = &JsValue)] key: &K, + #[js_sys(type = &JsValue)] value: &V, + ) -> Result, JsValue>; +} + +impl Clone for WeakMap { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } +} + +impl fmt::Debug for WeakMap { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } +} + +impl PartialEq for WeakMap { + fn eq(&self, other: &Self) -> bool { + >::as_ref(self) == >::as_ref(other) + } +} + +impl Default for WeakMap { + fn default() -> Self { + Self::new_typed() + } +} diff --git a/client/js-sys/src/builtins/weak_ref.rs b/client/js-sys/src/builtins/weak_ref.rs new file mode 100644 index 00000000..1e76de32 --- /dev/null +++ b/client/js-sys/src/builtins/weak_ref.rs @@ -0,0 +1,39 @@ +use core::fmt::{self, Formatter}; + +use super::Object; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) + #[js_sys(extends = Object)] + pub type WeakRef; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef/WeakRef) + #[js_sys(constructor, return_abi = Result)] + pub fn new(#[js_sys(type = &JsValue)] target: &T) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef/deref) + #[must_use] + #[js_sys(return_abi = Option)] + pub fn deref(self: &WeakRef) -> Option; +} + +impl Clone for WeakRef { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } +} + +impl fmt::Debug for WeakRef { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } +} + +impl PartialEq for WeakRef { + fn eq(&self, other: &Self) -> bool { + >::as_ref(self) == >::as_ref(other) + } +} diff --git a/client/js-sys/src/builtins/weak_set.rs b/client/js-sys/src/builtins/weak_set.rs new file mode 100644 index 00000000..7048b6a7 --- /dev/null +++ b/client/js-sys/src/builtins/weak_set.rs @@ -0,0 +1,67 @@ +use core::fmt::{self, Formatter}; + +use super::{Iterable, Object}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) + #[js_sys(extends = Object)] + pub type WeakSet; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/WeakSet) + #[must_use] + #[js_sys(constructor)] + pub fn new() -> WeakSet; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/WeakSet) + #[must_use] + #[js_sys(constructor, return_abi = WeakSet)] + pub fn new_typed() -> WeakSet; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/WeakSet) + #[js_sys(constructor, return_abi = Result)] + pub fn new_from_iterable>( + #[js_sys(type = &JsValue)] values: &I, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/add) + #[js_sys(return_abi = Result)] + pub fn add( + self: &WeakSet, + #[js_sys(type = &JsValue)] value: &T, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/delete) + #[must_use] + pub fn delete(self: &WeakSet, #[js_sys(type = &JsValue)] value: &T) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/has) + #[must_use] + pub fn has(self: &WeakSet, #[js_sys(type = &JsValue)] value: &T) -> bool; +} + +impl Clone for WeakSet { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } +} + +impl fmt::Debug for WeakSet { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), formatter) + } +} + +impl PartialEq for WeakSet { + fn eq(&self, other: &Self) -> bool { + >::as_ref(self) == >::as_ref(other) + } +} + +impl Default for WeakSet { + fn default() -> Self { + Self::new_typed() + } +} diff --git a/client/js-sys/src/builtins/webassembly/address.rs b/client/js-sys/src/builtins/webassembly/address.rs new file mode 100644 index 00000000..1d4f811d --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/address.rs @@ -0,0 +1,28 @@ +use alloc::string::String; + +use crate::JsString; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#address) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum AddressType { + I32, + I64, +} + +impl AddressType { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::I32 => "i32", + Self::I64 => "i64", + } + } + + pub(super) fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "i32" => Some(Self::I32), + "i64" => Some(Self::I64), + _ => None, + } + } +} diff --git a/client/js-sys/src/builtins/webassembly/error.rs b/client/js-sys/src/builtins/webassembly/error.rs new file mode 100644 index 00000000..11f07be2 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/error.rs @@ -0,0 +1,47 @@ +use crate::{Error, ErrorOptions, Object, js_sys}; + +macro_rules! error_types { + ($( + $type:ident = $js_name:literal { + type_doc = $type_doc:literal, + constructor_doc = $constructor_doc:literal, + } + )*) => {$( + #[js_sys(js_sys = crate, namespace = "WebAssembly")] + extern "js-sys" { + #[doc = $type_doc] + #[js_sys(js_name = $js_name, extends = Error, extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type $type; + + #[doc = $constructor_doc] + #[must_use] + #[js_sys(constructor)] + pub fn new(message: &str) -> $type; + + #[doc = $constructor_doc] + #[must_use] + #[js_sys(constructor)] + pub fn new_with_options(message: &str, options: &ErrorOptions) -> $type; + } + )*}; +} + +error_types! { + CompileError = "CompileError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/CompileError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/CompileError/CompileError)", + } + LinkError = "LinkError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/LinkError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/LinkError/LinkError)", + } + RuntimeError = "RuntimeError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/RuntimeError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/RuntimeError/RuntimeError)", + } + SuspendError = "SuspendError" { + type_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/SuspendError)", + constructor_doc = "[`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/SuspendError/SuspendError)", + } +} diff --git a/client/js-sys/src/builtins/webassembly/exception.rs b/client/js-sys/src/builtins/webassembly/exception.rs new file mode 100644 index 00000000..e19100a4 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/exception.rs @@ -0,0 +1,127 @@ +use super::global::ValueType; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/Tag) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type TagDescriptor; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/type) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type TagType; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/Tag#parameters) + #[must_use] + #[js_sys(getter = "parameters")] + pub fn parameters(self: &TagDescriptor) -> Array; + + #[js_sys(setter = "parameters")] + pub(crate) fn set_parameters(self: &TagDescriptor, value: &Array); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/type) + #[must_use] + #[js_sys(getter = "parameters")] + pub fn parameters(self: &TagType) -> Array; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception#options) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ExceptionOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception#options) + #[must_use] + #[js_sys(getter = "traceStack")] + pub fn trace_stack(self: &ExceptionOptions) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception#options) + #[js_sys(setter = "traceStack")] + pub fn set_trace_stack(self: &ExceptionOptions, value: bool); +} + +impl TagDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/Tag) + #[must_use] + pub fn new(parameters: &[ValueType]) -> Self { + let values = Array::new_typed(); + for parameter in parameters { + let _ = values.push(&JsString::from(parameter.as_str())); + } + + let descriptor = Self::unchecked_from(Object::new().into()); + descriptor.set_parameters(&values); + descriptor + } +} + +impl ExceptionOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception#options) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } +} + +impl Default for ExceptionOptions { + fn default() -> Self { + Self::new() + } +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag) + #[js_sys(js_name = "Tag", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Tag; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/Tag) + #[js_sys(constructor)] + pub fn new(descriptor: &TagDescriptor) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/type) + #[must_use] + #[js_sys(js_name = "type")] + pub fn type_(self: &Tag) -> TagType; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception) + #[js_sys(js_name = "Exception", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Exception; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception) + #[js_sys(constructor)] + pub fn new(tag: &Tag, payload: &[JsValue]) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception) + #[js_sys(constructor)] + pub fn new_with_options( + tag: &Tag, + payload: &[JsValue], + options: &ExceptionOptions, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/is) + #[must_use] + pub fn is(self: &Exception, tag: &Tag) -> bool; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/getArg) + #[js_sys(js_name = "getArg")] + pub fn get_arg(self: &Exception, tag: &Tag, index: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/stack) + #[must_use] + #[js_sys(getter)] + pub fn stack(self: &Exception) -> Option; +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/JSTag_static) + #[must_use] + #[js_sys(getter = "JSTag")] + pub fn js_tag() -> Tag; +} diff --git a/client/js-sys/src/builtins/webassembly/global.rs b/client/js-sys/src/builtins/webassembly/global.rs new file mode 100644 index 00000000..083d659e --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/global.rs @@ -0,0 +1,134 @@ +use alloc::string::String; + +use crate::hazard::JsCast; +use crate::{JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global#value) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ValueType { + I32, + I64, + F32, + F64, + V128, + ExternRef, + AnyFunc, +} + +impl ValueType { + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::I32 => "i32", + Self::I64 => "i64", + Self::F32 => "f32", + Self::F64 => "f64", + Self::V128 => "v128", + Self::ExternRef => "externref", + Self::AnyFunc => "anyfunc", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "i32" => Some(Self::I32), + "i64" => Some(Self::I64), + "f32" => Some(Self::F32), + "f64" => Some(Self::F64), + "v128" => Some(Self::V128), + "externref" => Some(Self::ExternRef), + "anyfunc" => Some(Self::AnyFunc), + _ => None, + } + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type GlobalDescriptor; + + #[js_sys(getter = "value")] + fn value_type_raw(self: &GlobalDescriptor) -> JsString; + + #[js_sys(setter = "value")] + fn set_value_type_raw(self: &GlobalDescriptor, value: &str); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global#mutable) + #[must_use] + #[js_sys(getter = "mutable")] + pub fn mutable(self: &GlobalDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global#mutable) + #[js_sys(setter = "mutable")] + pub fn set_mutable(self: &GlobalDescriptor, value: bool); +} + +impl GlobalDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global) + #[must_use] + pub fn new(value_type: ValueType) -> Self { + let descriptor = Self::unchecked_from(Object::new().into()); + descriptor.set_value_type(value_type); + descriptor + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global#value) + #[must_use] + pub fn value_type(&self) -> Option { + ValueType::from_js_string(&self.value_type_raw()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global#value) + pub fn set_value_type(&self, value: ValueType) { + self.set_value_type_raw(value.as_str()); + } +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global) + #[js_sys(js_name = "Global", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Global; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global) + #[js_sys(constructor)] + pub fn new(descriptor: &GlobalDescriptor) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global) + #[js_sys(constructor)] + pub fn new_with_value( + descriptor: &GlobalDescriptor, + value: &JsValue, + ) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/value) + #[js_sys(getter)] + pub fn value(self: &Global) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/valueOf) + #[js_sys(js_name = "valueOf")] + pub fn value_of(self: &Global) -> Result; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "webassembly.global.set_value", + "(global, value) => {{ global.value = value }}", +); + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "webassembly.global.set_value")] + fn set_global_value(global: &Global, value: &JsValue) -> Result<(), JsValue>; +} + +impl Global { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/value) + pub fn set_value(&self, value: &JsValue) -> Result<(), JsValue> { + set_global_value(self, value) + } +} diff --git a/client/js-sys/src/builtins/webassembly/instance.rs b/client/js-sys/src/builtins/webassembly/instance.rs new file mode 100644 index 00000000..c4339857 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/instance.rs @@ -0,0 +1,23 @@ +use super::module::Module; +use crate::{JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Instance; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance/Instance) + #[js_sys(constructor)] + pub fn new(module: &Module) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance/Instance) + #[js_sys(constructor)] + pub fn new_with_imports(module: &Module, imports: &Object) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance/exports) + #[must_use] + #[js_sys(getter)] + pub fn exports(self: &Instance) -> Object; +} diff --git a/client/js-sys/src/builtins/webassembly/jspi.rs b/client/js-sys/src/builtins/webassembly/jspi.rs new file mode 100644 index 00000000..ee427d49 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/jspi.rs @@ -0,0 +1,16 @@ +use crate::{Function, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Suspending) + #[js_sys(js_name = "Suspending", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Suspending; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Suspending/Suspending) + #[js_sys(constructor)] + pub fn new(function: &Function) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/promising_static) + pub fn promising(function: &Function) -> Result; +} diff --git a/client/js-sys/src/builtins/webassembly/memory.rs b/client/js-sys/src/builtins/webassembly/memory.rs new file mode 100644 index 00000000..172fcb07 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/memory.rs @@ -0,0 +1,118 @@ +use super::address::AddressType; +use crate::hazard::JsCast; +use crate::{JsString, JsValue, Object, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type MemoryDescriptor; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#initial) + #[must_use] + #[js_sys(getter = "initial")] + pub fn initial(self: &MemoryDescriptor) -> JsValue; + + #[js_sys(setter = "initial")] + fn set_initial32(self: &MemoryDescriptor, value: u32); + + #[js_sys(setter = "initial")] + fn set_initial64(self: &MemoryDescriptor, value: u64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#maximum) + #[must_use] + #[js_sys(getter = "maximum")] + pub fn maximum(self: &MemoryDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#maximum) + #[js_sys(setter = "maximum")] + pub fn set_maximum(self: &MemoryDescriptor, value: u32); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#using_a_64-bit_address) + #[js_sys(setter = "maximum")] + pub fn set_maximum64(self: &MemoryDescriptor, value: u64); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#shared) + #[must_use] + #[js_sys(getter = "shared")] + pub fn shared(self: &MemoryDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#shared) + #[js_sys(setter = "shared")] + pub fn set_shared(self: &MemoryDescriptor, value: bool); + + #[js_sys(getter = "address")] + fn address_raw(self: &MemoryDescriptor) -> Option; + + #[js_sys(setter = "address")] + fn set_address_raw(self: &MemoryDescriptor, value: &str); +} + +impl MemoryDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory) + #[must_use] + pub fn new(initial: u32) -> Self { + let descriptor = Self::unchecked_from(Object::new().into()); + descriptor.set_initial32(initial); + descriptor + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#using_a_64-bit_address) + #[must_use] + pub fn new64(initial: u64) -> Self { + let descriptor = Self::unchecked_from(Object::new().into()); + descriptor.set_address(AddressType::I64); + descriptor.set_initial64(initial); + descriptor + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#using_a_64-bit_address) + #[must_use] + pub fn address(&self) -> Option { + self.address_raw() + .as_ref() + .and_then(AddressType::from_js_string) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory#using_a_64-bit_address) + pub fn set_address(&self, value: AddressType) { + self.set_address_raw(value.as_str()); + } +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory) + #[js_sys(js_name = "Memory", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Memory; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory) + #[js_sys(constructor)] + pub fn new(descriptor: &MemoryDescriptor) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/buffer) + #[must_use] + #[js_sys(getter)] + pub fn buffer(self: &Memory) -> JsValue; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/grow) + pub fn grow(self: &Memory, delta: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/grow) + #[js_sys(js_name = "grow")] + pub fn grow64(self: &Memory, delta: u64) -> Result; +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#dom-memory-tofixedlengthbuffer) + #[must_use] + #[js_sys(js_name = "toFixedLengthBuffer")] + pub fn to_fixed_length_buffer(self: &Memory) -> JsValue; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#dom-memory-toresizablebuffer) + #[js_sys(js_name = "toResizableBuffer")] + pub fn to_resizable_buffer(self: &Memory) -> Result; +} diff --git a/client/js-sys/src/builtins/webassembly/mod.rs b/client/js-sys/src/builtins/webassembly/mod.rs new file mode 100644 index 00000000..a4f0d525 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/mod.rs @@ -0,0 +1,33 @@ +mod address; +mod error; +mod exception; +mod global; +mod instance; +mod jspi; +mod memory; +mod module; +mod namespace; +mod table; + +#[expect(non_snake_case, reason = "matches the JavaScript global name")] +pub mod WebAssembly { + pub use super::address::AddressType; + pub use super::error::{CompileError, LinkError, RuntimeError, SuspendError}; + pub use super::exception::{Exception, ExceptionOptions, Tag, TagDescriptor, TagType, js_tag}; + pub use super::global::{Global, GlobalDescriptor, ValueType}; + pub use super::instance::Instance; + pub use super::jspi::{Suspending, promising}; + pub use super::memory::{Memory, MemoryDescriptor}; + pub use super::module::{ + ImportExportKind, Module, ModuleExportDescriptor, ModuleImportDescriptor, + }; + pub use super::namespace::{ + CompileBuiltin, CompileOptions, InstantiatedSource, compile, compile_streaming, + compile_streaming_with_options, compile_with_options, instantiate_bytes, + instantiate_bytes_with_imports, instantiate_bytes_with_imports_and_options, + instantiate_module, instantiate_module_with_imports, instantiate_streaming, + instantiate_streaming_with_imports, instantiate_streaming_with_imports_and_options, + validate, validate_with_options, + }; + pub use super::table::{Table, TableDescriptor, TableElement}; +} diff --git a/client/js-sys/src/builtins/webassembly/module.rs b/client/js-sys/src/builtins/webassembly/module.rs new file mode 100644 index 00000000..632de7dd --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/module.rs @@ -0,0 +1,112 @@ +use alloc::string::String; + +use super::namespace::CompileOptions; +use crate::{Array, ArrayBuffer, JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/exports_static) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ImportExportKind { + Function, + Table, + Memory, + Global, + Tag, +} + +impl ImportExportKind { + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "function" => Some(Self::Function), + "table" => Some(Self::Table), + "memory" => Some(Self::Memory), + "global" => Some(Self::Global), + "tag" => Some(Self::Tag), + _ => None, + } + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/exports_static) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ModuleExportDescriptor; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/imports_static) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type ModuleImportDescriptor; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/exports_static) + #[must_use] + #[js_sys(getter)] + pub fn name(self: &ModuleExportDescriptor) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/imports_static) + #[must_use] + #[js_sys(getter)] + pub fn module(self: &ModuleImportDescriptor) -> JsString; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/imports_static) + #[must_use] + #[js_sys(getter)] + pub fn name(self: &ModuleImportDescriptor) -> JsString; +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Module; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/Module) + #[js_sys(constructor)] + pub fn new(bytes: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/Module) + #[js_sys(constructor)] + pub fn new_with_options(bytes: &JsValue, options: &CompileOptions) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/customSections_static) + #[js_sys(static_of = Module, js_name = "customSections")] + pub fn custom_sections( + module: &Module, + section_name: &str, + ) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/exports_static) + #[js_sys(static_of = Module)] + pub fn exports(module: &Module) -> Result, JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/imports_static) + #[js_sys(static_of = Module)] + pub fn imports(module: &Module) -> Result, JsValue>; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "kind")] + fn export_kind_raw(self: &ModuleExportDescriptor) -> JsString; + + #[js_sys(getter = "kind")] + fn import_kind_raw(self: &ModuleImportDescriptor) -> JsString; +} + +impl ModuleExportDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/exports_static) + #[must_use] + pub fn kind(&self) -> Option { + ImportExportKind::from_js_string(&self.export_kind_raw()) + } +} + +impl ModuleImportDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/imports_static) + #[must_use] + pub fn kind(&self) -> Option { + ImportExportKind::from_js_string(&self.import_kind_raw()) + } +} diff --git a/client/js-sys/src/builtins/webassembly/namespace.rs b/client/js-sys/src/builtins/webassembly/namespace.rs new file mode 100644 index 00000000..17185755 --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/namespace.rs @@ -0,0 +1,188 @@ +use alloc::string::String; +use alloc::vec::Vec; + +use super::instance::Instance; +use super::module::Module; +use crate::hazard::JsCast; +use crate::{Array, JsString, JsValue, Object, Promise, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum CompileBuiltin { + JsString, +} + +impl CompileBuiltin { + const fn as_str(self) -> &'static str { + match self { + Self::JsString => "js-string", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "js-string" => Some(Self::JsString), + _ => None, + } + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type CompileOptions; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type InstantiatedSource; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[must_use] + #[js_sys(getter)] + pub fn module(self: &InstantiatedSource) -> Module; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[must_use] + #[js_sys(getter)] + pub fn instance(self: &InstantiatedSource) -> Instance; +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + pub fn compile(bytes: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + #[js_sys(js_name = "compile")] + pub fn compile_with_options(bytes: &JsValue, options: &CompileOptions) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compileStreaming_static) + #[js_sys(js_name = "compileStreaming")] + pub fn compile_streaming(source: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compileStreaming_static) + #[js_sys(js_name = "compileStreaming")] + pub fn compile_streaming_with_options( + source: &JsValue, + options: &CompileOptions, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[js_sys(js_name = "instantiate")] + pub fn instantiate_bytes(bytes: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[js_sys(js_name = "instantiate")] + pub fn instantiate_bytes_with_imports( + bytes: &JsValue, + imports: &Object, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[js_sys(js_name = "instantiate")] + pub fn instantiate_bytes_with_imports_and_options( + bytes: &JsValue, + imports: &Object, + options: &CompileOptions, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[js_sys(js_name = "instantiate")] + pub fn instantiate_module(module: &Module) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static) + #[js_sys(js_name = "instantiate")] + pub fn instantiate_module_with_imports(module: &Module, imports: &Object) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiateStreaming_static) + #[js_sys(js_name = "instantiateStreaming")] + pub fn instantiate_streaming(source: &JsValue) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiateStreaming_static) + #[js_sys(js_name = "instantiateStreaming")] + pub fn instantiate_streaming_with_imports( + source: &JsValue, + imports: &Object, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiateStreaming_static) + #[js_sys(js_name = "instantiateStreaming")] + pub fn instantiate_streaming_with_imports_and_options( + source: &JsValue, + imports: &Object, + options: &CompileOptions, + ) -> Promise; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/validate_static) + pub fn validate(bytes: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/validate_static) + #[js_sys(js_name = "validate")] + pub fn validate_with_options( + bytes: &JsValue, + options: &CompileOptions, + ) -> Result; +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(getter = "builtins")] + pub(crate) fn builtins_raw(self: &CompileOptions) -> Option>; + + #[js_sys(setter = "builtins")] + pub(crate) fn set_builtins_raw(self: &CompileOptions, value: &Array); + + #[js_sys(getter = "importedStringConstants")] + fn imported_string_constants_raw(self: &CompileOptions) -> Option; + + #[js_sys(setter = "importedStringConstants")] + fn set_imported_string_constants_raw(self: &CompileOptions, value: &str); +} + +impl CompileOptions { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + #[must_use] + pub fn builtins(&self) -> Option> { + self.builtins_raw()? + .iter() + .map(|value| CompileBuiltin::from_js_string(&value)) + .collect() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + pub fn set_builtins(&self, values: &[CompileBuiltin]) { + let builtins: Array = Array::new_typed(); + for value in values { + let value = JsString::from(value.as_str()); + let _ = builtins.push(&value); + } + self.set_builtins_raw(&builtins); + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + #[must_use] + pub fn new() -> Self { + Self::unchecked_from(Object::new().into()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + #[must_use] + pub fn imported_string_constants(&self) -> Option { + self.imported_string_constants_raw() + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/compile_static) + pub fn set_imported_string_constants(&self, value: &str) { + self.set_imported_string_constants_raw(value); + } +} + +impl Default for CompileOptions { + fn default() -> Self { + Self::new() + } +} diff --git a/client/js-sys/src/builtins/webassembly/table.rs b/client/js-sys/src/builtins/webassembly/table.rs new file mode 100644 index 00000000..7730d46f --- /dev/null +++ b/client/js-sys/src/builtins/webassembly/table.rs @@ -0,0 +1,192 @@ +use alloc::string::String; + +use super::address::AddressType; +use crate::hazard::JsCast; +use crate::{JsString, JsValue, Object, js_sys}; + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table#element) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum TableElement { + AnyFunc, + ExternRef, +} + +impl TableElement { + const fn as_str(self) -> &'static str { + match self { + Self::AnyFunc => "anyfunc", + Self::ExternRef => "externref", + } + } + + fn from_js_string(value: &JsString) -> Option { + match String::from(value).as_str() { + "anyfunc" => Some(Self::AnyFunc), + "externref" => Some(Self::ExternRef), + _ => None, + } + } +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table) + #[js_sys(extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type TableDescriptor; + + #[js_sys(getter = "element")] + fn element_raw(self: &TableDescriptor) -> JsString; + + #[js_sys(setter = "element")] + fn set_element_raw(self: &TableDescriptor, value: &str); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table#initial) + #[must_use] + #[js_sys(getter = "initial")] + pub fn initial(self: &TableDescriptor) -> JsValue; + + #[js_sys(setter = "initial")] + fn set_initial32(self: &TableDescriptor, value: u32); + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table#maximum) + #[must_use] + #[js_sys(getter = "maximum")] + pub fn maximum(self: &TableDescriptor) -> Option; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table#maximum) + #[js_sys(setter = "maximum")] + pub fn set_maximum(self: &TableDescriptor, value: u32); + +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(setter = "initial")] + fn set_initial64(self: &TableDescriptor, value: u64); + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(setter = "maximum")] + pub fn set_maximum64(self: &TableDescriptor, value: u64); + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(getter = "address")] + fn address_raw(self: &TableDescriptor) -> Option; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(setter = "address")] + fn set_address_raw(self: &TableDescriptor, value: &str); +} + +impl TableDescriptor { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table) + #[must_use] + pub fn new(element: TableElement, initial: u32) -> Self { + let descriptor = Self::unchecked_from(Object::new().into()); + descriptor.set_element(element); + descriptor.set_initial32(initial); + descriptor + } + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[must_use] + pub fn new64(element: TableElement, initial: u64) -> Self { + let descriptor = Self::unchecked_from(Object::new().into()); + descriptor.set_element(element); + descriptor.set_address(AddressType::I64); + descriptor.set_initial64(initial); + descriptor + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table#element) + #[must_use] + pub fn element(&self) -> Option { + TableElement::from_js_string(&self.element_raw()) + } + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table#element) + pub fn set_element(&self, value: TableElement) { + self.set_element_raw(value.as_str()); + } + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[must_use] + pub fn address(&self) -> Option { + self.address_raw() + .as_ref() + .and_then(AddressType::from_js_string) + } + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + pub fn set_address(&self, value: AddressType) { + self.set_address_raw(value.as_str()); + } +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table) + #[js_sys(js_name = "Table", extends = Object)] + #[derive(Clone, Debug, PartialEq)] + pub type Table; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table) + #[js_sys(constructor)] + pub fn new(descriptor: &TableDescriptor) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table) + #[js_sys(constructor)] + pub fn new_with_value(descriptor: &TableDescriptor, value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/length) + #[must_use] + #[js_sys(getter)] + pub fn length(self: &Table) -> u32; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/get) + pub fn get(self: &Table, index: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/grow) + pub fn grow(self: &Table, delta: u32) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/grow) + #[js_sys(js_name = "grow")] + pub fn grow_with_value(self: &Table, delta: u32, value: &JsValue) -> Result; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/set) + pub fn set(self: &Table, index: u32) -> Result<(), JsValue>; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/set) + #[js_sys(js_name = "set")] + pub fn set_with_value(self: &Table, index: u32, value: &JsValue) -> Result<(), JsValue>; +} + +#[js_sys(js_sys = crate, namespace = "WebAssembly")] +extern "js-sys" { + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[must_use] + #[js_sys(getter = "length")] + pub fn length64(self: &Table) -> u64; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(js_name = "get")] + pub fn get64(self: &Table, index: u64) -> Result; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(js_name = "grow")] + pub fn grow64(self: &Table, delta: u64) -> Result; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(js_name = "grow")] + pub fn grow64_with_value(self: &Table, delta: u64, value: &JsValue) -> Result; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(js_name = "set")] + pub fn set64(self: &Table, index: u64) -> Result<(), JsValue>; + + /// [`WebAssembly` JavaScript interface](https://webassembly.github.io/spec/js-api/#tables) + #[js_sys(js_name = "set")] + pub fn set64_with_value(self: &Table, index: u64, value: &JsValue) -> Result<(), JsValue>; +} diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 6515caab..6cc5c773 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -41,8 +41,18 @@ impl IntoJsConv { #[derive(Clone, Copy)] pub struct FromJsConv { pub(crate) embed: Option<(&'static str, &'static str)>, + pub(crate) prepare: Option<&'static str>, pub(crate) templates: [&'static str; 4], - pub(crate) sret: Option<&'static str>, + pub(crate) sret: Option, +} + +/// Selects how an indirect JavaScript import result is written to Rust. +#[derive(Clone, Copy)] +pub enum Sret { + /// Converts the JavaScript value through the common slot templates first. + Slots(&'static str), + /// Passes the original JavaScript value directly to the writer. + Value(&'static str), } impl FromJsConv { @@ -51,11 +61,22 @@ impl FromJsConv { pub const fn slot1(template: &'static str) -> Self { Self { embed: None, + prepare: None, templates: [template, "", "", ""], sret: None, } } + /// Computes a value once before expanding the individual slot templates. + /// + /// The template receives the JavaScript argument as `$value`; slot + /// templates can refer to its result as `$prepared`. + #[must_use] + pub const fn prepare(mut self, template: &'static str) -> Self { + self.prepare = Some(template); + self + } + #[must_use] pub const fn slot2(mut self, template: &'static str) -> Self { self.templates[1] = template; @@ -74,13 +95,10 @@ impl FromJsConv { self } - /// Stores the slots in an indirect return area. - /// - /// The function receives every non-empty slot in order, followed by the - /// indirect return pointer. + /// Configures how an indirect JavaScript import result is written to Rust. #[must_use] - pub const fn sret(mut self, function: &'static str) -> Self { - self.sret = Some(function); + pub const fn sret(mut self, sret: Sret) -> Self { + self.sret = Some(sret); self } diff --git a/client/js-sys/src/interop/js/array.rs b/client/js-sys/src/interop/js/array.rs new file mode 100644 index 00000000..215e734f --- /dev/null +++ b/client/js-sys/src/interop/js/array.rs @@ -0,0 +1,570 @@ +use core::error::Error; +use core::fmt::{self, Display, Formatter}; +use core::mem::MaybeUninit; +use core::ops::Range; +use core::ptr; + +use crate::JsValue; +use crate::builtins::Array; +use crate::hazard::JsCast; +use crate::interop::slice::array_from_js_value_slice; +use crate::runtime::externref; +use crate::util::{PtrConst, PtrLength, PtrMut}; + +macro_rules! primitive_arrays { + ( + $( + $(#[$attr:meta])* + $ty:ty { + encode = $encode:ident, + from_slice = $from_slice:ident, + embed = $embed:literal, + constructor = $constructor:literal, + view = $view:literal $(,)? + } + )* + ) => { + #[crate::js_sys(js_sys = crate)] + extern "js-sys" { + #[js_sys(js_embed = "array.checked_length")] + fn array_checked_length(array: &Array) -> Result; + + // SAFETY: Every pointer and length pair must describe its matching + // output slice. + #[js_sys(js_embed = "array.js_value.encode")] + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn array_js_value_encode( + array: &Array, + array_ptr: PtrMut, + array_len: PtrLength, + externref_ptr: PtrConst, + externref_len: PtrLength, + write_output: bool, + ) -> Result; + + $( + $(#[$attr])* + // SAFETY: The pointer and length must describe a valid output slice. + #[js_sys(js_embed = $embed)] + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn $encode( + array: &Array<$ty>, + ptr: PtrMut<$ty>, + len: PtrLength<$ty>, + ) -> Result; + )* + } + + $( + $(#[$attr])* + js_bindgen::embed_js!( + module = "js_sys", + name = $embed, + required_embeds = [("js_sys", concat!("view.set", $view))], + "(array, ptr, len) => {{", + " const rawLength = array.length", + " const length = rawLength >>> 0", + " if (rawLength !== length) throw new TypeError('invalid array length')", + " if (length !== len) return length", + "", + " const values = new {constructor}(len)", + " for (let index = 0; index < len; index++) {{", + " values[index] = array[index]", + " }}", + " this.#jsEmbed.js_sys['view.set{view}'](ptr, values, len)", + " return length", + "}}", + constructor = interpolate $constructor, + view = interpolate $view, + ); + + $(#[$attr])* + impl Array<$ty> { + pub fn to_slice(&self, slice: &mut [$ty]) -> Result<(), TryFromArrayError> { + // SAFETY: Parameters are correct. + let result = unsafe { + $encode(self, PtrMut::new(slice), PtrLength::new(slice)) + }; + + check_copy_length(result, slice.len()) + } + + pub fn to_uninit_slice<'slice>( + &self, + slice: &'slice mut [MaybeUninit<$ty>], + ) -> Result<&'slice mut [$ty], TryFromArrayError> { + // SAFETY: Parameters are correct. + let result = unsafe { + $encode( + self, + PtrMut::from_uninit_slice(slice), + PtrLength::from_uninit_slice(slice), + ) + }; + + check_copy_length(result, slice.len())?; + // SAFETY: The staging typed array was fully initialized before it + // was copied into `slice`. + Ok(unsafe { assume_init_mut(slice) }) + } + + pub fn to_array(&self) -> Result<[$ty; N], TryFromArrayError> { + let mut array: MaybeUninit<[$ty; N]> = MaybeUninit::uninit(); + + // SAFETY: Parameters are correct. + let result = unsafe { + $encode( + self, + PtrMut::from_uninit_array(&mut array), + PtrLength::from_uninit_array(&array), + ) + }; + + check_copy_length(result, N)?; + // SAFETY: The staging typed array was fully initialized before it + // was copied into `array`. + Ok(unsafe { array.assume_init() }) + } + } + + $(#[$attr])* + impl From<&[$ty]> for Array<$ty> { + fn from(value: &[$ty]) -> Self { + crate::interop::slice::$from_slice(value) + } + } + )* + }; +} + +primitive_arrays! { + i8 { + encode = array_i8_encode, + from_slice = array_from_i8_slice, + embed = "array.i8.encode", + constructor = "Int8Array", + view = "Int8", + } + u8 { + encode = array_u8_encode, + from_slice = array_from_u8_slice, + embed = "array.u8.encode", + constructor = "Uint8Array", + view = "Uint8", + } + i16 { + encode = array_i16_encode, + from_slice = array_from_i16_slice, + embed = "array.i16.encode", + constructor = "Int16Array", + view = "Int16", + } + u16 { + encode = array_u16_encode, + from_slice = array_from_u16_slice, + embed = "array.u16.encode", + constructor = "Uint16Array", + view = "Uint16", + } + i32 { + encode = array_i32_encode, + from_slice = array_from_i32_slice, + embed = "array.i32.encode", + constructor = "Int32Array", + view = "Int32", + } + u32 { + encode = array_u32_encode, + from_slice = array_from_u32_slice, + embed = "array.u32.encode", + constructor = "Uint32Array", + view = "Uint32", + } + i64 { + encode = array_i64_encode, + from_slice = array_from_i64_slice, + embed = "array.i64.encode", + constructor = "BigInt64Array", + view = "BigInt64", + } + u64 { + encode = array_u64_encode, + from_slice = array_from_u64_slice, + embed = "array.u64.encode", + constructor = "BigUint64Array", + view = "BigUint64", + } + f32 { + encode = array_f32_encode, + from_slice = array_from_f32_slice, + embed = "array.f32.encode", + constructor = "Float32Array", + view = "Float32", + } + f64 { + encode = array_f64_encode, + from_slice = array_from_f64_slice, + embed = "array.f64.encode", + constructor = "Float64Array", + view = "Float64", + } + #[cfg(target_arch = "wasm32")] + isize { + encode = array_isize32_encode, + from_slice = array_from_isize_slice, + embed = "array.isize.encode", + constructor = "Int32Array", + view = "Int32", + } + #[cfg(target_arch = "wasm64")] + isize { + encode = array_isize64_encode, + from_slice = array_from_isize_slice, + embed = "array.isize.encode", + constructor = "BigInt64Array", + view = "BigInt64", + } + #[cfg(target_arch = "wasm32")] + usize { + encode = array_usize32_encode, + from_slice = array_from_usize_slice, + embed = "array.usize.encode", + constructor = "Uint32Array", + view = "Uint32", + } + #[cfg(target_arch = "wasm64")] + usize { + encode = array_usize64_encode, + from_slice = array_from_usize_slice, + embed = "array.usize.encode", + constructor = "BigUint64Array", + view = "BigUint64", + } +} + +impl Array { + #[must_use] + pub fn iter(&self) -> ArrayIter<'_, T> { + ArrayIter { + range: 0..self.length(), + array: self, + } + } +} + +/// A borrowed Rust iterator over an [`Array`]. +pub struct ArrayIter<'array, T = JsValue> { + array: &'array Array, + range: Range, +} + +impl core::iter::Iterator for ArrayIter<'_, T> { + type Item = T; + + fn next(&mut self) -> Option { + self.range + .next() + .map(|index| self.array.get_unchecked(index)) + } + + fn nth(&mut self, n: usize) -> Option { + self.range + .nth(n) + .map(|index| self.array.get_unchecked(index)) + } + + fn count(self) -> usize { + self.range.count() + } + + fn last(self) -> Option { + self.range + .last() + .map(|index| self.array.get_unchecked(index)) + } + + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +impl DoubleEndedIterator for ArrayIter<'_, T> { + fn next_back(&mut self) -> Option { + self.range + .next_back() + .map(|index| self.array.get_unchecked(index)) + } + + fn nth_back(&mut self, n: usize) -> Option { + self.range + .nth_back(n) + .map(|index| self.array.get_unchecked(index)) + } +} + +impl ExactSizeIterator for ArrayIter<'_, T> {} +impl core::iter::FusedIterator for ArrayIter<'_, T> {} + +/// An owned Rust iterator over an [`Array`]. +pub struct ArrayIntoIter { + array: Array, + range: Range, +} + +impl core::iter::Iterator for ArrayIntoIter { + type Item = T; + + fn next(&mut self) -> Option { + self.range + .next() + .map(|index| self.array.get_unchecked(index)) + } + + fn nth(&mut self, n: usize) -> Option { + self.range + .nth(n) + .map(|index| self.array.get_unchecked(index)) + } + + fn count(self) -> usize { + self.range.count() + } + + fn last(self) -> Option { + self.range + .last() + .map(|index| self.array.get_unchecked(index)) + } + + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +impl DoubleEndedIterator for ArrayIntoIter { + fn next_back(&mut self) -> Option { + self.range + .next_back() + .map(|index| self.array.get_unchecked(index)) + } + + fn nth_back(&mut self, n: usize) -> Option { + self.range + .nth_back(n) + .map(|index| self.array.get_unchecked(index)) + } +} + +impl ExactSizeIterator for ArrayIntoIter {} +impl core::iter::FusedIterator for ArrayIntoIter {} + +impl<'array, T: JsCast> IntoIterator for &'array Array { + type Item = T; + type IntoIter = ArrayIter<'array, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for Array { + type Item = T; + type IntoIter = ArrayIntoIter; + + fn into_iter(self) -> Self::IntoIter { + let range = 0..self.length(); + ArrayIntoIter { array: self, range } + } +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "array.checked_length", + "(array) => {{", + " const rawLength = array.length", + " const length = rawLength >>> 0", + " if (rawLength !== length) throw new TypeError('invalid array length')", + " return length", + "}}", +); + +impl From<&[T; N]> for Array +where + Self: for<'a> From<&'a [T]>, +{ + fn from(value: &[T; N]) -> Self { + value.as_slice().into() + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum TryFromArrayError { + LengthMismatch { actual: u32, expected: usize }, + JavaScript(JsValue), +} + +impl Display for TryFromArrayError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::LengthMismatch { actual, expected } => { + write!( + f, + "array length {actual} does not match destination length {expected}" + ) + } + Self::JavaScript(_) => f.write_str("JavaScript threw while copying the array"), + } + } +} + +impl Error for TryFromArrayError {} + +fn check_copy_length( + result: Result, + expected: usize, +) -> Result<(), TryFromArrayError> { + let actual = result.map_err(TryFromArrayError::JavaScript)?; + + if usize::try_from(actual) == Ok(expected) { + Ok(()) + } else { + Err(TryFromArrayError::LengthMismatch { actual, expected }) + } +} + +impl Array { + pub fn to_slice(&self, slice: &mut [T]) -> Result<(), TryFromArrayError> { + check_copy_length(array_checked_length(self.as_untyped()), slice.len())?; + let slots = externref::reserve_slots(slice.len()); + + let result = { + let js_slice = JsValue::from_slice_mut(slice); + // SAFETY: Parameters are correct. `write_output` is false, so JavaScript + // does not write through the destination pointer. + unsafe { + array_js_value_encode( + self.as_untyped(), + PtrMut::new(js_slice), + PtrLength::new(js_slice), + slots.ptr(), + slots.len(), + false, + ) + } + }; + + check_copy_length(result, slice.len())?; + slots.replace(slice); + Ok(()) + } + + pub fn to_uninit_slice<'slice>( + &self, + slice: &'slice mut [MaybeUninit], + ) -> Result<&'slice mut [T], TryFromArrayError> { + check_copy_length(array_checked_length(self.as_untyped()), slice.len())?; + let js_slice = JsValue::from_uninit_slice_mut(slice); + let slots = externref::reserve_slots(js_slice.len()); + + // SAFETY: Parameters are correct. + let result = unsafe { + array_js_value_encode( + self.as_untyped(), + PtrMut::from_uninit_slice(js_slice), + PtrLength::from_uninit_slice(js_slice), + slots.ptr(), + slots.len(), + true, + ) + }; + + check_copy_length(result, slice.len())?; + slots.commit(); + // SAFETY: Correctly initialized in JS. + Ok(unsafe { assume_init_mut(slice) }) + } + + pub fn to_array(&self) -> Result<[T; N], TryFromArrayError> { + check_copy_length(array_checked_length(self.as_untyped()), N)?; + let mut array: MaybeUninit<[T; N]> = MaybeUninit::uninit(); + let slots = externref::reserve_slots(N); + let js_array = JsValue::from_mut_uninit_array(&mut array); + + // SAFETY: Parameters are correct. + let result = unsafe { + array_js_value_encode( + self.as_untyped(), + PtrMut::from_uninit_array(js_array), + PtrLength::from_uninit_array(js_array), + slots.ptr(), + slots.len(), + true, + ) + }; + + check_copy_length(result, N)?; + slots.commit(); + // SAFETY: Correctly initialized in JS. + Ok(unsafe { array.assume_init() }) + } +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "array.js_value.encode", + required_embeds = [ + ("js_sys", "externref.table"), + ("js_sys", "view.getUint32"), + ("js_sys", "view.setUint32") + ], + "(array, arrPtr, arrLen, refPtr, refLen, writeOutput) => {{", + " const rawLength = array.length", + " const length = rawLength >>> 0", + " if (rawLength !== length) throw new TypeError('invalid array length')", + " if (length !== arrLen) return length", + "", + " const table = this.#jsEmbed.js_sys['externref.table']", + " const refIndices = new Uint32Array(", + " this.#jsEmbed.js_sys['view.getUint32'](refPtr, refLen),", + " )", + " if (writeOutput) {{", + " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", + " const elemIndex = refIndices[arrayIndex]", + " table.set(elemIndex, array[arrayIndex])", + " }}", + " this.#jsEmbed.js_sys['view.setUint32'](arrPtr, refIndices, arrLen)", + " }} else {{", + " for (let arrayIndex = 0; arrayIndex < arrLen; arrayIndex++) {{", + " table.set(refIndices[arrayIndex], array[arrayIndex])", + " }}", + " }}", + " return length", + "}}", +); + +impl From<&[T]> for Array { + fn from(value: &[T]) -> Self { + array_from_js_value_slice(value) + } +} + +// MSRV: Stable on v1.93. +const unsafe fn assume_init_mut(slice: &mut [MaybeUninit]) -> &mut [T] { + // SAFETY: copied from Std. + unsafe { &mut *(ptr::from_mut::<[MaybeUninit]>(slice) as *mut [T]) } +} diff --git a/client/js-sys/src/interop/js/async_iterator.rs b/client/js-sys/src/interop/js/async_iterator.rs new file mode 100644 index 00000000..46aec7c4 --- /dev/null +++ b/client/js-sys/src/interop/js/async_iterator.rs @@ -0,0 +1,126 @@ +use core::future::{Future, poll_fn}; +use core::pin::Pin; +use core::task::{Context, Poll}; + +use super::iterator::{CapturedNext, async_iterator_next_cached, read_result}; +use crate::builtins::iterator::async_iterator_from; +use crate::hazard::JsCast; +use crate::runtime::JsFuture; +use crate::{AsyncIterator, IteratorResult, JsValue}; + +/// A cancellation-safe asynchronous Rust iterator over the JavaScript `async` +/// iterator protocol. +pub struct AsyncIter { + iterator: AsyncIterator, + next_method: CapturedNext, + next: Option>, + done: bool, +} + +impl From> for AsyncIter { + fn from(iterator: AsyncIterator) -> Self { + Self::new(iterator) + } +} + +impl AsyncIter { + fn new(iterator: AsyncIterator) -> Self { + let next_method = CapturedNext::new(iterator.as_ref()); + + Self { + iterator, + next_method, + next: None, + done: false, + } + } + + fn try_new(iterator: AsyncIterator) -> Result { + let next_method = CapturedNext::try_new(iterator.as_ref())?; + + Ok(Self { + iterator, + next_method, + next: None, + done: false, + }) + } +} + +impl AsyncIter { + /// Polls the next item without issuing concurrent JavaScript `next()` + /// calls. + pub fn poll_next(&mut self, context: &mut Context<'_>) -> Poll>> { + if self.done { + return Poll::Ready(None); + } + + if self.next.is_none() { + let method = match self.next_method.method() { + Ok(method) => method, + Err(error) => { + self.done = true; + return Poll::Ready(Some(Err(error))); + } + }; + let promise = match async_iterator_next_cached(&self.iterator, method) { + Ok(promise) => promise, + Err(error) => { + self.done = true; + return Poll::Ready(Some(Err(error))); + } + }; + self.next = Some(promise.into()); + } + + let Some(future) = self.next.as_mut() else { + unreachable!(); + }; + let result = match Pin::new(future).poll(context) { + Poll::Pending => return Poll::Pending, + Poll::Ready(result) => result, + }; + self.next = None; + + let result = match result { + Ok(result) => result, + Err(error) => { + self.done = true; + return Poll::Ready(Some(Err(error))); + } + }; + + Poll::Ready(read_result(&result, &mut self.done)) + } + + /// Waits for the next item. + /// + /// Dropping this future keeps an in-flight JavaScript `next()` operation in + /// the iterator, so the following call resumes it instead of losing an + /// item. + #[must_use = "futures do nothing unless polled or awaited"] + #[expect( + clippy::should_implement_trait, + reason = "there is no standard asynchronous Iterator trait" + )] + pub fn next(&mut self) -> impl Future>> + '_ { + poll_fn(|context| self.poll_next(context)) + } +} + +impl AsyncIterator { + #[must_use] + pub fn into_async_iter(self) -> AsyncIter { + self.into() + } +} + +/// Returns an asynchronous Rust iterator for an asynchronous or synchronous +/// JavaScript `iterable`. +/// +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#description) +pub fn try_async_iter(value: &JsValue) -> Result, JsValue> { + async_iterator_from(value)? + .map(AsyncIter::try_new) + .transpose() +} diff --git a/client/js-sys/src/interop/js/iterator.rs b/client/js-sys/src/interop/js/iterator.rs new file mode 100644 index 00000000..288d86e8 --- /dev/null +++ b/client/js-sys/src/interop/js/iterator.rs @@ -0,0 +1,325 @@ +use crate::builtins::Intl::{SegmentData, Segments}; +use crate::builtins::iterator::iterator_from; +use crate::builtins::{ + Array, AsyncIterator, Function, IteratorResult, JsIterator, JsString, Map, Promise, Set, +}; +use crate::hazard::JsCast; +use crate::{JsValue, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "iterator.next.method")] + fn iterator_next_method(iterator: &JsValue) -> Result; + + #[js_sys(js_embed = "iterator.next.cached")] + fn iterator_next_cached( + #[js_sys(type = &JsValue)] iterator: &JsIterator, + next: &Function, + ) -> Result; + + #[js_sys(js_embed = "async_iterator.next.cached")] + pub(super) fn async_iterator_next_cached( + #[js_sys(type = &JsValue)] iterator: &AsyncIterator, + next: &Function, + ) -> Result, JsValue>; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.next.method", + "(iterator) => {{", + " const method = iterator.next", + " if (typeof method !== 'function')", + " throw new TypeError('iterator does not provide a next method')", + " return method", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "iterator.next.cached", + "(iterator, next) => {{", + " const result = next.call(iterator)", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('iterator next method returned a non-object value')", + " return result", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "async_iterator.next.cached", + "(iterator, next) => Promise.resolve(next.call(iterator)).then(result => {{", + " if (result == null || (typeof result !== 'object' && typeof result !== 'function'))", + " throw new TypeError('async iterator next method returned a non-object value')", + " return result", + "}})", +); + +pub(super) enum CapturedNext { + Method(Function), + Error(Option), +} + +impl CapturedNext { + pub(super) fn new(iterator: &JsValue) -> Self { + match iterator_next_method(iterator) { + Ok(method) => Self::Method(method), + Err(error) => Self::Error(Some(error)), + } + } + + pub(super) fn try_new(iterator: &JsValue) -> Result { + Ok(Self::Method(iterator_next_method(iterator)?)) + } + + pub(super) fn method(&mut self) -> Result<&Function, JsValue> { + match self { + Self::Method(method) => Ok(method), + Self::Error(error) => Err(error + .take() + .expect("captured iterator next error must only be observed once")), + } + } +} + +pub(super) fn read_result( + result: &IteratorResult, + done: &mut bool, +) -> Option> { + match result.done() { + Ok(true) => { + *done = true; + None + } + Ok(false) => { + let value = result.value().map(T::unchecked_from); + if value.is_err() { + *done = true; + } + Some(value) + } + Err(error) => { + *done = true; + Some(Err(error)) + } + } +} + +/// A borrowed Rust iterator over the JavaScript iterator protocol. +pub struct JsIter<'a, T = JsValue> { + iterator: &'a JsIterator, + next: CapturedNext, + done: bool, +} + +/// An owned Rust iterator over the JavaScript iterator protocol. +pub struct JsIntoIter { + iterator: JsIterator, + next: CapturedNext, + done: bool, +} + +fn next( + iterator: &JsIterator, + next: &mut CapturedNext, + done: &mut bool, +) -> Option> { + if *done { + return None; + } + + let method = match next.method() { + Ok(method) => method, + Err(error) => { + *done = true; + return Some(Err(error)); + } + }; + let result = match iterator_next_cached(iterator, method) { + Ok(result) => result, + Err(error) => { + *done = true; + return Some(Err(error)); + } + }; + + read_result(&result, done) +} + +impl JsIterator { + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) + #[must_use] + pub fn iter(&self) -> JsIter<'_, T> { + JsIter { + iterator: self, + next: CapturedNext::new(self.as_ref()), + done: false, + } + } +} + +impl<'a, T: JsCast> IntoIterator for &'a JsIterator { + type Item = Result; + type IntoIter = JsIter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl core::iter::Iterator for JsIter<'_, T> { + type Item = Result; + + fn next(&mut self) -> Option { + next(self.iterator, &mut self.next, &mut self.done) + } +} + +impl core::iter::FusedIterator for JsIter<'_, T> {} + +impl IntoIterator for JsIterator { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + JsIntoIter::new(self) + } +} + +impl JsIntoIter { + fn new(iterator: JsIterator) -> Self { + let next = CapturedNext::new(iterator.as_ref()); + + Self { + iterator, + next, + done: false, + } + } + + fn try_new(iterator: JsIterator) -> Result { + let next = CapturedNext::try_new(iterator.as_ref())?; + + Ok(Self { + iterator, + next, + done: false, + }) + } +} + +impl core::iter::Iterator for JsIntoIter { + type Item = Result; + + fn next(&mut self) -> Option { + next(&self.iterator, &mut self.next, &mut self.done) + } +} + +impl core::iter::FusedIterator for JsIntoIter {} + +/// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) +pub fn try_iter(value: &JsValue) -> Result, JsValue> { + iterator_from(value)?.map(JsIntoIter::try_new).transpose() +} + +impl JsString { + #[must_use] + pub fn iter(&self) -> JsIntoIter { + self.iterator().into_iter() + } +} + +impl IntoIterator for &JsString { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for JsString { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Map { + #[must_use] + pub fn iter(&self) -> JsIntoIter { + self.symbol_iterator().into_iter() + } +} + +impl IntoIterator for &Map { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for Map { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.symbol_iterator().into_iter() + } +} + +impl Set { + #[must_use] + pub fn iter(&self) -> JsIntoIter { + self.symbol_iterator().into_iter() + } +} + +impl IntoIterator for &Set { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for Set { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.symbol_iterator().into_iter() + } +} + +impl Segments { + #[must_use] + pub fn iter(&self) -> JsIntoIter { + self.symbol_iterator().into_iter() + } +} + +impl IntoIterator for &Segments { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for Segments { + type Item = Result; + type IntoIter = JsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.symbol_iterator().into_iter() + } +} diff --git a/client/js-sys/src/interop/js/mod.rs b/client/js-sys/src/interop/js/mod.rs new file mode 100644 index 00000000..566be159 --- /dev/null +++ b/client/js-sys/src/interop/js/mod.rs @@ -0,0 +1,10 @@ +mod array; +mod async_iterator; +mod iterator; +mod string; +mod typed_array; + +pub use array::{ArrayIntoIter, ArrayIter, TryFromArrayError}; +pub use async_iterator::{AsyncIter, try_async_iter}; +pub use iterator::{JsIntoIter, JsIter, try_iter}; +pub use typed_array::{TypedArray, TypedArrayCopyError, TypedArrayIntoIter, TypedArrayIter}; diff --git a/client/js-sys/src/interop/js/string.rs b/client/js-sys/src/interop/js/string.rs new file mode 100644 index 00000000..1e7e8ab7 --- /dev/null +++ b/client/js-sys/src/interop/js/string.rs @@ -0,0 +1,134 @@ +use alloc::string::String; +use core::convert::Infallible; +use core::fmt::{self, Display, Formatter}; +use core::str::FromStr; + +use crate::interop::string::js_string_from_str; +use crate::util::{PtrConst, PtrLength}; +use crate::{JsString, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "string.eq")] + // SAFETY: The pointer and length must describe a valid UTF-8 byte slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn string_eq(string: &JsString, array: PtrConst, len: PtrLength) -> bool; + + #[js_sys(js_embed = "string.identity")] + fn string_from_owned(value: String) -> JsString; + + #[js_sys(js_embed = "string.identity")] + fn string_to_owned(value: &JsString) -> String; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "string.identity", + "value => value" +); + +impl Display for JsString { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&String::from(self), formatter) + } +} + +impl fmt::Debug for JsString { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&String::from(self), formatter) + } +} + +impl PartialEq for JsString { + fn eq(&self, other: &str) -> bool { + js_bindgen::embed_js!( + module = "js_sys", + name = "string.eq", + required_embeds = [("js_sys", "string.decode")], + "(string, ptr, len) => {{", + " const other = this.#jsEmbed.js_sys['string.decode'](ptr, len)", + " return string === other", + "}}", + ); + + // SAFETY: Parameters are correct. + unsafe { + string_eq( + self, + PtrConst::new(other.as_bytes()), + PtrLength::new(other.as_bytes()), + ) + } + } +} + +impl PartialEq<&str> for JsString { + fn eq(&self, other: &&str) -> bool { + >::eq(self, other) + } +} + +impl PartialEq for JsString { + fn eq(&self, other: &String) -> bool { + >::eq(self, other) + } +} + +impl PartialEq<&String> for JsString { + fn eq(&self, other: &&String) -> bool { + >::eq(self, other) + } +} + +impl From<&str> for JsString { + fn from(value: &str) -> Self { + js_string_from_str(value) + } +} + +impl From<&JsString> for String { + fn from(value: &JsString) -> Self { + string_to_owned(value) + } +} + +impl From for String { + fn from(value: JsString) -> Self { + Self::from(&value) + } +} + +impl From for JsString { + fn from(value: String) -> Self { + string_from_owned(value) + } +} + +impl From for JsString { + fn from(value: char) -> Self { + let mut buffer = [0; 4]; + let value: &str = value.encode_utf8(&mut buffer); + Self::from(value) + } +} + +impl Default for JsString { + fn default() -> Self { + Self::from("") + } +} + +impl FromStr for JsString { + type Err = Infallible; + + fn from_str(value: &str) -> Result { + Ok(Self::from(value)) + } +} diff --git a/client/js-sys/src/interop/js/typed_array.rs b/client/js-sys/src/interop/js/typed_array.rs new file mode 100644 index 00000000..d9fb204c --- /dev/null +++ b/client/js-sys/src/interop/js/typed_array.rs @@ -0,0 +1,722 @@ +use alloc::vec::Vec; +use core::error::Error; +use core::fmt::{self, Display, Formatter}; +use core::mem::MaybeUninit; +use core::ops::Range; +use core::ptr; + +use crate::builtins::{ + BigInt64Array, BigUint64Array, Float16Array, Float32Array, Float64Array, Int8Array, Int16Array, + Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array, +}; +use crate::util::{PtrConst, PtrLength, PtrMut}; +use crate::{JsValue, js_sys}; + +/// A JavaScript typed-array type with a corresponding Rust storage element. +/// `Float16Array` uses `u16` to preserve raw `binary16` bits. +pub trait TypedArray: AsRef { + type Element: Copy; + type Value: Copy; + + const BYTES_PER_ELEMENT: u32; + + #[doc(hidden)] + fn typed_array_length(&self) -> usize; + + #[doc(hidden)] + fn typed_array_get(&self, index: usize) -> Option; +} + +/// A borrowed Rust iterator over a JavaScript typed array. +pub struct TypedArrayIter<'array, A: TypedArray> { + array: &'array A, + range: Range, +} + +fn typed_array_iter_next(array: &A, range: &mut Range) -> Option { + let index = range.next()?; + if let Some(value) = array.typed_array_get(index) { + Some(value) + } else { + *range = 0..0; + None + } +} + +fn typed_array_iter_nth_back( + array: &A, + range: &mut Range, + mut n: usize, +) -> Option { + for index in range.rev() { + if let Some(value) = array.typed_array_get(index) { + if n == 0 { + return Some(value); + } + n -= 1; + } + } + None +} + +impl core::iter::Iterator for TypedArrayIter<'_, A> { + type Item = A::Value; + + fn next(&mut self) -> Option { + typed_array_iter_next(self.array, &mut self.range) + } + + fn nth(&mut self, n: usize) -> Option { + let index = self.range.nth(n)?; + if let Some(value) = self.array.typed_array_get(index) { + Some(value) + } else { + self.range = 0..0; + None + } + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(self.range.len())) + } +} + +impl DoubleEndedIterator for TypedArrayIter<'_, A> { + fn next_back(&mut self) -> Option { + typed_array_iter_nth_back(self.array, &mut self.range, 0) + } + + fn nth_back(&mut self, n: usize) -> Option { + typed_array_iter_nth_back(self.array, &mut self.range, n) + } +} + +impl core::iter::FusedIterator for TypedArrayIter<'_, A> {} + +/// An owned Rust iterator over a JavaScript typed array. +pub struct TypedArrayIntoIter { + array: A, + range: Range, +} + +impl core::iter::Iterator for TypedArrayIntoIter { + type Item = A::Value; + + fn next(&mut self) -> Option { + typed_array_iter_next(&self.array, &mut self.range) + } + + fn nth(&mut self, n: usize) -> Option { + let index = self.range.nth(n)?; + if let Some(value) = self.array.typed_array_get(index) { + Some(value) + } else { + self.range = 0..0; + None + } + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(self.range.len())) + } +} + +impl DoubleEndedIterator for TypedArrayIntoIter { + fn next_back(&mut self) -> Option { + typed_array_iter_nth_back(&self.array, &mut self.range, 0) + } + + fn nth_back(&mut self, n: usize) -> Option { + typed_array_iter_nth_back(&self.array, &mut self.range, n) + } +} + +impl core::iter::FusedIterator for TypedArrayIntoIter {} + +#[derive(Debug)] +#[non_exhaustive] +pub enum TypedArrayCopyError { + LengthMismatch, + LengthOutOfRange, + JavaScript(JsValue), +} + +impl Display for TypedArrayCopyError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::LengthMismatch => { + formatter.write_str("typed array length does not match the Rust slice length") + } + Self::LengthOutOfRange => { + formatter.write_str("typed array length does not fit in a Rust `usize`") + } + Self::JavaScript(_) => { + formatter.write_str("JavaScript threw while copying the typed array") + } + } + } +} + +impl Error for TypedArrayCopyError {} + +fn check_copy(result: Result) -> Result<(), TypedArrayCopyError> { + match result { + Ok(true) => Ok(()), + Ok(false) => Err(TypedArrayCopyError::LengthMismatch), + Err(error) => Err(TypedArrayCopyError::JavaScript(error)), + } +} + +const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; + +#[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "the value is checked before converting it" +)] +fn length_to_usize(length: f64) -> Option { + #[cfg(target_arch = "wasm32")] + let maximum = f64::from(u32::MAX); + #[cfg(target_arch = "wasm64")] + let maximum = MAX_SAFE_INTEGER; + + (length >= 0.0 && length <= maximum && length % 1.0 == 0.0).then_some(length as usize) +} + +fn expect_usize_length(length: f64) -> usize { + length_to_usize(length).expect("typed array length does not fit in a Rust `usize`") +} + +#[expect( + clippy::cast_precision_loss, + reason = "typed-array indices are limited to JavaScript's exact integer range" +)] +fn index_to_number(index: usize) -> Option { + let number = index as f64; + (number <= MAX_SAFE_INTEGER).then_some(number) +} + +// The public `length()` binding intentionally follows normal JavaScript +// property lookup. Rust iteration and conversions describe the typed array's +// actual elements, so use the built-in `getter` just as native typed-array +// algorithms do. +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.length", + required_embeds = [("js_sys", "typed_array.intrinsics")], + "(array) => this.#jsEmbed.js_sys['typed_array.intrinsics'].length.call(array)", +); + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "typed_array.length")] + fn intrinsic_typed_array_length(array: &JsValue) -> f64; +} + +macro_rules! typed_array_traits { + ($name:ident: $element:ty => $value:ty, bytes = $bytes:literal, get = $get:ident) => { + impl $name { + #[must_use] + pub fn iter(&self) -> TypedArrayIter<'_, Self> { + TypedArrayIter { + array: self, + range: 0..expect_usize_length(intrinsic_typed_array_length(self.as_ref())), + } + } + } + + impl TypedArray for $name { + type Element = $element; + type Value = $value; + + const BYTES_PER_ELEMENT: u32 = $bytes; + + fn typed_array_length(&self) -> usize { + expect_usize_length(intrinsic_typed_array_length(self.as_ref())) + } + + fn typed_array_get(&self, index: usize) -> Option { + self.$get(index_to_number(index)?) + } + } + + impl<'array> IntoIterator for &'array $name { + type Item = <$name as TypedArray>::Value; + type IntoIter = TypedArrayIter<'array, $name>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } + } + + impl IntoIterator for $name { + type Item = <$name as TypedArray>::Value; + type IntoIter = TypedArrayIntoIter<$name>; + + fn into_iter(self) -> Self::IntoIter { + let range = 0..expect_usize_length(intrinsic_typed_array_length(self.as_ref())); + TypedArrayIntoIter { array: self, range } + } + } + }; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.intrinsics", + "(() => {{", + " const prototype = Object.getPrototypeOf(Uint8Array.prototype)", + " return {{", + " buffer: Object.getOwnPropertyDescriptor(prototype, 'buffer').get,", + " byteOffset: Object.getOwnPropertyDescriptor(prototype, 'byteOffset').get,", + " length: Object.getOwnPropertyDescriptor(prototype, 'length').get,", + " set: prototype.set,", + " }}", + "}})()", +); + +macro_rules! typed_array_interop { + ( + $name:ident : $element:ty, + constructor = $constructor:literal, + view = $view:literal, + bytes = $bytes:literal, + copy_to = $copy_to:ident, + copy_from = $copy_from:ident, + from_slice = $from_slice:ident, + copy_to_embed = $copy_to_embed:literal, + copy_from_embed = $copy_from_embed:literal, + from_slice_embed = $from_slice_embed:literal, + ) => { + #[js_sys(js_sys = crate)] + extern "js-sys" { + #[js_sys(js_embed = $copy_to_embed)] + unsafe fn $copy_to( + array: &$name, + ptr: PtrMut<$element>, + len: PtrLength<$element>, + ) -> Result; + + #[js_sys(js_embed = $copy_from_embed)] + unsafe fn $copy_from( + array: &$name, + ptr: PtrConst<$element>, + len: PtrLength<$element>, + ) -> Result; + + #[js_sys(js_embed = $from_slice_embed)] + unsafe fn $from_slice( + ptr: PtrConst<$element>, + len: PtrLength<$element>, + ) -> $name; + } + + impl $name { + pub fn copy_to( + &self, + destination: &mut [$element], + ) -> Result<(), TypedArrayCopyError> { + // SAFETY: The pointer and length describe `destination`. + let result = unsafe { + $copy_to( + self, + PtrMut::new(destination), + PtrLength::new(destination), + ) + }; + check_copy(result) + } + + pub fn copy_to_uninit<'destination>( + &self, + destination: &'destination mut [MaybeUninit<$element>], + ) -> Result<&'destination mut [$element], TypedArrayCopyError> { + // SAFETY: The pointer and length describe `destination`. + let result = unsafe { + $copy_to( + self, + PtrMut::from_uninit_slice(destination), + PtrLength::from_uninit_slice(destination), + ) + }; + check_copy(result)?; + // SAFETY: JavaScript initialized every element after checking the length. + Ok(unsafe { assume_init_mut(destination) }) + } + + pub fn copy_from( + &self, + source: &[$element], + ) -> Result<(), TypedArrayCopyError> { + // SAFETY: The pointer and length describe `source`. + let result = unsafe { + $copy_from(self, PtrConst::new(source), PtrLength::new(source)) + }; + check_copy(result) + } + + pub fn to_vec(&self) -> Result, TypedArrayCopyError> { + let len = length_to_usize(intrinsic_typed_array_length(self.as_ref())) + .ok_or(TypedArrayCopyError::LengthOutOfRange)?; + let mut output = Vec::with_capacity(len); + self.copy_to_uninit(&mut output.spare_capacity_mut()[..len])?; + // SAFETY: `copy_to_uninit` initialized all `len` elements. + unsafe { output.set_len(len) }; + Ok(output) + } + } + + impl From<&[$element]> for $name { + fn from(source: &[$element]) -> Self { + // SAFETY: The pointer and length describe `source`; the constructor + // copies it before returning. + unsafe { $from_slice(PtrConst::new(source), PtrLength::new(source)) } + } + } + + impl From<&[$element; N]> for $name { + fn from(source: &[$element; N]) -> Self { + Self::from(source.as_slice()) + } + } + + typed_array_traits!($name: $element => $element, bytes = $bytes, get = get); + + js_bindgen::embed_js!( + module = "js_sys", + name = $copy_to_embed, + required_embeds = [ + ("js_sys", "typed_array.intrinsics"), + ("js_sys", concat!("view.set", $view)) + ], + "(array, ptr, len) => {{", + " const intrinsics = this.#jsEmbed.js_sys['typed_array.intrinsics']", + " if (intrinsics.length.call(array) !== len) return false", + " this.#jsEmbed.js_sys['view.set{view}'](ptr, array, len)", + " return true", + "}}", + view = interpolate $view, + ); + + js_bindgen::embed_js!( + module = "js_sys", + name = $copy_from_embed, + required_embeds = [ + ("js_sys", "typed_array.intrinsics"), + ("js_sys", concat!("view.get", $view)) + ], + "(array, ptr, len) => {{", + " const intrinsics = this.#jsEmbed.js_sys['typed_array.intrinsics']", + " if (intrinsics.length.call(array) !== len) return false", + " intrinsics.set.call(", + " array, this.#jsEmbed.js_sys['view.get{view}'](ptr, len)", + " )", + " return true", + "}}", + view = interpolate $view, + ); + + js_bindgen::embed_js!( + module = "js_sys", + name = $from_slice_embed, + required_embeds = [("js_sys", concat!("view.get", $view))], + "(ptr, len) => new {constructor}(", + " this.#jsEmbed.js_sys['view.get{view}'](ptr, len)", + ")", + constructor = interpolate $constructor, + view = interpolate $view, + ); + }; +} + +typed_array_interop! { + Int8Array: i8, + constructor = "Int8Array", + view = "Int8", + bytes = 1, + copy_to = int8_array_copy_to, + copy_from = int8_array_copy_from, + from_slice = int8_array_from_slice, + copy_to_embed = "typed_array.Int8Array.copy_to", + copy_from_embed = "typed_array.Int8Array.copy_from", + from_slice_embed = "typed_array.Int8Array.from", +} + +typed_array_interop! { + Uint8Array: u8, + constructor = "Uint8Array", + view = "Uint8", + bytes = 1, + copy_to = uint8_array_copy_to, + copy_from = uint8_array_copy_from, + from_slice = uint8_array_from_slice, + copy_to_embed = "typed_array.Uint8Array.copy_to", + copy_from_embed = "typed_array.Uint8Array.copy_from", + from_slice_embed = "typed_array.Uint8Array.from", +} + +typed_array_interop! { + Uint8ClampedArray: u8, + constructor = "Uint8ClampedArray", + view = "Uint8", + bytes = 1, + copy_to = uint8_clamped_array_copy_to, + copy_from = uint8_clamped_array_copy_from, + from_slice = uint8_clamped_array_from_slice, + copy_to_embed = "typed_array.Uint8ClampedArray.copy_to", + copy_from_embed = "typed_array.Uint8ClampedArray.copy_from", + from_slice_embed = "typed_array.Uint8ClampedArray.from", +} + +typed_array_interop! { + Int16Array: i16, + constructor = "Int16Array", + view = "Int16", + bytes = 2, + copy_to = int16_array_copy_to, + copy_from = int16_array_copy_from, + from_slice = int16_array_from_slice, + copy_to_embed = "typed_array.Int16Array.copy_to", + copy_from_embed = "typed_array.Int16Array.copy_from", + from_slice_embed = "typed_array.Int16Array.from", +} + +typed_array_interop! { + Uint16Array: u16, + constructor = "Uint16Array", + view = "Uint16", + bytes = 2, + copy_to = uint16_array_copy_to, + copy_from = uint16_array_copy_from, + from_slice = uint16_array_from_slice, + copy_to_embed = "typed_array.Uint16Array.copy_to", + copy_from_embed = "typed_array.Uint16Array.copy_from", + from_slice_embed = "typed_array.Uint16Array.from", +} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "typed_array.Float16Array.copy_to_u16")] + unsafe fn float16_array_copy_to_u16( + array: &Float16Array, + ptr: PtrMut, + len: PtrLength, + ) -> Result; + + #[js_sys(js_embed = "typed_array.Float16Array.copy_from_u16")] + unsafe fn float16_array_copy_from_u16( + array: &Float16Array, + ptr: PtrConst, + len: PtrLength, + ) -> Result; + + #[js_sys(js_embed = "typed_array.Float16Array.from_u16")] + unsafe fn float16_array_from_u16( + ptr: PtrConst, + len: PtrLength, + ) -> Result; +} + +impl Float16Array { + /// Copies raw `IEEE 754 binary16` bit patterns into a new array. + pub fn new_from_u16_slice(source: &[u16]) -> Result { + // SAFETY: The pointer and length describe `source`; JavaScript copies it + // before returning. + unsafe { float16_array_from_u16(PtrConst::new(source), PtrLength::new(source)) } + } + + /// Copies the array's raw `IEEE 754 binary16` bit patterns into a slice. + pub fn copy_to_u16_slice(&self, destination: &mut [u16]) -> Result<(), TypedArrayCopyError> { + // SAFETY: The pointer and length describe `destination`. + let result = unsafe { + float16_array_copy_to_u16(self, PtrMut::new(destination), PtrLength::new(destination)) + }; + check_copy(result) + } + + /// Copies raw `IEEE 754 binary16` bit patterns into uninitialized storage. + pub fn copy_to_uninit_u16_slice<'destination>( + &self, + destination: &'destination mut [MaybeUninit], + ) -> Result<&'destination mut [u16], TypedArrayCopyError> { + // SAFETY: The pointer and length describe `destination`. + let result = unsafe { + float16_array_copy_to_u16( + self, + PtrMut::from_uninit_slice(destination), + PtrLength::from_uninit_slice(destination), + ) + }; + check_copy(result)?; + // SAFETY: JavaScript initialized every element after checking the length. + Ok(unsafe { assume_init_mut(destination) }) + } + + /// Copies raw `IEEE 754 binary16` bit patterns into the array. + pub fn copy_from_u16_slice(&self, source: &[u16]) -> Result<(), TypedArrayCopyError> { + // SAFETY: The pointer and length describe `source`. + let result = unsafe { + float16_array_copy_from_u16(self, PtrConst::new(source), PtrLength::new(source)) + }; + check_copy(result) + } + + /// Returns the array's raw `IEEE 754 binary16` bit patterns. + pub fn to_u16_vec(&self) -> Result, TypedArrayCopyError> { + let len = length_to_usize(intrinsic_typed_array_length(self.as_ref())) + .ok_or(TypedArrayCopyError::LengthOutOfRange)?; + let mut output = Vec::with_capacity(len); + self.copy_to_uninit_u16_slice(&mut output.spare_capacity_mut()[..len])?; + // SAFETY: `copy_to_uninit_u16_slice` initialized all `len` elements. + unsafe { output.set_len(len) }; + Ok(output) + } +} + +typed_array_traits!(Float16Array: u16 => f32, bytes = 2, get = get_as_f32); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.Float16Array.copy_to_u16", + required_embeds = [ + ("js_sys", "typed_array.intrinsics"), + ("js_sys", "view.setUint16") + ], + "(array, ptr, len) => {{", + " const intrinsics = this.#jsEmbed.js_sys['typed_array.intrinsics']", + " const length = intrinsics.length.call(array)", + " const bits = new Uint16Array(", + " intrinsics.buffer.call(array),", + " intrinsics.byteOffset.call(array),", + " length,", + " )", + " if (length !== len) return false", + " this.#jsEmbed.js_sys['view.setUint16'](ptr, bits, len)", + " return true", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.Float16Array.copy_from_u16", + required_embeds = [ + ("js_sys", "typed_array.intrinsics"), + ("js_sys", "view.getUint16") + ], + "(array, ptr, len) => {{", + " const intrinsics = this.#jsEmbed.js_sys['typed_array.intrinsics']", + " const length = intrinsics.length.call(array)", + " const bits = new Uint16Array(", + " intrinsics.buffer.call(array),", + " intrinsics.byteOffset.call(array),", + " length,", + " )", + " if (length !== len) return false", + " intrinsics.set.call(bits, this.#jsEmbed.js_sys['view.getUint16'](ptr, len))", + " return true", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "typed_array.Float16Array.from_u16", + required_embeds = [ + ("js_sys", "typed_array.intrinsics"), + ("js_sys", "view.getUint16") + ], + "(ptr, len) => {{", + " const array = new Float16Array(len)", + " const intrinsics = this.#jsEmbed.js_sys['typed_array.intrinsics']", + " const bits = new Uint16Array(", + " intrinsics.buffer.call(array),", + " intrinsics.byteOffset.call(array),", + " intrinsics.length.call(array),", + " )", + " intrinsics.set.call(bits, this.#jsEmbed.js_sys['view.getUint16'](ptr, len))", + " return array", + "}}", +); + +typed_array_interop! { + Int32Array: i32, + constructor = "Int32Array", + view = "Int32", + bytes = 4, + copy_to = int32_array_copy_to, + copy_from = int32_array_copy_from, + from_slice = int32_array_from_slice, + copy_to_embed = "typed_array.Int32Array.copy_to", + copy_from_embed = "typed_array.Int32Array.copy_from", + from_slice_embed = "typed_array.Int32Array.from", +} + +typed_array_interop! { + Uint32Array: u32, + constructor = "Uint32Array", + view = "Uint32", + bytes = 4, + copy_to = uint32_array_copy_to, + copy_from = uint32_array_copy_from, + from_slice = uint32_array_from_slice, + copy_to_embed = "typed_array.Uint32Array.copy_to", + copy_from_embed = "typed_array.Uint32Array.copy_from", + from_slice_embed = "typed_array.Uint32Array.from", +} + +typed_array_interop! { + Float32Array: f32, + constructor = "Float32Array", + view = "Float32", + bytes = 4, + copy_to = float32_array_copy_to, + copy_from = float32_array_copy_from, + from_slice = float32_array_from_slice, + copy_to_embed = "typed_array.Float32Array.copy_to", + copy_from_embed = "typed_array.Float32Array.copy_from", + from_slice_embed = "typed_array.Float32Array.from", +} + +typed_array_interop! { + Float64Array: f64, + constructor = "Float64Array", + view = "Float64", + bytes = 8, + copy_to = float64_array_copy_to, + copy_from = float64_array_copy_from, + from_slice = float64_array_from_slice, + copy_to_embed = "typed_array.Float64Array.copy_to", + copy_from_embed = "typed_array.Float64Array.copy_from", + from_slice_embed = "typed_array.Float64Array.from", +} + +typed_array_interop! { + BigInt64Array: i64, + constructor = "BigInt64Array", + view = "BigInt64", + bytes = 8, + copy_to = big_int64_array_copy_to, + copy_from = big_int64_array_copy_from, + from_slice = big_int64_array_from_slice, + copy_to_embed = "typed_array.BigInt64Array.copy_to", + copy_from_embed = "typed_array.BigInt64Array.copy_from", + from_slice_embed = "typed_array.BigInt64Array.from", +} + +typed_array_interop! { + BigUint64Array: u64, + constructor = "BigUint64Array", + view = "BigUint64", + bytes = 8, + copy_to = big_uint64_array_copy_to, + copy_from = big_uint64_array_copy_from, + from_slice = big_uint64_array_from_slice, + copy_to_embed = "typed_array.BigUint64Array.copy_to", + copy_from_embed = "typed_array.BigUint64Array.copy_from", + from_slice_embed = "typed_array.BigUint64Array.from", +} + +// MSRV: Stable on v1.93. +const unsafe fn assume_init_mut(slice: &mut [MaybeUninit]) -> &mut [T] { + // SAFETY: copied from Std. + unsafe { &mut *(ptr::from_mut::<[MaybeUninit]>(slice) as *mut [T]) } +} diff --git a/client/js-sys/src/interop/mod.rs b/client/js-sys/src/interop/mod.rs index 4e8d1f32..85cfcffc 100644 --- a/client/js-sys/src/interop/mod.rs +++ b/client/js-sys/src/interop/mod.rs @@ -1,2 +1,10 @@ +mod js; mod primitive; +mod slice; mod string; +mod vec; + +pub use js::{ + ArrayIntoIter, ArrayIter, AsyncIter, JsIntoIter, JsIter, TryFromArrayError, TypedArray, + TypedArrayCopyError, TypedArrayIntoIter, TypedArrayIter, try_async_iter, try_iter, +}; diff --git a/client/js-sys/src/interop/primitive.rs b/client/js-sys/src/interop/primitive.rs index ef798fa4..f8f2fdea 100644 --- a/client/js-sys/src/interop/primitive.rs +++ b/client/js-sys/src/interop/primitive.rs @@ -1,6 +1,6 @@ use crate::hazard::{ EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionFromAbi, OptionIntoAbi, ReturnAbi, - ReturnMode, Slot, WasmAbi, + ReturnMode, Slot, Sret, WasmAbi, }; use crate::r#macro::const_concat; @@ -52,11 +52,11 @@ macro_rules! sentinel_option { carrier: $carrier:ty, sentinel: $sentinel:expr, js_sentinel: $js_sentinel:literal, - types: [$([$($ty:ident),+ $(,)?] => { - to_js: $to_js:literal, - from_js: $from_js:literal, - }),+ $(,)?], - ) => {$($( + into_carrier: $into_carrier:ident, + to_js: $to_js:literal, + from_js: $from_js:literal, + types: [$($ty:ident),+ $(,)?], + ) => {$( // SAFETY: The sentinel lies outside the value range of this type. unsafe impl OptionIntoAbi<$ty> for $ty { const JS_CONV: Option = Some(IntoJsConv::new(const_concat!( @@ -70,7 +70,7 @@ macro_rules! sentinel_option { fn into_option_abi(value: Option<$ty>) -> Self::Abi { value.map_or($sentinel, |value| { - sentinel_option!(@into_abi value, $ty, $carrier) + sentinel_option!(@into_carrier $into_carrier, value, $carrier) }) } } @@ -88,7 +88,7 @@ macro_rules! sentinel_option { #[expect( clippy::allow_attributes, - reason = "the generic expansion covers both signed and unsigned carriers" + reason = "one macro body covers carriers with different cast lints" )] #[allow( clippy::cast_possible_truncation, @@ -99,50 +99,29 @@ macro_rules! sentinel_option { if raw == $sentinel { None } else { - Some(sentinel_option!(@from_abi raw, $ty)) + Some(raw as $ty) } } } - )+)+}; - (@into_abi $value:ident, bool, $carrier:ty) => { + )+}; + (@into_carrier widen, $value:ident, $carrier:ty) => { <$carrier>::from($value) }; - (@into_abi $value:ident, usize, $carrier:ty) => { - { - #[expect( - clippy::cast_precision_loss, - reason = "wasm32 usize values are exactly representable by f64" - )] - let carrier = $value as $carrier; - carrier - } - }; - (@into_abi $value:ident, isize, $carrier:ty) => { - { - #[expect( - clippy::cast_precision_loss, - reason = "wasm32 isize values are exactly representable by f64" - )] - let carrier = $value as $carrier; - carrier - } - }; - (@into_abi $value:ident, $ty:ident, $carrier:ty) => { - $value as $carrier - }; - (@from_abi $raw:ident, bool) => { - $raw != 0 - }; - (@from_abi $raw:ident, $ty:ident) => { - $raw as $ty - }; + (@into_carrier pointer, $value:ident, $carrier:ty) => {{ + #[expect( + clippy::cast_precision_loss, + reason = "wasm32 pointer-sized values are exactly representable by f64" + )] + let carrier = $value as $carrier; + carrier + }}; } macro_rules! indirect_option { - ($($ty:ty => { - decode: $decode:literal, + ($($ty:ident => { + decode: ($decode:literal, $decode_arguments:literal), encode: $encode:literal, - slots: [$($slot:literal),+ $(,)?], + slots: $slots:expr, }),+ $(,)?) => {$( // SAFETY: The optional value is represented by a presence tag followed by // its payload slots and is returned through a hidden pointer. @@ -154,8 +133,13 @@ macro_rules! indirect_option { // optional JavaScript value. unsafe impl OptionIntoAbi<$ty> for $ty { const JS_CONV: Option = Some( - IntoJsConv::new(indirect_option!(@decode $decode, [$($slot),+])) - .with_embed(("js_sys", $decode)), + IntoJsConv::new(const_concat!( + "this.#jsEmbed.js_sys['", + $decode, + "']", + $decode_arguments, + )) + .with_embed(("js_sys", $decode)), ); type Abi = Option<$ty>; @@ -168,11 +152,22 @@ macro_rules! indirect_option { // SAFETY: The encoder writes a JavaScript value as a presence tag and the // payload slots expected by `Option<$ty>`. unsafe impl OptionFromAbi<$ty> for $ty { - const JS_CONV: Option = Some( - indirect_option!(@output [$($slot),+]) - .sret(const_concat!("this.#jsEmbed.js_sys['", $encode, "']")) - .with_embed(("js_sys", $encode)), - ); + const JS_CONV: Option = { + const SLOTS: [&str; 4] = $slots; + + Some( + FromJsConv::slot1(SLOTS[0]) + .slot2(SLOTS[1]) + .slot3(SLOTS[2]) + .slot4(SLOTS[3]) + .sret(Sret::Slots(const_concat!( + "this.#jsEmbed.js_sys['", + $encode, + "']" + ))) + .with_embed(("js_sys", $encode)), + ) + }; type Abi = Option<$ty>; @@ -181,18 +176,6 @@ macro_rules! indirect_option { } } )+}; - (@decode $decode:literal, [$slot1:literal, $slot2:literal]) => { - const_concat!("this.#jsEmbed.js_sys['", $decode, "']($slot1, $slot2)") - }; - (@decode $decode:literal, [$slot1:literal, $slot2:literal, $slot3:literal]) => { - const_concat!("this.#jsEmbed.js_sys['", $decode, "']($slot1, $slot2, $slot3)") - }; - (@output [$slot1:literal, $slot2:literal]) => { - FromJsConv::slot1($slot1).slot2($slot2) - }; - (@output [$slot1:literal, $slot2:literal, $slot3:literal]) => { - FromJsConv::slot1($slot1).slot2($slot2).slot3($slot3) - }; } slot!("i32", bool, u8, u16, u32, i8, i16, i32); @@ -364,7 +347,7 @@ unsafe impl FromJS for u128 { const JS_CONV: Option = Some( FromJsConv::slot1("$value") .slot2("$value >> 64n") - .sret("this.#jsEmbed.js_sys['numeric.128.encode']") + .sret(Sret::Slots("this.#jsEmbed.js_sys['numeric.128.encode']")) .with_embed(("js_sys", "numeric.128.encode")), ); @@ -428,7 +411,7 @@ unsafe impl FromJS for i128 { const JS_CONV: Option = Some( FromJsConv::slot1("$value") .slot2("$value >> 64n") - .sret("this.#jsEmbed.js_sys['numeric.128.encode']") + .sret(Sret::Slots("this.#jsEmbed.js_sys['numeric.128.encode']")) .with_embed(("js_sys", "numeric.128.encode")), ); @@ -443,7 +426,7 @@ js_bindgen::embed_js!( module = "js_sys", name = "numeric.u128.decode", "(lo, hi) => {{", - " return BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", + " return BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", "}}", ); @@ -451,7 +434,7 @@ js_bindgen::embed_js!( module = "js_sys", name = "numeric.i128.decode", "(lo, hi) => {{", - " return BigInt.asUintN(64, lo) | (hi << 64n)", + " return BigInt.asUintN(64, lo) | (hi << 64n)", "}}", ); @@ -459,17 +442,17 @@ js_bindgen::embed_js!( module = "js_sys", name = "numeric.128.encode", "(() => {{", - " const memory = this.#memory", - " let buffer = memory.buffer", - " let view = new DataView(buffer)", - " return (lo, hi, out) => {{", - " if (out + 16 > buffer.byteLength) {{", - " buffer = memory.buffer", - " view = new DataView(buffer)", - " }}", - " view.setBigInt64(out, lo, true)", - " view.setBigInt64(out + 8, hi, true)", - " }}", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (lo, hi, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setBigInt64(out, lo, true)", + " view.setBigInt64(out + 8, hi, true)", + " }}", "}})()", ); @@ -479,40 +462,86 @@ const I32_OPTION_SENTINEL: i32 = 0x00ff_ffff; // `u32`, or widened `f32` value. const F64_OPTION_SENTINEL: f64 = 9_007_199_254_740_991.0; +// SAFETY: The sentinel is outside the Boolean carrier range. +unsafe impl OptionIntoAbi for bool { + const JS_CONV: Option = Some(IntoJsConv::new( + "$slot1 === 0x00ff_ffff ? undefined : $slot1 !== 0", + )); + + type Abi = i32; + + fn into_option_abi(value: Option) -> Self::Abi { + value.map_or(I32_OPTION_SENTINEL, i32::from) + } +} + +// SAFETY: The sentinel is decoded before the carrier is converted back to a +// Boolean. +unsafe impl OptionFromAbi for bool { + const JS_CONV: Option = Some(FromJsConv::slot1( + "$value == null ? 0x00ff_ffff : $value ? 1 : 0", + )); + + type Abi = i32; + + fn from_option_abi(raw: Self::Abi) -> Option { + if raw == I32_OPTION_SENTINEL { + None + } else { + Some(raw != 0) + } + } +} + sentinel_option! { carrier: i32, sentinel: I32_OPTION_SENTINEL, js_sentinel: "0x00ff_ffff", - types: [ - [i8, u8, i16, u16] => { - to_js: "$slot1", - from_js: "$value", - }, - [bool] => { - to_js: "$slot1 !== 0", - from_js: "$value ? 1 : 0", - }, - ], + into_carrier: widen, + to_js: "$slot1", + from_js: "$value", + types: [i8, u8, i16, u16], +} + +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "Number.MAX_SAFE_INTEGER", + into_carrier: widen, + to_js: "$slot1", + from_js: "$value >> 0", + types: [i32], } sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, js_sentinel: "Number.MAX_SAFE_INTEGER", - types: [ - [i32] => { - to_js: "$slot1", - from_js: "$value >> 0", - }, - [u32] => { - to_js: "$slot1", - from_js: "$value >>> 0", - }, - [f32] => { - to_js: "$slot1", - from_js: "Math.fround($value)", - }, - ], + into_carrier: widen, + to_js: "$slot1", + from_js: "$value >>> 0", + types: [u32], +} + +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "Number.MAX_SAFE_INTEGER", + into_carrier: widen, + to_js: "$slot1", + from_js: "Math.fround($value)", + types: [f32], +} + +#[cfg(target_arch = "wasm32")] +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "Number.MAX_SAFE_INTEGER", + into_carrier: pointer, + to_js: "$slot1", + from_js: "$value >> 0", + types: [isize], } #[cfg(target_arch = "wasm32")] @@ -520,67 +549,63 @@ sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, js_sentinel: "Number.MAX_SAFE_INTEGER", - types: [ - [isize] => { - to_js: "$slot1", - from_js: "$value >> 0", - }, - [usize] => { - to_js: "$slot1", - from_js: "$value >>> 0", - }, - ], + into_carrier: pointer, + to_js: "$slot1", + from_js: "$value >>> 0", + types: [usize], } indirect_option! { f64 => { - decode: "optional.f64.decode", + decode: ("optional.f64.decode", "($slot1, $slot2)"), encode: "optional.f64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0 : $value"], + slots: ["$value == null ? 0 : 1", "$value == null ? 0 : $value", "", ""], }, i64 => { - decode: "optional.i64.decode", + decode: ("optional.i64.decode", "($slot1, $slot2)"), encode: "optional.i64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], }, u64 => { - decode: "optional.u64.decode", + decode: ("optional.u64.decode", "($slot1, $slot2)"), encode: "optional.u64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], }, } #[cfg(target_arch = "wasm64")] indirect_option! { isize => { - decode: "optional.i64.decode", + decode: ("optional.i64.decode", "($slot1, $slot2)"), encode: "optional.i64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], }, usize => { - decode: "optional.u64.decode", + decode: ("optional.u64.decode", "($slot1, $slot2)"), encode: "optional.u64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value"], + slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], }, } indirect_option! { u128 => { - decode: "optional.u128.decode", + decode: ("optional.u128.decode", "($slot1, $slot2, $slot3)"), encode: "optional.128.encode", slots: [ "$value == null ? 0 : 1", "$value == null ? 0n : $value", "$value == null ? 0n : $value >> 64n", + "", ], }, i128 => { - decode: "optional.i128.decode", + decode: ("optional.i128.decode", "($slot1, $slot2, $slot3)"), encode: "optional.128.encode", slots: [ "$value == null ? 0 : 1", "$value == null ? 0n : $value", "$value == null ? 0n : $value >> 64n", + "", ], }, } @@ -589,8 +614,8 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.f64.decode", "(isSome, value) => {{", - " if (isSome === 0) return undefined", - " return value", + " if (isSome === 0) return undefined", + " return value", "}}", ); @@ -598,17 +623,17 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.f64.encode", "(() => {{", - " const memory = this.#memory", - " let buffer = memory.buffer", - " let view = new DataView(buffer)", - " return (isSome, value, out) => {{", - " if (out + 16 > buffer.byteLength) {{", - " buffer = memory.buffer", - " view = new DataView(buffer)", - " }}", - " view.setUint32(out, isSome, true)", - " view.setFloat64(out + 8, value, true)", - " }}", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, value, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setFloat64(out + 8, value, true)", + " }}", "}})()", ); @@ -616,8 +641,8 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.i64.decode", "(isSome, value) => {{", - " if (isSome === 0) return undefined", - " return value", + " if (isSome === 0) return undefined", + " return value", "}}", ); @@ -625,17 +650,17 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.i64.encode", "(() => {{", - " const memory = this.#memory", - " let buffer = memory.buffer", - " let view = new DataView(buffer)", - " return (isSome, value, out) => {{", - " if (out + 16 > buffer.byteLength) {{", - " buffer = memory.buffer", - " view = new DataView(buffer)", - " }}", - " view.setUint32(out, isSome, true)", - " view.setBigInt64(out + 8, value, true)", - " }}", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, value, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setBigInt64(out + 8, value, true)", + " }}", "}})()", ); @@ -643,8 +668,8 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.u64.decode", "(isSome, value) => {{", - " if (isSome === 0) return undefined", - " return BigInt.asUintN(64, value)", + " if (isSome === 0) return undefined", + " return BigInt.asUintN(64, value)", "}}", ); @@ -652,17 +677,17 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.u64.encode", "(() => {{", - " const memory = this.#memory", - " let buffer = memory.buffer", - " let view = new DataView(buffer)", - " return (isSome, value, out) => {{", - " if (out + 16 > buffer.byteLength) {{", - " buffer = memory.buffer", - " view = new DataView(buffer)", - " }}", - " view.setUint32(out, isSome, true)", - " view.setBigUint64(out + 8, value, true)", - " }}", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, value, out) => {{", + " if (out + 16 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setBigUint64(out + 8, value, true)", + " }}", "}})()", ); @@ -670,8 +695,8 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.u128.decode", "(isSome, lo, hi) => {{", - " if (isSome === 0) return undefined", - " return BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", + " if (isSome === 0) return undefined", + " return BigInt.asUintN(64, lo) | (BigInt.asUintN(64, hi) << 64n)", "}}", ); @@ -679,8 +704,8 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.i128.decode", "(isSome, lo, hi) => {{", - " if (isSome === 0) return undefined", - " return BigInt.asUintN(64, lo) | (hi << 64n)", + " if (isSome === 0) return undefined", + " return BigInt.asUintN(64, lo) | (hi << 64n)", "}}", ); @@ -688,17 +713,17 @@ js_bindgen::embed_js!( module = "js_sys", name = "optional.128.encode", "(() => {{", - " const memory = this.#memory", - " let buffer = memory.buffer", - " let view = new DataView(buffer)", - " return (isSome, lo, hi, out) => {{", - " if (out + 24 > buffer.byteLength) {{", - " buffer = memory.buffer", - " view = new DataView(buffer)", - " }}", - " view.setUint32(out, isSome, true)", - " view.setBigInt64(out + 8, lo, true)", - " view.setBigInt64(out + 16, hi, true)", - " }}", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " return (isSome, lo, hi, out) => {{", + " if (out + 24 > buffer.byteLength) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, isSome, true)", + " view.setBigInt64(out + 8, lo, true)", + " view.setBigInt64(out + 16, hi, true)", + " }}", "}})()", ); diff --git a/client/js-sys/src/interop/slice.rs b/client/js-sys/src/interop/slice.rs new file mode 100644 index 00000000..03538844 --- /dev/null +++ b/client/js-sys/src/interop/slice.rs @@ -0,0 +1,260 @@ +use crate::JsValue; +use crate::builtins::Array; +use crate::hazard::{IntoJS, IntoJsConv, JsCast}; +use crate::util::{ExternSlice, JS_PTR_LEN_ARGS, PtrConst, PtrLength}; + +macro_rules! primitive_slices { + ($( + $(#[$attribute:meta])* + $ty:ty => { + constructor: $constructor:literal, + view: $view:literal, + embed: $embed:literal, + decode: $decode:ident, + from_slice: $from_slice:ident, + } + ),+ $(,)?) => { + #[crate::js_sys(js_sys = crate)] + extern "js-sys" { + // SAFETY: The pointer and length must describe a valid `JsValue` slice. + #[js_sys(js_embed = "array.js_value.decode")] + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn array_js_value_decode( + array: PtrConst, + len: PtrLength, + ) -> Array; + + $( + $(#[$attribute])* + // SAFETY: The pointer and length must describe a valid slice of the + // declared element type. + #[js_sys(js_embed = $embed)] + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn $decode(array: PtrConst<$ty>, len: PtrLength<$ty>) -> Array<$ty>; + )+ + } + + $( + $(#[$attribute])* + pub(in crate::interop) fn $from_slice(value: &[$ty]) -> Array<$ty> { + // SAFETY: The pointer and length describe `value`. + unsafe { $decode(PtrConst::new(value), PtrLength::new(value)) } + } + + // SAFETY: The two slots describe a borrowed primitive slice, which + // JavaScript copies into an independent typed array before the import. + $(#[$attribute])* + #[expect( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented in the macro definition" + )] + unsafe impl IntoJS for &[$ty] { + const JS_CONV: Option = Some( + IntoJsConv::new(crate::const_concat!( + "new ", + $constructor, + "(this.#jsEmbed.js_sys['view.get", + $view, + "'](", + JS_PTR_LEN_ARGS, + "))" + )) + .with_embed(("js_sys", concat!("view.get", $view))), + ); + + type Abi = ExternSlice<$ty>; + + fn into_abi(self) -> Self::Abi { + ExternSlice::new(self) + } + } + + $(#[$attribute])* + js_bindgen::embed_js!( + module = "js_sys", + name = $embed, + required_embeds = [("js_sys", concat!("view.get", $view))], + "(ptr, len) => Array.from(", + " this.#jsEmbed.js_sys['view.get{view}'](ptr, len),", + ")", + view = interpolate $view, + ); + )+ + }; +} + +primitive_slices! { + i8 => { + constructor: "Int8Array", + view: "Int8", + embed: "array.i8.decode", + decode: array_i8_decode, + from_slice: array_from_i8_slice, + }, + u8 => { + constructor: "Uint8Array", + view: "Uint8", + embed: "array.u8.decode", + decode: array_u8_decode, + from_slice: array_from_u8_slice, + }, + i16 => { + constructor: "Int16Array", + view: "Int16", + embed: "array.i16.decode", + decode: array_i16_decode, + from_slice: array_from_i16_slice, + }, + u16 => { + constructor: "Uint16Array", + view: "Uint16", + embed: "array.u16.decode", + decode: array_u16_decode, + from_slice: array_from_u16_slice, + }, + i32 => { + constructor: "Int32Array", + view: "Int32", + embed: "array.i32.decode", + decode: array_i32_decode, + from_slice: array_from_i32_slice, + }, + u32 => { + constructor: "Uint32Array", + view: "Uint32", + embed: "array.u32.decode", + decode: array_u32_decode, + from_slice: array_from_u32_slice, + }, + i64 => { + constructor: "BigInt64Array", + view: "BigInt64", + embed: "array.i64.decode", + decode: array_i64_decode, + from_slice: array_from_i64_slice, + }, + u64 => { + constructor: "BigUint64Array", + view: "BigUint64", + embed: "array.u64.decode", + decode: array_u64_decode, + from_slice: array_from_u64_slice, + }, + f32 => { + constructor: "Float32Array", + view: "Float32", + embed: "array.f32.decode", + decode: array_f32_decode, + from_slice: array_from_f32_slice, + }, + f64 => { + constructor: "Float64Array", + view: "Float64", + embed: "array.f64.decode", + decode: array_f64_decode, + from_slice: array_from_f64_slice, + }, + #[cfg(target_arch = "wasm32")] + isize => { + constructor: "Int32Array", + view: "Int32", + embed: "array.isize.decode", + decode: array_isize_decode, + from_slice: array_from_isize_slice, + }, + #[cfg(target_arch = "wasm64")] + isize => { + constructor: "BigInt64Array", + view: "BigInt64", + embed: "array.isize.decode", + decode: array_isize_decode, + from_slice: array_from_isize_slice, + }, + #[cfg(target_arch = "wasm32")] + usize => { + constructor: "Uint32Array", + view: "Uint32", + embed: "array.usize.decode", + decode: array_usize_decode, + from_slice: array_from_usize_slice, + }, + #[cfg(target_arch = "wasm64")] + usize => { + constructor: "BigUint64Array", + view: "BigUint64", + embed: "array.usize.decode", + decode: array_usize_decode, + from_slice: array_from_usize_slice, + }, +} + +pub(in crate::interop) fn array_from_js_value_slice(value: &[T]) -> Array { + let slice = JsValue::from_slice(value); + // SAFETY: Parameters are correct. + let result = unsafe { array_js_value_decode(PtrConst::new(slice), PtrLength::new(slice)) }; + + Array::unchecked_from(result.into()) +} + +// SAFETY: The array delegates to the slice implementation with the same +// element representation. +unsafe impl<'a, T, const N: usize> IntoJS for &'a [T; N] +where + &'a [T]: IntoJS, +{ + const JS_CONV: Option = <&[T] as IntoJS>::JS_CONV; + + type Abi = <&'a [T] as IntoJS>::Abi; + + fn into_abi(self) -> Self::Abi { + self.as_slice().into_abi() + } +} + +// SAFETY: The two slots point to borrowed `JsValue` table indices, which the +// JavaScript decoder resolves before the import is called. +unsafe impl IntoJS for &[T] { + const JS_CONV: Option = Some( + IntoJsConv::new(crate::const_concat!( + "this.#jsEmbed.js_sys['array.js_value.decode'](", + JS_PTR_LEN_ARGS, + ")" + )) + .with_embed(("js_sys", "array.js_value.decode")), + ); + + type Abi = ExternSlice; + + fn into_abi(self) -> Self::Abi { + ExternSlice::new(JsValue::from_slice(self)) + } +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "array.js_value.decode", + required_embeds = [("js_sys", "externref.table"), ("js_sys", "view.getUint32")], + "(ptr, len) => {{", + " const array = new Array(len)", + " const table = this.#jsEmbed.js_sys['externref.table']", + " const refIndices = this.#jsEmbed.js_sys['view.getUint32'](ptr, len)", + " for (let arrayIndex = 0; arrayIndex < len; arrayIndex++) {{", + " array[arrayIndex] = table.get(refIndices[arrayIndex])", + " }}", + " return array", + "}}", +); diff --git a/client/js-sys/src/interop/string.rs b/client/js-sys/src/interop/string.rs index 77ca6c09..90f468c7 100644 --- a/client/js-sys/src/interop/string.rs +++ b/client/js-sys/src/interop/string.rs @@ -1,19 +1,69 @@ -use crate::hazard::{IntoJS, IntoJsConv}; -use crate::util::ExternSlice; +use alloc::boxed::Box; +use alloc::string::String; + +use crate::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionFromAbi, OptionIntoAbi, ReturnAbi, + ReturnMode, Sret, WasmAbi, +}; +use crate::util::{ExternSlice, JS_OPTION_PTR_LEN_ARGS, JS_PTR_LEN_ARGS, PtrConst, PtrLength}; +use crate::{JsString, js_sys}; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_embed = "string.decode")] + // SAFETY: The pointer and length must describe a valid UTF-8 byte slice. + #[expect( + clippy::allow_attributes, + reason = "the macro emits an unsafe ABI call" + )] + #[allow( + clippy::undocumented_unsafe_blocks, + reason = "the safety requirement is documented on this declaration" + )] + unsafe fn string_decode(array: PtrConst, len: PtrLength) -> JsString; +} + +pub(in crate::interop) fn js_string_from_str(value: &str) -> JsString { + // SAFETY: A Rust string is valid UTF-8, and its pointer and length describe + // the complete byte slice for the duration of the call. + unsafe { + string_decode( + PtrConst::new(value.as_bytes()), + PtrLength::new(value.as_bytes()), + ) + } +} #[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] js_bindgen::embed_js!( module = "js_sys", name = "string.decode", "(() => {{", - " const decoder = new TextDecoder('utf-8', {{", - " fatal: false,", - " ignoreBOM: false,", - " }})", - " return (ptr, len) => {{", - " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " return decoder.decode(view)", - " }}", + " const decoder = new TextDecoder('utf-8', {{", + " fatal: true,", + " ignoreBOM: true,", + " }})", + " decoder.decode()", + " return (ptr, len) => {{", + " if (len === 0) return ''", + " const view = new Uint8Array(this.#memory.buffer, ptr, len)", + " return decoder.decode(view)", + " }}", + "}})()", +); + +#[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] +js_bindgen::embed_js!( + module = "js_sys", + name = "string.shared.decode", + "(() => {{", + " if (this.#memory.buffer instanceof ArrayBuffer) return true", + " try {{", + " new TextDecoder().decode(new Uint8Array(this.#memory.buffer, 0, 0))", + " return true", + " }} catch {{", + " return false", + " }}", "}})()", ); @@ -21,27 +71,297 @@ js_bindgen::embed_js!( js_bindgen::embed_js!( module = "js_sys", name = "string.decode", - required_embeds = [("js_sys", "string.sab")], + required_embeds = [("js_sys", "string.shared.decode")], + "(() => {{", + " const decoder = new TextDecoder('utf-8', {{", + " fatal: true,", + " ignoreBOM: true,", + " }})", + " decoder.decode()", + " return (ptr, len) => {{", + " if (len === 0) return ''", + " const view = new Uint8Array(this.#memory.buffer, ptr, len)", + " return decoder.decode(", + " this.#jsEmbed.js_sys['string.shared.decode'] ? view : view.slice()", + " )", + " }}", + "}})()", +); + +#[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] +js_bindgen::embed_js!( + module = "js_sys", + name = "string.shared.encode", "(() => {{", - " const decoder = new TextDecoder('utf-8', {{", - " fatal: false,", - " ignoreBOM: false,", - " }})", - " return (ptr, len) => {{", - " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " return decoder.decode(", - " this.#jsEmbed.js_sys['string.sab'] ? view : view.slice()", - " )", - " }}", + " if (this.#memory.buffer instanceof ArrayBuffer) return true", + " try {{", + " const view = new Uint8Array(this.#memory.buffer, 0, 0)", + " new TextEncoder().encodeInto('', view)", + " return true", + " }} catch {{", + " return false", + " }}", "}})()", ); -// SAFETY: The UTF-8 byte slice is decoded into a JavaScript string before the -// import is called. +js_bindgen::embed_js!( + module = "js_sys", + name = "string.take", + required_embeds = [("js_sys", "string.decode")], + "(ptr, len) => {{", + " try {{", + " return this.#jsEmbed.js_sys['string.decode'](ptr, len)", + " }} finally {{", + " if (len !== 0) {{", + #[cfg(target_arch = "wasm32")] + " this.#jsExports['js_sys.memory.free'](ptr, len, 1)", + #[cfg(target_arch = "wasm64")] + " this.#jsExports['js_sys.memory.free'](BigInt(ptr), BigInt(len), 1n)", + " }}", + " }}", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "string.from_js", + required_embeds = [ + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + ("js_sys", "string.shared.encode") + ], + "(() => {{", + " const encoder = new TextEncoder()", + " const memory = this.#memory", + " let bytes = new Uint8Array(memory.buffer)", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " const ascii = (value, ptr, capacity) => {{", + #[cfg(target_arch = "wasm32")] + " const start = ptr >>> 0", + #[cfg(target_arch = "wasm64")] + " const start = Number(ptr)", + #[cfg(not(target_feature = "atomics"))] + " if (bytes.byteLength === 0) bytes = new Uint8Array(memory.buffer)", + #[cfg(target_feature = "atomics")] + " if (bytes.buffer !== memory.buffer || bytes.byteLength !== memory.buffer.byteLength)", + #[cfg(target_feature = "atomics")] + " bytes = new Uint8Array(memory.buffer)", + " let written = 0", + " for (; written < capacity; written++) {{", + " const code = value.charCodeAt(written)", + " if (code > 0x7f) break", + " bytes[start + written] = code", + " }}", + " return written", + " }}", + " const store = (ptr, written, out) => {{", + " if (buffer !== memory.buffer) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out, ptr, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out, ptr, true)", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out + 4, written, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out + 8, BigInt(written), true)", + " }}", + " const unicode = (value, ptr, capacity, written, out) => {{", + " if (written !== capacity) {{", + " if (written !== 0) value = value.slice(written)", + " const nextCapacity = written + value.length * 3", + " if (!Number.isSafeInteger(nextCapacity))", + " throw new RangeError('string is too large')", + "", + #[cfg(target_arch = "wasm32")] + " ptr = this.#jsExports['js_sys.memory.realloc'](ptr, capacity, nextCapacity, 1)", + #[cfg(target_arch = "wasm64")] + " ptr = this.#jsExports['js_sys.memory.realloc'](", + #[cfg(target_arch = "wasm64")] + " ptr, BigInt(capacity), BigInt(nextCapacity), 1n,", + #[cfg(target_arch = "wasm64")] + " )", + " capacity = nextCapacity", + #[cfg(target_arch = "wasm32")] + " const start = ptr >>> 0", + #[cfg(target_arch = "wasm64")] + " const start = Number(ptr)", + #[cfg(not(target_feature = "atomics"))] + " if (bytes.byteLength === 0) bytes = new Uint8Array(memory.buffer)", + #[cfg(target_feature = "atomics")] + " if (bytes.buffer !== memory.buffer || bytes.byteLength !== \ + memory.buffer.byteLength)", + #[cfg(target_feature = "atomics")] + " bytes = new Uint8Array(memory.buffer)", + " const target = bytes.subarray(start + written, start + capacity)", + "", + #[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] + " const encoded = encoder.encodeInto(value, target)", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " let encoded", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " if (this.#jsEmbed.js_sys['string.shared.encode']) {{", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " encoded = encoder.encodeInto(value, target)", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " }} else {{", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " const bytes = encoder.encode(value)", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " target.set(bytes)", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " encoded = {{ read: value.length, written: bytes.length }}", + #[cfg(all(target_feature = "atomics", not(js_sys_target_feature = "sab")))] + " }}", + " if (encoded.read !== value.length)", + " throw new RangeError('failed to encode the complete string')", + " written += encoded.written", + " }}", + "", + " if (written !== capacity) {{", + #[cfg(target_arch = "wasm32")] + " ptr = this.#jsExports['js_sys.memory.realloc'](ptr, capacity, written, 1)", + #[cfg(target_arch = "wasm64")] + " ptr = this.#jsExports['js_sys.memory.realloc'](", + #[cfg(target_arch = "wasm64")] + " ptr, BigInt(capacity), BigInt(written), 1n,", + #[cfg(target_arch = "wasm64")] + " )", + " }}", + "", + " if (out !== undefined) {{", + " store(ptr, written, out)", + " return", + " }}", + #[cfg(target_arch = "wasm32")] + " return [ptr, written]", + #[cfg(target_arch = "wasm64")] + " return [ptr, BigInt(written)]", + " }}", + " const slots = value => {{", + " if (typeof value !== 'string')", + " throw new TypeError(`expected a string, found ${{typeof value}}`)", + " const capacity = value.length", + #[cfg(target_arch = "wasm32")] + " const ptr = this.#jsExports['js_sys.memory.alloc'](capacity, 1)", + #[cfg(target_arch = "wasm64")] + " const ptr = this.#jsExports['js_sys.memory.alloc'](BigInt(capacity), 1n)", + " const written = ascii(value, ptr, capacity)", + " if (written !== capacity) return unicode(value, ptr, capacity, written)", + #[cfg(target_arch = "wasm32")] + " return [ptr, written]", + #[cfg(target_arch = "wasm64")] + " return [ptr, BigInt(written)]", + " }}", + " const sret = (value, out) => {{", + " if (typeof value !== 'string')", + " throw new TypeError(`expected a string, found ${{typeof value}}`)", + " const capacity = value.length", + #[cfg(target_arch = "wasm32")] + " const ptr = this.#jsExports['js_sys.memory.alloc'](capacity, 1)", + #[cfg(target_arch = "wasm64")] + " const ptr = this.#jsExports['js_sys.memory.alloc'](BigInt(capacity), 1n)", + " const written = ascii(value, ptr, capacity)", + " if (written !== capacity) return unicode(value, ptr, capacity, written, out)", + " store(ptr, written, out)", + " }}", + " return {{ slots, sret }}", + "}})()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "string.option.from_js", + required_embeds = [("js_sys", "string.from_js")], + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " const slots = value => {{", + " if (value == null) return [0, {zero}, {zero}]", + " const pair = this.#jsEmbed.js_sys['string.from_js'].slots(value)", + " return [1, pair[0], pair[1]]", + " }}", + " const sret = (value, out) => {{", + " if (value == null) {{", + " if (buffer !== memory.buffer) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, 0, true)", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out + 4, 0, true)", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out + 8, 0, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out + 8, 0n, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out + 16, 0n, true)", + " return", + " }}", + #[cfg(target_arch = "wasm32")] + " this.#jsEmbed.js_sys['string.from_js'].sret(value, out + 4)", + #[cfg(target_arch = "wasm64")] + " this.#jsEmbed.js_sys['string.from_js'].sret(value, out + 8)", + " if (buffer !== memory.buffer) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " view.setUint32(out, 1, true)", + " }}", + " return {{ slots, sret }}", + "}})()", + #[cfg(target_arch = "wasm32")] + zero = interpolate "0", + #[cfg(target_arch = "wasm64")] + zero = interpolate "0n", +); + +#[doc(hidden)] +pub struct StringAbi { + ptr: PtrConst, + len: PtrLength, +} + +// SAFETY: `StringAbi` is represented by its pointer and length, in that order. +unsafe impl WasmAbi for StringAbi { + type Slot1 = PtrConst; + type Slot2 = PtrLength; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + (self.ptr, self.len, EmptySlot::new(), EmptySlot::new()) + } + + fn join(ptr: Self::Slot1, len: Self::Slot2, _: Self::Slot3, _: Self::Slot4) -> Self { + Self { ptr, len } + } +} + +// SAFETY: The two-word aggregate uses a hidden return pointer under Rust's +// `extern "C"` calling convention. +unsafe impl ReturnAbi for StringAbi { + const MODE: ReturnMode = ReturnMode::Indirect; +} + +// SAFETY: The presence tag followed by the pointer and length forms a +// three-slot aggregate returned through a hidden pointer. +unsafe impl ReturnAbi for Option { + const MODE: ReturnMode = ReturnMode::Indirect; +} + +// SAFETY: The UTF-8 byte slice is decoded before the JavaScript call. unsafe impl IntoJS for &str { const JS_CONV: Option = Some( - IntoJsConv::new("this.#jsEmbed.js_sys['string.decode']($slot1, $slot2)") - .with_embed(("js_sys", "string.decode")), + IntoJsConv::new(crate::const_concat!( + "this.#jsEmbed.js_sys['string.decode'](", + JS_PTR_LEN_ARGS, + ")" + )) + .with_embed(("js_sys", "string.decode")), ); type Abi = ExternSlice; @@ -50,3 +370,94 @@ unsafe impl IntoJS for &str { ExternSlice::new(self.as_bytes()) } } + +// SAFETY: The allocation is decoded as UTF-8 and freed before JavaScript +// observes the converted value. +unsafe impl IntoJS for String { + const JS_CONV: Option = Some( + IntoJsConv::new(crate::const_concat!( + "this.#jsEmbed.js_sys['string.take'](", + JS_PTR_LEN_ARGS, + ")" + )) + .with_embed(("js_sys", "string.take")), + ); + + type Abi = StringAbi; + + fn into_abi(self) -> Self::Abi { + let bytes = self.into_bytes().into_boxed_slice(); + let len = bytes.len(); + let ptr = Box::into_raw(bytes).cast::(); + + StringAbi { + ptr: PtrConst::from_raw(ptr), + len: PtrLength::from_len(len), + } + } +} + +// SAFETY: The presence tag distinguishes `None` from every string, including +// the empty string. Only a present allocation is decoded and released. +unsafe impl OptionIntoAbi for StringAbi { + const JS_CONV: Option = Some( + IntoJsConv::new(crate::const_concat!( + "$slot1 === 0 ? undefined : this.#jsEmbed.js_sys['string.take'](", + JS_OPTION_PTR_LEN_ARGS, + ")" + )) + .with_embed(("js_sys", "string.take")), + ); + + type Abi = Option; + + fn into_option_abi(value: Option) -> Self::Abi { + value.map(::into_abi) + } +} + +// SAFETY: JavaScript allocates an exact-size byte buffer, fills it with valid +// UTF-8, and transfers ownership through the pointer and length slots. +unsafe impl FromJS for String { + const JS_CONV: Option = Some( + FromJsConv::slot1("$prepared[0]") + .slot2("$prepared[1]") + .prepare("this.#jsEmbed.js_sys['string.from_js'].slots($value)") + .sret(Sret::Value("this.#jsEmbed.js_sys['string.from_js'].sret")) + .with_embed(("js_sys", "string.from_js")), + ); + + type Abi = StringAbi; + + fn from_abi(raw: Self::Abi) -> Self { + let ptr = raw.ptr.as_ptr().cast_mut(); + let len = raw.len.get(); + // SAFETY: The conversion helper allocated exactly `len` bytes through the + // shared allocator and initialized all of them with valid UTF-8. + unsafe { + let bytes = Box::from_raw(core::ptr::slice_from_raw_parts_mut(ptr, len)); + Self::from_utf8_unchecked(bytes.into_vec()) + } + } +} + +// SAFETY: JavaScript `null` and `undefined` become `None`; every other value is +// converted once to an owned UTF-8 allocation and tagged as `Some`. +unsafe impl OptionFromAbi for StringAbi { + const JS_CONV: Option = Some( + FromJsConv::slot1("$prepared[0]") + .slot2("$prepared[1]") + .slot3("$prepared[2]") + .prepare("this.#jsEmbed.js_sys['string.option.from_js'].slots($value)") + .sret(Sret::Value( + "this.#jsEmbed.js_sys['string.option.from_js'].sret", + )) + .with_embed(("js_sys", "string.option.from_js")), + ); + + type Abi = Option; + + fn from_option_abi(raw: Self::Abi) -> Option { + raw.map(::from_abi) + } +} diff --git a/client/js-sys/src/interop/vec.rs b/client/js-sys/src/interop/vec.rs new file mode 100644 index 00000000..c2f20c79 --- /dev/null +++ b/client/js-sys/src/interop/vec.rs @@ -0,0 +1,597 @@ +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, ReturnAbi, ReturnMode, Sret, WasmAbi, +}; +use crate::util::{JS_PTR_LEN_ARGS, PtrConst, PtrLength}; +use crate::{JsString, JsValue}; + +js_bindgen::embed_js!( + module = "js_sys", + name = "vec.js_value.take", + required_embeds = [("js_sys", "array.js_value.decode")], + "(ptr, len) => {{", + " try {{", + " return this.#jsEmbed.js_sys['array.js_value.decode'](ptr, len)", + " }} finally {{", + #[cfg(target_arch = "wasm32")] + " this.#jsExports['js_sys.externref.recycle_slice'](ptr, len)", + #[cfg(target_arch = "wasm64")] + " this.#jsExports['js_sys.externref.recycle_slice'](BigInt(ptr), BigInt(len))", + " }}", + "}}", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "vec.js_value.from_js", + required_embeds = [("js_sys", "externref.table")], + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " const store = (ptr, len, out) => {{", + " if (buffer !== memory.buffer) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out, ptr, true)", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out + 4, len, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out, ptr, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out + 8, len, true)", + " }}", + " const fromJs = (value, out) => {{", + " if (!Array.isArray(value))", + " throw new TypeError('expected an Array')", + " const rawLength = value.length", + " const length = rawLength >>> 0", + " if (rawLength !== length)", + " throw new TypeError('invalid array length')", + "", + #[cfg(target_arch = "wasm32")] + " const ptr = this.#jsExports['js_sys.externref.reserve_slice'](length)", + #[cfg(target_arch = "wasm64")] + " const ptr = this.#jsExports['js_sys.externref.reserve_slice'](BigInt(length))", + " let transferred = false", + " try {{", + #[cfg(target_arch = "wasm32")] + " const address = ptr >>> 0", + #[cfg(target_arch = "wasm64")] + " const address = Number(ptr)", + " const table = this.#jsEmbed.js_sys['externref.table']", + " for (let index = 0; index < length; index++) {{", + " if (buffer !== memory.buffer) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + " // Read the slot before an element getter can grow memory.", + " const slot = view.getUint32(address + index * 4, true)", + " const element = value[index]", + " table.set(slot, element)", + " }}", + "", + " if (out === undefined) {{", + " transferred = true", + #[cfg(target_arch = "wasm32")] + " return [ptr, length]", + #[cfg(target_arch = "wasm64")] + " return [ptr, BigInt(length)]", + " }}", + #[cfg(target_arch = "wasm32")] + " store(ptr, length, out)", + #[cfg(target_arch = "wasm64")] + " store(ptr, BigInt(length), out)", + " transferred = true", + " }} finally {{", + " if (!transferred) {{", + #[cfg(target_arch = "wasm32")] + " this.#jsExports['js_sys.externref.recycle_slice'](ptr, length)", + #[cfg(target_arch = "wasm64")] + " this.#jsExports['js_sys.externref.recycle_slice'](ptr, BigInt(length))", + " }}", + " }}", + " }}", + " return {{", + " slots: fromJs,", + " sret: fromJs,", + " }}", + "}})()", +); + +js_bindgen::embed_js!( + module = "js_sys", + name = "vec.string.from_js", + required_embeds = [("js_sys", "vec.js_value.from_js")], + "(() => {{", + " const validate = value => {{", + " if (!Array.isArray(value))", + " throw new TypeError('expected an Array')", + " const rawLength = value.length", + " const length = rawLength >>> 0", + " if (rawLength !== length)", + " throw new TypeError('invalid array length')", + " const strings = new Array(length)", + " for (let index = 0; index < length; index++) {{", + " const element = value[index]", + " if (typeof element !== 'string')", + " throw new TypeError('expected an Array of strings')", + " strings[index] = element", + " }}", + " return strings", + " }}", + " return {{", + " slots: value => {{", + " return this.#jsEmbed.js_sys['vec.js_value.from_js'].slots(validate(value))", + " }},", + " sret: (value, out) => {{", + " return this.#jsEmbed.js_sys['vec.js_value.from_js'].sret(validate(value), out)", + " }},", + " }}", + "}})()", +); + +#[doc(hidden)] +pub struct VecAbi { + ptr: PtrConst, + len: PtrLength, +} + +impl VecAbi { + fn from_boxed_slice(value: Box<[T]>) -> Self { + let len = value.len(); + let ptr = Box::into_raw(value).cast::(); + + Self { + ptr: PtrConst::from_raw(ptr), + len: PtrLength::from_len(len), + } + } + + unsafe fn into_boxed_slice(self) -> Box<[T]> { + let ptr = self.ptr.as_ptr().cast_mut(); + let len = self.len.get(); + // SAFETY: The caller guarantees that the carrier owns an allocation for + // exactly `len` initialized `T` values. + unsafe { Box::from_raw(core::ptr::slice_from_raw_parts_mut(ptr, len)) } + } +} + +// SAFETY: `VecAbi` is represented by its element pointer and length, in that +// order. +unsafe impl WasmAbi for VecAbi { + type Slot1 = PtrConst; + type Slot2 = PtrLength; + type Slot3 = EmptySlot; + type Slot4 = EmptySlot; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + (self.ptr, self.len, EmptySlot::new(), EmptySlot::new()) + } + + fn join(ptr: Self::Slot1, len: Self::Slot2, _: Self::Slot3, _: Self::Slot4) -> Self { + Self { ptr, len } + } +} + +// SAFETY: The two-word aggregate uses a hidden return pointer under Rust's +// `extern "C"` calling convention. +unsafe impl ReturnAbi for VecAbi { + const MODE: ReturnMode = ReturnMode::Indirect; +} + +const JS_VALUE_VEC_TO_JS: IntoJsConv = IntoJsConv::new(crate::const_concat!( + "this.#jsEmbed.js_sys['vec.js_value.take'](", + JS_PTR_LEN_ARGS, + ")" +)) +.with_embed(("js_sys", "vec.js_value.take")); + +/// Element-level policy for moving an owned vector from Rust to JavaScript. +/// +/// # Safety +/// +/// `Abi`, `vector_into_abi`, and `JS_CONV` must describe one ownership- +/// transferring conversion for a boxed slice of `Self`. +#[doc(hidden)] +pub unsafe trait VectorIntoJS: Sized { + const JS_CONV: IntoJsConv; + + type Abi: WasmAbi; + + fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi; +} + +/// Element-level policy for moving an owned vector from JavaScript to Rust. +/// +/// # Safety +/// +/// `Abi`, `vector_from_abi`, and `JS_CONV` must describe one ownership- +/// transferring conversion for a boxed slice of `Self`. +#[doc(hidden)] +pub unsafe trait VectorFromJS: Sized { + const JS_CONV: FromJsConv; + + type Abi: WasmAbi; + + unsafe fn vector_from_abi(raw: Self::Abi) -> Box<[Self]>; +} + +// SAFETY: Delegated to the element's vector conversion policy. +unsafe impl IntoJS for Vec { + const JS_CONV: Option = Some(T::JS_CONV); + + type Abi = T::Abi; + + fn into_abi(self) -> Self::Abi { + T::vector_into_abi(self.into_boxed_slice()) + } +} + +// SAFETY: Delegated to the element's vector conversion policy. +unsafe impl FromJS for Vec { + const JS_CONV: Option = Some(T::JS_CONV); + + type Abi = T::Abi; + + fn from_abi(raw: Self::Abi) -> Self { + // SAFETY: `FromJS` guarantees that `raw` was produced by `T::JS_CONV`. + unsafe { T::vector_from_abi(raw) }.into_vec() + } +} + +// SAFETY: Every element is moved into its owned `JsValue` representation, and +// the JavaScript helper consumes the resulting table-index allocation. +unsafe impl VectorIntoJS for T +where + T: JsCast + Into, +{ + const JS_CONV: IntoJsConv = JS_VALUE_VEC_TO_JS; + + type Abi = VecAbi; + + fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi { + let values = vector + .into_vec() + .into_iter() + .map(Into::into) + .collect::>() + .into_boxed_slice(); + VecAbi::from_boxed_slice(values) + } +} + +// SAFETY: The helper creates an owned slice of valid `JsValue` table indices; +// `JsCast` transfers each value into the requested transparent wrapper. +unsafe impl VectorFromJS for T { + const JS_CONV: FromJsConv = FromJsConv::slot1("$prepared[0]") + .slot2("$prepared[1]") + .prepare("this.#jsEmbed.js_sys['vec.js_value.from_js'].slots($value)") + .sret(Sret::Value( + "this.#jsEmbed.js_sys['vec.js_value.from_js'].sret", + )) + .with_embed(("js_sys", "vec.js_value.from_js")); + + type Abi = VecAbi; + + unsafe fn vector_from_abi(raw: Self::Abi) -> Box<[Self]> { + // SAFETY: `vec.js_value.from_js` initialized every table index and + // transferred the exact-size allocation to this carrier. + unsafe { raw.into_boxed_slice() } + .into_vec() + .into_iter() + .map(T::unchecked_from) + .collect() + } +} + +// SAFETY: Strings are converted to owned JavaScript string values before the +// table-index allocation is transferred to JavaScript. +unsafe impl VectorIntoJS for String { + const JS_CONV: IntoJsConv = JS_VALUE_VEC_TO_JS; + + type Abi = VecAbi; + + fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi { + let values = vector + .into_vec() + .into_iter() + .map(|value| JsValue::from(JsString::from(value))) + .collect::>() + .into_boxed_slice(); + VecAbi::from_boxed_slice(values) + } +} + +// SAFETY: The JavaScript helper validates every array element as a string +// before transferring its owned table index to Rust. +unsafe impl VectorFromJS for String { + const JS_CONV: FromJsConv = FromJsConv::slot1("$prepared[0]") + .slot2("$prepared[1]") + .prepare("this.#jsEmbed.js_sys['vec.string.from_js'].slots($value)") + .sret(Sret::Value( + "this.#jsEmbed.js_sys['vec.string.from_js'].sret", + )) + .with_embed(("js_sys", "vec.string.from_js")); + + type Abi = VecAbi; + + unsafe fn vector_from_abi(raw: Self::Abi) -> Box<[Self]> { + // SAFETY: `vec.string.from_js` validated each value and then delegated to + // `vec.js_value.from_js`, which transferred the exact-size allocation. + unsafe { raw.into_boxed_slice() } + .into_vec() + .into_iter() + .map(|value| Self::from(JsString::unchecked_from(value))) + .collect() + } +} + +#[rustfmt::skip] +macro_rules! typed_vector { + ( + $ty:ty, + name = $name:literal, + constructor = $constructor:literal, + view = $view:literal $(,)? + ) => { + js_bindgen::embed_js!( + module = "js_sys", + name = concat!("vec.", $name, ".take"), + required_embeds = [("js_sys", concat!("view.get", $view))], + "(ptr, len) => {{", + " try {{", + " return new {constructor}(", + " this.#jsEmbed.js_sys['view.get{view}'](ptr, len),", + " )", + " }} finally {{", + #[cfg(target_arch = "wasm32")] + " this.#jsExports['js_sys.memory.free'](ptr, len * {size}, {align})", + #[cfg(target_arch = "wasm64")] + " this.#jsExports['js_sys.memory.free'](", + #[cfg(target_arch = "wasm64")] + " BigInt(ptr), BigInt(len) * {size}n, {align}n,", + #[cfg(target_arch = "wasm64")] + " )", + " }}", + "}}", + constructor = interpolate $constructor, + view = interpolate $view, + size = const core::mem::size_of::<$ty>(), + align = const core::mem::align_of::<$ty>(), + ); + + js_bindgen::embed_js!( + module = "js_sys", + name = concat!("vec.", $name, ".from_js"), + required_embeds = [("js_sys", concat!("view.set", $view))], + "(() => {{", + " const memory = this.#memory", + " let buffer = memory.buffer", + " let view = new DataView(buffer)", + " const store = (ptr, len, out) => {{", + " if (buffer !== memory.buffer) {{", + " buffer = memory.buffer", + " view = new DataView(buffer)", + " }}", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out, ptr, true)", + #[cfg(target_arch = "wasm32")] + " view.setUint32(out + 4, len, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out, ptr, true)", + #[cfg(target_arch = "wasm64")] + " view.setBigUint64(out + 8, len, true)", + " }}", + " const fromJs = (value, out) => {{", + " if (!(value instanceof {constructor}))", + " throw new TypeError('expected a {constructor}')", + " const length = value.length", + #[cfg(target_arch = "wasm32")] + " const ptr = this.#jsExports['js_sys.memory.alloc'](length * {size}, {align})", + #[cfg(target_arch = "wasm64")] + " const ptr = this.#jsExports['js_sys.memory.alloc'](", + #[cfg(target_arch = "wasm64")] + " BigInt(length) * {size}n, {align}n,", + #[cfg(target_arch = "wasm64")] + " )", + " let transferred = false", + " try {{", + #[cfg(target_arch = "wasm32")] + " const address = ptr >>> 0", + #[cfg(target_arch = "wasm64")] + " const address = Number(ptr)", + " this.#jsEmbed.js_sys['view.set{view}'](address, value, length)", + " if (out === undefined) {{", + " transferred = true", + #[cfg(target_arch = "wasm32")] + " return [ptr, length]", + #[cfg(target_arch = "wasm64")] + " return [ptr, BigInt(length)]", + " }}", + #[cfg(target_arch = "wasm32")] + " store(ptr, length, out)", + #[cfg(target_arch = "wasm64")] + " store(ptr, BigInt(length), out)", + " transferred = true", + " }} finally {{", + " if (!transferred) {{", + #[cfg(target_arch = "wasm32")] + " this.#jsExports['js_sys.memory.free'](", + #[cfg(target_arch = "wasm32")] + " ptr, length * {size}, {align},", + #[cfg(target_arch = "wasm32")] + " )", + #[cfg(target_arch = "wasm64")] + " this.#jsExports['js_sys.memory.free'](", + #[cfg(target_arch = "wasm64")] + " ptr, BigInt(length) * {size}n, {align}n,", + #[cfg(target_arch = "wasm64")] + " )", + " }}", + " }}", + " }}", + " return {{", + " slots: fromJs,", + " sret: fromJs,", + " }}", + "}})()", + constructor = interpolate $constructor, + view = interpolate $view, + size = const core::mem::size_of::<$ty>(), + align = const core::mem::align_of::<$ty>(), + ); + + // SAFETY: The helper copies the allocation into an independent typed + // array and releases the Rust buffer afterwards. + unsafe impl VectorIntoJS for $ty { + const JS_CONV: IntoJsConv = IntoJsConv::new(crate::const_concat!( + "this.#jsEmbed.js_sys['vec.", + $name, + ".take'](", + JS_PTR_LEN_ARGS, + ")" + )) + .with_embed(("js_sys", concat!("vec.", $name, ".take"))); + + type Abi = VecAbi; + + fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi { + VecAbi::from_boxed_slice(vector) + } + } + + // SAFETY: The helper allocates an exact-size buffer, initializes every + // element from the matching typed array, and transfers it to Rust. + unsafe impl VectorFromJS for $ty { + const JS_CONV: FromJsConv = FromJsConv::slot1("$prepared[0]") + .slot2("$prepared[1]") + .prepare(concat!( + "this.#jsEmbed.js_sys['vec.", + $name, + ".from_js'].slots($value)" + )) + .sret(Sret::Value(concat!( + "this.#jsEmbed.js_sys['vec.", + $name, + ".from_js'].sret" + ))) + .with_embed(("js_sys", concat!("vec.", $name, ".from_js"))); + + type Abi = VecAbi; + + unsafe fn vector_from_abi(raw: Self::Abi) -> Box<[Self]> { + // SAFETY: The matching `from_js` helper initialized every element and + // transferred the exact-size allocation to this carrier. + unsafe { raw.into_boxed_slice() } + } + } + }; +} + +typed_vector! { + i8, + name = "i8", + constructor = "Int8Array", + view = "Int8", +} + +typed_vector! { + u8, + name = "u8", + constructor = "Uint8Array", + view = "Uint8", +} + +typed_vector! { + i16, + name = "i16", + constructor = "Int16Array", + view = "Int16", +} + +typed_vector! { + u16, + name = "u16", + constructor = "Uint16Array", + view = "Uint16", +} + +typed_vector! { + i32, + name = "i32", + constructor = "Int32Array", + view = "Int32", +} + +typed_vector! { + u32, + name = "u32", + constructor = "Uint32Array", + view = "Uint32", +} + +typed_vector! { + i64, + name = "i64", + constructor = "BigInt64Array", + view = "BigInt64", +} + +typed_vector! { + u64, + name = "u64", + constructor = "BigUint64Array", + view = "BigUint64", +} + +typed_vector! { + f32, + name = "f32", + constructor = "Float32Array", + view = "Float32", +} + +typed_vector! { + f64, + name = "f64", + constructor = "Float64Array", + view = "Float64", +} + +#[cfg(target_arch = "wasm32")] +typed_vector! { + isize, + name = "isize", + constructor = "Int32Array", + view = "Int32", +} + +#[cfg(target_arch = "wasm64")] +typed_vector! { + isize, + name = "isize", + constructor = "BigInt64Array", + view = "BigInt64", +} + +#[cfg(target_arch = "wasm32")] +typed_vector! { + usize, + name = "usize", + constructor = "Uint32Array", + view = "Uint32", +} + +#[cfg(target_arch = "wasm64")] +typed_vector! { + usize, + name = "usize", + constructor = "BigUint64Array", + view = "BigUint64", +} diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index 6f4ae1b9..01effbc2 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -16,15 +16,30 @@ mod builtins; mod runtime; pub mod hazard; -// Implementations for passing Rust standard types across the JavaScript -// boundary. +// JavaScript `ABI` implementations for Rust types and Rust-facing APIs for +// JavaScript values. mod interop; #[doc(hidden)] pub mod r#macro; pub use builtins::{ - Error, ErrorOptions, Function, JsArray, JsBigInt, JsNumber, JsString, Object, Promise, - PromiseWithResolvers, TryFromJsArrayError, + AggregateError, Array, ArrayBuffer, ArrayBufferOptions, AsyncDisposableStack, AsyncFunction, + AsyncGenerator, AsyncGeneratorFunction, AsyncIterable, AsyncIterator, Atomics, Base64Alphabet, + Base64DecodeOptions, Base64EncodeOptions, Base64LastChunkHandling, BigInt, BigInt64Array, + BigUint64Array, Boolean, DataView, Date, DisposableStack, Error, ErrorOptions, EvalError, + FinalizationRegistry, Float16Array, Float32Array, Float64Array, Function, Generator, + GeneratorFunction, Int8Array, Int16Array, Int32Array, Intl, Iterable, IteratorResult, + IteratorZipKeyedOptions, IteratorZipMode, IteratorZipOptions, JSON, JsIterator, JsString, Map, + Math, Number, Object, Promise, PromiseWithResolvers, PropertyDescriptor, Proxy, ProxyRevocable, + RangeError, ReferenceError, Reflect, RegExp, RegExpIndicesArray, RegExpMatchArray, Set, + SharedArrayBuffer, SuppressedError, Symbol, SyntaxError, Temporal, TypeError, Uint8Array, + Uint8ArraySetResult, Uint8ClampedArray, Uint16Array, Uint32Array, UriError, WeakMap, WeakRef, + WeakSet, WebAssembly, decode_uri, decode_uri_component, encode_uri, encode_uri_component, eval, + global_this, is_finite, is_nan, parse_float, parse_int, parse_int_with_radix, +}; +pub use interop::{ + ArrayIntoIter, ArrayIter, AsyncIter, JsIntoIter, JsIter, TryFromArrayError, TypedArray, + TypedArrayCopyError, TypedArrayIntoIter, TypedArrayIter, try_async_iter, try_iter, }; pub use js_bindgen; pub use js_sys_macro::{closure, js_sys}; diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs index d8ebe6d4..f59c9a1c 100644 --- a/client/js-sys/src/macro.rs +++ b/client/js-sys/src/macro.rs @@ -5,8 +5,10 @@ mod import; mod result; mod text; mod wat; +mod writer; pub use abi::*; +pub use export::*; pub use import::*; pub use result::*; pub use text::*; @@ -15,10 +17,7 @@ pub use wat::*; // Text rendering. pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; // JavaScript export shims. -pub use crate::{ - js_export, js_export_arguments, js_export_input_arguments, js_export_output_expression, - js_export_parameters, js_export_promising, js_export_promising_then, js_export_result_throw, -}; +pub use crate::{js_export, js_export_promising}; // WAT closure shims. pub use crate::{ wat_closure, wat_closure_call, wat_closure_direct, wat_closure_indirect, diff --git a/client/js-sys/src/macro/abi.rs b/client/js-sys/src/macro/abi.rs index 9fd76505..c93deaa1 100644 --- a/client/js-sys/src/macro/abi.rs +++ b/client/js-sys/src/macro/abi.rs @@ -1,5 +1,5 @@ use crate::hazard::{ - FromJS, IntoJS, ReturnAbi, ReturnFromJS, ReturnIntoJS, Slot, WasmAbi, WasmRet, WatConv, + FromJS, IntoJS, ReturnAbi, ReturnFromJS, ReturnIntoJS, Slot, Sret, WasmAbi, WasmRet, WatConv, }; // Rust `ABI` shims used by generated import and export functions. @@ -72,6 +72,30 @@ pub fn join_output(value: OutputRet) -> T { T::from_return_abi(value) } +/// Lifts a return value whose JavaScript conversion is described by another +/// type with the same `ABI`. +/// +/// # Safety +/// +/// The JavaScript value produced for `A` must have the semantics expected by +/// `T`; sharing an `ABI` alone does not make the conversions interchangeable. +#[must_use] +#[inline] +pub unsafe fn join_output_as(value: OutputRet) -> T +where + T: ReturnFromJS, + A: ReturnFromJS, +{ + const { + assert!( + T::JS_CONV.is_result() == A::JS_CONV.is_result(), + "return conversion overrides must preserve Result semantics", + ); + } + + T::from_return_abi(value) +} + // Compile-time validation of conversion `metadata`. #[must_use] @@ -387,6 +411,17 @@ pub const fn js_output_templates() -> [&'static str; 4] { } } +#[must_use] +pub const fn js_output_prepare() -> &'static str { + match T::JS_CONV.conversion() { + Some(conv) => match conv.prepare { + Some(prepare) => prepare, + None => "", + }, + None => "", + } +} + #[must_use] pub const fn js_from_templates() -> [&'static str; 4] { if let Some(conv) = T::JS_CONV { @@ -396,18 +431,27 @@ pub const fn js_from_templates() -> [&'static str; 4] { } } +#[must_use] +pub const fn js_from_prepare() -> &'static str { + if let Some(conv) = T::JS_CONV + && let Some(prepare) = conv.prepare + { + prepare + } else { + "" + } +} + #[must_use] pub const fn js_output_has_conversion() -> bool { T::JS_CONV.conversion().is_some() } #[must_use] -pub const fn js_output_sret() -> &'static str { - if let Some(conv) = T::JS_CONV.conversion() - && let Some(sret) = conv.sret - { - sret +pub const fn js_output_sret() -> Option { + if let Some(conv) = T::JS_CONV.conversion() { + conv.sret } else { - "" + None } } diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs index 1001522a..0227e82b 100644 --- a/client/js-sys/src/macro/export.rs +++ b/client/js-sys/src/macro/export.rs @@ -1,3 +1,9 @@ +mod js; + +pub use js::*; + +use super::writer::Writer; + // WAT shim generation. #[doc(hidden)] @@ -183,232 +189,47 @@ macro_rules! wat_export { }}; } -// JavaScript wrapper helpers. - -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_input_arguments { - ($par:literal, $ty:ty $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::from_js_wat_slots::<$ty>(); - const TEMPLATES: [&::core::primitive::str; 4] = - $crate::r#macro::js_from_templates::<$ty>(); - const VALUES: [&::core::primitive::str; 4] = [ - $crate::r#macro::js_template!(TEMPLATES[0], value = $par), - $crate::r#macro::js_template!(TEMPLATES[1], value = $par), - $crate::r#macro::js_template!(TEMPLATES[2], value = $par), - $crate::r#macro::js_template!(TEMPLATES[3], value = $par), - ]; - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => [VALUES[0]], - !SLOTS[1].abi.is_empty() => [", ", VALUES[1]], - !SLOTS[2].abi.is_empty() => [", ", VALUES[2]], - !SLOTS[3].abi.is_empty() => [", ", VALUES[3]], - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_parameters { - () => { - "" - }; - (($par:literal, $ty:ty) $(,)?) => { - $par - }; - (($par:literal, $ty:ty), $(($rest_par:literal, $rest_ty:ty)),+ $(,)?) => { - $crate::r#macro::const_concat!( - $par, - ", ", - $crate::r#macro::js_export_parameters!($(($rest_par, $rest_ty)),+) - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_arguments { - () => { - "" - }; - (($par:literal, $ty:ty) $(,)?) => { - $crate::r#macro::js_export_input_arguments!($par, $ty) - }; - (($par:literal, $ty:ty), $(($rest_par:literal, $rest_ty:ty)),+ $(,)?) => {{ - const FIRST: &::core::primitive::str = - $crate::r#macro::js_export_input_arguments!($par, $ty); - const REST: &::core::primitive::str = - $crate::r#macro::js_export_arguments!($(($rest_par, $rest_ty)),+); - - $crate::r#macro::const_concat!( - FIRST, - $crate::r#macro::separator_between(FIRST, REST), - REST - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_output_expression { - ($ty:ty $(,)?) => {{ - const DIRECT: ::core::primitive::bool = $crate::r#macro::return_into_js_is_direct::<$ty>(); - const RESULT: ::core::primitive::bool = $crate::r#macro::return_into_js_is_result::<$ty>(); - const VALUES: [&::core::primitive::str; 4] = if RESULT { - ["ret[0]", "ret[1]", "", ""] - } else if DIRECT { - ["ret", "", "", ""] - } else { - ["ret[0]", "ret[1]", "ret[2]", "ret[3]"] - }; - - $crate::r#macro::js_template!( - $crate::r#macro::js_export_output_template::<$ty>(), - slots = VALUES, - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_result_throw { - ($indent:literal, $ty:ty $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = - $crate::r#macro::return_into_js_wat_slots::<$ty>(); - const ERROR_DISCRIMINANT: &::core::primitive::str = if SLOTS[0].abi.is_empty() { - "ret[0]" - } else if SLOTS[1].abi.is_empty() { - "ret[1]" - } else { - "ret[2]" - }; - const ERROR: &::core::primitive::str = if SLOTS[0].abi.is_empty() { - "ret[1]" - } else if SLOTS[1].abi.is_empty() { - "ret[2]" - } else { - "ret[3]" - }; - - if $crate::r#macro::return_into_js_is_result::<$ty>() { - $crate::r#macro::const_concat!( - $indent, - "if (", - ERROR_DISCRIMINANT, - " !== 0) throw ", - ERROR, - "\n", - ) - } else { - "" - } - }}; -} - /// Generates the complete JavaScript wrapper for one Rust export. #[doc(hidden)] #[macro_export] macro_rules! js_export { ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - const PARAMETERS: &::core::primitive::str = - $crate::r#macro::js_export_parameters!($(($par, $input)),*); - const ARGUMENTS: &::core::primitive::str = - $crate::r#macro::js_export_arguments!($(($par, $input)),*); - const PASSTHROUGH: ::core::primitive::bool = true - $(&& !$crate::r#macro::js_from_has_conversion::<$input>())*; - const RAW: &::core::primitive::str = $crate::r#macro::const_concat!( - "wasmExports['", - $export, - "']", - ); - - $($crate::r#macro::validate_from_js::<$input>();)* + const INPUTS: &[$crate::r#macro::ExportInput] = &[ + $($crate::r#macro::export_input::<$input>($par),)* + ]; + const DESCRIPTOR: $crate::r#macro::ExportDescriptor = + $crate::r#macro::ExportDescriptor::new( + $export, + INPUTS, + ::core::option::Option::None, + $crate::r#macro::ExportMode::Sync, + ); + const LEN: ::core::primitive::usize = + $crate::r#macro::export_js_len(&DESCRIPTOR); + const VALUE: [::core::primitive::u8; LEN] = + $crate::r#macro::render_export_js::(&DESCRIPTOR); - if PASSTHROUGH { - RAW - } else { - $crate::r#macro::const_concat!( - "(", - PARAMETERS, - ") => {\n ", - RAW, - "(", - ARGUMENTS, - ")\n}" - ) - } + // SAFETY: Rendering only concatenates and substitutes valid strings. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } }}; ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const PARAMETERS: &::core::primitive::str = - $crate::r#macro::js_export_parameters!($(($par, $input)),*); - const ARGUMENTS: &::core::primitive::str = - $crate::r#macro::js_export_arguments!($(($par, $input)),*); - const OUTPUT: &::core::primitive::str = - $crate::r#macro::js_export_output_expression!($output); - const THROW: &::core::primitive::str = - $crate::r#macro::js_export_result_throw!(" ", $output); - const PASSTHROUGH: ::core::primitive::bool = - !$crate::r#macro::js_return_has_conversion::<$output>() - && !$crate::r#macro::return_into_js_is_result::<$output>() - $(&& !$crate::r#macro::js_from_has_conversion::<$input>())*; - const RAW: &::core::primitive::str = $crate::r#macro::const_concat!( - "wasmExports['", - $export, - "']", - ); - - $($crate::r#macro::validate_from_js::<$input>();)* - $crate::r#macro::validate_return_into_js::<$output>(); - - if PASSTHROUGH { - RAW - } else { - $crate::r#macro::const_concat!( - "(", - PARAMETERS, - ") => {\n const ret = ", - RAW, - "(", - ARGUMENTS, - ")\n", - THROW, - " return ", - OUTPUT, - "\n}" - ) - } - }}; -} - -/// Generates handling for the value produced by a `promising` export. -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_promising_then { - () => { - "" - }; - ($output:ty) => {{ - const OUTPUT: &::core::primitive::str = - $crate::r#macro::js_export_output_expression!($output); - const POSTPROCESS: ::core::primitive::bool = - $crate::r#macro::js_return_has_conversion::<$output>() - || $crate::r#macro::return_into_js_is_result::<$output>(); - const THROW: &::core::primitive::str = - $crate::r#macro::js_export_result_throw!(" ", $output); + const INPUTS: &[$crate::r#macro::ExportInput] = &[ + $($crate::r#macro::export_input::<$input>($par),)* + ]; + const DESCRIPTOR: $crate::r#macro::ExportDescriptor = + $crate::r#macro::ExportDescriptor::new( + $export, + INPUTS, + ::core::option::Option::Some($crate::r#macro::export_output::<$output>()), + $crate::r#macro::ExportMode::Sync, + ); + const LEN: ::core::primitive::usize = + $crate::r#macro::export_js_len(&DESCRIPTOR); + const VALUE: [::core::primitive::u8; LEN] = + $crate::r#macro::render_export_js::(&DESCRIPTOR); - if POSTPROCESS { - $crate::r#macro::const_concat!( - ".then(ret => {\n", - THROW, - " return ", - OUTPUT, - "\n })", - ) - } else { - "" - } + // SAFETY: Rendering only concatenates and substitutes valid strings. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } }}; } @@ -416,40 +237,42 @@ macro_rules! js_export_promising_then { #[doc(hidden)] #[macro_export] macro_rules! js_export_promising { - ( - $export:expr, - ($(($par:literal, $input:ty)),*) - $(, $output:ty)? - $(,)? - ) => {{ - const PARAMETERS: &::core::primitive::str = - $crate::r#macro::js_export_parameters!($(($par, $input)),*); - const ARGUMENTS: &::core::primitive::str = - $crate::r#macro::js_export_arguments!($(($par, $input)),*); - const THEN: &::core::primitive::str = - $crate::r#macro::js_export_promising_then!($($output)?); - const PASSTHROUGH: ::core::primitive::bool = THEN.is_empty() - $(&& !$crate::r#macro::js_from_has_conversion::<$input>())*; - const RAW: &::core::primitive::str = $crate::r#macro::const_concat!( - "WebAssembly.promising(wasmExports['", - $export, - "'])", - ); - const WRAPPED: &::core::primitive::str = $crate::r#macro::const_concat!( - "(() => {\n const $promising = ", - RAW, - "\n return (", - PARAMETERS, - ") => $promising(", - ARGUMENTS, - ")", - THEN, - "\n})()", - ); + ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ + const INPUTS: &[$crate::r#macro::ExportInput] = &[ + $($crate::r#macro::export_input::<$input>($par),)* + ]; + const DESCRIPTOR: $crate::r#macro::ExportDescriptor = + $crate::r#macro::ExportDescriptor::new( + $export, + INPUTS, + ::core::option::Option::None, + $crate::r#macro::ExportMode::Promising, + ); + const LEN: ::core::primitive::usize = + $crate::r#macro::export_js_len(&DESCRIPTOR); + const VALUE: [::core::primitive::u8; LEN] = + $crate::r#macro::render_export_js::(&DESCRIPTOR); - $($crate::r#macro::validate_from_js::<$input>();)* - $($crate::r#macro::validate_return_into_js::<$output>();)? + // SAFETY: Rendering only concatenates and substitutes valid strings. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } + }}; + ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ + const INPUTS: &[$crate::r#macro::ExportInput] = &[ + $($crate::r#macro::export_input::<$input>($par),)* + ]; + const DESCRIPTOR: $crate::r#macro::ExportDescriptor = + $crate::r#macro::ExportDescriptor::new( + $export, + INPUTS, + ::core::option::Option::Some($crate::r#macro::export_output::<$output>()), + $crate::r#macro::ExportMode::Promising, + ); + const LEN: ::core::primitive::usize = + $crate::r#macro::export_js_len(&DESCRIPTOR); + const VALUE: [::core::primitive::u8; LEN] = + $crate::r#macro::render_export_js::(&DESCRIPTOR); - if PASSTHROUGH { RAW } else { WRAPPED } + // SAFETY: Rendering only concatenates and substitutes valid strings. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } }}; } diff --git a/client/js-sys/src/macro/export/js.rs b/client/js-sys/src/macro/export/js.rs new file mode 100644 index 00000000..9f191287 --- /dev/null +++ b/client/js-sys/src/macro/export/js.rs @@ -0,0 +1,430 @@ +use core::marker::PhantomData; + +use super::Writer; +use crate::hazard::{FromJS, ReturnIntoJS}; +use crate::r#macro::text::{JS_TEMPLATE_PLACEHOLDERS, js_template_placeholder}; +use crate::r#macro::{ + WatSlot, from_js_wat_slots, js_export_output_template, js_from_has_conversion, js_from_prepare, + js_from_templates, js_return_has_conversion, return_into_js_is_direct, + return_into_js_is_result, return_into_js_wat_slots, validate_from_js, validate_return_into_js, +}; + +/// All target-dependent information needed to render one exported argument. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ExportInput { + name: &'static str, + ty: &'static ExportInputType, +} + +#[derive(Clone, Copy)] +struct ExportInputType { + slots: [WatSlot; 4], + prepare: &'static str, + templates: [&'static str; 4], + has_conversion: bool, +} + +/// All target-dependent information needed to render one exported result. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ExportOutput { + slots: [WatSlot; 4], + template: &'static str, + direct: bool, + result: bool, + has_conversion: bool, +} + +/// Selects the JavaScript export wrapper generated for a descriptor. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub enum ExportMode { + Sync, + Promising, +} + +/// A semantic description of one JavaScript export. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct ExportDescriptor { + name: &'static str, + inputs: &'static [ExportInput], + output: Option<&'static ExportOutput>, + mode: ExportMode, +} + +struct ExportInputMetadata(PhantomData); + +impl ExportInputMetadata { + const VALUE: ExportInputType = { + validate_from_js::(); + + ExportInputType { + slots: from_js_wat_slots::(), + prepare: js_from_prepare::(), + templates: js_from_templates::(), + has_conversion: js_from_has_conversion::(), + } + }; +} + +struct ExportOutputMetadata(PhantomData); + +impl ExportOutputMetadata { + const VALUE: ExportOutput = { + validate_return_into_js::(); + + ExportOutput { + slots: return_into_js_wat_slots::(), + template: js_export_output_template::(), + direct: return_into_js_is_direct::(), + result: return_into_js_is_result::(), + has_conversion: js_return_has_conversion::(), + } + }; +} + +/// Builds the descriptor for one exported argument. +#[doc(hidden)] +#[must_use] +pub const fn export_input(name: &'static str) -> ExportInput { + ExportInput { + name, + ty: &ExportInputMetadata::::VALUE, + } +} + +/// Returns the descriptor for an exported result. +#[doc(hidden)] +#[must_use] +pub const fn export_output() -> &'static ExportOutput { + &ExportOutputMetadata::::VALUE +} + +impl ExportDescriptor { + /// Creates an export descriptor. + #[doc(hidden)] + #[must_use] + pub const fn new( + name: &'static str, + inputs: &'static [ExportInput], + output: Option<&'static ExportOutput>, + mode: ExportMode, + ) -> Self { + Self { + name, + inputs, + output, + mode, + } + } + + const fn is_passthrough(&self) -> bool { + let mut input = 0; + while input < self.inputs.len() { + if self.inputs[input].ty.has_conversion { + return false; + } + input += 1; + } + + match self.output { + Some(output) => !output.needs_postprocess(), + None => true, + } + } + + const fn has_prepares(&self) -> bool { + let mut input = 0; + while input < self.inputs.len() { + if !self.inputs[input].ty.prepare.is_empty() { + return true; + } + input += 1; + } + + false + } + + const fn write(&self, writer: &mut Writer) { + if self.is_passthrough() { + self.write_raw(writer); + return; + } + + match self.mode { + ExportMode::Sync => self.write_sync(writer), + ExportMode::Promising => self.write_promising(writer), + } + } + + const fn write_sync(&self, writer: &mut Writer) { + writer.write_byte(b'('); + self.write_parameters(writer); + writer.write_str(") => {\n"); + self.write_prepares(writer, " "); + + if let Some(output) = self.output { + writer.write_str(" const ret = "); + self.write_raw_call(writer); + writer.write_byte(b'\n'); + output.write_result_throw(writer, " "); + writer.write_str(" return "); + output.write_expression(writer); + writer.write_str("\n}"); + } else { + writer.write_str(" "); + self.write_raw_call(writer); + writer.write_str("\n}"); + } + } + + const fn write_promising(&self, writer: &mut Writer) { + writer.write_str("(() => {\n const $promising = "); + self.write_raw(writer); + writer.write_str("\n return ("); + self.write_parameters(writer); + + if self.has_prepares() { + writer.write_str(") => {\n"); + self.write_prepares(writer, " "); + writer.write_str(" return $promising("); + self.write_arguments(writer); + writer.write_byte(b')'); + self.write_promising_then(writer); + writer.write_str("\n }\n})()"); + } else { + writer.write_str(") => $promising("); + self.write_arguments(writer); + writer.write_byte(b')'); + self.write_promising_then(writer); + writer.write_str("\n})()"); + } + } + + const fn write_promising_then(&self, writer: &mut Writer) { + let Some(output) = self.output else { + return; + }; + if !output.needs_postprocess() { + return; + } + + writer.write_str(".then(ret => {\n"); + output.write_result_throw(writer, " "); + writer.write_str(" return "); + output.write_expression(writer); + writer.write_str("\n })"); + } + + const fn write_raw(&self, writer: &mut Writer) { + if matches!(self.mode, ExportMode::Promising) { + writer.write_str("WebAssembly.promising("); + } + + writer.write_str("wasmExports['"); + writer.write_str(self.name); + writer.write_str("']"); + + if matches!(self.mode, ExportMode::Promising) { + writer.write_byte(b')'); + } + } + + const fn write_raw_call(&self, writer: &mut Writer) { + self.write_raw(writer); + writer.write_byte(b'('); + self.write_arguments(writer); + writer.write_byte(b')'); + } + + const fn write_parameters(&self, writer: &mut Writer) { + let mut input = 0; + while input < self.inputs.len() { + if input != 0 { + writer.write_str(", "); + } + writer.write_str(self.inputs[input].name); + input += 1; + } + } + + const fn write_arguments(&self, writer: &mut Writer) { + let mut wrote_argument = false; + let mut input = 0; + + while input < self.inputs.len() { + let descriptor = &self.inputs[input]; + let mut slot = 0; + + while slot < descriptor.ty.slots.len() { + if !descriptor.ty.slots[slot].abi.is_empty() { + if wrote_argument { + writer.write_str(", "); + } + write_template( + writer, + descriptor.ty.templates[slot], + TemplateValues::Input(descriptor.name), + ); + wrote_argument = true; + } + slot += 1; + } + + input += 1; + } + } + + const fn write_prepares(&self, writer: &mut Writer, indent: &str) { + let mut input = 0; + while input < self.inputs.len() { + let descriptor = &self.inputs[input]; + if !descriptor.ty.prepare.is_empty() { + writer.write_str(indent); + writer.write_str("const "); + writer.write_str(descriptor.name); + writer.write_str("$prepared = "); + write_template( + writer, + descriptor.ty.prepare, + TemplateValues::Value(descriptor.name), + ); + writer.write_byte(b'\n'); + } + input += 1; + } + } +} + +impl ExportOutput { + const fn needs_postprocess(&self) -> bool { + self.has_conversion || self.result + } + + const fn write_expression(&self, writer: &mut Writer) { + write_template( + writer, + self.template, + TemplateValues::Output { + direct: self.direct, + result: self.result, + }, + ); + } + + const fn write_result_throw(&self, writer: &mut Writer, indent: &str) { + if !self.result { + return; + } + + let discriminant = if self.slots[0].abi.is_empty() { + 0 + } else if self.slots[1].abi.is_empty() { + 1 + } else { + 2 + }; + + writer.write_str(indent); + writer.write_str("if ("); + write_ret_index(writer, discriminant); + writer.write_str(" !== 0) throw "); + write_ret_index(writer, discriminant + 1); + writer.write_byte(b'\n'); + } +} + +/// Returns the exact byte length produced by [`render_export_js`]. +#[doc(hidden)] +#[must_use] +pub const fn export_js_len(descriptor: &ExportDescriptor) -> usize { + let mut writer = Writer::<0>::new(); + descriptor.write(&mut writer); + writer.len() +} + +/// Renders one JavaScript export into an exact-size byte array. +#[doc(hidden)] +#[must_use] +pub const fn render_export_js(descriptor: &ExportDescriptor) -> [u8; LEN] { + let mut writer = Writer::::new(); + descriptor.write(&mut writer); + assert!(writer.len() == LEN); + writer.finish_padded() +} + +#[derive(Clone, Copy)] +enum TemplateValues<'a> { + Input(&'a str), + Value(&'a str), + Output { direct: bool, result: bool }, +} + +const fn write_template( + writer: &mut Writer, + template: &str, + values: TemplateValues<'_>, +) { + let bytes = template.as_bytes(); + let mut input = 0; + + while input < bytes.len() { + let placeholder = js_template_placeholder(bytes, input); + + if placeholder == 0 { + match values { + TemplateValues::Input(value) | TemplateValues::Value(value) => { + writer.write_str(value); + } + TemplateValues::Output { .. } => {} + } + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else if placeholder == 1 { + if let TemplateValues::Input(name) = values { + writer.write_str(name); + writer.write_str("$prepared"); + } + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { + if let TemplateValues::Output { direct, result } = values { + write_output_slot(writer, direct, result, placeholder - 2); + } + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else { + let start = input; + input += 1; + while input < bytes.len() && bytes[input] != b'$' { + input += 1; + } + writer.write_str_range(template, start, input); + } + } +} + +const fn write_output_slot( + writer: &mut Writer, + direct: bool, + result: bool, + slot: usize, +) { + if result { + if slot < 2 { + write_ret_index(writer, slot); + } + } else if direct { + if slot == 0 { + writer.write_str("ret"); + } + } else { + write_ret_index(writer, slot); + } +} + +const fn write_ret_index(writer: &mut Writer, index: usize) { + assert!(index < 4); + writer.write_str("ret["); + writer.write_byte(b"0123"[index]); + writer.write_byte(b']'); +} diff --git a/client/js-sys/src/macro/import.rs b/client/js-sys/src/macro/import.rs index 1457bbda..496e6e83 100644 --- a/client/js-sys/src/macro/import.rs +++ b/client/js-sys/src/macro/import.rs @@ -1,18 +1,21 @@ mod js; mod wat; -mod writer; use core::marker::PhantomData; -use writer::Writer; - +use super::writer::Writer; use super::{ - WatSlot, into_js_wat_slots, js_input_template, js_output_has_conversion, js_output_sret, - js_output_templates, js_result_catch, js_result_try, return_from_js_is_direct, + WatSlot, into_js_wat_slots, js_input_template, js_output_has_conversion, js_output_prepare, + js_output_sret, js_output_templates, js_result_catch, js_result_try, return_from_js_is_direct, return_from_js_wat_slots, validate_into_js, validate_return_from_js, wat_result_catch, wat_result_default, wat_result_imports, wat_result_locals, wat_result_try, }; -use crate::hazard::{IntoJS, ReturnFromJS}; +use crate::hazard::{IntoJS, ReturnFromJS, Sret}; + +const JS_RETPTR_CONV: &str = crate::js_template!( + js_input_template::>(), + slots = ["$retptr", "", "", ""], +); /// All target-dependent information needed to render one imported argument. #[doc(hidden)] @@ -38,8 +41,9 @@ pub struct ImportOutput { slots: [WatSlot; 4], pointer: WatSlot, has_js_conversion: bool, + js_prepare: &'static str, js_templates: [&'static str; 4], - js_sret: &'static str, + js_sret: Option, js_try: &'static str, js_catch: &'static str, wat_result_imports: &'static str, @@ -125,6 +129,7 @@ impl ImportOutputMetadata { slots, pointer: into_js_wat_slots::>()[0], has_js_conversion: js_output_has_conversion::(), + js_prepare: js_output_prepare::(), js_templates: js_output_templates::(), js_sret: js_output_sret::(), js_try: js_result_try::(), diff --git a/client/js-sys/src/macro/import/js.rs b/client/js-sys/src/macro/import/js.rs index 2d23660d..e18d6677 100644 --- a/client/js-sys/src/macro/import/js.rs +++ b/client/js-sys/src/macro/import/js.rs @@ -1,4 +1,8 @@ -use super::{Capacity, ImportDescriptor, ImportInput, ImportJs, ImportOutput, Writer}; +use super::{ + Capacity, ImportDescriptor, ImportInput, ImportJs, ImportOutput, JS_RETPTR_CONV, Writer, +}; +use crate::hazard::Sret; +use crate::r#macro::text::{JS_TEMPLATE_PLACEHOLDERS, js_template_placeholder}; pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize { let Some(js) = descriptor.js else { @@ -69,8 +73,9 @@ pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize } None => { if wrapped { + capacity.add_str(" "); if descriptor.suspending { - capacity.add_str(" return "); + capacity.add_str("return "); } capacity.add_str(js.indirect_call); capacity.add_str("\n}"); @@ -119,7 +124,7 @@ const fn add_input_conversion_capacity(capacity: &mut Capacity, input: &ImportIn capacity.add_str(" "); add_slot_name_capacity(capacity, input.name); capacity.add_str(" = "); - add_template_capacity(capacity, input.ty.js_template, "", Some(input.name)); + add_template_capacity(capacity, input.ty.js_template, "", "", Some(input.name)); capacity.add(1); } @@ -138,6 +143,12 @@ const fn add_output_capacity( }; let indent = if catches_result { " " } else { " " }; + if !output.direct && !JS_RETPTR_CONV.is_empty() { + capacity.add_str(" $retptr = "); + capacity.add_str(JS_RETPTR_CONV); + capacity.add(1); + } + capacity.add_str(output.js_try); if convert_direct { @@ -155,33 +166,64 @@ const fn add_output_capacity( } if output.direct && !convert_direct { - add_template_capacity(capacity, output.js_templates[0], call, None); + add_template_capacity(capacity, output.js_templates[0], call, "", None); } else { capacity.add_str(call); } if convert_direct { + if !output.js_prepare.is_empty() { + capacity.add(1); + capacity.add_str(indent); + capacity.add_str("const $prepared = "); + add_template_capacity(capacity, output.js_prepare, "$ret", "", None); + } + capacity.add(1); capacity.add_str(indent); capacity.add_str("return "); - add_template_capacity(capacity, output.js_templates[0], "$ret", None); + add_template_capacity(capacity, output.js_templates[0], "$ret", "$prepared", None); } if !output.direct { - capacity.add(1); - capacity.add_str(indent); - capacity.add_str(output.js_sret); - capacity.add(1); - add_template_capacity(capacity, output.js_templates[0], "$ret", None); + match output.js_sret { + Some(Sret::Slots(function)) => { + if !output.js_prepare.is_empty() { + capacity.add(1); + capacity.add_str(indent); + capacity.add_str("const $prepared = "); + add_template_capacity(capacity, output.js_prepare, "$ret", "", None); + } - let mut slot = 1; - while slot < output.js_templates.len() { - capacity.add_str(", "); - add_template_capacity(capacity, output.js_templates[slot], "$ret", None); - slot += 1; - } + capacity.add(1); + capacity.add_str(indent); + capacity.add_str(function); + capacity.add(1); + add_template_capacity(capacity, output.js_templates[0], "$ret", "$prepared", None); + + let mut slot = 1; + while slot < output.js_templates.len() { + capacity.add_str(", "); + add_template_capacity( + capacity, + output.js_templates[slot], + "$ret", + "$prepared", + None, + ); + slot += 1; + } - capacity.add_str(", $retptr)"); + capacity.add_str(", $retptr)"); + } + Some(Sret::Value(function)) => { + capacity.add(1); + capacity.add_str(indent); + capacity.add_str(function); + capacity.add_str("($ret, $retptr)"); + } + None => panic!("indirect output missing sret"), + } } if catches_result { @@ -200,9 +242,14 @@ const fn add_template_capacity( capacity: &mut Capacity, template: &str, value: &str, + prepared: &str, slots: Option<&str>, ) { - let mut maximum_replacement = value.len(); + let mut maximum_replacement = if value.len() > prepared.len() { + value.len() + } else { + prepared.len() + }; if let Some(name) = slots { let mut slot_len = name.len(); @@ -220,7 +267,7 @@ const fn add_template_capacity( // placeholder is six bytes long, so at most `len / 6` are replaced. capacity.add_str(template); let Some(replacements) = - (template.len() / PLACEHOLDERS[0].len()).checked_mul(maximum_replacement) + (template.len() / JS_TEMPLATE_PLACEHOLDERS[0].len()).checked_mul(maximum_replacement) else { panic!("import section capacity overflows usize"); }; @@ -297,8 +344,9 @@ impl ImportDescriptor { Some(output) => write_output(writer, output, js, wrapped, await_output), None => { if wrapped { + writer.write_str(" "); if self.suspending { - writer.write_str(" return "); + writer.write_str("return "); } writer.write_str(js.indirect_call); writer.write_str("\n}"); @@ -352,7 +400,7 @@ const fn write_input_conversion(writer: &mut Writer, inpu writer.write_str(" "); write_slot_name(writer, input.name, 0); writer.write_str(" = "); - write_template(writer, input.ty.js_template, "", Some(input.name)); + write_template(writer, input.ty.js_template, "", "", Some(input.name)); writer.write_byte(b'\n'); } @@ -377,6 +425,12 @@ const fn write_output( }; let indent = if catches_result { " " } else { " " }; + if !output.direct && !JS_RETPTR_CONV.is_empty() { + writer.write_str(" $retptr = "); + writer.write_str(JS_RETPTR_CONV); + writer.write_byte(b'\n'); + } + writer.write_str(output.js_try); if convert_direct { @@ -397,7 +451,7 @@ const fn write_output( if await_output { writer.write_str("await ("); } - write_template(writer, output.js_templates[0], template_value, None); + write_template(writer, output.js_templates[0], template_value, "", None); } else { if await_output { writer.write_str("await ("); @@ -409,30 +463,61 @@ const fn write_output( } if convert_direct { + if !output.js_prepare.is_empty() { + writer.write_byte(b'\n'); + writer.write_str(indent); + writer.write_str("const $prepared = "); + write_template(writer, output.js_prepare, "$ret", "", None); + } + writer.write_byte(b'\n'); writer.write_str(indent); writer.write_str("return "); - write_template(writer, output.js_templates[0], "$ret", None); + write_template(writer, output.js_templates[0], "$ret", "$prepared", None); } if !output.direct { - writer.write_byte(b'\n'); - writer.write_str(indent); - writer.write_str(output.js_sret); - writer.write_byte(b'('); - write_template(writer, output.js_templates[0], "$ret", None); + match output.js_sret { + Some(Sret::Slots(function)) => { + if !output.js_prepare.is_empty() { + writer.write_byte(b'\n'); + writer.write_str(indent); + writer.write_str("const $prepared = "); + write_template(writer, output.js_prepare, "$ret", "", None); + } - let mut slot = 1; - while slot < output.js_templates.len() { - if template_len(output.js_templates[slot], "$ret", None) != 0 { - writer.write_str(", "); - write_template(writer, output.js_templates[slot], "$ret", None); - } + writer.write_byte(b'\n'); + writer.write_str(indent); + writer.write_str(function); + writer.write_byte(b'('); + write_template(writer, output.js_templates[0], "$ret", "$prepared", None); + + let mut slot = 1; + while slot < output.js_templates.len() { + if template_len(output.js_templates[slot], "$ret", "$prepared", None) != 0 { + writer.write_str(", "); + write_template( + writer, + output.js_templates[slot], + "$ret", + "$prepared", + None, + ); + } - slot += 1; - } + slot += 1; + } - writer.write_str(", $retptr)"); + writer.write_str(", $retptr)"); + } + Some(Sret::Value(function)) => { + writer.write_byte(b'\n'); + writer.write_str(indent); + writer.write_str(function); + writer.write_str("($ret, $retptr)"); + } + None => panic!("indirect output missing sret"), + } } if catches_result { @@ -449,29 +534,31 @@ const fn write_slot_name(writer: &mut Writer, name: &str, writer.write_byte(b"0123"[slot]); } -const PLACEHOLDERS: [&[u8]; 5] = [b"$value", b"$slot1", b"$slot2", b"$slot3", b"$slot4"]; - const fn write_template( writer: &mut Writer, template: &str, value: &str, + prepared: &str, slots: Option<&str>, ) { let bytes = template.as_bytes(); let mut input = 0; while input < bytes.len() { - let placeholder = template_placeholder(bytes, input); + let placeholder = js_template_placeholder(bytes, input); if placeholder == 0 { writer.write_str(value); - input += PLACEHOLDERS[placeholder].len(); - } else if placeholder < PLACEHOLDERS.len() { + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else if placeholder == 1 { + writer.write_str(prepared); + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { if let Some(name) = slots { - write_slot_name(writer, name, placeholder - 1); + write_slot_name(writer, name, placeholder - 2); } - input += PLACEHOLDERS[placeholder].len(); + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); } else { let start = input; input += 1; @@ -485,23 +572,26 @@ const fn write_template( } } -const fn template_len(template: &str, value: &str, slots: Option<&str>) -> usize { +const fn template_len(template: &str, value: &str, prepared: &str, slots: Option<&str>) -> usize { let bytes = template.as_bytes(); let mut input = 0; let mut output = 0; while input < bytes.len() { - let placeholder = template_placeholder(bytes, input); + let placeholder = js_template_placeholder(bytes, input); if placeholder == 0 { output += value.len(); - input += PLACEHOLDERS[placeholder].len(); - } else if placeholder < PLACEHOLDERS.len() { + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else if placeholder == 1 { + output += prepared.len(); + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); + } else if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { if let Some(name) = slots { output += name.len() + 2; } - input += PLACEHOLDERS[placeholder].len(); + input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); } else { let start = input; input += 1; @@ -516,36 +606,3 @@ const fn template_len(template: &str, value: &str, slots: Option<&str>) -> usize output } - -const fn template_placeholder(template: &[u8], index: usize) -> usize { - if template[index] != b'$' { - return PLACEHOLDERS.len(); - } - - let mut placeholder = 0; - while placeholder < PLACEHOLDERS.len() { - let candidate = PLACEHOLDERS[placeholder]; - - if index + candidate.len() <= template.len() { - let mut byte = 0; - let mut matches = true; - - while byte < candidate.len() { - if template[index + byte] != candidate[byte] { - matches = false; - break; - } - - byte += 1; - } - - if matches { - return placeholder; - } - } - - placeholder += 1; - } - - PLACEHOLDERS.len() -} diff --git a/client/js-sys/src/macro/import/wat.rs b/client/js-sys/src/macro/import/wat.rs index 26ed685f..38e1d651 100644 --- a/client/js-sys/src/macro/import/wat.rs +++ b/client/js-sys/src/macro/import/wat.rs @@ -1,5 +1,6 @@ use super::{Capacity, ImportDescriptor, ImportOutput, WatInputCapacity, Writer}; use crate::r#macro::WatSlot; +use crate::r#macro::wat::{wat_line_end, wat_lines_equal}; pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize { let mut capacity = Capacity::new(); @@ -184,7 +185,7 @@ pub(super) const fn write_wat_support_imports( let mut line_start = 0; while line_start < bytes.len() { - let line_end = line_end(source, line_start); + let line_end = wat_line_end(source, line_start); if line_end != line_start { let mut was_seen = false; @@ -193,7 +194,7 @@ pub(super) const fn write_wat_support_imports( while seen_index < seen_len { let candidate = seen[seen_index]; - if lines_equal( + if wat_lines_equal( source, line_start, line_end, @@ -275,10 +276,10 @@ const fn previous_line_was_seen( let mut candidate_start = 0; while candidate_start < limit { - let candidate_end = line_end(candidate, candidate_start); + let candidate_end = wat_line_end(candidate, candidate_start); if candidate_end != candidate_start - && lines_equal( + && wat_lines_equal( value, line_start, current_line_end, @@ -441,7 +442,7 @@ impl ImportDescriptor { let mut line_start = 0; while line_start < bytes.len() { - let line_end = line_end(source, line_start); + let line_end = wat_line_end(source, line_start); if line_end != line_start { let mut was_seen = false; @@ -450,7 +451,7 @@ impl ImportDescriptor { while seen_index < seen_len { let candidate = seen[seen_index]; - if lines_equal( + if wat_lines_equal( source, line_start, line_end, @@ -540,10 +541,10 @@ impl ImportDescriptor { let mut candidate_start = 0; while candidate_start < limit { - let candidate_end = line_end(candidate, candidate_start); + let candidate_end = wat_line_end(candidate, candidate_start); if candidate_end != candidate_start - && lines_equal( + && wat_lines_equal( value, line_start, current_line_end, @@ -716,40 +717,3 @@ const fn get_suffix(slot: usize) -> &'static str { _ => panic!("a Wasm ABI has exactly four slots"), } } - -const fn line_end(value: &str, start: usize) -> usize { - let bytes = value.as_bytes(); - let mut end = start; - - while end < bytes.len() && bytes[end] != b'\n' { - end += 1; - } - - end -} - -const fn lines_equal( - left: &str, - left_start: usize, - left_end: usize, - right: &str, - right_start: usize, - right_end: usize, -) -> bool { - if left_end - left_start != right_end - right_start { - return false; - } - - let left = left.as_bytes(); - let right = right.as_bytes(); - let mut offset = 0; - - while left_start + offset < left_end { - if left[left_start + offset] != right[right_start + offset] { - return false; - } - offset += 1; - } - - true -} diff --git a/client/js-sys/src/macro/result.rs b/client/js-sys/src/macro/result.rs index 8e8c3124..8f8a612e 100644 --- a/client/js-sys/src/macro/result.rs +++ b/client/js-sys/src/macro/result.rs @@ -7,7 +7,7 @@ use crate::runtime::externref::{ #[cfg(not(target_feature = "exception-handling"))] const DIRECT_CATCH: &str = " } catch ($error) { - const $index = this.#instance.exports['js_sys.exception.store']() + const $index = this.#jsExports['js_sys.exception.store']() this.#jsEmbed.js_sys['externref.table'].set($index, $error) return false } @@ -15,7 +15,7 @@ const DIRECT_CATCH: &str = " #[cfg(not(target_feature = "exception-handling"))] const INDIRECT_CATCH: &str = " } catch ($error) { - const $index = this.#instance.exports['js_sys.exception.store']() + const $index = this.#jsExports['js_sys.exception.store']() this.#jsEmbed.js_sys['externref.table'].set($index, $error) } }"; diff --git a/client/js-sys/src/macro/text.rs b/client/js-sys/src/macro/text.rs index 769e383a..de80cc67 100644 --- a/client/js-sys/src/macro/text.rs +++ b/client/js-sys/src/macro/text.rs @@ -2,11 +2,17 @@ #[macro_export] macro_rules! js_template { ($template:expr, value = $value:expr $(,)?) => { - $crate::r#macro::js_template!(@render $template, [$value, "", "", "", ""]) + $crate::r#macro::js_template!(@render $template, [$value, "", "", "", "", ""]) + }; + ($template:expr, value = $value:expr, prepared = $prepared:expr $(,)?) => { + $crate::r#macro::js_template!(@render $template, [ + $value, $prepared, "", "", "", "" + ]) }; ($template:expr, slots = $slots:expr $(,)?) => {{ const JS_TEMPLATE_SLOTS: [&::core::primitive::str; 4] = $slots; $crate::r#macro::js_template!(@render $template, [ + "", "", JS_TEMPLATE_SLOTS[0], JS_TEMPLATE_SLOTS[1], @@ -14,9 +20,9 @@ macro_rules! js_template { JS_TEMPLATE_SLOTS[3], ]) }}; - (@render $template:expr, [$value:expr, $slot1:expr, $slot2:expr, $slot3:expr, $slot4:expr $(,)?]) => {{ - const JS_TEMPLATE_REPLACEMENTS: [&::core::primitive::str; 5] = - [$value, $slot1, $slot2, $slot3, $slot4]; + (@render $template:expr, [$value:expr, $prepared:expr, $slot1:expr, $slot2:expr, $slot3:expr, $slot4:expr $(,)?]) => {{ + const JS_TEMPLATE_REPLACEMENTS: [&::core::primitive::str; 6] = + [$value, $prepared, $slot1, $slot2, $slot3, $slot4]; const JS_TEMPLATE_LEN: ::core::primitive::usize = $crate::r#macro::js_template_len( $template, &JS_TEMPLATE_REPLACEMENTS, @@ -178,9 +184,16 @@ const fn append_str(output: &mut [u8; LEN], offset: usize, val end } -const JS_TEMPLATE_PLACEHOLDERS: [&str; 5] = ["$value", "$slot1", "$slot2", "$slot3", "$slot4"]; +pub(super) const JS_TEMPLATE_PLACEHOLDERS: [&str; 6] = [ + "$value", + "$prepared", + "$slot1", + "$slot2", + "$slot3", + "$slot4", +]; -const fn js_template_placeholder(template: &[u8], index: usize) -> usize { +pub(super) const fn js_template_placeholder(template: &[u8], index: usize) -> usize { if template[index] != b'$' { return JS_TEMPLATE_PLACEHOLDERS.len(); } @@ -215,7 +228,7 @@ const fn js_template_placeholder(template: &[u8], index: usize) -> usize { } #[must_use] -pub const fn js_template_len(template: &str, replacements: &[&str; 5]) -> usize { +pub const fn js_template_len(template: &str, replacements: &[&str; 6]) -> usize { let template = template.as_bytes(); let mut input = 0; let mut output = 0; @@ -238,7 +251,7 @@ pub const fn js_template_len(template: &str, replacements: &[&str; 5]) -> usize #[must_use] pub const fn render_js_template( template: &str, - replacements: &[&str; 5], + replacements: &[&str; 6], ) -> [u8; LEN] { let template = template.as_bytes(); let mut rendered = [0; LEN]; diff --git a/client/js-sys/src/macro/wat.rs b/client/js-sys/src/macro/wat.rs index 78f72022..4a7ec087 100644 --- a/client/js-sys/src/macro/wat.rs +++ b/client/js-sys/src/macro/wat.rs @@ -4,7 +4,7 @@ pub const fn wat_conv_prefix(value: &str) -> &'static str { } #[must_use] -const fn wat_line_end(value: &str, start: usize) -> usize { +pub(super) const fn wat_line_end(value: &str, start: usize) -> usize { let bytes = value.as_bytes(); let mut end = start; @@ -15,7 +15,7 @@ const fn wat_line_end(value: &str, start: usize) -> usize { end } -const fn wat_lines_equal( +pub(super) const fn wat_lines_equal( left: &str, left_start: usize, left_end: usize, diff --git a/client/js-sys/src/macro/import/writer.rs b/client/js-sys/src/macro/writer.rs similarity index 96% rename from client/js-sys/src/macro/import/writer.rs rename to client/js-sys/src/macro/writer.rs index d854b8e8..95dbece9 100644 --- a/client/js-sys/src/macro/import/writer.rs +++ b/client/js-sys/src/macro/writer.rs @@ -1,4 +1,4 @@ -/// A const writer that renders directly into a custom section allocation. +/// A const writer that either counts bytes or renders into a fixed allocation. pub(super) struct Writer { bytes: [u8; LEN], position: usize, diff --git a/client/js-sys/src/runtime/allocator.rs b/client/js-sys/src/runtime/allocator.rs new file mode 100644 index 00000000..faabeb56 --- /dev/null +++ b/client/js-sys/src/runtime/allocator.rs @@ -0,0 +1,79 @@ +use alloc::alloc::{alloc, dealloc, handle_alloc_error, realloc}; +use core::alloc::Layout; +use core::ptr::NonNull; + +#[unsafe(export_name = "js_sys.memory.alloc")] +extern "C" fn allocate(size: usize, align: usize) -> *mut u8 { + let layout = layout(size, align); + if size == 0 { + return core::ptr::without_provenance_mut(layout.align()); + } + + // SAFETY: `layout` is non-empty and valid. + NonNull::new(unsafe { alloc(layout) }) + .unwrap_or_else(|| handle_alloc_error(layout)) + .as_ptr() +} + +pub(super) fn allocate_slice(len: usize) -> *mut T { + let layout = slice_layout::(len); + allocate(layout.size(), layout.align()).cast() +} + +#[unsafe(export_name = "js_sys.memory.realloc")] +unsafe extern "C" fn reallocate( + ptr: *mut u8, + old_size: usize, + new_size: usize, + align: usize, +) -> *mut u8 { + if old_size == 0 { + return allocate(new_size, align); + } + if new_size == 0 { + // SAFETY: The caller transfers the allocation described by this layout. + unsafe { dealloc(ptr, layout(old_size, align)) }; + return core::ptr::without_provenance_mut(align); + } + + let old_layout = layout(old_size, align); + let new_layout = layout(new_size, align); + // SAFETY: The caller transfers the allocation described by `old_layout`; + // the returned pointer owns the `resized` allocation. + NonNull::new(unsafe { realloc(ptr, old_layout, new_size) }) + .unwrap_or_else(|| handle_alloc_error(new_layout)) + .as_ptr() +} + +#[unsafe(export_name = "js_sys.memory.free")] +unsafe extern "C" fn release(ptr: *mut u8, size: usize, align: usize) { + if size == 0 { + return; + } + + // SAFETY: The caller transfers the allocation described by this layout and + // never uses it again. + unsafe { dealloc(ptr, layout(size, align)) }; +} + +pub(super) unsafe fn release_slice(ptr: *mut T, len: usize) { + let layout = slice_layout::(len); + // SAFETY: The caller transfers the slice allocation described by `layout`. + unsafe { release(ptr.cast(), layout.size(), layout.align()) }; +} + +#[inline] +fn layout(size: usize, align: usize) -> Layout { + match Layout::from_size_align(size, align) { + Ok(layout) => layout, + Err(_) => handle_alloc_error(Layout::new::()), + } +} + +#[inline] +fn slice_layout(len: usize) -> Layout { + match Layout::array::(len) { + Ok(layout) => layout, + Err(_) => handle_alloc_error(Layout::new::()), + } +} diff --git a/client/js-sys/src/runtime/closure.rs b/client/js-sys/src/runtime/closure.rs index e3a9aca4..9bad2ca0 100644 --- a/client/js-sys/src/runtime/closure.rs +++ b/client/js-sys/src/runtime/closure.rs @@ -1,10 +1,12 @@ use alloc::boxed::Box; use core::marker::PhantomData; use core::mem::{self, ManuallyDrop}; +use core::ops::Deref; use core::ptr; use crate::JsValue; -use crate::hazard::{IntoJS, IntoJsConv}; +use crate::builtins::Function; +use crate::hazard::{IntoJS, IntoJsConv, JsCast}; #[crate::js_sys(js_sys = crate)] extern "js-sys" { @@ -136,14 +138,14 @@ js_bindgen::embed_js!( module = "js_sys", name = "closure.finalization", "typeof FinalizationRegistry === 'undefined'", - " ? {{ register: () => {{}}, unregister: () => {{}} }}", - " : new FinalizationRegistry(state => {{", + " ? {{ register: () => {{}}, unregister: () => {{}} }}", + " : new FinalizationRegistry(state => {{", // The `unref` function can outlive its callback. Clear the pointer before // releasing it so a later call cannot release the allocation twice. - " const data = state.data", - " state.data = 0", - " if (data) this.#jsExports.closure_drop(data)", - " }})", + " const data = state.data", + " state.data = 0", + " if (data) this.#jsExports.closure_drop(data)", + " }})", ); js_bindgen::embed_js!( @@ -151,25 +153,25 @@ js_bindgen::embed_js!( name = "closure.own", required_embeds = [("js_sys", "closure.finalization")], "(callback, state) => {{", - " let owned = true", - " const release = () => {{", - " state.references -= 1", - " if (state.references === 0) {{", - " const data = state.data", - " state.data = 0", - " this.#jsEmbed.js_sys['closure.finalization'].unregister(state)", - " if (data) this.#jsExports.closure_drop(data)", - " }}", - " }}", - " callback.unref = () => {{", - " if (!owned) return", - " owned = false", - " release()", - " }}", - " this.#jsEmbed.js_sys['closure.finalization'].register(", - " callback, state, state", - " )", - " return release", + " let owned = true", + " const release = () => {{", + " state.references -= 1", + " if (state.references === 0) {{", + " const data = state.data", + " state.data = 0", + " this.#jsEmbed.js_sys['closure.finalization'].unregister(state)", + " if (data) this.#jsExports.closure_drop(data)", + " }}", + " }}", + " callback.unref = () => {{", + " if (!owned) return", + " owned = false", + " release()", + " }}", + " this.#jsEmbed.js_sys['closure.finalization'].register(", + " callback, state, state", + " )", + " return release", "}}", ); @@ -178,20 +180,20 @@ js_bindgen::embed_js!( name = "closure.make", required_embeds = [("js_sys", "closure.own")], "(data, call) => {{", - " const state = {{ data, references: 1 }}", - " const callback = (...args) => {{", - " if (!state.data) {{", - " throw new Error('closure invoked after being dropped')", - " }}", - " state.references += 1", - " try {{", - " return call(state.data, ...args)", - " }} finally {{", - " release()", - " }}", - " }}", - " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", - " return callback", + " const state = {{ data, references: 1 }}", + " const callback = (...args) => {{", + " if (!state.data) {{", + " throw new Error('closure invoked after being dropped')", + " }}", + " state.references += 1", + " try {{", + " return call(state.data, ...args)", + " }} finally {{", + " release()", + " }}", + " }}", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", "}}", ); @@ -200,23 +202,23 @@ js_bindgen::embed_js!( name = "closure.make_mut", required_embeds = [("js_sys", "closure.own")], "(data, call) => {{", - " const state = {{ data, references: 1 }}", - " const callback = (...args) => {{", - " if (!state.data) {{", - " throw new Error('closure invoked recursively or after being dropped')", - " }}", - " state.references += 1", - " const data = state.data", - " state.data = 0", - " try {{", - " return call(data, ...args)", - " }} finally {{", - " state.data = data", - " release()", - " }}", - " }}", - " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", - " return callback", + " const state = {{ data, references: 1 }}", + " const callback = (...args) => {{", + " if (!state.data) {{", + " throw new Error('closure invoked recursively or after being dropped')", + " }}", + " state.references += 1", + " const data = state.data", + " state.data = 0", + " try {{", + " return call(data, ...args)", + " }} finally {{", + " state.data = data", + " release()", + " }}", + " }}", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", "}}", ); @@ -225,27 +227,27 @@ js_bindgen::embed_js!( name = "closure.make_once", required_embeds = [("js_sys", "closure.own")], "(data, call) => {{", - " const state = {{ data, references: 1, called: false }}", - " const callback = (...args) => {{", - " if (!state.data) {{", - " throw new Error('closure invoked recursively or after being dropped')", - " }}", - " if (state.called) {{", - " throw new Error('FnOnce called more than once')", - " }}", - " state.called = true", - " state.references += 1", - " const data = state.data", - " state.data = 0", - " try {{", - " return call(data, ...args)", - " }} finally {{", - " state.data = data", - " release()", - " }}", - " }}", - " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", - " return callback", + " const state = {{ data, references: 1, called: false }}", + " const callback = (...args) => {{", + " if (!state.data) {{", + " throw new Error('closure invoked recursively or after being dropped')", + " }}", + " if (state.called) {{", + " throw new Error('FnOnce called more than once')", + " }}", + " state.called = true", + " state.references += 1", + " const data = state.data", + " state.data = 0", + " try {{", + " return call(data, ...args)", + " }} finally {{", + " state.data = data", + " release()", + " }}", + " }}", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", "}}", ); @@ -256,6 +258,22 @@ pub struct Closure { _type: PhantomData>, } +impl Deref for Closure { + type Target = Function; + + #[inline] + fn deref(&self) -> &Self::Target { + Function::unchecked_from_ref(self.as_js_value()) + } +} + +impl From> for Function { + #[inline] + fn from(value: Closure) -> Self { + Self::unchecked_from(value.into_js_value()) + } +} + impl Closure { #[must_use] pub fn as_js_value(&self) -> &JsValue { diff --git a/client/js-sys/src/runtime/externref.rs b/client/js-sys/src/runtime/externref.rs index a97b20cd..962b84d3 100644 --- a/client/js-sys/src/runtime/externref.rs +++ b/client/js-sys/src/runtime/externref.rs @@ -2,10 +2,11 @@ use alloc::vec::Vec; use core::cell::RefCell; use core::mem; +use super::allocator; use super::panic::panic; use crate::JsValue; use crate::hazard::JsCast; -use crate::util::PtrConst; +use crate::util::{PtrConst, PtrLength}; pub(crate) const WAT_TABLE_IMPORT: &str = "(import \"js_sys\" \"externref.table\" (table \ $js_sys.import.externref.table (@sym (name \ @@ -75,9 +76,9 @@ js_bindgen::embed_js!( module = "js_sys", name = "externref.table", "(() => {{", - " const table = new WebAssembly.Table({{ initial: 2, element: 'externref' }})", - " table.set(1, null)", - " return table", + " const table = new WebAssembly.Table({{ initial: 2, element: 'externref' }})", + " table.set(1, null)", + " return table", "}})()" ); @@ -194,8 +195,8 @@ impl ReservedSlots { PtrConst::new(&self.slots) } - pub(crate) fn len(&self) -> i32 { - self.slots.len().try_into().unwrap() + pub(crate) fn len(&self) -> PtrLength { + PtrLength::new(&self.slots) } pub(crate) fn commit(mut self) { @@ -236,16 +237,48 @@ pub(crate) fn reserve_slots(count: usize) -> ReservedSlots { slots .try_reserve_exact(count) .expect("failure to grow memory"); + let mut reserved = ReservedSlots { + slots, + committed: false, + }; let mut slab = EXTERNREF_SLAB.0.borrow_mut(); - while slots.len() < count { - slots.push(index_to_abi(slab.alloc())); + while reserved.slots.len() < count { + reserved.slots.push(index_to_abi(slab.alloc())); } - ReservedSlots { - slots, - committed: false, + reserved +} + +#[unsafe(export_name = "js_sys.externref.reserve_slice")] +extern "C" fn reserve_slice(len: usize) -> *mut i32 { + let ptr: *mut i32 = allocator::allocate_slice(len); + // SAFETY: The allocator provides writable storage for exactly `len` indices. + let slots = + unsafe { core::slice::from_raw_parts_mut(ptr.cast::>(), len) }; + let mut slab = EXTERNREF_SLAB.0.borrow_mut(); + for slot in slots { + slot.write(index_to_abi(slab.alloc())); + } + ptr +} + +#[unsafe(export_name = "js_sys.externref.recycle_slice")] +unsafe extern "C" fn recycle_slice(ptr: *const i32, len: usize) { + // SAFETY: The caller provides exactly `len` initialized table indices and + // transfers ownership of each one. + let slots = unsafe { core::slice::from_raw_parts(ptr, len) }; + let mut slab = EXTERNREF_SLAB.0.borrow_mut(); + for &index in slots { + if u32::from_ne_bytes(index.to_ne_bytes()) >= 2 { + remove(index); + slab.dealloc(index_from_abi(index)); + } } + drop(slab); + // SAFETY: The caller transfers the exact allocation returned by + // `reserve_slice` or an owned `JsValue` slice with the same representation. + unsafe { allocator::release_slice(ptr.cast_mut(), len) }; } #[cfg(not(target_feature = "exception-handling"))] diff --git a/client/js-sys/src/runtime/future/jspi/atomic.rs b/client/js-sys/src/runtime/future/jspi/atomic.rs index d9529a29..6fe65ae8 100644 --- a/client/js-sys/src/runtime/future/jspi/atomic.rs +++ b/client/js-sys/src/runtime/future/jspi/atomic.rs @@ -13,19 +13,19 @@ js_bindgen::embed_js!( name = "future.jspi.suspend", required_embeds = [("js_sys", "future.jspi.waits")], "state => {{", - " const buffer = this.#memory.buffer", - " const signal = new Int32Array(buffer, state, 1)", - " if (typeof SharedArrayBuffer !== 'undefined'", - " && buffer instanceof SharedArrayBuffer) {{", - " if (typeof Atomics.waitAsync !== 'function') {{", - " throw new Error('shared-memory JSPI requires Atomics.waitAsync')", - " }}", - " const result = Atomics.waitAsync(signal, 0, 1)", - " return result.async ? result.value : undefined", - " }}", - " if (signal[0] !== 1) return", - " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", - " return new Promise(resolve => waits.set(state, resolve))", + " const buffer = this.#memory.buffer", + " const signal = new Int32Array(buffer, state, 1)", + " if (typeof SharedArrayBuffer !== 'undefined'", + " && buffer instanceof SharedArrayBuffer) {{", + " if (typeof Atomics.waitAsync !== 'function') {{", + " throw new Error('shared-memory JSPI requires Atomics.waitAsync')", + " }}", + " const result = Atomics.waitAsync(signal, 0, 1)", + " return result.async ? result.value : undefined", + " }}", + " if (signal[0] !== 1) return", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " return new Promise(resolve => waits.set(state, resolve))", "}}", ); @@ -34,17 +34,17 @@ js_bindgen::embed_js!( name = "future.jspi.notify", required_embeds = [("js_sys", "future.jspi.waits")], "state => {{", - " const buffer = this.#memory.buffer", - " if (typeof SharedArrayBuffer !== 'undefined'", - " && buffer instanceof SharedArrayBuffer) {{", - " Atomics.notify(new Int32Array(buffer, state, 1), 0, 1)", - " return", - " }}", - " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", - " const resolve = waits.get(state)", - " if (resolve === undefined) return", - " waits.delete(state)", - " resolve()", + " const buffer = this.#memory.buffer", + " if (typeof SharedArrayBuffer !== 'undefined'", + " && buffer instanceof SharedArrayBuffer) {{", + " Atomics.notify(new Int32Array(buffer, state, 1), 0, 1)", + " return", + " }}", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " const resolve = waits.get(state)", + " if (resolve === undefined) return", + " waits.delete(state)", + " resolve()", "}}", ); diff --git a/client/js-sys/src/runtime/future/jspi/single.rs b/client/js-sys/src/runtime/future/jspi/single.rs index 3eb111d7..baef08ff 100644 --- a/client/js-sys/src/runtime/future/jspi/single.rs +++ b/client/js-sys/src/runtime/future/jspi/single.rs @@ -13,8 +13,8 @@ js_bindgen::embed_js!( name = "future.jspi.suspend", required_embeds = [("js_sys", "future.jspi.waits")], "state => {{", - " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", - " return new Promise(resolve => waits.set(state, resolve))", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " return new Promise(resolve => waits.set(state, resolve))", "}}", ); @@ -23,11 +23,11 @@ js_bindgen::embed_js!( name = "future.jspi.notify", required_embeds = [("js_sys", "future.jspi.waits")], "state => {{", - " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", - " const resolve = waits.get(state)", - " if (resolve === undefined) return", - " waits.delete(state)", - " resolve()", + " const waits = this.#jsEmbed.js_sys['future.jspi.waits']", + " const resolve = waits.get(state)", + " if (resolve === undefined) return", + " waits.delete(state)", + " resolve()", "}}", ); diff --git a/client/js-sys/src/runtime/future/mod.rs b/client/js-sys/src/runtime/future/mod.rs index 64ae2b9f..38bb9d49 100644 --- a/client/js-sys/src/runtime/future/mod.rs +++ b/client/js-sys/src/runtime/future/mod.rs @@ -20,14 +20,14 @@ js_bindgen::embed_js!( module = "js_sys", name = "future.observe", "(promise, callback) => {{", - " promise.then(", - " value => {{", - " try {{ callback(true, value) }} finally {{ callback.unref() }}", - " }},", - " error => {{", - " try {{ callback(false, error) }} finally {{ callback.unref() }}", - " }},", - " )", + " promise.then(", + " value => {{", + " try {{ callback(true, value) }} finally {{ callback.unref() }}", + " }},", + " error => {{", + " try {{ callback(false, error) }} finally {{ callback.unref() }}", + " }},", + " )", "}}", ); @@ -35,7 +35,7 @@ js_bindgen::embed_js!( module = "js_sys", name = "future.settle", "(resolvers, resolved, value) => {{", - " resolvers[resolved ? 'resolve' : 'reject'](value)", + " resolvers[resolved ? 'resolve' : 'reject'](value)", "}}", ); diff --git a/client/js-sys/src/runtime/future/task/atomic.rs b/client/js-sys/src/runtime/future/task/atomic.rs index 92b52d10..499ebc9e 100644 --- a/client/js-sys/src/runtime/future/task/atomic.rs +++ b/client/js-sys/src/runtime/future/task/atomic.rs @@ -26,31 +26,31 @@ js_bindgen::embed_js!( name = "future.atomic.wait", required_embeds = [("js_sys", "future.atomic.state")], "(state, awake, resume) => {{", - " const atomic = this.#jsEmbed.js_sys['future.atomic.state']", - " if (awake) {{", - " globalThis.queueMicrotask(resume)", - " return", - " }}", - " const buffer = this.#memory.buffer", - " if (typeof SharedArrayBuffer === 'undefined'", - " || !(buffer instanceof SharedArrayBuffer)) {{", - " atomic.waits.set(state, resume)", - " return", - " }}", - " if (typeof Atomics.waitAsync !== 'function') {{", - " throw new Error('Wasm atomics futures require Atomics.waitAsync')", - " }}", - " if (buffer !== atomic.buffer) {{", - " atomic.buffer = buffer", - " atomic.view = new Int32Array(buffer)", - " }}", - " const result = Atomics.waitAsync(", - " atomic.view,", - " state / Int32Array.BYTES_PER_ELEMENT,", - " 0,", - " )", - " if (result.async) result.value.then(resume)", - " else globalThis.queueMicrotask(resume)", + " const atomic = this.#jsEmbed.js_sys['future.atomic.state']", + " if (awake) {{", + " globalThis.queueMicrotask(resume)", + " return", + " }}", + " const buffer = this.#memory.buffer", + " if (typeof SharedArrayBuffer === 'undefined'", + " || !(buffer instanceof SharedArrayBuffer)) {{", + " atomic.waits.set(state, resume)", + " return", + " }}", + " if (typeof Atomics.waitAsync !== 'function') {{", + " throw new Error('Wasm atomics futures require Atomics.waitAsync')", + " }}", + " if (buffer !== atomic.buffer) {{", + " atomic.buffer = buffer", + " atomic.view = new Int32Array(buffer)", + " }}", + " const result = Atomics.waitAsync(", + " atomic.view,", + " state / Int32Array.BYTES_PER_ELEMENT,", + " 0,", + " )", + " if (result.async) result.value.then(resume)", + " else globalThis.queueMicrotask(resume)", "}}", ); @@ -59,26 +59,26 @@ js_bindgen::embed_js!( name = "future.atomic.notify", required_embeds = [("js_sys", "future.atomic.state")], "state => {{", - " const atomic = this.#jsEmbed.js_sys['future.atomic.state']", - " const buffer = this.#memory.buffer", - " if (typeof SharedArrayBuffer !== 'undefined'", - " && buffer instanceof SharedArrayBuffer) {{", - " if (buffer !== atomic.buffer) {{", - " atomic.buffer = buffer", - " atomic.view = new Int32Array(buffer)", - " }}", - " Atomics.notify(", - " atomic.view,", - " state / Int32Array.BYTES_PER_ELEMENT,", - " 1,", - " )", - " return", - " }}", - " const waits = atomic.waits", - " const resume = waits.get(state)", - " if (resume === undefined) return", - " waits.delete(state)", - " globalThis.queueMicrotask(resume)", + " const atomic = this.#jsEmbed.js_sys['future.atomic.state']", + " const buffer = this.#memory.buffer", + " if (typeof SharedArrayBuffer !== 'undefined'", + " && buffer instanceof SharedArrayBuffer) {{", + " if (buffer !== atomic.buffer) {{", + " atomic.buffer = buffer", + " atomic.view = new Int32Array(buffer)", + " }}", + " Atomics.notify(", + " atomic.view,", + " state / Int32Array.BYTES_PER_ELEMENT,", + " 1,", + " )", + " return", + " }}", + " const waits = atomic.waits", + " const resume = waits.get(state)", + " if (resume === undefined) return", + " waits.delete(state)", + " globalThis.queueMicrotask(resume)", "}}", ); diff --git a/client/js-sys/src/runtime/mod.rs b/client/js-sys/src/runtime/mod.rs index 65589a2e..b329ed04 100644 --- a/client/js-sys/src/runtime/mod.rs +++ b/client/js-sys/src/runtime/mod.rs @@ -1,3 +1,4 @@ +mod allocator; pub(crate) mod closure; pub(crate) mod exception; pub(crate) mod externref; diff --git a/client/js-sys/src/util.rs b/client/js-sys/src/util.rs index d4fdbb1e..10422a01 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -96,6 +96,20 @@ const PTR_INTO_JS_WAT_CONV: Option = Some(WatConv { r#type: "f64", }); +// An aggregate conversion supplies its own JavaScript template, so the +// pointer and length slots do not independently apply their `IntoJS` +// conversions. On `wasm32`, normalize both raw `i32` slots here. On `wasm64`, +// the WAT shim has already converted them to JavaScript numbers. +#[cfg(target_arch = "wasm32")] +pub(crate) const JS_PTR_LEN_ARGS: &str = "$slot1 >>> 0, $slot2 >>> 0"; +#[cfg(target_arch = "wasm64")] +pub(crate) const JS_PTR_LEN_ARGS: &str = "$slot1, $slot2"; + +#[cfg(target_arch = "wasm32")] +pub(crate) const JS_OPTION_PTR_LEN_ARGS: &str = "$slot2 >>> 0, $slot3 >>> 0"; +#[cfg(target_arch = "wasm64")] +pub(crate) const JS_OPTION_PTR_LEN_ARGS: &str = "$slot2, $slot3"; + #[repr(transparent)] pub struct PtrConst { ptr: *const T, @@ -113,6 +127,21 @@ impl PtrConst { ptr: core::ptr::from_ref(value), } } + + pub(crate) const fn from_raw(ptr: *const T) -> Self { + Self { ptr } + } + + #[must_use] + pub(crate) const fn as_ptr(&self) -> *const T { + self.ptr + } +} + +impl Default for PtrConst { + fn default() -> Self { + Self::from_raw(core::ptr::null()) + } } // SAFETY: `PtrConst` is transparent over a native Wasm pointer. On `wasm64`, @@ -182,23 +211,34 @@ pub struct PtrLength { impl PtrLength { pub(crate) fn new(value: &[T]) -> Self { - Self::internal(value.len()) + Self::from_len(value.len()) } pub(crate) fn from_uninit_array(_: &MaybeUninit<[T; N]>) -> Self { - Self::internal(N) + Self::from_len(N) } pub(crate) fn from_uninit_slice(value: &[MaybeUninit]) -> Self { - Self::internal(value.len()) + Self::from_len(value.len()) } - fn internal(len: usize) -> Self { + pub(crate) const fn from_len(len: usize) -> Self { Self { len, _ty: PhantomData, } } + + #[must_use] + pub(crate) const fn get(&self) -> usize { + self.len + } +} + +impl Default for PtrLength { + fn default() -> Self { + Self::from_len(0) + } } // SAFETY: `PtrLength` is transparent over `usize`. On `wasm64`, the WAT @@ -224,9 +264,9 @@ js_bindgen::embed_js!( module = "js_sys", name = "isLittleEndian", "(() => {{", - " const buffer = new ArrayBuffer(2)", - " new DataView(buffer).setInt16(0, 256, true)", - " return new Int16Array(buffer)[0] === 256;", + " const buffer = new ArrayBuffer(2)", + " new DataView(buffer).setInt16(0, 256, true)", + " return new Int16Array(buffer)[0] === 256;", "}})()", ); @@ -263,23 +303,23 @@ macro_rules! buffer { ], "(ptr, count) => {{", #[cfg(debug_assertions)] - " if (ptr % {size} !== 0)", + " if (ptr % {size} !== 0)", #[cfg(debug_assertions)] - " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", + " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", "", - " if (this.#jsEmbed.js_sys.isLittleEndian) {{", + " if (this.#jsEmbed.js_sys.isLittleEndian) {{", #[cfg(js_sys_target_feature = "unstable-rab")] - " const base = ptr / {size}", - " const view = {buffer}", - " return Array.from(view)", - " }} else {{", - " const out = new Array(count)", - " const view = {data}", - " for (let index = 0; index < count; index++) {{", - " out[index] = view.get{type}(ptr + index * {size}, true)", - " }}", - " return out", - " }}", + " const base = ptr / {size}", + " const view = {buffer}", + " return view", + " }} else {{", + " const out = new {type}Array(count)", + " const view = {data}", + " for (let index = 0; index < count; index++) {{", + " out[index] = view.get{type}(ptr + index * {size}, true)", + " }}", + " return out", + " }}", "}}", size = const $size, #[cfg(js_sys_target_feature = "unstable-rab")] @@ -303,14 +343,14 @@ macro_rules! buffer { ], "(ptr, count) => {{", #[cfg(debug_assertions)] - " if (ptr % {size} !== 0)", + " if (ptr % {size} !== 0)", #[cfg(debug_assertions)] - " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", + " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", "", #[cfg(js_sys_target_feature = "unstable-rab")] - " const base = ptr / {size}", - " const view = {buffer}", - " return Array.from(view)", + " const base = ptr / {size}", + " const view = {buffer}", + " return view", "}}", size = const $size, #[cfg(js_sys_target_feature = "unstable-rab")] @@ -329,16 +369,16 @@ macro_rules! buffer { ], "(ptr, count) => {{", #[cfg(debug_assertions)] - " if (ptr % {size} !== 0)", + " if (ptr % {size} !== 0)", #[cfg(debug_assertions)] - " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", + " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", "", - " const out = new Array(count)", - " const view = {data}", - " for (let index = 0; index < count; index++) {{", - " out[index] = view.get{type}(ptr + index * {size}, true)", - " }}", - " return out", + " const out = new {type}Array(count)", + " const view = {data}", + " for (let index = 0; index < count; index++) {{", + " out[index] = view.get{type}(ptr + index * {size}, true)", + " }}", + " return out", "}}", size = const $size, #[cfg(js_sys_target_feature = "unstable-rab")] @@ -359,26 +399,26 @@ macro_rules! buffer { #[cfg(js_sys_target_feature = "unstable-rab")] ("js_sys", "view.DataView") ], - "(ptr, array) => {{", + "(ptr, array, count) => {{", #[cfg(debug_assertions)] - " if (ptr % {size} !== 0)", + " if (ptr % {size} !== 0)", #[cfg(debug_assertions)] - " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", + " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", "", - " if (this.#jsEmbed.js_sys.isLittleEndian) {{", - " {buffer}.set(array, ptr / {size})", - " }} else {{", - " const view = {data}", - " for (let index = 0; index < array.length; index++) {{", - " view.set{type}(ptr + index * {size}, array[index], true)", - " }}", - " }}", + " if (this.#jsEmbed.js_sys.isLittleEndian) {{", + " {buffer}.set(array)", + " }} else {{", + " const view = {data}", + " for (let index = 0; index < count; index++) {{", + " view.set{type}(ptr + index * {size}, array[index], true)", + " }}", + " }}", "}}", size = const $size, #[cfg(js_sys_target_feature = "unstable-rab")] - buffer = interpolate concat!("this.#jsEmbed.js_sys['view.", $type, "']"), + buffer = interpolate concat!("this.#jsEmbed.js_sys['view.", $type, "'].subarray(ptr / ", $size, ", ptr / ", $size, " + count)"), #[cfg(not(js_sys_target_feature = "unstable-rab"))] - buffer = interpolate concat!("new ", $type, "Array(this.#memory.buffer)"), + buffer = interpolate concat!("new ", $type, "Array(this.#memory.buffer, ptr, count)"), #[cfg(js_sys_target_feature = "unstable-rab")] data = interpolate "this.#jsEmbed.js_sys['view.DataView']", #[cfg(not(js_sys_target_feature = "unstable-rab"))] @@ -394,19 +434,19 @@ macro_rules! buffer { #[cfg(js_sys_target_feature = "unstable-rab")] ("js_sys", concat!("view.", $type)), ], - "(ptr, array) => {{", + "(ptr, array, count) => {{", #[cfg(debug_assertions)] - " if (ptr % {size} !== 0)", + " if (ptr % {size} !== 0)", #[cfg(debug_assertions)] - " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", + " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", "", - " {buffer}.set(array, ptr / {size})", + " {buffer}.set(array)", "}}", size = const $size, #[cfg(js_sys_target_feature = "unstable-rab")] - buffer = interpolate concat!("this.#jsEmbed.js_sys['view.", $type, "']"), + buffer = interpolate concat!("this.#jsEmbed.js_sys['view.", $type, "'].subarray(ptr / ", $size, ", ptr / ", $size, " + count)"), #[cfg(not(js_sys_target_feature = "unstable-rab"))] - buffer = interpolate concat!("new ", $type, "Array(this.#memory.buffer)"), + buffer = interpolate concat!("new ", $type, "Array(this.#memory.buffer, ptr, count)"), ); #[cfg(js_sys_assume_endianness = "big")] @@ -417,16 +457,16 @@ macro_rules! buffer { #[cfg(js_sys_target_feature = "unstable-rab")] ("js_sys", "view.DataView") ], - "(ptr, array) => {{", + "(ptr, array, count) => {{", #[cfg(debug_assertions)] - " if (ptr % {size} !== 0)", + " if (ptr % {size} !== 0)", #[cfg(debug_assertions)] - " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", + " throw new WebAssembly.RuntimeError(`non-aligned pointer: ${{ptr}}`)", "", - " const view = {data}", - " for (let index = 0; index < array.length; index++) {{", - " view.set{type}(ptr + index * {size}, array[index], true)", - " }}", + " const view = {data}", + " for (let index = 0; index < count; index++) {{", + " view.set{type}(ptr + index * {size}, array[index], true)", + " }}", "}}", size = const $size, #[cfg(js_sys_target_feature = "unstable-rab")] @@ -438,8 +478,13 @@ macro_rules! buffer { }; } -buffer!("Uint32", 4_usize); +buffer!("Int8", 1_usize); +buffer!("Uint8", 1_usize); +buffer!("Int16", 2_usize); +buffer!("Uint16", 2_usize); buffer!("Int32", 4_usize); +buffer!("Uint32", 4_usize); +buffer!("Float32", 4_usize); buffer!("Float64", 8_usize); buffer!("BigUint64", 8_usize); buffer!("BigInt64", 8_usize); diff --git a/client/js-sys/tests/array.rs b/client/js-sys/tests/array.rs index 94f6c5f3..aec2674f 100644 --- a/client/js-sys/tests/array.rs +++ b/client/js-sys/tests/array.rs @@ -1,47 +1,170 @@ use core::array; +use core::mem::MaybeUninit; use js_bindgen_test::test; -use js_sys::{JsArray, JsString, JsValue, js_sys}; +use js_sys::{Array, JsString, JsValue, TryFromArrayError, js_sys}; js_bindgen::embed_js!(module = "array", name = "test", "(value) => value"); +js_bindgen::embed_js!( + module = "array", + name = "mutate_u32", + "(value) => {{", + " const original = value[0]", + " value[0] = 0", + " return original", + "}}", +); js_bindgen::embed_js!( module = "array", name = "throwing", "(value, len) => new Proxy(new Array(len).fill(value), {{", - " get(target, property) {{", - " if (property === '1') throw new Error('boom')", - " return target[property]", - " }}", + " get(target, property) {{", + " if (property === '1') throw new Error('boom')", + " return target[property]", + " }}", "}})", ); +js_bindgen::embed_js!( + module = "array", + name = "custom_js_iterator", + "() => {{", + " const array = ['indexed 0', 'indexed 1']", + " array[Symbol.iterator] = function* () {{ yield 'protocol' }}", + " return array", + "}}", +); +js_bindgen::embed_js!( + module = "array", + name = "invalid_length", + "(value) => new Proxy([value, value], {{", + " get(target, property) {{", + " if (property === 'length') return 2.5", + " return target[property]", + " }}", + "}})", +); +js_bindgen::embed_js!( + module = "array", + name = "externref_length", + required_embeds = [("js_sys", "externref.table")], + "() => this.#jsEmbed.js_sys['externref.table'].length", +); + +macro_rules! typed_slice_import { + ($name:ident: $element:ty => $constructor:literal, $embed:literal) => { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = $embed)] + fn $name(value: &[$element]) -> Array<$element>; + } + + js_bindgen::embed_js!( + module = "array", + name = $embed, + "(value) => {{", + " if (!(value instanceof {constructor}))", + " throw new TypeError('expected a {constructor}')", + " return Array.from(value)", + "}}", + constructor = interpolate $constructor, + ); + }; +} + +typed_slice_import!(i8_slice: i8 => "Int8Array", "i8_slice"); +typed_slice_import!(u64_slice: u64 => "BigUint64Array", "u64_slice"); +typed_slice_import!(f64_slice: f64 => "Float64Array", "f64_slice"); + +macro_rules! integer_array_roundtrip { + ($element:ty, $length:expr, $values:expr) => {{ + let values: [$element; $length] = $values; + let array: Array<$element> = Array::from(&values); + assert!(Array::is_array(array.as_ref())); + + let copied: [$element; $length] = array.to_array().unwrap(); + assert_eq!(copied, values); + + let mut copied = [<$element>::default(); $length]; + array.to_slice(&mut copied).unwrap(); + assert_eq!(copied, values); + }}; +} + +macro_rules! assert_float_values { + ($actual:expr, $expected:expr) => { + for (&actual, &expected) in $actual.iter().zip($expected.iter()) { + if expected.is_nan() { + assert!(actual.is_nan()); + } else { + assert_eq!(actual.to_bits(), expected.to_bits()); + } + } + }; +} + +macro_rules! float_array_roundtrip { + ($element:ty, $length:expr, $values:expr) => {{ + let values: [$element; $length] = $values; + let array: Array<$element> = Array::from(&values); + assert!(Array::is_array(array.as_ref())); + + let copied: [$element; $length] = array.to_array().unwrap(); + assert_float_values!(copied, values); + + let mut copied = [<$element>::default(); $length]; + array.to_slice(&mut copied).unwrap(); + assert_float_values!(copied, values); + }}; +} #[test] fn js_value() { #[js_sys] extern "js-sys" { #[js_sys(js_embed = "test")] - fn js(value: &[JsValue]) -> JsArray; + fn js(value: &[JsValue]) -> Array; #[js_sys(js_embed = "throwing")] - fn throwing(value: &JsValue, len: u32) -> JsArray; + fn throwing(value: &JsValue, len: u32) -> Array; + + #[js_sys(js_embed = "invalid_length")] + fn invalid_length(value: &JsValue) -> Array; + + #[js_sys(js_embed = "externref_length")] + fn externref_length() -> u32; } let rust_array = [JsValue::UNDEFINED; 42]; - let js_array = JsArray::from(&rust_array); + let js_array = Array::from(&rust_array); assert_eq!(rust_array.len(), js_array.length().try_into().unwrap()); + let empty: [JsValue; 0] = []; + assert_eq!(Array::from(&empty).to_array::<0>().unwrap(), empty); let ffi_array = js(&rust_array); assert_eq!(rust_array.len(), ffi_array.length().try_into().unwrap()); let mut wrong_length = [JsValue::UNDEFINED; 41]; - assert!(js_array.to_slice(&mut wrong_length).is_err()); + assert!(matches!( + js_array.to_slice(&mut wrong_length), + Err(TryFromArrayError::LengthMismatch { + actual: 42, + expected: 41, + }) + )); let previous = JsString::from("previous"); let previous: JsValue = previous.into(); let mut destination: [JsValue; 42] = array::from_fn(|_| previous.clone()); - let throwing = throwing(&JsValue::NULL, 42); - assert!(throwing.to_slice(&mut destination).is_err()); + let throwing_array = throwing(&JsValue::NULL, 42); + assert!(matches!( + throwing_array.to_slice(&mut destination), + Err(TryFromArrayError::JavaScript(_)) + )); assert!(destination.iter().all(|value| value == &previous)); + assert!(matches!( + invalid_length(&JsValue::NULL).to_array::<2>(), + Err(TryFromArrayError::JavaScript(_)) + )); js_array.to_slice(&mut destination).unwrap(); assert_eq!(rust_array, destination); @@ -51,6 +174,28 @@ fn js_value() { let returned_array: [JsValue; 42] = ffi_array.to_array().unwrap(); assert_eq!(rust_array, returned_array); + + let mut uninit: [MaybeUninit; 42] = array::from_fn(|_| MaybeUninit::uninit()); + let initialized = js_array.to_uninit_slice(&mut uninit).unwrap(); + assert_eq!(rust_array, initialized); + + let large: [JsValue; 129] = array::from_fn(|_| JsValue::UNDEFINED); + assert!(matches!( + throwing(&JsValue::NULL, 129).to_array::<129>(), + Err(TryFromArrayError::JavaScript(_)) + )); + assert_eq!(Array::from(&large).to_array::<129>().unwrap(), large); + + let table_length = externref_length(); + let mut oversized = [JsValue::UNDEFINED; 513]; + assert!(matches!( + Array::new().to_slice(&mut oversized), + Err(TryFromArrayError::LengthMismatch { + actual: 0, + expected: 513, + }) + )); + assert_eq!(externref_length(), table_length); } #[test] @@ -58,19 +203,162 @@ fn u32() { #[js_sys] extern "js-sys" { #[js_sys(js_embed = "test")] - fn u32(value: &[u32]) -> JsArray; + fn u32(value: &[u32]) -> Array; + + #[js_sys(js_embed = "throwing")] + fn throwing_u32(value: u32, len: u32) -> Array; + + #[js_sys(js_embed = "invalid_length")] + fn invalid_length_u32(value: u32) -> Array; + + #[js_sys(js_embed = "mutate_u32")] + fn mutate_u32(value: &[u32]) -> u32; } - let rust_array: [u32; 42] = array::from_fn(|i| i.try_into().unwrap()); - let js_array = JsArray::from(&rust_array); + let mut rust_array: [u32; 42] = array::from_fn(|i| i.try_into().unwrap()); + rust_array[0] = u32::MAX; + rust_array[1] = 0x8000_0000; + let js_array = Array::from(&rust_array); assert_eq!(rust_array.len(), js_array.length().try_into().unwrap()); + let empty: [u32; 0] = []; + assert_eq!(Array::from(&empty).to_array::<0>().unwrap(), empty); let ffi_array = u32(&rust_array); assert_eq!(rust_array.len(), ffi_array.length().try_into().unwrap()); + assert_eq!(mutate_u32(&rust_array), u32::MAX); + assert_eq!(rust_array[0], u32::MAX); let returned_array: [u32; 42] = js_array.to_array().unwrap(); assert_eq!(rust_array, returned_array); let returned_array: [u32; 42] = ffi_array.to_array().unwrap(); assert_eq!(rust_array, returned_array); + assert!(matches!( + invalid_length_u32(7).to_array::<2>(), + Err(TryFromArrayError::JavaScript(_)) + )); + + let mut destination = [0; 42]; + js_array.to_slice(&mut destination).unwrap(); + assert_eq!(rust_array, destination); + + let mut wrong_length = [0; 41]; + assert!(matches!( + js_array.to_slice(&mut wrong_length), + Err(TryFromArrayError::LengthMismatch { + actual: 42, + expected: 41, + }) + )); + + let mut uninit = [MaybeUninit::uninit(); 42]; + let initialized = js_array.to_uninit_slice(&mut uninit).unwrap(); + assert_eq!(rust_array, initialized); + + let throwing = throwing_u32(7, 42); + let mut destination = [u32::MAX; 42]; + assert!(matches!( + throwing.to_slice(&mut destination), + Err(TryFromArrayError::JavaScript(_)) + )); + assert_eq!(destination, [u32::MAX; 42]); +} + +#[test] +fn primitive_roundtrips() { + integer_array_roundtrip!(i8, 4, [i8::MIN, -1, 0, i8::MAX]); + integer_array_roundtrip!(u8, 4, [0, 1, 1 << 7, u8::MAX]); + integer_array_roundtrip!(i16, 4, [i16::MIN, -1, 0, i16::MAX]); + integer_array_roundtrip!(u16, 4, [0, 1, 1 << 15, u16::MAX]); + integer_array_roundtrip!(i32, 4, [i32::MIN, -1, 0, i32::MAX]); + integer_array_roundtrip!(u32, 4, [0, 1, 1 << 31, u32::MAX]); + integer_array_roundtrip!(i64, 4, [i64::MIN, -1, 0, i64::MAX]); + integer_array_roundtrip!(u64, 4, [0, 1, 1 << 63, u64::MAX]); + integer_array_roundtrip!(isize, 4, [isize::MIN, -1, 0, isize::MAX]); + integer_array_roundtrip!(usize, 4, [0, 1, 1usize << (usize::BITS - 1), usize::MAX]); + + float_array_roundtrip!( + f32, + 6, + [f32::NEG_INFINITY, -1.25, -0.0, 0.0, f32::INFINITY, f32::NAN,] + ); + float_array_roundtrip!( + f64, + 6, + [f64::NEG_INFINITY, -1.25, -0.0, 0.0, f64::INFINITY, f64::NAN,] + ); +} + +#[test] +fn primitive_slices() { + let values = [i8::MIN, -1, 0, i8::MAX]; + assert_eq!(i8_slice(&values).to_array::<4>().unwrap(), values); + + let values = [0, 1, 1 << 63, u64::MAX]; + assert_eq!(u64_slice(&values).to_array::<4>().unwrap(), values); + + let values = [f64::NEG_INFINITY, -0.0, f64::INFINITY, f64::NAN]; + let copied = f64_slice(&values).to_array::<4>().unwrap(); + assert_float_values!(copied, values); +} + +#[test] +fn rust_iteration() { + let values = [JsValue::NULL, JsValue::UNDEFINED, JsValue::NULL]; + let array = Array::of(&values); + let mut iterator = array.iter(); + + assert_eq!(iterator.size_hint(), (3, Some(3))); + assert_eq!(iterator.len(), 3); + assert_eq!(iterator.next(), Some(JsValue::NULL)); + assert_eq!(iterator.next_back(), Some(JsValue::NULL)); + assert_eq!(iterator.len(), 1); + assert_eq!(iterator.next(), Some(JsValue::UNDEFINED)); + assert_eq!(iterator.next(), None); + assert_eq!(iterator.next_back(), None); + assert_eq!(iterator.next(), None); + + let array = Array::of(&values[..2]); + let mut iterator = array.iter(); + array.set_length(4); + array.set(2, &JsValue::NULL); + array.set(3, &JsValue::NULL); + assert_eq!(iterator.len(), 2); + assert_eq!(iterator.next(), Some(JsValue::NULL)); + assert_eq!(iterator.next(), Some(JsValue::UNDEFINED)); + assert_eq!(iterator.next(), None); + + let array = Array::of(&values); + let shared = array.clone(); + let mut iterator = array.into_iter(); + shared.set_length(1); + assert_eq!(iterator.len(), 3); + assert_eq!(iterator.next(), Some(JsValue::NULL)); + assert_eq!(iterator.next_back(), Some(JsValue::UNDEFINED)); + assert_eq!(iterator.next(), Some(JsValue::UNDEFINED)); + assert_eq!(iterator.next(), None); +} + +#[test] +fn custom_symbol_iterator() { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "custom_js_iterator")] + fn custom_js_iterator() -> Array; + } + + let array = custom_js_iterator(); + assert_eq!( + array.to_array::<2>().unwrap(), + [JsString::from("indexed 0"), JsString::from("indexed 1")] + ); + let indexed: Vec = array.iter().map(|value| String::from(&value)).collect(); + assert_eq!(indexed, ["indexed 0", "indexed 1"]); + + let protocol: Vec = array + .symbol_iterator() + .into_iter() + .map(|value| String::from(&value.unwrap())) + .collect(); + assert_eq!(protocol, ["protocol"]); } diff --git a/client/js-sys/tests/future.rs b/client/js-sys/tests/future.rs index 010c5427..5c97a95a 100644 --- a/client/js-sys/tests/future.rs +++ b/client/js-sys/tests/future.rs @@ -1,11 +1,50 @@ +use core::cell::Cell; +use std::rc::Rc; + use js_bindgen_test::test; -use js_sys::{JsValue, Promise}; +use js_sys::{JsString, JsValue, Promise, future_to_promise, spawn_local}; #[test] -async fn promise() { +async fn promise_to_future() { let value = Promise::resolve(&JsValue::NULL).await.unwrap(); assert_eq!(value, JsValue::NULL); + assert_eq!( + Promise::reject(&JsValue::UNDEFINED).await.unwrap_err(), + JsValue::UNDEFINED + ); +} + +#[test] +async fn rust_future_to_promise() { + let resolved = JsValue::from(JsString::from("resolved")); + let value = resolved.clone(); + assert_eq!( + future_to_promise(async move { Ok(value) }).await.unwrap(), + resolved + ); + + let rejected = JsValue::from(JsString::from("rejected")); + let error = rejected.clone(); + assert_eq!( + future_to_promise::(async move { Err(error) }) + .await + .unwrap_err(), + rejected + ); +} + +#[test] +async fn spawn_local_is_deferred() { + let completed = Rc::new(Cell::new(false)); + let task_completed = Rc::clone(&completed); + spawn_local(async move { + task_completed.set(true); + }); + + assert!(!completed.get()); + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + assert!(completed.get()); } #[test] @@ -21,6 +60,7 @@ mod first { #[test] async fn same_name() { + // Regression test: generated async-test exports include their module path. Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); } } @@ -31,6 +71,7 @@ mod second { #[test] async fn same_name() { + // Keep this name equal to `first::same_name` to exercise macro hygiene. Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); } } diff --git a/client/js-sys/tests/hazard.rs b/client/js-sys/tests/hazard.rs index 1982fefb..a0e5c7bc 100644 --- a/client/js-sys/tests/hazard.rs +++ b/client/js-sys/tests/hazard.rs @@ -1,5 +1,5 @@ use js_bindgen_test::test; -use js_sys::hazard::{EmptySlot, FromJS, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; +use js_sys::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; use js_sys::js_sys; js_bindgen::embed_js!( @@ -32,23 +32,6 @@ struct Pair(u32, u32); struct Quad(u32, u32, u32, u32); -#[derive(Clone, Copy)] -struct ExportInput(u32); - -// SAFETY: `ExportInput` is reconstructed directly from one `NumberSlot`. -unsafe impl FromJS for ExportInput { - type Abi = NumberSlot; - - fn from_abi(raw: Self::Abi) -> Self { - Self(raw.0) - } -} - -#[js_sys] -fn from_js_only(value: ExportInput) -> u32 { - value.0 -} - // SAFETY: `Pair` is represented by its two `u32` fields in order. unsafe impl WasmAbi for Pair { type Slot1 = NumberSlot; diff --git a/client/js-sys/tests/iterator.rs b/client/js-sys/tests/iterator.rs new file mode 100644 index 00000000..95421bca --- /dev/null +++ b/client/js-sys/tests/iterator.rs @@ -0,0 +1,603 @@ +use core::future::Future; +use core::task::{Context, Poll, Waker}; + +use js_bindgen_test::test; +use js_sys::hazard::JsCast; +use js_sys::{ + AsyncIterator, JsFuture, JsIterator, JsString, JsValue, Number, js_sys, try_async_iter, + try_iter, +}; + +js_bindgen::embed_js!( + module = "iterator", + name = "sync.strings", + "() => ['one', 'two'][Symbol.iterator]()", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.values", + "() => [null, undefined]", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.not_iterable", + "() => ({{}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.symbol_throws", + "() => Object.defineProperty({{}}, Symbol.iterator, {{", + " get() {{ throw new Error('symbol') }}", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.symbol_not_callable", + "() => ({{ [Symbol.iterator]: 1 }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.invalid_iterator", + "() => ({{ [Symbol.iterator]: () => 1 }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.missing_next", + "() => ({{ [Symbol.iterator]: () => ({{}}) }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.next_throws", + "() => ({{ next() {{ throw new Error('next') }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.next_primitive", + "() => ({{ next() {{ return 1 }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.done_truthy", + "() => ({{ next() {{ return {{", + " done: 'yes',", + " get value() {{ throw new Error('value must not be read') }},", + "}} }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.done_throws", + "() => ({{ next() {{ return {{", + " get done() {{ throw new Error('done') }},", + "}} }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.value_throws", + "() => ({{ next() {{ return {{", + " done: false,", + " get value() {{ throw new Error('value') }},", + "}} }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.strings", + "() => (async function* () {{ yield 'one'; yield 'two' }})()", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.cached_next", + "() => ({{", + " reads: 0,", + " calls: 0,", + " [Symbol.asyncIterator]() {{ return this }},", + " get next() {{", + " this.reads++", + " const value = this.reads", + " return function() {{", + " this.calls++", + " return Promise.resolve({{ done: false, value }})", + " }}", + " }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.next_getter_throws", + "() => ({{", + " [Symbol.asyncIterator]() {{ return this }},", + " get next() {{ throw new Error('next getter') }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.plain_result", + "() => ({{", + " done: false,", + " [Symbol.asyncIterator]() {{ return this }},", + " next() {{", + " if (this.done) return {{ done: true }}", + " this.done = true", + " return {{ done: false, value: 'plain' }}", + " }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.promise_values", + "() => [Promise.resolve('one'), 'two']", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.promise_result", + "() => ({{", + " [Symbol.iterator]() {{ return this }},", + " next() {{ return Promise.resolve({{ done: true, value: 'not awaited' }}) }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.argument_counts", + "() => ({{", + " [Symbol.iterator]() {{ return this }},", + " next() {{ return {{ done: false, value: arguments.length }} }},", + " return() {{ return {{ done: true, value: arguments.length }} }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.close_without_throw", + "() => ({{", + " closed: false,", + " [Symbol.iterator]() {{ return this }},", + " next() {{ return {{ done: false }} }},", + " return() {{ this.closed = true; return {{ done: true }} }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.cached_next", + "() => ({{", + " reads: 0,", + " calls: 0,", + " [Symbol.iterator]() {{ return this }},", + " get next() {{", + " this.reads++", + " const value = this.reads", + " return function() {{", + " this.calls++", + " return {{ done: false, value }}", + " }}", + " }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.next_getter_throws", + "() => ({{", + " [Symbol.iterator]() {{ return this }},", + " get next() {{ throw new Error('next getter') }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "next.replace", + "(iterator) => Object.defineProperty(iterator, 'next', {{", + " value() {{ return {{ done: false, value: 99 }} }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.rejected_value", + "() => ({{", + " closed: false,", + " [Symbol.iterator]() {{ return this }},", + " next() {{ return {{ done: false, value: Promise.reject(null) }} }},", + " throw() {{ return {{ done: false, value: Promise.reject(null) }} }},", + " return() {{ this.closed = true; throw new Error('close') }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.next_throws", + "() => ({{ next() {{ throw new Error('next') }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.rejects", + "() => ({{ next() {{ return Promise.reject(new Error('reject')) }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.next_primitive", + "() => ({{ next() {{ return Promise.resolve(1) }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.done_throws", + "() => ({{ next() {{ return Promise.resolve({{", + " get done() {{ throw new Error('done') }},", + "}}) }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.value_throws", + "() => ({{ next() {{ return Promise.resolve({{", + " done: false,", + " get value() {{ throw new Error('value') }},", + "}}) }} }})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.cancel", + "() => ({{", + " calls: 0,", + " next() {{", + " this.calls++", + " return Promise.resolve(this.calls === 1", + " ? {{ done: false, value: 'kept' }}", + " : {{ done: true }})", + " }},", + "}})", +); +js_bindgen::embed_js!( + module = "iterator", + name = "async.calls", + "(iterator) => iterator.calls", +); +js_bindgen::embed_js!( + module = "iterator", + name = "closed", + "(iterator) => iterator.closed", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.reads", + "(iterator) => iterator.reads", +); +js_bindgen::embed_js!( + module = "iterator", + name = "sync.calls", + "(iterator) => iterator.calls", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "sync.strings")] + fn sync_strings() -> JsIterator; + + #[js_sys(js_embed = "sync.values")] + fn sync_values() -> JsValue; + + #[js_sys(js_embed = "sync.not_iterable")] + fn sync_not_iterable() -> JsValue; + + #[js_sys(js_embed = "sync.symbol_throws")] + fn sync_symbol_throws() -> JsValue; + + #[js_sys(js_embed = "sync.symbol_not_callable")] + fn sync_symbol_not_callable() -> JsValue; + + #[js_sys(js_embed = "sync.invalid_iterator")] + fn sync_invalid_iterator() -> JsValue; + + #[js_sys(js_embed = "sync.missing_next")] + fn sync_missing_next() -> JsValue; + + #[js_sys(js_embed = "sync.next_throws")] + fn sync_next_throws() -> JsIterator; + + #[js_sys(js_embed = "sync.next_primitive")] + fn sync_next_primitive() -> JsIterator; + + #[js_sys(js_embed = "sync.done_truthy")] + fn sync_done_truthy() -> JsIterator; + + #[js_sys(js_embed = "sync.done_throws")] + fn sync_done_throws() -> JsIterator; + + #[js_sys(js_embed = "sync.value_throws")] + fn sync_value_throws() -> JsIterator; + + #[js_sys(js_embed = "sync.promise_values")] + fn sync_promise_values() -> JsValue; + + #[js_sys(js_embed = "sync.promise_result")] + fn sync_promise_result() -> JsValue; + + #[js_sys(js_embed = "sync.argument_counts")] + fn sync_argument_counts() -> JsValue; + + #[js_sys(js_embed = "sync.close_without_throw")] + fn sync_close_without_throw() -> JsValue; + + #[js_sys(js_embed = "sync.cached_next")] + fn sync_cached_next() -> JsValue; + + #[js_sys(js_embed = "sync.next_getter_throws")] + fn sync_next_getter_throws() -> JsValue; + + #[js_sys(js_embed = "next.replace")] + fn replace_next(iterator: &JsValue); + + #[js_sys(js_embed = "sync.rejected_value")] + fn sync_rejected_value() -> JsValue; + + #[js_sys(js_embed = "async.strings")] + fn async_strings() -> AsyncIterator; + + #[js_sys(js_embed = "async.strings")] + fn async_values() -> JsValue; + + #[js_sys(js_embed = "async.cached_next")] + fn async_cached_next() -> JsValue; + + #[js_sys(js_embed = "async.next_getter_throws")] + fn async_next_getter_throws() -> JsValue; + + #[js_sys(js_embed = "async.plain_result")] + fn async_plain_result() -> AsyncIterator; + + #[js_sys(js_embed = "async.next_throws")] + fn async_next_throws() -> AsyncIterator; + + #[js_sys(js_embed = "async.rejects")] + fn async_rejects() -> AsyncIterator; + + #[js_sys(js_embed = "async.next_primitive")] + fn async_next_primitive() -> AsyncIterator; + + #[js_sys(js_embed = "async.done_throws")] + fn async_done_throws() -> AsyncIterator; + + #[js_sys(js_embed = "async.value_throws")] + fn async_value_throws() -> AsyncIterator; + + #[js_sys(js_embed = "async.cancel")] + fn async_cancel() -> AsyncIterator; + + #[js_sys(js_embed = "async.calls")] + fn async_calls(iterator: &AsyncIterator) -> u32; + + #[js_sys(js_embed = "closed")] + fn closed(value: &JsValue) -> bool; + + #[js_sys(js_embed = "sync.reads")] + fn sync_reads(value: &JsValue) -> u32; + + #[js_sys(js_embed = "sync.calls")] + fn sync_calls(value: &JsValue) -> u32; +} + +fn number(value: JsValue) -> f64 { + Number::::unchecked_from(value).value_of() +} + +fn assert_number(value: JsValue, expected: f64) { + assert!((number(value) - expected).abs() < f64::EPSILON); +} + +#[test] +fn sync_iterator() { + let iterator = sync_strings(); + let values: Vec<_> = iterator.iter().map(Result::unwrap).collect(); + assert_eq!(values, [JsString::from("one"), JsString::from("two")]); + + let values: Vec<_> = sync_strings().into_iter().map(Result::unwrap).collect(); + assert_eq!(values, [JsString::from("one"), JsString::from("two")]); +} + +#[test] +fn dynamic_iterator() { + let values: Vec<_> = try_iter(&sync_values()) + .unwrap() + .unwrap() + .map(Result::unwrap) + .collect(); + assert_eq!(values, [JsValue::NULL, JsValue::UNDEFINED]); + + assert!(try_iter(&sync_not_iterable()).unwrap().is_none()); + assert!(try_iter(&JsValue::NULL).unwrap().is_none()); + assert!(try_iter(&sync_symbol_throws()).is_err()); + assert!(try_iter(&sync_symbol_not_callable()).is_err()); + assert!(try_iter(&sync_invalid_iterator()).is_err()); + assert!(try_iter(&sync_missing_next()).is_err()); + assert!(try_iter(&sync_next_getter_throws()).is_err()); +} + +#[test] +fn sync_next_is_cached() { + let source = sync_cached_next(); + let mut iterator = try_iter(&source).unwrap().unwrap(); + assert_eq!(sync_reads(&source), 1); + + replace_next(&source); + assert_number(iterator.next().unwrap().unwrap(), 1.0); + assert_eq!(sync_reads(&source), 1); + assert_eq!(sync_calls(&source), 1); + + let source = sync_cached_next(); + let iterator: JsIterator = JsIterator::unchecked_from(source.clone()); + let result = iterator.next_result().unwrap(); + assert_number(result.value().unwrap(), 1.0); + replace_next(&source); + let result = iterator.next_result().unwrap(); + assert_number(result.value().unwrap(), 99.0); +} + +#[test] +fn sync_errors_are_fused() { + for iterator in [ + sync_next_throws(), + sync_next_primitive(), + sync_done_throws(), + sync_value_throws(), + ] { + let mut iterator = iterator.into_iter(); + assert!(iterator.next().unwrap().is_err()); + assert!(iterator.next().is_none()); + } + + let mut iterator = sync_done_truthy().into_iter(); + assert!(iterator.next().is_none()); + assert!(iterator.next().is_none()); +} + +#[test] +async fn async_iterator() { + let mut iterator = async_strings().into_async_iter(); + assert_eq!( + iterator.next().await.unwrap().unwrap(), + JsString::from("one") + ); + assert_eq!( + iterator.next().await.unwrap().unwrap(), + JsString::from("two") + ); + assert!(iterator.next().await.is_none()); + + let mut iterator = async_plain_result().into_async_iter(); + assert_eq!( + iterator.next().await.unwrap().unwrap(), + JsString::from("plain") + ); + assert!(iterator.next().await.is_none()); +} + +#[test] +async fn dynamic_async_iterator() { + let mut iterator = try_async_iter(&async_values()).unwrap().unwrap(); + let first = iterator.next().await.unwrap().unwrap(); + assert_eq!(JsString::unchecked_from(first), "one"); + + assert!(try_async_iter(&sync_not_iterable()).unwrap().is_none()); + assert!(try_async_iter(&JsValue::UNDEFINED).unwrap().is_none()); + assert!(try_async_iter(&async_next_getter_throws()).is_err()); + + let mut iterator = try_async_iter(&sync_promise_values()).unwrap().unwrap(); + let first = iterator.next().await.unwrap().unwrap(); + assert_eq!(JsString::unchecked_from(first), "one"); + let second = iterator.next().await.unwrap().unwrap(); + assert_eq!(JsString::unchecked_from(second), "two"); + assert!(iterator.next().await.is_none()); +} + +#[test] +async fn async_next_is_cached() { + let source = async_cached_next(); + let mut iterator = try_async_iter(&source).unwrap().unwrap(); + assert_eq!(sync_reads(&source), 1); + + replace_next(&source); + assert_number(iterator.next().await.unwrap().unwrap(), 1.0); + assert_eq!(sync_reads(&source), 1); + assert_eq!(sync_calls(&source), 1); +} + +#[test] +async fn async_from_sync() { + assert!(AsyncIterator::::try_from_value(&sync_symbol_not_callable()).is_err()); + + let iterator = AsyncIterator::::try_from_value(&sync_promise_result()) + .unwrap() + .unwrap(); + let result = JsFuture::from(iterator.next_result().unwrap()) + .await + .unwrap(); + assert!(!result.done().unwrap()); + assert_eq!(result.value().unwrap(), JsValue::UNDEFINED); + + let iterator = AsyncIterator::::try_from_value(&sync_argument_counts()) + .unwrap() + .unwrap(); + let result = JsFuture::from(iterator.next_result().unwrap()) + .await + .unwrap(); + assert_number(result.value().unwrap(), 0.0); + let result = JsFuture::from(iterator.next_result_with_value(&JsValue::NULL).unwrap()) + .await + .unwrap(); + assert_number(result.value().unwrap(), 1.0); + let result = JsFuture::from(iterator.return_result().unwrap().unwrap()) + .await + .unwrap(); + assert_number(result.value().unwrap(), 0.0); + let result = JsFuture::from( + iterator + .return_result_with_value(&JsValue::NULL) + .unwrap() + .unwrap(), + ) + .await + .unwrap(); + assert_number(result.value().unwrap(), 1.0); + + let source = sync_close_without_throw(); + let iterator = AsyncIterator::::try_from_value(&source) + .unwrap() + .unwrap(); + assert!( + JsFuture::from(iterator.throw_result(&JsValue::NULL).unwrap().unwrap()) + .await + .is_err() + ); + assert!(closed(&source)); + + let source = sync_cached_next(); + let iterator = AsyncIterator::::try_from_value(&source) + .unwrap() + .unwrap(); + let result = JsFuture::from(iterator.next_result().unwrap()) + .await + .unwrap(); + assert_number(result.value().unwrap(), 1.0); + assert_eq!(sync_reads(&source), 1); + assert_eq!(sync_calls(&source), 1); + + for use_throw in [false, true] { + let source = sync_rejected_value(); + let iterator = AsyncIterator::::try_from_value(&source) + .unwrap() + .unwrap(); + let promise = if use_throw { + iterator.throw_result(&JsValue::NULL).unwrap().unwrap() + } else { + iterator.next_result().unwrap() + }; + assert_eq!(JsFuture::from(promise).await.unwrap_err(), JsValue::NULL); + assert!(closed(&source)); + } +} + +#[test] +async fn async_errors_are_fused() { + for iterator in [ + async_next_throws(), + async_rejects(), + async_next_primitive(), + async_done_throws(), + async_value_throws(), + ] { + let mut iterator = iterator.into_async_iter(); + assert!(iterator.next().await.unwrap().is_err()); + assert!(iterator.next().await.is_none()); + } +} + +#[test] +async fn next_is_cancellation_safe() { + let iterator = async_cancel(); + let observed = iterator.clone(); + let mut iterator = iterator.into_async_iter(); + + { + let mut next = core::pin::pin!(iterator.next()); + let mut context = Context::from_waker(Waker::noop()); + assert!(matches!(next.as_mut().poll(&mut context), Poll::Pending)); + } + + assert_eq!( + iterator.next().await.unwrap().unwrap(), + JsString::from("kept") + ); + assert_eq!(async_calls(&observed), 1); +} diff --git a/client/js-sys/tests/numeric.rs b/client/js-sys/tests/numeric.rs index 9c66beaa..a86bf270 100644 --- a/client/js-sys/tests/numeric.rs +++ b/client/js-sys/tests/numeric.rs @@ -1,5 +1,5 @@ use js_bindgen_test::test; -use js_sys::{JsBigInt, JsNumber, JsString, js_sys}; +use js_sys::{BigInt, JsString, Number, js_sys}; use paste::paste; js_bindgen::embed_js!(module = "numeric", name = "test", "(value) => value"); @@ -9,18 +9,18 @@ fn bool() { #[js_sys] extern "js-sys" { #[js_sys(js_embed = "test")] - fn bool_input(value: bool) -> JsNumber; + fn bool_input(value: bool) -> Number; #[js_sys(js_embed = "test")] - fn bool_output(value: &JsNumber) -> bool; + fn bool_output(value: &Number) -> bool; } let r#false = bool_input(false); - assert_eq!(JsString::new(r#false.as_ref()), "false"); + assert_eq!(JsString::new(r#false.as_ref()).unwrap(), "false"); assert!(!bool_output(&r#false)); let r#true = bool_input(true); - assert_eq!(JsString::new(r#true.as_ref()), "true"); + assert_eq!(JsString::new(r#true.as_ref()).unwrap(), "true"); assert!(bool_output(&r#true)); } @@ -40,7 +40,7 @@ macro_rules! signed { internal!($js, $ty); let null = [<$ty _input>](0); - assert_eq!(JsString::new(null.as_ref()), 0.to_string()); + assert_eq!(JsString::new(null.as_ref()).unwrap(), 0.to_string()); assert_eq!([<$ty _output>](&null), 0); } })*}; @@ -59,28 +59,28 @@ macro_rules! internal { } let min = [<$ty _input>]($ty::MIN); - assert_eq!(JsString::new(min.as_ref()), $ty::MIN.to_string()); + assert_eq!(JsString::new(min.as_ref()).unwrap(), $ty::MIN.to_string()); assert_eq!([<$ty _output>](&min), $ty::MIN); let max = [<$ty _input>]($ty::MAX); - assert_eq!(JsString::new(max.as_ref()), $ty::MAX.to_string()); + assert_eq!(JsString::new(max.as_ref()).unwrap(), $ty::MAX.to_string()); assert_eq!([<$ty _output>](&max), $ty::MAX); } }; } -unsigned!(JsNumber, u8, u16, u32); -unsigned!(JsBigInt, u64, u128); +unsigned!(Number, u8, u16, u32); +unsigned!(BigInt, u64, u128); #[cfg(target_arch = "wasm32")] -unsigned!(JsNumber, usize); +unsigned!(Number, usize); #[cfg(target_arch = "wasm64")] -unsigned!(JsBigInt, usize); +unsigned!(BigInt, usize); -signed!(JsNumber, i8, i16, i32); -signed!(JsBigInt, i64, i128); +signed!(Number, i8, i16, i32); +signed!(BigInt, i64, i128); #[cfg(target_arch = "wasm32")] -signed!(JsNumber, isize); +signed!(Number, isize); #[cfg(target_arch = "wasm64")] -signed!(JsBigInt, isize); +signed!(BigInt, isize); diff --git a/client/js-sys/tests/optional.rs b/client/js-sys/tests/optional.rs index d0c2058d..64dd2964 100644 --- a/client/js-sys/tests/optional.rs +++ b/client/js-sys/tests/optional.rs @@ -6,7 +6,7 @@ use core::arch::wasm32 as wasm; use core::arch::wasm64 as wasm; use js_bindgen_test::test; -use js_sys::{JsArray, JsString, JsValue, js_sys}; +use js_sys::{Array, JsString, JsValue, js_sys}; js_bindgen::embed_js!(module = "optional", name = "test", "(value) => value"); @@ -79,13 +79,9 @@ fn numeric() { } assert!(f64_option(Some(f64::NAN)).unwrap().is_nan()); - assert_eq!(u128_option(Some(u128::MAX)), Some(u128::MAX)); - assert_eq!(u128_option(None), None); + // Growing memory invalidates JavaScript views. Exercise the indirect + // representations again after that cache boundary. assert_ne!(wasm::memory_grow::<0>(1), usize::MAX); - assert_eq!(i64_option(Some(i64::MIN)), Some(i64::MIN)); - assert_eq!(u64_option(Some(u64::MAX)), Some(u64::MAX)); - assert_eq!(isize_option(Some(isize::MIN)), Some(isize::MIN)); - assert_eq!(usize_option(Some(usize::MAX)), Some(usize::MAX)); assert_eq!(u128_option(Some(1_u128 << 64)), Some(1_u128 << 64)); assert_eq!(i128_option(Some(-1)), Some(-1)); assert_eq!(i128_option(None), None); @@ -111,7 +107,7 @@ fn js_value() { fn js_value_option(value: Option<&JsValue>) -> Option; #[js_sys(js_embed = "test")] - fn js_array_option(value: Option<&JsArray>) -> Option; + fn js_array_option(value: Option<&Array>) -> Option; #[js_sys(js_embed = "test")] fn js_string_option(value: Option<&JsString>) -> Option; @@ -123,9 +119,9 @@ fn js_value() { let string = JsString::from("test"); let string = js_value_option(Some(string.as_ref())).unwrap(); - assert_eq!(JsString::new(&string), "test"); + assert_eq!(JsString::new(&string).unwrap(), "test"); - let array = JsArray::from(&[JsValue::UNDEFINED]); + let array = Array::from(&[JsValue::UNDEFINED]); let array = js_array_option(Some(&array)).unwrap(); assert_eq!(array.length(), 1); assert!(js_array_option(None).is_none()); diff --git a/client/js-sys/tests/string.rs b/client/js-sys/tests/string.rs index 2ec85e50..0f7b8a08 100644 --- a/client/js-sys/tests/string.rs +++ b/client/js-sys/tests/string.rs @@ -1,16 +1,110 @@ use js_bindgen_test::test; -use js_sys::{JsString, js_sys}; +use js_sys::{JsString, JsValue, js_sys}; #[js_sys] extern "js-sys" { #[js_sys(js_embed = "test")] - fn test(value: &str) -> JsString; + fn js_string(value: &str) -> JsString; + + #[js_sys(js_embed = "identity")] + fn identity(value: String) -> String; + + #[js_sys(js_embed = "identity")] + fn optional_identity(value: Option) -> Option; + + #[js_sys(js_embed = "identity")] + fn result_identity(value: String) -> Result; + + #[js_sys(js_embed = "throw")] + fn result_error() -> Result; + + #[js_sys(js_embed = "wrong_type")] + fn wrong_type() -> Result; + + #[js_sys(js_embed = "lone_surrogate")] + fn lone_surrogate() -> String; + + #[js_sys(js_embed = "grow_memory")] + fn grow_memory(value: String) -> String; } js_bindgen::embed_js!(module = "string", name = "test", "(value) => value"); +js_bindgen::embed_js!(module = "string", name = "identity", "value => value"); +js_bindgen::embed_js!( + module = "string", + name = "throw", + "() => {{ throw 'error' }}" +); +js_bindgen::embed_js!(module = "string", name = "wrong_type", "() => 42"); +js_bindgen::embed_js!( + module = "string", + name = "lone_surrogate", + "() => '\\ud800'" +); +js_bindgen::embed_js!( + module = "string", + name = "grow_memory", + "value => {{", + #[cfg(target_arch = "wasm32")] + " this.#memory.grow(1)", + #[cfg(target_arch = "wasm64")] + " this.#memory.grow(1n)", + " return value", + "}}", +); + +#[test] +fn borrowed_roundtrip() { + assert_eq!(js_string("Hello, World!"), "Hello, World!"); +} + +#[test] +fn owned_roundtrip() { + for value in [ + "", + "Hello, World!", + "a\0b", + "你好,世界!🦀", + "\u{feff}leading byte-order mark", + ] { + assert_eq!(identity(value.to_owned()), value); + } +} + +#[test] +fn optional_owned_roundtrip() { + assert_eq!(optional_identity(None), None); + assert_eq!(optional_identity(Some(String::new())), Some(String::new())); + let value = String::from("optional 🦀"); + assert_eq!(optional_identity(Some(value.clone())), Some(value)); +} + +#[test] +fn result_owned_roundtrip() { + let value = String::from("result 🦀"); + assert_eq!(result_identity(value.clone()).unwrap(), value); + assert_eq!( + result_error().unwrap_err(), + JsValue::from(JsString::from("error")) + ); + assert!(wrong_type().is_err()); +} + +#[test] +fn rust_conversions() { + let string = JsString::from(String::from("line\n\"quoted\"")); + assert_eq!(String::from(string.clone()), "line\n\"quoted\""); + assert_eq!(format!("{string}"), "line\n\"quoted\""); + assert_eq!(format!("{string:?}"), "\"line\\n\\\"quoted\\\"\""); + + assert_eq!(JsString::from('🦀'), "🦀"); + assert_eq!(JsString::default(), ""); + assert_eq!("parsed".parse::().unwrap(), "parsed"); + assert_eq!(lone_surrogate(), "\u{fffd}"); +} #[test] -fn rust_string() { - let string = test("Hello, World!"); - assert_eq!(String::from(&string), "Hello, World!"); +fn survives_memory_growth() { + let value = "你好,世界!🦀".repeat(32_768); + assert_eq!(grow_memory(value.clone()), value); } diff --git a/client/js-sys/tests/typed_array.rs b/client/js-sys/tests/typed_array.rs new file mode 100644 index 00000000..37fcd2fc --- /dev/null +++ b/client/js-sys/tests/typed_array.rs @@ -0,0 +1,221 @@ +#![expect( + clippy::float_cmp, + reason = "typed-array copies must preserve exact values" +)] + +use js_bindgen_test::test; +use js_sys::hazard::JsCast; +use js_sys::{ + ArrayBuffer, BigInt64Array, BigUint64Array, Float16Array, Float64Array, Int8Array, + TypedArray, TypedArrayCopyError, Uint8Array, Uint8ClampedArray, Uint32Array, js_sys, +}; + +js_bindgen::embed_js!( + module = "typed_array", + name = "has_float16_array", + "() => typeof Float16Array === 'function'", +); +js_bindgen::embed_js!( + module = "typed_array", + name = "shadow_length", + "(array, length) => Object.defineProperty(array, 'length', {{ value: length }})", +); +js_bindgen::embed_js!( + module = "typed_array", + name = "has_resizable_array_buffer", + "() => typeof ArrayBuffer.prototype.resize === 'function'", +); +js_bindgen::embed_js!( + module = "typed_array", + name = "resizable", + "() => {{", + " const buffer = new ArrayBuffer(16, {{ maxByteLength: 16 }})", + " const array = new Uint32Array(buffer)", + " array.set([1, 2, 3, 4])", + " return array", + "}}", +); +js_bindgen::embed_js!( + module = "typed_array", + name = "resize", + "(array, byteLength) => array.buffer.resize(byteLength)", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "has_float16_array")] + fn has_float16_array() -> bool; + + #[js_sys(js_embed = "shadow_length")] + fn shadow_length(array: &Uint32Array, length: u32); + + #[js_sys(js_embed = "has_resizable_array_buffer")] + fn has_resizable_array_buffer() -> bool; + + #[js_sys(js_embed = "resizable")] + fn resizable() -> Uint32Array; + + #[js_sys(js_embed = "resize")] + fn resize(array: &Uint32Array, byte_length: u32); +} + +macro_rules! copy_tests { + ($name:ident, $array:ty, $values:expr, $replacement:expr) => { + #[test] + fn $name() { + let values = $values; + let array = <$array>::from(&values); + assert_eq!( + array.length(), + f64::from(u32::try_from(values.len()).unwrap()) + ); + assert_eq!(array.to_vec().unwrap(), values); + + let replacement = $replacement; + array.copy_from(&replacement).unwrap(); + let mut output = replacement; + output.fill(replacement[0]); + array.copy_to(&mut output).unwrap(); + assert_eq!(output, replacement); + + let mut wrong = [replacement[0]; 1]; + assert!(matches!( + array.copy_to(&mut wrong), + Err(TypedArrayCopyError::LengthMismatch) + )); + } + }; +} + +copy_tests!(int8, Int8Array, [-128_i8, 0, 127], [1_i8, 2, 3]); +copy_tests!(uint8, Uint8Array, [0_u8, 128, 255], [3_u8, 2, 1]); +copy_tests!( + uint8_clamped, + Uint8ClampedArray, + [0_u8, 128, 255], + [3_u8, 2, 1] +); +copy_tests!( + uint32, + Uint32Array, + [0_u32, 0x8000_0000, u32::MAX], + [1_u32, 2, 3] +); +copy_tests!( + float64, + Float64Array, + [f64::NEG_INFINITY, -0.0, f64::INFINITY], + [1.0_f64, 2.0, 3.0] +); +copy_tests!( + big_int64, + BigInt64Array, + [i64::MIN, 0, i64::MAX], + [1_i64, 2, 3] +); +copy_tests!( + big_uint64, + BigUint64Array, + [0_u64, 1 << 63, u64::MAX], + [1_u64, 2, 3] +); + +#[test] +fn float16() { + if !has_float16_array() { + return; + } + + let initial = [0x3c00_u16, 0xc000, 0x3555]; + let array = Float16Array::new_from_u16_slice(&initial).unwrap(); + assert_eq!(array.to_u16_vec().unwrap(), initial); + + let replacement = [0x0001, 0x7bff, 0xfc00]; + array.copy_from_u16_slice(&replacement).unwrap(); + let mut copied = [0; 3]; + array.copy_to_u16_slice(&mut copied).unwrap(); + assert_eq!(copied, replacement); + assert!(matches!( + array.copy_to_u16_slice(&mut [0]), + Err(TypedArrayCopyError::LengthMismatch) + )); +} + +#[test] +fn float16_errors() { + if !has_float16_array() { + return; + } + + let array = Float16Array::new_from_u16_slice(&[0x3c00]).unwrap(); + let buffer = ArrayBuffer::unchecked_from(array.buffer()); + buffer.transfer().unwrap(); + + assert!(matches!( + array.copy_to_u16_slice(&mut [0]), + Err(TypedArrayCopyError::JavaScript(_)) + )); +} + +#[test] +fn rust_iteration() { + let array = Uint32Array::from(&[5, 8, 13]); + let mut iter = array.iter(); + assert_eq!(iter.size_hint(), (0, Some(3))); + assert_eq!(iter.next(), Some(5)); + assert_eq!(iter.next_back(), Some(13)); + assert_eq!(iter.next(), Some(8)); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_back(), None); + + assert_eq!(array.clone().into_iter().collect::>(), [5, 8, 13]); + assert_eq!((&array).into_iter().rev().collect::>(), [13, 8, 5]); +} + +#[test] +fn copy_ignores_shadowed_length() { + let array = Uint32Array::from(&[3, 5]); + shadow_length(&array, 1); + + let mut destination = [u32::MAX]; + assert!(matches!( + array.copy_to(&mut destination), + Err(TypedArrayCopyError::LengthMismatch) + )); + assert_eq!(destination, [u32::MAX]); + + assert!(matches!( + array.copy_from(&[8]), + Err(TypedArrayCopyError::LengthMismatch) + )); + assert_eq!(array.get(0.0), Some(3)); + assert_eq!(array.get(1.0), Some(5)); + assert_eq!(array.typed_array_length(), 2); + assert_eq!(array.to_vec().unwrap(), [3, 5]); + assert_eq!(array.iter().collect::>(), [3, 5]); + + let array = Uint32Array::from(&[3, 5]); + shadow_length(&array, 3); + let mut destination = [u32::MAX; 3]; + assert!(matches!( + array.copy_to(&mut destination), + Err(TypedArrayCopyError::LengthMismatch) + )); + assert_eq!(destination, [u32::MAX; 3]); + assert_eq!(array.clone().into_iter().collect::>(), [3, 5]); +} + +#[test] +fn shrinking_during_iteration() { + if !has_resizable_array_buffer() { + return; + } + + let array = resizable(); + let mut iter = array.iter(); + assert_eq!(iter.size_hint(), (0, Some(4))); + resize(&array, 8); + assert_eq!(iter.next_back(), Some(2)); + assert_eq!(iter.next_back(), Some(1)); + assert_eq!(iter.next_back(), None); +} diff --git a/client/js-sys/tests/value.rs b/client/js-sys/tests/value.rs index 0a2cc050..a95de1b5 100644 --- a/client/js-sys/tests/value.rs +++ b/client/js-sys/tests/value.rs @@ -2,17 +2,25 @@ use js_bindgen_test::test; use js_sys::{JsString, JsValue, js_sys}; js_bindgen::embed_js!(module = "value", name = "nan", "() => NaN"); - +js_bindgen::embed_js!( + module = "value", + name = "externref_length", + required_embeds = [("js_sys", "externref.table")], + "() => this.#jsEmbed.js_sys['externref.table'].length", +); #[js_sys] extern "js-sys" { #[js_sys(js_embed = "nan")] fn nan() -> JsValue; + + #[js_sys(js_embed = "externref_length")] + fn externref_length() -> u32; } #[test] fn undefined() { let value = JsValue::UNDEFINED.clone(); - let string = JsString::new(&value); + let string = JsString::new(&value).unwrap(); let string = String::from(&string); assert_eq!(string, "undefined"); @@ -21,28 +29,27 @@ fn undefined() { #[test] fn null() { let value = JsValue::NULL.clone(); - let string = JsString::new(&value); + let string = JsString::new(&value).unwrap(); let string = String::from(&string); assert_eq!(string, "null"); } #[test] -fn clone() { - let value = JsString::from("Hello, World!"); - let value = value.clone(); - assert_eq!(value, "Hello, World!"); -} - -#[test] -fn strict_equality_is_not_reflexive() { +fn nan_strict_equality() { let value = nan(); assert!(!PartialEq::eq(&value, &value)); } #[test] -fn many_live_values() { +fn externref_reuse() { let value = JsString::from("Hello, World!"); let values: Vec<_> = (0..512).map(|_| value.clone()).collect(); - assert_eq!(values.len(), 512); + assert!(values.iter().all(|candidate| candidate == &value)); + let grown_length = externref_length(); + drop(values); + + let reused: Vec<_> = (0..512).map(|_| value.clone()).collect(); + assert!(reused.iter().all(|candidate| candidate == &value)); + assert_eq!(externref_length(), grown_length); } diff --git a/client/js-sys/tests/vec.rs b/client/js-sys/tests/vec.rs new file mode 100644 index 00000000..6d81a8a1 --- /dev/null +++ b/client/js-sys/tests/vec.rs @@ -0,0 +1,219 @@ +use js_bindgen_test::test; +use js_sys::{JsString, JsValue, js_sys}; + +macro_rules! identity { + ($name:ident: $element:ty) => { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "identity")] + fn $name(value: Vec<$element>) -> Vec<$element>; + } + }; +} + +macro_rules! typed_identity { + ($name:ident: $element:ty => $constructor:literal, $embed:literal) => { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = $embed)] + fn $name(value: Vec<$element>) -> Vec<$element>; + } + + js_bindgen::embed_js!( + module = "vec", + name = $embed, + "value => {{", + " if (!(value instanceof {constructor}))", + " throw new TypeError('expected a {constructor}')", + " return value", + "}}", + constructor = interpolate $constructor, + ); + }; +} + +identity!(js_value_identity: JsString); +identity!(string_identity: String); +typed_identity!(i8_identity: i8 => "Int8Array", "i8_identity"); +typed_identity!(u8_identity: u8 => "Uint8Array", "u8_identity"); +typed_identity!(i16_identity: i16 => "Int16Array", "i16_identity"); +typed_identity!(u16_identity: u16 => "Uint16Array", "u16_identity"); +typed_identity!(i32_identity: i32 => "Int32Array", "i32_identity"); +typed_identity!(u32_identity: u32 => "Uint32Array", "u32_identity"); +typed_identity!(i64_identity: i64 => "BigInt64Array", "i64_identity"); +typed_identity!(u64_identity: u64 => "BigUint64Array", "u64_identity"); +typed_identity!(f32_identity: f32 => "Float32Array", "f32_identity"); +typed_identity!(f64_identity: f64 => "Float64Array", "f64_identity"); + +#[cfg(target_arch = "wasm32")] +typed_identity!(isize_identity: isize => "Int32Array", "isize_identity"); +#[cfg(target_arch = "wasm64")] +typed_identity!(isize_identity: isize => "BigInt64Array", "isize_identity"); +#[cfg(target_arch = "wasm32")] +typed_identity!(usize_identity: usize => "Uint32Array", "usize_identity"); +#[cfg(target_arch = "wasm64")] +typed_identity!(usize_identity: usize => "BigUint64Array", "usize_identity"); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "wrong_js_value_type")] + fn wrong_js_value_type() -> Result, JsValue>; + + #[js_sys(js_embed = "wrong_u32_type")] + fn wrong_u32_type() -> Result, JsValue>; + + #[js_sys(js_embed = "throwing_array")] + fn throwing_array() -> Result, JsValue>; + + #[js_sys(js_embed = "growing_array")] + fn growing_array() -> Vec; + + #[js_sys(js_embed = "externref_length")] + fn externref_length() -> u32; +} + +js_bindgen::embed_js!(module = "vec", name = "identity", "value => value",); +js_bindgen::embed_js!( + module = "vec", + name = "wrong_js_value_type", + "() => new Uint32Array()", +); +js_bindgen::embed_js!(module = "vec", name = "wrong_u32_type", "() => []"); +js_bindgen::embed_js!( + module = "vec", + name = "throwing_array", + "() => new Proxy([null, null], {{", + " get(target, property, receiver) {{", + " if (property === '1') throw new Error('boom')", + " return Reflect.get(target, property, receiver)", + " }},", + "}})", +); +js_bindgen::embed_js!( + module = "vec", + name = "growing_array", + "(() => {{", + " const memory = this.#memory", + " return () => new Proxy([null, null], {{", + " get(target, property, receiver) {{", + #[cfg(target_arch = "wasm32")] + " if (property === '0') memory.grow(1)", + #[cfg(target_arch = "wasm64")] + " if (property === '0') memory.grow(1n)", + " return Reflect.get(target, property, receiver)", + " }},", + " }})", + "}})()", +); +js_bindgen::embed_js!( + module = "vec", + name = "externref_length", + required_embeds = [("js_sys", "externref.table")], + "() => this.#jsEmbed.js_sys['externref.table'].length", +); + +#[test] +fn js_value_roundtrip() { + let values = vec![ + JsString::from("first"), + JsString::from(""), + JsString::from("第三个 🦀"), + ]; + let result = js_value_identity(values); + + assert_eq!(result.len(), 3); + assert_eq!(result[0], "first"); + assert_eq!(result[1], ""); + assert_eq!(result[2], "第三个 🦀"); + assert!(js_value_identity(Vec::new()).is_empty()); +} + +#[test] +fn u32_roundtrip() { + assert_eq!(u32_identity(vec![0, 1, u32::MAX]), [0, 1, u32::MAX]); + assert!(u32_identity(Vec::new()).is_empty()); +} + +#[test] +fn string_roundtrip() { + let values = vec![ + String::from("first"), + String::new(), + String::from("第三个 🦀"), + ]; + assert_eq!(string_identity(values.clone()), values); +} + +#[test] +fn numeric_roundtrips() { + assert_eq!( + i8_identity(vec![i8::MIN, -1, 0, i8::MAX]), + [i8::MIN, -1, 0, i8::MAX] + ); + assert_eq!(u8_identity(vec![0, 1, u8::MAX]), [0, 1, u8::MAX]); + assert_eq!( + i16_identity(vec![i16::MIN, -1, 0, i16::MAX]), + [i16::MIN, -1, 0, i16::MAX] + ); + assert_eq!(u16_identity(vec![0, 1, u16::MAX]), [0, 1, u16::MAX]); + assert_eq!( + i32_identity(vec![i32::MIN, -1, 0, i32::MAX]), + [i32::MIN, -1, 0, i32::MAX] + ); + assert_eq!( + i64_identity(vec![i64::MIN, -1, 0, i64::MAX]), + [i64::MIN, -1, 0, i64::MAX] + ); + assert_eq!(u64_identity(vec![0, 1, u64::MAX]), [0, 1, u64::MAX]); + assert_eq!( + isize_identity(vec![isize::MIN, -1, 0, isize::MAX]), + [isize::MIN, -1, 0, isize::MAX], + ); + assert_eq!(usize_identity(vec![0, 1, usize::MAX]), [0, 1, usize::MAX]); + assert_eq!( + f32_identity(vec![-1.25, -0.0, 0.0, f32::INFINITY]), + [-1.25, -0.0, 0.0, f32::INFINITY] + ); + assert_eq!( + f64_identity(vec![-1.25, -0.0, 0.0, f64::INFINITY]), + [-1.25, -0.0, 0.0, f64::INFINITY] + ); +} + +#[test] +fn invalid_representation() { + assert!(wrong_js_value_type().is_err()); + assert!(wrong_u32_type().is_err()); +} + +#[test] +fn failed_conversion_recycles_slots() { + assert!(throwing_array().is_err()); + let grown_length = externref_length(); + + for _ in 0..256 { + assert!(throwing_array().is_err()); + } + + assert_eq!(externref_length(), grown_length); +} + +#[test] +fn conversion_survives_memory_growth() { + let result = growing_array(); + assert_eq!(result, [JsValue::NULL, JsValue::NULL]); +} + +#[test] +fn roundtrips_recycle_slots() { + let value = JsString::from("value"); + let roundtrip = || js_value_identity((0..64).map(|_| value.clone()).collect()); + + drop(roundtrip()); + let grown_length = externref_length(); + for _ in 0..16 { + drop(roundtrip()); + } + + assert_eq!(externref_length(), grown_length); +} diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs index 3b2bc23a..9a94352d 100644 --- a/client/web-sys/src/console.gen.rs +++ b/client/web-sys/src/console.gen.rs @@ -73,7 +73,7 @@ pub fn error(data: &JsValue) { }; } const _: () = { - const IMPORTS: &[r#macro::ImportDescriptor] = &[ + static IMPORTS: &[r#macro::ImportDescriptor] = &[ r#macro::ImportDescriptor::new( "web_sys", "console.log0", diff --git a/host/cli-lib/src/js/imports.mjs b/host/cli-lib/src/js/imports.mjs index 7e7a7fec..b5d231c7 100644 --- a/host/cli-lib/src/js/imports.mjs +++ b/host/cli-lib/src/js/imports.mjs @@ -62,11 +62,10 @@ export class JsBindgen { this.#finished = true; // Export wrappers generated by `js-sys` use this stable binding. const wasmExports = instance.exports; - this.#jsExports = JBG_PLACEHOLDER_JS_EXPORT; - const exports = Object.assign(Object.create(null), wasmExports, this.#jsExports); + this.#jsExports = Object.assign(Object.create(null), wasmExports, JBG_PLACEHOLDER_JS_EXPORT); return { instance, - exports, + exports: this.#jsExports, }; }); } diff --git a/host/cli-lib/src/js/imports.mts b/host/cli-lib/src/js/imports.mts index 52888220..f9fce793 100644 --- a/host/cli-lib/src/js/imports.mts +++ b/host/cli-lib/src/js/imports.mts @@ -83,15 +83,14 @@ export class JsBindgen { // Export wrappers generated by `js-sys` use this stable binding. const wasmExports = instance.exports - this.#jsExports = JBG_PLACEHOLDER_JS_EXPORT - const exports = Object.assign( + this.#jsExports = Object.assign( Object.create(null) as WebAssembly.Instance["exports"], wasmExports, - this.#jsExports + JBG_PLACEHOLDER_JS_EXPORT ) return { instance, - exports, + exports: this.#jsExports, } }) } diff --git a/host/js-sys-bindgen/Cargo.toml b/host/js-sys-bindgen/Cargo.toml index 6b08013f..797e3943 100644 --- a/host/js-sys-bindgen/Cargo.toml +++ b/host/js-sys-bindgen/Cargo.toml @@ -11,9 +11,8 @@ bench = false doctest = false [dependencies] -foldhash = { workspace = true } -hashbrown = { workspace = true } -itertools = { workspace = true, features = ["use_alloc"] } +foldhash = { workspace = true, optional = true } +hashbrown = { workspace = true, optional = true } proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true, features = [ @@ -29,17 +28,19 @@ xxhash-rust = { workspace = true } [dev-dependencies] anyhow = { workspace = true } cargo_metadata = { workspace = true } +foldhash = { workspace = true } +hashbrown = { workspace = true } indoc = { workspace = true } inline-snap = { workspace = true } +itertools = { workspace = true, features = ["use_alloc"] } js-bindgen-ld-shared = { workspace = true } prettyplease = { workspace = true } -similar-asserts = { workspace = true } tempfile = { workspace = true } wasmparser = { workspace = true } [features] -file = [] -web-idl = ["dep:weedle2"] +file = ["dep:foldhash", "dep:hashbrown"] +web-idl = ["dep:foldhash", "dep:hashbrown", "dep:weedle2"] [lints] workspace = true diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index 6369b7a0..b2e10abc 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -3,7 +3,6 @@ use std::mem; use std::ops::DerefMut; use std::string::ToString; -use itertools::Itertools; use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::spanned::Spanned; @@ -19,7 +18,7 @@ mod js; mod options; use js::ForeignItem; -use options::FunctionOptions; +use options::{BindingKind, FunctionOptions}; pub(crate) struct FunctionImport { pub(crate) cfg_attrs: Vec, @@ -31,6 +30,7 @@ pub(crate) struct FunctionImport { struct FunctionPlan { inputs: Vec, output_ty: Option, + output_abi_override: Option, impl_generic_params: TokenStream, binding: ForeignItem, suspending: bool, @@ -60,6 +60,21 @@ impl InputArg { } } +fn join_input_slots(inputs: &[InputArg]) -> String { + let mut inputs = inputs.iter(); + let Some(first) = inputs.next() else { + return String::new(); + }; + let mut output = first.slot_names[0].to_string(); + + for input in inputs { + output.push_str(", "); + output.push_str(&input.slot_names[0].to_string()); + } + + output +} + pub(crate) fn expand( hygiene: &mut Hygiene<'_>, namespace: Option<&str>, @@ -123,6 +138,7 @@ pub(crate) fn expand( let FunctionPlan { inputs, output_ty, + output_abi_override, impl_generic_params, binding, .. @@ -162,7 +178,8 @@ pub(crate) fn expand( ] }) .collect(); - let foreign_output = output_ty.as_ref().map_or_else( + let output_abi_ty = output_abi_override.as_ref().or(output_ty.as_ref()); + let foreign_output = output_abi_ty.map_or_else( TokenStream::new, |ty| quote_spanned!(span=> -> #macro_path::OutputRet<#ty>), ); @@ -171,7 +188,14 @@ pub(crate) fn expand( #(#split_inputs)* unsafe { #ident(#(#foreign_input_names),*) } }}; - let foreign_call = if output_ty.is_some() { + let foreign_call = if let Some(output_abi_ty) = output_abi_override.as_ref() { + let output_ty = output_ty.as_ref().expect("validated during parsing"); + + quote_spanned!(span=> { + let value = #foreign_call; + unsafe { #macro_path::join_output_as::<#output_ty, #output_abi_ty>(value) } + }) + } else if output_ty.is_some() { quote_spanned!(span=> #macro_path::join_output(#foreign_call)) } else { quote_spanned!(span=> #foreign_call;) @@ -213,7 +237,8 @@ impl FunctionPlan { span: Span, ) -> Result { let suspending = options.suspending; - let external_implementation = options.import || options.embed.is_some(); + let external_implementation = options.binding.is_external(); + let output_abi_override = options.return_abi.clone(); let (inputs, self_ty) = Self::parse_inputs(hygiene, sig, cfg_attrs, span, external_implementation)?; let binding = @@ -222,12 +247,16 @@ impl FunctionPlan { ReturnType::Default => None, ReturnType::Type(_, ty) => Some(*ty.clone()), }; + if output_abi_override.is_some() && output_ty.is_none() { + return Err(Error::new(span, "`return_abi` requires a return value")); + } let impl_generic_params = Self::impl_generic_params(&binding, &mut sig.generics); Ok(Self { inputs, output_ty, + output_abi_override, impl_generic_params, binding, suspending, @@ -340,20 +369,16 @@ impl FunctionPlan { js_name, static_of, variadic, - constructor, - getter, - setter, - embed, - import, + binding, + return_abi: _, suspending: _, } = options; - if import { - return Ok(ForeignItem::Import); - } - if let Some(embed) = embed { - return Ok(ForeignItem::Embed(embed)); - } + let binding = match binding { + BindingKind::Import => return Ok(ForeignItem::Import), + BindingKind::Embed(embed) => return Ok(ForeignItem::Embed(embed)), + binding => binding, + }; let js_inputs: Vec<_> = inputs .iter() @@ -361,7 +386,7 @@ impl FunctionPlan { .collect(); let argument_count = sig.inputs.len() - usize::from(self_ty.is_some()); - if constructor && self_ty.is_some() { + if matches!(&binding, BindingKind::Constructor) && self_ty.is_some() { return Err(Error::new( span, "`constructor` cannot be used with a `self` parameter", @@ -379,48 +404,98 @@ impl FunctionPlan { "`variadic` requires at least one argument", )); } + if matches!( + &binding, + BindingKind::IndexingGetter + | BindingKind::IndexingSetter + | BindingKind::IndexingDeleter + ) && self_ty.is_none() + { + return Err(Error::new( + span, + "indexing operations require a `self` parameter", + )); + } - if constructor { - // `constructor` applies to this foreign function. Its return type - // selects the Rust `impl` owner and JavaScript invokes it with `new`. - let owner = Self::constructor_type(&sig.output)?; - let name = js_name.unwrap_or_else(|| Self::type_js_name(&owner, js_names)); - let path = ForeignItem::global_path(namespace, &name); + match binding { + BindingKind::Constructor => { + // `constructor` applies to this foreign function. Its return type + // selects the Rust `impl` owner and JavaScript invokes it with `new`. + let owner = Self::constructor_type(&sig.output)?; + let name = js_name.unwrap_or_else(|| Self::type_js_name(&owner, js_names)); + let path = ForeignItem::global_path(namespace, &name); - return Ok(ForeignItem::constructor(owner, &path, variadic, &js_inputs)); - } + Ok(ForeignItem::constructor(owner, &path, variadic, &js_inputs)) + } + BindingKind::IndexingGetter => { + if argument_count != 1 || !matches!(&sig.output, ReturnType::Type(..)) { + return Err(Error::new( + span, + "`indexing_getter` requires one argument and a return value", + )); + } - let name = getter - .as_ref() - .or(setter.as_ref()) - .cloned() - .or(js_name) - .unwrap_or_else(|| sig.ident.to_string()); - let (owner, path, receiver) = - Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); - - if getter.is_some() { - if argument_count != 0 || !matches!(&sig.output, ReturnType::Type(..)) { - return Err(Error::new( - span, - "`getter` requires no arguments and a return value", - )); + Ok(ForeignItem::indexing_getter( + self_ty.expect("validated above"), + &js_inputs, + )) } + BindingKind::IndexingSetter => { + if argument_count != 2 { + return Err(Error::new(span, "`indexing_setter` requires two arguments")); + } - Ok(ForeignItem::getter(owner, path)) - } else if setter.is_some() { - if argument_count != 1 || !matches!(&sig.output, ReturnType::Default) { - return Err(Error::new( - span, - "`setter` requires one argument and no return value", - )); + Ok(ForeignItem::indexing_setter( + self_ty.expect("validated above"), + &js_inputs, + )) } + BindingKind::IndexingDeleter => { + if argument_count != 1 { + return Err(Error::new(span, "`indexing_deleter` requires one argument")); + } - Ok(ForeignItem::setter(owner, &path, receiver, &js_inputs)) - } else { - Ok(ForeignItem::call( - owner, path, receiver, variadic, namespace, &js_inputs, - )) + Ok(ForeignItem::indexing_deleter( + self_ty.expect("validated above"), + &js_inputs, + )) + } + BindingKind::Getter(name) => { + if argument_count != 0 || !matches!(&sig.output, ReturnType::Type(..)) { + return Err(Error::new( + span, + "`getter` requires no arguments and a return value", + )); + } + let (owner, path, _) = + Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); + + Ok(ForeignItem::getter(owner, path)) + } + BindingKind::Setter(name) => { + if argument_count != 1 || !matches!(&sig.output, ReturnType::Default) { + return Err(Error::new( + span, + "`setter` requires one argument and no return value", + )); + } + let (owner, path, receiver) = + Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); + + Ok(ForeignItem::setter(owner, &path, receiver, &js_inputs)) + } + BindingKind::Call => { + let name = js_name.unwrap_or_else(|| sig.ident.to_string()); + let (owner, path, receiver) = + Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); + + Ok(ForeignItem::call( + owner, &path, receiver, variadic, namespace, &js_inputs, + )) + } + BindingKind::Embed(_) | BindingKind::Import => { + unreachable!("external bindings returned above") + } } } @@ -558,10 +633,12 @@ impl FunctionPlan { let Self { inputs, output_ty, + output_abi_override, binding, suspending, .. } = self; + let output_abi_ty = output_abi_override.as_ref().or(output_ty.as_ref()); let input_descriptors = inputs.iter().map(|input| { let name = &input.descriptor_name; let ty = &input.abi_type; @@ -588,7 +665,7 @@ impl FunctionPlan { required_embeds.push(quote_spanned!(span=> #macro_path::js_input_embed::<#ty>())); } - if let Some(ty) = output_ty { + if let Some(ty) = output_abi_ty { required_embeds.push(quote_spanned!(span=> #macro_path::js_output_embed::<#ty>())); required_embeds.push(quote_spanned!(span=> #macro_path::js_result_embed::<#ty>())); } @@ -609,10 +686,7 @@ impl FunctionPlan { }), ForeignItem::Embed(name) => { let path = format!("this.#jsEmbed.{crate_}['{name}']"); - let arguments = inputs - .iter() - .map(|input| input.slot_names[0].to_string()) - .join(", "); + let arguments = join_input_slots(inputs); let indirect_call = format!("{path}({arguments})"); Some(quote_spanned! {span=> @@ -631,7 +705,7 @@ impl FunctionPlan { } else { quote_spanned!(span=> ::core::option::Option::None) }; - let output = if let Some(output) = output_ty { + let output = if let Some(output) = output_abi_ty { quote_spanned!(span=> ::core::option::Option::Some(#macro_path::import_output::<#output>()) ) diff --git a/host/js-sys-bindgen/src/function/js.rs b/host/js-sys-bindgen/src/function/js.rs index 622d2ea6..5712ba1c 100644 --- a/host/js-sys-bindgen/src/function/js.rs +++ b/host/js-sys-bindgen/src/function/js.rs @@ -1,4 +1,3 @@ -use itertools::Itertools; use syn::{Ident, Path}; /// The final JavaScript binding selected for a foreign function. @@ -45,7 +44,7 @@ impl ForeignItem { pub(super) fn call( owner: Option, - path: String, + path: &str, receiver: bool, variadic: bool, namespace: Option<&str>, @@ -61,7 +60,7 @@ impl ForeignItem { let direct_call = if direct_wrapper { indirect_call.clone() } else { - path + path.to_owned() }; Self::Generate { @@ -85,12 +84,7 @@ impl ForeignItem { } pub(super) fn getter(owner: Option, path: String) -> Self { - Self::Generate { - owner, - direct_wrapper: true, - direct_call: path.clone(), - indirect_call: path, - } + Self::expression(owner, path) } pub(super) fn setter( @@ -102,12 +96,25 @@ impl ForeignItem { let arguments = Self::arguments(inputs, receiver, false); let call = format!("{path} = {arguments}"); - Self::Generate { - owner, - direct_wrapper: true, - direct_call: call.clone(), - indirect_call: call, - } + Self::expression(owner, call) + } + + pub(super) fn indexing_getter(owner: Path, inputs: &[String]) -> Self { + let call = format!("{}[{}]", inputs[0], inputs[1]); + + Self::expression(Some(owner), call) + } + + pub(super) fn indexing_setter(owner: Path, inputs: &[String]) -> Self { + let call = format!("{}[{}] = {}", inputs[0], inputs[1], inputs[2]); + + Self::expression(Some(owner), call) + } + + pub(super) fn indexing_deleter(owner: Path, inputs: &[String]) -> Self { + let call = format!("delete {}[{}]", inputs[0], inputs[1]); + + Self::expression(Some(owner), call) } pub(super) fn global_path(namespace: Option<&str>, name: &str) -> String { @@ -125,6 +132,15 @@ impl ForeignItem { .expect("static and instance bindings always have an owner") } + fn expression(owner: Option, expression: String) -> Self { + Self::Generate { + owner, + direct_wrapper: true, + direct_call: expression.clone(), + indirect_call: expression, + } + } + fn arguments(inputs: &[String], receiver: bool, variadic: bool) -> String { let inputs = if receiver { &inputs[1..] } else { inputs }; @@ -136,10 +152,10 @@ impl ForeignItem { if inputs.is_empty() { format!("...{last}") } else { - format!("{}, ...{last}", inputs.iter().join(", ")) + format!("{}, ...{last}", inputs.join(", ")) } } else { - inputs.iter().join(", ") + inputs.join(", ") } } } diff --git a/host/js-sys-bindgen/src/function/options.rs b/host/js-sys-bindgen/src/function/options.rs index 35ebf54e..449e53c5 100644 --- a/host/js-sys-bindgen/src/function/options.rs +++ b/host/js-sys-bindgen/src/function/options.rs @@ -1,7 +1,6 @@ -use syn::{Attribute, Error, Ident, LitStr, Path, Result}; +use syn::{Attribute, Error, Ident, LitStr, Path, Result, Type}; /// Options read from `#[js_sys(...)]` on a foreign function declaration. -#[derive(Default)] pub(super) struct FunctionOptions { /// Overrides the JavaScript function or constructor name. pub(super) js_name: Option, @@ -10,24 +9,54 @@ pub(super) struct FunctionOptions { /// Spreads the foreign function's last argument at the JavaScript call /// site. pub(super) variadic: bool, - /// Generates a JavaScript `new` expression for the foreign function. - pub(super) constructor: bool, - /// Reads this JavaScript property instead of calling a function. - pub(super) getter: Option, - /// Writes this JavaScript property instead of calling a function. - pub(super) setter: Option, - /// Uses a named JavaScript implementation embedded by the current crate. - pub(super) embed: Option, - /// Leaves the JavaScript implementation to the import object. - pub(super) import: bool, + /// The validated JavaScript implementation and operation. + pub(super) binding: BindingKind, + /// Uses this concrete type to describe the JavaScript return conversion. + pub(super) return_abi: Option, /// Allows a Promise returned by the JavaScript implementation to suspend /// the current Wasm stack. pub(super) suspending: bool, } +/// The mutually exclusive JavaScript binding selected by the attributes. +pub(super) enum BindingKind { + Call, + Constructor, + Getter(String), + Setter(String), + IndexingGetter, + IndexingSetter, + IndexingDeleter, + Embed(String), + Import, +} + +impl BindingKind { + pub(super) fn is_external(&self) -> bool { + matches!(self, Self::Embed(_) | Self::Import) + } +} + +#[derive(Default)] +struct RawFunctionOptions { + js_name: Option, + static_of: Option, + variadic: bool, + constructor: bool, + getter: Option, + setter: Option, + indexing_getter: bool, + indexing_setter: bool, + indexing_deleter: bool, + embed: Option, + import: bool, + return_abi: Option, + suspending: bool, +} + impl FunctionOptions { pub(super) fn parse(attrs: &mut Vec, rust_name: &Ident) -> Result { - let mut options = Self::default(); + let mut options = RawFunctionOptions::default(); for attr in attrs.extract_if(.., |attr| attr.path().is_ident("js_sys")) { attr.parse_nested_meta(|meta| { @@ -75,6 +104,12 @@ impl FunctionOptions { } else { Ok(()) } + } else if meta.path.is_ident("indexing_getter") { + parse_flag(&meta, "indexing_getter", &mut options.indexing_getter) + } else if meta.path.is_ident("indexing_setter") { + parse_flag(&meta, "indexing_setter", &mut options.indexing_setter) + } else if meta.path.is_ident("indexing_deleter") { + parse_flag(&meta, "indexing_deleter", &mut options.indexing_deleter) } else if meta.path.is_ident("js_embed") { let name = meta.value()?.parse::()?.value(); @@ -85,6 +120,14 @@ impl FunctionOptions { } } else if meta.path.is_ident("js_import") { parse_flag(&meta, "js_import", &mut options.import) + } else if meta.path.is_ident("return_abi") { + let ty = meta.value()?.parse()?; + + if options.return_abi.replace(ty).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } } else if meta.path.is_ident("suspending") { parse_flag(&meta, "suspending", &mut options.suspending) } else { @@ -94,15 +137,21 @@ impl FunctionOptions { } options.validate(rust_name)?; - Ok(options) + Ok(options.finish()) } +} +impl RawFunctionOptions { fn validate(&self, rust_name: &Ident) -> Result<()> { let source_count = usize::from(self.import) + usize::from(self.embed.is_some()); let operation_count = usize::from(self.constructor) + usize::from(self.getter.is_some()) - + usize::from(self.setter.is_some()); + + usize::from(self.setter.is_some()) + + usize::from(self.indexing_getter) + + usize::from(self.indexing_setter) + + usize::from(self.indexing_deleter); let has_property = self.getter.is_some() || self.setter.is_some(); + let has_indexing = self.indexing_getter || self.indexing_setter || self.indexing_deleter; let has_binding_options = self.js_name.is_some() || self.static_of.is_some() || operation_count != 0 @@ -124,7 +173,7 @@ impl FunctionOptions { if operation_count > 1 { return Err(Error::new_spanned( rust_name, - "`constructor`, `getter`, and `setter` are mutually exclusive", + "JavaScript operations are mutually exclusive", )); } if self.constructor && self.static_of.is_some() { @@ -133,22 +182,67 @@ impl FunctionOptions { "`constructor` cannot be combined with `static_of`", )); } - if has_property && self.js_name.is_some() { + if (has_property || has_indexing) && self.js_name.is_some() { return Err(Error::new_spanned( rust_name, - "`js_name` cannot be combined with `getter` or `setter`; specify the field on the \ - property operation", + "`js_name` cannot be combined with a property operation", )); } - if has_property && self.variadic { + if (has_property || has_indexing) && self.variadic { return Err(Error::new_spanned( rust_name, - "`variadic` cannot be combined with `getter` or `setter`", + "`variadic` cannot be combined with a property operation", )); } Ok(()) } + + fn finish(self) -> FunctionOptions { + let Self { + js_name, + static_of, + variadic, + constructor, + getter, + setter, + indexing_getter, + indexing_setter, + indexing_deleter, + embed, + import, + return_abi, + suspending, + } = self; + let binding = if import { + BindingKind::Import + } else if let Some(embed) = embed { + BindingKind::Embed(embed) + } else if constructor { + BindingKind::Constructor + } else if let Some(getter) = getter { + BindingKind::Getter(getter) + } else if let Some(setter) = setter { + BindingKind::Setter(setter) + } else if indexing_getter { + BindingKind::IndexingGetter + } else if indexing_setter { + BindingKind::IndexingSetter + } else if indexing_deleter { + BindingKind::IndexingDeleter + } else { + BindingKind::Call + }; + + FunctionOptions { + js_name, + static_of, + variadic, + binding, + return_abi, + suspending, + } + } } fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, name: &str, value: &mut bool) -> Result<()> { diff --git a/host/js-sys-bindgen/src/hygiene.rs b/host/js-sys-bindgen/src/hygiene.rs index aac54a54..4edc8947 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -1,24 +1,24 @@ -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] use foldhash::fast::FixedState; -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] use hashbrown::{HashMap, HashSet}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; use syn::{Attribute, Ident, Path, parse_quote_spanned}; -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] use syn::{ItemUse, parse_quote}; pub(crate) enum Hygiene<'a> { /// File generation mode: emit short paths and record the required `use` /// items. - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Imports(&'a mut ImportManager), /// Procedural macro mode: emit paths qualified through the selected crate. Qualified { js_sys: Option<&'a Path> }, } #[cfg_attr( - not(any(feature = "file", feature = "web-idl", test)), + not(any(feature = "file", feature = "web-idl")), expect( unused_variables, reason = "attributes are only consumed by source-generation hygiene" @@ -43,7 +43,7 @@ impl Hygiene<'_> { fn js_sys_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Hygiene::Imports(imports) => { imports.js_sys_push(attrs, ident.clone()); parse_quote_spanned!(span=> #ident) @@ -56,7 +56,7 @@ impl Hygiene<'_> { fn hazard_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Hygiene::Imports(imports) => { imports.hazard_push(attrs, ident.clone()); parse_quote_spanned!(span=> #ident) @@ -69,7 +69,7 @@ impl Hygiene<'_> { pub(crate) fn as_ref(&mut self, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Hygiene::Imports(_) => { parse_quote_spanned!(span=> AsRef) } @@ -81,7 +81,7 @@ impl Hygiene<'_> { pub(crate) fn deref(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Hygiene::Imports(imports) => { imports.deref.insert(attrs.to_vec()); parse_quote_spanned!(span=> Deref) @@ -94,7 +94,7 @@ impl Hygiene<'_> { pub(crate) fn phantom_data(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Hygiene::Imports(imports) => { imports .phantom_data @@ -109,7 +109,7 @@ impl Hygiene<'_> { pub(crate) fn from(&mut self, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl", test))] + #[cfg(any(feature = "file", feature = "web-idl"))] Hygiene::Imports(_) => { parse_quote_spanned!(span=> From) } @@ -128,12 +128,12 @@ impl Hygiene<'_> { } } -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] type FixedHashMap = HashMap; -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] type FixedHashSet = HashSet; -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] pub(crate) struct ImportManager { js_sys: Path, deref: FixedHashSet>, @@ -142,7 +142,7 @@ pub(crate) struct ImportManager { hazard_imports: FixedHashMap, FixedHashSet>, } -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] impl ImportManager { #[must_use] pub(crate) fn new(js_sys: Option) -> Self { @@ -207,7 +207,7 @@ impl ImportManager { } } -#[cfg(any(feature = "file", feature = "web-idl", test))] +#[cfg(any(feature = "file", feature = "web-idl"))] impl ToTokens for ImportManager { fn to_tokens(&self, tokens: &mut TokenStream) { for item_use in self.iter() { diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index df54ec84..6193cd18 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -287,7 +287,7 @@ fn render_import_groups(imports: Vec) -> TokenStream { output.extend(quote::quote! { #(#cfg_attrs)* const _: () = { - const IMPORTS: &[#macro_path::ImportDescriptor] = &[#(#descriptors),*]; + static IMPORTS: &[#macro_path::ImportDescriptor] = &[#(#descriptors),*]; const WAT_CAPACITY: ::core::primitive::usize = #macro_path::import_wat_capacity(IMPORTS); diff --git a/host/js-sys-bindgen/src/tests/closure.rs b/host/js-sys-bindgen/src/tests/closure.rs index 6ed205f1..ba5173ed 100644 --- a/host/js-sys-bindgen/src/tests/closure.rs +++ b/host/js-sys-bindgen/src/tests/closure.rs @@ -8,7 +8,10 @@ fn expand(input: TokenStream) -> String { } #[test] -fn package_version_disambiguates_symbols() { +fn symbols_are_stable_and_disambiguated() { + let input = quote!(dyn FnMut(i32) -> i32, move |value| value + 1); + assert_eq!(expand(input.clone()), expand(input)); + let first = crate::closure::closure_with( quote!(dyn Fn(), || {}), "test-crate", @@ -29,12 +32,6 @@ fn package_version_disambiguates_symbols() { assert_ne!(first, second); } -#[test] -fn expansion_is_deterministic() { - let input = quote!(dyn FnMut(i32) -> i32, move |value| value + 1); - assert_eq!(expand(input.clone()), expand(input)); -} - #[test] fn invalid_trait_object() { let error = crate::closure::closure_with( diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs index 7a89143a..b2fa0519 100644 --- a/host/js-sys-bindgen/src/tests/macro/export.rs +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -2,13 +2,9 @@ use proc_macro2::TokenStream; use quote::quote; use syn::File; -fn expand(function: &TokenStream) -> (String, String) { - expand_with_attr(&TokenStream::new(), function) -} - -fn expand_with_attr(attr: &TokenStream, function: &TokenStream) -> (String, String) { +fn expand(attr: TokenStream, function: &TokenStream) -> (String, String) { let function = syn::parse2(quote! { #function }).unwrap(); - let output = crate::export::r#macro(attr.clone(), &function, Some("test_crate")).unwrap(); + let output = crate::export::r#macro(attr, &function, Some("test_crate")).unwrap(); let output = prettyplease::unparse(&syn::parse2::(output).unwrap()); let dir = tempfile::tempdir().unwrap(); let (wat, js_import, js_export) = super::inner(dir.path(), &output).unwrap(); @@ -18,144 +14,60 @@ fn expand_with_attr(attr: &TokenStream, function: &TokenStream) -> (String, Stri } #[test] -fn js_name_expression() { - let (wat, js) = expand_with_attr( - "e!(js_name = concat!("module", "::answer")), - "e! { - fn answer() -> u32 { - 42 - } - }, - ); - - inline_snap::inline_snap!( - wat, - r#" -(import "env" "raw" (func $raw (@sym (name "__export_module::answer")) (result i32))) -(func $export (@sym (name "module::answer")) (result i32) - call $raw (@reloc) -)"# - ); - assert_eq!( - js, - r"() => { - const ret = wasmExports['module::answer']() - return ret >>> 0 -}" - ); -} - -#[test] -fn promising_direct() { - let (_, js) = expand_with_attr( - "e!(promising), +fn named_indirect_export_end_to_end() { + let (wat, js) = expand( + quote!(js_name = concat!("module", "::add")), "e! { - fn echo(value: i32) -> i32 { - value + pub fn add(value: u32, delta: u128) -> u128 { + u128::from(value) + delta } }, ); - assert_eq!(js, "WebAssembly.promising(wasmExports['echo'])"); + assert!(wat.contains(r#"(@sym (name "__export_module::add"))"#)); + assert!(wat.contains("(param i32) (param i32) (param i64 i64)")); + assert!(wat.contains("(result i64 i64)")); + assert!(wat.contains("global.get $__stack_pointer")); + assert!(wat.contains("i64.load offset=8")); + let expected = [ + "(arg0, arg1) => {", + " const ret = wasmExports['module::add'](arg0, arg1, arg1 >> 64n)", + " return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1])", + "}", + ] + .join("\n"); + assert_eq!(js, expected); } #[test] -fn promising_without_output_converts_inputs() { - let (_, js) = expand_with_attr( - "e!(promising), +fn promising_result_end_to_end() { + let (wat, js) = expand( + quote!(promising), "e! { - fn notify(value: u128) { - let _ = value; - } - }, - ); - - assert_eq!( - js, - "(() => {\n const $promising = WebAssembly.promising(wasmExports['notify'])\n \ - return (arg0) => $promising(arg0, arg0 >> 64n)\n})()", - ); -} - -#[test] -fn promising_postprocesses_fulfilled_values() { - let (_, js) = expand_with_attr( - "e!(promising), - "e! { - fn echo(value: u32) -> u32 { - value - } - }, - ); - - assert_eq!( - js, - "(() => {\n const $promising = WebAssembly.promising(wasmExports['echo'])\n return \ - (arg0) => $promising(arg0).then(ret => {\n return ret >>> 0\n })\n})()", - ); -} - -#[test] -fn promising_externref_uses_passthrough() { - let (_, js) = expand_with_attr( - "e!(promising), - "e! { - fn echo(value: JsValue) -> JsValue { - value - } - }, - ); - - assert_eq!(js, "WebAssembly.promising(wasmExports['echo'])"); -} - -#[test] -fn promising_converts_multivalue_results() { - let (_, js) = expand_with_attr( - "e!(promising), - "e! { - fn echo(value: u128) -> u128 { - value - } - }, - ); - - assert_eq!( - js, - "(() => {\n const $promising = WebAssembly.promising(wasmExports['echo'])\n return \ - (arg0) => $promising(arg0, arg0 >> 64n).then(ret => {\n return \ - this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1])\n })\n})()", - ); -} - -#[test] -fn promising_turns_result_errors_into_rejections() { - let (_, js) = expand_with_attr( - "e!(promising), - "e! { - fn checked(value: i32) -> Result { + pub fn checked(value: u128) -> Result { Ok(value) } }, ); + assert!(wat.contains("(result i64 i64 i32 externref)")); + assert!(wat.contains("global.get $__stack_pointer")); assert_eq!( js, "(() => {\n const $promising = WebAssembly.promising(wasmExports['checked'])\n \ - return (arg0) => $promising(arg0).then(ret => {\n if (ret[1] !== 0) throw \ - ret[2]\n return ret[0]\n })\n})()", + return (arg0) => $promising(arg0, arg0 >> 64n).then(ret => {\n if (ret[2] !== 0) \ + throw ret[3]\n return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], \ + ret[1])\n })\n})()" ); } #[test] -fn promising_attribute_is_a_flag() { - let function = syn::parse2(quote! { +fn invalid_export_options() { + let function: syn::ItemFn = syn::parse_quote! { fn answer() -> i32 { 42 } - }) - .unwrap(); - + }; let error = crate::export::r#macro(quote!(promising = true), &function, Some("test_crate")) .unwrap_err(); assert_eq!(error.to_string(), "`promising` supports no values"); @@ -163,409 +75,13 @@ fn promising_attribute_is_a_flag() { let error = crate::export::r#macro(quote!(promising, promising), &function, Some("test_crate")) .unwrap_err(); assert_eq!(error.to_string(), "duplicate `promising` argument"); -} -#[test] -fn borrowed_return_is_rejected() { - let function = syn::parse2(quote! { + let borrowed: syn::ItemFn = syn::parse_quote! { fn echo(value: &JsString) -> &JsString { value } - }) - .unwrap(); + }; let error = - crate::export::r#macro(TokenStream::new(), &function, Some("test_crate")).unwrap_err(); - + crate::export::r#macro(TokenStream::new(), &borrowed, Some("test_crate")).unwrap_err(); assert_eq!(error.to_string(), "cannot return a borrowed reference"); } - -#[test] -fn direct() { - let (wat, js) = expand("e! { - fn echo(value: u32) -> u32 { - value - } - }); - - inline_snap::inline_snap!( - wat, - r#" -(import "env" "raw" (func $raw (@sym (name "__export_echo")) (param i32) (result i32))) -(func $export (@sym (name "echo")) (param $arg0_0 i32) (result i32) - local.get $arg0_0 - call $raw (@reloc) -)"# - ); - assert_eq!( - js, - r"(arg0) => { - const ret = wasmExports['echo'](arg0) - return ret >>> 0 -}" - ); -} - -#[test] -fn scalar_passthrough() { - let (_, js) = expand("e! { - fn echo(value: i32) -> i32 { - value - } - }); - - assert_eq!(js, "wasmExports['echo']"); -} - -#[test] -fn wat_slot_conversions() { - let (wat, js) = expand("e! { - pub fn drop_value(value: JsValue) { - let _ = value; - } - }); - - inline_snap::inline_snap!( - wat, - " - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) - (import \"env\" \"raw\" (func $raw (@sym (name \"__export_drop_value\")) (param i32))) - (func $export (@sym (name \"drop_value\")) (param $arg0_0 externref) - (local $js_sys.externref.value externref) - (local $js_sys.externref.index i32) - local.get $arg0_0 - local.set $js_sys.externref.value - call $js_sys.externref.next (@reloc) - local.tee $js_sys.externref.index - local.get $js_sys.externref.value - table.set $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - call $raw (@reloc) - )" - ); - assert_eq!(js, "wasmExports['drop_value']"); - - let (wat, js) = expand("e! { - pub fn undefined() -> Option { - None - } - }); - - inline_snap::inline_snap!( - wat, - " - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) - (import \"env\" \"raw\" (func $raw (@sym (name \"__export_undefined\")) (result i32))) - (func $export (@sym (name \"undefined\")) (result externref) - (local $js_sys.externref.index i32) - call $raw (@reloc) - local.tee $js_sys.externref.index - table.get $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - i32.const 2 - i32.ge_u - if - local.get $js_sys.externref.index - call $js_sys.externref.release (@reloc) - end - )" - ); - assert_eq!(js, "wasmExports['undefined']"); -} - -#[test] -fn indirect_and_multiple_parameters() { - let (wat, js) = expand("e! { - pub fn add(value: u32, delta: u128) -> u128 { - u128::from(value) + delta - } - }); - - inline_snap::inline_snap!( - wat, - r#" -(import "env" "raw" (func $raw (@sym (name "__export_add")) (param i32) (param i32) (param i64 i64))) -(import "env" "__stack_pointer" (global $__stack_pointer (mut i32))) -(func $export (@sym (name "add")) (param $arg0_0 i32) (param $arg1_0 i64) (param $arg1_1 i64) (result i64 i64) - (local $retptr i32) - global.get $__stack_pointer - i32.const 16 - i32.sub - local.tee $retptr - global.set $__stack_pointer - local.get $retptr - local.get $arg0_0 - local.get $arg1_0 - local.get $arg1_1 - call $raw (@reloc) - local.get $retptr - i64.load offset=0 - local.get $retptr - i64.load offset=8 - local.get $retptr - i32.const 16 - i32.add - global.set $__stack_pointer -)"# - ); - assert_eq!( - js, - r"(arg0, arg1) => { - const ret = wasmExports['add'](arg0, arg1, arg1 >> 64n) - return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1]) -}" - ); -} - -#[test] -fn result() { - let (wat, js) = expand("e! { - pub fn checked_add(value: u128, delta: u128) -> Result { - value.checked_add(delta).ok_or(JsValue::UNDEFINED) - } - }); - - inline_snap::inline_snap!( - wat, - " - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) - (import \"env\" \"raw\" (func $raw (@sym (name \"__export_checked_add\")) (param i32) (param i64 \ - i64) (param i64 i64))) - (import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut i32))) - (func $export (@sym (name \"checked_add\")) (param $arg0_0 i64) (param $arg0_1 i64) (param \ - $arg1_0 i64) (param $arg1_1 i64) (result i64 i64 i32 externref) - (local $retptr i32) - (local $js_sys.result.discriminant i32) - (local $js_sys.externref.index i32) - global.get $__stack_pointer - i32.const 32 - i32.sub - local.tee $retptr - global.set $__stack_pointer - local.get $retptr - local.get $arg0_0 - local.get $arg0_1 - local.get $arg1_0 - local.get $arg1_1 - call $raw (@reloc) - local.get $retptr - i64.load offset=0 - local.get $retptr - i64.load offset=8 - local.get $retptr - i32.load offset=16 - local.tee $js_sys.result.discriminant - local.get $retptr - i32.load offset=20 - local.set $js_sys.externref.index - local.get $js_sys.result.discriminant - if (result externref) - local.get $js_sys.externref.index - table.get $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - i32.const 2 - i32.ge_u - if - local.get $js_sys.externref.index - call $js_sys.externref.release (@reloc) - end - else - ref.null extern - end - local.get $retptr - i32.const 32 - i32.add - global.set $__stack_pointer - )" - ); - assert_eq!( - js, - r"(arg0, arg1) => { - const ret = wasmExports['checked_add'](arg0, arg0 >> 64n, arg1, arg1 >> 64n) - if (ret[2] !== 0) throw ret[3] - return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1]) -}" - ); -} - -#[test] -fn single_slot_result() { - let (wat, js) = expand("e! { - pub fn checked_add(value: i32, delta: i32) -> Result { - value.checked_add(delta).ok_or(JsValue::UNDEFINED) - } - }); - - inline_snap::inline_snap!( - wat, - " - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) - (import \"env\" \"raw\" (func $raw (@sym (name \"__export_checked_add\")) (param i32) (param \ - i32) (param i32))) - (import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut i32))) - (func $export (@sym (name \"checked_add\")) (param $arg0_0 i32) (param $arg1_0 i32) (result i32 \ - i32 externref) - (local $retptr i32) - (local $js_sys.result.discriminant i32) - (local $js_sys.externref.index i32) - global.get $__stack_pointer - i32.const 16 - i32.sub - local.tee $retptr - global.set $__stack_pointer - local.get $retptr - local.get $arg0_0 - local.get $arg1_0 - call $raw (@reloc) - local.get $retptr - i32.load offset=0 - local.get $retptr - i32.load offset=4 - local.tee $js_sys.result.discriminant - local.get $retptr - i32.load offset=8 - local.set $js_sys.externref.index - local.get $js_sys.result.discriminant - if (result externref) - local.get $js_sys.externref.index - table.get $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - i32.const 2 - i32.ge_u - if - local.get $js_sys.externref.index - call $js_sys.externref.release (@reloc) - end - else - ref.null extern - end - local.get $retptr - i32.const 16 - i32.add - global.set $__stack_pointer - )" - ); - assert_eq!( - js, - r"(arg0, arg1) => { - const ret = wasmExports['checked_add'](arg0, arg1) - if (ret[1] !== 0) throw ret[2] - return ret[0] -}" - ); -} - -#[test] -fn unit_result() { - let (wat, js) = expand("e! { - pub fn succeeds() -> Result<(), JsValue> { - Ok(()) - } - }); - - inline_snap::inline_snap!( - wat, - " - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.release\" (func $js_sys.externref.release (@sym) (param i32))) - (import \"env\" \"raw\" (func $raw (@sym (name \"__export_succeeds\")) (param i32))) - (import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut i32))) - (func $export (@sym (name \"succeeds\")) (result i32 externref) - (local $retptr i32) - (local $js_sys.result.discriminant i32) - (local $js_sys.externref.index i32) - global.get $__stack_pointer - i32.const 16 - i32.sub - local.tee $retptr - global.set $__stack_pointer - local.get $retptr - call $raw (@reloc) - local.get $retptr - i32.load offset=0 - local.tee $js_sys.result.discriminant - local.get $retptr - i32.load offset=4 - local.set $js_sys.externref.index - local.get $js_sys.result.discriminant - if (result externref) - local.get $js_sys.externref.index - table.get $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - i32.const 2 - i32.ge_u - if - local.get $js_sys.externref.index - call $js_sys.externref.release (@reloc) - end - else - ref.null extern - end - local.get $retptr - i32.const 16 - i32.add - global.set $__stack_pointer - )" - ); - assert_eq!( - js, - r"() => { - const ret = wasmExports['succeeds']() - if (ret[0] !== 0) throw ret[1] - return undefined -}" - ); -} - -#[test] -fn no_parameters() { - let (wat, js) = expand("e! { - pub fn answer() -> u32 { - 42 - } - }); - - inline_snap::inline_snap!( - wat, - r#" -(import "env" "raw" (func $raw (@sym (name "__export_answer")) (result i32))) -(func $export (@sym (name "answer")) (result i32) - call $raw (@reloc) -)"# - ); - assert_eq!( - js, - r"() => { - const ret = wasmExports['answer']() - return ret >>> 0 -}" - ); -} - -#[test] -fn no_return_value() { - let (wat, js) = expand("e! { - pub fn nothing(value: u32) -> () { - let _ = value; - } - }); - - inline_snap::inline_snap!( - wat, - r#" -(import "env" "raw" (func $raw (@sym (name "__export_nothing")) (param i32))) -(func $export (@sym (name "nothing")) (param $arg0_0 i32) - local.get $arg0_0 - call $raw (@reloc) -)"# - ); - assert_eq!(js, "wasmExports['nothing']"); -} diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index 8c47e19b..4ec7638e 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -1,945 +1,211 @@ -#[test] -fn basic() { - test!( - {}, - { - extern "js-sys" { - pub fn log(data: &JsValue); - } - }, - { - pub fn log(data: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } +use proc_macro2::TokenStream; +use quote::quote; +use syn::Item; - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data); - unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.log", - indirect_call: "globalThis.log(arg0_0)", - required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); +fn expand(attr: TokenStream, input: syn::ItemForeignMod) -> String { + let items = crate::r#macro::expand_for_test(attr, input, "test_crate") + .unwrap() + .into_items() + .unwrap(); - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.log (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", - ); + prettyplease::unparse(&syn::File { + shebang: None, + attrs: Vec::new(), + items, + }) } -#[test] -fn namespace() { - test!( - { namespace = "console" }, - { - extern "js-sys" { - pub fn log(data: &JsValue); - } - }, - { - pub fn log(data: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.console.log"] - fn log( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data); - unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "console.log", - "test_crate.console.log", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: true, - direct_call: "globalThis.console.log(arg0_0)", - indirect_call: "globalThis.console.log(arg0_0)", - required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"console.log\" (func $test_crate.import.console.log (@sym (name \ - \"test_crate.import.console.log\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.console.log (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.console.log (@reloc) - )", - "(arg0_0) => globalThis.console.log(arg0_0)", - ); +fn link(input: syn::ItemForeignMod) -> (Option, Option) { + let output = expand(TokenStream::new(), input); + let dir = tempfile::tempdir().unwrap(); + let (wat, js, _) = super::inner(dir.path(), &output).unwrap(); + (wat, js) } #[test] -fn js_sys() { - test!( - { js_sys = js_sys }, - { - extern "js-sys" { - pub fn log(data: &JsValue); - } - }, - { - pub fn log(data: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log( - arg0_0: js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: js_sys::r#macro::InputSlot4<&JsValue>, - ); - } +fn imports_are_batched_end_to_end() { + let (wat, js) = link(syn::parse_quote! { + extern "js-sys" { + #[js_sys(js_import)] + pub fn first(value: &JsValue); - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - js_sys::r#macro::split_input::<&JsValue>(data); - unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[js_sys::r#macro::ImportDescriptor] = - &[js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.log", - indirect_call: "globalThis.log(arg0_0)", - required_embeds: &[js_sys::r#macro::js_input_embed::<&JsValue>()], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - js_sys::r#macro::import_wat_capacity(IMPORTS); + #[js_sys(js_import)] + pub fn second(value: &JsValue); + } + }); + let wat = wat.unwrap(); - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: js_sys::r#macro::ImportSection = - js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: js_sys::r#macro::ImportSection = - js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.log (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", + assert!(wat.contains(r#"(import "test_crate" "first""#)); + assert!(wat.contains(r#"(import "test_crate" "second""#)); + assert_eq!( + wat.matches(r#"(import "js_sys" "externref.table""#).count(), + 1 ); -} - -#[test] -fn two_parameters() { - test!( - {}, - { - extern "js-sys" { - pub fn log(data1: &JsValue, data2: &JsValue); - } - }, - { - pub fn log(data1: &JsValue, data2: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - arg1_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg1_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg1_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg1_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data1); - let (arg1_0, arg1_1, arg1_2, arg1_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data2); - unsafe { - log( - arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3, - ) - } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[ - ::js_sys::r#macro::import_input::<&JsValue>("arg0"), - ::js_sys::r#macro::import_input::<&JsValue>("arg1"), - ], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.log", - indirect_call: "globalThis.log(arg0_0, arg1_0)", - required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")) (param externref externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.log (@sym) (param $arg0_0 i32) (param $arg1_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - local.get $arg1_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", + assert_eq!( + wat.matches("table.get $js_sys.import.externref.table") + .count(), + 2 ); + assert_eq!(js, None); } #[test] -fn groups_functions_and_shared_wat_imports() { - test!( - {}, - { +fn binding_options() { + let output = expand( + quote!(js_sys = renamed, namespace = "console"), + syn::parse_quote! { extern "js-sys" { - #[js_sys(js_import)] - pub fn first(value: &JsValue); + pub fn log(value: &JsValue); - #[js_sys(js_import)] - pub fn second(value: &JsValue); - } - }, - { - pub fn first(value: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.first"] - fn first( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } + #[js_sys(js_name = "warn")] + pub fn renamed_log(value: &JsValue); - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(value); - unsafe { first(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - - pub fn second(value: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.second"] - fn second( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } + #[js_sys(return_abi = JsValue)] + pub fn value() -> JsTest; - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(value); - unsafe { second(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = &[ - ::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "first", - "test_crate.first", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::None, - ), - ::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "second", - "test_crate.second", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::None, - ), - ]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - }; - }, - "(import \"test_crate\" \"first\" (func $test_crate.import.first (@sym (name \ - \"test_crate.import.first\")) (param externref))) - (import \"test_crate\" \"second\" (func $test_crate.import.second (@sym (name \ - \"test_crate.import.second\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.first (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.first (@reloc) - ) - (func $test_crate.second (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.second (@reloc) - )", - None, - ); -} - -#[test] -fn empty() { - test!( - {}, - { - extern "js-sys" { - pub fn log(); - } - }, - { - pub fn log() { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(); - } - - { - unsafe { log() } - }; + #[cfg(all())] + pub fn configured(); } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.log", - indirect_call: "globalThis.log()", - required_embeds: &[], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")))) - (func $test_crate.log (@sym) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", ); -} - -#[test] -fn js_name() { - test!( - {}, - { - extern "js-sys" { - #[js_sys(js_name = "log")] - pub fn logx(data: &JsValue); - } - }, - { - pub fn logx(data: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.logx"] - fn logx( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data); - unsafe { logx(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "logx", - "test_crate.logx", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.log", - indirect_call: "globalThis.log(arg0_0)", - required_embeds: &[::js_sys::r#macro::js_input_embed::<&JsValue>()], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"logx\" (func $test_crate.import.logx (@sym (name \ - \"test_crate.import.logx\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.logx (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.logx (@reloc) - )", - "globalThis.log", - ); -} + assert!(output.contains("renamed::r#macro::InputSlot1")); + assert!(output.contains(r#"direct_call: "globalThis.console.log(arg0_0)""#)); + assert!(output.contains(r#"direct_call: "globalThis.console.warn(arg0_0)""#)); + assert!(output.contains("join_output_as::")); + assert!(output.contains("#[cfg(all())]")); -#[test] -fn js_import() { - test!( - {}, - { + let output = expand( + TokenStream::new(), + syn::parse_quote! { extern "js-sys" { #[js_sys(js_import)] - pub fn log(data: &JsValue); - } - }, - { - pub fn log(data: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data); - unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::None, - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); + pub fn imported(); - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - }; - }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.log (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.log (@reloc) - )", - None, - ); -} - -#[test] -fn js_embed() { - test!( - {}, - { - extern "js-sys" { #[js_sys(js_embed = "embed")] - pub fn log(data: &JsValue); + pub fn embedded(); } }, - { - pub fn log(data: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log( - arg0_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = - ::js_sys::r#macro::split_input::<&JsValue>(data); - unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[::js_sys::r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "this.#jsEmbed.test_crate['embed']", - indirect_call: "this.#jsEmbed.test_crate['embed'](arg0_0)", - required_embeds: &[ - ("test_crate", "embed"), - ::js_sys::r#macro::js_input_embed::<&JsValue>(), - ], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.log (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.log (@reloc) - )", - "this.#jsEmbed.test_crate['embed']", ); + assert!(output.contains("::core::option::Option::None")); + assert!(output.contains(r#"direct_call: "this.#jsEmbed.test_crate['embed']""#)); } #[test] -fn suspending_direct() { - let js = generated_js(syn::parse_quote! { - extern "js-sys" { - #[js_sys(suspending)] - pub fn wait(value: i32) -> i32; - } - }); - - assert_eq!(js, "new WebAssembly.Suspending(globalThis.wait)"); -} - -#[test] -fn suspending_converts_inputs_before_calling_javascript() { - let js = generated_js(syn::parse_quote! { +fn suspending_end_to_end() { + let (_, js) = link(syn::parse_quote! { extern "js-sys" { #[js_sys(suspending)] - pub fn wait(value: u32) -> u32; + pub fn wait() -> u128; } }); assert_eq!( - js, - "new WebAssembly.Suspending((arg0_0) => {\n arg0_0 = arg0_0 >>> 0\n return \ - globalThis.wait(arg0_0)\n})", + js.unwrap(), + "new WebAssembly.Suspending(async ($retptr) => {\n $retptr = $retptr >>> 0\n const \ + $ret = await (globalThis.wait())\n this.#jsEmbed.js_sys['numeric.128.encode']($ret, \ + $ret >> 64n, $retptr)\n})", ); } #[test] -fn suspending_converts_fulfilled_indirect_results() { - let js = generated_js(syn::parse_quote! { +fn preserves_successful_functions_after_an_error() { + let input = syn::parse_quote! { extern "js-sys" { - #[js_sys(suspending)] - pub fn wait() -> u128; + pub fn good(value: i32) -> i32; + pub async fn bad(); } - }); + }; + let (Some(output), error) = + crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate").unwrap_err() + else { + panic!("expected the successful function to be preserved"); + }; - assert_eq!( - js, - "new WebAssembly.Suspending(async ($retptr) => {\n const $ret = await \ - (globalThis.wait())\n this.#jsEmbed.js_sys['numeric.128.encode']($ret, $ret >> 64n, \ - $retptr)\n})", - ); + let items = output.into_items().unwrap(); + let functions: Vec<_> = items + .iter() + .filter_map(|item| match item { + Item::Fn(function) => Some(function.sig.ident.to_string()), + _ => None, + }) + .collect(); + assert_eq!(functions, ["good"]); + assert_eq!(error.to_string(), "`async` functions are not supported"); } #[test] -fn r#return() { - test!( - {}, +fn invalid_options() { + macro_rules! assert_error { + ($input:tt, $expected:literal) => { + assert_eq!(super::macro_error(syn::parse_quote! $input), $expected); + }; + } + + assert_error!( + { + extern "C" { + pub fn log(); + } + }, + "expected `js-sys` ABI" + ); + assert_error!( { extern "js-sys" { - pub fn is_nan() -> JsValue; + #[js_sys(js_name = "renamed", js_import)] + pub fn log(); } }, + "`js_import` and `js_embed` cannot be combined with JavaScript binding options" + ); + assert_error!( { - pub fn is_nan() -> JsValue { - unsafe extern "C" { - #[link_name = "test_crate.is_nan"] - fn is_nan() -> ::js_sys::r#macro::OutputRet; - } - - ::js_sys::r#macro::join_output({ unsafe { is_nan() } }) + extern "js-sys" { + #[js_sys(js_import, suspending)] + pub fn wait(); } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "is_nan", - "test_crate.is_nan", - &[], - ::core::option::Option::Some(::js_sys::r#macro::import_output::()), - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.is_nan", - indirect_call: "globalThis.is_nan()", - required_embeds: &[ - ::js_sys::r#macro::js_output_embed::(), - ::js_sys::r#macro::js_result_embed::(), - ], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; }, - "(import \"test_crate\" \"is_nan\" (func $test_crate.import.is_nan (@sym (name \ - \"test_crate.import.is_nan\")) (result externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) - (func $test_crate.is_nan (@sym) (result i32) - (local $js_sys.externref.value externref) - (local $js_sys.externref.index i32) - call $test_crate.import.is_nan (@reloc) - local.set $js_sys.externref.value - call $js_sys.externref.next (@reloc) - local.tee $js_sys.externref.index - local.get $js_sys.externref.value - table.set $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - )", - "globalThis.is_nan", + "`suspending` cannot be combined with `js_import`; provide a `WebAssembly.Suspending` \ + import directly" ); -} - -#[test] -fn cfg() { - test!( - {}, + assert_error!( { extern "js-sys" { - #[cfg(all())] - pub fn log(); + #[js_sys(suspending, suspending)] + pub fn wait(); } }, + "duplicate attribute" + ); + assert_error!( { - #[cfg(all())] - pub fn log() { - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(); - } - - { - unsafe { log() } - }; + extern "js-sys" { + pub fn log( + #[js_sys(type = i32)] + #[js_sys(type = u32)] + value: i32, + ); } - - #[cfg(all())] - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "log", - "test_crate.log", - &[], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: false, - direct_call: "globalThis.log", - indirect_call: "globalThis.log()", - required_embeds: &[], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; }, - "(import \"test_crate\" \"log\" (func $test_crate.import.log (@sym (name \ - \"test_crate.import.log\")))) - (func $test_crate.log (@sym) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", + "duplicate attribute" ); -} - -#[test] -fn preserves_successful_functions_after_an_error() { - let input = syn::parse_quote! { - extern "js-sys" { - pub fn good(value: i32) -> i32; - pub async fn bad(); - } - }; - let (Some(output), error) = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap_err() - else { - panic!("expected the successful function to be preserved"); - }; - - let mut output = output.into_token_stream(); - output.extend(error.into_compile_error()); - let output = output.to_string(); - - assert!(output.contains("pub fn good")); - assert!(output.contains("\"test_crate.good\"")); - assert_eq!(output.matches("ImportDescriptor :: new").count(), 1); - assert!(!output.contains("fn bad")); - assert!(output.contains("compile_error")); - assert!(output.contains("`async` functions are not supported")); -} - -#[test] -fn requires_js_sys_abi() { - let input = syn::parse_quote! { - extern { - pub fn log(); - } - }; - - assert_eq!(super::macro_error(input), "expected `js-sys` ABI"); -} - -#[test] -fn incompatible_binding_options_are_rejected() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(js_name = "renamed", js_import)] - pub fn log(); - } - }; - - assert_eq!( - super::macro_error(input), - "`js_import` and `js_embed` cannot be combined with JavaScript binding options" + assert_error!( + { + extern "js-sys" { + #[js_sys(return_abi = JsValue)] + pub fn value(); + } + }, + "`return_abi` requires a return value" ); -} - -#[test] -fn suspending_requires_a_generated_binding() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(js_import, suspending)] - pub fn wait(); - } - }; - - assert_eq!( - super::macro_error(input), - "`suspending` cannot be combined with `js_import`; provide a `WebAssembly.Suspending` \ - import directly", + assert_error!( + { + extern "js-sys" { + #[js_sys(return_abi = JsValue, return_abi = JsTest)] + pub fn value() -> JsValue; + } + }, + "duplicate attribute" ); } - -#[test] -fn duplicate_suspending_is_rejected() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(suspending, suspending)] - pub fn wait(); - } - }; - - assert_eq!(super::macro_error(input), "duplicate attribute"); -} - -#[test] -fn duplicate_parameter_abi_override_is_rejected() { - let input = syn::parse_quote! { - extern "js-sys" { - pub fn log( - #[js_sys(type = i32)] - #[js_sys(type = u32)] - value: i32, - ); - } - }; - - assert_eq!(super::macro_error(input), "duplicate attribute"); -} -fn generated_js(input: syn::ItemForeignMod) -> String { - let output = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap() - .into_items() - .unwrap(); - let output = prettyplease::unparse(&syn::File { - shebang: None, - attrs: Vec::new(), - items: output, - }); - let dir = tempfile::tempdir().unwrap(); - let (_, js, _) = super::inner(dir.path(), &output).unwrap(); - - js.unwrap() -} diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index c41a3479..a99ce4be 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -1,549 +1,95 @@ -fn generated_items(input: syn::ItemForeignMod) -> Vec { - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") +use proc_macro2::TokenStream; + +fn generated_rust(input: syn::ItemForeignMod) -> String { + let items = crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate") .unwrap() .into_items() - .unwrap() -} + .unwrap(); -fn generated_rust(input: syn::ItemForeignMod) -> String { - let output = generated_items(input); prettyplease::unparse(&syn::File { shebang: None, attrs: Vec::new(), - items: output, + items, }) } -fn generated_js(input: syn::ItemForeignMod) -> String { - let output = generated_rust(input); - let dir = tempfile::tempdir().unwrap(); - let (_, js, _) = super::inner(dir.path(), &output).unwrap(); - js.unwrap() -} - -#[test] -fn method() { - test!( - {}, - { - extern "js-sys" { - pub fn test(self: &JsTest); - } - }, - { - impl JsTest { - pub fn test(self: &JsTest) { - unsafe extern "C" { - #[link_name = "test_crate.JsTest.test"] - fn test( - arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { - ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) - }; - unsafe { test(arg0_0, arg0_1, arg0_2, arg0_3) } - }; - } - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "JsTest.test", - "test_crate.JsTest.test", - &[::js_sys::r#macro::import_input::<&::js_sys::JsValue>( - "arg0", - )], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: true, - direct_call: "arg0_0.test()", - indirect_call: "arg0_0.test()", - required_embeds: &[::js_sys::r#macro::js_input_embed::< - &::js_sys::JsValue, - >()], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ - \"test_crate.import.JsTest.test\")) (param externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.JsTest.test (@reloc) - )", - "(arg0_0) => arg0_0.test()", - ); -} - -#[test] -fn method_par() { - test!( - {}, - { - extern "js-sys" { - pub fn test(self: &JsTest, par1: &JsValue, par2: &JsValue); - } - }, - { - impl JsTest { - pub fn test(self: &JsTest, par1: &JsValue, par2: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.JsTest.test"] - fn test( - arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, - arg1_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg1_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg1_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg1_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - arg2_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg2_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg2_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg2_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { - ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) - }; - let (arg1_0, arg1_1, arg1_2, arg1_3) = - ::js_sys::r#macro::split_input::<&JsValue>(par1); - let (arg2_0, arg2_1, arg2_2, arg2_3) = - ::js_sys::r#macro::split_input::<&JsValue>(par2); - unsafe { - test( - arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3, - arg2_0, arg2_1, arg2_2, arg2_3, - ) - } - }; - } - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "JsTest.test", - "test_crate.JsTest.test", - &[ - ::js_sys::r#macro::import_input::<&::js_sys::JsValue>("arg0"), - ::js_sys::r#macro::import_input::<&JsValue>("arg1"), - ::js_sys::r#macro::import_input::<&JsValue>("arg2"), - ], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: true, - direct_call: "arg0_0.test(arg1_0, arg2_0)", - indirect_call: "arg0_0.test(arg1_0, arg2_0)", - required_embeds: &[ - ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), - ::js_sys::r#macro::js_input_embed::<&JsValue>(), - ], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ - \"test_crate.import.JsTest.test\")) (param externref externref externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) (param $arg2_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - local.get $arg1_0 - table.get $js_sys.import.externref.table (@reloc) - local.get $arg2_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.JsTest.test (@reloc) - )", - "(arg0_0, arg1_0, arg2_0) => arg0_0.test(arg1_0, arg2_0)", - ); -} - #[test] -fn variadic() { - let input = syn::parse_quote! { +fn member_operations() { + let output = generated_rust(syn::parse_quote! { extern "js-sys" { - #[js_sys(variadic)] - pub fn push(self: &JsTest, first: &JsValue, rest: &[JsValue]); - } - }; - - assert_eq!( - generated_js(input), - "(arg0_0, arg1_0, arg2_0, arg2_1) => {\n arg2_0 = \ - this.#jsEmbed.js_sys['array.js_value.decode'](arg2_0, arg2_1)\narg0_0.push(arg1_0, \ - ...arg2_0)\n}" - ); -} - -#[test] -fn global_variadic() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(variadic)] - pub fn call(values: &JsArray); - } - }; - - assert_eq!( - generated_js(input), - "(arg0_0) => globalThis.call(...arg0_0)" - ); -} + #[js_sys(js_name = "JavaScriptType")] + pub type RustType; -#[test] -fn type_js_name() { - let input = syn::parse_quote! { - extern "js-sys" { #[js_sys(constructor)] pub fn new() -> RustType; - #[js_sys(static_of = RustType, js_name = "create")] - pub fn create(); - - #[js_sys(js_name = "JavaScriptType")] - pub type RustType; - } - }; + #[js_sys(static_of = RustType, getter = "value")] + pub fn static_value() -> i32; - let output = generated_rust(input); - assert!(output.contains(r#"direct_call: "new globalThis.JavaScriptType()""#)); - assert!(output.contains(r#"direct_call: "globalThis.JavaScriptType.create()""#)); -} + pub fn call(self: &RustType); -#[test] -fn constructor_uses_return_owner() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(constructor)] - pub fn new() -> Result; + #[js_sys(getter = "value")] + pub fn value(self: &RustType) -> i32; - #[js_sys(js_name = "JavaScriptType")] - pub type RustType; - } - }; + #[js_sys(setter)] + pub fn set_value(self: &RustType, value: i32); - let output = generated_items(input); - let output = prettyplease::unparse(&syn::File { - shebang: None, - attrs: Vec::new(), - items: output, - }); - assert!(output.contains(r#"direct_call: "new globalThis.JavaScriptType()""#)); -} + #[js_sys(indexing_getter)] + pub fn get(self: &RustType, index: u32) -> JsValue; -#[test] -fn static_properties_use_the_type_js_name() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(static_of = RustType, getter = "value")] - pub fn value() -> i32; + #[js_sys(indexing_setter)] + pub fn set(self: &RustType, index: u32, value: &JsValue); - #[js_sys(static_of = RustType, setter = "value")] - pub fn set_value(value: i32); + #[js_sys(indexing_deleter)] + pub fn delete(self: &RustType, index: u32); - #[js_sys(js_name = "JavaScriptType")] - pub type RustType; + #[js_sys(variadic)] + pub fn push(self: &RustType, first: &JsValue, rest: &[JsValue]); } - }; + }); - let output = generated_rust(input); - assert!(output.contains(r#"direct_call: "globalThis.JavaScriptType.value""#)); - assert!(output.contains(r#"direct_call: "globalThis.JavaScriptType.value = arg0_0""#)); + let operations: Vec<_> = output + .lines() + .filter_map(|line| line.trim().strip_prefix("direct_call: ")) + .map(|line| line.trim_end_matches(',')) + .collect(); + assert_eq!( + operations, + [ + r#""new globalThis.JavaScriptType()""#, + r#""globalThis.JavaScriptType.value""#, + r#""arg0_0.call()""#, + r#""arg0_0.value""#, + r#""arg0_0.value = arg1_0""#, + r#""arg0_0[arg1_0]""#, + r#""arg0_0[arg1_0] = arg2_0""#, + r#""delete arg0_0[arg1_0]""#, + r#""arg0_0.push(arg1_0, ...arg2_0)""#, + ] + ); } #[test] -fn variadic_requires_an_argument() { - let input = syn::parse_quote! { +fn invalid_member_options() { + let variadic = syn::parse_quote! { extern "js-sys" { #[js_sys(variadic)] pub fn call(); } }; - assert_eq!( - super::macro_error(input), + super::macro_error(variadic), "`variadic` requires at least one argument" ); -} - -#[test] -fn named_getter() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(getter = "value")] - pub fn renamed(self: &JsTest) -> i32; - } - }; - - let output = generated_rust(input); - assert!(output.contains(r#"direct_call: "arg0_0.value""#)); - assert!(output.contains(r#"indirect_call: "arg0_0.value""#)); -} - -#[test] -fn named_setter() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(setter = "value")] - pub fn renamed(self: &JsTest, value: i32); - } - }; - - let output = generated_rust(input); - assert!(output.contains(r#"direct_call: "arg0_0.value = arg1_0""#)); - assert!(output.contains(r#"indirect_call: "arg0_0.value = arg1_0""#)); -} - -#[test] -fn inferred_setter() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(setter)] - pub fn set_value(self: &JsTest, value: i32); - } - }; - let output = generated_rust(input); - assert!(output.contains(r#"direct_call: "arg0_0.value = arg1_0""#)); - assert!(output.contains(r#"indirect_call: "arg0_0.value = arg1_0""#)); -} - -#[test] -fn setter_requires_a_field_name() { - let input = syn::parse_quote! { + let setter = syn::parse_quote! { extern "js-sys" { #[js_sys(setter)] pub fn update(self: &JsTest, value: i32); } }; - let (_, error) = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap_err(); - assert_eq!( - error.to_string(), + super::macro_error(setter), "`setter` cannot infer a field name; use `setter = \"field\"`" ); } - -#[test] -fn getter() { - test!( - {}, - { - extern "js-sys" { - #[js_sys(getter)] - pub fn test(self: &JsTest) -> JsValue; - } - }, - { - impl JsTest { - pub fn test(self: &JsTest) -> JsValue { - unsafe extern "C" { - #[link_name = "test_crate.JsTest.test"] - fn test( - arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, - ) -> ::js_sys::r#macro::OutputRet; - } - - ::js_sys::r#macro::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { - ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) - }; - unsafe { test(arg0_0, arg0_1, arg0_2, arg0_3) } - }) - } - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "JsTest.test", - "test_crate.JsTest.test", - &[::js_sys::r#macro::import_input::<&::js_sys::JsValue>( - "arg0", - )], - ::core::option::Option::Some(::js_sys::r#macro::import_output::()), - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: true, - direct_call: "arg0_0.test", - indirect_call: "arg0_0.test", - required_embeds: &[ - ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), - ::js_sys::r#macro::js_output_embed::(), - ::js_sys::r#macro::js_result_embed::(), - ], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ - \"test_crate.import.JsTest.test\")) (param externref) (result externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32))) - (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) (result i32) - (local $js_sys.externref.value externref) - (local $js_sys.externref.index i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.JsTest.test (@reloc) - local.set $js_sys.externref.value - call $js_sys.externref.next (@reloc) - local.tee $js_sys.externref.index - local.get $js_sys.externref.value - table.set $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - )", - "(arg0_0) => arg0_0.test", - ); -} - -#[test] -fn setter() { - test!( - {}, - { - extern "js-sys" { - #[js_sys(setter = "test")] - pub fn test(self: &JsTest, value: &JsValue); - } - }, - { - impl JsTest { - pub fn test(self: &JsTest, value: &JsValue) { - unsafe extern "C" { - #[link_name = "test_crate.JsTest.test"] - fn test( - arg0_0: ::js_sys::r#macro::InputSlot1<&::js_sys::JsValue>, - arg0_1: ::js_sys::r#macro::InputSlot2<&::js_sys::JsValue>, - arg0_2: ::js_sys::r#macro::InputSlot3<&::js_sys::JsValue>, - arg0_3: ::js_sys::r#macro::InputSlot4<&::js_sys::JsValue>, - arg1_0: ::js_sys::r#macro::InputSlot1<&JsValue>, - arg1_1: ::js_sys::r#macro::InputSlot2<&JsValue>, - arg1_2: ::js_sys::r#macro::InputSlot3<&JsValue>, - arg1_3: ::js_sys::r#macro::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { - ::js_sys::r#macro::split_input_as::<&::js_sys::JsValue>(self) - }; - let (arg1_0, arg1_1, arg1_2, arg1_3) = - ::js_sys::r#macro::split_input::<&JsValue>(value); - unsafe { - test( - arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3, - ) - } - }; - } - } - const _: () = { - const IMPORTS: &[::js_sys::r#macro::ImportDescriptor] = - &[::js_sys::r#macro::ImportDescriptor::new( - "test_crate", - "JsTest.test", - "test_crate.JsTest.test", - &[ - ::js_sys::r#macro::import_input::<&::js_sys::JsValue>("arg0"), - ::js_sys::r#macro::import_input::<&JsValue>("arg1"), - ], - ::core::option::Option::None, - ::core::option::Option::Some(::js_sys::r#macro::ImportJs { - direct_wrapper: true, - direct_call: "arg0_0.test = arg1_0", - indirect_call: "arg0_0.test = arg1_0", - required_embeds: &[ - ::js_sys::r#macro::js_input_embed::<&::js_sys::JsValue>(), - ::js_sys::r#macro::js_input_embed::<&JsValue>(), - ], - }), - )]; - const WAT_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_wat_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_wat::(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = - ::js_sys::r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: ::js_sys::r#macro::ImportSection = - ::js_sys::r#macro::import_js::(IMPORTS); - }; - }, - "(import \"test_crate\" \"JsTest.test\" (func $test_crate.import.JsTest.test (@sym (name \ - \"test_crate.import.JsTest.test\")) (param externref externref))) - (import \"js_sys\" \"externref.table\" (table $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref)) - (func $test_crate.JsTest.test (@sym) (param $arg0_0 i32) (param $arg1_0 i32) - local.get $arg0_0 - table.get $js_sys.import.externref.table (@reloc) - local.get $arg1_0 - table.get $js_sys.import.externref.table (@reloc) - call $test_crate.import.JsTest.test (@reloc) - )", - "(arg0_0, arg1_0) => arg0_0.test = arg1_0", - ); -} diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index 244af0ee..a3681e27 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -15,66 +15,6 @@ use wasmparser::{Parser, Payload}; use crate::r#macro; -macro_rules! test { - ($attr:tt, $input:tt, $expected:tt, $wat:literal, $js_import:expr $(,)?) => { - test!($attr, $input, $expected, wat: $wat, js: $js_import) - }; - ($attr:tt, $input:tt, $expected:tt, None, $js_import:expr $(,)?) => { - test!($attr, $input, $expected, js: $js_import) - }; - ($attr:tt, $input:tt, $expected:tt, $(wat: $wat:literal,)? js: $js_import:expr) => {{ - use inline_snap::inline_snap; - use quote::quote; - use syn::File; - - use crate::r#macro; - - let attr = quote! $attr; - let input = quote! $input; - - let foreign_mod = syn::parse2(input.clone()).unwrap(); - let output = r#macro::expand_for_test(attr.clone(), foreign_mod, "test_crate") - .unwrap() - .into_items() - .unwrap(); - let output = prettyplease::unparse(&File { - shebang: None, - attrs: Vec::new(), - items: output, - }); - - inline_snap!(output.clone(), $expected); - - let dir = tempfile::tempdir().unwrap(); - let (wat_output, js_import_output, _) = - crate::tests::r#macro::inner(dir.path(), &output).unwrap(); - - #[allow(clippy::allow_attributes, unused_assignments, unused_mut, reason = "depends on the input")] - let mut wat: Option<&str> = None; - $(wat = Some($wat);)? - match (wat, wat_output) { - $((Some(_), Some(wat_output)) => { - inline_snap!(wat_output, $wat); - })? - (None, None) => (), - (wat, wat_output) => { - similar_asserts::assert_eq!(wat, wat_output.as_deref()); - } - } - - let js_import = Option::from($js_import); - match (js_import, js_import_output) { - (Some(js_import), Some(js_import_output)) => { - similar_asserts::assert_eq!(js_import, js_import_output); - } - (None, None) => (), - (js_import, js_import_output) => { - similar_asserts::assert_eq!(js_import, js_import_output.as_deref()); - } - } - }}; -} - mod export; mod function; mod member; diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index 0365e5d8..12324634 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -1,123 +1,43 @@ -#[test] -fn basic() { - test!( - {}, - { - extern "js-sys" { - pub type JsString; - } - }, - { - #[repr(transparent)] - pub struct JsString(::js_sys::JsValue); - - impl ::core::convert::AsRef<::js_sys::JsValue> for JsString { - fn as_ref(&self) -> &::js_sys::JsValue { - &self.0 - } - } - - impl ::core::convert::From for ::js_sys::JsValue { - fn from(value: JsString) -> Self { - value.0 - } - } - - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::IntoJS for JsString { - type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - - fn into_abi(self) -> Self::Abi { - ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) - } - } - }, - None, - None, - ); -} +use proc_macro2::TokenStream; -#[test] -fn generic() { - test!( - {}, - { - extern "js-sys" { - pub type JsString; - } - }, - { - #[repr(transparent)] - pub struct JsString { - value: ::js_sys::JsValue, - _type: ::core::marker::PhantomData, - } - - impl ::core::convert::AsRef<::js_sys::JsValue> for JsString { - fn as_ref(&self) -> &::js_sys::JsValue { - &self.value - } - } - - impl ::core::convert::From> for ::js_sys::JsValue { - fn from(value: JsString) -> Self { - value.value - } - } - - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::IntoJS for JsString { - type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - - fn into_abi(self) -> Self::Abi { - ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) - } - } - }, - None, - None, - ); +fn expand(input: syn::ItemForeignMod) -> Vec { + crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap() } #[test] -fn multiple_generic_kinds() { - let input = syn::parse_quote! { +fn generic_options_and_extends() { + let output = expand(syn::parse_quote! { extern "js-sys" { - pub type Generic<'a, T, U, const N: usize>; + #[js_sys(extends = JsTest)] + pub type Child; } - }; - let output = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap() - .into_items() - .unwrap(); + }); let output = prettyplease::unparse(&syn::File { shebang: None, attrs: Vec::new(), items: output, }); - let dir = tempfile::tempdir().unwrap(); - super::inner(dir.path(), &output).unwrap(); + assert!(output.contains("pub struct Child")); + assert!(output.contains("impl ::core::convert::AsRef for Child")); + assert!(output.contains("impl ::core::convert::From> for JsTest")); } #[test] -fn cfg_attr_only_applies_to_the_declared_type() { - let input = syn::parse_quote! { +fn attributes_are_scoped_and_duplicate_names_do_not_panic() { + let output = expand(syn::parse_quote! { extern "js-sys" { #[cfg_attr(all(), derive(Clone))] - pub type JsString; + pub type First; + pub type Duplicate; + pub type Duplicate; } - }; - let output = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap() - .into_items() - .unwrap(); + }); - assert_eq!(output.len(), 5); + assert_eq!(output.len(), 15); for (index, item) in output.into_iter().enumerate() { let attrs = match item { syn::Item::Struct(item) => item.attrs, @@ -129,142 +49,3 @@ fn cfg_attr_only_applies_to_the_declared_type() { assert_eq!(has_cfg_attr, index == 0); } } - -#[test] -fn duplicate_type_names_do_not_panic_in_the_macro() { - let input = syn::parse_quote! { - extern "js-sys" { - pub type Duplicate; - pub type Duplicate; - } - }; - let output = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap() - .into_items() - .unwrap(); - - assert_eq!(output.len(), 10); -} - -#[test] -fn default() { - test!( - {}, - { - extern "js-sys" { - pub type JsString; - } - }, - { - #[repr(transparent)] - pub struct JsString { - value: ::js_sys::JsValue, - _type: ::core::marker::PhantomData, - } - - impl ::core::convert::AsRef<::js_sys::JsValue> for JsString { - fn as_ref(&self) -> &::js_sys::JsValue { - &self.value - } - } - - impl ::core::convert::From> for ::js_sys::JsValue { - fn from(value: JsString) -> Self { - value.value - } - } - - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::IntoJS for JsString { - type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - - fn into_abi(self) -> Self::Abi { - ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) - } - } - }, - None, - None, - ); -} - -#[test] -fn r#trait() { - test!( - {}, - { - extern "js-sys" { - pub type JsString; - } - }, - { - #[repr(transparent)] - pub struct JsString { - value: ::js_sys::JsValue, - _type: ::core::marker::PhantomData, - } - - impl ::core::convert::AsRef<::js_sys::JsValue> for JsString { - fn as_ref(&self) -> &::js_sys::JsValue { - &self.value - } - } - - impl ::core::convert::From> for ::js_sys::JsValue { - fn from(value: JsString) -> Self { - value.value - } - } - - unsafe impl ::js_sys::hazard::JsCast for JsString {} - - unsafe impl ::js_sys::hazard::IntoJS for JsString { - type Abi = <::js_sys::JsValue as ::js_sys::hazard::IntoJS>::Abi; - - fn into_abi(self) -> Self::Abi { - ::js_sys::hazard::IntoJS::into_abi(::js_sys::JsValue::from(self)) - } - } - }, - None, - None, - ); -} - -#[test] -fn extends() { - let input = syn::parse_quote! { - extern "js-sys" { - #[js_sys(extends = JsTest)] - #[js_sys(extends = JsArray)] - pub type Child; - } - }; - let output = - crate::r#macro::expand_for_test(proc_macro2::TokenStream::new(), input, "test_crate") - .unwrap() - .into_items() - .unwrap(); - let mut output = prettyplease::unparse(&syn::File { - shebang: None, - attrs: Vec::new(), - items: output, - }); - output.push_str( - r" -fn assert_extends(value: &Child) { - let _: &JsTest = value; - let _: &JsArray = ::core::convert::AsRef::::as_ref(value); -} - -fn into_parent(value: Child) -> JsArray { - value.into() -} -", - ); - - let dir = tempfile::tempdir().unwrap(); - super::inner(dir.path(), &output).unwrap(); -} diff --git a/host/js-sys-bindgen/src/tests/mod.rs b/host/js-sys-bindgen/src/tests/mod.rs index a2c4180b..122a3cbd 100644 --- a/host/js-sys-bindgen/src/tests/mod.rs +++ b/host/js-sys-bindgen/src/tests/mod.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "web-idl")] macro_rules! test { ($output:tt, $expected:tt $(,)?) => { let output = syn::parse_quote! $output; @@ -9,6 +10,5 @@ macro_rules! test { mod closure; mod r#macro; -mod r#type; #[cfg(feature = "web-idl")] mod web_idl; diff --git a/host/js-sys-bindgen/src/tests/type.rs b/host/js-sys-bindgen/src/tests/type.rs deleted file mode 100644 index 7834dc3f..00000000 --- a/host/js-sys-bindgen/src/tests/type.rs +++ /dev/null @@ -1,182 +0,0 @@ -use syn::parse_quote; - -use crate::hygiene::{Hygiene, ImportManager}; -use crate::r#type::Type; - -#[test] -fn basic() { - let mut imports = ImportManager::new(None); - let items = Type::new( - &mut Hygiene::Imports(&mut imports), - parse_quote!( - type Test; - ), - ); - - test!( - { - #imports - - #items - }, - { - use js_sys::JsValue; - use js_sys::hazard::{IntoJS, JsCast}; - - #[repr(transparent)] - struct Test(JsValue); - - impl AsRef for Test { - fn as_ref(&self) -> &JsValue { - &self.0 - } - } - - impl From for JsValue { - fn from(value: Test) -> Self { - value.0 - } - } - - unsafe impl JsCast for Test {} - - unsafe impl IntoJS for Test { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } - } - - }, - ); -} - -#[test] -fn generic() { - let mut imports = ImportManager::new(None); - let items = Type::new( - &mut Hygiene::Imports(&mut imports), - parse_quote!( - type Test; - ), - ); - - test!( - { - #imports - - #items - }, - { - use core::marker::PhantomData; - use js_sys::JsValue; - use js_sys::hazard::{IntoJS, JsCast}; - - #[repr(transparent)] - struct Test { - value: JsValue, - _type: PhantomData, - } - - impl AsRef for Test { - fn as_ref(&self) -> &JsValue { - &self.value - } - } - - impl From> for JsValue { - fn from(value: Test) -> Self { - value.value - } - } - - unsafe impl JsCast for Test {} - - unsafe impl IntoJS for Test { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } - } - - }, - ); -} - -#[test] -fn extends() { - let mut imports = ImportManager::new(None); - let items = Type::with_extends( - &mut Hygiene::Imports(&mut imports), - parse_quote!( - type Test; - ), - &[parse_quote!(Base)], - ); - - test!( - { - #imports - - #items - }, - { - use core::ops::Deref; - use js_sys::JsValue; - use js_sys::hazard::{IntoJS, JsCast}; - - #[repr(transparent)] - struct Test(JsValue); - - impl AsRef for Test { - fn as_ref(&self) -> &JsValue { - &self.0 - } - } - - impl From for JsValue { - fn from(value: Test) -> Self { - value.0 - } - } - - unsafe impl JsCast for Test {} - - unsafe impl IntoJS for Test { - type Abi = ::Abi; - - fn into_abi(self) -> Self::Abi { - IntoJS::into_abi(JsValue::from(self)) - } - } - - impl Deref for Test { - type Target = Base; - - #[inline] - fn deref(&self) -> &Self::Target { - >::as_ref(self) - } - } - - impl AsRef for Test { - #[inline] - fn as_ref(&self) -> &Base { - ::unchecked_from_ref( - >::as_ref(self), - ) - } - } - - impl From for Base { - #[inline] - fn from(value: Test) -> Self { - ::unchecked_from(JsValue::from(value)) - } - } - - }, - ); -} diff --git a/host/js-sys-bindgen/src/type.rs b/host/js-sys-bindgen/src/type.rs index 15101ef6..4025035f 100644 --- a/host/js-sys-bindgen/src/type.rs +++ b/host/js-sys-bindgen/src/type.rs @@ -57,7 +57,7 @@ impl TypeOptions { } impl Type { - #[cfg(any(feature = "web-idl", test))] + #[cfg(feature = "web-idl")] #[must_use] pub(crate) fn new(hygiene: &mut Hygiene<'_>, item: ForeignItemType) -> Self { Self::with_extends(hygiene, item, &[]) diff --git a/host/shared/src/web_driver/mod.rs b/host/shared/src/web_driver/mod.rs index ebeb12e2..6143e57f 100644 --- a/host/shared/src/web_driver/mod.rs +++ b/host/shared/src/web_driver/mod.rs @@ -188,9 +188,12 @@ impl WebDriverKind { * {} - {}\n\ * {} - {}\n\ * {} - pre-installed on macOS", - Self::Chrome, Self::Chrome.to_download_url().unwrap(), - Self::Gecko, Self::Gecko.to_download_url().unwrap(), - Self::Edge, Self::Edge.to_download_url().unwrap(), + Self::Chrome, + Self::Chrome.to_download_url().unwrap(), + Self::Gecko, + Self::Gecko.to_download_url().unwrap(), + Self::Edge, + Self::Edge.to_download_url().unwrap(), Self::Safari, ) } diff --git a/web/playground/src/main.rs b/web/playground/src/main.rs index 68e4e5de..8327b10b 100644 --- a/web/playground/src/main.rs +++ b/web/playground/src/main.rs @@ -2,7 +2,7 @@ use std::random::random; use std::time::Instant; -use js_sys::Error; +use js_sys::{Error, Uint8Array}; fn main() { let ins = Instant::now(); @@ -16,6 +16,10 @@ fn main() { let g5 = (bits & 0xffffffffffff) as u64; let uuid = format!("{g1:08x}-{g2:04x}-{g3:04x}-{g4:04x}-{g5:012x}"); + for v in Uint8Array::from(&[0, 1, 2, 3]) { + println!("{v}"); + } + let elapsed = ins.elapsed(); println!("JS {}", err.to_string()); println!("result: {uuid}, cost: {elapsed:?}"); From 4f13c0419485ccf7ed1a9de65766b397fc026bcc Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:07:35 +0800 Subject: [PATCH 15/21] Add js-bindgen Wire protocol --- benchmarks/Cargo.lock | 5 + client/Cargo.toml | 1 + client/e2e/examples/vec.rs | 12 + client/js-bindgen/src/lib.rs | 2 +- client/js-sys/Cargo.toml | 1 + client/js-sys/src/hazard.rs | 218 ++---- client/js-sys/src/interop/primitive.rs | 191 ++--- client/js-sys/src/interop/slice.rs | 4 +- client/js-sys/src/interop/string.rs | 20 +- client/js-sys/src/interop/vec.rs | 34 +- client/js-sys/src/lib.rs | 2 +- client/js-sys/src/macro.rs | 29 - client/js-sys/src/macro/abi.rs | 457 ----------- client/js-sys/src/macro/closure.rs | 270 ------- client/js-sys/src/macro/export.rs | 278 ------- client/js-sys/src/macro/export/js.rs | 430 ----------- client/js-sys/src/macro/import.rs | 433 ----------- client/js-sys/src/macro/import/js.rs | 608 --------------- client/js-sys/src/macro/import/wat.rs | 719 ------------------ client/js-sys/src/macro/result.rs | 181 ----- client/js-sys/src/macro/text.rs | 276 ------- client/js-sys/src/macro/wat.rs | 316 -------- client/js-sys/src/macro/writer.rs | 102 --- client/js-sys/src/runtime/exception.rs | 84 +- client/js-sys/src/runtime/externref.rs | 55 +- client/js-sys/src/runtime/value.rs | 70 +- client/js-sys/src/util.rs | 18 +- client/js-sys/src/wire/export.rs | 92 +++ client/js-sys/src/wire/import.rs | 66 ++ client/js-sys/src/wire/macro.rs | 60 ++ client/js-sys/src/wire/mod.rs | 123 +++ client/js-sys/tests/hazard.rs | 12 +- client/wabii/src/lib.rs | 12 +- client/wabii/src/random.64.wat | 4 - client/wabii/src/random.wat | 4 - client/wabii/src/stdio.64.wat | 4 - client/wabii/src/stdio.wat | 4 - client/wabii/src/time.wat | 4 - client/web-sys/src/console.gen.rs | 213 +++--- client/web-sys/src/console.js-sys.rs | 2 + host/Cargo.toml | 2 + host/dev/src/codegen.rs | 18 +- host/js-sys-bindgen/src/closure.rs | 53 +- host/js-sys-bindgen/src/export.rs | 97 ++- host/js-sys-bindgen/src/function.rs | 125 +-- host/js-sys-bindgen/src/hygiene.rs | 2 +- host/js-sys-bindgen/src/macro.rs | 131 +++- host/js-sys-bindgen/src/tests/macro/export.rs | 60 -- .../src/tests/macro/function.rs | 61 +- host/js-sys-bindgen/src/tests/macro/member.rs | 32 +- host/js-sys-bindgen/src/tests/macro/mod.rs | 202 ----- host/ld-shared/src/lib.rs | 129 ++-- host/ld/Cargo.toml | 1 + host/ld/src/args.rs | 10 +- host/ld/src/js.rs | 226 ++++-- host/ld/src/main.rs | 2 +- host/ld/src/post.rs | 6 +- host/ld/src/pre.rs | 164 +++- host/ld/src/wire/export/js.rs | 194 +++++ host/ld/src/wire/export/mod.rs | 24 + host/ld/src/wire/export/wat.rs | 376 +++++++++ host/ld/src/wire/import/js.rs | 268 +++++++ host/ld/src/wire/import/mod.rs | 15 + host/ld/src/wire/import/wat.rs | 264 +++++++ host/ld/src/wire/js.rs | 51 ++ host/ld/src/wire/mod.rs | 51 ++ host/ld/src/wire/wat.rs | 188 +++++ host/macro/src/custom_section.rs | 164 ++-- host/macro/src/lib.rs | 33 +- host/macro/src/tests/global_wat.rs | 147 +--- host/macro/src/tests/import_js.rs | 48 +- host/macro/tests/ui/custom_section.stderr | 45 +- host/wire/Cargo.toml | 18 + host/wire/LICENSE-APACHE | 1 + host/wire/LICENSE-MIT | 1 + host/wire/src/abi.rs | 489 ++++++++++++ host/wire/src/decode/export.rs | 201 +++++ host/wire/src/decode/import.rs | 353 +++++++++ host/wire/src/decode/mod.rs | 345 +++++++++ host/wire/src/decode/value.rs | 305 ++++++++ host/wire/src/encode/export.rs | 162 ++++ host/wire/src/encode/import.rs | 315 ++++++++ host/wire/src/encode/mod.rs | 541 +++++++++++++ host/wire/src/lib.rs | 65 ++ host/wire/src/model.rs | 322 ++++++++ host/wire/src/schema.rs | 489 ++++++++++++ host/wire/src/tests.rs | 396 ++++++++++ web/playground/src/main.rs | 33 +- 88 files changed, 7002 insertions(+), 5639 deletions(-) delete mode 100644 client/js-sys/src/macro.rs delete mode 100644 client/js-sys/src/macro/abi.rs delete mode 100644 client/js-sys/src/macro/closure.rs delete mode 100644 client/js-sys/src/macro/export.rs delete mode 100644 client/js-sys/src/macro/export/js.rs delete mode 100644 client/js-sys/src/macro/import.rs delete mode 100644 client/js-sys/src/macro/import/js.rs delete mode 100644 client/js-sys/src/macro/import/wat.rs delete mode 100644 client/js-sys/src/macro/result.rs delete mode 100644 client/js-sys/src/macro/text.rs delete mode 100644 client/js-sys/src/macro/wat.rs delete mode 100644 client/js-sys/src/macro/writer.rs create mode 100644 client/js-sys/src/wire/export.rs create mode 100644 client/js-sys/src/wire/import.rs create mode 100644 client/js-sys/src/wire/macro.rs create mode 100644 client/js-sys/src/wire/mod.rs create mode 100644 host/ld/src/wire/export/js.rs create mode 100644 host/ld/src/wire/export/mod.rs create mode 100644 host/ld/src/wire/export/wat.rs create mode 100644 host/ld/src/wire/import/js.rs create mode 100644 host/ld/src/wire/import/mod.rs create mode 100644 host/ld/src/wire/import/wat.rs create mode 100644 host/ld/src/wire/js.rs create mode 100644 host/ld/src/wire/mod.rs create mode 100644 host/ld/src/wire/wat.rs create mode 100644 host/wire/Cargo.toml create mode 120000 host/wire/LICENSE-APACHE create mode 120000 host/wire/LICENSE-MIT create mode 100644 host/wire/src/abi.rs create mode 100644 host/wire/src/decode/export.rs create mode 100644 host/wire/src/decode/import.rs create mode 100644 host/wire/src/decode/mod.rs create mode 100644 host/wire/src/decode/value.rs create mode 100644 host/wire/src/encode/export.rs create mode 100644 host/wire/src/encode/import.rs create mode 100644 host/wire/src/encode/mod.rs create mode 100644 host/wire/src/lib.rs create mode 100644 host/wire/src/model.rs create mode 100644 host/wire/src/schema.rs create mode 100644 host/wire/src/tests.rs diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock index 73065ca8..62977882 100644 --- a/benchmarks/Cargo.lock +++ b/benchmarks/Cargo.lock @@ -56,11 +56,16 @@ dependencies = [ name = "js-bindgen-macro" version = "0.1.0" +[[package]] +name = "js-bindgen-wire" +version = "0.1.0" + [[package]] name = "js-sys" version = "0.0.0" dependencies = [ "js-bindgen", + "js-bindgen-wire", "js-sys-macro", ] diff --git a/client/Cargo.toml b/client/Cargo.toml index 4c207c6c..f85faff4 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -28,6 +28,7 @@ js-bindgen = { path = "js-bindgen", default-features = false } js-bindgen-macro = { path = "../host/macro" } js-bindgen-test = { path = "test" } js-bindgen-test-macro = { path = "../host/test-macro" } +js-bindgen-wire = { path = "../host/wire" } js-sys = { path = "js-sys" } js-sys-macro = { path = "../host/js-sys-macro" } mini-alloc = "1" diff --git a/client/e2e/examples/vec.rs b/client/e2e/examples/vec.rs index 62ef2e11..43163db6 100644 --- a/client/e2e/examples/vec.rs +++ b/client/e2e/examples/vec.rs @@ -15,6 +15,8 @@ fn main() { // ;; (() => { const bits = exports["pointer_width"](); const input = bits === 64 ? new BigUint64Array([0n, 0xffffffffffffffffn]) : new Uint32Array([0, 0xffffffff]); const result = exports["usize_roundtrip"](input); const Constructor = bits === 64 ? BigUint64Array : Uint32Array; return result !== input && result instanceof Constructor && result.length === 2 && result[0] === input[0] && result[1] === input[1] })() // ;; (() => { const input = ['first', '', '第三个 🦀']; const result = exports["string_roundtrip"](input); return result !== input && Array.isArray(result) && result.join('|') === 'first||第三个 🦀' })() // ;; (() => { try { exports["string_roundtrip"](['valid', 42]); return false } catch (error) { return error instanceof TypeError } })() + // ;; exports["sum_u32_slice"](new Uint32Array([1, 2, 3, 0xffffffff])) === 5 + // ;; exports["join_string_slice"](['first', '', '第三个 🦀']) === 'first||第三个 🦀' } use js_sys::{JsValue, js_sys}; @@ -73,3 +75,13 @@ fn pointer_width() -> u32 { fn string_roundtrip(value: Vec) -> Vec { value } + +#[js_sys] +fn sum_u32_slice(value: &[u32]) -> u32 { + value.iter().fold(0, |sum, value| sum.wrapping_add(*value)) +} + +#[js_sys] +fn join_string_slice(value: &[String]) -> String { + value.join("|") +} diff --git a/client/js-bindgen/src/lib.rs b/client/js-bindgen/src/lib.rs index 26844d77..8476fafa 100644 --- a/client/js-bindgen/src/lib.rs +++ b/client/js-bindgen/src/lib.rs @@ -14,4 +14,4 @@ unsafe extern "C" {} #[doc(hidden)] pub mod r#macro; -pub use js_bindgen_macro::{embed_js, export_js, import_js, unsafe_global_wat}; +pub use js_bindgen_macro::{embed_js, import_js, unsafe_global_wat}; diff --git a/client/js-sys/Cargo.toml b/client/js-sys/Cargo.toml index 508d7690..d086958f 100644 --- a/client/js-sys/Cargo.toml +++ b/client/js-sys/Cargo.toml @@ -12,6 +12,7 @@ test = false [dependencies] js-bindgen = { workspace = true } +js-bindgen-wire = { workspace = true } js-sys-macro = { workspace = true } [dev-dependencies] diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 6cc5c773..12eef9a2 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -1,137 +1,14 @@ use core::mem::{ManuallyDrop, MaybeUninit}; use core::ptr; +pub use js_bindgen_wire::abi::{ + FromJsConv, IntoJsConv, JsCatch, JsEmbed, RefType, ResultLayout, ReturnConv, ReturnMode, Sret, + WatCatch, WatConv, WatImport, WatImportKind, WatIndexType, WatLocal, WatSlot, WatType, +}; + use crate::JsValue; use crate::runtime::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; -// Conversion `metadata`. -#[derive(Clone, Copy)] -pub struct WatConv { - pub imports: Option<&'static str>, - pub locals: Option<&'static str>, - pub conv: &'static str, - pub r#type: &'static str, -} - -/// Converts primitive `ABI` slots into one JavaScript value. -#[derive(Clone, Copy)] -pub struct IntoJsConv { - pub(crate) embed: Option<(&'static str, &'static str)>, - pub(crate) template: &'static str, -} - -impl IntoJsConv { - /// Produces one JavaScript value from `$slot1` through `$slot4`. - #[must_use] - pub const fn new(template: &'static str) -> Self { - Self { - embed: None, - template, - } - } - - #[must_use] - pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { - self.embed = Some(embed); - self - } -} - -/// Converts one JavaScript value into primitive `ABI` slots. -#[derive(Clone, Copy)] -pub struct FromJsConv { - pub(crate) embed: Option<(&'static str, &'static str)>, - pub(crate) prepare: Option<&'static str>, - pub(crate) templates: [&'static str; 4], - pub(crate) sret: Option, -} - -/// Selects how an indirect JavaScript import result is written to Rust. -#[derive(Clone, Copy)] -pub enum Sret { - /// Converts the JavaScript value through the common slot templates first. - Slots(&'static str), - /// Passes the original JavaScript value directly to the writer. - Value(&'static str), -} - -impl FromJsConv { - /// Produces `ABI` slots from `$value`. - #[must_use] - pub const fn slot1(template: &'static str) -> Self { - Self { - embed: None, - prepare: None, - templates: [template, "", "", ""], - sret: None, - } - } - - /// Computes a value once before expanding the individual slot templates. - /// - /// The template receives the JavaScript argument as `$value`; slot - /// templates can refer to its result as `$prepared`. - #[must_use] - pub const fn prepare(mut self, template: &'static str) -> Self { - self.prepare = Some(template); - self - } - - #[must_use] - pub const fn slot2(mut self, template: &'static str) -> Self { - self.templates[1] = template; - self - } - - #[must_use] - pub const fn slot3(mut self, template: &'static str) -> Self { - self.templates[2] = template; - self - } - - #[must_use] - pub const fn slot4(mut self, template: &'static str) -> Self { - self.templates[3] = template; - self - } - - /// Configures how an indirect JavaScript import result is written to Rust. - #[must_use] - pub const fn sret(mut self, sret: Sret) -> Self { - self.sret = Some(sret); - self - } - - #[must_use] - pub const fn with_embed(mut self, embed: (&'static str, &'static str)) -> Self { - self.embed = Some(embed); - self - } -} - -/// Describes how a function return is handled at the JavaScript boundary. -#[derive(Clone, Copy)] -pub enum ReturnConv { - /// The value is returned normally. - Value(Option), - /// `Ok` is returned normally and `Err` follows the exception path. - Result(Option), -} - -impl ReturnConv { - #[must_use] - pub const fn conversion(self) -> Option { - match self { - Self::Value(value) | Self::Result(value) => value, - } - } - - #[must_use] - pub const fn is_result(self) -> bool { - matches!(self, Self::Result(_)) - } -} - // Wasm `ABI` carriers. /// One carrier position in the Wasm function `ABI`. @@ -139,10 +16,10 @@ impl ReturnConv { /// # Safety /// /// `WAT_TYPE` must describe the carrier's Rust Wasm `ABI`. Each conversion must -/// consume or produce that type as appropriate. `WAT_TYPE` must not be empty -/// except for [`EmptySlot`]. +/// consume or produce that type as appropriate. Only [`EmptySlot`] may use +/// `None`. pub unsafe trait Slot { - const WAT_TYPE: &'static str; + const WAT_TYPE: Option; const INTO_JS_WAT_CONV: Option = None; const FROM_JS_WAT_CONV: Option = None; } @@ -168,19 +45,6 @@ pub unsafe trait WasmAbi: Sized { -> Self; } -#[derive(Clone, Copy)] -pub enum ReturnMode { - Direct, - Indirect, -} - -impl ReturnMode { - #[must_use] - pub const fn is_direct(self) -> bool { - matches!(self, Self::Direct) - } -} - /// A [`WasmAbi`] that can be returned through the Rust `extern "C"` `ABI`. /// /// # Safety @@ -190,6 +54,7 @@ impl ReturnMode { /// pointer type for its hidden return parameter. pub unsafe trait ReturnAbi: WasmAbi { const MODE: ReturnMode; + const RESULT_LAYOUT: Option = None; } /// The FFI-safe return representation of a [`WasmAbi`] value. @@ -250,7 +115,7 @@ impl EmptySlot { // SAFETY: `EmptySlot` is an absent slot and therefore has no WAT type. unsafe impl Slot for EmptySlot { - const WAT_TYPE: &'static str = ""; + const WAT_TYPE: Option = None; } // SAFETY: Every non-empty `Slot` is a complete single-slot `ABI` carrier. @@ -393,12 +258,14 @@ where /// # Safety /// -/// `Abi`, `from_abi`, and `JS_CONV` must describe one consistent conversion -/// from a JavaScript value to a Rust value. `JS_CONV` produces the primitive -/// slots and `from_abi` reconstructs the Rust value. Multi-slot `ABI` -/// representations must define one slot template for every non-empty slot. +/// `Abi`, `from_abi`, `JS_CONV`, and `JS_SRET` must describe one consistent +/// conversion from a JavaScript value to a Rust value. `JS_CONV` produces the +/// primitive slots and `from_abi` reconstructs the Rust value. Multi-slot +/// `ABI` representations must define one slot template for every non-empty +/// slot. Indirect import returns must also define `JS_SRET`. pub unsafe trait FromJS { const JS_CONV: Option = None; + const JS_SRET: Option = None; type Abi: WasmAbi; @@ -412,11 +279,12 @@ pub unsafe trait FromJS { /// /// # Safety /// -/// `Abi`, `from_option_abi`, and `JS_CONV` must describe one consistent -/// conversion from a JavaScript value to `Option`. +/// `Abi`, `from_option_abi`, `JS_CONV`, and `JS_SRET` must describe one +/// consistent conversion from a JavaScript value to `Option`. #[doc(hidden)] pub unsafe trait OptionFromAbi: WasmAbi { const JS_CONV: Option = T::JS_CONV; + const JS_SRET: Option = T::JS_SRET; type Abi: WasmAbi; @@ -429,6 +297,7 @@ where T::Abi: OptionFromAbi, { const JS_CONV: Option = >::JS_CONV; + const JS_SRET: Option = >::JS_SRET; type Abi = >::Abi; @@ -440,11 +309,12 @@ where /// Converts the return value of a JavaScript import into its Rust result. /// /// `Abi` describes the successful return value and must support the Rust -/// return `ABI`. Indirect returns must define an `sret` conversion. The raw +/// return `ABI`. Indirect returns must define `JS_SRET`. The raw /// carrier may be uninitialized when JavaScript throws, so implementations /// that catch exceptions must inspect the exception state before decoding it. pub trait ReturnFromJS { const JS_CONV: ReturnConv; + const JS_SRET: Option; type Abi: ReturnAbi; @@ -457,6 +327,7 @@ where T::Abi: ReturnAbi, { const JS_CONV: ReturnConv = ReturnConv::Value(T::JS_CONV); + const JS_SRET: Option = T::JS_SRET; type Abi = T::Abi; @@ -478,7 +349,8 @@ pub struct ResultIntoJsAbi { value: Result::Abi>, } -const RESULT_DISCRIMINANT_LOCAL: &str = " (local $js_sys.result.discriminant i32)"; +const RESULT_DISCRIMINANT_LOCAL: WatLocal = + WatLocal::new("js_sys.result.discriminant", WatType::I32); const RESULT_ERROR_WAT_CONV: &str = "\ local.set $js_sys.externref.index local.get $js_sys.result.discriminant @@ -504,13 +376,13 @@ pub struct ResultDiscriminantAbi(u32); // SAFETY: The transparent `i32` discriminant is also recorded in a local for // the following error slot conversion. unsafe impl Slot for ResultDiscriminantAbi { - const WAT_TYPE: &'static str = "i32"; - const INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: None, - locals: Some(RESULT_DISCRIMINANT_LOCAL), - conv: "local.tee $js_sys.result.discriminant", - r#type: "i32", - }); + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + &[], + &[RESULT_DISCRIMINANT_LOCAL], + "local.tee $js_sys.result.discriminant", + WatType::I32, + )); } /// An owned `externref` table index transferred by a [`Result`] error. @@ -525,13 +397,13 @@ pub struct ResultErrorAbi(::Abi); // SAFETY: `JsValue` uses a transparent `i32` table index as its Rust `ABI`. The // preceding result discriminant is recorded before this conversion runs. unsafe impl Slot for ResultErrorAbi { - const WAT_TYPE: &'static str = "i32"; - const INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: Some(WAT_TAKE_IMPORTS), - locals: Some(WAT_INDEX_LOCAL), - conv: RESULT_ERROR_WAT_CONV, - r#type: "externref", - }); + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_TAKE_IMPORTS, + &[WAT_INDEX_LOCAL], + RESULT_ERROR_WAT_CONV, + WatType::ExternRef, + )); } // SAFETY: The first two slots match the successful value's `ABI`. The third @@ -592,6 +464,17 @@ where T::Slot2: Default, { const MODE: ReturnMode = ReturnMode::Indirect; + const RESULT_LAYOUT: Option = { + let discriminant = if T::Slot1::WAT_TYPE.is_none() { + 0 + } else if T::Slot2::WAT_TYPE.is_none() { + 1 + } else { + 2 + }; + + Some(ResultLayout::new(discriminant, discriminant + 1)) + }; } impl ReturnIntoJS for Result @@ -622,6 +505,7 @@ where T::Abi: ReturnAbi, { const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); + const JS_SRET: Option = T::JS_SRET; type Abi = T::Abi; diff --git a/client/js-sys/src/interop/primitive.rs b/client/js-sys/src/interop/primitive.rs index f8f2fdea..1d67ba96 100644 --- a/client/js-sys/src/interop/primitive.rs +++ b/client/js-sys/src/interop/primitive.rs @@ -1,14 +1,14 @@ use crate::hazard::{ EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionFromAbi, OptionIntoAbi, ReturnAbi, - ReturnMode, Slot, Sret, WasmAbi, + ReturnMode, Slot, Sret, WasmAbi, WatType, }; -use crate::r#macro::const_concat; +use crate::wire::const_concat; macro_rules! slot { - ($wat:literal, $($ty:ty),+ $(,)?) => {$( + ($wat:expr, $($ty:ty),+ $(,)?) => {$( // SAFETY: The declared WAT type describes this primitive `ABI` slot. unsafe impl Slot for $ty { - const WAT_TYPE: &'static str = $wat; + const WAT_TYPE: Option = Some($wat); } // SAFETY: Primitive scalar values are returned directly. @@ -119,7 +119,7 @@ macro_rules! sentinel_option { macro_rules! indirect_option { ($($ty:ident => { - decode: ($decode:literal, $decode_arguments:literal), + decode: $decode:expr, encode: $encode:literal, slots: $slots:expr, }),+ $(,)?) => {$( @@ -132,15 +132,7 @@ macro_rules! indirect_option { // SAFETY: The decoder combines the presence tag and payload slots into one // optional JavaScript value. unsafe impl OptionIntoAbi<$ty> for $ty { - const JS_CONV: Option = Some( - IntoJsConv::new(const_concat!( - "this.#jsEmbed.js_sys['", - $decode, - "']", - $decode_arguments, - )) - .with_embed(("js_sys", $decode)), - ); + const JS_CONV: Option = Some($decode); type Abi = Option<$ty>; @@ -153,21 +145,28 @@ macro_rules! indirect_option { // payload slots expected by `Option<$ty>`. unsafe impl OptionFromAbi<$ty> for $ty { const JS_CONV: Option = { - const SLOTS: [&str; 4] = $slots; - - Some( - FromJsConv::slot1(SLOTS[0]) - .slot2(SLOTS[1]) - .slot3(SLOTS[2]) - .slot4(SLOTS[3]) - .sret(Sret::Slots(const_concat!( - "this.#jsEmbed.js_sys['", - $encode, - "']" - ))) - .with_embed(("js_sys", $encode)), - ) + const SLOTS: [Option<&str>; 4] = $slots; + + let Some(slot1) = SLOTS[0] else { + panic!("an indirect option requires a presence slot"); + }; + let mut conversion = FromJsConv::slot1(slot1); + if let Some(slot2) = SLOTS[1] { + conversion = conversion.slot2(slot2); + } + if let Some(slot3) = SLOTS[2] { + conversion = conversion.slot3(slot3); + } + if let Some(slot4) = SLOTS[3] { + conversion = conversion.slot4(slot4); + } + Some(conversion.with_embed("js_sys", $encode)) }; + const JS_SRET: Option = Some(Sret::Slots(const_concat!( + "this.#jsEmbed.js_sys['", + $encode, + "']" + ))); type Abi = Option<$ty>; @@ -178,14 +177,14 @@ macro_rules! indirect_option { )+}; } -slot!("i32", bool, u8, u16, u32, i8, i16, i32); -slot!("i64", u64, i64); -slot!("f32", f32); -slot!("f64", f64); +slot!(WatType::I32, bool, u8, u16, u32, i8, i16, i32); +slot!(WatType::I64, u64, i64); +slot!(WatType::F32, f32); +slot!(WatType::F64, f64); #[cfg(target_arch = "wasm32")] -slot!("i32", isize, usize); +slot!(WatType::I32, isize, usize); #[cfg(target_arch = "wasm64")] -slot!("i64", isize, usize); +slot!(WatType::I64, isize, usize); // SAFETY: Unit has no Rust-to-JavaScript payload and becomes `undefined`. unsafe impl IntoJS for () { @@ -331,7 +330,7 @@ unsafe impl ReturnAbi for u128 { unsafe impl IntoJS for u128 { const JS_CONV: Option = Some( IntoJsConv::new("this.#jsEmbed.js_sys['numeric.u128.decode']($slot1, $slot2)") - .with_embed(("js_sys", "numeric.u128.decode")), + .with_embed("js_sys", "numeric.u128.decode"), ); type Abi = Self; @@ -347,9 +346,11 @@ unsafe impl FromJS for u128 { const JS_CONV: Option = Some( FromJsConv::slot1("$value") .slot2("$value >> 64n") - .sret(Sret::Slots("this.#jsEmbed.js_sys['numeric.128.encode']")) - .with_embed(("js_sys", "numeric.128.encode")), + .with_embed("js_sys", "numeric.128.encode"), ); + const JS_SRET: Option = Some(Sret::Slots( + "this.#jsEmbed.js_sys['numeric.128.encode']", + )); type Abi = Self; @@ -395,7 +396,7 @@ unsafe impl ReturnAbi for i128 { unsafe impl IntoJS for i128 { const JS_CONV: Option = Some( IntoJsConv::new("this.#jsEmbed.js_sys['numeric.i128.decode']($slot1, $slot2)") - .with_embed(("js_sys", "numeric.i128.decode")), + .with_embed("js_sys", "numeric.i128.decode"), ); type Abi = Self; @@ -411,9 +412,11 @@ unsafe impl FromJS for i128 { const JS_CONV: Option = Some( FromJsConv::slot1("$value") .slot2("$value >> 64n") - .sret(Sret::Slots("this.#jsEmbed.js_sys['numeric.128.encode']")) - .with_embed(("js_sys", "numeric.128.encode")), + .with_embed("js_sys", "numeric.128.encode"), ); + const JS_SRET: Option = Some(Sret::Slots( + "this.#jsEmbed.js_sys['numeric.128.encode']", + )); type Abi = Self; @@ -506,7 +509,7 @@ sentinel_option! { sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, - js_sentinel: "Number.MAX_SAFE_INTEGER", + js_sentinel: "9007199254740991", into_carrier: widen, to_js: "$slot1", from_js: "$value >> 0", @@ -516,7 +519,7 @@ sentinel_option! { sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, - js_sentinel: "Number.MAX_SAFE_INTEGER", + js_sentinel: "9007199254740991", into_carrier: widen, to_js: "$slot1", from_js: "$value >>> 0", @@ -526,7 +529,7 @@ sentinel_option! { sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, - js_sentinel: "Number.MAX_SAFE_INTEGER", + js_sentinel: "9007199254740991", into_carrier: widen, to_js: "$slot1", from_js: "Math.fround($value)", @@ -537,7 +540,7 @@ sentinel_option! { sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, - js_sentinel: "Number.MAX_SAFE_INTEGER", + js_sentinel: "9007199254740991", into_carrier: pointer, to_js: "$slot1", from_js: "$value >> 0", @@ -548,7 +551,7 @@ sentinel_option! { sentinel_option! { carrier: f64, sentinel: F64_OPTION_SENTINEL, - js_sentinel: "Number.MAX_SAFE_INTEGER", + js_sentinel: "9007199254740991", into_carrier: pointer, to_js: "$slot1", from_js: "$value >>> 0", @@ -557,68 +560,90 @@ sentinel_option! { indirect_option! { f64 => { - decode: ("optional.f64.decode", "($slot1, $slot2)"), + decode: IntoJsConv::new("$slot1 === 0 ? undefined : $slot2"), encode: "optional.f64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0 : $value", "", ""], + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0 : $value"), + None, + None, + ], }, i64 => { - decode: ("optional.i64.decode", "($slot1, $slot2)"), + decode: IntoJsConv::new("$slot1 === 0 ? undefined : $slot2"), encode: "optional.i64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], }, u64 => { - decode: ("optional.u64.decode", "($slot1, $slot2)"), + decode: IntoJsConv::new("$slot1 === 0 ? undefined : BigInt.asUintN(64, $slot2)"), encode: "optional.u64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], }, } #[cfg(target_arch = "wasm64")] indirect_option! { isize => { - decode: ("optional.i64.decode", "($slot1, $slot2)"), + decode: IntoJsConv::new("$slot1 === 0 ? undefined : $slot2"), encode: "optional.i64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], }, usize => { - decode: ("optional.u64.decode", "($slot1, $slot2)"), + decode: IntoJsConv::new("$slot1 === 0 ? undefined : BigInt.asUintN(64, $slot2)"), encode: "optional.u64.encode", - slots: ["$value == null ? 0 : 1", "$value == null ? 0n : $value", "", ""], + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], }, } indirect_option! { u128 => { - decode: ("optional.u128.decode", "($slot1, $slot2, $slot3)"), + decode: IntoJsConv::new( + "this.#jsEmbed.js_sys['optional.u128.decode']($slot1, $slot2, $slot3)", + ) + .with_embed("js_sys", "optional.u128.decode"), encode: "optional.128.encode", slots: [ - "$value == null ? 0 : 1", - "$value == null ? 0n : $value", - "$value == null ? 0n : $value >> 64n", - "", + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + Some("$value == null ? 0n : $value >> 64n"), + None, ], }, i128 => { - decode: ("optional.i128.decode", "($slot1, $slot2, $slot3)"), + decode: IntoJsConv::new( + "this.#jsEmbed.js_sys['optional.i128.decode']($slot1, $slot2, $slot3)", + ) + .with_embed("js_sys", "optional.i128.decode"), encode: "optional.128.encode", slots: [ - "$value == null ? 0 : 1", - "$value == null ? 0n : $value", - "$value == null ? 0n : $value >> 64n", - "", + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + Some("$value == null ? 0n : $value >> 64n"), + None, ], }, } -js_bindgen::embed_js!( - module = "js_sys", - name = "optional.f64.decode", - "(isSome, value) => {{", - " if (isSome === 0) return undefined", - " return value", - "}}", -); - js_bindgen::embed_js!( module = "js_sys", name = "optional.f64.encode", @@ -637,15 +662,6 @@ js_bindgen::embed_js!( "}})()", ); -js_bindgen::embed_js!( - module = "js_sys", - name = "optional.i64.decode", - "(isSome, value) => {{", - " if (isSome === 0) return undefined", - " return value", - "}}", -); - js_bindgen::embed_js!( module = "js_sys", name = "optional.i64.encode", @@ -664,15 +680,6 @@ js_bindgen::embed_js!( "}})()", ); -js_bindgen::embed_js!( - module = "js_sys", - name = "optional.u64.decode", - "(isSome, value) => {{", - " if (isSome === 0) return undefined", - " return BigInt.asUintN(64, value)", - "}}", -); - js_bindgen::embed_js!( module = "js_sys", name = "optional.u64.encode", diff --git a/client/js-sys/src/interop/slice.rs b/client/js-sys/src/interop/slice.rs index 03538844..b6b5afea 100644 --- a/client/js-sys/src/interop/slice.rs +++ b/client/js-sys/src/interop/slice.rs @@ -73,7 +73,7 @@ macro_rules! primitive_slices { JS_PTR_LEN_ARGS, "))" )) - .with_embed(("js_sys", concat!("view.get", $view))), + .with_embed("js_sys", concat!("view.get", $view)), ); type Abi = ExternSlice<$ty>; @@ -234,7 +234,7 @@ unsafe impl IntoJS for &[T] { JS_PTR_LEN_ARGS, ")" )) - .with_embed(("js_sys", "array.js_value.decode")), + .with_embed("js_sys", "array.js_value.decode"), ); type Abi = ExternSlice; diff --git a/client/js-sys/src/interop/string.rs b/client/js-sys/src/interop/string.rs index 90f468c7..3b48ed0b 100644 --- a/client/js-sys/src/interop/string.rs +++ b/client/js-sys/src/interop/string.rs @@ -361,7 +361,7 @@ unsafe impl IntoJS for &str { JS_PTR_LEN_ARGS, ")" )) - .with_embed(("js_sys", "string.decode")), + .with_embed("js_sys", "string.decode"), ); type Abi = ExternSlice; @@ -380,7 +380,7 @@ unsafe impl IntoJS for String { JS_PTR_LEN_ARGS, ")" )) - .with_embed(("js_sys", "string.take")), + .with_embed("js_sys", "string.take"), ); type Abi = StringAbi; @@ -406,7 +406,7 @@ unsafe impl OptionIntoAbi for StringAbi { JS_OPTION_PTR_LEN_ARGS, ")" )) - .with_embed(("js_sys", "string.take")), + .with_embed("js_sys", "string.take"), ); type Abi = Option; @@ -423,9 +423,11 @@ unsafe impl FromJS for String { FromJsConv::slot1("$prepared[0]") .slot2("$prepared[1]") .prepare("this.#jsEmbed.js_sys['string.from_js'].slots($value)") - .sret(Sret::Value("this.#jsEmbed.js_sys['string.from_js'].sret")) - .with_embed(("js_sys", "string.from_js")), + .with_embed("js_sys", "string.from_js"), ); + const JS_SRET: Option = Some(Sret::Value( + "this.#jsEmbed.js_sys['string.from_js'].sret", + )); type Abi = StringAbi; @@ -449,11 +451,11 @@ unsafe impl OptionFromAbi for StringAbi { .slot2("$prepared[1]") .slot3("$prepared[2]") .prepare("this.#jsEmbed.js_sys['string.option.from_js'].slots($value)") - .sret(Sret::Value( - "this.#jsEmbed.js_sys['string.option.from_js'].sret", - )) - .with_embed(("js_sys", "string.option.from_js")), + .with_embed("js_sys", "string.option.from_js"), ); + const JS_SRET: Option = Some(Sret::Value( + "this.#jsEmbed.js_sys['string.option.from_js'].sret", + )); type Abi = Option; diff --git a/client/js-sys/src/interop/vec.rs b/client/js-sys/src/interop/vec.rs index c2f20c79..e708a02b 100644 --- a/client/js-sys/src/interop/vec.rs +++ b/client/js-sys/src/interop/vec.rs @@ -3,7 +3,8 @@ use alloc::string::String; use alloc::vec::Vec; use crate::hazard::{ - EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, ReturnAbi, ReturnMode, Sret, WasmAbi, + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, RefFromJS, ReturnAbi, ReturnMode, + Sret, WasmAbi, }; use crate::util::{JS_PTR_LEN_ARGS, PtrConst, PtrLength}; use crate::{JsString, JsValue}; @@ -190,7 +191,7 @@ const JS_VALUE_VEC_TO_JS: IntoJsConv = IntoJsConv::new(crate::const_concat!( JS_PTR_LEN_ARGS, ")" )) -.with_embed(("js_sys", "vec.js_value.take")); +.with_embed("js_sys", "vec.js_value.take"); /// Element-level policy for moving an owned vector from Rust to JavaScript. /// @@ -216,6 +217,7 @@ pub unsafe trait VectorIntoJS: Sized { #[doc(hidden)] pub unsafe trait VectorFromJS: Sized { const JS_CONV: FromJsConv; + const JS_SRET: Sret; type Abi: WasmAbi; @@ -236,6 +238,7 @@ unsafe impl IntoJS for Vec { // SAFETY: Delegated to the element's vector conversion policy. unsafe impl FromJS for Vec { const JS_CONV: Option = Some(T::JS_CONV); + const JS_SRET: Option = Some(T::JS_SRET); type Abi = T::Abi; @@ -245,6 +248,13 @@ unsafe impl FromJS for Vec { } } +impl RefFromJS for [T] +where + Vec: FromJS, +{ + type Anchor = Vec; +} + // SAFETY: Every element is moved into its owned `JsValue` representation, and // the JavaScript helper consumes the resulting table-index allocation. unsafe impl VectorIntoJS for T @@ -272,10 +282,8 @@ unsafe impl VectorFromJS for T { const JS_CONV: FromJsConv = FromJsConv::slot1("$prepared[0]") .slot2("$prepared[1]") .prepare("this.#jsEmbed.js_sys['vec.js_value.from_js'].slots($value)") - .sret(Sret::Value( - "this.#jsEmbed.js_sys['vec.js_value.from_js'].sret", - )) - .with_embed(("js_sys", "vec.js_value.from_js")); + .with_embed("js_sys", "vec.js_value.from_js"); + const JS_SRET: Sret = Sret::Value("this.#jsEmbed.js_sys['vec.js_value.from_js'].sret"); type Abi = VecAbi; @@ -314,10 +322,8 @@ unsafe impl VectorFromJS for String { const JS_CONV: FromJsConv = FromJsConv::slot1("$prepared[0]") .slot2("$prepared[1]") .prepare("this.#jsEmbed.js_sys['vec.string.from_js'].slots($value)") - .sret(Sret::Value( - "this.#jsEmbed.js_sys['vec.string.from_js'].sret", - )) - .with_embed(("js_sys", "vec.string.from_js")); + .with_embed("js_sys", "vec.string.from_js"); + const JS_SRET: Sret = Sret::Value("this.#jsEmbed.js_sys['vec.string.from_js'].sret"); type Abi = VecAbi; @@ -457,7 +463,7 @@ macro_rules! typed_vector { JS_PTR_LEN_ARGS, ")" )) - .with_embed(("js_sys", concat!("vec.", $name, ".take"))); + .with_embed("js_sys", concat!("vec.", $name, ".take")); type Abi = VecAbi; @@ -476,12 +482,12 @@ macro_rules! typed_vector { $name, ".from_js'].slots($value)" )) - .sret(Sret::Value(concat!( + .with_embed("js_sys", concat!("vec.", $name, ".from_js")); + const JS_SRET: Sret = Sret::Value(concat!( "this.#jsEmbed.js_sys['vec.", $name, ".from_js'].sret" - ))) - .with_embed(("js_sys", concat!("vec.", $name, ".from_js"))); + )); type Abi = VecAbi; diff --git a/client/js-sys/src/lib.rs b/client/js-sys/src/lib.rs index 01effbc2..bf50dc8d 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -20,7 +20,7 @@ pub mod hazard; // JavaScript values. mod interop; #[doc(hidden)] -pub mod r#macro; +pub mod wire; pub use builtins::{ AggregateError, Array, ArrayBuffer, ArrayBufferOptions, AsyncDisposableStack, AsyncFunction, diff --git a/client/js-sys/src/macro.rs b/client/js-sys/src/macro.rs deleted file mode 100644 index f59c9a1c..00000000 --- a/client/js-sys/src/macro.rs +++ /dev/null @@ -1,29 +0,0 @@ -mod abi; -mod closure; -mod export; -mod import; -mod result; -mod text; -mod wat; -mod writer; - -pub use abi::*; -pub use export::*; -pub use import::*; -pub use result::*; -pub use text::*; -pub use wat::*; - -// Text rendering. -pub use crate::{const_concat, const_concat_if, const_integer_str, js_template}; -// JavaScript export shims. -pub use crate::{js_export, js_export_promising}; -// WAT closure shims. -pub use crate::{ - wat_closure, wat_closure_call, wat_closure_direct, wat_closure_indirect, - wat_closure_table_import, -}; -// WAT export shims. -pub use crate::{wat_export, wat_export_direct, wat_export_indirect}; -// Shared WAT helpers. -pub use crate::{wat_imports, wat_input, wat_locals, wat_slots, wat_unique_list}; diff --git a/client/js-sys/src/macro/abi.rs b/client/js-sys/src/macro/abi.rs deleted file mode 100644 index c93deaa1..00000000 --- a/client/js-sys/src/macro/abi.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::hazard::{ - FromJS, IntoJS, ReturnAbi, ReturnFromJS, ReturnIntoJS, Slot, Sret, WasmAbi, WasmRet, WatConv, -}; - -// Rust `ABI` shims used by generated import and export functions. - -pub type InputSlot1 = <::Abi as WasmAbi>::Slot1; -pub type InputSlot2 = <::Abi as WasmAbi>::Slot2; -pub type InputSlot3 = <::Abi as WasmAbi>::Slot3; -pub type InputSlot4 = <::Abi as WasmAbi>::Slot4; - -pub type FromJsSlot1 = <::Abi as WasmAbi>::Slot1; -pub type FromJsSlot2 = <::Abi as WasmAbi>::Slot2; -pub type FromJsSlot3 = <::Abi as WasmAbi>::Slot3; -pub type FromJsSlot4 = <::Abi as WasmAbi>::Slot4; - -pub type OutputSlot1 = <::Abi as WasmAbi>::Slot1; -pub type OutputSlot2 = <::Abi as WasmAbi>::Slot2; -pub type OutputSlot3 = <::Abi as WasmAbi>::Slot3; -pub type OutputSlot4 = <::Abi as WasmAbi>::Slot4; -pub type OutputRet = core::mem::MaybeUninit::Abi>>; - -pub type ReturnSlot1 = <::Abi as WasmAbi>::Slot1; -pub type ReturnSlot2 = <::Abi as WasmAbi>::Slot2; -pub type ReturnSlot3 = <::Abi as WasmAbi>::Slot3; -pub type ReturnSlot4 = <::Abi as WasmAbi>::Slot4; - -#[must_use] -#[inline] -pub fn split_input( - value: T, -) -> (InputSlot1, InputSlot2, InputSlot3, InputSlot4) { - WasmAbi::split(T::into_abi(value)) -} - -#[must_use] -#[inline] -pub fn join_from_js( - slot1: ::Slot1, - slot2: ::Slot2, - slot3: ::Slot3, - slot4: ::Slot4, -) -> T { - T::from_abi(T::Abi::join(slot1, slot2, slot3, slot4)) -} - -#[must_use] -#[inline] -pub fn return_to_js(value: T) -> WasmRet { - WasmRet::from_abi(T::into_return_abi(value)) -} - -/// Lowers a value through a different [`IntoJS`] implementation with the same -/// `ABI`. This is reserved for generated `#[js_sys(type = ...)]` overrides, -/// where `T` must also describe the value's WAT and JavaScript conversions. -/// -/// # Safety -/// -/// The value's lowering must have the semantics expected by `T`; sharing an -/// `ABI` alone does not make two [`IntoJS`] implementations interchangeable. -#[must_use] -#[inline] -pub unsafe fn split_input_as( - value: impl IntoJS, -) -> (InputSlot1, InputSlot2, InputSlot3, InputSlot4) { - WasmAbi::split(IntoJS::into_abi(value)) -} - -#[must_use] -#[inline] -pub fn join_output(value: OutputRet) -> T { - T::from_return_abi(value) -} - -/// Lifts a return value whose JavaScript conversion is described by another -/// type with the same `ABI`. -/// -/// # Safety -/// -/// The JavaScript value produced for `A` must have the semantics expected by -/// `T`; sharing an `ABI` alone does not make the conversions interchangeable. -#[must_use] -#[inline] -pub unsafe fn join_output_as(value: OutputRet) -> T -where - T: ReturnFromJS, - A: ReturnFromJS, -{ - const { - assert!( - T::JS_CONV.is_result() == A::JS_CONV.is_result(), - "return conversion overrides must preserve Result semantics", - ); - } - - T::from_return_abi(value) -} - -// Compile-time validation of conversion `metadata`. - -#[must_use] -pub const fn into_js_is_multislot() -> bool { - ! as Slot>::WAT_TYPE.is_empty() - || ! as Slot>::WAT_TYPE.is_empty() - || ! as Slot>::WAT_TYPE.is_empty() -} - -pub const fn validate_into_js() { - assert!( - !into_js_is_multislot::() || T::JS_CONV.is_some(), - "multi-slot IntoJS implementations must define IntoJS::JS_CONV", - ); -} - -pub const fn validate_from_js() { - let conversion = T::JS_CONV; - let templates = match conversion { - None => [""; 4], - Some(conv) => conv.templates, - }; - let slots = from_js_wat_slots::(); - let mut slot = 0; - - while slot < slots.len() { - assert!( - conversion.is_none() || templates[slot].is_empty() == slots[slot].abi.is_empty(), - "FromJS::JS_CONV templates must match its non-empty ABI slots", - ); - slot += 1; - } - - assert!( - conversion.is_some() - || slots[1].abi.is_empty() && slots[2].abi.is_empty() && slots[3].abi.is_empty(), - "multi-slot FromJS implementations must define FromJS::JS_CONV", - ); -} - -pub const fn validate_return_from_js() { - let indirect = !return_from_js_is_direct::(); - let conversion = T::JS_CONV.conversion(); - let (templates, sret) = match conversion { - None => ([""; 4], None), - Some(conv) => (conv.templates, conv.sret), - }; - let slots = return_from_js_wat_slots::(); - let mut slot = 0; - - while slot < slots.len() { - assert!( - conversion.is_none() || templates[slot].is_empty() == slots[slot].abi.is_empty(), - "FromJS::JS_CONV templates must match its non-empty ABI slots", - ); - slot += 1; - } - - assert!( - !indirect || conversion.is_some(), - "indirect FromJS implementations must define FromJS::JS_CONV", - ); - assert!( - indirect == sret.is_some(), - "FromJS::JS_CONV must define sret exactly for indirect returns", - ); -} - -// WAT `metadata` shared by import and export shims. - -/// The `WAT` representation of one `ABI` slot at a JavaScript boundary. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct WatSlot { - /// The carrier type in the Rust function `ABI`. - pub abi: &'static str, - /// The type visible at the JavaScript boundary. - pub boundary: &'static str, - /// Optional WAT imports required by the conversion. - pub imports: &'static str, - /// Optional scratch locals required by the conversion. - pub locals: &'static str, - /// WAT instructions that convert between `abi` and `boundary`. - pub conv: &'static str, -} - -const fn wat_slot(wat_conv: Option) -> WatSlot { - let (boundary, imports, locals, conv) = match wat_conv { - Some(WatConv { - imports, - locals, - conv, - r#type, - }) => ( - r#type, - match imports { - Some(imports) => imports, - None => "", - }, - match locals { - Some(locals) => locals, - None => "", - }, - conv, - ), - None => (S::WAT_TYPE, "", "", ""), - }; - - WatSlot { - abi: S::WAT_TYPE, - boundary, - imports, - locals, - conv, - } -} - -#[must_use] -pub const fn into_js_wat_slots() -> [WatSlot; 4] { - [ - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - ] -} - -#[must_use] -pub const fn from_js_wat_slots() -> [WatSlot; 4] { - [ - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - ] -} - -#[must_use] -pub const fn return_from_js_wat_slots() -> [WatSlot; 4] { - [ - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - ] -} - -#[must_use] -pub const fn return_into_js_wat_slots() -> [WatSlot; 4] { - [ - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - ] -} - -#[must_use] -pub const fn wat_direct() -> &'static str { - if return_from_js_is_direct::() { - return_from_js_wat_slots::()[0].abi - } else { - "" - } -} - -#[must_use] -pub const fn return_from_js_is_direct() -> bool { - ::MODE.is_direct() -} - -#[must_use] -pub const fn return_from_js_is_result() -> bool { - T::JS_CONV.is_result() -} - -pub const fn validate_return_into_js() { - let conv = T::JS_CONV.conversion(); - let multislot = if T::JS_CONV.is_result() { - ! as Slot>::WAT_TYPE.is_empty() - } else { - ! as Slot>::WAT_TYPE.is_empty() - || ! as Slot>::WAT_TYPE.is_empty() - || ! as Slot>::WAT_TYPE.is_empty() - }; - - assert!( - !multislot || conv.is_some(), - "multi-slot ReturnIntoJS implementations must define a JavaScript conversion", - ); -} - -#[must_use] -pub const fn return_into_js_is_direct() -> bool { - ::MODE.is_direct() -} - -#[must_use] -pub const fn export_output_frame_size() -> usize { - // LLVM keeps the Wasm stack pointer 16-byte aligned. Rounding every shim - // frame to that alignment preserves the invariant when the frame is allocated. - const STACK_ALIGNMENT: usize = 16; - let size = core::mem::size_of::>(); - - (size + STACK_ALIGNMENT - 1) & !(STACK_ALIGNMENT - 1) -} - -#[must_use] -pub const fn export_output_slot_offset() -> usize { - WasmRet::::slot_offset::() -} - -#[must_use] -pub const fn wat_pointer_type() -> &'static str { - crate::util::WAT_PTR_TYPE -} - -// JavaScript conversion `metadata`. - -#[must_use] -pub const fn js_input_embed() -> (&'static str, &'static str) { - js_embed(match T::JS_CONV { - Some(conv) => conv.embed, - None => None, - }) -} - -#[must_use] -pub const fn js_return_embed() -> (&'static str, &'static str) { - js_embed(match T::JS_CONV.conversion() { - Some(conv) => conv.embed, - None => None, - }) -} - -#[must_use] -pub const fn js_return_has_conversion() -> bool { - T::JS_CONV.conversion().is_some() -} - -#[must_use] -pub const fn js_output_embed() -> (&'static str, &'static str) { - js_embed(match T::JS_CONV.conversion() { - Some(conv) => conv.embed, - None => None, - }) -} - -#[must_use] -pub const fn js_from_embed() -> (&'static str, &'static str) { - js_embed(match T::JS_CONV { - Some(conv) => conv.embed, - None => None, - }) -} - -#[must_use] -pub const fn js_from_has_conversion() -> bool { - T::JS_CONV.is_some() -} - -#[must_use] -pub const fn js_result_embed() -> (&'static str, &'static str) { - if T::JS_CONV.is_result() { - ("js_sys", "externref.table") - } else { - ("", "") - } -} - -const fn js_embed(embed: Option<(&'static str, &'static str)>) -> (&'static str, &'static str) { - if let Some(embed) = embed { - embed - } else { - ("", "") - } -} - -#[must_use] -pub const fn js_input_template() -> &'static str { - if let Some(conv) = T::JS_CONV { - conv.template - } else { - "" - } -} - -#[must_use] -pub const fn js_export_output_template() -> &'static str { - let template = match T::JS_CONV.conversion() { - Some(conv) => conv.template, - None => "", - }; - - if template.is_empty() { - "$slot1" - } else { - template - } -} - -#[must_use] -pub const fn return_into_js_is_result() -> bool { - T::JS_CONV.is_result() -} - -#[must_use] -pub const fn js_output_templates() -> [&'static str; 4] { - if let Some(conv) = T::JS_CONV.conversion() { - conv.templates - } else { - ["$value", "", "", ""] - } -} - -#[must_use] -pub const fn js_output_prepare() -> &'static str { - match T::JS_CONV.conversion() { - Some(conv) => match conv.prepare { - Some(prepare) => prepare, - None => "", - }, - None => "", - } -} - -#[must_use] -pub const fn js_from_templates() -> [&'static str; 4] { - if let Some(conv) = T::JS_CONV { - conv.templates - } else { - ["$value", "", "", ""] - } -} - -#[must_use] -pub const fn js_from_prepare() -> &'static str { - if let Some(conv) = T::JS_CONV - && let Some(prepare) = conv.prepare - { - prepare - } else { - "" - } -} - -#[must_use] -pub const fn js_output_has_conversion() -> bool { - T::JS_CONV.conversion().is_some() -} - -#[must_use] -pub const fn js_output_sret() -> Option { - if let Some(conv) = T::JS_CONV.conversion() { - conv.sret - } else { - None - } -} diff --git a/client/js-sys/src/macro/closure.rs b/client/js-sys/src/macro/closure.rs deleted file mode 100644 index 97294e26..00000000 --- a/client/js-sys/src/macro/closure.rs +++ /dev/null @@ -1,270 +0,0 @@ -// WAT shims for Rust closures called from JavaScript. - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_closure_table_import { - () => {{ - const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); - - $crate::r#macro::const_concat!( - "\n(import \"env\" \"__indirect_function_table\" ", - "(table $js_sys.closure.table ", - "(@sym (name \"__indirect_function_table\")) ", - POINTER, - " 0 funcref))" - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_closure_call { - ($call_shim:ty $(,)?) => {{ - const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); - type StoredCallShim = $call_shim; - const OFFSET_VALUE: ::core::primitive::usize = - $crate::ClosureHeader::call_shim_offset::(); - const OFFSET: &::core::primitive::str = $crate::r#macro::const_integer_str!(OFFSET_VALUE); - - $crate::r#macro::const_concat!( - " local.get $js_sys.closure.data\n ", - POINTER, - ".load offset=", - OFFSET, - "\n call_indirect $js_sys.closure.table (type $js_sys.closure.call) (@reloc)" - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_closure_direct { - ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); - const DATA: $crate::r#macro::WatSlot = - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>()[0]; - - $crate::r#macro::const_concat!( - $crate::r#macro::wat_imports!( - slots = [ - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - ], - extras = [], - ), - $crate::r#macro::wat_closure_table_import!(), - "\n(type $js_sys.closure.call (func (param ", - POINTER, - ")", - $($crate::r#macro::wat_input!(export raw_param; $input),)* - "))\n", - "(func $export (@sym (name \"", - $export, - "\")) (param $data ", - DATA.boundary, - ")", - $($crate::r#macro::wat_input!(export params; $par, $input),)* - " (local $js_sys.closure.data ", - POINTER, - ")", - $crate::r#macro::wat_locals!( - slots = [ - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - ], - extras = [], - ), - "\n local.get $data", - $crate::r#macro::wat_conv_prefix(DATA.conv), - DATA.conv, - "\n local.set $js_sys.closure.data\n", - " local.get $js_sys.closure.data\n", - $($crate::r#macro::wat_input!(export gets; $par, $input),)* - $crate::r#macro::wat_closure_call!($call_shim), - "\n)" - ) - }}; - ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); - const DATA: $crate::r#macro::WatSlot = - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>()[0]; - const OUTPUT: $crate::r#macro::WatSlot = - $crate::r#macro::return_into_js_wat_slots::<$output>()[0]; - - $crate::r#macro::const_concat!( - $crate::r#macro::wat_imports!( - slots = [ - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - $crate::r#macro::wat_closure_table_import!(), - "\n(type $js_sys.closure.call (func (param ", - POINTER, - ")", - $($crate::r#macro::wat_input!(export raw_param; $input),)* - " (result ", - OUTPUT.abi, - ")))\n", - "(func $export (@sym (name \"", - $export, - "\")) (param $data ", - DATA.boundary, - ")", - $($crate::r#macro::wat_input!(export params; $par, $input),)* - " (result ", - OUTPUT.boundary, - ") (local $js_sys.closure.data ", - POINTER, - ")", - $crate::r#macro::wat_locals!( - slots = [ - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - "\n local.get $data", - $crate::r#macro::wat_conv_prefix(DATA.conv), - DATA.conv, - "\n local.set $js_sys.closure.data\n", - " local.get $js_sys.closure.data\n", - $($crate::r#macro::wat_input!(export gets; $par, $input),)* - $crate::r#macro::wat_closure_call!($call_shim), - $crate::r#macro::wat_conv_prefix(OUTPUT.conv), - OUTPUT.conv, - "\n)" - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_closure_indirect { - ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); - const DATA: $crate::r#macro::WatSlot = - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>()[0]; - const SIZE: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_frame_size::<$output>() - ); - const RESULT_TYPES: &::core::primitive::str = - $crate::r#macro::wat_slots!( - types, - $crate::r#macro::return_into_js_wat_slots::<$output>(), - boundary, - ); - - $crate::r#macro::const_concat!( - $crate::r#macro::wat_imports!( - slots = [ - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - $crate::r#macro::wat_closure_table_import!(), - "\n(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut ", - POINTER, - ")))\n", - "(type $js_sys.closure.call (func (param ", - POINTER, - ") (param ", - POINTER, - ")", - $($crate::r#macro::wat_input!(export raw_param; $input),)* - "))\n", - "(func $export (@sym (name \"", - $export, - "\")) (param $data ", - DATA.boundary, - ")", - $($crate::r#macro::wat_input!(export params; $par, $input),)* - " (result ", - RESULT_TYPES, - ")\n (local $retptr ", - POINTER, - ")\n (local $js_sys.closure.data ", - POINTER, - ")", - $crate::r#macro::wat_locals!( - slots = [ - $crate::r#macro::from_js_wat_slots::<::core::primitive::usize>(), - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - "\n local.get $data", - $crate::r#macro::wat_conv_prefix(DATA.conv), - DATA.conv, - "\n local.set $js_sys.closure.data\n", - " global.get $__stack_pointer\n ", - POINTER, - ".const ", - SIZE, - "\n ", - POINTER, - ".sub\n local.tee $retptr\n global.set $__stack_pointer\n", - " local.get $retptr\n", - " local.get $js_sys.closure.data\n", - $($crate::r#macro::wat_input!(export gets; $par, $input),)* - $crate::r#macro::wat_closure_call!($call_shim), - "\n", - $crate::r#macro::wat_slots!( - loads, - $output, - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ), - " local.get $retptr\n ", - POINTER, - ".const ", - SIZE, - "\n ", - POINTER, - ".add\n global.set $__stack_pointer\n)" - ) - }}; -} - -/// Generates a closure dispatcher that calls the raw shim stored in its -/// allocation. -#[doc(hidden)] -#[macro_export] -macro_rules! wat_closure { - ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - $crate::r#macro::validate_from_js::<::core::primitive::usize>(); - $($crate::r#macro::validate_from_js::<$input>();)* - - $crate::r#macro::wat_closure_direct!( - $export, - $call_shim, - ($(($par, $input)),*), - ) - }}; - ($export:expr, $call_shim:ty, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - $crate::r#macro::validate_from_js::<::core::primitive::usize>(); - $($crate::r#macro::validate_from_js::<$input>();)* - $crate::r#macro::validate_return_into_js::<$output>(); - - if $crate::r#macro::return_into_js_is_direct::<$output>() { - $crate::r#macro::wat_closure_direct!( - $export, - $call_shim, - ($(($par, $input)),*), - $output, - ) - } else { - $crate::r#macro::wat_closure_indirect!( - $export, - $call_shim, - ($(($par, $input)),*), - $output, - ) - } - }}; -} diff --git a/client/js-sys/src/macro/export.rs b/client/js-sys/src/macro/export.rs deleted file mode 100644 index 0227e82b..00000000 --- a/client/js-sys/src/macro/export.rs +++ /dev/null @@ -1,278 +0,0 @@ -mod js; - -pub use js::*; - -use super::writer::Writer; - -// WAT shim generation. - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_direct { - ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => { - $crate::r#macro::const_concat!( - $crate::r#macro::wat_imports!( - slots = [ - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - ], - extras = [], - ), - "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", - $raw, - "\"))", - $($crate::r#macro::wat_input!(export raw_param; $input),)* - "))\n", - "(func $export (@sym (name \"", - $export, - "\"))", - $($crate::r#macro::wat_input!(export params; $par, $input),)* - $crate::r#macro::wat_locals!( - slots = [ - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - ], - extras = [], - ), - "\n", - $($crate::r#macro::wat_input!(export gets; $par, $input),)* - " call $raw (@reloc)\n", - ")" - ) - }; - ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const SLOT: $crate::r#macro::WatSlot = - $crate::r#macro::return_into_js_wat_slots::<$output>()[0]; - - $crate::r#macro::const_concat!( - $crate::r#macro::wat_imports!( - slots = [ - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", - $raw, - "\"))", - $($crate::r#macro::wat_input!(export raw_param; $input),)* - " (result ", - SLOT.abi, - ")))\n", - "(func $export (@sym (name \"", - $export, - "\"))", - $($crate::r#macro::wat_input!(export params; $par, $input),)* - " (result ", - SLOT.boundary, - ")", - $crate::r#macro::wat_locals!( - slots = [ - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - "\n", - $($crate::r#macro::wat_input!(export gets; $par, $input),)* - " call $raw (@reloc)", - $crate::r#macro::wat_conv_prefix(SLOT.conv), - SLOT.conv, - "\n)" - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export_indirect { - ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const POINTER: &::core::primitive::str = $crate::r#macro::wat_pointer_type(); - const SIZE: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_frame_size::<$output>() - ); - const RESULT_TYPES: &::core::primitive::str = - $crate::r#macro::wat_slots!( - types, - $crate::r#macro::return_into_js_wat_slots::<$output>(), - boundary, - ); - - $crate::r#macro::const_concat!( - $crate::r#macro::wat_imports!( - slots = [ - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - "\n(import \"env\" \"raw\" (func $raw (@sym (name \"", - $raw, - "\")) (param ", - POINTER, - ")", - $($crate::r#macro::wat_input!(export raw_param; $input),)* - "))\n", - "(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut ", - POINTER, - ")))\n", - "(func $export (@sym (name \"", - $export, - "\"))", - $($crate::r#macro::wat_input!(export params; $par, $input),)* - " (result ", - RESULT_TYPES, - ")\n", - " (local $retptr ", - POINTER, - ")", - $crate::r#macro::wat_locals!( - slots = [ - $($crate::r#macro::from_js_wat_slots::<$input>(),)* - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ], - extras = [], - ), - "\n", - " global.get $__stack_pointer\n ", - POINTER, - ".const ", - SIZE, - "\n ", - POINTER, - ".sub\n local.tee $retptr\n global.set $__stack_pointer\n", - " local.get $retptr\n", - $($crate::r#macro::wat_input!(export gets; $par, $input),)* - " call $raw (@reloc)\n", - $crate::r#macro::wat_slots!( - loads, - $output, - $crate::r#macro::return_into_js_wat_slots::<$output>(), - ), - " local.get $retptr\n ", - POINTER, - ".const ", - SIZE, - "\n ", - POINTER, - ".add\n global.set $__stack_pointer\n)" - ) - }}; -} - -/// Generates the complete WAT shim for one Rust export. -#[doc(hidden)] -#[macro_export] -macro_rules! wat_export { - ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - $($crate::r#macro::validate_from_js::<$input>();)* - - $crate::r#macro::wat_export_direct!($raw, $export, ($(($par, $input)),*)) - }}; - ($raw:expr, $export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - $($crate::r#macro::validate_from_js::<$input>();)* - $crate::r#macro::validate_return_into_js::<$output>(); - - if $crate::r#macro::return_into_js_is_direct::<$output>() { - $crate::r#macro::wat_export_direct!( - $raw, - $export, - ($(($par, $input)),*), - $output, - ) - } else { - $crate::r#macro::wat_export_indirect!( - $raw, - $export, - ($(($par, $input)),*), - $output, - ) - } - }}; -} - -/// Generates the complete JavaScript wrapper for one Rust export. -#[doc(hidden)] -#[macro_export] -macro_rules! js_export { - ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - const INPUTS: &[$crate::r#macro::ExportInput] = &[ - $($crate::r#macro::export_input::<$input>($par),)* - ]; - const DESCRIPTOR: $crate::r#macro::ExportDescriptor = - $crate::r#macro::ExportDescriptor::new( - $export, - INPUTS, - ::core::option::Option::None, - $crate::r#macro::ExportMode::Sync, - ); - const LEN: ::core::primitive::usize = - $crate::r#macro::export_js_len(&DESCRIPTOR); - const VALUE: [::core::primitive::u8; LEN] = - $crate::r#macro::render_export_js::(&DESCRIPTOR); - - // SAFETY: Rendering only concatenates and substitutes valid strings. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; - ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const INPUTS: &[$crate::r#macro::ExportInput] = &[ - $($crate::r#macro::export_input::<$input>($par),)* - ]; - const DESCRIPTOR: $crate::r#macro::ExportDescriptor = - $crate::r#macro::ExportDescriptor::new( - $export, - INPUTS, - ::core::option::Option::Some($crate::r#macro::export_output::<$output>()), - $crate::r#macro::ExportMode::Sync, - ); - const LEN: ::core::primitive::usize = - $crate::r#macro::export_js_len(&DESCRIPTOR); - const VALUE: [::core::primitive::u8; LEN] = - $crate::r#macro::render_export_js::(&DESCRIPTOR); - - // SAFETY: Rendering only concatenates and substitutes valid strings. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; -} - -/// Generates a JavaScript wrapper for a Wasm export marked as `promising`. -#[doc(hidden)] -#[macro_export] -macro_rules! js_export_promising { - ($export:expr, ($(($par:literal, $input:ty)),*) $(,)?) => {{ - const INPUTS: &[$crate::r#macro::ExportInput] = &[ - $($crate::r#macro::export_input::<$input>($par),)* - ]; - const DESCRIPTOR: $crate::r#macro::ExportDescriptor = - $crate::r#macro::ExportDescriptor::new( - $export, - INPUTS, - ::core::option::Option::None, - $crate::r#macro::ExportMode::Promising, - ); - const LEN: ::core::primitive::usize = - $crate::r#macro::export_js_len(&DESCRIPTOR); - const VALUE: [::core::primitive::u8; LEN] = - $crate::r#macro::render_export_js::(&DESCRIPTOR); - - // SAFETY: Rendering only concatenates and substitutes valid strings. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; - ($export:expr, ($(($par:literal, $input:ty)),*), $output:ty $(,)?) => {{ - const INPUTS: &[$crate::r#macro::ExportInput] = &[ - $($crate::r#macro::export_input::<$input>($par),)* - ]; - const DESCRIPTOR: $crate::r#macro::ExportDescriptor = - $crate::r#macro::ExportDescriptor::new( - $export, - INPUTS, - ::core::option::Option::Some($crate::r#macro::export_output::<$output>()), - $crate::r#macro::ExportMode::Promising, - ); - const LEN: ::core::primitive::usize = - $crate::r#macro::export_js_len(&DESCRIPTOR); - const VALUE: [::core::primitive::u8; LEN] = - $crate::r#macro::render_export_js::(&DESCRIPTOR); - - // SAFETY: Rendering only concatenates and substitutes valid strings. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; -} diff --git a/client/js-sys/src/macro/export/js.rs b/client/js-sys/src/macro/export/js.rs deleted file mode 100644 index 9f191287..00000000 --- a/client/js-sys/src/macro/export/js.rs +++ /dev/null @@ -1,430 +0,0 @@ -use core::marker::PhantomData; - -use super::Writer; -use crate::hazard::{FromJS, ReturnIntoJS}; -use crate::r#macro::text::{JS_TEMPLATE_PLACEHOLDERS, js_template_placeholder}; -use crate::r#macro::{ - WatSlot, from_js_wat_slots, js_export_output_template, js_from_has_conversion, js_from_prepare, - js_from_templates, js_return_has_conversion, return_into_js_is_direct, - return_into_js_is_result, return_into_js_wat_slots, validate_from_js, validate_return_into_js, -}; - -/// All target-dependent information needed to render one exported argument. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ExportInput { - name: &'static str, - ty: &'static ExportInputType, -} - -#[derive(Clone, Copy)] -struct ExportInputType { - slots: [WatSlot; 4], - prepare: &'static str, - templates: [&'static str; 4], - has_conversion: bool, -} - -/// All target-dependent information needed to render one exported result. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ExportOutput { - slots: [WatSlot; 4], - template: &'static str, - direct: bool, - result: bool, - has_conversion: bool, -} - -/// Selects the JavaScript export wrapper generated for a descriptor. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub enum ExportMode { - Sync, - Promising, -} - -/// A semantic description of one JavaScript export. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ExportDescriptor { - name: &'static str, - inputs: &'static [ExportInput], - output: Option<&'static ExportOutput>, - mode: ExportMode, -} - -struct ExportInputMetadata(PhantomData); - -impl ExportInputMetadata { - const VALUE: ExportInputType = { - validate_from_js::(); - - ExportInputType { - slots: from_js_wat_slots::(), - prepare: js_from_prepare::(), - templates: js_from_templates::(), - has_conversion: js_from_has_conversion::(), - } - }; -} - -struct ExportOutputMetadata(PhantomData); - -impl ExportOutputMetadata { - const VALUE: ExportOutput = { - validate_return_into_js::(); - - ExportOutput { - slots: return_into_js_wat_slots::(), - template: js_export_output_template::(), - direct: return_into_js_is_direct::(), - result: return_into_js_is_result::(), - has_conversion: js_return_has_conversion::(), - } - }; -} - -/// Builds the descriptor for one exported argument. -#[doc(hidden)] -#[must_use] -pub const fn export_input(name: &'static str) -> ExportInput { - ExportInput { - name, - ty: &ExportInputMetadata::::VALUE, - } -} - -/// Returns the descriptor for an exported result. -#[doc(hidden)] -#[must_use] -pub const fn export_output() -> &'static ExportOutput { - &ExportOutputMetadata::::VALUE -} - -impl ExportDescriptor { - /// Creates an export descriptor. - #[doc(hidden)] - #[must_use] - pub const fn new( - name: &'static str, - inputs: &'static [ExportInput], - output: Option<&'static ExportOutput>, - mode: ExportMode, - ) -> Self { - Self { - name, - inputs, - output, - mode, - } - } - - const fn is_passthrough(&self) -> bool { - let mut input = 0; - while input < self.inputs.len() { - if self.inputs[input].ty.has_conversion { - return false; - } - input += 1; - } - - match self.output { - Some(output) => !output.needs_postprocess(), - None => true, - } - } - - const fn has_prepares(&self) -> bool { - let mut input = 0; - while input < self.inputs.len() { - if !self.inputs[input].ty.prepare.is_empty() { - return true; - } - input += 1; - } - - false - } - - const fn write(&self, writer: &mut Writer) { - if self.is_passthrough() { - self.write_raw(writer); - return; - } - - match self.mode { - ExportMode::Sync => self.write_sync(writer), - ExportMode::Promising => self.write_promising(writer), - } - } - - const fn write_sync(&self, writer: &mut Writer) { - writer.write_byte(b'('); - self.write_parameters(writer); - writer.write_str(") => {\n"); - self.write_prepares(writer, " "); - - if let Some(output) = self.output { - writer.write_str(" const ret = "); - self.write_raw_call(writer); - writer.write_byte(b'\n'); - output.write_result_throw(writer, " "); - writer.write_str(" return "); - output.write_expression(writer); - writer.write_str("\n}"); - } else { - writer.write_str(" "); - self.write_raw_call(writer); - writer.write_str("\n}"); - } - } - - const fn write_promising(&self, writer: &mut Writer) { - writer.write_str("(() => {\n const $promising = "); - self.write_raw(writer); - writer.write_str("\n return ("); - self.write_parameters(writer); - - if self.has_prepares() { - writer.write_str(") => {\n"); - self.write_prepares(writer, " "); - writer.write_str(" return $promising("); - self.write_arguments(writer); - writer.write_byte(b')'); - self.write_promising_then(writer); - writer.write_str("\n }\n})()"); - } else { - writer.write_str(") => $promising("); - self.write_arguments(writer); - writer.write_byte(b')'); - self.write_promising_then(writer); - writer.write_str("\n})()"); - } - } - - const fn write_promising_then(&self, writer: &mut Writer) { - let Some(output) = self.output else { - return; - }; - if !output.needs_postprocess() { - return; - } - - writer.write_str(".then(ret => {\n"); - output.write_result_throw(writer, " "); - writer.write_str(" return "); - output.write_expression(writer); - writer.write_str("\n })"); - } - - const fn write_raw(&self, writer: &mut Writer) { - if matches!(self.mode, ExportMode::Promising) { - writer.write_str("WebAssembly.promising("); - } - - writer.write_str("wasmExports['"); - writer.write_str(self.name); - writer.write_str("']"); - - if matches!(self.mode, ExportMode::Promising) { - writer.write_byte(b')'); - } - } - - const fn write_raw_call(&self, writer: &mut Writer) { - self.write_raw(writer); - writer.write_byte(b'('); - self.write_arguments(writer); - writer.write_byte(b')'); - } - - const fn write_parameters(&self, writer: &mut Writer) { - let mut input = 0; - while input < self.inputs.len() { - if input != 0 { - writer.write_str(", "); - } - writer.write_str(self.inputs[input].name); - input += 1; - } - } - - const fn write_arguments(&self, writer: &mut Writer) { - let mut wrote_argument = false; - let mut input = 0; - - while input < self.inputs.len() { - let descriptor = &self.inputs[input]; - let mut slot = 0; - - while slot < descriptor.ty.slots.len() { - if !descriptor.ty.slots[slot].abi.is_empty() { - if wrote_argument { - writer.write_str(", "); - } - write_template( - writer, - descriptor.ty.templates[slot], - TemplateValues::Input(descriptor.name), - ); - wrote_argument = true; - } - slot += 1; - } - - input += 1; - } - } - - const fn write_prepares(&self, writer: &mut Writer, indent: &str) { - let mut input = 0; - while input < self.inputs.len() { - let descriptor = &self.inputs[input]; - if !descriptor.ty.prepare.is_empty() { - writer.write_str(indent); - writer.write_str("const "); - writer.write_str(descriptor.name); - writer.write_str("$prepared = "); - write_template( - writer, - descriptor.ty.prepare, - TemplateValues::Value(descriptor.name), - ); - writer.write_byte(b'\n'); - } - input += 1; - } - } -} - -impl ExportOutput { - const fn needs_postprocess(&self) -> bool { - self.has_conversion || self.result - } - - const fn write_expression(&self, writer: &mut Writer) { - write_template( - writer, - self.template, - TemplateValues::Output { - direct: self.direct, - result: self.result, - }, - ); - } - - const fn write_result_throw(&self, writer: &mut Writer, indent: &str) { - if !self.result { - return; - } - - let discriminant = if self.slots[0].abi.is_empty() { - 0 - } else if self.slots[1].abi.is_empty() { - 1 - } else { - 2 - }; - - writer.write_str(indent); - writer.write_str("if ("); - write_ret_index(writer, discriminant); - writer.write_str(" !== 0) throw "); - write_ret_index(writer, discriminant + 1); - writer.write_byte(b'\n'); - } -} - -/// Returns the exact byte length produced by [`render_export_js`]. -#[doc(hidden)] -#[must_use] -pub const fn export_js_len(descriptor: &ExportDescriptor) -> usize { - let mut writer = Writer::<0>::new(); - descriptor.write(&mut writer); - writer.len() -} - -/// Renders one JavaScript export into an exact-size byte array. -#[doc(hidden)] -#[must_use] -pub const fn render_export_js(descriptor: &ExportDescriptor) -> [u8; LEN] { - let mut writer = Writer::::new(); - descriptor.write(&mut writer); - assert!(writer.len() == LEN); - writer.finish_padded() -} - -#[derive(Clone, Copy)] -enum TemplateValues<'a> { - Input(&'a str), - Value(&'a str), - Output { direct: bool, result: bool }, -} - -const fn write_template( - writer: &mut Writer, - template: &str, - values: TemplateValues<'_>, -) { - let bytes = template.as_bytes(); - let mut input = 0; - - while input < bytes.len() { - let placeholder = js_template_placeholder(bytes, input); - - if placeholder == 0 { - match values { - TemplateValues::Input(value) | TemplateValues::Value(value) => { - writer.write_str(value); - } - TemplateValues::Output { .. } => {} - } - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else if placeholder == 1 { - if let TemplateValues::Input(name) = values { - writer.write_str(name); - writer.write_str("$prepared"); - } - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - if let TemplateValues::Output { direct, result } = values { - write_output_slot(writer, direct, result, placeholder - 2); - } - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else { - let start = input; - input += 1; - while input < bytes.len() && bytes[input] != b'$' { - input += 1; - } - writer.write_str_range(template, start, input); - } - } -} - -const fn write_output_slot( - writer: &mut Writer, - direct: bool, - result: bool, - slot: usize, -) { - if result { - if slot < 2 { - write_ret_index(writer, slot); - } - } else if direct { - if slot == 0 { - writer.write_str("ret"); - } - } else { - write_ret_index(writer, slot); - } -} - -const fn write_ret_index(writer: &mut Writer, index: usize) { - assert!(index < 4); - writer.write_str("ret["); - writer.write_byte(b"0123"[index]); - writer.write_byte(b']'); -} diff --git a/client/js-sys/src/macro/import.rs b/client/js-sys/src/macro/import.rs deleted file mode 100644 index 496e6e83..00000000 --- a/client/js-sys/src/macro/import.rs +++ /dev/null @@ -1,433 +0,0 @@ -mod js; -mod wat; - -use core::marker::PhantomData; - -use super::writer::Writer; -use super::{ - WatSlot, into_js_wat_slots, js_input_template, js_output_has_conversion, js_output_prepare, - js_output_sret, js_output_templates, js_result_catch, js_result_try, return_from_js_is_direct, - return_from_js_wat_slots, validate_into_js, validate_return_from_js, wat_result_catch, - wat_result_default, wat_result_imports, wat_result_locals, wat_result_try, -}; -use crate::hazard::{IntoJS, ReturnFromJS, Sret}; - -const JS_RETPTR_CONV: &str = crate::js_template!( - js_input_template::>(), - slots = ["$retptr", "", "", ""], -); - -/// All target-dependent information needed to render one imported argument. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ImportInput { - name: &'static str, - ty: &'static ImportInputType, -} - -#[derive(Clone, Copy)] -struct ImportInputType { - slots: [WatSlot; 4], - js_template: &'static str, - has_js_conversion: bool, - wat_capacity: WatInputCapacity, -} - -/// All target-dependent information needed to render one imported result. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ImportOutput { - direct: bool, - slots: [WatSlot; 4], - pointer: WatSlot, - has_js_conversion: bool, - js_prepare: &'static str, - js_templates: [&'static str; 4], - js_sret: Option, - js_try: &'static str, - js_catch: &'static str, - wat_result_imports: &'static str, - wat_result_locals: &'static str, - wat_result_try: &'static str, - wat_result_catch: &'static str, - wat_result_default: &'static str, - wat_capacity: usize, -} - -/// JavaScript-specific parts of an import descriptor. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ImportJs { - pub direct_wrapper: bool, - pub direct_call: &'static str, - pub indirect_call: &'static str, - pub required_embeds: &'static [(&'static str, &'static str)], -} - -/// A semantic description of one JavaScript import. -/// -/// Rendering is deliberately centralized in ordinary `const fn`s. Generated -/// bindings construct this value once instead of expanding a tree of string -/// concatenation macros and materializing every intermediate fragment. -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct ImportDescriptor { - module: &'static str, - import: &'static str, - shim: &'static str, - inputs: &'static [ImportInput], - output: Option<&'static ImportOutput>, - js: Option, - suspending: bool, - wat_capacity: usize, - js_capacity: usize, -} - -/// One framed sequence of length-prefixed custom-section records. -/// -/// `capacity` lets multiple padded fragments be concatenated in one custom -/// section. `used` excludes the zero-filled tail of `records`. -#[doc(hidden)] -#[repr(C)] -pub struct ImportSection { - capacity: [u8; 4], - used: [u8; 4], - records: [u8; CAPACITY], -} - -struct ImportInputMetadata(PhantomData); - -#[derive(Clone, Copy)] -struct WatInputCapacity { - fixed: usize, - name_uses: usize, -} - -impl ImportInputMetadata { - const VALUE: ImportInputType = { - validate_into_js::(); - let slots = into_js_wat_slots::(); - - ImportInputType { - slots, - js_template: js_input_template::(), - has_js_conversion: T::JS_CONV.is_some(), - wat_capacity: wat::input_capacity(&slots), - } - }; -} - -struct ImportOutputMetadata(PhantomData); - -impl ImportOutputMetadata { - const VALUE: ImportOutput = { - validate_return_from_js::(); - let slots = return_from_js_wat_slots::(); - let direct = return_from_js_is_direct::(); - let mut output = ImportOutput { - direct, - slots, - pointer: into_js_wat_slots::>()[0], - has_js_conversion: js_output_has_conversion::(), - js_prepare: js_output_prepare::(), - js_templates: js_output_templates::(), - js_sret: js_output_sret::(), - js_try: js_result_try::(), - js_catch: js_result_catch::(direct), - wat_result_imports: wat_result_imports::(), - wat_result_locals: wat_result_locals::(), - wat_result_try: wat_result_try::(), - wat_result_catch: wat_result_catch::(), - wat_result_default: wat_result_default::(), - wat_capacity: 0, - }; - - output.wat_capacity = wat::output_capacity(&output); - output - }; -} - -#[doc(hidden)] -#[must_use] -pub const fn import_input(name: &'static str) -> ImportInput { - ImportInput { - name, - ty: &ImportInputMetadata::::VALUE, - } -} - -#[doc(hidden)] -#[must_use] -pub const fn import_output() -> &'static ImportOutput { - &ImportOutputMetadata::::VALUE -} - -impl ImportDescriptor { - #[doc(hidden)] - #[must_use] - pub const fn new( - module: &'static str, - import: &'static str, - shim: &'static str, - inputs: &'static [ImportInput], - output: Option<&'static ImportOutput>, - js: Option, - ) -> Self { - Self::build(module, import, shim, inputs, output, js, false) - } - - #[doc(hidden)] - #[must_use] - pub const fn new_suspending( - module: &'static str, - import: &'static str, - shim: &'static str, - inputs: &'static [ImportInput], - output: Option<&'static ImportOutput>, - js: Option, - ) -> Self { - assert!( - js.is_some(), - "suspending imports require a generated JavaScript binding", - ); - if let Some(output) = output { - assert!( - !catches_result_in_js_from_output(output), - "suspending Result imports require the Wasm exception-handling target feature", - ); - } - Self::build(module, import, shim, inputs, output, js, true) - } - - const fn build( - module: &'static str, - import: &'static str, - shim: &'static str, - inputs: &'static [ImportInput], - output: Option<&'static ImportOutput>, - js: Option, - suspending: bool, - ) -> Self { - let mut descriptor = Self { - module, - import, - shim, - inputs, - output, - js, - suspending, - wat_capacity: 0, - js_capacity: 0, - }; - - descriptor.wat_capacity = wat::descriptor_capacity(&descriptor); - descriptor.js_capacity = js::descriptor_capacity(&descriptor); - descriptor - } - - #[must_use] - const fn needs_js_shim(&self) -> bool { - let mut input = 0; - - while input < self.inputs.len() { - if self.inputs[input].ty.has_js_conversion { - return true; - } - input += 1; - } - - match self.output { - Some(output) => output.has_js_conversion || catches_result_in_js_from_output(output), - None => false, - } - } - - const fn awaits_suspending_output(&self) -> bool { - if !self.suspending { - return false; - } - - match self.output { - Some(output) => output.has_js_conversion || catches_result_in_js_from_output(output), - None => false, - } - } -} - -/// Returns a safe upper bound for [`import_wat`]. -/// -/// This follows the rendering logic without scanning declaration contents, so -/// it is suitable for sizing a padded, single-pass section. -#[doc(hidden)] -#[must_use] -pub const fn import_wat_capacity(imports: &[ImportDescriptor]) -> usize { - let mut capacity = Capacity::new(); - let mut import = 0; - - if !imports.is_empty() { - capacity.add(4); - capacity.add(imports.len() - 1); - } - - while import < imports.len() { - capacity.add(imports[import].wat_capacity); - import += 1; - } - - capacity.get() -} - -#[doc(hidden)] -#[must_use] -pub const fn import_wat( - imports: &[ImportDescriptor], -) -> ImportSection { - let mut writer = Writer::::new(); - write_wat(&mut writer, imports); - - ImportSection::new(writer) -} - -/// Returns a safe upper bound for [`import_js`]. -/// -/// This follows the rendering logic without scanning template contents, so it -/// is suitable for sizing a padded, single-pass section. -#[doc(hidden)] -#[must_use] -pub const fn import_js_capacity(imports: &[ImportDescriptor]) -> usize { - let mut capacity = Capacity::new(); - let mut import = 0; - - while import < imports.len() { - if imports[import].js.is_some() { - capacity.add(4); - capacity.add(imports[import].js_capacity); - } - import += 1; - } - - capacity.get() -} - -#[doc(hidden)] -#[must_use] -pub const fn import_js( - imports: &[ImportDescriptor], -) -> ImportSection { - let mut writer = Writer::::new(); - write_js(&mut writer, imports); - - ImportSection::new(writer) -} - -const fn write_wat(writer: &mut Writer, imports: &[ImportDescriptor]) { - if imports.is_empty() { - return; - } - - let header = writer.len(); - writer.write_u32(0); - let start = writer.len(); - let mut import = 0; - - while import < imports.len() { - if import != 0 { - writer.write_byte(b'\n'); - } - - imports[import].write_wat_boundary_import(writer); - import += 1; - } - - wat::write_wat_support_imports(writer, imports); - - import = 0; - while import < imports.len() { - imports[import].write_wat_shim(writer); - import += 1; - } - - let record_len = writer.len() - start; - writer.set_u32(header, record_len); -} - -const fn write_js(writer: &mut Writer, imports: &[ImportDescriptor]) { - let mut import = 0; - - while import < imports.len() { - if imports[import].js.is_some() { - let header = writer.len(); - writer.write_u32(0); - let start = writer.len(); - imports[import].write_js_record(writer); - let record_len = writer.len() - start; - writer.set_u32(header, record_len); - } - import += 1; - } -} - -#[must_use] -const fn catches_result_in_js_from_output(output: &ImportOutput) -> bool { - !output.js_try.is_empty() -} - -pub(super) struct Capacity(usize); - -impl Capacity { - pub const fn new() -> Self { - Self(0) - } - - pub const fn add(&mut self, additional: usize) { - self.0 = match self.0.checked_add(additional) { - Some(capacity) => capacity, - None => panic!("import section capacity overflows usize"), - }; - } - - pub const fn add_str(&mut self, value: &str) { - self.add(value.len()); - } - - pub const fn add_repeated_str(&mut self, value: &str, repetitions: usize) { - let Some(additional) = value.len().checked_mul(repetitions) else { - panic!("import section capacity overflows usize"); - }; - self.add(additional); - } - - pub const fn add_wat_lines(&mut self, value: &str) { - if !value.is_empty() { - // The separators already present in `value` become the leading - // newlines of all but its first line. - self.add(value.len()); - self.add(1); - } - } - - pub const fn get(self) -> usize { - self.0 - } -} - -impl ImportSection { - const fn new(writer: Writer) -> Self { - assert!(CAPACITY <= u32::MAX as usize); - let used = writer.len(); - assert!(used <= u32::MAX as usize); - - Self { - capacity: u32_bytes(CAPACITY), - used: u32_bytes(used), - records: writer.finish_padded(), - } - } -} - -const fn u32_bytes(value: usize) -> [u8; 4] { - assert!(value <= u32::MAX as usize); - let bytes = value.to_le_bytes(); - - [bytes[0], bytes[1], bytes[2], bytes[3]] -} diff --git a/client/js-sys/src/macro/import/js.rs b/client/js-sys/src/macro/import/js.rs deleted file mode 100644 index e18d6677..00000000 --- a/client/js-sys/src/macro/import/js.rs +++ /dev/null @@ -1,608 +0,0 @@ -use super::{ - Capacity, ImportDescriptor, ImportInput, ImportJs, ImportOutput, JS_RETPTR_CONV, Writer, -}; -use crate::hazard::Sret; -use crate::r#macro::text::{JS_TEMPLATE_PLACEHOLDERS, js_template_placeholder}; - -pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize { - let Some(js) = descriptor.js else { - return 0; - }; - let mut capacity = Capacity::new(); - - capacity.add(2); - capacity.add_str(descriptor.module); - capacity.add(2); - capacity.add_str(descriptor.import); - capacity.add(1); - - let mut embed = 0; - while embed < js.required_embeds.len() { - let (module, name) = js.required_embeds[embed]; - - capacity.add(2); - capacity.add_str(module); - capacity.add(2); - capacity.add_str(name); - embed += 1; - } - - if descriptor.suspending { - capacity.add_str("new WebAssembly.Suspending("); - } - - let wrapped = descriptor.needs_js_shim(); - let await_output = descriptor.awaits_suspending_output(); - - if await_output { - capacity.add_str("async "); - capacity.add_str("await ("); - capacity.add(1); - } - - if wrapped { - capacity.add(1); - - if let Some(output) = descriptor.output - && !output.direct - { - capacity.add_str("$retptr"); - - if !descriptor.inputs.is_empty() { - capacity.add_str(", "); - } - } - - add_input_parameters_capacity(&mut capacity, descriptor.inputs); - capacity.add_str(") => {\n"); - } else if js.direct_wrapper { - capacity.add(1); - add_input_parameters_capacity(&mut capacity, descriptor.inputs); - capacity.add_str(") => "); - } - - let mut input = 0; - while input < descriptor.inputs.len() { - add_input_conversion_capacity(&mut capacity, &descriptor.inputs[input]); - input += 1; - } - - match descriptor.output { - Some(output) => { - add_output_capacity(&mut capacity, output, js, wrapped); - } - None => { - if wrapped { - capacity.add_str(" "); - if descriptor.suspending { - capacity.add_str("return "); - } - capacity.add_str(js.indirect_call); - capacity.add_str("\n}"); - } else { - capacity.add_str(js.direct_call); - } - } - } - - if descriptor.suspending { - capacity.add(1); - } - - capacity.get() -} - -const fn add_input_parameters_capacity(capacity: &mut Capacity, inputs: &[ImportInput]) { - let mut input = 0; - - while input < inputs.len() { - if input != 0 { - capacity.add_str(", "); - } - - let descriptor = &inputs[input]; - add_slot_name_capacity(capacity, descriptor.name); - - let mut slot = 1; - while slot < descriptor.ty.slots.len() { - if !descriptor.ty.slots[slot].abi.is_empty() { - capacity.add_str(", "); - add_slot_name_capacity(capacity, descriptor.name); - } - slot += 1; - } - - input += 1; - } -} - -const fn add_input_conversion_capacity(capacity: &mut Capacity, input: &ImportInput) { - if !input.ty.has_js_conversion { - return; - } - - capacity.add_str(" "); - add_slot_name_capacity(capacity, input.name); - capacity.add_str(" = "); - add_template_capacity(capacity, input.ty.js_template, "", "", Some(input.name)); - capacity.add(1); -} - -const fn add_output_capacity( - capacity: &mut Capacity, - output: &ImportOutput, - js: ImportJs, - wrapped: bool, -) { - let convert_direct = output.direct && output.has_js_conversion; - let catches_result = !output.js_try.is_empty(); - let call = if wrapped { - js.indirect_call - } else { - js.direct_call - }; - let indent = if catches_result { " " } else { " " }; - - if !output.direct && !JS_RETPTR_CONV.is_empty() { - capacity.add_str(" $retptr = "); - capacity.add_str(JS_RETPTR_CONV); - capacity.add(1); - } - - capacity.add_str(output.js_try); - - if convert_direct { - capacity.add_str(indent); - capacity.add_str("const $ret = "); - } else if output.direct { - if catches_result { - capacity.add_str(" return "); - } else if wrapped { - capacity.add_str(" return "); - } - } else { - capacity.add_str(indent); - capacity.add_str("const $ret = "); - } - - if output.direct && !convert_direct { - add_template_capacity(capacity, output.js_templates[0], call, "", None); - } else { - capacity.add_str(call); - } - - if convert_direct { - if !output.js_prepare.is_empty() { - capacity.add(1); - capacity.add_str(indent); - capacity.add_str("const $prepared = "); - add_template_capacity(capacity, output.js_prepare, "$ret", "", None); - } - - capacity.add(1); - capacity.add_str(indent); - capacity.add_str("return "); - add_template_capacity(capacity, output.js_templates[0], "$ret", "$prepared", None); - } - - if !output.direct { - match output.js_sret { - Some(Sret::Slots(function)) => { - if !output.js_prepare.is_empty() { - capacity.add(1); - capacity.add_str(indent); - capacity.add_str("const $prepared = "); - add_template_capacity(capacity, output.js_prepare, "$ret", "", None); - } - - capacity.add(1); - capacity.add_str(indent); - capacity.add_str(function); - capacity.add(1); - add_template_capacity(capacity, output.js_templates[0], "$ret", "$prepared", None); - - let mut slot = 1; - while slot < output.js_templates.len() { - capacity.add_str(", "); - add_template_capacity( - capacity, - output.js_templates[slot], - "$ret", - "$prepared", - None, - ); - slot += 1; - } - - capacity.add_str(", $retptr)"); - } - Some(Sret::Value(function)) => { - capacity.add(1); - capacity.add_str(indent); - capacity.add_str(function); - capacity.add_str("($ret, $retptr)"); - } - None => panic!("indirect output missing sret"), - } - } - - if catches_result { - capacity.add_str(output.js_catch); - } else if wrapped { - capacity.add_str("\n}"); - } -} - -const fn add_slot_name_capacity(capacity: &mut Capacity, name: &str) { - capacity.add_str(name); - capacity.add(2); -} - -const fn add_template_capacity( - capacity: &mut Capacity, - template: &str, - value: &str, - prepared: &str, - slots: Option<&str>, -) { - let mut maximum_replacement = if value.len() > prepared.len() { - value.len() - } else { - prepared.len() - }; - - if let Some(name) = slots { - let mut slot_len = name.len(); - slot_len = match slot_len.checked_add(2) { - Some(slot_len) => slot_len, - None => panic!("import section capacity overflows usize"), - }; - - if slot_len > maximum_replacement { - maximum_replacement = slot_len; - } - } - - // Literal bytes contribute at most `template.len()`. Every recognized - // placeholder is six bytes long, so at most `len / 6` are replaced. - capacity.add_str(template); - let Some(replacements) = - (template.len() / JS_TEMPLATE_PLACEHOLDERS[0].len()).checked_mul(maximum_replacement) - else { - panic!("import section capacity overflows usize"); - }; - capacity.add(replacements); -} - -impl ImportDescriptor { - pub(super) const fn write_js_record(&self, writer: &mut Writer) { - let Some(js) = self.js else { - return; - }; - - writer.write_u16(self.module.len()); - writer.write_str(self.module); - writer.write_u16(self.import.len()); - writer.write_str(self.import); - - assert!(js.required_embeds.len() <= u8::MAX as usize); - writer.write_byte(js.required_embeds.len().to_le_bytes()[0]); - - let mut embed = 0; - while embed < js.required_embeds.len() { - let (module, name) = js.required_embeds[embed]; - - writer.write_u16(module.len()); - writer.write_str(module); - writer.write_u16(name.len()); - writer.write_str(name); - embed += 1; - } - - self.write_js(writer, js); - } - - const fn write_js(&self, writer: &mut Writer, js: ImportJs) { - let wrapped = self.needs_js_shim(); - let await_output = self.awaits_suspending_output(); - - if self.suspending { - writer.write_str("new WebAssembly.Suspending("); - } - - if wrapped { - if await_output { - writer.write_str("async "); - } - writer.write_byte(b'('); - - if let Some(output) = self.output - && !output.direct - { - writer.write_str("$retptr"); - - if !self.inputs.is_empty() { - writer.write_str(", "); - } - } - - write_input_parameters(writer, self.inputs); - writer.write_str(") => {\n"); - } else if js.direct_wrapper { - writer.write_byte(b'('); - write_input_parameters(writer, self.inputs); - writer.write_str(") => "); - } - - let mut input = 0; - while input < self.inputs.len() { - write_input_conversion(writer, &self.inputs[input]); - input += 1; - } - - match self.output { - Some(output) => write_output(writer, output, js, wrapped, await_output), - None => { - if wrapped { - writer.write_str(" "); - if self.suspending { - writer.write_str("return "); - } - writer.write_str(js.indirect_call); - writer.write_str("\n}"); - } else { - writer.write_str(js.direct_call); - } - } - } - - if self.suspending { - writer.write_byte(b')'); - } - } -} - -const fn write_input_parameters( - writer: &mut Writer, - inputs: &[ImportInput], -) { - let mut input = 0; - - while input < inputs.len() { - if input != 0 { - writer.write_str(", "); - } - - write_input_parameter(writer, &inputs[input]); - input += 1; - } -} - -const fn write_input_parameter(writer: &mut Writer, input: &ImportInput) { - write_slot_name(writer, input.name, 0); - - let mut slot = 1; - while slot < input.ty.slots.len() { - if !input.ty.slots[slot].abi.is_empty() { - writer.write_str(", "); - write_slot_name(writer, input.name, slot); - } - - slot += 1; - } -} - -const fn write_input_conversion(writer: &mut Writer, input: &ImportInput) { - if !input.ty.has_js_conversion { - return; - } - - writer.write_str(" "); - write_slot_name(writer, input.name, 0); - writer.write_str(" = "); - write_template(writer, input.ty.js_template, "", "", Some(input.name)); - writer.write_byte(b'\n'); -} - -const fn write_output( - writer: &mut Writer, - output: &ImportOutput, - js: ImportJs, - wrapped: bool, - await_output: bool, -) { - let convert_direct = output.direct && output.has_js_conversion; - let catches_result = !output.js_try.is_empty(); - let call = if wrapped { - js.indirect_call - } else { - js.direct_call - }; - let template_value = if output.direct && !convert_direct { - call - } else { - "$ret" - }; - let indent = if catches_result { " " } else { " " }; - - if !output.direct && !JS_RETPTR_CONV.is_empty() { - writer.write_str(" $retptr = "); - writer.write_str(JS_RETPTR_CONV); - writer.write_byte(b'\n'); - } - - writer.write_str(output.js_try); - - if convert_direct { - writer.write_str(indent); - writer.write_str("const $ret = "); - } else if output.direct { - if catches_result { - writer.write_str(" return "); - } else if wrapped { - writer.write_str(" return "); - } - } else { - writer.write_str(indent); - writer.write_str("const $ret = "); - } - - if output.direct && !convert_direct { - if await_output { - writer.write_str("await ("); - } - write_template(writer, output.js_templates[0], template_value, "", None); - } else { - if await_output { - writer.write_str("await ("); - } - writer.write_str(call); - } - if await_output { - writer.write_byte(b')'); - } - - if convert_direct { - if !output.js_prepare.is_empty() { - writer.write_byte(b'\n'); - writer.write_str(indent); - writer.write_str("const $prepared = "); - write_template(writer, output.js_prepare, "$ret", "", None); - } - - writer.write_byte(b'\n'); - writer.write_str(indent); - writer.write_str("return "); - write_template(writer, output.js_templates[0], "$ret", "$prepared", None); - } - - if !output.direct { - match output.js_sret { - Some(Sret::Slots(function)) => { - if !output.js_prepare.is_empty() { - writer.write_byte(b'\n'); - writer.write_str(indent); - writer.write_str("const $prepared = "); - write_template(writer, output.js_prepare, "$ret", "", None); - } - - writer.write_byte(b'\n'); - writer.write_str(indent); - writer.write_str(function); - writer.write_byte(b'('); - write_template(writer, output.js_templates[0], "$ret", "$prepared", None); - - let mut slot = 1; - while slot < output.js_templates.len() { - if template_len(output.js_templates[slot], "$ret", "$prepared", None) != 0 { - writer.write_str(", "); - write_template( - writer, - output.js_templates[slot], - "$ret", - "$prepared", - None, - ); - } - - slot += 1; - } - - writer.write_str(", $retptr)"); - } - Some(Sret::Value(function)) => { - writer.write_byte(b'\n'); - writer.write_str(indent); - writer.write_str(function); - writer.write_str("($ret, $retptr)"); - } - None => panic!("indirect output missing sret"), - } - } - - if catches_result { - writer.write_str(output.js_catch); - } else if wrapped { - writer.write_str("\n}"); - } -} - -const fn write_slot_name(writer: &mut Writer, name: &str, slot: usize) { - assert!(slot < 4); - writer.write_str(name); - writer.write_byte(b'_'); - writer.write_byte(b"0123"[slot]); -} - -const fn write_template( - writer: &mut Writer, - template: &str, - value: &str, - prepared: &str, - slots: Option<&str>, -) { - let bytes = template.as_bytes(); - let mut input = 0; - - while input < bytes.len() { - let placeholder = js_template_placeholder(bytes, input); - - if placeholder == 0 { - writer.write_str(value); - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else if placeholder == 1 { - writer.write_str(prepared); - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - if let Some(name) = slots { - write_slot_name(writer, name, placeholder - 2); - } - - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else { - let start = input; - input += 1; - - while input < bytes.len() && bytes[input] != b'$' { - input += 1; - } - - writer.write_str_range(template, start, input); - } - } -} - -const fn template_len(template: &str, value: &str, prepared: &str, slots: Option<&str>) -> usize { - let bytes = template.as_bytes(); - let mut input = 0; - let mut output = 0; - - while input < bytes.len() { - let placeholder = js_template_placeholder(bytes, input); - - if placeholder == 0 { - output += value.len(); - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else if placeholder == 1 { - output += prepared.len(); - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - if let Some(name) = slots { - output += name.len() + 2; - } - - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else { - let start = input; - input += 1; - - while input < bytes.len() && bytes[input] != b'$' { - input += 1; - } - - output += input - start; - } - } - - output -} diff --git a/client/js-sys/src/macro/import/wat.rs b/client/js-sys/src/macro/import/wat.rs deleted file mode 100644 index 38e1d651..00000000 --- a/client/js-sys/src/macro/import/wat.rs +++ /dev/null @@ -1,719 +0,0 @@ -use super::{Capacity, ImportDescriptor, ImportOutput, WatInputCapacity, Writer}; -use crate::r#macro::WatSlot; -use crate::r#macro::wat::{wat_line_end, wat_lines_equal}; - -pub(super) const fn descriptor_capacity(descriptor: &ImportDescriptor) -> usize { - let mut capacity = Capacity::new(); - - capacity.add_str("(import \""); - capacity.add_str(descriptor.module); - capacity.add_str("\" \""); - capacity.add_str(descriptor.import); - capacity.add_str("\" (func $"); - add_import_name_capacity(&mut capacity, descriptor); - capacity.add_str(" (@sym (name \""); - add_import_name_capacity(&mut capacity, descriptor); - capacity.add_str("\"))"); - - if !input_types_are_empty(descriptor.inputs) { - capacity.add_str(" (param "); - capacity.add(descriptor.inputs.len() - 1); - capacity.add(1); - } - - capacity.add_str("))"); - capacity.add_str("\n(func $"); - capacity.add_str(descriptor.shim); - capacity.add_str(" (@sym)"); - - let mut input = 0; - while input < descriptor.inputs.len() { - let argument = &descriptor.inputs[input]; - capacity.add(argument.ty.wat_capacity.fixed); - capacity.add_repeated_str(argument.name, argument.ty.wat_capacity.name_uses); - input += 1; - } - - if let Some(output) = descriptor.output { - capacity.add(output.wat_capacity); - } - - capacity.add_str("\n call $"); - add_import_name_capacity(&mut capacity, descriptor); - capacity.add_str(" (@reloc)"); - capacity.add_str("\n)"); - capacity.get() -} - -pub(super) const fn input_capacity(slots: &[WatSlot; 4]) -> WatInputCapacity { - let mut capacity = Capacity::new(); - let mut name_uses = 0; - let mut slot = 0; - let mut wrote_type = false; - let mut wrote_get = false; - - while slot < slots.len() { - let descriptor = &slots[slot]; - - if !descriptor.boundary.is_empty() { - if wrote_type { - capacity.add(1); - } - capacity.add_str(descriptor.boundary); - wrote_type = true; - } - - if !descriptor.abi.is_empty() { - capacity.add_str(" (param $"); - capacity.add_str(slot_suffix(slot)); - capacity.add_str(descriptor.abi); - capacity.add(1); - name_uses += 1; - - if wrote_get { - capacity.add(1); - } - capacity.add_str(" local.get $"); - capacity.add_str(get_suffix(slot)); - add_conversion_capacity(&mut capacity, descriptor.conv); - name_uses += 1; - wrote_get = true; - } - - capacity.add_wat_lines(descriptor.imports); - capacity.add_wat_lines(descriptor.locals); - slot += 1; - } - - // `write_wat` starts every input's local-get sequence on a new line, - // including a zero-slot input. - capacity.add(1); - WatInputCapacity { - fixed: capacity.get(), - name_uses, - } -} - -pub(super) const fn output_capacity(output: &ImportOutput) -> usize { - let mut capacity = Capacity::new(); - - if output.direct { - capacity.add_str(" (result "); - capacity.add_str(output.slots[0].boundary); - capacity.add(1); - capacity.add_str(" (result "); - capacity.add_str(output.slots[0].abi); - capacity.add(1); - capacity.add_wat_lines(output.slots[0].imports); - capacity.add_wat_lines(output.slots[0].locals); - - if !output.slots[0].conv.is_empty() { - capacity.add_str("\n "); - capacity.add_str(output.slots[0].conv); - } - } else { - capacity.add_str(" (param $retptr "); - capacity.add_str(output.pointer.boundary); - capacity.add(1); - capacity.add_str(" (param $retptr "); - capacity.add_str(output.pointer.abi); - capacity.add(1); - capacity.add_wat_lines(output.pointer.locals); - capacity.add_str("\n local.get $retptr"); - add_conversion_capacity(&mut capacity, output.pointer.conv); - } - - capacity.add_wat_lines(output.wat_result_imports); - capacity.add_wat_lines(output.wat_result_locals); - capacity.add_str(output.wat_result_try); - capacity.add_str(output.wat_result_catch); - capacity.add_str(output.wat_result_default); - capacity.get() -} - -const fn add_import_name_capacity(capacity: &mut Capacity, descriptor: &ImportDescriptor) { - capacity.add_str(descriptor.module); - capacity.add_str(".import."); - capacity.add_str(descriptor.import); -} - -const fn add_conversion_capacity(capacity: &mut Capacity, conversion: &str) { - if !conversion.is_empty() { - capacity.add_str("\n "); - capacity.add_str(conversion); - } -} - -pub(super) const fn write_wat_support_imports( - writer: &mut Writer, - descriptors: &[ImportDescriptor], -) { - let mut descriptor_index = 0; - let mut source_index = 0; - - while descriptor_index < descriptors.len() { - let descriptor = &descriptors[descriptor_index]; - let source_count = descriptor.unique_source_count(); - - while source_index < source_count && descriptor.unique_source(true, source_index).is_empty() - { - source_index += 1; - } - - if source_index < source_count { - break; - } - - descriptor_index += 1; - source_index = 0; - } - - if descriptor_index == descriptors.len() { - return; - } - - let mut seen = [SeenLine::EMPTY; 32]; - let mut seen_len = 0; - - while descriptor_index < descriptors.len() { - let descriptor = &descriptors[descriptor_index]; - let source_count = descriptor.unique_source_count(); - - while source_index < source_count { - let source = descriptor.unique_source(true, source_index); - let bytes = source.as_bytes(); - let mut line_start = 0; - - while line_start < bytes.len() { - let line_end = wat_line_end(source, line_start); - - if line_end != line_start { - let mut was_seen = false; - let mut seen_index = 0; - - while seen_index < seen_len { - let candidate = seen[seen_index]; - - if wat_lines_equal( - source, - line_start, - line_end, - candidate.source, - candidate.start, - candidate.end, - ) { - was_seen = true; - break; - } - - seen_index += 1; - } - - if !was_seen && seen_len == seen.len() { - was_seen = previous_line_was_seen( - descriptors, - descriptor_index, - source_index, - line_start, - line_end, - ); - } - - if !was_seen { - writer.write_byte(b'\n'); - writer.write_str_range(source, line_start, line_end); - - if seen_len < seen.len() { - seen[seen_len] = SeenLine { - source, - start: line_start, - end: line_end, - }; - seen_len += 1; - } - } - } - - line_start = line_end + 1; - } - - source_index += 1; - } - - descriptor_index += 1; - source_index = 0; - } -} - -const fn previous_line_was_seen( - descriptors: &[ImportDescriptor], - descriptor_index: usize, - source_index: usize, - line_start: usize, - current_line_end: usize, -) -> bool { - let value = descriptors[descriptor_index].unique_source(true, source_index); - let mut candidate_descriptor_index = 0; - - while candidate_descriptor_index <= descriptor_index { - let descriptor = &descriptors[candidate_descriptor_index]; - let source_count = if candidate_descriptor_index == descriptor_index { - source_index + 1 - } else { - descriptor.unique_source_count() - }; - let mut candidate_source_index = 0; - - while candidate_source_index < source_count { - let candidate = descriptor.unique_source(true, candidate_source_index); - let limit = if candidate_descriptor_index == descriptor_index - && candidate_source_index == source_index - { - line_start - } else { - candidate.len() - }; - let mut candidate_start = 0; - - while candidate_start < limit { - let candidate_end = wat_line_end(candidate, candidate_start); - - if candidate_end != candidate_start - && wat_lines_equal( - value, - line_start, - current_line_end, - candidate, - candidate_start, - candidate_end, - ) { - return true; - } - - candidate_start = candidate_end + 1; - } - - candidate_source_index += 1; - } - - candidate_descriptor_index += 1; - } - - false -} - -impl ImportDescriptor { - pub(super) const fn write_wat_boundary_import( - &self, - writer: &mut Writer, - ) { - writer.write_str("(import \""); - writer.write_str(self.module); - writer.write_str("\" \""); - writer.write_str(self.import); - writer.write_str("\" (func $"); - self.write_import_name(writer); - writer.write_str(" (@sym (name \""); - self.write_import_name(writer); - writer.write_str("\"))"); - - if let Some(output) = self.output - && !output.direct - { - writer.write_str(" (param $retptr "); - writer.write_str(output.pointer.boundary); - writer.write_byte(b')'); - } - - if !input_types_are_empty(self.inputs) { - writer.write_str(" (param "); - write_input_types(writer, self.inputs); - writer.write_byte(b')'); - } - - if let Some(output) = self.output - && output.direct - { - writer.write_str(" (result "); - writer.write_str(output.slots[0].boundary); - writer.write_byte(b')'); - } - - writer.write_str("))"); - } - - pub(super) const fn write_wat_shim(&self, writer: &mut Writer) { - writer.write_str("\n(func $"); - writer.write_str(self.shim); - writer.write_str(" (@sym)"); - - if let Some(output) = self.output - && !output.direct - { - writer.write_str(" (param $retptr "); - writer.write_str(output.pointer.abi); - writer.write_byte(b')'); - } - - let mut input = 0; - while input < self.inputs.len() { - write_input_params( - writer, - self.inputs[input].name, - &self.inputs[input].ty.slots, - ); - input += 1; - } - - if let Some(output) = self.output - && output.direct - { - writer.write_str(" (result "); - writer.write_str(output.slots[0].abi); - writer.write_byte(b')'); - } - - self.write_unique_lines(writer, false); - - if let Some(output) = self.output { - writer.write_str(output.wat_result_try); - - if !output.direct { - writer.write_str("\n local.get $retptr"); - write_conversion(writer, output.pointer.conv); - } - } - - input = 0; - while input < self.inputs.len() { - writer.write_byte(b'\n'); - write_input_gets( - writer, - self.inputs[input].name, - &self.inputs[input].ty.slots, - ); - input += 1; - } - - writer.write_str("\n call $"); - self.write_import_name(writer); - writer.write_str(" (@reloc)"); - - if let Some(output) = self.output { - if output.direct && !output.slots[0].conv.is_empty() { - writer.write_str("\n "); - writer.write_str(output.slots[0].conv); - } - - writer.write_str(output.wat_result_catch); - writer.write_str(output.wat_result_default); - } - - writer.write_str("\n)"); - } - - const fn write_import_name(&self, writer: &mut Writer) { - writer.write_str(self.module); - writer.write_str(".import."); - writer.write_str(self.import); - } - - /// Writes the newline-delimited union used by the old `wat_imports!` and - /// `wat_locals!` macros. A line is emitted only at its first occurrence, - /// preserving both the original source order and its leading newline. - const fn write_unique_lines(&self, writer: &mut Writer, imports: bool) { - let source_count = self.unique_source_count(); - let mut source_index = 0; - - while source_index < source_count && self.unique_source(imports, source_index).is_empty() { - source_index += 1; - } - - if source_index == source_count { - return; - } - - let mut seen = [SeenLine::EMPTY; 8]; - let mut seen_len = 0; - - while source_index < source_count { - let source = self.unique_source(imports, source_index); - let bytes = source.as_bytes(); - let mut line_start = 0; - - while line_start < bytes.len() { - let line_end = wat_line_end(source, line_start); - - if line_end != line_start { - let mut was_seen = false; - let mut seen_index = 0; - - while seen_index < seen_len { - let candidate = seen[seen_index]; - - if wat_lines_equal( - source, - line_start, - line_end, - candidate.source, - candidate.start, - candidate.end, - ) { - was_seen = true; - break; - } - - seen_index += 1; - } - - if !was_seen && seen_len == seen.len() { - was_seen = self.line_was_seen(imports, source_index, line_start, line_end); - } - - if !was_seen { - writer.write_byte(b'\n'); - writer.write_str_range(source, line_start, line_end); - - if seen_len < seen.len() { - seen[seen_len] = SeenLine { - source, - start: line_start, - end: line_end, - }; - seen_len += 1; - } - } - } - - line_start = line_end + 1; - } - - source_index += 1; - } - } - - const fn unique_source_count(&self) -> usize { - self.inputs.len() * 4 + if self.output.is_some() { 2 } else { 0 } - } - - const fn unique_source(&self, imports: bool, index: usize) -> &'static str { - let input_sources = self.inputs.len() * 4; - - if index < input_sources { - let slot = &self.inputs[index / 4].ty.slots[index % 4]; - return if imports { slot.imports } else { slot.locals }; - } - - let Some(output) = self.output else { - return ""; - }; - - match index - input_sources { - 0 => output_source(output, imports), - 1 => { - if imports { - output.wat_result_imports - } else { - output.wat_result_locals - } - } - _ => "", - } - } - - const fn line_was_seen( - &self, - imports: bool, - source_index: usize, - line_start: usize, - current_line_end: usize, - ) -> bool { - let value = self.unique_source(imports, source_index); - let mut candidate_source_index = 0; - - while candidate_source_index <= source_index { - let candidate = self.unique_source(imports, candidate_source_index); - let limit = if candidate_source_index == source_index { - line_start - } else { - candidate.len() - }; - let mut candidate_start = 0; - - while candidate_start < limit { - let candidate_end = wat_line_end(candidate, candidate_start); - - if candidate_end != candidate_start - && wat_lines_equal( - value, - line_start, - current_line_end, - candidate, - candidate_start, - candidate_end, - ) { - return true; - } - - candidate_start = candidate_end + 1; - } - - candidate_source_index += 1; - } - - false - } -} - -#[derive(Clone, Copy)] -struct SeenLine { - source: &'static str, - start: usize, - end: usize, -} - -impl SeenLine { - const EMPTY: Self = Self { - source: "", - start: 0, - end: 0, - }; -} - -const fn output_source(output: &ImportOutput, imports: bool) -> &'static str { - if imports { - if output.direct { - output.slots[0].imports - } else { - "" - } - } else if output.direct { - output.slots[0].locals - } else { - output.pointer.locals - } -} - -const fn input_types_are_empty(inputs: &[super::ImportInput]) -> bool { - if inputs.is_empty() { - return true; - } - - // Input groups are separated by a space, so two or more arguments always - // produce a non-empty fragment. - if inputs.len() > 1 { - return false; - } - - let slots = &inputs[0].ty.slots; - slots[0].boundary.is_empty() - && slots[1].boundary.is_empty() - && slots[2].boundary.is_empty() - && slots[3].boundary.is_empty() -} - -const fn write_input_types( - writer: &mut Writer, - inputs: &[super::ImportInput], -) { - let mut input = 0; - - while input < inputs.len() { - if input != 0 { - writer.write_byte(b' '); - } - write_slot_types(writer, &inputs[input].ty.slots); - input += 1; - } -} - -const fn write_slot_types(writer: &mut Writer, slots: &[WatSlot; 4]) { - let mut slot = 0; - let mut wrote_type = false; - - while slot < slots.len() { - let r#type = slots[slot].boundary; - - if !r#type.is_empty() { - if wrote_type { - writer.write_byte(b' '); - } - writer.write_str(r#type); - wrote_type = true; - } - - slot += 1; - } -} - -const fn write_input_params( - writer: &mut Writer, - name: &str, - slots: &[WatSlot; 4], -) { - let mut slot = 0; - - while slot < slots.len() { - if !slots[slot].abi.is_empty() { - writer.write_str(" (param $"); - writer.write_str(name); - writer.write_str(slot_suffix(slot)); - writer.write_str(slots[slot].abi); - writer.write_byte(b')'); - } - - slot += 1; - } -} - -const fn write_input_gets( - writer: &mut Writer, - name: &str, - slots: &[WatSlot; 4], -) { - let mut slot = 0; - let mut wrote_get = false; - - while slot < slots.len() { - if !slots[slot].abi.is_empty() { - if wrote_get { - writer.write_byte(b'\n'); - } - - writer.write_str(" local.get $"); - writer.write_str(name); - writer.write_str(get_suffix(slot)); - write_conversion(writer, slots[slot].conv); - wrote_get = true; - } - - slot += 1; - } -} - -const fn write_conversion(writer: &mut Writer, conversion: &str) { - if !conversion.is_empty() { - writer.write_str("\n "); - writer.write_str(conversion); - } -} - -const fn slot_suffix(slot: usize) -> &'static str { - match slot { - 0 => "_0 ", - 1 => "_1 ", - 2 => "_2 ", - 3 => "_3 ", - _ => panic!("a Wasm ABI has exactly four slots"), - } -} - -const fn get_suffix(slot: usize) -> &'static str { - match slot { - 0 => "_0", - 1 => "_1", - 2 => "_2", - 3 => "_3", - _ => panic!("a Wasm ABI has exactly four slots"), - } -} diff --git a/client/js-sys/src/macro/result.rs b/client/js-sys/src/macro/result.rs deleted file mode 100644 index 8f8a612e..00000000 --- a/client/js-sys/src/macro/result.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::hazard::ReturnFromJS; -#[cfg(target_feature = "exception-handling")] -use crate::runtime::externref::{ - WAT_INDEX_LOCAL, WAT_INSERT_CONV, WAT_NEXT_IMPORT, WAT_TABLE_IMPORT, WAT_VALUE_LOCAL, -}; - -#[cfg(not(target_feature = "exception-handling"))] -const DIRECT_CATCH: &str = " - } catch ($error) { - const $index = this.#jsExports['js_sys.exception.store']() - this.#jsEmbed.js_sys['externref.table'].set($index, $error) - return false - } -}"; -#[cfg(not(target_feature = "exception-handling"))] -const INDIRECT_CATCH: &str = " - } catch ($error) { - const $index = this.#jsExports['js_sys.exception.store']() - this.#jsEmbed.js_sys['externref.table'].set($index, $error) - } -}"; - -#[cfg(target_feature = "exception-handling")] -const WAT_TAG_IMPORT: &str = "(import \"js_sys\" \"exception.tag\" (tag $js_sys.exception.tag \ - (@sym (name \"js_sys.exception.tag\")) (param externref)))"; -#[cfg(target_feature = "exception-handling")] -const WAT_STORE_IMPORT: &str = - "(import \"env\" \"js_sys.exception.store\" (func $js_sys.exception.store (@sym) (param i32)))"; -#[cfg(target_feature = "exception-handling")] -const WAT_IMPORTS: &str = crate::const_concat!( - WAT_TAG_IMPORT, - "\n", - WAT_TABLE_IMPORT, - "\n", - WAT_NEXT_IMPORT, - "\n", - WAT_STORE_IMPORT, -); -#[cfg(target_feature = "exception-handling")] -const WAT_LOCALS: &str = crate::const_concat!(WAT_VALUE_LOCAL, "\n", WAT_INDEX_LOCAL); -#[cfg(target_feature = "exception-handling")] -const WAT_CATCH: &str = crate::const_concat!( - "\n return", - "\n )", - "\n unreachable", - "\n )", - "\n ", - WAT_INSERT_CONV, - "\n call $js_sys.exception.store (@reloc)", -); - -#[must_use] -pub const fn js_result_try() -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - "" - } - - #[cfg(not(target_feature = "exception-handling"))] - { - if crate::r#macro::return_from_js_is_result::() { - " try {\n" - } else { - "" - } - } -} - -#[must_use] -pub const fn js_result_catch(direct: bool) -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - let _ = direct; - "" - } - - #[cfg(not(target_feature = "exception-handling"))] - { - if !crate::r#macro::return_from_js_is_result::() { - "" - } else if direct { - DIRECT_CATCH - } else { - INDIRECT_CATCH - } - } -} - -#[must_use] -pub const fn wat_result_imports() -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - if crate::r#macro::return_from_js_is_result::() { - WAT_IMPORTS - } else { - "" - } - } - - #[cfg(not(target_feature = "exception-handling"))] - { - "" - } -} - -#[must_use] -pub const fn wat_result_locals() -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - if crate::r#macro::return_from_js_is_result::() { - WAT_LOCALS - } else { - "" - } - } - - #[cfg(not(target_feature = "exception-handling"))] - { - "" - } -} - -#[must_use] -pub const fn wat_result_try() -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - if crate::r#macro::return_from_js_is_result::() { - "\n (block $js_sys.exception.catch (result externref)\n (try_table (catch \ - $js_sys.exception.tag $js_sys.exception.catch) (@reloc)" - } else { - "" - } - } - - #[cfg(not(target_feature = "exception-handling"))] - { - "" - } -} - -#[must_use] -pub const fn wat_result_catch() -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - if crate::r#macro::return_from_js_is_result::() { - WAT_CATCH - } else { - "" - } - } - - #[cfg(not(target_feature = "exception-handling"))] - { - "" - } -} - -#[must_use] -pub const fn wat_result_default() -> &'static str { - #[cfg(target_feature = "exception-handling")] - { - if !crate::r#macro::return_from_js_is_result::() - || !crate::r#macro::return_from_js_is_direct::() - { - return ""; - } - - match crate::r#macro::wat_direct::().as_bytes() { - b"i32" => "\n i32.const 0", - b"i64" => "\n i64.const 0", - b"f32" => "\n f32.const 0", - b"f64" => "\n f64.const 0", - _ => panic!("unsupported direct return type"), - } - } - - #[cfg(not(target_feature = "exception-handling"))] - { - "" - } -} diff --git a/client/js-sys/src/macro/text.rs b/client/js-sys/src/macro/text.rs deleted file mode 100644 index de80cc67..00000000 --- a/client/js-sys/src/macro/text.rs +++ /dev/null @@ -1,276 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! js_template { - ($template:expr, value = $value:expr $(,)?) => { - $crate::r#macro::js_template!(@render $template, [$value, "", "", "", "", ""]) - }; - ($template:expr, value = $value:expr, prepared = $prepared:expr $(,)?) => { - $crate::r#macro::js_template!(@render $template, [ - $value, $prepared, "", "", "", "" - ]) - }; - ($template:expr, slots = $slots:expr $(,)?) => {{ - const JS_TEMPLATE_SLOTS: [&::core::primitive::str; 4] = $slots; - $crate::r#macro::js_template!(@render $template, [ - "", - "", - JS_TEMPLATE_SLOTS[0], - JS_TEMPLATE_SLOTS[1], - JS_TEMPLATE_SLOTS[2], - JS_TEMPLATE_SLOTS[3], - ]) - }}; - (@render $template:expr, [$value:expr, $prepared:expr, $slot1:expr, $slot2:expr, $slot3:expr, $slot4:expr $(,)?]) => {{ - const JS_TEMPLATE_REPLACEMENTS: [&::core::primitive::str; 6] = - [$value, $prepared, $slot1, $slot2, $slot3, $slot4]; - const JS_TEMPLATE_LEN: ::core::primitive::usize = $crate::r#macro::js_template_len( - $template, - &JS_TEMPLATE_REPLACEMENTS, - ); - const JS_TEMPLATE_VALUE: [::core::primitive::u8; JS_TEMPLATE_LEN] = - $crate::r#macro::render_js_template::( - $template, - &JS_TEMPLATE_REPLACEMENTS, - ); - - // SAFETY: Rendering only replaces complete ASCII placeholders with valid strings. - unsafe { ::core::str::from_utf8_unchecked(&JS_TEMPLATE_VALUE) } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! const_concat { - ($($value:expr),* $(,)?) => {{ - const VALUES: &[&::core::primitive::str] = &[$($value),*]; - const LEN: ::core::primitive::usize = $crate::r#macro::const_concat_len(VALUES); - const VALUE: [::core::primitive::u8; LEN] = - $crate::r#macro::render_concat::(VALUES); - - // SAFETY: Joining valid strings keeps the result valid. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! const_concat_if { - ($($condition:expr => [$($value:expr),* $(,)?]),* $(,)?) => {{ - const GROUPS: &[(::core::primitive::bool, &[&::core::primitive::str])] = &[ - $(($condition, &[$($value),*]),)* - ]; - const LEN: ::core::primitive::usize = - $crate::r#macro::const_concat_if_len(GROUPS); - const VALUE: [::core::primitive::u8; LEN] = - $crate::r#macro::render_concat_if::(GROUPS); - - // SAFETY: Joining valid strings keeps the result valid. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! const_integer_str { - ($value:expr $(,)?) => {{ - const INTEGER: $crate::js_bindgen::r#macro::ConstInteger<::core::primitive::usize> = - $crate::js_bindgen::r#macro::ConstInteger($value); - const LEN: ::core::primitive::usize = INTEGER.__jbg_len(); - const VALUE: [::core::primitive::u8; LEN] = INTEGER.__jbg_to_le_bytes::(); - - // SAFETY: The integer formatter only emits ASCII digits. - unsafe { ::core::str::from_utf8_unchecked(&VALUE) } - }}; -} - -#[must_use] -pub const fn separator_between(left: &str, right: &str) -> &'static str { - if left.is_empty() || right.is_empty() { - "" - } else { - ", " - } -} - -#[must_use] -pub const fn const_concat_len(values: &[&str]) -> usize { - let mut len = 0; - let mut index = 0; - - while index < values.len() { - len += values[index].len(); - index += 1; - } - - len -} - -#[must_use] -pub const fn const_concat_if_len(groups: &[(bool, &[&str])]) -> usize { - let mut len = 0; - let mut group_index = 0; - - while group_index < groups.len() { - if groups[group_index].0 { - let values = groups[group_index].1; - let mut value_index = 0; - - while value_index < values.len() { - len += values[value_index].len(); - value_index += 1; - } - } - - group_index += 1; - } - - len -} - -#[must_use] -pub const fn render_concat(values: &[&str]) -> [u8; LEN] { - let mut output = [0; LEN]; - let mut offset = 0; - let mut index = 0; - - while index < values.len() { - offset = append_str(&mut output, offset, values[index]); - index += 1; - } - - output -} - -#[must_use] -pub const fn render_concat_if(groups: &[(bool, &[&str])]) -> [u8; LEN] { - let mut output = [0; LEN]; - let mut offset = 0; - let mut group_index = 0; - - while group_index < groups.len() { - if groups[group_index].0 { - let values = groups[group_index].1; - let mut value_index = 0; - - while value_index < values.len() { - offset = append_str(&mut output, offset, values[value_index]); - value_index += 1; - } - } - - group_index += 1; - } - - output -} - -const fn append_str(output: &mut [u8; LEN], offset: usize, value: &str) -> usize { - let bytes = value.as_bytes(); - let Some(end) = offset.checked_add(bytes.len()) else { - panic!("string append overflows usize"); - }; - assert!(end <= LEN); - - // SAFETY: `end <= LEN` proves that the destination range is in bounds. - // The source is a valid string slice and cannot overlap the output array. - unsafe { - core::ptr::copy_nonoverlapping( - bytes.as_ptr(), - output.as_mut_ptr().add(offset), - bytes.len(), - ); - } - - end -} - -pub(super) const JS_TEMPLATE_PLACEHOLDERS: [&str; 6] = [ - "$value", - "$prepared", - "$slot1", - "$slot2", - "$slot3", - "$slot4", -]; - -pub(super) const fn js_template_placeholder(template: &[u8], index: usize) -> usize { - if template[index] != b'$' { - return JS_TEMPLATE_PLACEHOLDERS.len(); - } - - let mut placeholder = 0; - - while placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - let candidate = JS_TEMPLATE_PLACEHOLDERS[placeholder].as_bytes(); - - if index + candidate.len() <= template.len() { - let mut byte = 0; - let mut matches = true; - - while byte < candidate.len() { - if template[index + byte] != candidate[byte] { - matches = false; - break; - } - - byte += 1; - } - - if matches { - return placeholder; - } - } - - placeholder += 1; - } - - JS_TEMPLATE_PLACEHOLDERS.len() -} - -#[must_use] -pub const fn js_template_len(template: &str, replacements: &[&str; 6]) -> usize { - let template = template.as_bytes(); - let mut input = 0; - let mut output = 0; - - while input < template.len() { - let placeholder = js_template_placeholder(template, input); - - if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - output += replacements[placeholder].len(); - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else { - output += 1; - input += 1; - } - } - - output -} - -#[must_use] -pub const fn render_js_template( - template: &str, - replacements: &[&str; 6], -) -> [u8; LEN] { - let template = template.as_bytes(); - let mut rendered = [0; LEN]; - let mut input = 0; - let mut output = 0; - - while input < template.len() { - let placeholder = js_template_placeholder(template, input); - - if placeholder < JS_TEMPLATE_PLACEHOLDERS.len() { - output = append_str(&mut rendered, output, replacements[placeholder]); - - input += JS_TEMPLATE_PLACEHOLDERS[placeholder].len(); - } else { - rendered[output] = template[input]; - output += 1; - input += 1; - } - } - - rendered -} diff --git a/client/js-sys/src/macro/wat.rs b/client/js-sys/src/macro/wat.rs deleted file mode 100644 index 4a7ec087..00000000 --- a/client/js-sys/src/macro/wat.rs +++ /dev/null @@ -1,316 +0,0 @@ -#[must_use] -pub const fn wat_conv_prefix(value: &str) -> &'static str { - if value.is_empty() { "" } else { "\n " } -} - -#[must_use] -pub(super) const fn wat_line_end(value: &str, start: usize) -> usize { - let bytes = value.as_bytes(); - let mut end = start; - - while end < bytes.len() && bytes[end] != b'\n' { - end += 1; - } - - end -} - -pub(super) const fn wat_lines_equal( - left: &str, - left_start: usize, - left_end: usize, - right: &str, - right_start: usize, - right_end: usize, -) -> bool { - if left_end - left_start != right_end - right_start { - return false; - } - - let left = left.as_bytes(); - let right = right.as_bytes(); - let mut offset = 0; - - while left_start + offset < left_end { - if left[left_start + offset] != right[right_start + offset] { - return false; - } - - offset += 1; - } - - true -} - -const fn wat_line_was_seen( - values: &[&str], - value_index: usize, - line_start: usize, - line_end: usize, -) -> bool { - let value = values[value_index]; - let mut candidate_value_index = 0; - - while candidate_value_index <= value_index { - let candidate = values[candidate_value_index]; - let limit = if candidate_value_index == value_index { - line_start - } else { - candidate.len() - }; - let mut candidate_start = 0; - - while candidate_start < limit { - let candidate_end = wat_line_end(candidate, candidate_start); - - if candidate_end != candidate_start - && wat_lines_equal( - value, - line_start, - line_end, - candidate, - candidate_start, - candidate_end, - ) { - return true; - } - - candidate_start = candidate_end + 1; - } - - candidate_value_index += 1; - } - - false -} - -#[must_use] -pub const fn wat_unique_lines_len(values: &[&str]) -> usize { - let mut size = 0; - let mut value_index = 0; - - while value_index < values.len() { - let value = values[value_index]; - let mut line_start = 0; - - while line_start < value.len() { - let line_end = wat_line_end(value, line_start); - - if line_end != line_start - && !wat_line_was_seen(values, value_index, line_start, line_end) - { - size += 1 + line_end - line_start; - } - - line_start = line_end + 1; - } - - value_index += 1; - } - - size -} - -#[must_use] -pub const fn render_wat_unique_lines(values: &[&str]) -> [u8; SIZE] { - let mut output = [0; SIZE]; - let mut output_index = 0; - let mut value_index = 0; - - while value_index < values.len() { - let value = values[value_index]; - let bytes = value.as_bytes(); - let mut line_start = 0; - - while line_start < bytes.len() { - let line_end = wat_line_end(value, line_start); - - if line_end != line_start - && !wat_line_was_seen(values, value_index, line_start, line_end) - { - output[output_index] = b'\n'; - output_index += 1; - - let mut byte_index = line_start; - while byte_index < line_end { - output[output_index] = bytes[byte_index]; - output_index += 1; - byte_index += 1; - } - } - - line_start = line_end + 1; - } - - value_index += 1; - } - - output -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_unique_list { - ($($value:expr),* $(,)?) => {{ - const VALUES: &[&::core::primitive::str] = &[$($value),*]; - const SIZE: ::core::primitive::usize = - $crate::r#macro::wat_unique_lines_len(VALUES); - const OUTPUT: [::core::primitive::u8; SIZE] = - $crate::r#macro::render_wat_unique_lines(VALUES); - - if let ::core::result::Result::Ok(value) = ::core::str::from_utf8(&OUTPUT) { - value - } else { - ::core::panic!() - } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_imports { - ( - slots = [$($slots:expr),* $(,)?], - extras = [$($extra:expr),* $(,)?], - ) => { - $crate::r#macro::wat_unique_list!( - $( - ($slots)[0].imports, - ($slots)[1].imports, - ($slots)[2].imports, - ($slots)[3].imports, - )* - $($extra,)* - ) - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! wat_locals { - ( - slots = [$($slots:expr),* $(,)?], - extras = [$($extra:expr),* $(,)?], - ) => { - $crate::r#macro::wat_unique_list!( - $( - ($slots)[0].locals, - ($slots)[1].locals, - ($slots)[2].locals, - ($slots)[3].locals, - )* - $($extra,)* - ) - }; -} - -/// Renders the repeated parts of a four-slot `WasmAbi`. -#[doc(hidden)] -#[macro_export] -macro_rules! wat_slots { - (types, $slots:expr, $field:ident $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; - const SEP1: &::core::primitive::str = - if SLOTS[0].$field.is_empty() { "" } else { " " }; - const SEP2: &::core::primitive::str = - if SLOTS[0].$field.is_empty() && SLOTS[1].$field.is_empty() { - "" - } else { - " " - }; - const SEP3: &::core::primitive::str = - if SLOTS[0].$field.is_empty() - && SLOTS[1].$field.is_empty() - && SLOTS[2].$field.is_empty() - { - "" - } else { - " " - }; - - $crate::r#macro::const_concat_if!( - !SLOTS[0].$field.is_empty() => [SLOTS[0].$field], - !SLOTS[1].$field.is_empty() => [SEP1, SLOTS[1].$field], - !SLOTS[2].$field.is_empty() => [SEP2, SLOTS[2].$field], - !SLOTS[3].$field.is_empty() => [SEP3, SLOTS[3].$field], - ) - }}; - (grouped_param, $slots:expr, $field:ident $(,)?) => {{ - const TYPES: &::core::primitive::str = - $crate::r#macro::wat_slots!(types, $slots, $field); - - $crate::r#macro::const_concat_if!( - !TYPES.is_empty() => [" (param ", TYPES, ")"], - ) - }}; - (params, $par:literal, $slots:expr, $field:ident $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => [" (param $", $par, "_0 ", SLOTS[0].$field, ")"], - !SLOTS[1].abi.is_empty() => [" (param $", $par, "_1 ", SLOTS[1].$field, ")"], - !SLOTS[2].abi.is_empty() => [" (param $", $par, "_2 ", SLOTS[2].$field, ")"], - !SLOTS[3].abi.is_empty() => [" (param $", $par, "_3 ", SLOTS[3].$field, ")"], - ) - }}; - (export_gets, $par:literal, $slots:expr $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => [" local.get $", $par, "_0", $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], - !SLOTS[1].abi.is_empty() => [" local.get $", $par, "_1", $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], - !SLOTS[2].abi.is_empty() => [" local.get $", $par, "_2", $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], - !SLOTS[3].abi.is_empty() => [" local.get $", $par, "_3", $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], - ) - }}; - (loads, $ty:ty, $slots:expr $(,)?) => {{ - const SLOTS: [$crate::r#macro::WatSlot; 4] = $slots; - const OFFSET_0: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 0>() - ); - const OFFSET_1: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 1>() - ); - const OFFSET_2: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 2>() - ); - const OFFSET_3: &::core::primitive::str = $crate::r#macro::const_integer_str!( - $crate::r#macro::export_output_slot_offset::<$ty, 3>() - ); - - $crate::r#macro::const_concat_if!( - !SLOTS[0].abi.is_empty() => [" local.get $retptr\n ", SLOTS[0].abi, ".load offset=", OFFSET_0, $crate::r#macro::wat_conv_prefix(SLOTS[0].conv), SLOTS[0].conv, "\n"], - !SLOTS[1].abi.is_empty() => [" local.get $retptr\n ", SLOTS[1].abi, ".load offset=", OFFSET_1, $crate::r#macro::wat_conv_prefix(SLOTS[1].conv), SLOTS[1].conv, "\n"], - !SLOTS[2].abi.is_empty() => [" local.get $retptr\n ", SLOTS[2].abi, ".load offset=", OFFSET_2, $crate::r#macro::wat_conv_prefix(SLOTS[2].conv), SLOTS[2].conv, "\n"], - !SLOTS[3].abi.is_empty() => [" local.get $retptr\n ", SLOTS[3].abi, ".load offset=", OFFSET_3, $crate::r#macro::wat_conv_prefix(SLOTS[3].conv), SLOTS[3].conv, "\n"], - ) - }}; -} - -/// Renders input fragments for Rust exports. -#[doc(hidden)] -#[macro_export] -macro_rules! wat_input { - (export raw_param; $ty:ty $(,)?) => { - $crate::r#macro::wat_slots!( - grouped_param, - $crate::r#macro::from_js_wat_slots::<$ty>(), - abi, - ) - }; - (export params; $par:literal, $ty:ty $(,)?) => { - $crate::r#macro::wat_slots!( - params, - $par, - $crate::r#macro::from_js_wat_slots::<$ty>(), - boundary, - ) - }; - (export gets; $par:literal, $ty:ty $(,)?) => { - $crate::r#macro::wat_slots!( - export_gets, - $par, - $crate::r#macro::from_js_wat_slots::<$ty>(), - ) - }; -} diff --git a/client/js-sys/src/macro/writer.rs b/client/js-sys/src/macro/writer.rs deleted file mode 100644 index 95dbece9..00000000 --- a/client/js-sys/src/macro/writer.rs +++ /dev/null @@ -1,102 +0,0 @@ -/// A const writer that either counts bytes or renders into a fixed allocation. -pub(super) struct Writer { - bytes: [u8; LEN], - position: usize, -} - -impl Writer { - pub const fn new() -> Self { - Self { - bytes: [0; LEN], - position: 0, - } - } - - pub const fn len(&self) -> usize { - self.position - } - - pub const fn write_byte(&mut self, value: u8) { - if LEN != 0 { - assert!(self.position < LEN); - self.bytes[self.position] = value; - } - - self.position += 1; - } - - pub const fn write_str(&mut self, value: &str) { - let bytes = value.as_bytes(); - - if LEN != 0 { - assert!(bytes.len() <= LEN - self.position); - - // SAFETY: The assertion above proves that both ranges are valid and - // a string borrowed by the descriptor cannot overlap the output. - unsafe { - core::ptr::copy_nonoverlapping( - bytes.as_ptr(), - self.bytes.as_mut_ptr().add(self.position), - bytes.len(), - ); - } - } - - self.position += bytes.len(); - } - - pub const fn write_str_range(&mut self, value: &str, start: usize, end: usize) { - assert!(start <= end && end <= value.len()); - let len = end - start; - - if LEN != 0 { - assert!(len <= LEN - self.position); - - // SAFETY: Both assertions prove the source and destination ranges - // are valid, and they belong to different allocations. - unsafe { - core::ptr::copy_nonoverlapping( - value.as_ptr().add(start), - self.bytes.as_mut_ptr().add(self.position), - len, - ); - } - } - - self.position += len; - } - - pub const fn write_u16(&mut self, value: usize) { - assert!(value <= u16::MAX as usize); - let bytes = value.to_le_bytes(); - self.write_byte(bytes[0]); - self.write_byte(bytes[1]); - } - - pub const fn write_u32(&mut self, value: usize) { - assert!(value <= u32::MAX as usize); - let bytes = value.to_le_bytes(); - self.write_byte(bytes[0]); - self.write_byte(bytes[1]); - self.write_byte(bytes[2]); - self.write_byte(bytes[3]); - } - - pub const fn set_u32(&mut self, offset: usize, value: usize) { - assert!(value <= u32::MAX as usize); - - if LEN != 0 { - assert!(offset <= LEN - 4); - let bytes = value.to_le_bytes(); - self.bytes[offset] = bytes[0]; - self.bytes[offset + 1] = bytes[1]; - self.bytes[offset + 2] = bytes[2]; - self.bytes[offset + 3] = bytes[3]; - } - } - - pub const fn finish_padded(self) -> [u8; LEN] { - assert!(self.position <= LEN); - self.bytes - } -} diff --git a/client/js-sys/src/runtime/exception.rs b/client/js-sys/src/runtime/exception.rs index e5ae6d03..1238c074 100644 --- a/client/js-sys/src/runtime/exception.rs +++ b/client/js-sys/src/runtime/exception.rs @@ -1,8 +1,90 @@ use core::cell::Cell; -#[cfg(not(target_feature = "exception-handling"))] use super::externref; use crate::JsValue; +#[cfg(not(target_feature = "exception-handling"))] +use crate::hazard::{JsCatch, JsEmbed}; +#[cfg(target_feature = "exception-handling")] +use crate::hazard::{WatCatch, WatImport, WatImportKind, WatType}; +use js_bindgen_wire::WireImportCatch; + +#[cfg(not(target_feature = "exception-handling"))] +const JS_CATCH_EMBEDS: &[JsEmbed] = &[JsEmbed::new("js_sys", "externref.table")]; +#[cfg(not(target_feature = "exception-handling"))] +const JS_DIRECT_CATCH: &str = " + } catch ($error) { + const $index = this.#jsExports['js_sys.exception.store']() + this.#jsEmbed.js_sys['externref.table'].set($index, $error) + return false + } +}"; +#[cfg(not(target_feature = "exception-handling"))] +const JS_INDIRECT_CATCH: &str = " + } catch ($error) { + const $index = this.#jsExports['js_sys.exception.store']() + this.#jsEmbed.js_sys['externref.table'].set($index, $error) + } +}"; + +#[cfg(target_feature = "exception-handling")] +const WAT_TAG_IMPORT: WatImport = WatImport::new( + "js_sys", + "exception.tag", + "js_sys.exception.tag", + Some("js_sys.exception.tag"), + WatImportKind::Tag { + parameters: &[WatType::ExternRef], + }, +); +#[cfg(target_feature = "exception-handling")] +const WAT_STORE_IMPORT: WatImport = WatImport::new( + "env", + "js_sys.exception.store", + "js_sys.exception.store", + None, + WatImportKind::Function { + parameters: &[WatType::I32], + results: &[], + }, +); +#[cfg(target_feature = "exception-handling")] +const WAT_CATCH_IMPORTS: &[WatImport] = &[ + WAT_TAG_IMPORT, + externref::WAT_TABLE_IMPORT, + externref::WAT_NEXT_IMPORT, + WAT_STORE_IMPORT, +]; +#[cfg(target_feature = "exception-handling")] +const WAT_TRY: &str = " + (block $js_sys.exception.catch (result externref) + (try_table (catch $js_sys.exception.tag $js_sys.exception.catch) (@reloc)"; +#[cfg(target_feature = "exception-handling")] +const WAT_CATCH: &str = " + return + ) + unreachable + ) + local.set $js_sys.externref.value + call $js_sys.externref.next (@reloc) + local.tee $js_sys.externref.index + local.get $js_sys.externref.value + table.set $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + call $js_sys.exception.store (@reloc)"; + +#[cfg(not(target_feature = "exception-handling"))] +pub(crate) const IMPORT_CATCH: WireImportCatch = WireImportCatch::JavaScript(JsCatch::new( + JS_CATCH_EMBEDS, + JS_DIRECT_CATCH, + JS_INDIRECT_CATCH, +)); +#[cfg(target_feature = "exception-handling")] +pub(crate) const IMPORT_CATCH: WireImportCatch = WireImportCatch::Wasm(WatCatch::new( + WAT_CATCH_IMPORTS, + externref::WAT_INSERT_LOCALS, + WAT_TRY, + WAT_CATCH, +)); thread_local! { static EXCEPTION: Cell = const { Cell::new(0) }; diff --git a/client/js-sys/src/runtime/externref.rs b/client/js-sys/src/runtime/externref.rs index 962b84d3..79d44c19 100644 --- a/client/js-sys/src/runtime/externref.rs +++ b/client/js-sys/src/runtime/externref.rs @@ -5,24 +5,47 @@ use core::mem; use super::allocator; use super::panic::panic; use crate::JsValue; -use crate::hazard::JsCast; +use crate::hazard::{JsCast, RefType, WatImport, WatImportKind, WatIndexType, WatLocal, WatType}; use crate::util::{PtrConst, PtrLength}; -pub(crate) const WAT_TABLE_IMPORT: &str = "(import \"js_sys\" \"externref.table\" (table \ - $js_sys.import.externref.table (@sym (name \ - \"js_sys.externref.table\")) 2 externref))"; -pub(crate) const WAT_NEXT_IMPORT: &str = - "(import \"env\" \"js_sys.externref.next\" (func $js_sys.externref.next (@sym) (result i32)))"; -pub(crate) const WAT_RELEASE_IMPORT: &str = "(import \"env\" \"js_sys.externref.release\" (func \ - $js_sys.externref.release (@sym) (param i32)))"; -pub(crate) const WAT_VALUE_LOCAL: &str = " (local $js_sys.externref.value externref)"; -pub(crate) const WAT_INDEX_LOCAL: &str = " (local $js_sys.externref.index i32)"; -pub(crate) const WAT_INSERT_IMPORTS: &str = - crate::const_concat!(WAT_TABLE_IMPORT, "\n", WAT_NEXT_IMPORT); -pub(crate) const WAT_TAKE_IMPORTS: &str = - crate::const_concat!(WAT_TABLE_IMPORT, "\n", WAT_RELEASE_IMPORT); -pub(crate) const WAT_INSERT_LOCALS: &str = - crate::const_concat!(WAT_VALUE_LOCAL, "\n", WAT_INDEX_LOCAL); +pub(crate) const WAT_TABLE_IMPORT: WatImport = WatImport::new( + "js_sys", + "externref.table", + "js_sys.import.externref.table", + Some("js_sys.externref.table"), + WatImportKind::Table { + index_type: WatIndexType::I32, + minimum: 2, + maximum: None, + element: RefType::ExternRef, + }, +); +pub(crate) const WAT_NEXT_IMPORT: WatImport = WatImport::new( + "env", + "js_sys.externref.next", + "js_sys.externref.next", + None, + WatImportKind::Function { + parameters: &[], + results: &[WatType::I32], + }, +); +const WAT_RELEASE_IMPORT: WatImport = WatImport::new( + "env", + "js_sys.externref.release", + "js_sys.externref.release", + None, + WatImportKind::Function { + parameters: &[WatType::I32], + results: &[], + }, +); +const WAT_VALUE_LOCAL: WatLocal = WatLocal::new("js_sys.externref.value", WatType::ExternRef); +pub(crate) const WAT_INDEX_LOCAL: WatLocal = WatLocal::new("js_sys.externref.index", WatType::I32); +pub(crate) const WAT_TABLE_IMPORTS: &[WatImport] = &[WAT_TABLE_IMPORT]; +pub(crate) const WAT_INSERT_IMPORTS: &[WatImport] = &[WAT_TABLE_IMPORT, WAT_NEXT_IMPORT]; +pub(crate) const WAT_TAKE_IMPORTS: &[WatImport] = &[WAT_TABLE_IMPORT, WAT_RELEASE_IMPORT]; +pub(crate) const WAT_INSERT_LOCALS: &[WatLocal] = &[WAT_VALUE_LOCAL, WAT_INDEX_LOCAL]; pub(crate) const WAT_INSERT_CONV: &str = "\ local.set $js_sys.externref.value call $js_sys.externref.next (@reloc) diff --git a/client/js-sys/src/runtime/value.rs b/client/js-sys/src/runtime/value.rs index 9ece6803..a2537f41 100644 --- a/client/js-sys/src/runtime/value.rs +++ b/client/js-sys/src/runtime/value.rs @@ -4,11 +4,11 @@ use core::slice; use super::externref::{ WAT_GET_CONV, WAT_INDEX_LOCAL, WAT_INSERT_CONV, WAT_INSERT_IMPORTS, WAT_INSERT_LOCALS, - WAT_OPTIONAL_INSERT_CONV, WAT_TABLE_IMPORT, WAT_TAKE_CONV, WAT_TAKE_IMPORTS, release, + WAT_OPTIONAL_INSERT_CONV, WAT_TABLE_IMPORTS, WAT_TAKE_CONV, WAT_TAKE_IMPORTS, release, }; use crate::hazard::{ FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, OptionFromAbi, OptionIntoAbi, ReturnAbi, - ReturnMode, Slot, WatConv, + ReturnMode, Slot, WatConv, WatType, }; #[derive(Debug)] @@ -48,19 +48,19 @@ impl Default for JsValueAbi { // SAFETY: `JsValueAbi` transfers ownership of an `i32` table index across the // JS boundary. unsafe impl Slot for JsValueAbi { - const WAT_TYPE: &'static str = "i32"; - const INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: Some(WAT_TAKE_IMPORTS), - locals: Some(WAT_INDEX_LOCAL), - conv: WAT_TAKE_CONV, - r#type: "externref", - }); - const FROM_JS_WAT_CONV: Option = Some(WatConv { - imports: Some(WAT_INSERT_IMPORTS), - locals: Some(WAT_INSERT_LOCALS), - conv: WAT_INSERT_CONV, - r#type: "externref", - }); + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_TAKE_IMPORTS, + &[WAT_INDEX_LOCAL], + WAT_TAKE_CONV, + WatType::ExternRef, + )); + const FROM_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_INSERT_IMPORTS, + WAT_INSERT_LOCALS, + WAT_INSERT_CONV, + WatType::ExternRef, + )); } // SAFETY: A transparent `i32` carrier is returned directly. @@ -71,32 +71,32 @@ unsafe impl ReturnAbi for JsValueAbi { // SAFETY: `JsValueRefAbi` borrows an `externref` table entry for the duration // of the JS call. unsafe impl Slot for JsValueRefAbi { - const WAT_TYPE: &'static str = "i32"; - const INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: Some(WAT_TABLE_IMPORT), - locals: None, - conv: WAT_GET_CONV, - r#type: "externref", - }); + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_TABLE_IMPORTS, + &[], + WAT_GET_CONV, + WatType::ExternRef, + )); } // SAFETY: `OptionalJsValueAbi` is an `i32` table index. At the JS boundary, // null is represented by index zero and non-null `externref` values are // inserted into the `externref` table. unsafe impl Slot for OptionalJsValueAbi { - const WAT_TYPE: &'static str = "i32"; - const INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: Some(WAT_TAKE_IMPORTS), - locals: Some(WAT_INDEX_LOCAL), - conv: WAT_TAKE_CONV, - r#type: "externref", - }); - const FROM_JS_WAT_CONV: Option = Some(WatConv { - imports: Some(WAT_INSERT_IMPORTS), - locals: Some(WAT_INSERT_LOCALS), - conv: WAT_OPTIONAL_INSERT_CONV, - r#type: "externref", - }); + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_TAKE_IMPORTS, + &[WAT_INDEX_LOCAL], + WAT_TAKE_CONV, + WatType::ExternRef, + )); + const FROM_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_INSERT_IMPORTS, + WAT_INSERT_LOCALS, + WAT_OPTIONAL_INSERT_CONV, + WatType::ExternRef, + )); } // SAFETY: A transparent `i32` carrier is returned directly. diff --git a/client/js-sys/src/util.rs b/client/js-sys/src/util.rs index 10422a01..f99ee1e4 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -1,7 +1,7 @@ use core::marker::PhantomData; use core::mem::MaybeUninit; -use crate::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; +use crate::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv, WatType}; macro_rules! thread_local { ($($vis:vis static $name:ident: $ty:ty = $value:expr;)*) => { @@ -83,18 +83,14 @@ type JsPointerType = u32; #[cfg(target_arch = "wasm64")] type JsPointerType = f64; -pub(crate) const WAT_PTR_TYPE: &str = ::WAT_TYPE; +pub(crate) const WAT_PTR_TYPE: Option = ::WAT_TYPE; #[cfg(target_arch = "wasm32")] const PTR_INTO_JS_WAT_CONV: Option = None; #[cfg(target_arch = "wasm64")] -const PTR_INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: None, - locals: None, - conv: "f64.convert_i64_u", - r#type: "f64", -}); +const PTR_INTO_JS_WAT_CONV: Option = + Some(WatConv::new(&[], &[], "f64.convert_i64_u", WatType::F64)); // An aggregate conversion supplies its own JavaScript template, so the // pointer and length slots do not independently apply their `IntoJS` @@ -147,7 +143,7 @@ impl Default for PtrConst { // SAFETY: `PtrConst` is transparent over a native Wasm pointer. On `wasm64`, // the WAT shim converts it to `f64` without losing precision. unsafe impl Slot for PtrConst { - const WAT_TYPE: &'static str = WAT_PTR_TYPE; + const WAT_TYPE: Option = WAT_PTR_TYPE; const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; } @@ -188,7 +184,7 @@ impl PtrMut { // SAFETY: `PtrMut` is transparent over a native Wasm pointer. On `wasm64`, // the WAT shim converts it to `f64` without losing precision. unsafe impl Slot for PtrMut { - const WAT_TYPE: &'static str = WAT_PTR_TYPE; + const WAT_TYPE: Option = WAT_PTR_TYPE; const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; } @@ -244,7 +240,7 @@ impl Default for PtrLength { // SAFETY: `PtrLength` is transparent over `usize`. On `wasm64`, the WAT // shim converts it to `f64` without losing precision. unsafe impl Slot for PtrLength { - const WAT_TYPE: &'static str = WAT_PTR_TYPE; + const WAT_TYPE: Option = WAT_PTR_TYPE; const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; } diff --git a/client/js-sys/src/wire/export.rs b/client/js-sys/src/wire/export.rs new file mode 100644 index 00000000..4960079a --- /dev/null +++ b/client/js-sys/src/wire/export.rs @@ -0,0 +1,92 @@ +use crate::ClosureHeader; +use crate::hazard::{FromJS, ReturnAbi, ReturnIntoJS, Slot, WasmRet}; +use crate::wire::{ + FromJsSlot1, FromJsSlot2, FromJsSlot3, FromJsSlot4, ReturnSlot1, ReturnSlot2, ReturnSlot3, + ReturnSlot4, WireExport, WireExportInput, WireExportInputType, WireExportOutput, + WireExportOutputType, wat_slot, +}; + +trait MetadataFor: 'static { + const VALUE: &'static Self; +} + +impl MetadataFor for WireExportInputType { + const VALUE: &'static Self = &Self::new( + [ + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + ], + T::JS_CONV, + ); +} + +impl MetadataFor for WireExportOutputType { + const VALUE: &'static Self = &{ + let mode = ::MODE; + let result_layout = ::RESULT_LAYOUT; + let slots = [ + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + ]; + let (frame_size, slot_offsets) = if mode.is_direct() { + (0, [0; 4]) + } else { + // LLVM keeps the Wasm stack pointer 16-byte aligned. + let size = core::mem::size_of::>(); + ( + (size + 15) & !15, + [ + WasmRet::::slot_offset::<0>(), + WasmRet::::slot_offset::<1>(), + WasmRet::::slot_offset::<2>(), + WasmRet::::slot_offset::<3>(), + ], + ) + }; + + Self::new( + mode, + T::JS_CONV, + slots, + frame_size, + slot_offsets, + result_layout, + ) + }; +} + +/// Builds the wire descriptor for one exported argument. +#[doc(hidden)] +#[must_use] +pub const fn wire_export_input(name: &'static str) -> WireExportInput { + WireExportInput::new(name, >::VALUE) +} + +/// Builds the result reference for one JavaScript-facing Wasm export. +#[doc(hidden)] +#[must_use] +pub const fn wire_export_output() -> WireExportOutput { + WireExportOutput::new(>::VALUE) +} + +/// Builds a closure dispatcher export using the closure header's call shim. +#[doc(hidden)] +#[must_use] +pub const fn wire_closure_export( + module: &'static str, + name: &'static str, + inputs: &'static [WireExportInput], + output: Option, +) -> WireExport { + WireExport::new_closure( + module, + name, + ClosureHeader::call_shim_offset::(), + inputs, + output, + ) +} diff --git a/client/js-sys/src/wire/import.rs b/client/js-sys/src/wire/import.rs new file mode 100644 index 00000000..d3abc958 --- /dev/null +++ b/client/js-sys/src/wire/import.rs @@ -0,0 +1,66 @@ +use crate::hazard::{IntoJS, ReturnAbi, ReturnFromJS, Slot}; +use crate::util::PtrMut; +use crate::wire::{ + InputSlot1, InputSlot2, InputSlot3, InputSlot4, OutputSlot1, OutputSlot2, OutputSlot3, + OutputSlot4, WireImportCatch, WireImportInputType, WireImportOutputType, wat_slot, +}; + +trait MetadataFor: 'static { + const VALUE: &'static Self; +} + +impl MetadataFor for WireImportInputType { + const VALUE: &'static Self = &Self::new( + [ + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + wat_slot::>( as Slot>::INTO_JS_WAT_CONV), + ], + T::JS_CONV, + ); +} + +impl MetadataFor for WireImportOutputType { + const VALUE: &'static Self = &{ + Self::new( + ::MODE, + T::JS_CONV, + T::JS_SRET, + [ + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + wat_slot::>( as Slot>::FROM_JS_WAT_CONV), + ], + ) + }; +} + +/// Returns the shared wire descriptor for one imported argument type. +#[doc(hidden)] +#[must_use] +pub const fn wire_import_input_type() -> &'static WireImportInputType { + >::VALUE +} + +/// Returns the `ABI` data for an indirect import's return pointer. +#[doc(hidden)] +#[must_use] +pub const fn wire_import_retptr_type() -> &'static WireImportInputType { + >>::VALUE +} + +/// Returns the shared wire descriptor for one imported result type. +#[doc(hidden)] +#[must_use] +pub const fn wire_import_output_type() -> &'static WireImportOutputType { + >::VALUE +} + +/// Returns the exception lowering shared by imported `Result` types. +#[doc(hidden)] +#[must_use] +pub const fn wire_import_catch() -> WireImportCatch { + crate::runtime::exception::IMPORT_CATCH +} diff --git a/client/js-sys/src/wire/macro.rs b/client/js-sys/src/wire/macro.rs new file mode 100644 index 00000000..9ecd72be --- /dev/null +++ b/client/js-sys/src/wire/macro.rs @@ -0,0 +1,60 @@ +#[doc(hidden)] +#[macro_export] +macro_rules! const_concat { + ($($value:expr),* $(,)?) => {{ + const VALUES: &[&::core::primitive::str] = &[$($value),*]; + const LEN: ::core::primitive::usize = $crate::wire::const_concat_len(VALUES); + const VALUE: [::core::primitive::u8; LEN] = + $crate::wire::render_concat::(VALUES); + + // SAFETY: Joining valid strings keeps the result valid. + unsafe { ::core::str::from_utf8_unchecked(&VALUE) } + }}; +} + +#[must_use] +pub const fn const_concat_len(values: &[&str]) -> usize { + let mut len = 0; + let mut index = 0; + + while index < values.len() { + len += values[index].len(); + index += 1; + } + + len +} + +#[must_use] +pub const fn render_concat(values: &[&str]) -> [u8; LEN] { + let mut output = [0; LEN]; + let mut offset = 0; + let mut index = 0; + + while index < values.len() { + offset = append_str(&mut output, offset, values[index]); + index += 1; + } + + output +} + +const fn append_str(output: &mut [u8; LEN], offset: usize, value: &str) -> usize { + let bytes = value.as_bytes(); + let Some(end) = offset.checked_add(bytes.len()) else { + panic!("string append overflows usize"); + }; + assert!(end <= LEN); + + // SAFETY: `end <= LEN` proves that the destination range is in bounds. + // The source is a valid string slice and cannot overlap the output array. + unsafe { + core::ptr::copy_nonoverlapping( + bytes.as_ptr(), + output.as_mut_ptr().add(offset), + bytes.len(), + ); + } + + end +} diff --git a/client/js-sys/src/wire/mod.rs b/client/js-sys/src/wire/mod.rs new file mode 100644 index 00000000..6ae2033a --- /dev/null +++ b/client/js-sys/src/wire/mod.rs @@ -0,0 +1,123 @@ +mod export; +mod import; +mod r#macro; + +pub use export::*; +pub use import::*; +pub use js_bindgen_wire::abi::{JsCatch, JsEmbed, WatCatch}; +pub use js_bindgen_wire::{ + Wire, WireBlob, WireExport, WireExportInput, WireExportInputType, WireExportOutput, + WireExportOutputType, WireImport, WireImportBinding, WireImportCatch, WireImportInput, + WireImportInputType, WireImportOutput, WireImportOutputType, WireImportTypeTable, + wire_blob_len, +}; +pub use r#macro::*; + +// Text rendering. +pub use crate::const_concat; +use crate::hazard::{ + FromJS, IntoJS, ReturnFromJS, ReturnIntoJS, Slot, WasmAbi, WasmRet, WatConv, WatSlot, +}; + +pub(crate) const fn wat_slot(conversion: Option) -> Option { + match S::WAT_TYPE { + Some(abi) => Some(WatSlot::new(abi, conversion)), + None => None, + } +} + +use core::mem::MaybeUninit; + +// Rust `ABI` shims used by generated import and export functions. + +pub type InputSlot1 = <::Abi as WasmAbi>::Slot1; +pub type InputSlot2 = <::Abi as WasmAbi>::Slot2; +pub type InputSlot3 = <::Abi as WasmAbi>::Slot3; +pub type InputSlot4 = <::Abi as WasmAbi>::Slot4; + +pub type FromJsSlot1 = <::Abi as WasmAbi>::Slot1; +pub type FromJsSlot2 = <::Abi as WasmAbi>::Slot2; +pub type FromJsSlot3 = <::Abi as WasmAbi>::Slot3; +pub type FromJsSlot4 = <::Abi as WasmAbi>::Slot4; + +pub type OutputSlot1 = <::Abi as WasmAbi>::Slot1; +pub type OutputSlot2 = <::Abi as WasmAbi>::Slot2; +pub type OutputSlot3 = <::Abi as WasmAbi>::Slot3; +pub type OutputSlot4 = <::Abi as WasmAbi>::Slot4; +pub type OutputRet = MaybeUninit::Abi>>; + +pub type ReturnSlot1 = <::Abi as WasmAbi>::Slot1; +pub type ReturnSlot2 = <::Abi as WasmAbi>::Slot2; +pub type ReturnSlot3 = <::Abi as WasmAbi>::Slot3; +pub type ReturnSlot4 = <::Abi as WasmAbi>::Slot4; + +#[must_use] +#[inline] +pub fn split_input( + value: T, +) -> (InputSlot1, InputSlot2, InputSlot3, InputSlot4) { + WasmAbi::split(T::into_abi(value)) +} + +#[must_use] +#[inline] +pub fn join_from_js( + slot1: ::Slot1, + slot2: ::Slot2, + slot3: ::Slot3, + slot4: ::Slot4, +) -> T { + T::from_abi(T::Abi::join(slot1, slot2, slot3, slot4)) +} + +#[must_use] +#[inline] +pub fn return_to_js(value: T) -> WasmRet { + WasmRet::from_abi(T::into_return_abi(value)) +} + +/// Lowers a value through a different [`IntoJS`] implementation with the same +/// `ABI`. This is reserved for generated `#[js_sys(type = ...)]` overrides, +/// where `T` must also describe the value's WAT and JavaScript conversions. +/// +/// # Safety +/// +/// The value's lowering must have the semantics expected by `T`; sharing an +/// `ABI` alone does not make two [`IntoJS`] implementations interchangeable. +#[must_use] +#[inline] +pub unsafe fn split_input_as( + value: impl IntoJS, +) -> (InputSlot1, InputSlot2, InputSlot3, InputSlot4) { + WasmAbi::split(IntoJS::into_abi(value)) +} + +#[must_use] +#[inline] +pub fn join_output(value: OutputRet) -> T { + T::from_return_abi(value) +} + +/// Lifts a return value whose JavaScript conversion is described by another +/// type with the same `ABI`. +/// +/// # Safety +/// +/// The JavaScript value produced for `A` must have the semantics expected by +/// `T`; sharing an `ABI` alone does not make the conversions interchangeable. +#[must_use] +#[inline] +pub unsafe fn join_output_as(value: OutputRet) -> T +where + T: ReturnFromJS, + A: ReturnFromJS, +{ + const { + assert!( + T::JS_CONV.is_result() == A::JS_CONV.is_result(), + "return conversion overrides must preserve Result semantics", + ); + } + + T::from_return_abi(value) +} diff --git a/client/js-sys/tests/hazard.rs b/client/js-sys/tests/hazard.rs index a0e5c7bc..8df62c97 100644 --- a/client/js-sys/tests/hazard.rs +++ b/client/js-sys/tests/hazard.rs @@ -1,5 +1,5 @@ use js_bindgen_test::test; -use js_sys::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv}; +use js_sys::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv, WatType}; use js_sys::js_sys; js_bindgen::embed_js!( @@ -19,13 +19,9 @@ struct NumberSlot(u32); // SAFETY: `NumberSlot` is an i32 carrier converted to a JS Number on input. unsafe impl Slot for NumberSlot { - const WAT_TYPE: &'static str = "i32"; - const INTO_JS_WAT_CONV: Option = Some(WatConv { - imports: None, - locals: None, - conv: "f64.convert_i32_u", - r#type: "f64", - }); + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = + Some(WatConv::new(&[], &[], "f64.convert_i32_u", WatType::F64)); } struct Pair(u32, u32); diff --git a/client/wabii/src/lib.rs b/client/wabii/src/lib.rs index a69e7ac5..3c231162 100644 --- a/client/wabii/src/lib.rs +++ b/client/wabii/src/lib.rs @@ -5,22 +5,12 @@ macro_rules! include_wat { #[expect(unused, reason = "link_section")] const _: () = { const WAT: &[u8] = include_bytes!($path); - const USED: ::core::primitive::usize = WAT.len() + 4; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; N], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; N]); #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout<{ WAT.len() }> = Layout( - #[expect(clippy::cast_possible_truncation, reason = "link_section")] - ::core::primitive::u32::to_le_bytes(USED as ::core::primitive::u32), - #[expect(clippy::cast_possible_truncation, reason = "link_section")] - ::core::primitive::u32::to_le_bytes(USED as ::core::primitive::u32), #[expect(clippy::cast_possible_truncation, reason = "link_section")] ::core::primitive::u32::to_le_bytes(WAT.len() as ::core::primitive::u32), *include_bytes!($path), diff --git a/client/wabii/src/random.64.wat b/client/wabii/src/random.64.wat index 2ad5ef35..46f2be1d 100644 --- a/client/wabii/src/random.64.wat +++ b/client/wabii/src/random.64.wat @@ -2,10 +2,6 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" - ;; block capacity and used length: 393 - "\89\01\00\00" - "\89\01\00\00" - ;; wabii:random.atomics_fill ;; record length: 224 "\e0\00\00\00" diff --git a/client/wabii/src/random.wat b/client/wabii/src/random.wat index 14a56d9f..d29a8e52 100644 --- a/client/wabii/src/random.wat +++ b/client/wabii/src/random.wat @@ -2,10 +2,6 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" - ;; block capacity and used length: 313 - "\39\01\00\00" - "\39\01\00\00" - ;; wabii:random.atomics_fill ;; record length: 184 "\b8\00\00\00" diff --git a/client/wabii/src/stdio.64.wat b/client/wabii/src/stdio.64.wat index 86bc0934..b59e32e1 100644 --- a/client/wabii/src/stdio.64.wat +++ b/client/wabii/src/stdio.64.wat @@ -2,10 +2,6 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" - ;; block capacity and used length: 252 - "\fc\00\00\00" - "\fc\00\00\00" - ;; wabii:stdio.stdout ;; record length: 121 "\79\00\00\00" diff --git a/client/wabii/src/stdio.wat b/client/wabii/src/stdio.wat index 075df05a..4b6bb78f 100644 --- a/client/wabii/src/stdio.wat +++ b/client/wabii/src/stdio.wat @@ -2,10 +2,6 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" - ;; block capacity and used length: 252 - "\fc\00\00\00" - "\fc\00\00\00" - ;; wabii:stdio.stdout ;; record length: 121 "\79\00\00\00" diff --git a/client/wabii/src/time.wat b/client/wabii/src/time.wat index fd0eadc0..bee92347 100644 --- a/client/wabii/src/time.wat +++ b/client/wabii/src/time.wat @@ -2,10 +2,6 @@ ;; Do not edit by hand. (@custom "js_bindgen.import" - ;; block capacity and used length: 278 - "\16\01\00\00" - "\16\01\00\00" - ;; wabii:time.performance_now ;; record length: 64 "\40\00\00\00" diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs index 9a94352d..858a68f8 100644 --- a/client/web-sys/src/console.gen.rs +++ b/client/web-sys/src/console.gen.rs @@ -2,7 +2,7 @@ #![allow(warnings)] -use js_sys::r#macro; +use js_sys::wire; use js_sys::JsValue; use js_sys::hazard::JsCast; @@ -19,17 +19,15 @@ pub fn log(data: &[T]) { unsafe extern "C" { #[link_name = "web_sys.console.log"] fn log( - arg0_0: r#macro::InputSlot1<&[JsValue]>, - arg0_1: r#macro::InputSlot2<&[JsValue]>, - arg0_2: r#macro::InputSlot3<&[JsValue]>, - arg0_3: r#macro::InputSlot4<&[JsValue]>, + arg0_0: wire::InputSlot1<&[JsValue]>, + arg0_1: wire::InputSlot2<&[JsValue]>, + arg0_2: wire::InputSlot3<&[JsValue]>, + arg0_3: wire::InputSlot4<&[JsValue]>, ); } { - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { - r#macro::split_input_as::<&[JsValue]>(data) - }; + let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { wire::split_input_as::<&[JsValue]>(data) }; unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } }; } @@ -38,20 +36,20 @@ pub fn log2(data1: &JsValue, data2: &JsValue) { unsafe extern "C" { #[link_name = "web_sys.console.log2"] fn log2( - arg0_0: r#macro::InputSlot1<&JsValue>, - arg0_1: r#macro::InputSlot2<&JsValue>, - arg0_2: r#macro::InputSlot3<&JsValue>, - arg0_3: r#macro::InputSlot4<&JsValue>, - arg1_0: r#macro::InputSlot1<&JsValue>, - arg1_1: r#macro::InputSlot2<&JsValue>, - arg1_2: r#macro::InputSlot3<&JsValue>, - arg1_3: r#macro::InputSlot4<&JsValue>, + arg0_0: wire::InputSlot1<&JsValue>, + arg0_1: wire::InputSlot2<&JsValue>, + arg0_2: wire::InputSlot3<&JsValue>, + arg0_3: wire::InputSlot4<&JsValue>, + arg1_0: wire::InputSlot1<&JsValue>, + arg1_1: wire::InputSlot2<&JsValue>, + arg1_2: wire::InputSlot3<&JsValue>, + arg1_3: wire::InputSlot4<&JsValue>, ); } { - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(data1); - let (arg1_0, arg1_1, arg1_2, arg1_3) = r#macro::split_input::<&JsValue>(data2); + let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data1); + let (arg1_0, arg1_1, arg1_2, arg1_3) = wire::split_input::<&JsValue>(data2); unsafe { log2(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } }; } @@ -60,84 +58,123 @@ pub fn error(data: &JsValue) { unsafe extern "C" { #[link_name = "web_sys.console.error"] fn error( - arg0_0: r#macro::InputSlot1<&JsValue>, - arg0_1: r#macro::InputSlot2<&JsValue>, - arg0_2: r#macro::InputSlot3<&JsValue>, - arg0_3: r#macro::InputSlot4<&JsValue>, + arg0_0: wire::InputSlot1<&JsValue>, + arg0_1: wire::InputSlot2<&JsValue>, + arg0_2: wire::InputSlot3<&JsValue>, + arg0_3: wire::InputSlot4<&JsValue>, ); } { - let (arg0_0, arg0_1, arg0_2, arg0_3) = r#macro::split_input::<&JsValue>(data); + let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data); unsafe { error(arg0_0, arg0_1, arg0_2, arg0_3) } }; } + +pub fn error1(data: &JsValue) -> u128 { + unsafe extern "C" { + #[link_name = "web_sys.console.error1"] + fn error1( + arg0_0: wire::InputSlot1<&JsValue>, + arg0_1: wire::InputSlot2<&JsValue>, + arg0_2: wire::InputSlot3<&JsValue>, + arg0_3: wire::InputSlot4<&JsValue>, + ) -> wire::OutputRet; + } + + wire::join_output({ + let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data); + unsafe { error1(arg0_0, arg0_1, arg0_2, arg0_3) } + }) +} const _: () = { - static IMPORTS: &[r#macro::ImportDescriptor] = &[ - r#macro::ImportDescriptor::new( - "web_sys", - "console.log0", - "web_sys.console.log0", - &[], - ::core::option::Option::None, - ::core::option::Option::Some(r#macro::ImportJs { - direct_wrapper: true, - direct_call: "globalThis.console.log()", - indirect_call: "globalThis.console.log()", - required_embeds: &[], - }), - ), - r#macro::ImportDescriptor::new( - "web_sys", - "console.log", - "web_sys.console.log", - &[r#macro::import_input::<&[JsValue]>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(r#macro::ImportJs { - direct_wrapper: true, - direct_call: "globalThis.console.log(arg0_0)", - indirect_call: "globalThis.console.log(arg0_0)", - required_embeds: &[r#macro::js_input_embed::<&[JsValue]>()], - }), - ), - r#macro::ImportDescriptor::new( - "web_sys", - "console.log2", - "web_sys.console.log2", - &[r#macro::import_input::<&JsValue>("arg0"), r#macro::import_input::<&JsValue>("arg1")], - ::core::option::Option::None, - ::core::option::Option::Some(r#macro::ImportJs { - direct_wrapper: true, - direct_call: "globalThis.console.log(arg0_0, arg1_0)", - indirect_call: "globalThis.console.log(arg0_0, arg1_0)", - required_embeds: &[r#macro::js_input_embed::<&JsValue>()], - }), - ), - r#macro::ImportDescriptor::new( - "web_sys", - "console.error", - "web_sys.console.error", - &[r#macro::import_input::<&JsValue>("arg0")], - ::core::option::Option::None, - ::core::option::Option::Some(r#macro::ImportJs { - direct_wrapper: true, - direct_call: "globalThis.console.error(arg0_0)", - indirect_call: "globalThis.console.error(arg0_0)", - required_embeds: &[r#macro::js_input_embed::<&JsValue>()], - }), - ), - ]; - const WAT_CAPACITY: ::core::primitive::usize = r#macro::import_wat_capacity(IMPORTS); + const TABLE: &wire::WireImportTypeTable = &wire::WireImportTypeTable::new( + wire::wire_import_retptr_type(), + &[wire::wire_import_input_type::<&[JsValue]>(), wire::wire_import_input_type::<&JsValue>()], + &[wire::wire_import_output_type::()], + wire::wire_import_catch(), + ); + pub const WIRE: wire::Wire = wire::Wire::imports( + TABLE, + &[ + wire::WireImport::new( + "web_sys", + "console.log0", + &[], + ::core::option::Option::None, + ::core::option::Option::Some( + wire::WireImportBinding::new( + ::core::option::Option::None, + "globalThis.console.log()", + &[], + ), + ), + false, + ), + wire::WireImport::new( + "web_sys", + "console.log", + &[wire::WireImportInput::new("arg0", 0usize)], + ::core::option::Option::None, + ::core::option::Option::Some( + wire::WireImportBinding::new( + ::core::option::Option::None, + "globalThis.console.log(arg0_0)", + &[], + ), + ), + false, + ), + wire::WireImport::new( + "web_sys", + "console.log2", + &[ + wire::WireImportInput::new("arg0", 1usize), + wire::WireImportInput::new("arg1", 1usize), + ], + ::core::option::Option::None, + ::core::option::Option::Some( + wire::WireImportBinding::new( + ::core::option::Option::None, + "globalThis.console.log(arg0_0, arg1_0)", + &[], + ), + ), + false, + ), + wire::WireImport::new( + "web_sys", + "console.error", + &[wire::WireImportInput::new("arg0", 1usize)], + ::core::option::Option::None, + ::core::option::Option::Some( + wire::WireImportBinding::new( + ::core::option::Option::None, + "globalThis.console.error(arg0_0)", + &[], + ), + ), + false, + ), + wire::WireImport::new( + "web_sys", + "console.error1", + &[wire::WireImportInput::new("arg0", 1usize)], + ::core::option::Option::Some(wire::WireImportOutput::new(0usize)), + ::core::option::Option::Some( + wire::WireImportBinding::new( + ::core::option::Option::None, + "globalThis.console.error1(arg0_0)", + &[], + ), + ), + false, + ), + ], + ); + pub const LEN: ::core::primitive::usize = wire::wire_blob_len(&WIRE); #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: r#macro::ImportSection = r#macro::import_wat::< - WAT_CAPACITY, - >(IMPORTS); - const JS_CAPACITY: ::core::primitive::usize = r#macro::import_js_capacity(IMPORTS); - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: r#macro::ImportSection = r#macro::import_js::< - JS_CAPACITY, - >(IMPORTS); + #[unsafe(link_section = "js_bindgen.wire")] + pub static WIRE_SECTION: wire::WireBlob = wire::WireBlob::new(&WIRE); }; diff --git a/client/web-sys/src/console.js-sys.rs b/client/web-sys/src/console.js-sys.rs index 278353b6..97ea2252 100644 --- a/client/web-sys/src/console.js-sys.rs +++ b/client/web-sys/src/console.js-sys.rs @@ -12,4 +12,6 @@ extern "js-sys" { pub fn log2(data1: &JsValue, data2: &JsValue); pub fn error(data: &JsValue); + + pub fn error1(data: &JsValue) -> u128; } diff --git a/host/Cargo.toml b/host/Cargo.toml index d7d60cee..086f5cd1 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -18,6 +18,7 @@ members = [ "test-macro", "wasm-ld-opt", "web-driver", + "wire", ] default-members = ["dev"] @@ -56,6 +57,7 @@ itertools = { version = "0.15", default-features = false } js-bindgen-cli-lib = { path = "cli-lib" } js-bindgen-ld-shared = { path = "ld-shared" } js-bindgen-shared = { path = "shared" } +js-bindgen-wire = { version = "0.1.0", path = "wire" } js-sys-bindgen = { path = "js-sys-bindgen" } memmap2 = "0.9" mime = "0.3" diff --git a/host/dev/src/codegen.rs b/host/dev/src/codegen.rs index 7db181d3..054e3858 100644 --- a/host/dev/src/codegen.rs +++ b/host/dev/src/codegen.rs @@ -68,31 +68,19 @@ fn generate_wat(spec: &Spec) -> String { `.\n;; Do not edit by hand.\n\n", ); - format_section(&mut output, "js_bindgen.import", &spec.imports, true); - format_section(&mut output, "js_bindgen.embed", &spec.embeds, false); + format_section(&mut output, "js_bindgen.import", &spec.imports); + format_section(&mut output, "js_bindgen.embed", &spec.embeds); output } -fn format_section(output: &mut String, section: &str, entries: &[JsEntry], framed: bool) { +fn format_section(output: &mut String, section: &str, entries: &[JsEntry]) { if entries.is_empty() { return; } writeln!(output, "(@custom {section:?}").unwrap(); - if framed { - let used = entries.iter().fold(0u32, |used, entry| { - used.checked_add(4) - .and_then(|used| used.checked_add(record_len(entry))) - .expect("JS section is too large") - }); - writeln!(output, " ;; block capacity and used length: {used}").unwrap(); - write_binary_string(output, &used.to_le_bytes()); - write_binary_string(output, &used.to_le_bytes()); - writeln!(output).unwrap(); - } - for entry in entries { format_entry(output, entry); } diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index d5149873..8933df88 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -62,16 +62,15 @@ pub(crate) fn closure_with( raw_inputs, join_inputs, arguments, - codegen_inputs, - mut required_embeds, + mut wire_inputs, raw_output, - output_argument, + wire_output, } = lower_abi(inputs.iter().copied(), output, &js_sys)?; - let mut js_codegen_inputs = vec![quote_spanned!(span=> ("data", ::core::primitive::usize))]; - js_codegen_inputs.extend(codegen_inputs.iter().cloned()); - required_embeds.insert( + wire_inputs.insert( 0, - quote_spanned!(span=> #js_sys::r#macro::js_from_embed::<::core::primitive::usize>()), + quote_spanned! {span=> + #js_sys::wire::wire_export_input::<::core::primitive::usize>("data") + }, ); let closure_bound = if signature.kind == ClosureKind::Shared { if let Some(output) = output { @@ -104,7 +103,7 @@ pub(crate) fn closure_with( let call_body = if output.is_some() { quote_spanned! {span=> #(#join_inputs)* - #js_sys::r#macro::return_to_js({ + #js_sys::wire::return_to_js({ #callback_call }) } @@ -159,29 +158,23 @@ pub(crate) fn closure_with( ) } - #js_sys::js_bindgen::unsafe_global_wat! { - "{}", - interpolate #js_sys::r#macro::wat_closure!( - #call_name, - CallShim, - (#(#codegen_inputs),*) - #output_argument, - ), - } + #[expect(dead_code, reason = "stored in a custom section")] + pub const WIRE: #js_sys::wire::Wire = + #js_sys::wire::Wire::exports(&[ + #js_sys::wire::wire_closure_export::( + #crate_name, + #call_name, + &[#(#wire_inputs),*], + #wire_output, + ), + ]); + #[expect(dead_code, reason = "stored in a custom section")] + pub const LEN: ::core::primitive::usize = #js_sys::wire::wire_blob_len(&WIRE); - #js_sys::js_bindgen::export_js! { - module = #crate_name, - name = #call_name, - required_embeds = [ - #(#required_embeds),* - ], - "{}", - interpolate #js_sys::r#macro::js_export!( - #call_name, - (#(#js_codegen_inputs),*) - #output_argument, - ), - } + #[expect(dead_code, reason = "stored in a custom section")] + #[unsafe(link_section = "js_bindgen.wire")] + pub static WIRE_SECTION: #js_sys::wire::WireBlob = + #js_sys::wire::WireBlob::new(&WIRE); #js_sys::js_bindgen::embed_js! { module = #crate_name, diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 4ea310cf..c3b59e39 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -52,8 +52,7 @@ pub(crate) fn r#macro( let span = function.span(); let js_sys: Path = js_sys.unwrap_or_else(|| parse_quote!(::js_sys)); - let js_bindgen_path: Path = parse_quote!(#js_sys::js_bindgen); - let macro_path: Path = parse_quote!(#js_sys::r#macro); + let macro_path: Path = parse_quote!(#js_sys::wire); let ident = &function.sig.ident; let export_name = js_name.map_or_else( || { @@ -84,10 +83,9 @@ pub(crate) fn r#macro( raw_inputs, join_inputs, arguments, - codegen_inputs, - required_embeds, + wire_inputs, raw_output, - output_argument, + wire_output, .. } = lower_abi(inputs, output_ty, &js_sys)?; @@ -108,10 +106,10 @@ pub(crate) fn r#macro( #call; } }; - let js_export = if promising { - quote_spanned!(span=> #macro_path::js_export_promising!) + let descriptor_constructor = if promising { + format_ident!("new_symbol_promising", span = span) } else { - quote_spanned!(span=> #macro_path::js_export!) + format_ident!("new_symbol", span = span) }; Ok(quote_spanned! {span=> @@ -125,29 +123,24 @@ pub(crate) fn r#macro( #raw_body } - #js_bindgen_path::unsafe_global_wat! { - "{}", - interpolate #macro_path::wat_export!( - #raw_export_name, - #export_name, - (#(#codegen_inputs),*) - #output_argument, - ), - } + #[expect(dead_code, reason = "stored in a custom section")] + pub const WIRE: #macro_path::Wire = + #macro_path::Wire::exports(&[ + #macro_path::WireExport::#descriptor_constructor( + #crate_name, + #export_name, + #raw_export_name, + &[#(#wire_inputs),*], + #wire_output, + ), + ]); + #[expect(dead_code, reason = "stored in a custom section")] + pub const LEN: ::core::primitive::usize = #macro_path::wire_blob_len(&WIRE); - #js_bindgen_path::export_js! { - module = #crate_name, - name = #export_name, - required_embeds = [ - #(#required_embeds),* - ], - "{}", - interpolate #js_export( - #export_name, - (#(#codegen_inputs),*) - #output_argument, - ), - } + #[expect(dead_code, reason = "stored in a custom section")] + #[unsafe(link_section = "js_bindgen.wire")] + pub static WIRE_SECTION: #macro_path::WireBlob = + #macro_path::WireBlob::new(&WIRE); }; }) } @@ -157,10 +150,9 @@ pub(crate) struct ExportAbi { pub raw_inputs: Vec, pub join_inputs: Vec, pub arguments: Vec, - pub codegen_inputs: Vec, - pub required_embeds: Vec, + pub wire_inputs: Vec, pub raw_output: TokenStream, - pub output_argument: TokenStream, + pub wire_output: TokenStream, } pub(crate) fn lower_abi<'a>( @@ -172,8 +164,7 @@ pub(crate) fn lower_abi<'a>( let mut raw_inputs = Vec::new(); let mut join_inputs = Vec::new(); let mut arguments = Vec::new(); - let mut codegen_inputs = Vec::new(); - let mut required_embeds = Vec::new(); + let mut wire_inputs = Vec::new(); for (index, ty) in inputs.into_iter().enumerate() { let span = ty.span(); @@ -203,7 +194,7 @@ pub(crate) fn lower_abi<'a>( for slot in 1_usize..=4 { let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = span); let slot_alias = format_ident!("FromJsSlot{slot}", span = span); - let raw_type = quote_spanned!(span=> #js_sys::r#macro::#slot_alias<#js_ty>); + let raw_type = quote_spanned!(span=> #js_sys::wire::#slot_alias<#js_ty>); raw_types.push(raw_type.clone()); raw_inputs.push(quote_spanned!(span=> #slot_ident: #raw_type)); @@ -215,22 +206,28 @@ pub(crate) fn lower_abi<'a>( let ty = &reference.elem; join_inputs.push(quote_spanned! {span=> - let #anchor = #js_sys::r#macro::join_from_js::<#js_ty>(#(#slots),*); + let #anchor = #js_sys::wire::join_from_js::<#js_ty>(#(#slots),*); let #argument = ::core::borrow::Borrow::<#ty>::borrow(&#anchor); }); } else { join_inputs.push(quote_spanned! {span=> - let #argument = #js_sys::r#macro::join_from_js::<#js_ty>(#(#slots),*); + let #argument = #js_sys::wire::join_from_js::<#js_ty>(#(#slots),*); }); } - codegen_inputs.push(quote_spanned!(span=> (#parameter, #js_ty))); - required_embeds.push(quote_spanned!(span=> #js_sys::r#macro::js_from_embed::<#js_ty>())); + wire_inputs.push(quote_spanned! {span=> + #js_sys::wire::wire_export_input::<#js_ty>(#parameter) + }); arguments.push(argument); } - let (raw_output, output_argument) = output.map_or_else( - || (TokenStream::new(), TokenStream::new()), + let (raw_output, wire_output) = output.map_or_else( + || { + ( + TokenStream::new(), + quote_spanned!(js_sys.span()=> ::core::option::Option::None), + ) + }, |output| { ( quote_spanned! {output.span()=> @@ -238,25 +235,23 @@ pub(crate) fn lower_abi<'a>( <#output as #js_sys::hazard::ReturnIntoJS>::Abi > }, - quote_spanned!(output.span()=> , #output), + quote_spanned! {output.span()=> + ::core::option::Option::Some( + #js_sys::wire::wire_export_output::<#output>() + ) + }, ) }, ); - if let Some(output) = output { - required_embeds - .push(quote_spanned!(output.span()=> #js_sys::r#macro::js_return_embed::<#output>())); - } - Ok(ExportAbi { raw_types, raw_inputs, join_inputs, arguments, - codegen_inputs, - required_embeds, + wire_inputs, raw_output, - output_argument, + wire_output, }) } diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index b2e10abc..5d629d04 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -7,9 +7,9 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::spanned::Spanned; use syn::{ - Attribute, Error, FnArg, ForeignItemFn, GenericArgument, GenericParam, Generics, Ident, Pat, - PatIdent, PatType, Path, PathArguments, Receiver, Result, ReturnType, Signature, Token, Type, - TypePath, TypeReference, parse_quote, + Attribute, Error, FnArg, ForeignItemFn, GenericArgument, GenericParam, Generics, Ident, LitStr, + Pat, PatIdent, PatType, Path, PathArguments, Receiver, Result, ReturnType, Signature, Token, + Type, TypePath, TypeReference, parse_quote, }; use crate::hygiene::Hygiene; @@ -20,13 +20,26 @@ mod options; use js::ForeignItem; use options::{BindingKind, FunctionOptions}; +/// Backend-independent description of one JavaScript import. pub(crate) struct FunctionImport { pub(crate) cfg_attrs: Vec, - pub(crate) descriptor: TokenStream, - pub(crate) needs_js_section: bool, + pub(crate) module: LitStr, + pub(crate) name: LitStr, + pub(crate) input_names: Vec, + pub(crate) input_types: Vec, + pub(crate) output_type: Option, + pub(crate) binding: Option, + pub(crate) suspending: bool, pub(crate) macro_path: Path, } +/// JavaScript binding data shared by the flat and direct Wire emitters. +pub(crate) struct FunctionBinding { + pub(crate) direct: Option, + pub(crate) call: String, + pub(crate) required_embeds: Vec, +} + struct FunctionPlan { inputs: Vec, output_ty: Option, @@ -127,14 +140,7 @@ pub(crate) fn expand( let import_name = plan.binding.import_name(namespace, &sig.ident); let link_name = format!("{crate_}.{import_name}"); let macro_path = hygiene.r#macro(&cfg_attrs, span); - let import = plan.import_descriptor( - ¯o_path, - crate_, - &import_name, - &link_name, - &cfg_attrs, - span, - ); + let import = plan.import_descriptor(¯o_path, crate_, &import_name, &cfg_attrs, span); let FunctionPlan { inputs, output_ty, @@ -626,7 +632,6 @@ impl FunctionPlan { macro_path: &Path, crate_: &str, import_name: &str, - link_name: &str, cfg_attrs: &[Attribute], span: Span, ) -> FunctionImport { @@ -639,100 +644,52 @@ impl FunctionPlan { .. } = self; let output_abi_ty = output_abi_override.as_ref().or(output_ty.as_ref()); - let input_descriptors = inputs.iter().map(|input| { - let name = &input.descriptor_name; - let ty = &input.abi_type; - - quote_spanned!(span=> #macro_path::import_input::<#ty>(#name)) - }); let input_tys: Vec<_> = inputs.iter().map(|input| &input.abi_type).collect(); - let mut unique_inputs = Vec::new(); - - for &ty in &input_tys { - if !unique_inputs.contains(&ty) { - unique_inputs.push(ty); - } - } - let mut required_embeds = Vec::new(); if let ForeignItem::Embed(name) = binding { - required_embeds.push(quote_spanned!(span=> (#crate_, #name))); - } - - for ty in &unique_inputs { - required_embeds.push(quote_spanned!(span=> #macro_path::js_input_embed::<#ty>())); - } - - if let Some(ty) = output_abi_ty { - required_embeds.push(quote_spanned!(span=> #macro_path::js_output_embed::<#ty>())); - required_embeds.push(quote_spanned!(span=> #macro_path::js_result_embed::<#ty>())); + required_embeds.push(quote_spanned!(span=> + #macro_path::JsEmbed::new(#crate_, #name) + )); } - let js = match binding { + let binding = match binding { ForeignItem::Generate { direct_wrapper, direct_call, indirect_call, .. - } => Some(quote_spanned! {span=> - #macro_path::ImportJs { - direct_wrapper: #direct_wrapper, - direct_call: #direct_call, - indirect_call: #indirect_call, - required_embeds: &[#(#required_embeds),*], - } + } => Some(FunctionBinding { + direct: (!direct_wrapper).then(|| direct_call.clone()), + call: indirect_call.clone(), + required_embeds, }), ForeignItem::Embed(name) => { let path = format!("this.#jsEmbed.{crate_}['{name}']"); let arguments = join_input_slots(inputs); let indirect_call = format!("{path}({arguments})"); - Some(quote_spanned! {span=> - #macro_path::ImportJs { - direct_wrapper: false, - direct_call: #path, - indirect_call: #indirect_call, - required_embeds: &[#(#required_embeds),*], - } + Some(FunctionBinding { + direct: Some(path), + call: indirect_call, + required_embeds, }) } ForeignItem::Import => None, }; - let js_value = if let Some(js) = &js { - quote_spanned!(span=> ::core::option::Option::Some(#js)) - } else { - quote_spanned!(span=> ::core::option::Option::None) - }; - let output = if let Some(output) = output_abi_ty { - quote_spanned!(span=> - ::core::option::Option::Some(#macro_path::import_output::<#output>()) - ) - } else { - quote_spanned!(span=> ::core::option::Option::None) - }; - let needs_js_section = js.is_some(); - let descriptor_constructor = if *suspending { - quote_spanned!(span=> #macro_path::ImportDescriptor::new_suspending) - } else { - quote_spanned!(span=> #macro_path::ImportDescriptor::new) - }; - let descriptor = quote_spanned! {span=> - #descriptor_constructor( - #crate_, - #import_name, - #link_name, - &[#(#input_descriptors),*], - #output, - #js_value, - ) - }; - FunctionImport { cfg_attrs: cfg_attrs.to_vec(), - descriptor, - needs_js_section, + module: LitStr::new(crate_, span), + name: LitStr::new(import_name, span), + input_names: inputs + .iter() + .map(|input| input.descriptor_name.clone()) + .collect(), + input_types: input_tys.into_iter().cloned().collect(), + output_type: output_abi_ty.cloned(), + binding, + suspending: *suspending, macro_path: macro_path.clone(), } } diff --git a/host/js-sys-bindgen/src/hygiene.rs b/host/js-sys-bindgen/src/hygiene.rs index 4edc8947..27d850dc 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -38,7 +38,7 @@ impl Hygiene<'_> { } pub(crate) fn r#macro(&mut self, attrs: &[Attribute], span: Span) -> Path { - self.js_sys_item(attrs, &parse_quote_spanned!(span=> r#macro), span) + self.js_sys_item(attrs, &parse_quote_spanned!(span=> wire), span) } fn js_sys_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index 6193cd18..b47aa1ed 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -235,8 +235,7 @@ impl GeneratedItems { struct ImportGroup { cfg_attrs: Vec, - descriptors: Vec, - needs_js_section: bool, + imports: Vec, macro_path: Path, } @@ -251,14 +250,12 @@ fn render_import_groups(imports: Vec) -> TokenStream { .iter_mut() .find(|group| group.cfg_attrs == import.cfg_attrs) { - group.descriptors.push(import.descriptor); - group.needs_js_section |= import.needs_js_section; + group.imports.push(import); } else { groups.push(ImportGroup { - cfg_attrs: import.cfg_attrs, - descriptors: vec![import.descriptor], - needs_js_section: import.needs_js_section, - macro_path: import.macro_path, + cfg_attrs: import.cfg_attrs.clone(), + macro_path: import.macro_path.clone(), + imports: vec![import], }); } } @@ -268,35 +265,113 @@ fn render_import_groups(imports: Vec) -> TokenStream { .fold(TokenStream::new(), |mut output, group| { let ImportGroup { cfg_attrs, - descriptors, - needs_js_section, + imports, macro_path, } = group; - let js = needs_js_section.then(|| { - quote::quote! { - const JS_CAPACITY: ::core::primitive::usize = - #macro_path::import_js_capacity(IMPORTS); - - #[used] - #[unsafe(link_section = "js_bindgen.import")] - static JS_SECTION: #macro_path::ImportSection = - #macro_path::import_js::(IMPORTS); + let mut input_types = Vec::::new(); + let mut output_types = Vec::::new(); + for import in &imports { + for ty in &import.input_types { + if !input_types.contains(ty) { + input_types.push(ty.clone()); + } } - }); + if let Some(ty) = &import.output_type + && !output_types.contains(ty) + { + output_types.push(ty.clone()); + } + } + let mut wire_descriptors = Vec::new(); + for import in &imports { + let module = &import.module; + let name = &import.name; + let input_names = &import.input_names; + let suspending = import.suspending; + let input_indices: Vec<_> = import + .input_types + .iter() + .map(|ty| { + input_types + .iter() + .position(|candidate| candidate == ty) + .expect("every input type was collected") + }) + .collect(); + let output_index = if let Some(ty) = &import.output_type { + let index = output_types + .iter() + .position(|candidate| candidate == ty) + .expect("every output type was collected"); + quote::quote!(::core::option::Option::Some( + #macro_path::WireImportOutput::new(#index) + )) + } else { + quote::quote!(::core::option::Option::None) + }; + let wire_inputs = input_names.iter().zip(input_indices).map(|(name, index)| { + quote::quote!( + #macro_path::WireImportInput::new(#name, #index) + ) + }); + let binding = if let Some(binding) = &import.binding { + let direct = if let Some(direct) = &binding.direct { + let direct = LitStr::new(direct, module.span()); + quote::quote!(::core::option::Option::Some(#direct)) + } else { + quote::quote!(::core::option::Option::None) + }; + let call = LitStr::new(&binding.call, module.span()); + let embeds = &binding.required_embeds; + quote::quote! { + ::core::option::Option::Some(#macro_path::WireImportBinding::new( + #direct, + #call, + &[#(#embeds),*], + )) + } + } else { + quote::quote!(::core::option::Option::None) + }; + wire_descriptors.push(quote::quote! { + #macro_path::WireImport::new( + #module, + #name, + &[#(#wire_inputs),*], + #output_index, + #binding, + #suspending, + ) + }); + } + let input_type_descriptors = input_types + .iter() + .map(|ty| quote::quote!(#macro_path::wire_import_input_type::<#ty>())) + .collect::>(); + let output_type_descriptors = output_types + .iter() + .map(|ty| quote::quote!(#macro_path::wire_import_output_type::<#ty>())) + .collect::>(); output.extend(quote::quote! { #(#cfg_attrs)* const _: () = { - static IMPORTS: &[#macro_path::ImportDescriptor] = &[#(#descriptors),*]; - const WAT_CAPACITY: ::core::primitive::usize = - #macro_path::import_wat_capacity(IMPORTS); + const TABLE: &#macro_path::WireImportTypeTable = + &#macro_path::WireImportTypeTable::new( + #macro_path::wire_import_retptr_type(), + &[#(#input_type_descriptors),*], + &[#(#output_type_descriptors),*], + #macro_path::wire_import_catch(), + ); + pub const WIRE: #macro_path::Wire = + #macro_path::Wire::imports(TABLE, &[#(#wire_descriptors),*]); + pub const LEN: ::core::primitive::usize = + #macro_path::wire_blob_len(&WIRE); #[used] - #[unsafe(link_section = "js_bindgen.wat")] - static WAT_SECTION: #macro_path::ImportSection = - #macro_path::import_wat::(IMPORTS); - - #js + #[unsafe(link_section = "js_bindgen.wire")] + pub static WIRE_SECTION: #macro_path::WireBlob = + #macro_path::WireBlob::new(&WIRE); }; }); output diff --git a/host/js-sys-bindgen/src/tests/macro/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs index b2fa0519..70be4a12 100644 --- a/host/js-sys-bindgen/src/tests/macro/export.rs +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -1,65 +1,5 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::File; - -fn expand(attr: TokenStream, function: &TokenStream) -> (String, String) { - let function = syn::parse2(quote! { #function }).unwrap(); - let output = crate::export::r#macro(attr, &function, Some("test_crate")).unwrap(); - let output = prettyplease::unparse(&syn::parse2::(output).unwrap()); - let dir = tempfile::tempdir().unwrap(); - let (wat, js_import, js_export) = super::inner(dir.path(), &output).unwrap(); - - assert_eq!(js_import, None); - (wat.unwrap(), js_export.unwrap()) -} - -#[test] -fn named_indirect_export_end_to_end() { - let (wat, js) = expand( - quote!(js_name = concat!("module", "::add")), - "e! { - pub fn add(value: u32, delta: u128) -> u128 { - u128::from(value) + delta - } - }, - ); - - assert!(wat.contains(r#"(@sym (name "__export_module::add"))"#)); - assert!(wat.contains("(param i32) (param i32) (param i64 i64)")); - assert!(wat.contains("(result i64 i64)")); - assert!(wat.contains("global.get $__stack_pointer")); - assert!(wat.contains("i64.load offset=8")); - let expected = [ - "(arg0, arg1) => {", - " const ret = wasmExports['module::add'](arg0, arg1, arg1 >> 64n)", - " return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], ret[1])", - "}", - ] - .join("\n"); - assert_eq!(js, expected); -} - -#[test] -fn promising_result_end_to_end() { - let (wat, js) = expand( - quote!(promising), - "e! { - pub fn checked(value: u128) -> Result { - Ok(value) - } - }, - ); - - assert!(wat.contains("(result i64 i64 i32 externref)")); - assert!(wat.contains("global.get $__stack_pointer")); - assert_eq!( - js, - "(() => {\n const $promising = WebAssembly.promising(wasmExports['checked'])\n \ - return (arg0) => $promising(arg0, arg0 >> 64n).then(ret => {\n if (ret[2] !== 0) \ - throw ret[3]\n return this.#jsEmbed.js_sys['numeric.u128.decode'](ret[0], \ - ret[1])\n })\n})()" - ); -} #[test] fn invalid_export_options() { diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index 4ec7638e..5b23d9cf 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -15,40 +15,6 @@ fn expand(attr: TokenStream, input: syn::ItemForeignMod) -> String { }) } -fn link(input: syn::ItemForeignMod) -> (Option, Option) { - let output = expand(TokenStream::new(), input); - let dir = tempfile::tempdir().unwrap(); - let (wat, js, _) = super::inner(dir.path(), &output).unwrap(); - (wat, js) -} - -#[test] -fn imports_are_batched_end_to_end() { - let (wat, js) = link(syn::parse_quote! { - extern "js-sys" { - #[js_sys(js_import)] - pub fn first(value: &JsValue); - - #[js_sys(js_import)] - pub fn second(value: &JsValue); - } - }); - let wat = wat.unwrap(); - - assert!(wat.contains(r#"(import "test_crate" "first""#)); - assert!(wat.contains(r#"(import "test_crate" "second""#)); - assert_eq!( - wat.matches(r#"(import "js_sys" "externref.table""#).count(), - 1 - ); - assert_eq!( - wat.matches("table.get $js_sys.import.externref.table") - .count(), - 2 - ); - assert_eq!(js, None); -} - #[test] fn binding_options() { let output = expand( @@ -69,9 +35,9 @@ fn binding_options() { }, ); - assert!(output.contains("renamed::r#macro::InputSlot1")); - assert!(output.contains(r#"direct_call: "globalThis.console.log(arg0_0)""#)); - assert!(output.contains(r#"direct_call: "globalThis.console.warn(arg0_0)""#)); + assert!(output.contains("renamed::wire::InputSlot1")); + assert!(output.contains("globalThis.console.log(arg0_0)")); + assert!(output.contains("globalThis.console.warn(arg0_0)")); assert!(output.contains("join_output_as::")); assert!(output.contains("#[cfg(all())]")); @@ -87,25 +53,8 @@ fn binding_options() { } }, ); - assert!(output.contains("::core::option::Option::None")); - assert!(output.contains(r#"direct_call: "this.#jsEmbed.test_crate['embed']""#)); -} - -#[test] -fn suspending_end_to_end() { - let (_, js) = link(syn::parse_quote! { - extern "js-sys" { - #[js_sys(suspending)] - pub fn wait() -> u128; - } - }); - - assert_eq!( - js.unwrap(), - "new WebAssembly.Suspending(async ($retptr) => {\n $retptr = $retptr >>> 0\n const \ - $ret = await (globalThis.wait())\n this.#jsEmbed.js_sys['numeric.128.encode']($ret, \ - $ret >> 64n, $retptr)\n})", - ); + assert!(output.contains("test_crate.imported")); + assert!(output.contains("this.#jsEmbed.test_crate['embed']")); } #[test] diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index a99ce4be..e670520d 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -48,25 +48,19 @@ fn member_operations() { } }); - let operations: Vec<_> = output - .lines() - .filter_map(|line| line.trim().strip_prefix("direct_call: ")) - .map(|line| line.trim_end_matches(',')) - .collect(); - assert_eq!( - operations, - [ - r#""new globalThis.JavaScriptType()""#, - r#""globalThis.JavaScriptType.value""#, - r#""arg0_0.call()""#, - r#""arg0_0.value""#, - r#""arg0_0.value = arg1_0""#, - r#""arg0_0[arg1_0]""#, - r#""arg0_0[arg1_0] = arg2_0""#, - r#""delete arg0_0[arg1_0]""#, - r#""arg0_0.push(arg1_0, ...arg2_0)""#, - ] - ); + for operation in [ + "new globalThis.JavaScriptType()", + "globalThis.JavaScriptType.value", + "arg0_0.call()", + "arg0_0.value", + "arg0_0.value = arg1_0", + "arg0_0[arg1_0]", + "arg0_0[arg1_0] = arg2_0", + "delete arg0_0[arg1_0]", + "arg0_0.push(arg1_0, ...arg2_0)", + ] { + assert!(output.contains(operation)); + } } #[test] diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index a3681e27..b5349369 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -1,17 +1,4 @@ -use std::io::Cursor; -use std::path::Path; -use std::process::Command; -use std::{env, fs}; - -use anyhow::{Context, Result, anyhow, bail, ensure}; -use cargo_metadata::{Artifact, CompilerMessage, Message, Target}; -use itertools::Itertools; -use js_bindgen_ld_shared::{ - IMPORT_SECTION, JsBindgenJsSectionParser, JsBindgenWatSectionParser, WAT_SECTION, -}; use proc_macro2::TokenStream; -use syn::parse_quote; -use wasmparser::{Parser, Payload}; use crate::r#macro; @@ -25,192 +12,3 @@ fn macro_error(input: syn::ItemForeignMod) -> String { error.to_string() } - -fn inner(tmp: &Path, source: &str) -> Result<(Option, Option, Option)> { - let js_sys = env::current_dir()? - .parent() - .and_then(Path::parent) - .context("unexpected directory structure")? - .join("client") - .join("js-sys"); - let cargo_toml = indoc::formatdoc!( - r#"[package] - name = "test-crate" - edition = "2024" - publish = false - - [dependencies] - js-sys = {{ path = '{}' }} - "#, - js_sys.display(), - ); - fs::write(tmp.join("Cargo.toml"), cargo_toml)?; - - let js_test = r#macro::expand_for_test( - TokenStream::new(), - parse_quote! { extern "js-sys" { pub type JsTest; } }, - "test_crate", - ) - .unwrap() - .into_token_stream(); - - let src = tmp.join("src"); - fs::create_dir(&src)?; - let lib = src.join("lib.rs"); - fs::write( - &lib, - indoc::formatdoc!( - r#"#![no_std] - #![cfg_attr(target_arch = "wasm64", feature(simd_wasm64))] - - extern crate alloc; - - use alloc::alloc::{{GlobalAlloc, Layout}}; - #[cfg(target_arch = "wasm32")] - use core::arch::wasm32::unreachable; - #[cfg(target_arch = "wasm64")] - use core::arch::wasm64::unreachable; - - use js_sys::*; - - #[panic_handler] - fn panic(_: &core::panic::PanicInfo<'_>) -> ! {{ - unreachable(); - }} - - struct Allocator; - - unsafe impl GlobalAlloc for Allocator {{ - unsafe fn alloc(&self, _: Layout) -> *mut u8 {{ - unimplemented!() - }} - - unsafe fn dealloc(&self, _: *mut u8, _: Layout) {{ - unimplemented!() - }} - }} - - #[global_allocator] - static ALLOC: Allocator = Allocator; - - {js_test} - - fn assert_optional_js_test() - where - ::core::option::Option: - ::js_sys::hazard::IntoJS + ::js_sys::hazard::FromJS, - {{}} - - {source} - "# - ), - )?; - - let output = Command::new("cargo") - .current_dir(tmp) - .arg("build") - .args(["--target", "wasm32-unknown-unknown"]) - .args(["--message-format", "json"]) - .output()?; - - if !output.status.success() { - if !output.stderr.is_empty() { - eprintln!( - "------ cargo stderr ------\n{}", - String::from_utf8_lossy(&output.stderr) - ); - - if !output.stderr.ends_with(b"\n") { - eprintln!(); - } - } - - let reader = Cursor::new(output.stdout); - - for message in Message::parse_stream(reader) { - if let Message::CompilerMessage(CompilerMessage { message, .. }) = message? { - println!("{message}"); - } - } - - bail!("Cargo failed with status: {}", output.status) - } - - let reader = Cursor::new(output.stdout); - - let mut wat_output = None; - let mut js_import_output = None; - let mut js_export_output = None; - - for message in Message::parse_stream(reader) { - if let Message::CompilerArtifact(Artifact { - target: Target { src_path, .. }, - filenames, - .. - }) = message? - && src_path.canonicalize()? == lib.canonicalize()? - { - for filename in filenames { - js_bindgen_ld_shared::ld_input_parser(filename.as_os_str(), |_, data, _| { - for payload in Parser::new(0).parse_all(data) { - let payload = payload?; - - match payload { - Payload::CustomSection(c) if c.name() == WAT_SECTION => { - let wat = JsBindgenWatSectionParser::new(&c) - .exactly_one() - .map_err(|wats| { - anyhow!( - "found multiple WAT outputs in a single section: \ - {wats:?}" - ) - })?; - ensure!(wat_output.is_none(), "found multiple WAT outputs"); - wat_output = Some(wat.to_owned()); - js_bindgen_ld_shared::wat_to_object(false, wat).unwrap(); - } - Payload::CustomSection(c) if c.name() == IMPORT_SECTION => { - let mut parser = JsBindgenJsSectionParser::new(&c); - - let import = parser.next().unwrap(); - - if import.module != "test_crate" { - continue; - } - - ensure!( - parser.next().is_none(), - "found multiple JS import outputs in a single section: \ - {parser:?}" - ); - - js_import_output = Some(import.js.to_owned()); - } - Payload::CustomSection(c) if c.name() == "js_bindgen.export" => { - let mut parser = JsBindgenJsSectionParser::new(&c); - let export = parser.next().unwrap(); - - if export.module != "test_crate" { - continue; - } - - ensure!( - parser.next().is_none(), - "found multiple JS export outputs in a single section: \ - {parser:?}" - ); - - js_export_output = Some(export.js.to_owned()); - } - _ => (), - } - } - - Ok(()) - })??; - } - } - } - - Ok((wat_output, js_import_output, js_export_output)) -} diff --git a/host/ld-shared/src/lib.rs b/host/ld-shared/src/lib.rs index 3797a92e..a23f97e8 100644 --- a/host/ld-shared/src/lib.rs +++ b/host/ld-shared/src/lib.rs @@ -11,6 +11,7 @@ use wasmparser::CustomSectionReader; pub const WAT_SECTION: &str = "js_bindgen.wat"; pub const IMPORT_SECTION: &str = "js_bindgen.import"; +pub const WIRE_SECTION: &str = "js_bindgen.wire"; /// Creates a relocatable Wasm object from the WAT input. pub fn wat_to_object(wasm64: bool, wat: &str) -> rwat::Result> { @@ -120,7 +121,7 @@ pub struct JsBindgenWatSectionParser<'cs>(CustomSectionParser<'cs>); impl<'cs> JsBindgenWatSectionParser<'cs> { #[must_use] pub fn new(custom_section: &CustomSectionReader<'cs>) -> Self { - Self(CustomSectionParser::new(custom_section, true)) + Self(CustomSectionParser::new(custom_section)) } } @@ -146,6 +147,34 @@ impl<'cs> Iterator for JsBindgenWatSectionParser<'cs> { } } +#[derive(Clone)] +pub struct JsBindgenWireSectionParser<'cs>(CustomSectionParser<'cs>); + +impl<'cs> JsBindgenWireSectionParser<'cs> { + #[must_use] + pub fn new(custom_section: &CustomSectionReader<'cs>) -> Self { + Self(CustomSectionParser::new(custom_section)) + } +} + +impl Debug for JsBindgenWireSectionParser<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let rest: Vec<_> = self.clone().collect(); + + f.debug_tuple("JsBindgenWireSectionParser") + .field(&rest.as_slice()) + .finish() + } +} + +impl<'cs> Iterator for JsBindgenWireSectionParser<'cs> { + type Item = &'cs [u8]; + + fn next(&mut self) -> Option { + self.0.next() + } +} + #[derive(Clone)] pub struct JsBindgenJsSectionParser<'cs>(CustomSectionParser<'cs>); @@ -166,10 +195,7 @@ pub struct JsRequiredEmbed<'cs> { impl<'cs> JsBindgenJsSectionParser<'cs> { #[must_use] pub fn new(custom_section: &CustomSectionReader<'cs>) -> Self { - Self(CustomSectionParser::new( - custom_section, - custom_section.name() == IMPORT_SECTION, - )) + Self(CustomSectionParser::new(custom_section)) } } @@ -253,71 +279,14 @@ impl<'cs> Iterator for JsBindgenJsSectionParser<'cs> { #[derive(Clone)] struct CustomSectionParser<'cs> { name: &'cs str, - data: SectionData<'cs>, -} - -#[derive(Clone)] -enum SectionData<'cs> { - Records(&'cs [u8]), - Framed { - blocks: &'cs [u8], - records: &'cs [u8], - }, + data: &'cs [u8], } impl<'cs> CustomSectionParser<'cs> { - fn new(custom_section: &CustomSectionReader<'cs>, framed: bool) -> Self { - let data = if framed { - // `Linkers` concatenate statics assigned to the same custom section. - // Each block carries its allocated and initialized lengths, followed - // by length-prefixed records and any trailing padding. - SectionData::Framed { - blocks: custom_section.data(), - records: &[], - } - } else { - SectionData::Records(custom_section.data()) - }; - + fn new(custom_section: &CustomSectionReader<'cs>) -> Self { Self { name: custom_section.name(), - data, - } - } - - fn next_record(&mut self) -> Option<&'cs [u8]> { - let name = self.name; - - match &mut self.data { - SectionData::Records(records) => take_record(name, records), - SectionData::Framed { blocks, records } => loop { - if !records.is_empty() { - return take_record(name, records); - } - if blocks.is_empty() { - return None; - } - - let header = blocks.split_off(..8).unwrap_or_else(|| { - panic!("found incomplete block header in custom section `{name}`") - }); - let capacity = u32::from_le_bytes(header[..4].try_into().unwrap()) as usize; - let used = u32::from_le_bytes(header[4..].try_into().unwrap()) as usize; - - assert!( - used <= capacity, - "block uses {used} bytes but has capacity {capacity} in custom section \ - `{name}`" - ); - - let block = blocks.split_off(..capacity).unwrap_or_else(|| { - panic!( - "block has capacity {capacity}, but not enough bytes remain in custom \ - section `{name}`" - ) - }); - *records = &block[..used]; - }, + data: custom_section.data(), } } } @@ -326,21 +295,19 @@ impl<'cs> Iterator for CustomSectionParser<'cs> { type Item = &'cs [u8]; fn next(&mut self) -> Option { - self.next_record() - } -} - -fn take_record<'data>(name: &str, data: &mut &'data [u8]) -> Option<&'data [u8]> { - if let Some(length) = data.split_off(..4) { - let length = u32::from_le_bytes(length.try_into().unwrap()) as usize; - - Some( - data.split_off(..length) - .unwrap_or_else(|| panic!("invalid length encoding in custom section `{name}`")), - ) - } else if data.is_empty() { - None - } else { - panic!("found left over bytes in custom section `{name}`: {data:?}"); + if let Some(length) = self.data.split_off(..4) { + let length = u32::from_le_bytes(length.try_into().unwrap()) as usize; + + Some(self.data.split_off(..length).unwrap_or_else(|| { + panic!("invalid length encoding in custom section `{}`", self.name) + })) + } else if self.data.is_empty() { + None + } else { + panic!( + "found left over bytes in custom section `{}`: {:?}", + self.name, self.data + ); + } } } diff --git a/host/ld/Cargo.toml b/host/ld/Cargo.toml index 24a51841..0f789008 100644 --- a/host/ld/Cargo.toml +++ b/host/ld/Cargo.toml @@ -18,6 +18,7 @@ hashbrown = { workspace = true, features = ["default-hasher"] } js-bindgen-cli-lib = { workspace = true } js-bindgen-ld-shared = { workspace = true } js-bindgen-shared = { workspace = true, features = ["memmap"] } +js-bindgen-wire = { workspace = true, features = ["alloc"] } postcard = { workspace = true, features = ["alloc"] } wasm-encoder = { workspace = true } wasmparser = { workspace = true } diff --git a/host/ld/src/args.rs b/host/ld/src/args.rs index 217fa7cf..a0113a7e 100644 --- a/host/ld/src/args.rs +++ b/host/ld/src/args.rs @@ -173,7 +173,7 @@ impl<'args> Arguments<'args> { #[cfg(test)] mod tests { - use std::ffi::OsString; + use std::ffi::OsStr; use crate::args::Arguments; @@ -183,8 +183,12 @@ mod tests { let args = Arguments::new(args); assert!(args.web()); - let mut iter = args.pass_args().iter(); - assert_eq!(iter.next().copied(), Some(&OsString::from("--no-entry"))); + let pass_args = args.pass_args(); + let mut iter = pass_args.iter(); + assert_eq!( + iter.next().map(|value| value.as_os_str()), + Some(OsStr::new("--no-entry")) + ); assert!(iter.next().is_none()); } } diff --git a/host/ld/src/js.rs b/host/ld/src/js.rs index 31f6fc40..8e70d969 100644 --- a/host/ld/src/js.rs +++ b/host/ld/src/js.rs @@ -17,7 +17,7 @@ pub struct JsStore { embed: FixedHashMap>, expected_embed: HashMap>, provided_embed: HashMap>, - provided_export: FixedHashMap, + export: FixedHashMap, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -36,9 +36,142 @@ struct JsEmbed { struct JsExport { module: String, binding: JsWithEmbeds, + kind: JsExportKind, +} + +#[derive(Debug, PartialEq, Eq)] +enum JsExportKind { + Symbol, + Closure { shim: String }, } impl JsStore { + pub fn add_js_import( + &mut self, + module: &str, + name: &str, + js: String, + embeds: impl IntoIterator, + ) -> Result<()> { + let binding = JsWithEmbeds { + js, + embeds: embeds + .into_iter() + .map(|(module, name)| JsEmbed { module, name }) + .collect(), + }; + let definitions = self.provided_import.entry_ref(module).or_default(); + + if let Some(previous) = definitions.get(name) { + if previous != &binding { + bail!( + "found multiple JS imports for `{module}:{name}`\n\tJS Import 1:\n{previous:?}\n\tJS Import \ + 2:\n{binding:?}", + ); + } + } else { + definitions.insert(name.to_owned(), binding.clone()); + } + + if self + .expected_import + .get_mut(module) + .is_some_and(|names| names.remove(name)) + { + self.import + .entry_ref(module) + .or_default() + .insert(name.to_owned(), binding.js.clone()); + + for embed in binding.embeds { + self.require_js_embed(embed); + } + } + + Ok(()) + } + + pub fn add_symbol_export( + &mut self, + module: &str, + name: &str, + js: String, + embeds: impl IntoIterator, + ) -> Result<()> { + let definition = JsExport { + module: module.to_owned(), + binding: JsWithEmbeds { + js, + embeds: embeds + .into_iter() + .map(|(module, name)| JsEmbed { module, name }) + .collect(), + }, + kind: JsExportKind::Symbol, + }; + + if let Some(previous) = self.export.get(name) { + bail!( + "found multiple JS exports named `{name}` from `{}` and `{}`\n\tJS Export 1:\n{:?}\n\tJS Export \ + 2:\n{:?}", + previous.module, + definition.module, + previous.binding, + definition.binding + ); + } + + for embed in definition.binding.embeds.iter().cloned() { + self.require_js_embed(embed); + } + + self.export.insert(name.into(), definition); + Ok(()) + } + + pub fn add_closure_export( + &mut self, + module: &str, + name: &str, + js: String, + embeds: impl IntoIterator, + shim: &str, + ) -> Result { + let definition = JsExport { + module: module.to_owned(), + binding: JsWithEmbeds { + js, + embeds: embeds + .into_iter() + .map(|(module, name)| JsEmbed { module, name }) + .collect(), + }, + kind: JsExportKind::Closure { + shim: shim.to_owned(), + }, + }; + + if let Some(previous) = self.export.get(name) { + if previous == &definition { + return Ok(false); + } + bail!( + "found incompatible closure exports named `{name}` from `{}` and `{}`\n\tClosure 1:\n{:?}\n\tClosure 2:\n{:?}", + previous.module, + definition.module, + previous, + definition, + ); + } + + for embed in definition.binding.embeds.iter().cloned() { + self.require_js_embed(embed); + } + + self.export.insert(name.into(), definition); + Ok(true) + } + pub fn add_import(&mut self, import: Import<'_>) -> Result<()> { if let Some(js) = self .provided_import @@ -72,41 +205,15 @@ impl JsStore { pub fn add_js_imports(&mut self, custom_section: &CustomSectionReader<'_>) -> Result<()> { for import in JsBindgenJsSectionParser::new(custom_section) { - let binding = JsWithEmbeds { - js: import.js.to_owned(), - embeds: import.embeds.into_iter().map(JsEmbed::from).collect(), - }; - let definitions = self.provided_import.entry_ref(import.module).or_default(); - - if let Some(previous) = definitions.get(import.name) { - if previous != &binding { - bail!( - "found multiple JS imports for `{}:{}`\n\tJS Import 1:\n{:?}\n\tJS Import \ - 2:\n{:?}", - import.module, - import.name, - previous, - binding - ); - } - } else { - definitions.insert(import.name.to_owned(), binding.clone()); - } - - if self - .expected_import - .get_mut(import.module) - .is_some_and(|names| names.remove(import.name)) - { - self.import - .entry_ref(import.module) - .or_default() - .insert(import.name.to_owned(), binding.js.clone()); - - for embed in binding.embeds { - self.require_js_embed(embed); - } - } + self.add_js_import( + import.module, + import.name, + import.js.to_owned(), + import + .embeds + .into_iter() + .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), + )?; } Ok(()) @@ -154,51 +261,6 @@ impl JsStore { Ok(()) } - pub fn add_js_exports( - &mut self, - custom_section: &CustomSectionReader<'_>, - ) -> Result> { - let mut names = Vec::new(); - - for export in JsBindgenJsSectionParser::new(custom_section) { - let binding = JsWithEmbeds { - js: export.js.to_owned(), - embeds: export.embeds.into_iter().map(JsEmbed::from).collect(), - }; - let definition = JsExport { - module: export.module.to_owned(), - binding, - }; - - if let Some(previous) = self.provided_export.get(export.name) { - if previous == &definition { - continue; - } - - bail!( - "found multiple JS exports named `{}` from `{}` and `{}`\n JS Export \ - 1:\n{:?}\n JS Export 2:\n{:?}", - export.name, - previous.module, - export.module, - previous.binding, - definition.binding, - ); - } - - names.push(export.name.to_owned()); - - for embed in definition.binding.embeds.iter().cloned() { - self.require_js_embed(embed); - } - - self.provided_export - .insert(export.name.to_owned(), definition); - } - - Ok(names) - } - fn require_js_embed(&mut self, embed: JsEmbed) { if !self .embed @@ -244,7 +306,7 @@ impl JsStore { js_import: self.import, js_embed: self.embed, js_export: self - .provided_export + .export .into_iter() .map(|(name, export)| (name, export.binding.js)) .collect(), diff --git a/host/ld/src/main.rs b/host/ld/src/main.rs index 5b6f6c20..e7e9d602 100644 --- a/host/ld/src/main.rs +++ b/host/ld/src/main.rs @@ -2,6 +2,7 @@ mod args; mod js; mod post; mod pre; +mod wire; use std::process::{self, Command}; use std::{env, fs}; @@ -12,7 +13,6 @@ use crate::args::Arguments; use crate::pre::PreOutput; fn main() { - // Read arguments. let args = argfile::expand_args_from(env::args_os(), argfile::parse_response, argfile::PREFIX) .unwrap(); let args = Arguments::new(&args[1..]); diff --git a/host/ld/src/post.rs b/host/ld/src/post.rs index d5e54577..87fee72d 100644 --- a/host/ld/src/post.rs +++ b/host/ld/src/post.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use js_bindgen_cli_lib::{JS_OUTPUT_SECTION, MainMemory}; -use js_bindgen_ld_shared::{IMPORT_SECTION, WAT_SECTION}; +use js_bindgen_ld_shared::{IMPORT_SECTION, WAT_SECTION, WIRE_SECTION}; use js_bindgen_shared::{IS_COMPAT_SECTION, IS_TEST_SECTION}; use wasm_encoder::{ CustomSection, EntityType, ExportSection, ImportSection, Module, ProducersField, @@ -74,9 +74,9 @@ pub fn processing( export_section.append_to(&mut wasm_output); } // Don't write back our own custom sections. - Payload::CustomSection(c) if matches!(c.name(), WAT_SECTION | IMPORT_SECTION) => {} + Payload::CustomSection(c) + if matches!(c.name(), WAT_SECTION | IMPORT_SECTION | WIRE_SECTION) => {} Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => (), - Payload::CustomSection(c) if c.name() == "js_bindgen.export" => (), // Register ourselves in the producer section. Payload::CustomSection(c) if c.name() == "producers" => { let KnownCustom::Producers(c) = c.as_known() else { diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index a1a020f6..a2de1100 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -6,12 +6,16 @@ use std::time::SystemTime; use anyhow::Result; use js_bindgen_cli_lib::MainMemory; -use js_bindgen_ld_shared::{IMPORT_SECTION, JsBindgenWatSectionParser, WAT_SECTION}; +use js_bindgen_ld_shared::{ + IMPORT_SECTION, JsBindgenWatSectionParser, JsBindgenWireSectionParser, WAT_SECTION, + WIRE_SECTION, +}; use js_bindgen_shared::ReadFile; use wasmparser::{Parser, Payload}; use crate::args::Arguments; use crate::js::JsStore; +use crate::wire::{self, RenderedExport, RenderedRecord}; pub struct PreOutput<'args> { pub add_args: Vec, @@ -68,7 +72,7 @@ pub fn processing<'a>(args: &'a Arguments<'a>) -> PreOutput<'a> { is_test |= is_libtest(input); } - js_bindgen_ld_shared::ld_input_parser(input, |path, data, object_mtime| { + js_bindgen_ld_shared::ld_input_parser(input, |path, data, object_mtime| -> Result<()> { process_object( &mut js_store, &mut seen_wat, @@ -99,6 +103,43 @@ fn is_libtest(input: &OsStr) -> bool { .is_some_and(|name| name.starts_with("libtest-")) } +fn compile_wat( + wasm_path: &Path, + wasm64: bool, + wat: &str, + object_mtime: Option, +) -> Result>> { + // The cache is shared by concurrent linker processes. Hold the lock through + // freshness validation, generation, and parsing. + let lock_path = wasm_path.with_added_extension("lock"); + let file = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(lock_path)?; + file.lock()?; + + let mut wasm_bytes = None; + + // We first use a fingerprint to quickly determine whether `wasm.o` needs to be + // regenerated: https://doc.rust-lang.org/1.92.0/nightly-rustc/cargo/core/compiler/fingerprint/index.html#fingerprints-and-unithashs + // + // Then we compare the `mtime` of the `.o` files with that of `wasm.o`. If it is + // `None`(should not occur on major platforms), or if the `.o` files are + // newer than `wasm.o`, we regenerate `wasm.o`. + if !wasm_path.exists() || { + js_bindgen_shared::mtime(&std::fs::metadata(wasm_path)?)? + .zip(object_mtime) + .is_none_or(|(t1, t2)| t1 < t2) + } { + let wasm = js_bindgen_ld_shared::wat_to_object(wasm64, wat)?; + fs::write(wasm_path, &wasm)?; + wasm_bytes = Some(wasm); + } + Ok(wasm_bytes) +} + /// Extracts any WAT instructions from `js-bindgen`, builds object files from /// them and passes them to the linker. fn process_object( @@ -113,6 +154,11 @@ fn process_object( // Multiple files from the same object file need different names. let mut file_counter = 0; + let mut next_wasm_object = || { + file_counter += 1; + archive_path.with_added_extension(format!("wasm.{file_counter}.o")) + }; + for payload in Parser::new(0).parse_all(object) { let payload = match payload { Ok(payload) => payload, @@ -126,39 +172,11 @@ fn process_object( match &payload { Payload::CustomSection(c) if c.name() == WAT_SECTION => { for wat in JsBindgenWatSectionParser::new(c) { - file_counter += 1; + let wasm_path = next_wasm_object(); if !seen_wat.insert(wat.to_owned()) { continue; } - let wasm_path = - archive_path.with_added_extension(format!("wasm.{file_counter}.o")); - // The cache is shared by concurrent linker processes. Hold the lock through - // freshness validation, generation, and parsing. - let lock_path = wasm_path.with_added_extension("lock"); - let lock = fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(lock_path)?; - lock.lock()?; - let mut wasm_bytes = None; - - // We first use a fingerprint to quickly determine whether `wasm.o` needs to be - // regenerated: https://doc.rust-lang.org/1.92.0/nightly-rustc/cargo/core/compiler/fingerprint/index.html#fingerprints-and-unithashs - // - // Then we compare the `mtime` of the `.o` files with that of `wasm.o`. If it is - // `None`(should not occur on major platforms), or if the `.o` files are - // newer than `wasm.o`, we regenerate `wasm.o`. - if !wasm_path.exists() || { - js_bindgen_shared::mtime(&std::fs::metadata(&wasm_path)?)? - .zip(object_mtime) - .is_none_or(|(t1, t2)| t1 < t2) - } { - let wasm = js_bindgen_ld_shared::wat_to_object(wasm64, wat)?; - fs::write(&wasm_path, &wasm)?; - wasm_bytes = Some(wasm); - } + let wasm_bytes = compile_wat(&wasm_path, wasm64, wat, object_mtime)?; let exist_file; let wasm_object: &[u8] = if let Some(bytes) = &wasm_bytes { @@ -178,10 +196,49 @@ fn process_object( js_bindgen_shared::mtime(&std::fs::metadata(&wasm_path)?)?, )?; - drop(lock); add_args.push(wasm_path.into()); } } + Payload::CustomSection(c) if c.name() == WIRE_SECTION => { + for blob in JsBindgenWireSectionParser::new(c) { + match wire::decode_and_render(blob)? { + RenderedRecord::Imports(rendered) => { + for import in rendered.bindings { + js_store.add_js_import( + import.module, + import.name, + import.js, + import.embeds.into_iter().map(|embed| { + (embed.module.to_owned(), embed.name.to_owned()) + }), + )?; + } + + if let Some(wat) = rendered.wat { + let wasm_path = next_wasm_object(); + if seen_wat.insert(wat.clone()) { + compile_wat(&wasm_path, wasm64, &wat, object_mtime)?; + add_args.push(wasm_path.into()); + } + } + } + RenderedRecord::Exports(exports) => { + for export in exports { + let Some((name, shim)) = register_export(js_store, export)? else { + continue; + }; + + add_args.push(format!("--export={name}").into()); + let wasm_path = next_wasm_object(); + if seen_wat.insert(shim.clone()) { + compile_wat(&wasm_path, wasm64, &shim, object_mtime)?; + add_args.push(wasm_path.into()); + } + } + } + } + } + } // Extract all JS imports. Payload::CustomSection(c) if c.name() == IMPORT_SECTION => { js_store.add_js_imports(c)?; @@ -190,12 +247,6 @@ fn process_object( Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => { js_store.add_js_embeds(c)?; } - // Extract JS export wrappers and keep their WAT shim symbols alive. - Payload::CustomSection(c) if c.name() == "js_bindgen.export" => { - for name in js_store.add_js_exports(c)? { - add_args.push(format!("--export={name}").into()); - } - } _ => (), } } @@ -203,6 +254,41 @@ fn process_object( Ok(()) } +fn register_export<'wire>( + js_store: &mut JsStore, + export: RenderedExport<'wire>, +) -> Result> { + match export { + RenderedExport::Symbol { binding, shim } => { + let name = binding.name; + js_store.add_symbol_export( + binding.module, + name, + binding.js, + binding + .embeds + .into_iter() + .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), + )?; + Ok(Some((name, shim))) + } + RenderedExport::Closure { binding, shim } => { + let name = binding.name; + let inserted = js_store.add_closure_export( + binding.module, + name, + binding.js, + binding + .embeds + .into_iter() + .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), + &shim, + )?; + Ok(inserted.then_some((name, shim))) + } + } +} + fn main_memory<'args>( arch: Arch, wasm_ld_args: &Arguments<'args>, diff --git a/host/ld/src/wire/export/js.rs b/host/ld/src/wire/export/js.rs new file mode 100644 index 00000000..24839524 --- /dev/null +++ b/host/ld/src/wire/export/js.rs @@ -0,0 +1,194 @@ +use js_bindgen_wire::model::{Export, ExportInput, ExportOutput}; + +use crate::wire::JsBinding; +use crate::wire::js::{Placeholder, render_template}; + +/// Renders one decoded Rust export or closure dispatcher. +pub(super) fn render<'a>(export: &Export<'a>) -> JsBinding<'a> { + JsBinding { + module: export.module, + name: export.name, + js: render_export(export), + embeds: export.embeds.clone(), + } +} + +fn render_export(export: &Export<'_>) -> String { + let function = format!("wasmExports['{}']", export.name); + let callable = if export.promising { + format!("WebAssembly.promising({function})") + } else { + function + }; + + if is_passthrough(export) { + return callable; + } + + let parameters = export + .inputs + .iter() + .map(|input| input.name) + .collect::>() + .join(", "); + let arguments = render_arguments(&export.inputs); + + if export.promising { + render_promising(export, &callable, ¶meters, &arguments) + } else { + render_sync(export, &callable, ¶meters, &arguments) + } +} + +fn render_sync(export: &Export<'_>, callable: &str, parameters: &str, arguments: &str) -> String { + let prepares = render_prepares(&export.inputs, " "); + let call = format!("{callable}({arguments})"); + if let Some(output) = export.output.as_ref() { + format!( + "({parameters}) => {{\n{prepares} const ret = {call}\n{}\n}}", + render_output(output, " ") + ) + } else { + format!("({parameters}) => {{\n{prepares} {call}\n}}") + } +} + +fn render_promising( + export: &Export<'_>, + callable: &str, + parameters: &str, + arguments: &str, +) -> String { + let prepares = render_prepares(&export.inputs, " "); + let then = export.output.as_ref().map_or_else(String::new, |output| { + if output.js_conversion().is_none() && output.result().is_none() { + String::new() + } else { + format!( + ".then(ret => {{\n{}\n }})", + render_output(output, " ") + ) + } + }); + + if prepares.is_empty() { + format!( + "(() => {{\n const $promising = {callable}\n return ({parameters}) => $promising({arguments}){then}\n}})()" + ) + } else { + format!( + "(() => {{\n const $promising = {callable}\n return ({parameters}) => {{\n{prepares} return $promising({arguments}){then}\n }}\n}})()" + ) + } +} + +fn is_passthrough(export: &Export<'_>) -> bool { + !export.inputs.iter().any(|input| input.conversion.is_some()) + && export + .output + .as_ref() + .is_none_or(|output| output.js_conversion().is_none() && output.result().is_none()) +} + +fn render_arguments(inputs: &[ExportInput<'_>]) -> String { + let mut arguments = String::new(); + let mut wrote_argument = false; + for input in inputs { + if let Some(conversion) = input.conversion.as_ref() { + for expression in &conversion.expressions { + if wrote_argument { + arguments.push_str(", "); + } + arguments.push_str(&render_input_template(expression, input.name)); + wrote_argument = true; + } + } else if !input.slots.is_empty() { + if wrote_argument { + arguments.push_str(", "); + } + arguments.push_str(input.name); + wrote_argument = true; + } + } + arguments +} + +fn render_prepares(inputs: &[ExportInput<'_>], indent: &str) -> String { + inputs + .iter() + .filter_map(|input| { + let prepare = input + .conversion + .as_ref() + .and_then(|conversion| conversion.prepare) + .filter(|prepare| !prepare.is_empty())?; + Some(format!( + "{indent}const {}$prepared = {}\n", + input.name, + render_prepare_template(prepare, input.name) + )) + }) + .collect() +} + +fn render_output(output: &ExportOutput<'_>, indent: &str) -> String { + let result = if let Some(result) = output.result() { + format!( + "{indent}if (ret[{}] !== 0) throw ret[{}]\n", + result.discriminant, result.error + ) + } else { + String::new() + }; + let expression = output.js_conversion().map_or_else( + || render_output_slot(output, 0), + |template| render_output_template(template, output), + ); + format!("{result}{indent}return {expression}") +} + +fn render_input_template(template: &str, name: &str) -> String { + render_template(template, |rendered, placeholder| match placeholder { + Placeholder::Value => rendered.push_str(name), + Placeholder::Prepared => { + rendered.push_str(name); + rendered.push_str("$prepared"); + } + Placeholder::Slot(_) => {} + }) +} + +fn render_prepare_template(template: &str, value: &str) -> String { + render_template(template, |rendered, placeholder| { + if placeholder == Placeholder::Value { + rendered.push_str(value); + } + }) +} + +fn render_output_template(template: &str, output: &ExportOutput<'_>) -> String { + render_template(template, |rendered, placeholder| { + if let Placeholder::Slot(slot) = placeholder { + rendered.push_str(&render_output_slot(output, slot)); + } + }) +} + +fn render_output_slot(output: &ExportOutput<'_>, slot: usize) -> String { + if let Some(result) = output.result() { + if slot < usize::from(result.discriminant) { + return render_ret(slot); + } + } else if output.is_direct() { + if slot == 0 { + return "ret".to_owned(); + } + } else { + return render_ret(slot); + } + String::new() +} + +fn render_ret(index: usize) -> String { + format!("ret[{index}]") +} diff --git a/host/ld/src/wire/export/mod.rs b/host/ld/src/wire/export/mod.rs new file mode 100644 index 00000000..d2d24dff --- /dev/null +++ b/host/ld/src/wire/export/mod.rs @@ -0,0 +1,24 @@ +//! Host-side rendering for Rust exports and closure dispatchers. + +mod js; +mod wat; + +use js_bindgen_wire::model::{Callee, Export}; + +use crate::wire::RenderedExport; + +pub(super) fn render<'a>(exports: &[Export<'a>]) -> Vec> { + exports + .iter() + .map(|export| { + let binding = js::render(export); + let shim = wat::render(core::slice::from_ref(export)) + .expect("one export always produces a Wasm shim"); + + match export.callee { + Callee::Symbol { .. } => RenderedExport::Symbol { binding, shim }, + Callee::Closure { .. } => RenderedExport::Closure { binding, shim }, + } + }) + .collect() +} diff --git a/host/ld/src/wire/export/wat.rs b/host/ld/src/wire/export/wat.rs new file mode 100644 index 00000000..34e73c5f --- /dev/null +++ b/host/ld/src/wire/export/wat.rs @@ -0,0 +1,376 @@ +use std::fmt::Write; + +use crate::wire::wat::{WatImports, WatLocals, write_conversion}; +use js_bindgen_wire::abi::WatType; +use js_bindgen_wire::model::{Callee, Export, ExportInput, ExportInputKind, ExportOutput, Slot}; + +struct ExportRenderer<'export, 'wire> { + index: usize, + export: &'export Export<'wire>, +} + +impl<'export, 'wire> ExportRenderer<'export, 'wire> { + fn new(index: usize, export: &'export Export<'wire>) -> Self { + Self { index, export } + } +} + +/// Renders one WAT module fragment containing all decoded exports. +pub(super) fn render(exports: &[Export<'_>]) -> Option { + if exports.is_empty() { + return None; + } + + let pointer_type = exports[0].pointer_type(); + let mut items = Vec::with_capacity(exports.len() * 2 + 2); + let mut imports = WatImports::default(); + for (index, export) in exports.iter().enumerate() { + let renderer = ExportRenderer::new(index, export); + for_each_conversion_slot(export, |slot| { + imports.extend(slot.imports()); + }); + if let Callee::Symbol { name } = renderer.export.callee { + let mut identifier = String::new(); + renderer.write_symbol_identifier(&mut identifier); + imports.insert( + &identifier, + renderer.render_symbol_import(&identifier, name), + ); + } + } + + if exports + .iter() + .any(|export| matches!(export.callee, Callee::Closure { .. })) + { + imports.insert( + "js_sys.closure.table", + format!( + "(import \"env\" \"__indirect_function_table\" (table \ + $js_sys.closure.table (@sym (name \"__indirect_function_table\")) \ + {pointer_type} 0 funcref))", + ), + ); + } + + if exports.iter().any(|export| { + export + .output + .as_ref() + .is_some_and(|output| !output.is_direct()) + }) { + imports.insert( + "__stack_pointer", + format!( + "(import \"env\" \"__stack_pointer\" \ + (global $__stack_pointer (mut {pointer_type})))", + ), + ); + } + + let imports = imports.render(); + if !imports.is_empty() { + items.push(imports); + } + + for (index, export) in exports.iter().enumerate() { + let renderer = ExportRenderer::new(index, export); + if renderer.is_closure() { + items.push(renderer.render_closure_type()); + } + } + + for (index, export) in exports.iter().enumerate() { + items.push(ExportRenderer::new(index, export).render_shim()); + } + + Some(items.join("\n")) +} + +impl ExportRenderer<'_, '_> { + fn pointer_type(&self) -> WatType { + self.export.pointer_type() + } + + fn is_indirect(&self) -> bool { + self.export + .output + .as_ref() + .is_some_and(|output| !output.is_direct()) + } + + fn is_closure(&self) -> bool { + matches!(self.export.callee, Callee::Closure { .. }) + } + + fn closure_data_slot(&self) -> &Slot<'_> { + self.export + .inputs + .iter() + .find(|input| input.kind == ExportInputKind::ClosureData) + .and_then(|input| input.slots.first()) + .expect("a closure export has one data slot") + } + + fn write_symbol_identifier(&self, output: &mut String) { + write!(output, "js_sys.export.symbol.{}", self.index) + .expect("writing to a String cannot fail"); + } + + fn render_symbol_import(&self, identifier: &str, symbol: &str) -> String { + let retptr = if self.is_indirect() { + format!(" (param {})", self.pointer_type()) + } else { + String::new() + }; + let mut parameters = String::new(); + for input in &self.export.inputs { + if input.slots.is_empty() { + continue; + } + parameters.push_str(" (param"); + for slot in &input.slots { + write!(parameters, " {}", slot.abi).expect("writing to a String cannot fail"); + } + parameters.push(')'); + } + let result = match self.export.output.as_ref() { + Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.abi), + _ => String::new(), + }; + + format!( + "(import \"env\" \"symbol\" (func ${identifier} \ + (@sym (name \"{symbol}\")){retptr}{parameters}{result}))", + ) + } + + fn render_closure_type(&self) -> String { + let retptr = if self.is_indirect() { + format!(" (param {})", self.pointer_type()) + } else { + String::new() + }; + let mut parameters = String::new(); + for input in &self.export.inputs { + if input.kind != ExportInputKind::Value || input.slots.is_empty() { + continue; + } + parameters.push_str(" (param"); + for slot in &input.slots { + write!(parameters, " {}", slot.abi).expect("writing to a String cannot fail"); + } + parameters.push(')'); + } + let result = match self.export.output.as_ref() { + Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.abi), + _ => String::new(), + }; + + format!( + "(type $js_sys.closure.call.{} (func{retptr} \ + (param {}){parameters}{result}))", + self.index, + self.pointer_type(), + ) + } + + fn render_shim(&self) -> String { + let parameters = self + .export + .inputs + .iter() + .flat_map(|input| { + input.slots.iter().enumerate().map(|(slot_index, slot)| { + let boundary = slot.boundary(); + if input.kind == ExportInputKind::ClosureData { + format!(" (param $data {boundary})") + } else { + format!(" (param ${}_{slot_index} {boundary})", input.name) + } + }) + }) + .collect::(); + let result = self + .export + .output + .as_ref() + .map_or_else(String::new, |output| match output { + ExportOutput::Direct { slot, .. } => format!(" (result {})", slot.boundary()), + ExportOutput::Indirect { frame, .. } => { + let types = frame + .slots + .iter() + .map(|frame_slot| frame_slot.slot.boundary().as_str()) + .collect::>() + .join(" "); + if types.is_empty() { + " (result)".to_owned() + } else { + format!(" (result {types})") + } + } + }); + + let mut wat = format!( + "(func $js_sys.export.{} (@sym (name \"{}\")){parameters}{result}", + self.index, self.export.name, + ); + self.write_prologue(&mut wat); + self.write_call(&mut wat); + self.write_epilogue(&mut wat); + wat.push_str("\n)"); + wat + } + + fn write_prologue(&self, wat: &mut String) { + if self.is_indirect() { + write!(wat, "\n (local $retptr {})", self.pointer_type()) + .expect("writing to a String cannot fail"); + } + if self.is_closure() { + write!( + wat, + "\n (local $js_sys.closure.data {})", + self.pointer_type() + ) + .expect("writing to a String cannot fail"); + } + + let mut locals = WatLocals::default(); + for_each_conversion_slot(self.export, |slot| { + locals.extend(slot.locals()); + }); + let locals = locals.render(); + if !locals.is_empty() { + wat.push('\n'); + wat.push_str(&locals); + } + + if self.is_closure() { + let slot = self.closure_data_slot(); + wat.push_str("\n local.get $data"); + if let Some(instruction) = slot.instruction() { + write_conversion(wat, instruction); + } + wat.push_str("\n local.set $js_sys.closure.data"); + } + + if let Some(ExportOutput::Indirect { frame, .. }) = self.export.output.as_ref() { + write!( + wat, + "\n global.get $__stack_pointer\n {}.const {}\n \ + {}.sub\n local.tee $retptr\n global.set $__stack_pointer", + self.pointer_type(), + frame.size, + self.pointer_type(), + ) + .expect("writing to a String cannot fail"); + } + } + + fn write_call(&self, wat: &mut String) { + match self.export.callee { + Callee::Symbol { .. } => { + if self.is_indirect() { + wat.push_str("\n local.get $retptr"); + } + for input in &self.export.inputs { + write_abi_arguments(wat, input); + } + wat.push_str("\n call $"); + self.write_symbol_identifier(wat); + wat.push_str(" (@reloc)"); + } + Callee::Closure { call_shim_offset } => { + if self.is_indirect() { + wat.push_str("\n local.get $retptr"); + } + wat.push_str("\n local.get $js_sys.closure.data"); + for input in self + .export + .inputs + .iter() + .filter(|input| input.kind == ExportInputKind::Value) + { + write_abi_arguments(wat, input); + } + write!( + wat, + "\n local.get $js_sys.closure.data\n \ + {}.load offset={call_shim_offset}\n \ + call_indirect $js_sys.closure.table \ + (type $js_sys.closure.call.{}) (@reloc)", + self.pointer_type(), + self.index, + ) + .expect("writing to a String cannot fail"); + } + } + } + + fn write_epilogue(&self, wat: &mut String) { + if let Some(output) = self.export.output.as_ref() { + match output { + ExportOutput::Direct { slot, .. } => { + if let Some(instruction) = slot.instruction() { + write_conversion(wat, instruction); + } + } + ExportOutput::Indirect { frame, .. } => { + for frame_slot in &frame.slots { + write!( + wat, + "\n local.get $retptr\n {}.load offset={}", + frame_slot.slot.abi, frame_slot.offset, + ) + .expect("writing to a String cannot fail"); + if let Some(instruction) = frame_slot.slot.instruction() { + write_conversion(wat, instruction); + } + } + write!( + wat, + "\n local.get $retptr\n {}.const {}\n \ + {}.add\n global.set $__stack_pointer", + self.pointer_type(), + frame.size, + self.pointer_type(), + ) + .expect("writing to a String cannot fail"); + } + } + } + } +} + +fn for_each_conversion_slot<'export, 'wire>( + export: &'export Export<'wire>, + mut visit: impl FnMut(&'export Slot<'wire>), +) { + for input in &export.inputs { + for slot in &input.slots { + visit(slot); + } + } + if let Some(output) = export.output.as_ref() { + match output { + ExportOutput::Direct { slot, .. } => visit(slot), + ExportOutput::Indirect { frame, .. } => { + for frame_slot in &frame.slots { + visit(&frame_slot.slot); + } + } + } + } +} + +fn write_abi_arguments(wat: &mut String, input: &ExportInput<'_>) { + for (slot_index, slot) in input.slots.iter().enumerate() { + write!(wat, "\n local.get ${}_{slot_index}", input.name) + .expect("writing to a String cannot fail"); + if let Some(instruction) = slot.instruction() { + write_conversion(wat, instruction); + } + } +} diff --git a/host/ld/src/wire/import/js.rs b/host/ld/src/wire/import/js.rs new file mode 100644 index 00000000..49f2af95 --- /dev/null +++ b/host/ld/src/wire/import/js.rs @@ -0,0 +1,268 @@ +use std::fmt::Write; + +use js_bindgen_wire::model::{ + DirectImportConversion, Import, ImportBinding, ImportCatch, ImportErrorMode, ImportGroup, + ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, +}; + +use crate::wire::{ + JsBinding, + js::{Placeholder, render_template}, +}; + +/// Renders every import which has a generated JavaScript binding. +pub(super) fn render<'a>(group: &ImportGroup<'a>) -> Vec> { + let group_catch = match group.catch.as_ref() { + Some(ImportCatch::JavaScript(catch)) => Some(catch), + Some(ImportCatch::Wasm(_)) | None => None, + }; + + group + .imports + .iter() + .filter_map(|import| { + let binding = import.binding.as_ref()?; + let catches = import + .output + .as_ref() + .is_some_and(|output| output.error == ImportErrorMode::CatchInJavaScript); + let catch = catches.then(|| { + group_catch + .expect("a JavaScript-catching Result import has no JavaScript catch metadata") + }); + let mut embeds = binding.embeds.clone(); + if let Some(catch) = catch { + embeds.extend(catch.embeds.iter().copied()); + } + Some(JsBinding { + module: import.module, + name: import.name, + js: render_binding(import, binding, catch), + embeds, + }) + }) + .collect() +} + +fn render_binding( + import: &Import<'_>, + binding: &ImportBinding<'_>, + catch: Option<&JsCatch<'_>>, +) -> String { + debug_assert!(!import.suspending || catch.is_none()); + let output_needs_wrapper = import + .output + .as_ref() + .is_some_and(|output| output_needs_wrapper(output, catch)); + let needs_wrapper = import + .inputs + .iter() + .any(|input| input.js_conversion.is_some()) + || output_needs_wrapper; + let await_output = import.suspending && output_needs_wrapper; + let parameters = render_parameters(import); + + let js = if needs_wrapper { + let conversions = render_input_conversions(&import.inputs); + let asynchronous = if await_output { "async " } else { "" }; + let body = if let Some(output) = import.output.as_ref() { + render_output(output, binding.call_expression, await_output, catch) + } else { + let return_ = if import.suspending { "return " } else { "" }; + format!(" {return_}{}\n}}", binding.call_expression) + }; + format!("{asynchronous}({parameters}) => {{\n{conversions}{body}") + } else if let Some(direct) = binding.direct_expression { + direct.to_owned() + } else { + format!("({parameters}) => {}", binding.call_expression) + }; + + if import.suspending { + format!("new WebAssembly.Suspending({js})") + } else { + js + } +} + +fn output_needs_wrapper(output: &ImportOutput<'_>, catch: Option<&JsCatch<'_>>) -> bool { + catch.is_some() + || match output.abi { + ImportOutputAbi::Direct { conversion, .. } => conversion.is_some(), + ImportOutputAbi::Indirect { .. } => true, + } +} + +fn render_parameters(import: &Import<'_>) -> String { + let mut parameters = Vec::new(); + if import + .output + .as_ref() + .is_some_and(|output| !output.is_direct()) + { + parameters.push("$retptr".to_owned()); + } + parameters.extend( + import + .inputs + .iter() + .flat_map(|input| (0..input.slots.len()).map(|slot| format!("{}_{slot}", input.name))), + ); + parameters.join(", ") +} + +fn render_input_conversions(inputs: &[ImportInput<'_>]) -> String { + inputs + .iter() + .filter_map(|input| { + let template = input.js_conversion?; + let declaration = if input.slots.is_empty() { "const " } else { "" }; + let expression = render_input_template(template, input.name); + Some(format!( + " {declaration}{}_0 = {expression}\n", + input.name + )) + }) + .collect() +} + +fn render_output( + output: &ImportOutput<'_>, + call: &str, + await_output: bool, + catch: Option<&JsCatch<'_>>, +) -> String { + match &output.abi { + ImportOutputAbi::Direct { conversion, .. } => { + render_direct_output(call, conversion.as_ref(), await_output, catch) + } + ImportOutputAbi::Indirect { retptr, writer } => { + render_indirect_output(call, retptr, writer, await_output, catch) + } + } +} + +fn render_direct_output( + call: &str, + conversion: Option<&DirectImportConversion<'_>>, + await_output: bool, + catch: Option<&JsCatch<'_>>, +) -> String { + let indent = if catch.is_some() { " " } else { " " }; + let call = if await_output { + format!("await ({call})") + } else { + call.to_owned() + }; + let mut js = if catch.is_some() { + " try {\n".to_owned() + } else { + String::new() + }; + + if let Some(conversion) = conversion { + write!(js, "{indent}const $ret = {call}").expect("writing to a String cannot fail"); + js.push_str(&render_prepare(conversion.prepare, indent)); + write!( + js, + "\n{indent}return {}", + render_result_template(conversion.expression), + ) + .expect("writing to a String cannot fail"); + } else { + write!(js, "{indent}return {call}").expect("writing to a String cannot fail"); + } + + js.push_str(catch.map_or("\n}", |catch| catch.direct)); + js +} + +fn render_indirect_output( + call: &str, + retptr: &ImportRetptr<'_>, + writer: &ImportWriter<'_>, + await_output: bool, + catch: Option<&JsCatch<'_>>, +) -> String { + let indent = if catch.is_some() { " " } else { " " }; + let mut js = String::new(); + if let Some(template) = retptr.js_conversion + && !template.is_empty() + { + writeln!(js, " $retptr = {}", render_retptr_template(template)) + .expect("writing to a String cannot fail"); + } + if catch.is_some() { + js.push_str(" try {\n"); + } + + let call = if await_output { + format!("await ({call})") + } else { + call.to_owned() + }; + write!(js, "{indent}const $ret = {call}").expect("writing to a String cannot fail"); + + match writer { + ImportWriter::Slots { + function, + prepare, + expressions, + } => { + js.push_str(&render_prepare(*prepare, indent)); + let mut arguments = expressions + .iter() + .map(|expression| render_result_template(expression)) + .filter(|expression| !expression.is_empty()) + .collect::>(); + arguments.push("$retptr".to_owned()); + write!(js, "\n{indent}{function}({})", arguments.join(", ")) + .expect("writing to a String cannot fail"); + } + ImportWriter::Value { function } => { + write!(js, "\n{indent}{function}($ret, $retptr)") + .expect("writing to a String cannot fail"); + } + } + + js.push_str(catch.map_or("\n}", |catch| catch.indirect)); + js +} + +fn render_prepare(prepare: Option<&str>, indent: &str) -> String { + prepare + .filter(|prepare| !prepare.is_empty()) + .map_or_else(String::new, |prepare| { + format!( + "\n{indent}const $prepared = {}", + render_result_template(prepare), + ) + }) +} + +fn render_input_template(template: &str, name: &str) -> String { + render_template(template, |rendered, placeholder| { + if let Placeholder::Slot(slot) = placeholder { + write!(rendered, "{name}_{slot}").expect("writing to a String cannot fail"); + } + }) +} + +fn render_result_template(template: &str) -> String { + render_template(template, |rendered, placeholder| match placeholder { + Placeholder::Value => rendered.push_str("$ret"), + Placeholder::Prepared => rendered.push_str("$prepared"), + Placeholder::Slot(_) => {} + }) +} + +fn render_retptr_template(template: &str) -> String { + render_template(template, |rendered, placeholder| match placeholder { + Placeholder::Slot(0) => rendered.push_str("$retptr"), + Placeholder::Value => rendered.push_str("$value"), + Placeholder::Prepared => rendered.push_str("$prepared"), + Placeholder::Slot(slot) => { + write!(rendered, "$slot{}", slot + 1).expect("writing to a String cannot fail"); + } + }) +} diff --git a/host/ld/src/wire/import/mod.rs b/host/ld/src/wire/import/mod.rs new file mode 100644 index 00000000..4a13900f --- /dev/null +++ b/host/ld/src/wire/import/mod.rs @@ -0,0 +1,15 @@ +//! Rendering of JavaScript imports and their Wasm `ABI` shims. + +mod js; +mod wat; + +use js_bindgen_wire::model::ImportGroup; + +use super::RenderedGroup; + +pub(super) fn render<'a>(group: &ImportGroup<'a>) -> RenderedGroup<'a> { + RenderedGroup { + bindings: js::render(group), + wat: wat::render(group), + } +} diff --git a/host/ld/src/wire/import/wat.rs b/host/ld/src/wire/import/wat.rs new file mode 100644 index 00000000..a192d744 --- /dev/null +++ b/host/ld/src/wire/import/wat.rs @@ -0,0 +1,264 @@ +use std::fmt::Write; + +use js_bindgen_wire::abi::WatType; +use js_bindgen_wire::model::{ + Import, ImportCatch, ImportErrorMode, ImportGroup, ImportOutput, ImportOutputAbi, Slot, + WatCatch, +}; + +use crate::wire::wat::{WatImports, WatLocals, write_conversion}; + +/// Renders the imported functions followed by their Rust `ABI` shims. +pub(super) fn render(group: &ImportGroup<'_>) -> Option { + if group.imports.is_empty() { + return None; + } + + let group_catch = match group.catch.as_ref() { + Some(ImportCatch::Wasm(catch)) => Some(catch), + Some(ImportCatch::JavaScript(_)) | None => None, + }; + let mut imports = WatImports::default(); + let mut shims = Vec::with_capacity(group.imports.len()); + for import in &group.imports { + let catches = import + .output + .as_ref() + .is_some_and(|output| output.error == ImportErrorMode::CatchInWasm); + let catch = catches.then(|| { + group_catch.expect("a Wasm-catching Result import has no Wasm catch metadata") + }); + render_wat_import(&mut imports, import); + + let mut locals = WatLocals::default(); + for slot in conversion_slots(import) { + imports.extend(slot.imports()); + locals.extend(slot.locals()); + } + if let Some(catch) = catch { + imports.extend(&catch.imports); + locals.extend(&catch.locals); + } + shims.push(Shim { + import, + catch, + locals, + }); + } + + let mut wat = imports.render(); + for shim in shims { + wat.push('\n'); + render_shim(&mut wat, shim); + } + Some(wat) +} + +struct Shim<'group, 'wire> { + import: &'group Import<'wire>, + catch: Option<&'group WatCatch<'wire>>, + locals: WatLocals<'wire>, +} + +// On Wasm32, imports without a return value, with a direct return, and with an +// indirect return respectively render as: +// +// ```wat +// ;; fn notify(value: u32) +// (import "js_sys" "notify" +// (func $js_sys.import.notify +// (@sym (name "js_sys.import.notify")) +// (param i32))) +// +// ;; fn identity(value: u32) -> u32 +// (import "js_sys" "identity" +// (func $js_sys.import.identity +// (@sym (name "js_sys.import.identity")) +// (param i32) +// (result i32))) +// +// ;; fn wide(value: u128) -> u128 +// (import "js_sys" "wide" +// (func $js_sys.import.wide +// (@sym (name "js_sys.import.wide")) +// (param $retptr i32) +// (param i64 i64))) +// ``` +fn render_wat_import(imports: &mut WatImports, import: &Import<'_>) { + let identifier = format!("{}.import.{}", import.module, import.name); + let mut wat = format!( + "(import \"{}\" \"{}\" (func ${identifier} \ + (@sym (name \"{identifier}\"))", + import.module, import.name, + ); + + if let Some(ImportOutput { + abi: ImportOutputAbi::Indirect { retptr, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (param $retptr {})", retptr.slot.boundary()) + .expect("writing to a String cannot fail"); + } + + let input_types = import + .inputs + .iter() + .flat_map(|input| &input.slots) + .map(Slot::boundary) + .map(WatType::as_str) + .collect::>(); + if !input_types.is_empty() { + write!(wat, " (param {})", input_types.join(" ")).expect("writing to a String cannot fail"); + } + + if let Some(ImportOutput { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (result {})", slot.boundary()).expect("writing to a String cannot fail"); + } + + wat.push_str("))"); + imports.insert(&identifier, wat); +} + +// The three imports shown above are exposed to Rust through these `ABI` shims: +// +// ```wat +// ;; fn notify(value: u32) +// (func $js_sys.notify (@sym) (param $value_0 i32) +// local.get $value_0 +// call $js_sys.import.notify (@reloc) +// ) +// +// ;; fn identity(value: u32) -> u32 +// (func $js_sys.identity (@sym) (param $value_0 i32) (result i32) +// local.get $value_0 +// call $js_sys.import.identity (@reloc) +// ) +// +// ;; fn wide(value: u128) -> u128 +// (func $js_sys.wide (@sym) (param $retptr i32) (param $value_0 i64) (param $value_1 i64) +// local.get $retptr +// local.get $value_0 +// local.get $value_1 +// call $js_sys.import.wide (@reloc) +// ) +// ``` +fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { + let Shim { + import, + catch, + locals, + } = shim; + write!(wat, "(func ${}.{} (@sym)", import.module, import.name) + .expect("writing to a String cannot fail"); + + if let Some(ImportOutput { + abi: ImportOutputAbi::Indirect { retptr, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (param $retptr {})", retptr.slot.abi) + .expect("writing to a String cannot fail"); + } + + for input in &import.inputs { + for (index, slot) in input.slots.iter().enumerate() { + write!(wat, " (param ${}_{index} {})", input.name, slot.abi) + .expect("writing to a String cannot fail"); + } + } + + if let Some(ImportOutput { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (result {})", slot.abi).expect("writing to a String cannot fail"); + } + + let locals = locals.render(); + if !locals.is_empty() { + wat.push('\n'); + wat.push_str(&locals); + } + + if let Some(catch) = catch { + wat.push_str(catch.try_); + } + + if let Some(ImportOutput { + abi: ImportOutputAbi::Indirect { retptr, .. }, + .. + }) = import.output.as_ref() + { + write_slot_get(wat, "$retptr", &retptr.slot); + } + + for input in &import.inputs { + for (index, slot) in input.slots.iter().enumerate() { + write!(wat, "\n local.get ${}_{index}", input.name) + .expect("writing to a String cannot fail"); + if let Some(instruction) = slot.instruction() { + write_conversion(wat, instruction); + } + } + } + + write!( + wat, + "\n call ${}.import.{} (@reloc)", + import.module, import.name, + ) + .expect("writing to a String cannot fail"); + + if let Some(ImportOutput { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + && let Some(instruction) = slot.instruction() + { + write_conversion(wat, instruction); + } + + if let Some(catch) = catch { + wat.push_str(catch.catch); + if let Some(ImportOutput { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + { + let zero = slot.abi.zero(); + write!(wat, "\n {zero}").expect("writing to a String cannot fail"); + } + } + + wat.push_str("\n)"); +} + +fn conversion_slots<'import, 'wire>( + import: &'import Import<'wire>, +) -> impl Iterator> { + let retptr = import.output.as_ref().and_then(|output| match &output.abi { + ImportOutputAbi::Indirect { retptr, .. } => Some(&retptr.slot), + ImportOutputAbi::Direct { .. } => None, + }); + let result = import.output.as_ref().and_then(|output| match &output.abi { + ImportOutputAbi::Direct { slot, .. } => Some(slot), + ImportOutputAbi::Indirect { .. } => None, + }); + retptr + .into_iter() + .chain(import.inputs.iter().flat_map(|input| input.slots.iter())) + .chain(result) +} + +fn write_slot_get(wat: &mut String, local: &str, slot: &Slot<'_>) { + write!(wat, "\n local.get {local}").expect("writing to a String cannot fail"); + if let Some(instruction) = slot.instruction() { + write_conversion(wat, instruction); + } +} diff --git a/host/ld/src/wire/js.rs b/host/ld/src/wire/js.rs new file mode 100644 index 00000000..ee5a2bc2 --- /dev/null +++ b/host/ld/src/wire/js.rs @@ -0,0 +1,51 @@ +//! Shared JavaScript template rendering support. + +/// One placeholder recognized in a JavaScript conversion template. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum Placeholder { + Value, + Prepared, + Slot(usize), +} + +const PLACEHOLDERS: [(&str, Placeholder); 6] = [ + ("$value", Placeholder::Value), + ("$prepared", Placeholder::Prepared), + ("$slot1", Placeholder::Slot(0)), + ("$slot2", Placeholder::Slot(1)), + ("$slot3", Placeholder::Slot(2)), + ("$slot4", Placeholder::Slot(3)), +]; + +/// Renders a conversion template using the supplied placeholder resolver. +/// +/// Unrecognized `$` sequences are copied without interpretation. +pub(super) fn render_template( + template: &str, + mut resolve: impl FnMut(&mut String, Placeholder), +) -> String { + let mut output = String::new(); + let bytes = template.as_bytes(); + let mut input = 0; + + while input < bytes.len() { + if bytes[input] == b'$' + && let Some((name, placeholder)) = PLACEHOLDERS + .iter() + .find(|(name, _)| bytes[input..].starts_with(name.as_bytes())) + { + resolve(&mut output, *placeholder); + input += name.len(); + continue; + } + + let start = input; + input += 1; + while input < bytes.len() && bytes[input] != b'$' { + input += 1; + } + output.push_str(&template[start..input]); + } + + output +} diff --git a/host/ld/src/wire/mod.rs b/host/ld/src/wire/mod.rs new file mode 100644 index 00000000..fdd02731 --- /dev/null +++ b/host/ld/src/wire/mod.rs @@ -0,0 +1,51 @@ +//! Rendering of decoded `js-sys` wire records. + +mod export; +mod import; +mod js; +mod wat; + +use js_bindgen_wire::{ + Error, decode, + model::{Embed, Record}, +}; + +/// One JavaScript binding ready for the linker store. +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct JsBinding<'a> { + pub(crate) module: &'a str, + pub(crate) name: &'a str, + pub(crate) js: String, + pub(crate) embeds: Vec>, +} + +/// The JavaScript bindings and WAT shims emitted for one wire record. +pub(crate) struct RenderedGroup<'a> { + pub(crate) bindings: Vec>, + pub(crate) wat: Option, +} + +/// One Rust export with its JavaScript binding and Wasm boundary shim. +pub(crate) enum RenderedExport<'a> { + Symbol { + binding: JsBinding<'a>, + shim: String, + }, + Closure { + binding: JsBinding<'a>, + shim: String, + }, +} + +/// Whether a rendered group describes imports or exports. +pub(crate) enum RenderedRecord<'a> { + Imports(RenderedGroup<'a>), + Exports(Vec>), +} + +pub(crate) fn decode_and_render(bytes: &[u8]) -> Result, Error> { + Ok(match decode(bytes)? { + Record::Imports(group) => RenderedRecord::Imports(import::render(&group)), + Record::Exports(exports) => RenderedRecord::Exports(export::render(&exports)), + }) +} diff --git a/host/ld/src/wire/wat.rs b/host/ld/src/wire/wat.rs new file mode 100644 index 00000000..c0305ab1 --- /dev/null +++ b/host/ld/src/wire/wat.rs @@ -0,0 +1,188 @@ +//! Shared WAT emission helpers. + +use std::collections::HashMap; +use std::fmt::Write; + +use js_bindgen_wire::abi::WatType; +use js_bindgen_wire::model::{WatImport, WatImportKind, WatLocal}; + +#[derive(Default)] +pub(super) struct WatImports { + indices: HashMap, + entries: Vec, +} + +impl WatImports { + pub(super) fn extend(&mut self, imports: &[WatImport<'_>]) { + for import in imports { + let mut wat = String::new(); + write_import(&mut wat, import); + self.insert(import.identifier, wat); + } + } + + pub(super) fn insert(&mut self, identifier: &str, wat: String) { + if let Some(&index) = self.indices.get(identifier) { + assert_eq!( + self.entries[index], wat, + "conflicting WAT imports use `${identifier}`", + ); + } else { + self.indices + .insert(identifier.to_owned(), self.entries.len()); + self.entries.push(wat); + } + } + + pub(super) fn render(self) -> String { + let mut wat = String::new(); + for entry in self.entries { + write_separator(&mut wat); + wat.push_str(&entry); + } + wat + } +} + +#[derive(Default)] +pub(super) struct WatLocals<'wire> { + entries: Vec>, +} + +impl<'wire> WatLocals<'wire> { + pub(super) fn extend(&mut self, locals: &[WatLocal<'wire>]) { + for &local in locals { + if let Some(existing) = self + .entries + .iter() + .find(|existing| existing.name == local.name) + { + assert_eq!( + existing, &local, + "conflicting WAT locals use `${}`", + local.name, + ); + } else { + self.entries.push(local); + } + } + } + + pub(super) fn render(self) -> String { + let mut wat = String::new(); + for local in self.entries { + write_separator(&mut wat); + write!(wat, " (local ${} {})", local.name, local.ty) + .expect("writing to a String cannot fail"); + } + wat + } +} + +pub(super) fn write_conversion(wat: &mut String, conversion: &str) { + if !conversion.is_empty() { + wat.push_str("\n "); + wat.push_str(conversion); + } +} + +fn write_import(wat: &mut String, import: &WatImport<'_>) { + write!(wat, "(import \"{}\" \"{}\" (", import.module, import.name) + .expect("writing to a String cannot fail"); + + match &import.kind { + WatImportKind::Function { + parameters, + results, + } => { + write!(wat, "func ${} ", import.identifier).expect("writing to a String cannot fail"); + write_symbol(wat, import.symbol_name); + write_types(wat, "param", parameters); + write_types(wat, "result", results); + } + WatImportKind::Table { + index_type, + minimum, + maximum, + element, + } => { + write!(wat, "table ${} ", import.identifier).expect("writing to a String cannot fail"); + write_symbol(wat, import.symbol_name); + write!(wat, " {}{minimum}", index_type.wat_prefix()) + .expect("writing to a String cannot fail"); + if let Some(maximum) = maximum { + write!(wat, " {maximum}").expect("writing to a String cannot fail"); + } + write!(wat, " {element}").expect("writing to a String cannot fail"); + } + WatImportKind::Tag { parameters } => { + write!(wat, "tag ${} ", import.identifier).expect("writing to a String cannot fail"); + write_symbol(wat, import.symbol_name); + write_types(wat, "param", parameters); + } + } + + wat.push_str("))"); +} + +fn write_symbol(wat: &mut String, name: Option<&str>) { + if let Some(name) = name { + write!(wat, "(@sym (name \"{name}\"))").expect("writing to a String cannot fail"); + } else { + wat.push_str("(@sym)"); + } +} + +fn write_types(wat: &mut String, kind: &str, types: &[WatType]) { + if types.is_empty() { + return; + } + write!(wat, " ({kind}").expect("writing to a String cannot fail"); + for ty in types { + write!(wat, " {ty}").expect("writing to a String cannot fail"); + } + wat.push(')'); +} + +fn write_separator(wat: &mut String) { + if !wat.is_empty() { + wat.push('\n'); + } +} + +#[cfg(test)] +mod tests { + use super::WatImports; + + #[test] + fn identical_imports_are_emitted_once() { + let mut imports = WatImports::default(); + imports.insert( + "value", + "(import \"env\" \"value\" (global $value i32))".into(), + ); + imports.insert( + "value", + "(import \"env\" \"value\" (global $value i32))".into(), + ); + + assert_eq!( + imports.render(), + "(import \"env\" \"value\" (global $value i32))" + ); + } + + #[test] + #[should_panic(expected = "conflicting WAT imports use `$value`")] + fn conflicting_imports_are_rejected() { + let mut imports = WatImports::default(); + imports.insert( + "value", + "(import \"env\" \"value\" (global $value i32))".into(), + ); + imports.insert( + "value", + "(import \"env\" \"value\" (global $value i64))".into(), + ); + } +} diff --git a/host/macro/src/custom_section.rs b/host/macro/src/custom_section.rs index 220d8233..90378571 100644 --- a/host/macro/src/custom_section.rs +++ b/host/macro/src/custom_section.rs @@ -738,69 +738,71 @@ impl CustomSection { .flatten() } - /// When `framed` is true, the block capacity and used length precede the - /// record length. Otherwise, the layout contains only the record length. - fn output_layout(&self, framed: bool) -> impl Iterator { + /// ```"not rust" + /// #[repr(C)] + /// struct Layout([u8; 4], #([u8; LEN_]),*); + /// ``` + fn output_layout(&self) -> impl Iterator { let span = Span::mixed_site(); - let header_fields = if framed { 3 } else { 1 }; - let tys = (0..header_fields) - .flat_map(move |_| { - [ + // ``` + // [u8; 4], #([u8; LEN_]),* + // ``` + let tys = [ + group( + Delimiter::Bracket, + path(["core", "primitive", "u8"], span).chain([ + Punct::new(';', Spacing::Alone).into(), + Literal::usize_unsuffixed(4).into(), + ]), + ), + Punct::new(',', Spacing::Alone).into(), + ] + .into_iter() + .chain(self.flattened_values().flat_map(move |value| { + value + .cfg_iter() + .chain([ group( Delimiter::Bracket, path(["core", "primitive", "u8"], span).chain([ Punct::new(';', Spacing::Alone).into(), - Literal::usize_unsuffixed(4).into(), + match value.kind { + FlattenedValueKind::Bytes(bytes) => { + Literal::usize_unsuffixed(bytes.len()).into() + } + FlattenedValueKind::Const | FlattenedValueKind::Interpolate => { + ident(&format!("LEN_{}", value.name)) + } + FlattenedValueKind::InterpolateWithLength => { + Literal::usize_unsuffixed(2).into() + } + FlattenedValueKind::TupleCount => { + Literal::usize_unsuffixed(1).into() + } + }, ]), ), Punct::new(',', Spacing::Alone).into(), - ] - }) - .chain(self.flattened_values().flat_map(move |value| { - value - .cfg_iter() - .chain([ - group( - Delimiter::Bracket, - path(["core", "primitive", "u8"], span).chain([ - Punct::new(';', Spacing::Alone).into(), - match value.kind { - FlattenedValueKind::Bytes(bytes) => { - Literal::usize_unsuffixed(bytes.len()).into() - } - FlattenedValueKind::Const | FlattenedValueKind::Interpolate => { - ident(&format!("LEN_{}", value.name)) - } - FlattenedValueKind::InterpolateWithLength => { - Literal::usize_unsuffixed(2).into() - } - FlattenedValueKind::TupleCount => { - Literal::usize_unsuffixed(1).into() - } - }, - ]), - ), - Punct::new(',', Spacing::Alone).into(), - ]) - .chain( - matches!(value.kind, FlattenedValueKind::InterpolateWithLength) - .then(|| { - value.cfg_iter().chain([ - group( - Delimiter::Bracket, - path(["core", "primitive", "u8"], span).chain([ - Punct::new(';', Spacing::Alone).into(), - ident(&format!("LEN_{}", value.name)), - ]), - ), - Punct::new(',', Spacing::Alone).into(), - ]) - }) - .into_iter() - .flatten(), - ) - })); + ]) + .chain( + matches!(value.kind, FlattenedValueKind::InterpolateWithLength) + .then(|| { + value.cfg_iter().chain([ + group( + Delimiter::Bracket, + path(["core", "primitive", "u8"], span).chain([ + Punct::new(';', Spacing::Alone).into(), + ident(&format!("LEN_{}", value.name)), + ]), + ), + Punct::new(',', Spacing::Alone).into(), + ]) + }) + .into_iter() + .flatten(), + ) + })); // ``` // #[repr(C)] @@ -823,7 +825,11 @@ impl CustomSection { .into_iter() } - fn output_custom_section(&self, name: &str, framed: bool) -> impl Iterator { + /// ```"not rust" + /// #[link_section = name] + /// static CUSTOM_SECTION: Layout = Layout(...(u32::to_le_bytes(LEN), #(ARR_),*)); + /// ``` + fn output_custom_section(&self, name: &str) -> impl Iterator { let span = Span::mixed_site(); // ``` @@ -847,23 +853,12 @@ impl CustomSection { ), ]; + // ``` + // (u32::to_le_bytes(LEN), #(ARR_),*) + // ``` let values = group( Delimiter::Parenthesis, - (0..if framed { 2 } else { 0 }) - .flat_map(move |_| { - path(["core", "primitive", "u32", "to_le_bytes"], span).chain([ - group( - Delimiter::Parenthesis, - [ - ident("LEN"), - Punct::new('+', Spacing::Alone).into(), - Literal::u32_unsuffixed(4).into(), - ], - ), - Punct::new(',', Spacing::Alone).into(), - ]) - }) - .chain(path(["core", "primitive", "u32", "to_le_bytes"], span)) + path(["core", "primitive", "u32", "to_le_bytes"], span) .chain([ group(Delimiter::Parenthesis, iter::once(ident("LEN"))), Punct::new(',', Spacing::Alone).into(), @@ -906,7 +901,22 @@ impl CustomSection { link_section.into_iter().chain(custom_section) } - fn output_inner(self, name: &str, framed: bool) -> TokenStream { + /// ```"not rust" + /// const _: () = { + /// const LEN: u32 = { + /// let mut len: usize = 0; + /// #(len += LEN_;)* + /// len as _ + /// }; + /// + /// #[repr(C)] + /// struct Layout([u8; 4], #([u8; LEN_]),*); + /// + /// #[link_section = name] + /// static CUSTOM_SECTION: Layout = Layout(u32::to_le_bytes(LEN), #(ARR_),*); + /// }; + /// ``` + pub fn output(self, name: &str) -> TokenStream { r#const( "_", iter::once(group(Delimiter::Parenthesis, iter::empty())), @@ -915,20 +925,12 @@ impl CustomSection { self.output_values() .chain(self.output_len()) .chain(self.output_tuple_count()) - .chain(self.output_layout(framed)) - .chain(self.output_custom_section(name, framed)), + .chain(self.output_layout()) + .chain(self.output_custom_section(name)), )), ) .collect() } - - pub fn output(self, name: &str) -> TokenStream { - self.output_inner(name, false) - } - - pub fn output_framed(self, name: &str) -> TokenStream { - self.output_inner(name, true) - } } impl Bytes { diff --git a/host/macro/src/lib.rs b/host/macro/src/lib.rs index 98edf1e1..b5af3fce 100644 --- a/host/macro/src/lib.rs +++ b/host/macro/src/lib.rs @@ -30,7 +30,7 @@ fn global_wat_internal(input: TokenStream) -> Result { let mut custom_section = CustomSection::new(); parse_string_arguments(&mut input, Span::mixed_site(), &mut custom_section)?; - Ok(custom_section.output_framed("js_bindgen.wat")) + Ok(custom_section.output("js_bindgen.wat")) } #[proc_macro] @@ -43,7 +43,7 @@ pub fn embed_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream { } fn embed_js_internal(input: TokenStream) -> Result { - js_internal(input, "js_bindgen.embed", false) + js_internal(input, "js_bindgen.embed") } #[proc_macro] @@ -58,29 +58,10 @@ pub fn import_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream } fn import_js_internal(input: TokenStream) -> Result { - js_internal(input, "js_bindgen.import", true) + js_internal(input, "js_bindgen.import") } -#[proc_macro] -pub fn export_js(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream { - #[cfg_attr( - not(test), - expect(clippy::useless_conversion, reason = "`proc-macro2` compatibility") - )] - export_js_internal(input.into()) - .unwrap_or_else(|e| e) - .into() -} - -fn export_js_internal(input: TokenStream) -> Result { - js_internal(input, "js_bindgen.export", false) -} - -fn js_internal( - input: TokenStream, - section: &str, - framed: bool, -) -> Result { +fn js_internal(input: TokenStream, section: &str) -> Result { let mut input = input.into_iter().peekable(); let mut custom_section = CustomSection::new(); @@ -91,11 +72,7 @@ fn js_internal( parse_required_embeds(&mut input, &mut custom_section)?; parse_string_arguments(&mut input, Span::mixed_site(), &mut custom_section)?; - Ok(if framed { - custom_section.output_framed(section) - } else { - custom_section.output(section) - }) + Ok(custom_section.output(section)) } fn parse_required_embeds( diff --git a/host/macro/src/tests/global_wat.rs b/host/macro/src/tests/global_wat.rs index 481da417..1bc85d3d 100644 --- a/host/macro/src/tests/global_wat.rs +++ b/host/macro/src/tests/global_wat.rs @@ -14,20 +14,10 @@ fn basic() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 7], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; 7]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -43,18 +33,10 @@ fn minimum() { len as _ }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - ); + struct Layout([::core::primitive::u8; 4]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN)); }; }); } @@ -73,20 +55,10 @@ fn no_newline() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 3], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; 3]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -124,8 +96,6 @@ fn merge_strings() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 12], #[cfg(test)] [::core::primitive::u8; 4], @@ -134,8 +104,6 @@ fn merge_strings() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, #[cfg(test)] @@ -182,8 +150,6 @@ fn merge_edge_1() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 1], #[cfg(test)] [::core::primitive::u8; 1], @@ -193,8 +159,6 @@ fn merge_edge_1() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, #[cfg(test)] @@ -231,16 +195,12 @@ fn merge_edge_2() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], #[cfg(test)] [::core::primitive::u8; 1], ); #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), #[cfg(test)] ARR_0, @@ -278,8 +238,6 @@ fn cfg() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 6], #[cfg(test)] [::core::primitive::u8; 6], @@ -288,8 +246,6 @@ fn cfg() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, #[cfg(test)] @@ -314,20 +270,10 @@ fn escape() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 6], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; 6]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -348,20 +294,10 @@ fn escape_newline() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 8], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; 8]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -383,20 +319,10 @@ fn interpolate() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; LEN_0], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; LEN_0]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -418,20 +344,10 @@ fn r#const() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; LEN_0], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; LEN_0]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -467,8 +383,6 @@ fn interpolate_macro() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; LEN_0], [::core::primitive::u8; 1], @@ -477,8 +391,6 @@ fn interpolate_macro() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, ARR_1, @@ -505,20 +417,11 @@ fn named_const() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; LEN_par], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; LEN_par]); #[unsafe(link_section = "js_bindgen.wat")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_par, - ); + static CUSTOM_SECTION: Layout = + Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_par); }; }); } @@ -553,16 +456,12 @@ fn named_cfg() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], #[cfg(test)] [::core::primitive::u8; LEN_par], ); #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), #[cfg(test)] ARR_par, @@ -614,8 +513,6 @@ fn named_cfg_2() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; LEN_par_1], [::core::primitive::u8; 1], @@ -624,8 +521,6 @@ fn named_cfg_2() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_par_1, ARR_0, @@ -680,8 +575,6 @@ fn named_cfg_same() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], #[cfg(test)] [::core::primitive::u8; LEN_par], #[cfg(not(test))] [::core::primitive::u8; LEN_par], @@ -689,8 +582,6 @@ fn named_cfg_same() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), #[cfg(test)] ARR_par, @@ -741,8 +632,6 @@ fn named_const_cfg_same() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], #[cfg(test)] [::core::primitive::u8; LEN_par], #[cfg(not(test))] [::core::primitive::u8; LEN_par], @@ -750,8 +639,6 @@ fn named_const_cfg_same() { #[unsafe(link_section = "js_bindgen.wat")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), #[cfg(test)] ARR_par, diff --git a/host/macro/src/tests/import_js.rs b/host/macro/src/tests/import_js.rs index d2853b56..2fe8a42d 100644 --- a/host/macro/src/tests/import_js.rs +++ b/host/macro/src/tests/import_js.rs @@ -17,20 +17,10 @@ fn basic() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 18], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; 18]); #[unsafe(link_section = "js_bindgen.import")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -70,8 +60,6 @@ fn required_embeds() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 11], [::core::primitive::u8; 2], @@ -82,8 +70,6 @@ fn required_embeds() { #[unsafe(link_section = "js_bindgen.import")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, VAL_1_LEN, @@ -130,8 +116,6 @@ fn required_embeds_expr() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 11], [::core::primitive::u8; 2], @@ -142,8 +126,6 @@ fn required_embeds_expr() { #[unsafe(link_section = "js_bindgen.import")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, VAL_1_LEN, @@ -216,8 +198,6 @@ fn required_embeds_cfg() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 10], [::core::primitive::u8; 1], @@ -229,8 +209,6 @@ fn required_embeds_cfg() { #[unsafe(link_section = "js_bindgen.import")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, [TUPLE_COUNT], @@ -299,8 +277,6 @@ fn required_embeds_multiple() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 11], [::core::primitive::u8; 2], @@ -315,8 +291,6 @@ fn required_embeds_multiple() { #[unsafe(link_section = "js_bindgen.import")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, VAL_1_LEN, @@ -362,20 +336,10 @@ fn required_embeds_empty() { }; #[repr(C)] - struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], - [::core::primitive::u8; 11], - ); + struct Layout([::core::primitive::u8; 4], [::core::primitive::u8; 11]); #[unsafe(link_section = "js_bindgen.import")] - static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN), - ARR_0, - ); + static CUSTOM_SECTION: Layout = Layout(::core::primitive::u32::to_le_bytes(LEN), ARR_0); }; }); } @@ -523,8 +487,6 @@ fn required_embeds_mixed() { #[repr(C)] struct Layout( - [::core::primitive::u8; 4], - [::core::primitive::u8; 4], [::core::primitive::u8; 4], [::core::primitive::u8; 10], [::core::primitive::u8; 1], @@ -548,8 +510,6 @@ fn required_embeds_mixed() { #[unsafe(link_section = "js_bindgen.import")] static CUSTOM_SECTION: Layout = Layout( - ::core::primitive::u32::to_le_bytes(LEN + 4), - ::core::primitive::u32::to_le_bytes(LEN + 4), ::core::primitive::u32::to_le_bytes(LEN), ARR_0, [TUPLE_COUNT], diff --git a/host/macro/tests/ui/custom_section.stderr b/host/macro/tests/ui/custom_section.stderr index 75a2a00c..a3f441b7 100644 --- a/host/macro/tests/ui/custom_section.stderr +++ b/host/macro/tests/ui/custom_section.stderr @@ -21,19 +21,28 @@ error[E0308]: mismatched types --> tests/ui/custom_section.rs:6:50 | 6 | js_bindgen::unsafe_global_wat!("{}", interpolate Bar); - | ^^^ expected `&str`, found `Bar` + | -------------------------------------------------^^^- + | | | + | | expected `&str`, found `Bar` + | expected because of the type of the constant error[E0308]: mismatched types --> tests/ui/custom_section.rs:9:50 | 9 | js_bindgen::unsafe_global_wat!("{}", interpolate 42); - | ^^ expected `&str`, found integer + | -------------------------------------------------^^- + | | | + | | expected `&str`, found integer + | expected because of the type of the constant error[E0308]: mismatched types --> tests/ui/custom_section.rs:12:73 | 12 | js_bindgen::import_js!(module = "foo", name = "bar", required_embeds = [42], ""); - | ^^ expected `(&str, &str)`, found integer + | ------------------------------------------------------------------------^^------ + | | | + | | expected `(&str, &str)`, found integer + | expected because of the type of the constant | = note: expected tuple `(&'static str, &'static str)` found type `{integer}` @@ -42,7 +51,10 @@ error[E0308]: mismatched types --> tests/ui/custom_section.rs:15:72 | 15 | js_bindgen::embed_js!(module = "foo", name = "bar", required_embeds = [42], ""); - | ^^ expected `(&str, &str)`, found integer + | -----------------------------------------------------------------------^^------ + | | | + | | expected `(&str, &str)`, found integer + | expected because of the type of the constant | = note: expected tuple `(&'static str, &'static str)` found type `{integer}` @@ -51,7 +63,10 @@ error[E0308]: mismatched types --> tests/ui/custom_section.rs:18:73 | 18 | js_bindgen::import_js!(module = "foo", name = "bar", required_embeds = ["qux"], ""); - | ^^^^^ expected `(&str, &str)`, found `&str` + | ------------------------------------------------------------------------^^^^^------ + | | | + | | expected `(&str, &str)`, found `&str` + | expected because of the type of the constant | = note: expected tuple `(&'static str, &'static str)` found reference `&'static str` @@ -59,8 +74,14 @@ error[E0308]: mismatched types error[E0308]: mismatched types --> tests/ui/custom_section.rs:24:21 | -24 | required_embeds = [("qux")], - | ^^^^^^^ expected `(&str, &str)`, found `&str` +21 | / js_bindgen::import_js!( +22 | | module = "foo", +23 | | name = "bar", +24 | | required_embeds = [("qux")], + | | ^^^^^^^ expected `(&str, &str)`, found `&str` +25 | | "" +26 | | ); + | |_- expected because of the type of the constant | = note: expected tuple `(&'static str, &'static str)` found reference `&'static str` @@ -68,8 +89,14 @@ error[E0308]: mismatched types error[E0308]: mismatched types --> tests/ui/custom_section.rs:32:21 | -32 | required_embeds = [("qux",)], - | ^^^^^^^^ expected a tuple with 2 elements, found one with 1 element +29 | / js_bindgen::import_js!( +30 | | module = "foo", +31 | | name = "bar", +32 | | required_embeds = [("qux",)], + | | ^^^^^^^^ expected a tuple with 2 elements, found one with 1 element +33 | | "" +34 | | ); + | |_- expected because of the type of the constant | = note: expected tuple `(&'static str, &'static str)` found tuple `(&'static str,)` diff --git a/host/wire/Cargo.toml b/host/wire/Cargo.toml new file mode 100644 index 00000000..6910f386 --- /dev/null +++ b/host/wire/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "js-bindgen-wire" +version = "0.1.0" +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +include = { workspace = true } + +[features] +default = [] +alloc = [] + +[lib] +bench = false +doctest = false + +[lints] +workspace = true diff --git a/host/wire/LICENSE-APACHE b/host/wire/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/host/wire/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/host/wire/LICENSE-MIT b/host/wire/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/host/wire/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/host/wire/src/abi.rs b/host/wire/src/abi.rs new file mode 100644 index 00000000..ff1127bd --- /dev/null +++ b/host/wire/src/abi.rs @@ -0,0 +1,489 @@ +//! Shared JavaScript-boundary `ABI` descriptions. + +#[cfg(feature = "alloc")] +use core::fmt; + +/// One primitive `WebAssembly` value type. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum WatType { + I32, + I64, + F32, + F64, + V128, + ExternRef, + FuncRef, +} + +impl WatType { + #[cfg(feature = "alloc")] + pub(crate) const MAX_TAG: u8 = 6; + + #[must_use] + #[cfg(feature = "alloc")] + pub const fn as_str(self) -> &'static str { + match self { + Self::I32 => "i32", + Self::I64 => "i64", + Self::F32 => "f32", + Self::F64 => "f64", + Self::V128 => "v128", + Self::ExternRef => "externref", + Self::FuncRef => "funcref", + } + } + + /// Returns an instruction that places this type's default value on the stack. + #[must_use] + #[cfg(feature = "alloc")] + pub const fn zero(self) -> &'static str { + match self { + Self::I32 => "i32.const 0", + Self::I64 => "i64.const 0", + Self::F32 => "f32.const 0", + Self::F64 => "f64.const 0", + Self::V128 => "v128.const i32x4 0 0 0 0", + Self::ExternRef => "ref.null extern", + Self::FuncRef => "ref.null func", + } + } + + pub(crate) const fn tag(self) -> u8 { + match self { + Self::I32 => 0, + Self::I64 => 1, + Self::F32 => 2, + Self::F64 => 3, + Self::V128 => 4, + Self::ExternRef => 5, + Self::FuncRef => 6, + } + } + + #[cfg(feature = "alloc")] + pub(crate) const fn from_tag(tag: u8) -> Option { + match tag { + 0 => Some(Self::I32), + 1 => Some(Self::I64), + 2 => Some(Self::F32), + 3 => Some(Self::F64), + 4 => Some(Self::V128), + 5 => Some(Self::ExternRef), + 6 => Some(Self::FuncRef), + _ => None, + } + } +} + +#[cfg(feature = "alloc")] +impl fmt::Display for WatType { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// The index type of a `WebAssembly` table. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum WatIndexType { + I32, + I64, +} + +impl WatIndexType { + #[cfg(feature = "alloc")] + pub(crate) const MAX_TAG: u8 = 1; + + #[must_use] + #[cfg(feature = "alloc")] + pub const fn as_str(self) -> &'static str { + match self { + Self::I32 => "i32", + Self::I64 => "i64", + } + } + + /// Returns the explicit table-index prefix used by canonical WAT. + #[must_use] + #[cfg(feature = "alloc")] + pub const fn wat_prefix(self) -> &'static str { + match self { + Self::I32 => "", + Self::I64 => "i64 ", + } + } + + pub(crate) const fn tag(self) -> u8 { + match self { + Self::I32 => 0, + Self::I64 => 1, + } + } + + #[cfg(feature = "alloc")] + pub(crate) const fn from_tag(tag: u8) -> Option { + match tag { + 0 => Some(Self::I32), + 1 => Some(Self::I64), + _ => None, + } + } +} + +#[cfg(feature = "alloc")] +impl fmt::Display for WatIndexType { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// A reference type accepted by a `WebAssembly` table. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RefType { + ExternRef, + FuncRef, +} + +impl RefType { + #[cfg(feature = "alloc")] + pub(crate) const MAX_TAG: u8 = 1; + + #[must_use] + #[cfg(feature = "alloc")] + pub const fn as_str(self) -> &'static str { + match self { + Self::ExternRef => "externref", + Self::FuncRef => "funcref", + } + } + + pub(crate) const fn tag(self) -> u8 { + match self { + Self::ExternRef => 0, + Self::FuncRef => 1, + } + } + + #[cfg(feature = "alloc")] + pub(crate) const fn from_tag(tag: u8) -> Option { + match tag { + 0 => Some(Self::ExternRef), + 1 => Some(Self::FuncRef), + _ => None, + } + } +} + +#[cfg(feature = "alloc")] +impl fmt::Display for RefType { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// One JavaScript source fragment required by a generated binding. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct JsEmbed { + pub module: &'static str, + pub name: &'static str, +} + +impl JsEmbed { + #[must_use] + pub const fn new(module: &'static str, name: &'static str) -> Self { + Self { module, name } + } +} + +/// JavaScript exception-catching support shared by one import type table. +/// +/// The `renderer` places the appropriate catch suffix after the successful +/// direct or indirect result path. `embeds` contains the JavaScript `runtime` +/// values referenced by those suffixes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct JsCatch { + pub embeds: &'static [JsEmbed], + pub direct: &'static str, + pub indirect: &'static str, +} + +impl JsCatch { + #[must_use] + pub const fn new( + embeds: &'static [JsEmbed], + direct: &'static str, + indirect: &'static str, + ) -> Self { + Self { + embeds, + direct, + indirect, + } + } +} + +/// One structured WAT import required by a boundary conversion. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WatImport { + pub module: &'static str, + pub name: &'static str, + pub identifier: &'static str, + pub symbol_name: Option<&'static str>, + pub kind: WatImportKind, +} + +impl WatImport { + #[must_use] + pub const fn new( + module: &'static str, + name: &'static str, + identifier: &'static str, + symbol_name: Option<&'static str>, + kind: WatImportKind, + ) -> Self { + Self { + module, + name, + identifier, + symbol_name, + kind, + } + } +} + +/// The kind and type of one structured WAT import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WatImportKind { + Function { + parameters: &'static [WatType], + results: &'static [WatType], + }, + Table { + index_type: WatIndexType, + minimum: u64, + maximum: Option, + element: RefType, + }, + Tag { + parameters: &'static [WatType], + }, +} + +/// One structured local required by a boundary conversion. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WatLocal { + pub name: &'static str, + pub ty: WatType, +} + +impl WatLocal { + #[must_use] + pub const fn new(name: &'static str, ty: WatType) -> Self { + Self { name, ty } + } +} + +/// Wasm exception-catching support shared by one import type table. +/// +/// `try_` is inserted before the imported call and its boundary conversions; +/// `catch` is inserted after the successful path. The `renderer` remains +/// responsible for the direct result's type-specific fallback value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WatCatch { + pub imports: &'static [WatImport], + pub locals: &'static [WatLocal], + pub try_: &'static str, + pub catch: &'static str, +} + +impl WatCatch { + #[must_use] + pub const fn new( + imports: &'static [WatImport], + locals: &'static [WatLocal], + try_: &'static str, + catch: &'static str, + ) -> Self { + Self { + imports, + locals, + try_, + catch, + } + } +} + +/// `WAT` required to translate one primitive `WebAssembly` slot. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WatConv { + pub imports: &'static [WatImport], + pub locals: &'static [WatLocal], + pub instruction: &'static str, + pub boundary: WatType, +} + +impl WatConv { + #[must_use] + pub const fn new( + imports: &'static [WatImport], + locals: &'static [WatLocal], + instruction: &'static str, + boundary: WatType, + ) -> Self { + Self { + imports, + locals, + instruction, + boundary, + } + } +} + +/// One primitive `WebAssembly` slot and its boundary conversion. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WatSlot { + pub abi: WatType, + pub wat: Option, +} + +impl WatSlot { + #[must_use] + pub const fn new(abi: WatType, wat: Option) -> Self { + Self { abi, wat } + } + + #[must_use] + pub const fn plain(abi: WatType) -> Self { + Self::new(abi, None) + } +} + +/// Converts primitive `ABI` slots into one JavaScript value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct IntoJsConv { + pub embed: Option, + pub template: &'static str, +} + +impl IntoJsConv { + #[must_use] + pub const fn new(template: &'static str) -> Self { + Self { + embed: None, + template, + } + } + + #[must_use] + pub const fn with_embed(mut self, module: &'static str, name: &'static str) -> Self { + self.embed = Some(JsEmbed::new(module, name)); + self + } +} + +/// Converts one JavaScript value into primitive `ABI` slots. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FromJsConv { + pub embed: Option, + pub prepare: Option<&'static str>, + pub templates: [Option<&'static str>; 4], +} + +impl FromJsConv { + #[must_use] + pub const fn slot1(template: &'static str) -> Self { + Self { + embed: None, + prepare: None, + templates: [Some(template), None, None, None], + } + } + + #[must_use] + pub const fn prepare(mut self, template: &'static str) -> Self { + self.prepare = Some(template); + self + } + + #[must_use] + pub const fn slot2(mut self, template: &'static str) -> Self { + self.templates[1] = Some(template); + self + } + + #[must_use] + pub const fn slot3(mut self, template: &'static str) -> Self { + self.templates[2] = Some(template); + self + } + + #[must_use] + pub const fn slot4(mut self, template: &'static str) -> Self { + self.templates[3] = Some(template); + self + } + + #[must_use] + pub const fn with_embed(mut self, module: &'static str, name: &'static str) -> Self { + self.embed = Some(JsEmbed::new(module, name)); + self + } +} + +/// Selects how an indirect JavaScript import result is written to Rust. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Sret { + Slots(&'static str), + Value(&'static str), +} + +/// Describes how a function return is handled at the JavaScript boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ReturnConv { + Value(Option), + Result(Option), +} + +impl ReturnConv { + #[must_use] + pub const fn conversion(self) -> Option { + match self { + Self::Value(value) | Self::Result(value) => value, + } + } + + #[must_use] + pub const fn is_result(self) -> bool { + matches!(self, Self::Result(_)) + } +} + +/// Describes how a Rust function return is represented in its C `ABI`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ReturnMode { + Direct, + Indirect, +} + +impl ReturnMode { + #[must_use] + pub const fn is_direct(self) -> bool { + matches!(self, Self::Direct) + } +} + +/// Positions of an exported Result's control slots. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ResultLayout { + pub discriminant: u8, + pub error: u8, +} + +impl ResultLayout { + #[must_use] + pub const fn new(discriminant: u8, error: u8) -> Self { + Self { + discriminant, + error, + } + } +} diff --git a/host/wire/src/decode/export.rs b/host/wire/src/decode/export.rs new file mode 100644 index 00000000..c7e1596a --- /dev/null +++ b/host/wire/src/decode/export.rs @@ -0,0 +1,201 @@ +use alloc::vec::Vec; + +use crate::{ + EXPORT_FLAGS, EXPORT_HAS_OUTPUT, EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_FLAGS, + EXPORT_OUTPUT_RESULT, EXPORT_PROMISING, Error, PointerWidth, SLOT_COUNT, + model::{ + Callee, Embed, Export, ExportInput, ExportInputConversion, ExportInputKind, ExportOutput, + FrameSlot, ResultLayout, ReturnFrame, + }, +}; + +use super::{Decode, Decoder, value}; + +pub(super) fn decode<'a>( + decoder: &mut Decoder<'a>, + pointer_width: PointerWidth, +) -> Result>, Error> { + let count = decoder.count("export")?; + let mut exports = Vec::with_capacity(count); + for _ in 0..count { + exports.push(Export::decode_with(decoder, pointer_width)?); + } + Ok(exports) +} + +impl<'a> Export<'a> { + fn decode_with(decoder: &mut Decoder<'a>, pointer_width: PointerWidth) -> Result { + let module = decoder.string()?; + let name = decoder.string()?; + let flags = decoder.flags("export", EXPORT_FLAGS)?; + let callee = Callee::decode(decoder)?; + + let input_count = decoder.count("export input")?; + let mut inputs = Vec::with_capacity(input_count); + let mut embeds = Vec::new(); + for index in 0..input_count { + let kind = if matches!(callee, Callee::Closure { .. }) && index == 0 { + ExportInputKind::ClosureData + } else { + ExportInputKind::Value + }; + let (input, embed) = ExportInput::decode_with(decoder, kind)?; + inputs.push(input); + embeds.extend(embed); + } + if matches!(callee, Callee::Closure { .. }) { + decoder.ensure( + inputs.first().is_some_and(|input| { + input.slots.len() == 1 && input.slots[0].abi == pointer_width.wat_type() + }), + "closure export data must occupy one pointer slot", + )?; + } + + let output = if flags & EXPORT_HAS_OUTPUT != 0 { + let (output, embed) = ExportOutput::decode_with(decoder)?; + embeds.extend(embed); + Some(output) + } else { + None + }; + + Ok(Self { + module, + name, + pointer_width, + inputs, + output, + embeds, + promising: flags & EXPORT_PROMISING != 0, + callee, + }) + } +} + +impl<'a> Decode<'a> for Callee<'a> { + fn decode(decoder: &mut Decoder<'a>) -> Result { + match decoder.tag("export callee", 1)? { + 0 => { + let name = decoder.string()?; + decoder.ensure(!name.is_empty(), "export callee has an empty symbol name")?; + Ok(Self::Symbol { name }) + } + 1 => Ok(Self::Closure { + call_shim_offset: decoder.u64()?, + }), + _ => unreachable!(), + } + } +} + +impl<'a> ExportInput<'a> { + fn decode_with( + decoder: &mut Decoder<'a>, + kind: ExportInputKind, + ) -> Result<(Self, Option>), Error> { + let name = decoder.string()?; + let slots = value::slots(decoder)?; + let conversion = value::optional_from_js_conversion(decoder)?; + let (conversion, embed) = if let Some(conversion) = conversion { + let embed = conversion.embed; + value::validate_templates(decoder, &slots, &conversion.templates)?; + let conversion = Some(ExportInputConversion { + prepare: conversion.prepare, + expressions: conversion.templates.into_iter().flatten().collect(), + }); + (conversion, embed) + } else { + (None, None) + }; + let slots = value::compact_abi(decoder, &slots)?; + decoder.ensure( + conversion.is_some() || slots.len() <= 1, + "multi-slot export input has no JavaScript conversion", + )?; + Ok(( + Self { + kind, + name, + slots, + conversion, + }, + embed, + )) + } +} + +impl<'a> ExportOutput<'a> { + fn decode_with(decoder: &mut Decoder<'a>) -> Result<(Self, Option>), Error> { + let flags = decoder.flags("export output", EXPORT_OUTPUT_FLAGS)?; + let slots = value::slots(decoder)?; + let conversion = value::optional_into_js_conversion(decoder)?; + let (embed, js_conversion) = match conversion { + Some((embed, template)) => (embed, Some(template)), + None => (None, None), + }; + let frame_size = decoder.u64()?; + let slot_offsets = [ + decoder.u64()?, + decoder.u64()?, + decoder.u64()?, + decoder.u64()?, + ]; + let direct = flags & EXPORT_OUTPUT_DIRECT != 0; + let result = flags & EXPORT_OUTPUT_RESULT != 0; + let result_layout = if result { + Some(ResultLayout { + discriminant: decoder.u8()?, + error: decoder.u8()?, + }) + } else { + None + }; + + decoder.ensure( + !direct || frame_size == 0 && slot_offsets == [0; SLOT_COUNT], + "direct export output unexpectedly has a return frame", + )?; + decoder.ensure( + direct || frame_size != 0, + "indirect export output is missing its return frame", + )?; + let output = if direct { + let slots = value::compact(&slots); + decoder.ensure(slots.len() == 1, "direct export output must have one slot")?; + decoder.ensure(!result, "direct export output cannot be a Result")?; + ExportOutput::Direct { + slot: slots[0].clone(), + js_conversion, + } + } else { + // Result control slots can follow empty value slots, as in `Result<()>`, + // so export outputs cannot require a contiguous `ABI` prefix. + let frame_slots: Vec<_> = slots + .into_iter() + .zip(slot_offsets) + .filter_map(|(slot, offset)| slot.map(|slot| FrameSlot { slot, offset })) + .collect(); + let result = if let Some(result) = result_layout { + let discriminant = usize::from(result.discriminant); + let error = usize::from(result.error); + decoder.ensure( + error == discriminant + 1 && error + 1 == frame_slots.len(), + "Result export control slots must terminate the return frame", + )?; + Some(result) + } else { + None + }; + ExportOutput::Indirect { + frame: ReturnFrame { + size: frame_size, + slots: frame_slots, + }, + js_conversion, + result, + } + }; + Ok((output, embed)) + } +} diff --git a/host/wire/src/decode/import.rs b/host/wire/src/decode/import.rs new file mode 100644 index 00000000..effc5b35 --- /dev/null +++ b/host/wire/src/decode/import.rs @@ -0,0 +1,353 @@ +use alloc::{rc::Rc, vec::Vec}; + +use crate::{ + Error, IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_FLAGS, IMPORT_HAS_BINDING, + IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_FLAGS, IMPORT_OUTPUT_RESULT, + IMPORT_SUSPENDING, PointerWidth, + abi::WatType, + model::{ + DirectImportConversion, Embed, Import, ImportBinding, ImportCatch, ImportErrorMode, + ImportGroup, ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, + JsCatch, Slot, WatCatch, + }, +}; + +use super::{Decode, Decoder, value}; + +struct InputType<'a> { + slots: Vec>, + js_conversion: Option<&'a str>, + embed: Option>, +} + +struct OutputType<'a> { + abi: ImportOutputAbi<'a>, + embeds: [Option>; 2], + result: bool, +} + +pub(super) fn decode<'a>( + decoder: &mut Decoder<'a>, + pointer_width: PointerWidth, +) -> Result, Error> { + let input_type_count = decoder.count("import input type")?; + let mut input_types = Vec::with_capacity(input_type_count); + for _ in 0..input_type_count { + input_types.push(InputType::decode(decoder)?); + } + + let output_type_count = decoder.count("import output type")?; + let pointer_type = if output_type_count == 0 { + None + } else { + let pointer = InputType::decode(decoder)?; + decoder.ensure( + pointer.slots.len() == 1 && pointer.slots[0].abi == pointer_width.wat_type(), + "import return pointer does not match the target pointer width", + )?; + Some(pointer) + }; + let mut output_types = Vec::with_capacity(output_type_count); + for _ in 0..output_type_count { + let Some(pointer) = pointer_type.as_ref() else { + return decoder.invalid("a non-empty output table has no pointer type"); + }; + output_types.push(OutputType::decode_with(decoder, pointer)?); + } + let catch = if output_types.iter().any(|output| output.result) { + Some(decode_catch(decoder)?) + } else { + None + }; + + let import_count = decoder.count("import")?; + let mut imports = Vec::with_capacity(import_count); + for _ in 0..import_count { + imports.push(Import::decode_with( + decoder, + &input_types, + &output_types, + catch.as_ref(), + )?); + } + Ok(ImportGroup { catch, imports }) +} + +fn decode_catch<'a>(decoder: &mut Decoder<'a>) -> Result, Error> { + Ok(match decoder.tag("import catch", IMPORT_CATCH_WASM)? { + IMPORT_CATCH_JAVASCRIPT => { + let embed_count = decoder.count("import catch embed")?; + let mut embeds = Vec::with_capacity(embed_count); + for _ in 0..embed_count { + embeds.push(Embed { + module: decoder.string()?, + name: decoder.string()?, + }); + } + let direct = decoder.string()?; + let indirect = decoder.string()?; + decoder.ensure(!direct.is_empty(), "direct import catch template is empty")?; + decoder.ensure( + !indirect.is_empty(), + "indirect import catch template is empty", + )?; + ImportCatch::JavaScript(JsCatch { + embeds: Rc::from(embeds), + direct, + indirect, + }) + } + IMPORT_CATCH_WASM => { + let imports = value::decode_imports(decoder)?; + let locals = value::decode_locals(decoder)?; + let try_ = decoder.string()?; + let catch = decoder.string()?; + decoder.ensure(!try_.is_empty(), "WAT import try template is empty")?; + decoder.ensure(!catch.is_empty(), "WAT import catch template is empty")?; + ImportCatch::Wasm(WatCatch { + imports, + locals, + try_, + catch, + }) + } + _ => unreachable!(), + }) +} + +impl<'a> Decode<'a> for InputType<'a> { + fn decode(decoder: &mut Decoder<'a>) -> Result { + let slots = value::slots(decoder)?; + let slots = value::compact_abi(decoder, &slots)?; + let conversion = value::optional_into_js_conversion(decoder)?; + let (embed, js_conversion) = match conversion { + Some((embed, template)) => (embed, Some(template)), + None => (None, None), + }; + decoder.ensure( + js_conversion.is_some() || slots.len() <= 1, + "multi-slot import input has no JavaScript conversion", + )?; + Ok(Self { + slots, + js_conversion, + embed, + }) + } +} + +impl<'a> OutputType<'a> { + fn decode_with(decoder: &mut Decoder<'a>, pointer: &InputType<'a>) -> Result { + let flags = decoder.flags("import output", IMPORT_OUTPUT_FLAGS)?; + + let direct = flags & IMPORT_OUTPUT_DIRECT != 0; + let result = flags & IMPORT_OUTPUT_RESULT != 0; + let slots = value::slots(decoder)?; + value::validate_abi(decoder, &slots)?; + let conversion = value::optional_from_js_conversion(decoder)?; + let has_conversion = conversion.is_some(); + if let Some(conversion) = &conversion { + value::validate_templates(decoder, &slots, &conversion.templates)?; + } + let sret = if direct { + None + } else { + Some(value::sret(decoder)?) + }; + let (embed, prepare, templates, writer) = match conversion { + Some(conversion) => { + let writer = match sret { + Some(value::Sret::Slots(function)) => Some(ImportWriter::Slots { + function, + prepare: conversion.prepare, + expressions: conversion.templates.iter().copied().flatten().collect(), + }), + Some(value::Sret::Value(function)) => Some(ImportWriter::Value { function }), + None => None, + }; + ( + conversion.embed, + conversion.prepare, + conversion.templates, + writer, + ) + } + None => (None, None, [None; 4], None), + }; + decoder.ensure( + has_conversion || sret.is_none(), + "import output has a writer without a JavaScript conversion", + )?; + decoder.ensure( + pointer.slots.len() == 1, + "import return pointer must have one slot", + )?; + let abi = if direct { + let slots = value::compact(&slots); + decoder.ensure(slots.len() == 1, "direct import output must have one slot")?; + if result { + decoder.ensure( + matches!( + slots[0].abi, + WatType::I32 | WatType::I64 | WatType::F32 | WatType::F64 + ), + "unsupported direct Result return slot", + )?; + } + decoder.ensure(writer.is_none(), "direct import output has an sret writer")?; + ImportOutputAbi::Direct { + slot: slots[0].clone(), + conversion: if let Some(expression) = templates[0] { + Some(DirectImportConversion { + prepare, + expression, + }) + } else { + decoder.ensure( + prepare.is_none(), + "import output prepares a missing conversion", + )?; + None + }, + } + } else { + decoder.ensure( + templates[0].is_some(), + "indirect import output has no conversion", + )?; + let Some(writer) = writer else { + return decoder.invalid("indirect import output has no sret writer"); + }; + ImportOutputAbi::Indirect { + retptr: ImportRetptr { + slot: pointer.slots[0].clone(), + js_conversion: pointer.js_conversion, + }, + writer, + } + }; + + let pointer_embed = if direct { None } else { pointer.embed }; + Ok(Self { + abi, + embeds: [pointer_embed, embed], + result, + }) + } +} + +impl<'a> Import<'a> { + fn decode_with( + decoder: &mut Decoder<'a>, + input_types: &[InputType<'a>], + output_types: &[OutputType<'a>], + catch: Option<&ImportCatch<'a>>, + ) -> Result { + let module = decoder.string()?; + let name = decoder.string()?; + let flags = decoder.flags("import", IMPORT_FLAGS)?; + + let input_count = decoder.count("import input")?; + let mut inputs = Vec::with_capacity(input_count); + let mut conversion_embeds = Vec::new(); + for _ in 0..input_count { + let name = decoder.string()?; + let index = decoder.u32()?; + let ty = value::table_entry(decoder, input_types, index, "import input type")?; + conversion_embeds.extend(ty.embed); + inputs.push(ImportInput::from_type(name, ty)); + } + + let output = if flags & IMPORT_HAS_OUTPUT != 0 { + let index = decoder.u32()?; + let ty = value::table_entry(decoder, output_types, index, "import output type")?; + conversion_embeds.extend(ty.embeds.iter().flatten().copied()); + let error = if ty.result { + match catch { + Some(ImportCatch::JavaScript(_)) => ImportErrorMode::CatchInJavaScript, + Some(ImportCatch::Wasm(_)) => ImportErrorMode::CatchInWasm, + None => return decoder.invalid("Result import has no catch metadata"), + } + } else { + ImportErrorMode::Infallible + }; + Some(ImportOutput { + abi: ty.abi.clone(), + error, + }) + } else { + None + }; + + let binding = if flags & IMPORT_HAS_BINDING != 0 { + let mut binding = ImportBinding::decode(decoder)?; + conversion_embeds.append(&mut binding.embeds); + binding.embeds = conversion_embeds; + Some(binding) + } else { + decoder.ensure( + conversion_embeds.is_empty(), + "an import without a JavaScript binding requires an embed", + )?; + None + }; + let suspending = flags & IMPORT_SUSPENDING != 0; + decoder.ensure( + output + .as_ref() + .is_none_or(|output| output.error != ImportErrorMode::CatchInJavaScript) + || binding.is_some(), + "a JavaScript-catching Result import has no binding", + )?; + decoder.ensure( + !suspending || binding.is_some(), + "suspending import has no JavaScript binding", + )?; + decoder.ensure( + !suspending + || output + .as_ref() + .is_none_or(|output| output.error != ImportErrorMode::CatchInJavaScript), + "suspending Result imports require exception handling", + )?; + + Ok(Self { + module, + name, + inputs, + output, + binding, + suspending, + }) + } +} + +impl<'a> ImportInput<'a> { + fn from_type(name: &'a str, ty: &InputType<'a>) -> Self { + Self { + name, + slots: ty.slots.clone(), + js_conversion: ty.js_conversion, + } + } +} + +impl<'a> Decode<'a> for ImportBinding<'a> { + fn decode(decoder: &mut Decoder<'a>) -> Result { + let direct_expression = decoder.optional_string()?; + let call_expression = decoder.string()?; + let embed_count = decoder.count("import embed")?; + let mut embeds = Vec::with_capacity(embed_count); + for _ in 0..embed_count { + embeds.push(Embed { + module: decoder.string()?, + name: decoder.string()?, + }); + } + Ok(Self { + direct_expression, + call_expression, + embeds, + }) + } +} diff --git a/host/wire/src/decode/mod.rs b/host/wire/src/decode/mod.rs new file mode 100644 index 00000000..11bb1b81 --- /dev/null +++ b/host/wire/src/decode/mod.rs @@ -0,0 +1,345 @@ +//! Allocation-backed decoding of wire records. + +mod export; +mod import; +mod value; + +use core::{fmt, str}; + +use crate::{KIND_EXPORT, KIND_IMPORT, MAGIC, PointerWidth, VERSION, model::Record}; + +/// A type that can be decoded from a wire record. +pub(crate) trait Decode<'de>: Sized { + fn decode(decoder: &mut Decoder<'de>) -> Result; +} + +/// Decodes one complete wire record. +pub fn decode(bytes: &[u8]) -> Result, Error> { + let mut decoder = Decoder::new(bytes); + let record = Record::decode(&mut decoder)?; + decoder.finish()?; + Ok(record) +} + +impl<'de> Decode<'de> for Record<'de> { + fn decode(decoder: &mut Decoder<'de>) -> Result { + let magic_offset = decoder.position(); + if decoder.bytes(MAGIC.len())? != MAGIC { + return Err(Error::new(magic_offset, ErrorKind::InvalidMagic)); + } + let version_offset = decoder.position(); + let version = decoder.u16()?; + if version != VERSION { + return Err(Error::new( + version_offset, + ErrorKind::UnsupportedVersion(version), + )); + } + let width_offset = decoder.position(); + let pointer_width = match decoder.u8()? { + 4 => PointerWidth::Wasm32, + 8 => PointerWidth::Wasm64, + width => { + return Err(Error::new( + width_offset, + ErrorKind::InvalidPointerWidth(width), + )); + } + }; + let kind_offset = decoder.position(); + match decoder.u8()? { + KIND_IMPORT => import::decode(decoder, pointer_width).map(Self::Imports), + KIND_EXPORT => export::decode(decoder, pointer_width).map(Self::Exports), + kind => Err(Error::new(kind_offset, ErrorKind::UnknownRecordKind(kind))), + } + } +} + +/// A cursor over one borrowed wire record. +pub(crate) struct Decoder<'de> { + bytes: &'de [u8], + position: usize, +} + +impl<'de> Decoder<'de> { + #[must_use] + pub(crate) const fn new(bytes: &'de [u8]) -> Self { + Self { bytes, position: 0 } + } + + #[must_use] + pub(crate) const fn position(&self) -> usize { + self.position + } + + pub(crate) fn u8(&mut self) -> Result { + Ok(self.bytes(1)?[0]) + } + + pub(crate) fn boolean(&mut self, error_context: &'static str) -> Result { + let offset = self.position; + match self.u8()? { + 0 => Ok(false), + 1 => Ok(true), + value => Err(Error::new( + offset, + ErrorKind::InvalidBoolean { + error_context, + value, + }, + )), + } + } + + fn u16(&mut self) -> Result { + let bytes = self.bytes(2)?; + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + + pub(crate) fn u32(&mut self) -> Result { + let bytes = self.bytes(4)?; + Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } + + pub(crate) fn u64(&mut self) -> Result { + let bytes = self.bytes(8)?; + Ok(u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ])) + } + + pub(crate) fn optional_u64( + &mut self, + error_context: &'static str, + ) -> Result, Error> { + if self.boolean(error_context)? { + self.u64().map(Some) + } else { + Ok(None) + } + } + + pub(crate) fn count(&mut self, error_context: &'static str) -> Result { + let offset = self.position; + let count = self.u32()? as usize; + if count > self.remaining() { + return Err(Error::new( + offset, + ErrorKind::CountExceedsRecord { + error_context, + count, + }, + )); + } + Ok(count) + } + + pub(crate) fn optional_string(&mut self) -> Result, Error> { + let offset = self.position; + let length = self.u32()?; + if length == u32::MAX { + return Ok(None); + } + let bytes = self.bytes(length as usize)?; + str::from_utf8(bytes) + .map(Some) + .map_err(|_| Error::new(offset, ErrorKind::InvalidUtf8)) + } + + pub(crate) fn string(&mut self) -> Result<&'de str, Error> { + let offset = self.position; + self.optional_string()? + .ok_or_else(|| Error::new(offset, ErrorKind::MissingString)) + } + + fn bytes(&mut self, length: usize) -> Result<&'de [u8], Error> { + let offset = self.position; + let Some(end) = offset.checked_add(length) else { + return Err(Error::new( + offset, + ErrorKind::UnexpectedEnd { needed: length }, + )); + }; + let Some(bytes) = self.bytes.get(offset..end) else { + return Err(Error::new( + offset, + ErrorKind::UnexpectedEnd { needed: length }, + )); + }; + self.position = end; + Ok(bytes) + } + + fn finish(self) -> Result<(), Error> { + let remaining = self.remaining(); + if remaining == 0 { + Ok(()) + } else { + Err(Error::new( + self.position, + ErrorKind::TrailingBytes(remaining), + )) + } + } + + const fn remaining(&self) -> usize { + self.bytes.len() - self.position + } + + pub(crate) fn invalid(&self, message: &'static str) -> Result { + Err(Error::new(self.position, ErrorKind::InvalidValue(message))) + } + + pub(crate) fn ensure(&self, condition: bool, message: &'static str) -> Result<(), Error> { + if condition { + Ok(()) + } else { + self.invalid(message) + } + } + + pub(crate) fn flags(&mut self, error_context: &'static str, allowed: u8) -> Result { + let offset = self.position; + let flags = self.u8()?; + let unknown = flags & !allowed; + if unknown == 0 { + Ok(flags) + } else { + Err(Error::new( + offset, + ErrorKind::UnknownFlags { + error_context, + unknown, + }, + )) + } + } + + pub(crate) fn tag(&mut self, error_context: &'static str, maximum: u8) -> Result { + let offset = self.position; + let tag = self.u8()?; + if tag <= maximum { + Ok(tag) + } else { + Err(Error::new( + offset, + ErrorKind::UnknownTag { error_context, tag }, + )) + } + } +} + +/// A structural or semantic wire decoding failure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Error { + offset: usize, + kind: ErrorKind, +} + +impl Error { + const fn new(offset: usize, kind: ErrorKind) -> Self { + Self { offset, kind } + } + + #[must_use] + pub const fn offset(&self) -> usize { + self.offset + } + + #[must_use] + pub const fn kind(&self) -> &ErrorKind { + &self.kind + } +} + +/// The precise category of a wire decoding failure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ErrorKind { + UnexpectedEnd { + needed: usize, + }, + InvalidMagic, + UnsupportedVersion(u16), + InvalidPointerWidth(u8), + UnknownRecordKind(u8), + InvalidBoolean { + error_context: &'static str, + value: u8, + }, + InvalidUtf8, + MissingString, + TrailingBytes(usize), + CountExceedsRecord { + error_context: &'static str, + count: usize, + }, + UnknownFlags { + error_context: &'static str, + unknown: u8, + }, + UnknownTag { + error_context: &'static str, + tag: u8, + }, + IndexOutOfBounds { + table: &'static str, + index: u32, + len: usize, + }, + InvalidValue(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "wire error at byte {}: ", self.offset)?; + match self.kind { + ErrorKind::UnexpectedEnd { needed } => { + write!(formatter, "record ends before the next {needed} bytes") + } + ErrorKind::InvalidMagic => formatter.write_str("invalid magic"), + ErrorKind::UnsupportedVersion(version) => { + write!(formatter, "unsupported version {version}") + } + ErrorKind::InvalidPointerWidth(width) => { + write!(formatter, "invalid pointer width {width}") + } + ErrorKind::UnknownRecordKind(kind) => write!(formatter, "unknown record kind {kind}"), + ErrorKind::InvalidBoolean { + error_context, + value, + } => { + write!(formatter, "invalid {error_context} boolean {value}") + } + ErrorKind::InvalidUtf8 => formatter.write_str("string is not UTF-8"), + ErrorKind::MissingString => formatter.write_str("required string is absent"), + ErrorKind::TrailingBytes(bytes) => write!(formatter, "{bytes} trailing bytes"), + ErrorKind::CountExceedsRecord { + error_context, + count, + } => { + write!( + formatter, + "{error_context} count {count} exceeds the record" + ) + } + ErrorKind::UnknownFlags { + error_context, + unknown, + } => { + write!(formatter, "unknown {error_context} flags {unknown:#x}") + } + ErrorKind::UnknownTag { error_context, tag } => { + write!(formatter, "unknown {error_context} tag {tag}") + } + ErrorKind::IndexOutOfBounds { table, index, len } => { + write!( + formatter, + "{table} index {index} is out of bounds for length {len}" + ) + } + ErrorKind::InvalidValue(message) => formatter.write_str(message), + } + } +} + +impl core::error::Error for Error {} diff --git a/host/wire/src/decode/value.rs b/host/wire/src/decode/value.rs new file mode 100644 index 00000000..4d2aea1b --- /dev/null +++ b/host/wire/src/decode/value.rs @@ -0,0 +1,305 @@ +use alloc::{rc::Rc, vec::Vec}; + +use crate::{ + Error, ErrorKind, SLOT_COUNT, WAT_IMPORT_FUNCTION, WAT_IMPORT_TABLE, WAT_IMPORT_TAG, + abi::{RefType, WatIndexType, WatType}, + model::{Embed, Slot, WatConversion, WatImport, WatImportKind, WatLocal}, +}; + +use super::{Decode, Decoder}; + +pub(super) type WireSlots<'a> = [Option>; SLOT_COUNT]; + +pub(super) struct FromJsConversion<'a> { + pub embed: Option>, + pub prepare: Option<&'a str>, + pub templates: [Option<&'a str>; SLOT_COUNT], +} + +#[derive(Clone, Copy)] +pub(super) enum Sret<'a> { + Slots(&'a str), + Value(&'a str), +} + +impl<'de> Decode<'de> for Slot<'de> { + fn decode(decoder: &mut Decoder<'de>) -> Result { + let abi = decode_wat_type(decoder)?; + let wat = if decoder.boolean("slot WAT presence")? { + Some(WatConversion { + imports: decode_imports(decoder)?, + locals: decode_locals(decoder)?, + instruction: decoder.string()?, + boundary: decode_wat_type(decoder)?, + }) + } else { + None + }; + Ok(Self { abi, wat }) + } +} + +impl<'de> Decode<'de> for WatImport<'de> { + fn decode(decoder: &mut Decoder<'de>) -> Result { + let tag = decoder.tag("WAT import", WAT_IMPORT_TAG)?; + let module = required_nonempty(decoder, "WAT import has an empty module")?; + let name = required_nonempty(decoder, "WAT import has an empty name")?; + let identifier = required_nonempty(decoder, "WAT import has an empty identifier")?; + let symbol_name = decoder.optional_string()?; + decoder.ensure( + symbol_name.is_none_or(|symbol_name| !symbol_name.is_empty()), + "WAT import has an empty symbol name", + )?; + let kind = match tag { + WAT_IMPORT_FUNCTION => WatImportKind::Function { + parameters: decode_types(decoder, "WAT function parameter")?, + results: decode_types(decoder, "WAT function result")?, + }, + WAT_IMPORT_TABLE => { + let index_type = decode_index_type(decoder)?; + let minimum = decoder.u64()?; + let maximum = decoder.optional_u64("WAT table maximum presence")?; + decoder.ensure( + maximum.is_none_or(|maximum| maximum >= minimum), + "WAT table import maximum is smaller than its minimum", + )?; + let element = decode_ref_type(decoder)?; + WatImportKind::Table { + index_type, + minimum, + maximum, + element, + } + } + WAT_IMPORT_TAG => WatImportKind::Tag { + parameters: decode_types(decoder, "WAT tag parameter")?, + }, + _ => unreachable!(), + }; + Ok(Self { + module, + name, + identifier, + symbol_name, + kind, + }) + } +} + +impl<'de> Decode<'de> for WatLocal<'de> { + fn decode(decoder: &mut Decoder<'de>) -> Result { + Ok(Self { + name: required_nonempty(decoder, "WAT local has an empty name")?, + ty: decode_wat_type(decoder)?, + }) + } +} + +pub(super) fn decode_imports<'de>( + decoder: &mut Decoder<'de>, +) -> Result]>, Error> { + let count = decoder.count("WAT import")?; + let mut imports = Vec::with_capacity(count); + for _ in 0..count { + imports.push(WatImport::decode(decoder)?); + } + Ok(imports.into()) +} + +pub(super) fn decode_locals<'de>(decoder: &mut Decoder<'de>) -> Result]>, Error> { + let count = decoder.count("WAT local")?; + let mut locals = Vec::with_capacity(count); + for _ in 0..count { + locals.push(WatLocal::decode(decoder)?); + } + Ok(locals.into()) +} + +fn decode_types( + decoder: &mut Decoder<'_>, + error_context: &'static str, +) -> Result, Error> { + let count = decoder.count(error_context)?; + let mut types = Vec::with_capacity(count); + for _ in 0..count { + types.push(decode_wat_type(decoder)?); + } + Ok(types) +} + +fn decode_wat_type(decoder: &mut Decoder<'_>) -> Result { + let tag = decoder.tag("WAT type", WatType::MAX_TAG)?; + let Some(ty) = WatType::from_tag(tag) else { + unreachable!(); + }; + Ok(ty) +} + +fn decode_index_type(decoder: &mut Decoder<'_>) -> Result { + let tag = decoder.tag("WAT table index type", WatIndexType::MAX_TAG)?; + let Some(ty) = WatIndexType::from_tag(tag) else { + unreachable!(); + }; + Ok(ty) +} + +fn decode_ref_type(decoder: &mut Decoder<'_>) -> Result { + let tag = decoder.tag("WAT reference type", RefType::MAX_TAG)?; + let Some(ty) = RefType::from_tag(tag) else { + unreachable!(); + }; + Ok(ty) +} + +fn required_nonempty<'de>( + decoder: &mut Decoder<'de>, + error_message: &'static str, +) -> Result<&'de str, Error> { + let value = decoder.string()?; + decoder.ensure(!value.is_empty(), error_message)?; + Ok(value) +} + +pub(super) fn optional_embed<'de>(decoder: &mut Decoder<'de>) -> Result>, Error> { + if decoder.boolean("embed presence")? { + Ok(Some(Embed { + module: decoder.string()?, + name: decoder.string()?, + })) + } else { + Ok(None) + } +} + +pub(super) fn optional_into_js_conversion<'de>( + decoder: &mut Decoder<'de>, +) -> Result>, &'de str)>, Error> { + if !decoder.boolean("JavaScript conversion presence")? { + return Ok(None); + } + Ok(Some((optional_embed(decoder)?, decoder.string()?))) +} + +pub(super) fn optional_from_js_conversion<'de>( + decoder: &mut Decoder<'de>, +) -> Result>, Error> { + if !decoder.boolean("JavaScript conversion presence")? { + return Ok(None); + } + let embed = optional_embed(decoder)?; + let prepare = decoder.optional_string()?; + let templates = templates(decoder)?; + Ok(Some(FromJsConversion { + embed, + prepare, + templates, + })) +} + +pub(super) fn sret<'de>(decoder: &mut Decoder<'de>) -> Result, Error> { + Ok(match decoder.tag("JavaScript return writer", 1)? { + 0 => Sret::Slots(decoder.string()?), + 1 => Sret::Value(decoder.string()?), + _ => unreachable!(), + }) +} + +pub(super) fn slots<'de>(decoder: &mut Decoder<'de>) -> Result, Error> { + let offset = decoder.position(); + let mask = decoder.u8()?; + let allowed = (1 << SLOT_COUNT) - 1; + if mask & !allowed != 0 { + return Err(Error::new( + offset, + ErrorKind::InvalidValue("invalid wire slot mask"), + )); + } + let mut slots = [const { None }; SLOT_COUNT]; + let mut index = 0; + while index < slots.len() { + if mask & (1 << index) != 0 { + slots[index] = Some(Slot::decode(decoder)?); + } + index += 1; + } + Ok(slots) +} + +pub(super) fn templates<'de>( + decoder: &mut Decoder<'de>, +) -> Result<[Option<&'de str>; SLOT_COUNT], Error> { + let offset = decoder.position(); + let mask = decoder.u8()?; + let allowed = (1 << SLOT_COUNT) - 1; + if mask & !allowed != 0 { + return Err(Error::new( + offset, + ErrorKind::InvalidValue("invalid wire template mask"), + )); + } + let mut templates = [None; SLOT_COUNT]; + let mut index = 0; + while index < templates.len() { + if mask & (1 << index) != 0 { + templates[index] = Some(decoder.string()?); + } + index += 1; + } + Ok(templates) +} + +pub(super) fn compact<'a>(slots: &WireSlots<'a>) -> Vec> { + slots.iter().flatten().cloned().collect() +} + +pub(super) fn compact_abi<'a>( + decoder: &Decoder<'_>, + slots: &WireSlots<'a>, +) -> Result>, Error> { + validate_abi(decoder, slots)?; + Ok(compact(slots)) +} + +pub(super) fn validate_abi(decoder: &Decoder<'_>, slots: &WireSlots<'_>) -> Result<(), Error> { + let mut empty = false; + for slot in slots { + if slot.is_some() { + decoder.ensure(!empty, "wire ABI has a populated slot after an empty slot")?; + } else { + empty = true; + } + } + Ok(()) +} + +pub(super) fn validate_templates( + decoder: &Decoder<'_>, + slots: &WireSlots<'_>, + templates: &[Option<&str>; SLOT_COUNT], +) -> Result<(), Error> { + for (slot, template) in slots.iter().zip(templates) { + decoder.ensure( + slot.is_some() == template.is_some(), + "JavaScript conversion does not match its populated slots", + )?; + } + Ok(()) +} + +pub(super) fn table_entry<'a, T>( + decoder: &Decoder<'_>, + table: &'a [T], + index: u32, + name: &'static str, +) -> Result<&'a T, Error> { + table.get(index as usize).ok_or_else(|| { + Error::new( + decoder.position(), + ErrorKind::IndexOutOfBounds { + table: name, + index, + len: table.len(), + }, + ) + }) +} diff --git a/host/wire/src/encode/export.rs b/host/wire/src/encode/export.rs new file mode 100644 index 00000000..636f1331 --- /dev/null +++ b/host/wire/src/encode/export.rs @@ -0,0 +1,162 @@ +use core::mem::size_of; + +use crate::{ + EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_RESULT, WireExport, WireExportCallee, WireExportInput, + WireExportOutput, WireExportOutputType, +}; + +use super::{Encoder, Sizer}; + +impl Encoder { + pub(super) const fn exports(&mut self, exports: &[WireExport]) { + self.count(exports.len()); + let mut index = 0; + while index < exports.len() { + exports[index].encode(self); + index += 1; + } + } +} + +impl Sizer { + pub(super) const fn exports(&mut self, exports: &[WireExport]) { + self.add(size_of::()); + let mut index = 0; + while index < exports.len() { + exports[index].size(self); + index += 1; + } + } +} + +impl WireExportCallee { + const fn encode(self, encoder: &mut Encoder) { + match self { + Self::Symbol(symbol) => { + encoder.u8(0); + encoder.string(symbol); + } + Self::Closure { call_shim_offset } => { + encoder.u8(1); + encoder.u64(call_shim_offset as u64); + } + } + } + + const fn size(self, sizer: &mut Sizer) { + sizer.add(1); + match self { + Self::Symbol(symbol) => sizer.string(symbol), + Self::Closure { .. } => sizer.add(size_of::()), + } + } +} + +impl WireExportInput { + const fn encode(self, encoder: &mut Encoder) { + encoder.string(self.name); + encoder.slots(&self.ty.slots); + match self.ty.conversion { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + } + + const fn size(self, sizer: &mut Sizer) { + sizer.string(self.name); + sizer.slots(&self.ty.slots); + sizer.add(1); + if let Some(conversion) = self.ty.conversion { + conversion.size(sizer); + } + } +} + +impl WireExportOutputType { + const fn encode(&self, encoder: &mut Encoder) { + encoder.u8(flag(self.mode.is_direct(), EXPORT_OUTPUT_DIRECT) + | flag(self.conversion.is_result(), EXPORT_OUTPUT_RESULT)); + encoder.slots(&self.slots); + match self.conversion.conversion() { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + encoder.u64(self.frame_size as u64); + let mut index = 0; + while index < self.slot_offsets.len() { + encoder.u64(self.slot_offsets[index] as u64); + index += 1; + } + if let Some(result) = self.result { + encoder.u8(result.discriminant); + encoder.u8(result.error); + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.add(1); + sizer.slots(&self.slots); + sizer.add(1); + if let Some(conversion) = self.conversion.conversion() { + conversion.size(sizer); + } + sizer.add(5 * size_of::()); + if self.result.is_some() { + sizer.add(2); + } + } +} + +const fn flag(enabled: bool, value: u8) -> u8 { + if enabled { value } else { 0 } +} + +impl WireExportOutput { + const fn encode(self, encoder: &mut Encoder) { + self.ty.encode(encoder); + } + + const fn size(self, sizer: &mut Sizer) { + self.ty.size(sizer); + } +} + +impl WireExport { + const fn encode(&self, encoder: &mut Encoder) { + encoder.string(self.module); + encoder.string(self.name); + encoder.u8(self.flags); + self.callee.encode(encoder); + encoder.count(self.inputs.len()); + let mut index = 0; + while index < self.inputs.len() { + self.inputs[index].encode(encoder); + index += 1; + } + if let Some(output) = self.output { + output.encode(encoder); + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.string(self.module); + sizer.string(self.name); + sizer.add(1); + self.callee.size(sizer); + sizer.add(size_of::()); + let mut index = 0; + while index < self.inputs.len() { + self.inputs[index].size(sizer); + index += 1; + } + if let Some(output) = self.output { + output.size(sizer); + } + } +} diff --git a/host/wire/src/encode/import.rs b/host/wire/src/encode/import.rs new file mode 100644 index 00000000..9984cb9c --- /dev/null +++ b/host/wire/src/encode/import.rs @@ -0,0 +1,315 @@ +use core::mem::size_of; + +use crate::{ + IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_HAS_BINDING, IMPORT_HAS_OUTPUT, + IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, WireImport, WireImportBinding, + WireImportCatch, WireImportInput, WireImportInputType, WireImportOutput, WireImportOutputType, + WireImportTypeTable, + abi::{JsCatch, WatCatch}, +}; + +use super::{Encoder, Sizer}; + +impl Encoder { + pub(super) const fn imports(&mut self, table: &WireImportTypeTable, imports: &[WireImport]) { + table.encode(self); + self.count(imports.len()); + let mut index = 0; + while index < imports.len() { + imports[index].encode(self); + index += 1; + } + } +} + +impl Sizer { + pub(super) const fn imports(&mut self, table: &WireImportTypeTable, imports: &[WireImport]) { + table.size(self); + self.add(size_of::()); + let mut index = 0; + while index < imports.len() { + imports[index].size(self); + index += 1; + } + } +} + +impl WireImportTypeTable { + const fn encode(&self, encoder: &mut Encoder) { + encoder.count(self.input_types.len()); + let mut index = 0; + while index < self.input_types.len() { + self.input_types[index].encode(encoder); + index += 1; + } + + encoder.count(self.output_types.len()); + if !self.output_types.is_empty() { + self.retptr_type.encode(encoder); + } + index = 0; + while index < self.output_types.len() { + self.output_types[index].encode(encoder); + index += 1; + } + if self.has_result() { + self.catch.encode(encoder); + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.add(size_of::()); + let mut index = 0; + while index < self.input_types.len() { + self.input_types[index].size(sizer); + index += 1; + } + + sizer.add(size_of::()); + if !self.output_types.is_empty() { + self.retptr_type.size(sizer); + } + index = 0; + while index < self.output_types.len() { + self.output_types[index].size(sizer); + index += 1; + } + if self.has_result() { + self.catch.size(sizer); + } + } +} + +impl WireImportCatch { + const fn encode(self, encoder: &mut Encoder) { + match self { + Self::JavaScript(catch) => { + encoder.u8(IMPORT_CATCH_JAVASCRIPT); + catch.encode(encoder); + } + Self::Wasm(catch) => { + encoder.u8(IMPORT_CATCH_WASM); + catch.encode(encoder); + } + } + } + + const fn size(self, sizer: &mut Sizer) { + sizer.add(1); + match self { + Self::JavaScript(catch) => catch.size(sizer), + Self::Wasm(catch) => catch.size(sizer), + } + } +} + +impl JsCatch { + const fn encode(self, encoder: &mut Encoder) { + encoder.count(self.embeds.len()); + let mut index = 0; + while index < self.embeds.len() { + encoder.string(self.embeds[index].module); + encoder.string(self.embeds[index].name); + index += 1; + } + encoder.string(self.direct); + encoder.string(self.indirect); + } + + const fn size(self, sizer: &mut Sizer) { + sizer.add(size_of::()); + let mut index = 0; + while index < self.embeds.len() { + sizer.string(self.embeds[index].module); + sizer.string(self.embeds[index].name); + index += 1; + } + sizer.string(self.direct); + sizer.string(self.indirect); + } +} + +impl WatCatch { + const fn encode(self, encoder: &mut Encoder) { + encoder.count(self.imports.len()); + let mut index = 0; + while index < self.imports.len() { + self.imports[index].encode(encoder); + index += 1; + } + encoder.count(self.locals.len()); + index = 0; + while index < self.locals.len() { + self.locals[index].encode(encoder); + index += 1; + } + encoder.string(self.try_); + encoder.string(self.catch); + } + + const fn size(self, sizer: &mut Sizer) { + sizer.add(size_of::()); + let mut index = 0; + while index < self.imports.len() { + self.imports[index].size(sizer); + index += 1; + } + sizer.add(size_of::()); + index = 0; + while index < self.locals.len() { + self.locals[index].size(sizer); + index += 1; + } + sizer.string(self.try_); + sizer.string(self.catch); + } +} + +impl WireImportInputType { + const fn encode(&self, encoder: &mut Encoder) { + encoder.slots(&self.slots); + match self.conversion { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.slots(&self.slots); + sizer.add(1); + if let Some(conversion) = self.conversion { + conversion.size(sizer); + } + } +} + +impl WireImportOutputType { + const fn encode(&self, encoder: &mut Encoder) { + encoder.u8(flag(self.mode.is_direct(), IMPORT_OUTPUT_DIRECT) + | flag(self.conversion.is_result(), IMPORT_OUTPUT_RESULT)); + encoder.slots(&self.slots); + match self.conversion.conversion() { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + if let Some(sret) = self.sret { + encoder.sret(sret); + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.add(1); + sizer.slots(&self.slots); + sizer.add(1); + if let Some(conversion) = self.conversion.conversion() { + conversion.size(sizer); + } + if let Some(sret) = self.sret { + sizer.sret(sret); + } + } +} + +impl WireImportInput { + const fn encode(&self, encoder: &mut Encoder) { + encoder.string(self.name); + encoder.u32(wire_u32(self.type_index)); + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.string(self.name); + sizer.add(size_of::()); + } +} + +impl WireImportOutput { + const fn encode(self, encoder: &mut Encoder) { + encoder.u32(wire_u32(self.type_index)); + } +} + +impl WireImportBinding { + const fn encode(self, encoder: &mut Encoder) { + encoder.optional_string(self.direct); + encoder.string(self.call); + encoder.count(self.required_embeds.len()); + let mut index = 0; + while index < self.required_embeds.len() { + let embed = self.required_embeds[index]; + encoder.string(embed.module); + encoder.string(embed.name); + index += 1; + } + } + + const fn size(self, sizer: &mut Sizer) { + sizer.optional_string(self.direct); + sizer.string(self.call); + sizer.add(size_of::()); + let mut index = 0; + while index < self.required_embeds.len() { + let embed = self.required_embeds[index]; + sizer.string(embed.module); + sizer.string(embed.name); + index += 1; + } + } +} + +impl WireImport { + const fn encode(&self, encoder: &mut Encoder) { + encoder.string(self.module); + encoder.string(self.name); + encoder.u8(flag(self.suspending, IMPORT_SUSPENDING) + | flag(self.output.is_some(), IMPORT_HAS_OUTPUT) + | flag(self.binding.is_some(), IMPORT_HAS_BINDING)); + encoder.count(self.inputs.len()); + let mut index = 0; + while index < self.inputs.len() { + self.inputs[index].encode(encoder); + index += 1; + } + if let Some(output) = self.output { + output.encode(encoder); + } + if let Some(binding) = self.binding { + binding.encode(encoder); + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.string(self.module); + sizer.string(self.name); + sizer.add(1 + size_of::()); + let mut index = 0; + while index < self.inputs.len() { + self.inputs[index].size(sizer); + index += 1; + } + if self.output.is_some() { + sizer.add(size_of::()); + } + if let Some(binding) = self.binding { + binding.size(sizer); + } + } +} + +const fn flag(enabled: bool, value: u8) -> u8 { + if enabled { value } else { 0 } +} + +#[expect( + clippy::cast_possible_truncation, + reason = "the function asserts that the value fits in u32" +)] +const fn wire_u32(value: usize) -> u32 { + assert!(value <= u32::MAX as usize); + value as u32 +} diff --git a/host/wire/src/encode/mod.rs b/host/wire/src/encode/mod.rs new file mode 100644 index 00000000..fe28e8bb --- /dev/null +++ b/host/wire/src/encode/mod.rs @@ -0,0 +1,541 @@ +//! Constant serialization of static wire descriptions. + +mod export; +mod import; + +use core::mem::size_of; + +use crate::{ + MAGIC, VERSION, WAT_IMPORT_FUNCTION, WAT_IMPORT_TABLE, WAT_IMPORT_TAG, Wire, + abi::{ + FromJsConv, IntoJsConv, JsEmbed, Sret, WatImport, WatImportKind, WatLocal, WatSlot, WatType, + }, + schema::WireKind, +}; + +/// A raw, self-contained wire record without custom-section framing. +#[derive(Clone, Copy)] +pub struct WireRecord { + bytes: [u8; N], +} + +impl WireRecord { + #[must_use] + pub const fn new(wire: &Wire) -> Self { + Self { + bytes: encode::(wire), + } + } + + #[must_use] + pub const fn as_bytes(&self) -> &[u8; N] { + &self.bytes + } + + #[must_use] + pub const fn into_bytes(self) -> [u8; N] { + self.bytes + } +} + +/// A length-prefixed wire record stored in the `js_bindgen.wire` custom section. +#[repr(C)] +pub struct WireBlob { + record_len: [u8; 4], + bytes: [u8; N], +} + +impl WireBlob { + #[must_use] + pub const fn new(wire: &Wire) -> Self { + let record_len = wire_u32(N); + + Self { + record_len: record_len.to_le_bytes(), + bytes: encode::(wire), + } + } +} + +/// Computes the exact encoded size of a wire record. +#[must_use] +pub const fn wire_blob_len(wire: &Wire) -> usize { + let mut sizer = Sizer::new(); + sizer.header(); + match &wire.kind { + WireKind::Imports { table, imports } => sizer.imports(table, imports), + WireKind::Exports(exports) => sizer.exports(exports), + } + sizer.position +} + +const fn encode(wire: &Wire) -> [u8; N] { + let mut encoder = Encoder::::new(); + encoder.header(wire.pointer_width.bytes(), wire.kind.tag()); + match &wire.kind { + WireKind::Imports { table, imports } => encoder.imports(table, imports), + WireKind::Exports(exports) => encoder.exports(exports), + } + assert!(encoder.position == N); + encoder.bytes +} + +impl WireKind { + const fn tag(&self) -> u8 { + match self { + Self::Imports { .. } => crate::KIND_IMPORT, + Self::Exports(_) => crate::KIND_EXPORT, + } + } +} + +pub(crate) struct Encoder { + bytes: [u8; N], + position: usize, +} + +impl Encoder { + const fn new() -> Self { + Self { + bytes: [0; N], + position: 0, + } + } + + const fn header(&mut self, pointer_width: u8, kind: u8) { + self.bytes(&MAGIC); + self.u16(VERSION); + self.u8(pointer_width); + self.u8(kind); + } + + pub(crate) const fn string(&mut self, value: &'static str) { + self.optional_string(Some(value)); + } + + pub(crate) const fn optional_string(&mut self, value: Option<&'static str>) { + let Some(value) = value else { + self.u32(u32::MAX); + return; + }; + let len = wire_string_len(value.len()); + let position = self.position; + let value_position = position + size_of::(); + let end = value_position + len as usize; + assert!(end <= N); + let length = len.to_le_bytes(); + self.bytes[position] = length[0]; + self.bytes[position + 1] = length[1]; + self.bytes[position + 2] = length[2]; + self.bytes[position + 3] = length[3]; + if len != 0 { + // SAFETY: The bounds check covers the destination, and the source + // is a distinct immutable string. + unsafe { + core::ptr::copy_nonoverlapping( + value.as_ptr(), + self.bytes.as_mut_ptr().add(value_position), + len as usize, + ); + } + } + self.position = end; + } + + pub(crate) const fn count(&mut self, value: usize) { + self.u32(wire_u32(value)); + } + + pub(crate) const fn wat_types(&mut self, values: &[WatType]) { + self.count(values.len()); + let mut index = 0; + while index < values.len() { + self.u8(values[index].tag()); + index += 1; + } + } + + pub(crate) const fn slots(&mut self, slots: &[Option; 4]) { + let mask = slot_mask(slots); + self.u8(mask); + let mut index = 0; + while index < slots.len() { + if let Some(slot) = slots[index] { + slot.encode(self); + } + index += 1; + } + } + + pub(crate) const fn templates(&mut self, templates: &[Option<&'static str>; 4]) { + let mask = template_mask(templates); + self.u8(mask); + let mut index = 0; + while index < templates.len() { + if mask & (1 << index) != 0 { + self.string(match templates[index] { + Some(template) => template, + None => unreachable!(), + }); + } + index += 1; + } + } + + pub(crate) const fn embed(&mut self, embed: Option) { + match embed { + Some(embed) => { + self.u8(1); + self.string(embed.module); + self.string(embed.name); + } + None => self.u8(0), + } + } + + pub(crate) const fn u64(&mut self, value: u64) { + self.bytes(&value.to_le_bytes()); + } + + pub(crate) const fn optional_u64(&mut self, value: Option) { + match value { + Some(value) => { + self.u8(1); + self.u64(value); + } + None => self.u8(0), + } + } + + pub(crate) const fn u32(&mut self, value: u32) { + self.bytes(&value.to_le_bytes()); + } + + const fn u16(&mut self, value: u16) { + self.bytes(&value.to_le_bytes()); + } + + pub(crate) const fn u8(&mut self, value: u8) { + assert!(self.position < N); + self.bytes[self.position] = value; + self.position += 1; + } + + const fn bytes(&mut self, value: &[u8]) { + let Some(end) = self.position.checked_add(value.len()) else { + panic!("wire record write overflow"); + }; + assert!(end <= N); + if !value.is_empty() { + // SAFETY: The bounds check covers the destination, and the private + // output buffer cannot overlap `value`. + unsafe { + core::ptr::copy_nonoverlapping( + value.as_ptr(), + self.bytes.as_mut_ptr().add(self.position), + value.len(), + ); + } + } + self.position = end; + } +} + +// Sizing remains separate from `Encoder`: using `Encoder<0>` adds a branch +// to every write during constant evaluation and slows large import groups. +pub(crate) struct Sizer { + position: usize, +} + +impl Sizer { + const fn new() -> Self { + Self { position: 0 } + } + + const fn header(&mut self) { + self.position += MAGIC.len() + size_of::() + 2 * size_of::(); + } + + pub(crate) const fn string(&mut self, value: &'static str) { + self.optional_string(Some(value)); + } + + pub(crate) const fn optional_string(&mut self, value: Option<&'static str>) { + self.position += size_of::(); + if let Some(value) = value { + assert!(value.len() < u32::MAX as usize); + self.position += value.len(); + } + } + + pub(crate) const fn wat_types(&mut self, values: &[WatType]) { + self.add(size_of::() + values.len()); + } + + pub(crate) const fn add(&mut self, bytes: usize) { + let Some(position) = self.position.checked_add(bytes) else { + panic!("wire record size overflow"); + }; + self.position = position; + } + + pub(crate) const fn slots(&mut self, slots: &[Option; 4]) { + self.add(1); + let mut index = 0; + while index < slots.len() { + if let Some(slot) = slots[index] { + slot.size(self); + } + index += 1; + } + } + + pub(crate) const fn templates(&mut self, templates: &[Option<&'static str>; 4]) { + self.add(1); + let mask = template_mask(templates); + let mut index = 0; + while index < templates.len() { + if mask & (1 << index) != 0 { + self.optional_string(templates[index]); + } + index += 1; + } + } + + pub(crate) const fn embed(&mut self, embed: Option) { + self.add(1); + if let Some(embed) = embed { + self.string(embed.module); + self.string(embed.name); + } + } + + pub(crate) const fn optional_u64(&mut self, value: Option) { + self.add(size_of::()); + if value.is_some() { + self.add(size_of::()); + } + } +} + +impl WatSlot { + const fn encode(&self, encoder: &mut Encoder) { + encoder.u8(self.abi.tag()); + match self.wat { + Some(wat) => { + encoder.u8(1); + encoder.count(wat.imports.len()); + let mut index = 0; + while index < wat.imports.len() { + wat.imports[index].encode(encoder); + index += 1; + } + encoder.count(wat.locals.len()); + index = 0; + while index < wat.locals.len() { + wat.locals[index].encode(encoder); + index += 1; + } + encoder.string(wat.instruction); + encoder.u8(wat.boundary.tag()); + } + None => encoder.u8(0), + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.add(1); + sizer.add(1); + if let Some(wat) = self.wat { + sizer.add(size_of::()); + let mut index = 0; + while index < wat.imports.len() { + wat.imports[index].size(sizer); + index += 1; + } + sizer.add(size_of::()); + index = 0; + while index < wat.locals.len() { + wat.locals[index].size(sizer); + index += 1; + } + sizer.string(wat.instruction); + sizer.add(1); + } + } +} + +impl WatImport { + const fn encode(&self, encoder: &mut Encoder) { + encoder.u8(self.kind.tag()); + encoder.string(self.module); + encoder.string(self.name); + encoder.string(self.identifier); + encoder.optional_string(self.symbol_name); + match self.kind { + WatImportKind::Function { + parameters, + results, + } => { + encoder.wat_types(parameters); + encoder.wat_types(results); + } + WatImportKind::Table { + index_type, + minimum, + maximum, + element, + } => { + encoder.u8(index_type.tag()); + encoder.u64(minimum); + encoder.optional_u64(maximum); + encoder.u8(element.tag()); + } + WatImportKind::Tag { parameters } => encoder.wat_types(parameters), + } + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.add(size_of::()); + sizer.string(self.module); + sizer.string(self.name); + sizer.string(self.identifier); + sizer.optional_string(self.symbol_name); + match self.kind { + WatImportKind::Function { + parameters, + results, + } => { + sizer.wat_types(parameters); + sizer.wat_types(results); + } + WatImportKind::Table { + index_type: _, + minimum: _, + maximum, + element: _, + } => { + sizer.add(1); + sizer.add(size_of::()); + sizer.optional_u64(maximum); + sizer.add(1); + } + WatImportKind::Tag { parameters } => sizer.wat_types(parameters), + } + } +} + +impl WatImportKind { + const fn tag(self) -> u8 { + match self { + Self::Function { .. } => WAT_IMPORT_FUNCTION, + Self::Table { .. } => WAT_IMPORT_TABLE, + Self::Tag { .. } => WAT_IMPORT_TAG, + } + } +} + +impl WatLocal { + const fn encode(&self, encoder: &mut Encoder) { + encoder.string(self.name); + encoder.u8(self.ty.tag()); + } + + const fn size(&self, sizer: &mut Sizer) { + sizer.string(self.name); + sizer.add(1); + } +} + +impl IntoJsConv { + pub(crate) const fn encode(&self, encoder: &mut Encoder) { + encoder.embed(self.embed); + encoder.string(self.template); + } + + pub(crate) const fn size(&self, sizer: &mut Sizer) { + sizer.embed(self.embed); + sizer.string(self.template); + } +} + +impl FromJsConv { + pub(crate) const fn encode(&self, encoder: &mut Encoder) { + encoder.embed(self.embed); + encoder.optional_string(self.prepare); + encoder.templates(&self.templates); + } + + pub(crate) const fn size(&self, sizer: &mut Sizer) { + sizer.embed(self.embed); + sizer.optional_string(self.prepare); + sizer.templates(&self.templates); + } +} + +impl Encoder { + pub(crate) const fn sret(&mut self, sret: Sret) { + match sret { + Sret::Slots(function) => { + self.u8(0); + self.string(function); + } + Sret::Value(function) => { + self.u8(1); + self.string(function); + } + } + } +} + +impl Sizer { + pub(crate) const fn sret(&mut self, sret: Sret) { + self.add(1); + let function = match sret { + Sret::Slots(function) | Sret::Value(function) => function, + }; + self.string(function); + } +} + +const fn slot_mask(slots: &[Option; 4]) -> u8 { + let mut mask = 0; + let mut index = 0; + while index < slots.len() { + if slots[index].is_some() { + mask |= 1 << index; + } + index += 1; + } + mask +} + +const fn template_mask(templates: &[Option<&str>; 4]) -> u8 { + let mut mask = 0; + let mut index = 0; + while index < templates.len() { + if templates[index].is_some() { + mask |= 1 << index; + } + index += 1; + } + mask +} + +#[expect( + clippy::cast_possible_truncation, + reason = "the function asserts that the value fits in u32" +)] +const fn wire_u32(value: usize) -> u32 { + assert!(value <= u32::MAX as usize); + value as u32 +} + +#[expect( + clippy::cast_possible_truncation, + reason = "the function asserts that the value fits in u32" +)] +const fn wire_string_len(value: usize) -> u32 { + assert!(value < u32::MAX as usize); + value as u32 +} diff --git a/host/wire/src/lib.rs b/host/wire/src/lib.rs new file mode 100644 index 00000000..31138ee4 --- /dev/null +++ b/host/wire/src/lib.rs @@ -0,0 +1,65 @@ +//! The wire protocol shared by `js-sys` and `js-bindgen-ld`. + +#![no_std] + +#[cfg(feature = "alloc")] +extern crate alloc; + +mod encode; +mod schema; + +#[doc(hidden)] +pub mod abi; + +#[cfg(feature = "alloc")] +mod decode; +#[cfg(feature = "alloc")] +pub mod model; + +pub use encode::{WireBlob, WireRecord, wire_blob_len}; +pub use schema::*; + +#[cfg(feature = "alloc")] +pub use decode::{Error, ErrorKind, decode}; + +/// Identifies a wire record independently of its payload kind. +pub const MAGIC: [u8; 8] = *b"JBGWIRE\0"; + +/// The single protocol version used by imports and exports. +pub const VERSION: u16 = 1; + +#[cfg(feature = "alloc")] +pub(crate) const SLOT_COUNT: usize = 4; +pub(crate) const KIND_IMPORT: u8 = 0; +pub(crate) const KIND_EXPORT: u8 = 1; + +pub(crate) const WAT_IMPORT_FUNCTION: u8 = 0; +pub(crate) const WAT_IMPORT_TABLE: u8 = 1; +pub(crate) const WAT_IMPORT_TAG: u8 = 2; + +pub(crate) const IMPORT_SUSPENDING: u8 = 1 << 0; +pub(crate) const IMPORT_HAS_OUTPUT: u8 = 1 << 1; +pub(crate) const IMPORT_HAS_BINDING: u8 = 1 << 2; +#[cfg(feature = "alloc")] +pub(crate) const IMPORT_FLAGS: u8 = IMPORT_SUSPENDING | IMPORT_HAS_OUTPUT | IMPORT_HAS_BINDING; + +pub(crate) const IMPORT_OUTPUT_DIRECT: u8 = 1 << 0; +pub(crate) const IMPORT_OUTPUT_RESULT: u8 = 1 << 1; +#[cfg(feature = "alloc")] +pub(crate) const IMPORT_OUTPUT_FLAGS: u8 = IMPORT_OUTPUT_DIRECT | IMPORT_OUTPUT_RESULT; + +pub(crate) const IMPORT_CATCH_JAVASCRIPT: u8 = 0; +pub(crate) const IMPORT_CATCH_WASM: u8 = 1; + +pub(crate) const EXPORT_PROMISING: u8 = 1 << 0; +pub(crate) const EXPORT_HAS_OUTPUT: u8 = 1 << 1; +#[cfg(feature = "alloc")] +pub(crate) const EXPORT_FLAGS: u8 = EXPORT_PROMISING | EXPORT_HAS_OUTPUT; + +pub(crate) const EXPORT_OUTPUT_DIRECT: u8 = 1 << 0; +pub(crate) const EXPORT_OUTPUT_RESULT: u8 = 1 << 1; +#[cfg(feature = "alloc")] +pub(crate) const EXPORT_OUTPUT_FLAGS: u8 = EXPORT_OUTPUT_DIRECT | EXPORT_OUTPUT_RESULT; + +#[cfg(all(test, feature = "alloc"))] +mod tests; diff --git a/host/wire/src/model.rs b/host/wire/src/model.rs new file mode 100644 index 00000000..65b8f52f --- /dev/null +++ b/host/wire/src/model.rs @@ -0,0 +1,322 @@ +//! Canonical model consumed by JavaScript and `WAT` `renderers`. + +use alloc::{rc::Rc, vec::Vec}; + +pub use crate::PointerWidth; +pub use crate::abi::ResultLayout; +use crate::abi::{RefType, WatIndexType, WatType}; + +/// One primitive `Wasm` `ABI` slot. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Slot<'a> { + pub abi: WatType, + pub wat: Option>, +} + +impl<'a> Slot<'a> { + #[must_use] + pub fn boundary(&self) -> WatType { + self.wat + .as_ref() + .map_or(self.abi, |conversion| conversion.boundary) + } + + #[must_use] + pub fn imports(&self) -> &[WatImport<'a>] { + self.wat + .as_ref() + .map_or(&[], |conversion| conversion.imports.as_ref()) + } + + #[must_use] + pub fn locals(&self) -> &[WatLocal<'a>] { + self.wat + .as_ref() + .map_or(&[], |conversion| conversion.locals.as_ref()) + } + + #[must_use] + pub fn instruction(&self) -> Option<&'a str> { + self.wat.as_ref().map(|conversion| conversion.instruction) + } +} + +/// `WAT` required to translate one slot across the JavaScript boundary. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WatConversion<'a> { + pub boundary: WatType, + pub imports: Rc<[WatImport<'a>]>, + pub locals: Rc<[WatLocal<'a>]>, + pub instruction: &'a str, +} + +/// One decoded structured `WAT` import. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WatImport<'a> { + pub module: &'a str, + pub name: &'a str, + pub identifier: &'a str, + pub symbol_name: Option<&'a str>, + pub kind: WatImportKind, +} + +/// The kind and type of one decoded structured `WAT` import. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum WatImportKind { + Function { + parameters: Vec, + results: Vec, + }, + Table { + index_type: WatIndexType, + minimum: u64, + maximum: Option, + element: RefType, + }, + Tag { + parameters: Vec, + }, +} + +/// One decoded structured local required by a boundary conversion. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WatLocal<'a> { + pub name: &'a str, + pub ty: WatType, +} + +/// One JavaScript source fragment required by a generated binding. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Embed<'a> { + pub module: &'a str, + pub name: &'a str, +} + +/// JavaScript exception-catching support shared by an import group. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct JsCatch<'a> { + pub embeds: Rc<[Embed<'a>]>, + pub direct: &'a str, + pub indirect: &'a str, +} + +/// Wasm exception-catching support shared by an import group. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WatCatch<'a> { + pub imports: Rc<[WatImport<'a>]>, + pub locals: Rc<[WatLocal<'a>]>, + pub try_: &'a str, + pub catch: &'a str, +} + +/// Where and how imported JavaScript exceptions are lowered. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ImportCatch<'a> { + JavaScript(JsCatch<'a>), + Wasm(WatCatch<'a>), +} + +/// One named argument passed from Rust to an imported JavaScript function. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportInput<'a> { + pub name: &'a str, + pub slots: Vec>, + pub js_conversion: Option<&'a str>, +} + +/// Where an imported `Result` catches a JavaScript exception. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ImportErrorMode { + Infallible, + CatchInJavaScript, + CatchInWasm, +} + +/// JavaScript conversion for a direct import result. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectImportConversion<'a> { + pub prepare: Option<&'a str>, + pub expression: &'a str, +} + +/// The return pointer accepted by an indirect import. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportRetptr<'a> { + pub slot: Slot<'a>, + pub js_conversion: Option<&'a str>, +} + +/// How JavaScript writes an indirect import result into Rust's return area. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ImportWriter<'a> { + Slots { + function: &'a str, + prepare: Option<&'a str>, + expressions: Vec<&'a str>, + }, + Value { + function: &'a str, + }, +} + +/// The `ABI` shape of one imported result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ImportOutputAbi<'a> { + Direct { + slot: Slot<'a>, + conversion: Option>, + }, + Indirect { + retptr: ImportRetptr<'a>, + writer: ImportWriter<'a>, + }, +} + +/// One value returned by an imported JavaScript function. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportOutput<'a> { + pub abi: ImportOutputAbi<'a>, + pub error: ImportErrorMode, +} + +impl ImportOutput<'_> { + #[must_use] + pub const fn is_direct(&self) -> bool { + matches!(&self.abi, ImportOutputAbi::Direct { .. }) + } +} + +/// JavaScript call expressions and required embeds for one import. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportBinding<'a> { + pub direct_expression: Option<&'a str>, + pub call_expression: &'a str, + pub embeds: Vec>, +} + +/// One decoded JavaScript import. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Import<'a> { + pub module: &'a str, + pub name: &'a str, + pub inputs: Vec>, + pub output: Option>, + pub binding: Option>, + pub suspending: bool, +} + +/// One decoded group of JavaScript imports and its shared exception lowering. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportGroup<'a> { + pub catch: Option>, + pub imports: Vec>, +} + +/// Why an export input exists in the Rust call `ABI`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ExportInputKind { + Value, + ClosureData, +} + +/// JavaScript conversion for one exported argument. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExportInputConversion<'a> { + pub prepare: Option<&'a str>, + pub expressions: Vec<&'a str>, +} + +/// One JavaScript argument accepted by a `Wasm` export. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExportInput<'a> { + pub kind: ExportInputKind, + pub name: &'a str, + pub slots: Vec>, + pub conversion: Option>, +} + +/// One slot loaded from an indirect Rust return frame. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FrameSlot<'a> { + pub slot: Slot<'a>, + pub offset: u64, +} + +/// Stack storage used by an indirect Rust return. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReturnFrame<'a> { + pub size: u64, + pub slots: Vec>, +} + +/// One value returned from Rust to JavaScript. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ExportOutput<'a> { + Direct { + slot: Slot<'a>, + js_conversion: Option<&'a str>, + }, + Indirect { + frame: ReturnFrame<'a>, + js_conversion: Option<&'a str>, + result: Option, + }, +} + +impl<'a> ExportOutput<'a> { + #[must_use] + pub const fn is_direct(&self) -> bool { + matches!(self, Self::Direct { .. }) + } + + #[must_use] + pub const fn js_conversion(&self) -> Option<&'a str> { + match self { + Self::Direct { js_conversion, .. } | Self::Indirect { js_conversion, .. } => { + *js_conversion + } + } + } + + #[must_use] + pub const fn result(&self) -> Option { + match self { + Self::Direct { .. } => None, + Self::Indirect { result, .. } => *result, + } + } +} + +/// How a public export shim reaches Rust code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Callee<'a> { + Symbol { name: &'a str }, + Closure { call_shim_offset: u64 }, +} + +/// One decoded JavaScript-facing `Wasm` export. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Export<'a> { + pub module: &'a str, + pub name: &'a str, + pub pointer_width: PointerWidth, + pub inputs: Vec>, + pub output: Option>, + pub embeds: Vec>, + pub promising: bool, + pub callee: Callee<'a>, +} + +impl Export<'_> { + #[must_use] + pub const fn pointer_type(&self) -> WatType { + self.pointer_width.wat_type() + } +} + +/// One decoded import or export group. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Record<'a> { + Imports(ImportGroup<'a>), + Exports(Vec>), +} diff --git a/host/wire/src/schema.rs b/host/wire/src/schema.rs new file mode 100644 index 00000000..3e545c50 --- /dev/null +++ b/host/wire/src/schema.rs @@ -0,0 +1,489 @@ +//! Static, const-constructible wire descriptions. + +use core::mem::size_of; + +use crate::{ + EXPORT_HAS_OUTPUT, EXPORT_PROMISING, + abi::{ + FromJsConv, IntoJsConv, JsCatch, JsEmbed, ResultLayout, ReturnConv, ReturnMode, Sret, + WatCatch, WatSlot, WatType, + }, +}; + +/// The target's native pointer width. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PointerWidth { + Wasm32, + Wasm64, +} + +impl PointerWidth { + /// Returns the width of a native pointer in bytes. + #[must_use] + pub const fn bytes(self) -> u8 { + match self { + Self::Wasm32 => 4, + Self::Wasm64 => 8, + } + } + + /// Returns the `WAT` type of a native pointer. + #[must_use] + pub const fn wat_type(self) -> WatType { + match self { + Self::Wasm32 => WatType::I32, + Self::Wasm64 => WatType::I64, + } + } + + pub(crate) const fn native() -> Self { + if size_of::() == 4 { + Self::Wasm32 + } else { + assert!(size_of::() == 8); + Self::Wasm64 + } + } +} + +/// Type-level data shared by imported arguments of the same Rust type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportInputType { + pub(crate) slots: [Option; 4], + pub(crate) conversion: Option, +} + +impl WireImportInputType { + #[must_use] + pub const fn new(slots: [Option; 4], conversion: Option) -> Self { + Self { slots, conversion } + } +} + +/// Type-level data shared by imported results of the same Rust type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportOutputType { + pub(crate) mode: ReturnMode, + pub(crate) conversion: ReturnConv, + pub(crate) sret: Option, + pub(crate) slots: [Option; 4], +} + +impl WireImportOutputType { + #[must_use] + pub const fn new( + mode: ReturnMode, + conversion: ReturnConv, + sret: Option, + slots: [Option; 4], + ) -> Self { + assert!(mode.is_direct() == sret.is_none()); + assert!(conversion.conversion().is_some() || sret.is_none()); + Self { + mode, + conversion, + sret, + slots, + } + } +} + +/// Exception lowering shared by all `Result` entries in an import type table. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WireImportCatch { + /// JavaScript wraps the call in `try`/`catch` and records the exception. + JavaScript(JsCatch), + /// Wasm exception handling catches and records the exception in the `ABI` shim. + Wasm(WatCatch), +} + +/// Type definitions shared by one group of JavaScript imports. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportTypeTable { + pub(crate) retptr_type: &'static WireImportInputType, + pub(crate) input_types: &'static [&'static WireImportInputType], + pub(crate) output_types: &'static [&'static WireImportOutputType], + pub(crate) catch: WireImportCatch, +} + +impl WireImportTypeTable { + #[must_use] + pub const fn new( + retptr_type: &'static WireImportInputType, + input_types: &'static [&'static WireImportInputType], + output_types: &'static [&'static WireImportOutputType], + catch: WireImportCatch, + ) -> Self { + Self { + retptr_type, + input_types, + output_types, + catch, + } + } + + #[must_use] + pub(crate) const fn has_result(&self) -> bool { + let mut index = 0; + while index < self.output_types.len() { + if self.output_types[index].conversion.is_result() { + return true; + } + index += 1; + } + false + } +} + +/// One named argument accepted by a JavaScript import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportInput { + pub(crate) name: &'static str, + pub(crate) type_index: usize, +} + +impl WireImportInput { + #[must_use] + pub const fn new(name: &'static str, type_index: usize) -> Self { + Self { name, type_index } + } +} + +/// JavaScript binding data for one import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportBinding { + pub(crate) direct: Option<&'static str>, + pub(crate) call: &'static str, + pub(crate) required_embeds: &'static [JsEmbed], +} + +impl WireImportBinding { + #[must_use] + pub const fn new( + direct: Option<&'static str>, + call: &'static str, + required_embeds: &'static [JsEmbed], + ) -> Self { + Self { + direct, + call, + required_embeds, + } + } +} + +/// The result type referenced by one JavaScript import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportOutput { + pub(crate) type_index: usize, +} + +impl WireImportOutput { + #[must_use] + pub const fn new(type_index: usize) -> Self { + Self { type_index } + } +} + +/// One semantic JavaScript import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImport { + pub(crate) module: &'static str, + pub(crate) name: &'static str, + pub(crate) inputs: &'static [WireImportInput], + pub(crate) output: Option, + pub(crate) binding: Option, + pub(crate) suspending: bool, +} + +impl WireImport { + #[must_use] + pub const fn new( + module: &'static str, + name: &'static str, + inputs: &'static [WireImportInput], + output: Option, + binding: Option, + suspending: bool, + ) -> Self { + assert!(!suspending || binding.is_some()); + Self { + module, + name, + inputs, + output, + binding, + suspending, + } + } +} + +/// Type-level data shared by exported arguments with the same Rust type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireExportInputType { + pub(crate) slots: [Option; 4], + pub(crate) conversion: Option, +} + +impl WireExportInputType { + #[must_use] + pub const fn new(slots: [Option; 4], conversion: Option) -> Self { + Self { slots, conversion } + } +} + +/// One named argument accepted by a JavaScript-facing `Wasm` export. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireExportInput { + pub(crate) name: &'static str, + pub(crate) ty: &'static WireExportInputType, +} + +impl WireExportInput { + #[must_use] + pub const fn new(name: &'static str, ty: &'static WireExportInputType) -> Self { + Self { name, ty } + } +} + +/// Type-level data shared by exported results with the same Rust type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireExportOutputType { + pub(crate) mode: ReturnMode, + pub(crate) conversion: ReturnConv, + pub(crate) slots: [Option; 4], + pub(crate) frame_size: usize, + pub(crate) slot_offsets: [usize; 4], + pub(crate) result: Option, +} + +impl WireExportOutputType { + #[must_use] + pub const fn new( + mode: ReturnMode, + conversion: ReturnConv, + slots: [Option; 4], + frame_size: usize, + slot_offsets: [usize; 4], + result: Option, + ) -> Self { + assert!(conversion.is_result() == result.is_some()); + Self { + mode, + conversion, + slots, + frame_size, + slot_offsets, + result, + } + } +} + +/// The result type referenced by one JavaScript-facing `Wasm` export. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireExportOutput { + pub(crate) ty: &'static WireExportOutputType, +} + +impl WireExportOutput { + #[must_use] + pub const fn new(ty: &'static WireExportOutputType) -> Self { + Self { ty } + } +} + +/// Describes how an exported `Wasm` shim reaches Rust code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WireExportCallee { + Symbol(&'static str), + Closure { call_shim_offset: usize }, +} + +impl WireExportCallee { + #[must_use] + const fn symbol(name: &'static str) -> Self { + Self::Symbol(name) + } + + #[must_use] + const fn closure(call_shim_offset: usize) -> Self { + Self::Closure { call_shim_offset } + } +} + +/// One semantic JavaScript-facing export. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireExport { + pub(crate) module: &'static str, + pub(crate) name: &'static str, + pub(crate) inputs: &'static [WireExportInput], + pub(crate) output: Option, + pub(crate) callee: WireExportCallee, + pub(crate) flags: u8, +} + +impl WireExport { + #[must_use] + const fn new( + module: &'static str, + name: &'static str, + inputs: &'static [WireExportInput], + output: Option, + callee: WireExportCallee, + promising: bool, + ) -> Self { + Self { + module, + name, + inputs, + output, + callee, + flags: flag(promising, EXPORT_PROMISING) | flag(output.is_some(), EXPORT_HAS_OUTPUT), + } + } + + #[must_use] + pub const fn new_symbol( + module: &'static str, + name: &'static str, + symbol: &'static str, + inputs: &'static [WireExportInput], + output: Option, + ) -> Self { + Self::new( + module, + name, + inputs, + output, + WireExportCallee::symbol(symbol), + false, + ) + } + + #[must_use] + pub const fn new_symbol_promising( + module: &'static str, + name: &'static str, + symbol: &'static str, + inputs: &'static [WireExportInput], + output: Option, + ) -> Self { + Self::new( + module, + name, + inputs, + output, + WireExportCallee::symbol(symbol), + true, + ) + } + + #[must_use] + pub const fn new_closure( + module: &'static str, + name: &'static str, + call_shim_offset: usize, + inputs: &'static [WireExportInput], + output: Option, + ) -> Self { + assert!(!inputs.is_empty(), "closure exports require a data input"); + Self::new( + module, + name, + inputs, + output, + WireExportCallee::closure(call_shim_offset), + false, + ) + } +} + +#[derive(Clone, Copy)] +pub(crate) enum WireKind { + Imports { + table: &'static WireImportTypeTable, + imports: &'static [WireImport], + }, + Exports(&'static [WireExport]), +} + +/// One import or export group encoded into a wire record. +#[derive(Clone, Copy)] +pub struct Wire { + pub(crate) pointer_width: PointerWidth, + pub(crate) kind: WireKind, +} + +impl Wire { + #[must_use] + pub const fn imports( + table: &'static WireImportTypeTable, + imports: &'static [WireImport], + ) -> Self { + Self::imports_with(PointerWidth::native(), table, imports) + } + + #[must_use] + pub(crate) const fn imports_with( + pointer_width: PointerWidth, + table: &'static WireImportTypeTable, + imports: &'static [WireImport], + ) -> Self { + validate_imports(table, imports); + Self { + pointer_width, + kind: WireKind::Imports { table, imports }, + } + } + + #[must_use] + pub const fn exports(exports: &'static [WireExport]) -> Self { + Self::exports_with(PointerWidth::native(), exports) + } + + #[must_use] + pub(crate) const fn exports_with( + pointer_width: PointerWidth, + exports: &'static [WireExport], + ) -> Self { + Self { + pointer_width, + kind: WireKind::Exports(exports), + } + } +} + +const fn validate_imports(table: &WireImportTypeTable, imports: &[WireImport]) { + let mut index = 0; + while index < imports.len() { + let import = &imports[index]; + let mut input_index = 0; + while input_index < import.inputs.len() { + assert!( + import.inputs[input_index].type_index < table.input_types.len(), + "import input type index is out of bounds", + ); + input_index += 1; + } + if let Some(output) = import.output { + assert!( + output.type_index < table.output_types.len(), + "import output type index is out of bounds", + ); + } + if import.suspending { + if let Some(output) = import.output { + assert!( + !table.output_types[output.type_index].conversion.is_result() + || matches!(table.catch, WireImportCatch::Wasm(_)), + "suspending Result imports require the Wasm exception-handling target feature", + ); + } + } + index += 1; + } +} + +const fn flag(enabled: bool, value: u8) -> u8 { + if enabled { value } else { 0 } +} diff --git a/host/wire/src/tests.rs b/host/wire/src/tests.rs new file mode 100644 index 00000000..8222b129 --- /dev/null +++ b/host/wire/src/tests.rs @@ -0,0 +1,396 @@ +use alloc::{vec, vec::Vec}; + +use crate::abi::{ + FromJsConv, IntoJsConv, JsCatch as AbiJsCatch, JsEmbed, RefType, ReturnConv, ReturnMode, Sret, + WatCatch as AbiWatCatch, WatConv as AbiWatConv, WatImport as AbiWatImport, + WatImportKind as AbiWatImportKind, WatIndexType, WatLocal as AbiWatLocal, WatSlot, WatType, +}; +use crate::model::*; +use crate::*; + +const I32: Option = Some(WatSlot::plain(WatType::I32)); +const I64: Option = Some(WatSlot::plain(WatType::I64)); +const WAT_IMPORTS: &[AbiWatImport] = &[ + AbiWatImport::new( + "env", + "support.function", + "support.function", + None, + AbiWatImportKind::Function { + parameters: &[WatType::I32], + results: &[WatType::ExternRef], + }, + ), + AbiWatImport::new( + "support", + "table", + "support.import.table", + Some("support.table"), + AbiWatImportKind::Table { + index_type: WatIndexType::I32, + minimum: 2, + maximum: None, + element: RefType::ExternRef, + }, + ), + AbiWatImport::new( + "support", + "table64", + "support.import.table64", + None, + AbiWatImportKind::Table { + index_type: WatIndexType::I64, + minimum: 3, + maximum: Some(8), + element: RefType::FuncRef, + }, + ), + AbiWatImport::new( + "support", + "exception", + "support.exception", + Some("support.exception"), + AbiWatImportKind::Tag { + parameters: &[WatType::ExternRef], + }, + ), +]; +const WAT_LOCALS: &[AbiWatLocal] = &[ + AbiWatLocal::new("support.value", WatType::ExternRef), + AbiWatLocal::new("support.index", WatType::I32), +]; +const WAT_CATCH: WireImportCatch = WireImportCatch::Wasm(AbiWatCatch::new( + WAT_IMPORTS, + WAT_LOCALS, + "(try_table (catch $support.exception $support.catch)", + ") local.set $support.index", +)); +const JS_CATCH_EMBEDS: &[JsEmbed] = &[JsEmbed::new("support", "table")]; +const JS_CATCH: WireImportCatch = WireImportCatch::JavaScript(AbiJsCatch::new( + JS_CATCH_EMBEDS, + "} catch ($error) { return false } }", + "} catch ($error) { store($error) } }", +)); +const CONVERTED_I32: Option = Some(WatSlot::new( + WatType::I32, + Some(AbiWatConv::new( + WAT_IMPORTS, + WAT_LOCALS, + "call $support.function (@reloc)", + WatType::ExternRef, + )), +)); + +const IMPORT_INPUT_TYPE: WireImportInputType = WireImportInputType::new( + [CONVERTED_I32, None, None, None], + Some(IntoJsConv::new("$slot1").with_embed("js_sys", "input.convert")), +); +const IMPORT_INPUT_TYPES: &[&WireImportInputType] = &[&IMPORT_INPUT_TYPE]; +const IMPORT_RETPTR_TYPE: WireImportInputType = WireImportInputType::new( + [I32, None, None, None], + Some(IntoJsConv::new("$slot1 >>> 0").with_embed("js_sys", "retptr.convert")), +); +const IMPORT_OUTPUT_TYPE_DIRECT: WireImportOutputType = WireImportOutputType::new( + ReturnMode::Direct, + ReturnConv::Value(None), + None, + [I32, None, None, None], +); +const IMPORT_OUTPUT_TYPE_INDIRECT: WireImportOutputType = WireImportOutputType::new( + ReturnMode::Indirect, + ReturnConv::Result(Some( + FromJsConv::slot1("$ret[0]") + .prepare("prepare") + .slot2("$ret[1]") + .with_embed("js_sys", "output.convert"), + )), + Some(Sret::Slots("store")), + [I64, I64, None, None], +); +const IMPORT_OUTPUT_TYPES: &[&WireImportOutputType] = + &[&IMPORT_OUTPUT_TYPE_DIRECT, &IMPORT_OUTPUT_TYPE_INDIRECT]; +const IMPORT_TYPE_TABLE: WireImportTypeTable = WireImportTypeTable::new( + &IMPORT_RETPTR_TYPE, + IMPORT_INPUT_TYPES, + IMPORT_OUTPUT_TYPES, + WAT_CATCH, +); +const IMPORT_EMBEDS: &[JsEmbed] = &[JsEmbed::new("js_sys", "identity")]; +const IMPORTS: &[WireImport] = &[ + WireImport::new( + "js_sys", + "number.identity", + &[WireImportInput::new("arg0", 0)], + Some(WireImportOutput::new(0)), + Some(WireImportBinding::new( + Some("globalThis.identity"), + "globalThis.identity(arg0)", + IMPORT_EMBEDS, + )), + false, + ), + WireImport::new( + "js_sys", + "wide.suspending", + &[], + Some(WireImportOutput::new(1)), + Some(WireImportBinding::new(None, "globalThis.wide()", &[])), + true, + ), +]; +const IMPORT_WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &IMPORT_TYPE_TABLE, IMPORTS); +const IMPORT_LEN: usize = wire_blob_len(&IMPORT_WIRE); +const IMPORT_RECORD: WireRecord = WireRecord::new(&IMPORT_WIRE); + +const EXPORT_I32_INPUT: WireExportInputType = + WireExportInputType::new([I32, None, None, None], None); +const EXPORT_PTR_INPUT: WireExportInputType = + WireExportInputType::new([I64, None, None, None], None); +const EXPORT_DIRECT_OUTPUT: WireExportOutputType = WireExportOutputType::new( + ReturnMode::Direct, + ReturnConv::Value(None), + [I32, None, None, None], + 0, + [0; 4], + None, +); +const EXPORT_RESULT_UNIT_OUTPUT: WireExportOutputType = WireExportOutputType::new( + ReturnMode::Indirect, + ReturnConv::Result(None), + [None, None, I32, I32], + 16, + [0, 0, 0, 8], + Some(ResultLayout::new(0, 1)), +); +const EXPORTS: &[WireExport] = &[ + WireExport::new_symbol( + "exports", + "foo", + "foo.raw", + &[WireExportInput::new("arg0", &EXPORT_I32_INPUT)], + Some(WireExportOutput::new(&EXPORT_DIRECT_OUTPUT)), + ), + WireExport::new_closure( + "exports", + "closure", + 0x1_0000_000c, + &[WireExportInput::new("data", &EXPORT_PTR_INPUT)], + Some(WireExportOutput::new(&EXPORT_RESULT_UNIT_OUTPUT)), + ), +]; +const EXPORT_WIRE: Wire = Wire::exports_with(PointerWidth::Wasm64, EXPORTS); +const EXPORT_LEN: usize = wire_blob_len(&EXPORT_WIRE); +const EXPORT_RECORD: WireRecord = WireRecord::new(&EXPORT_WIRE); + +#[test] +fn imports() { + let Record::Imports(group) = decode(IMPORT_RECORD.as_bytes()).unwrap() else { + panic!("expected imports"); + }; + let imports = &group.imports; + assert_eq!(imports.len(), 2); + assert_eq!(imports[0].name, "number.identity"); + assert_eq!( + imports[0] + .binding + .as_ref() + .unwrap() + .embeds + .iter() + .map(|embed| embed.name) + .collect::>(), + ["input.convert", "identity"] + ); + let conversion = imports[0].inputs[0].slots[0].wat.as_ref().unwrap(); + assert_eq!(conversion.boundary, WatType::ExternRef); + assert_eq!(conversion.imports.len(), 4); + assert_eq!(conversion.imports[0].identifier, "support.function"); + assert!(matches!( + &conversion.imports[0].kind, + WatImportKind::Function { + parameters, + results, + } if parameters == &[WatType::I32] && results == &[WatType::ExternRef] + )); + assert!(matches!( + &conversion.imports[1].kind, + WatImportKind::Table { + index_type: WatIndexType::I32, + minimum: 2, + maximum: None, + element: RefType::ExternRef, + } + )); + assert!(matches!( + &conversion.imports[2].kind, + WatImportKind::Table { + index_type: WatIndexType::I64, + minimum: 3, + maximum: Some(8), + element: RefType::FuncRef, + } + )); + assert!(matches!( + &conversion.imports[3].kind, + WatImportKind::Tag { parameters } if parameters == &[WatType::ExternRef] + )); + assert_eq!( + conversion.locals.as_ref(), + &[ + WatLocal { + name: "support.value", + ty: WatType::ExternRef, + }, + WatLocal { + name: "support.index", + ty: WatType::I32, + }, + ] + ); + assert!(imports[1].suspending); + let Some(ImportCatch::Wasm(catch)) = &group.catch else { + panic!("expected Wasm catch metadata"); + }; + assert_eq!(catch.imports.len(), 4); + assert_eq!(catch.locals.len(), 2); + assert_eq!( + catch.try_, + "(try_table (catch $support.exception $support.catch)" + ); + assert_eq!(catch.catch, ") local.set $support.index"); + assert_eq!( + imports[1] + .binding + .as_ref() + .unwrap() + .embeds + .iter() + .map(|embed| embed.name) + .collect::>(), + ["retptr.convert", "output.convert"] + ); + assert!(matches!( + imports[1].output.as_ref().unwrap().abi, + ImportOutputAbi::Indirect { .. } + )); +} + +#[test] +fn javascript_catch_roundtrip() { + const TABLE: WireImportTypeTable = WireImportTypeTable::new( + &IMPORT_RETPTR_TYPE, + IMPORT_INPUT_TYPES, + IMPORT_OUTPUT_TYPES, + JS_CATCH, + ); + const IMPORTS: &[WireImport] = &[WireImport::new( + "support", + "fallible", + &[], + Some(WireImportOutput::new(1)), + Some(WireImportBinding::new(None, "fallible()", &[])), + false, + )]; + const WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &TABLE, IMPORTS); + const LEN: usize = wire_blob_len(&WIRE); + const RECORD: WireRecord = WireRecord::new(&WIRE); + + let Record::Imports(group) = decode(RECORD.as_bytes()).unwrap() else { + panic!("expected imports"); + }; + let Some(ImportCatch::JavaScript(catch)) = group.catch else { + panic!("expected JavaScript catch metadata"); + }; + assert_eq!(catch.direct, "} catch ($error) { return false } }"); + assert_eq!(catch.indirect, "} catch ($error) { store($error) } }"); + assert_eq!( + catch.embeds.as_ref(), + &[Embed { + module: "support", + name: "table" + }] + ); + assert_eq!( + group.imports[0].output.as_ref().unwrap().error, + ImportErrorMode::CatchInJavaScript + ); +} + +#[test] +fn catch_payload_is_omitted_without_result_types() { + const OUTPUT_TYPES: &[&WireImportOutputType] = &[&IMPORT_OUTPUT_TYPE_DIRECT]; + const TABLE: WireImportTypeTable = + WireImportTypeTable::new(&IMPORT_RETPTR_TYPE, &[], OUTPUT_TYPES, JS_CATCH); + const WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &TABLE, &[]); + const LEN: usize = wire_blob_len(&WIRE); + const RECORD: WireRecord = WireRecord::new(&WIRE); + + let Record::Imports(group) = decode(RECORD.as_bytes()).unwrap() else { + panic!("expected imports"); + }; + assert!(group.catch.is_none()); + assert!(group.imports.is_empty()); + assert!( + !RECORD + .as_bytes() + .windows("catch ($error)".len()) + .any(|window| window == b"catch ($error)") + ); +} + +#[test] +fn exports() { + let Record::Exports(exports) = decode(EXPORT_RECORD.as_bytes()).unwrap() else { + panic!("expected exports"); + }; + assert_eq!(exports.len(), 2); + assert_eq!(exports[0].pointer_width, PointerWidth::Wasm64); + assert!(matches!( + exports[1].callee, + Callee::Closure { + call_shim_offset: 0x1_0000_000c + } + )); + let Some(ExportOutput::Indirect { frame, result, .. }) = &exports[1].output else { + panic!("expected indirect output"); + }; + assert_eq!(frame.slots.len(), 2); + assert_eq!( + *result, + Some(ResultLayout { + discriminant: 0, + error: 1 + }) + ); +} + +#[test] +fn invalid_records() { + let mut version = IMPORT_RECORD.as_bytes().to_vec(); + version[8] = 0xff; + assert!(matches!( + decode(&version).unwrap_err().kind(), + ErrorKind::UnsupportedVersion(_) + )); + + let mut kind = IMPORT_RECORD.as_bytes().to_vec(); + kind[11] = 0xff; + assert_eq!( + decode(&kind).unwrap_err().kind(), + &ErrorKind::UnknownRecordKind(0xff) + ); + + let mut pointer_width = IMPORT_RECORD.as_bytes().to_vec(); + pointer_width[10] = 8; + assert!(matches!( + decode(&pointer_width).unwrap_err().kind(), + ErrorKind::InvalidValue(_) + )); + + let mut trailing = Vec::from(IMPORT_RECORD.as_bytes().as_slice()); + trailing.extend(vec![0]); + assert_eq!( + decode(&trailing).unwrap_err().kind(), + &ErrorKind::TrailingBytes(1) + ); +} diff --git a/web/playground/src/main.rs b/web/playground/src/main.rs index 8327b10b..886d436c 100644 --- a/web/playground/src/main.rs +++ b/web/playground/src/main.rs @@ -1,28 +1,27 @@ -#![feature(random)] -use std::random::random; use std::time::Instant; -use js_sys::{Error, Uint8Array}; +use js_sys::hazard::JsCast; +use js_sys::{Function, JsString, JsValue, Promise, closure}; fn main() { let ins = Instant::now(); - let err = Error::new("hahah"); - let bits: u128 = random(..); - let g1 = (bits >> 96) as u32; - let g2 = (bits >> 80) as u16; - let g3 = (0x4000 | (bits >> 64) & 0x0fff) as u16; - let g4 = (0x8000 | (bits >> 48) & 0x3fff) as u16; - let g5 = (bits & 0xffffffffffff) as u64; - let uuid = format!("{g1:08x}-{g2:04x}-{g3:04x}-{g4:04x}-{g5:012x}"); + let value = JsString::from("hahaha").into(); + let executor = closure!(dyn FnMut(Function, Function), move |resolve, _reject| { + resolve + .call(&JsValue::UNDEFINED, core::slice::from_ref(&value)) + .unwrap(); + }); + let f1 = async { + let ret = Promise::new(&executor).await.unwrap(); + String::from(&JsString::unchecked_from(ret)) + }; + let f2 = async move { 1 }; - for v in Uint8Array::from(&[0, 1, 2, 3]) { - println!("{v}"); - } + let f1 = js_sys::block_on(f1); + let f2 = js_sys::block_on(f2); - let elapsed = ins.elapsed(); - println!("JS {}", err.to_string()); - println!("result: {uuid}, cost: {elapsed:?}"); + println!("future1: {f1}, future2: {f2:?}, cost: {:?}", ins.elapsed()); } #[cfg(test)] From c0948b21eaf255b462dac88a69229b10d9d60cd6 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:07:43 +0800 Subject: [PATCH 16/21] Deduplicate Closure shims with COMDAT --- host/js-sys-bindgen/src/closure.rs | 23 ++++++++++++------ host/js-sys-bindgen/src/function.rs | 18 ++++++++++++++ host/js-sys-bindgen/src/macro.rs | 12 ++++++++-- host/ld/src/pre.rs | 20 ++++------------ host/ld/src/wire/import/wat.rs | 5 ++++ host/ld/src/wire/wat.rs | 37 ----------------------------- host/wire/src/decode/import.rs | 12 +++++++--- host/wire/src/encode/import.rs | 14 +++++++---- host/wire/src/lib.rs | 4 +++- host/wire/src/model.rs | 6 ++++- host/wire/src/schema.rs | 23 ++++++++++++++++++ host/wire/src/tests.rs | 4 +++- 12 files changed, 105 insertions(+), 73 deletions(-) diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index 8933df88..e583ef3e 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -12,6 +12,8 @@ use syn::{ use xxhash_rust::xxh3::xxh3_128; use crate::export::{ExportAbi, lower_abi}; +use crate::hygiene::Hygiene; +use crate::{function, r#macro::render_import_groups}; mod keyword { syn::custom_keyword!(js_sys); @@ -55,6 +57,18 @@ pub(crate) fn closure_with( ), span, ); + let factory_item = parse_quote_spanned! {span=> + #[js_sys(js_embed = #factory_name)] + fn #factory_ident( + data: ::core::primitive::usize, + ) -> #js_sys::JsValue; + }; + let mut hygiene = Hygiene::Qualified { + js_sys: Some(&js_sys), + }; + let (factory_function, factory_import) = + function::expand_closure_factory(&mut hygiene, crate_name, factory_item)?; + let factory_wire = render_import_groups(vec![factory_import]); let inputs: Vec<_> = signature.inputs.iter().collect(); let output = signature.output.as_ref(); let ExportAbi { @@ -183,13 +197,8 @@ pub(crate) fn closure_with( #factory_js, } - #[#js_sys::js_sys(js_sys = #js_sys)] - extern "js-sys" { - #[js_sys(js_embed = #factory_name)] - fn #factory_ident( - data: ::core::primitive::usize, - ) -> #js_sys::JsValue; - } + #factory_function + #factory_wire let allocation = allocate(#expression); let value = #factory_ident(allocation.data()); diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index 5d629d04..ceba9b07 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -30,9 +30,16 @@ pub(crate) struct FunctionImport { pub(crate) output_type: Option, pub(crate) binding: Option, pub(crate) suspending: bool, + pub(crate) kind: FunctionImportKind, pub(crate) macro_path: Path, } +#[derive(Clone, Copy)] +pub(crate) enum FunctionImportKind { + Normal, + ClosureFactory, +} + /// JavaScript binding data shared by the flat and direct Wire emitters. pub(crate) struct FunctionBinding { pub(crate) direct: Option, @@ -232,6 +239,16 @@ pub(crate) fn expand( Ok((item, import)) } +pub(crate) fn expand_closure_factory( + hygiene: &mut Hygiene<'_>, + crate_: &str, + item: ForeignItemFn, +) -> Result<(TokenStream, FunctionImport)> { + let (function, mut import) = expand(hygiene, None, crate_, &HashMap::new(), item)?; + import.kind = FunctionImportKind::ClosureFactory; + Ok((function, import)) +} + impl FunctionPlan { fn parse( hygiene: &mut Hygiene<'_>, @@ -690,6 +707,7 @@ impl FunctionPlan { output_type: output_abi_ty.cloned(), binding, suspending: *suspending, + kind: FunctionImportKind::Normal, macro_path: macro_path.clone(), } } diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index b47aa1ed..255d7c60 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -239,7 +239,7 @@ struct ImportGroup { macro_path: Path, } -fn render_import_groups(imports: Vec) -> TokenStream { +pub(crate) fn render_import_groups(imports: Vec) -> TokenStream { let mut groups: Vec = Vec::new(); for import in imports { @@ -288,6 +288,14 @@ fn render_import_groups(imports: Vec) -> TokenStream { let name = &import.name; let input_names = &import.input_names; let suspending = import.suspending; + let constructor = match import.kind { + crate::function::FunctionImportKind::Normal => { + quote::quote!(#macro_path::WireImport::new) + } + crate::function::FunctionImportKind::ClosureFactory => { + quote::quote!(#macro_path::WireImport::closure_factory) + } + }; let input_indices: Vec<_> = import .input_types .iter() @@ -335,7 +343,7 @@ fn render_import_groups(imports: Vec) -> TokenStream { }; wire_descriptors.push(quote::quote! { - #macro_path::WireImport::new( + #constructor( #module, #name, &[#(#wire_inputs),*], diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index a2de1100..a25444ae 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::ffi::{OsStr, OsString}; use std::fs; use std::path::Path; @@ -63,7 +62,6 @@ pub fn processing<'a>(args: &'a Arguments<'a>) -> PreOutput<'a> { let main_memory = main_memory(arch, args, &mut add_args); let mut js_store = JsStore::default(); - let mut seen_wat = HashSet::new(); let mut is_test = false; // Extract embedded WAT from object files. @@ -75,7 +73,6 @@ pub fn processing<'a>(args: &'a Arguments<'a>) -> PreOutput<'a> { js_bindgen_ld_shared::ld_input_parser(input, |path, data, object_mtime| -> Result<()> { process_object( &mut js_store, - &mut seen_wat, matches!(arch, Arch::Wasm64), &mut add_args, path, @@ -144,7 +141,6 @@ fn compile_wat( /// them and passes them to the linker. fn process_object( js_store: &mut JsStore, - seen_wat: &mut HashSet, wasm64: bool, add_args: &mut Vec, archive_path: &Path, @@ -173,9 +169,6 @@ fn process_object( Payload::CustomSection(c) if c.name() == WAT_SECTION => { for wat in JsBindgenWatSectionParser::new(c) { let wasm_path = next_wasm_object(); - if !seen_wat.insert(wat.to_owned()) { - continue; - } let wasm_bytes = compile_wat(&wasm_path, wasm64, wat, object_mtime)?; let exist_file; @@ -188,7 +181,6 @@ fn process_object( process_object( js_store, - seen_wat, wasm64, &mut Vec::new(), &wasm_path, @@ -216,10 +208,8 @@ fn process_object( if let Some(wat) = rendered.wat { let wasm_path = next_wasm_object(); - if seen_wat.insert(wat.clone()) { - compile_wat(&wasm_path, wasm64, &wat, object_mtime)?; - add_args.push(wasm_path.into()); - } + compile_wat(&wasm_path, wasm64, &wat, object_mtime)?; + add_args.push(wasm_path.into()); } } RenderedRecord::Exports(exports) => { @@ -230,10 +220,8 @@ fn process_object( add_args.push(format!("--export={name}").into()); let wasm_path = next_wasm_object(); - if seen_wat.insert(shim.clone()) { - compile_wat(&wasm_path, wasm64, &shim, object_mtime)?; - add_args.push(wasm_path.into()); - } + compile_wat(&wasm_path, wasm64, &shim, object_mtime)?; + add_args.push(wasm_path.into()); } } } diff --git a/host/ld/src/wire/import/wat.rs b/host/ld/src/wire/import/wat.rs index a192d744..908d9e99 100644 --- a/host/ld/src/wire/import/wat.rs +++ b/host/ld/src/wire/import/wat.rs @@ -1,5 +1,6 @@ use std::fmt::Write; +use js_bindgen_wire::WireImportKind; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{ Import, ImportCatch, ImportErrorMode, ImportGroup, ImportOutput, ImportOutputAbi, Slot, @@ -155,6 +156,10 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { } = shim; write!(wat, "(func ${}.{} (@sym)", import.module, import.name) .expect("writing to a String cannot fail"); + if import.kind == WireImportKind::ClosureFactory { + write!(wat, " (@comdat \"{}.{}\")", import.module, import.name) + .expect("writing to a String cannot fail"); + } if let Some(ImportOutput { abi: ImportOutputAbi::Indirect { retptr, .. }, diff --git a/host/ld/src/wire/wat.rs b/host/ld/src/wire/wat.rs index c0305ab1..8d0cddbf 100644 --- a/host/ld/src/wire/wat.rs +++ b/host/ld/src/wire/wat.rs @@ -149,40 +149,3 @@ fn write_separator(wat: &mut String) { wat.push('\n'); } } - -#[cfg(test)] -mod tests { - use super::WatImports; - - #[test] - fn identical_imports_are_emitted_once() { - let mut imports = WatImports::default(); - imports.insert( - "value", - "(import \"env\" \"value\" (global $value i32))".into(), - ); - imports.insert( - "value", - "(import \"env\" \"value\" (global $value i32))".into(), - ); - - assert_eq!( - imports.render(), - "(import \"env\" \"value\" (global $value i32))" - ); - } - - #[test] - #[should_panic(expected = "conflicting WAT imports use `$value`")] - fn conflicting_imports_are_rejected() { - let mut imports = WatImports::default(); - imports.insert( - "value", - "(import \"env\" \"value\" (global $value i32))".into(), - ); - imports.insert( - "value", - "(import \"env\" \"value\" (global $value i64))".into(), - ); - } -} diff --git a/host/wire/src/decode/import.rs b/host/wire/src/decode/import.rs index effc5b35..c1739c1f 100644 --- a/host/wire/src/decode/import.rs +++ b/host/wire/src/decode/import.rs @@ -1,9 +1,9 @@ use alloc::{rc::Rc, vec::Vec}; use crate::{ - Error, IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_FLAGS, IMPORT_HAS_BINDING, - IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_FLAGS, IMPORT_OUTPUT_RESULT, - IMPORT_SUSPENDING, PointerWidth, + Error, IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_CLOSURE_FACTORY, IMPORT_FLAGS, + IMPORT_HAS_BINDING, IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_FLAGS, + IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, PointerWidth, WireImportKind, abi::WatType, model::{ DirectImportConversion, Embed, Import, ImportBinding, ImportCatch, ImportErrorMode, @@ -246,6 +246,11 @@ impl<'a> Import<'a> { let module = decoder.string()?; let name = decoder.string()?; let flags = decoder.flags("import", IMPORT_FLAGS)?; + let kind = if flags & IMPORT_CLOSURE_FACTORY == 0 { + WireImportKind::Normal + } else { + WireImportKind::ClosureFactory + }; let input_count = decoder.count("import input")?; let mut inputs = Vec::with_capacity(input_count); @@ -314,6 +319,7 @@ impl<'a> Import<'a> { Ok(Self { module, name, + kind, inputs, output, binding, diff --git a/host/wire/src/encode/import.rs b/host/wire/src/encode/import.rs index 9984cb9c..1cd3b9c0 100644 --- a/host/wire/src/encode/import.rs +++ b/host/wire/src/encode/import.rs @@ -1,10 +1,10 @@ use core::mem::size_of; use crate::{ - IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_HAS_BINDING, IMPORT_HAS_OUTPUT, - IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, WireImport, WireImportBinding, - WireImportCatch, WireImportInput, WireImportInputType, WireImportOutput, WireImportOutputType, - WireImportTypeTable, + IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_CLOSURE_FACTORY, IMPORT_HAS_BINDING, + IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, WireImport, + WireImportBinding, WireImportCatch, WireImportInput, WireImportInputType, WireImportKind, + WireImportOutput, WireImportOutputType, WireImportTypeTable, abi::{JsCatch, WatCatch}, }; @@ -268,7 +268,11 @@ impl WireImport { encoder.string(self.name); encoder.u8(flag(self.suspending, IMPORT_SUSPENDING) | flag(self.output.is_some(), IMPORT_HAS_OUTPUT) - | flag(self.binding.is_some(), IMPORT_HAS_BINDING)); + | flag(self.binding.is_some(), IMPORT_HAS_BINDING) + | flag( + matches!(self.kind, WireImportKind::ClosureFactory), + IMPORT_CLOSURE_FACTORY, + )); encoder.count(self.inputs.len()); let mut index = 0; while index < self.inputs.len() { diff --git a/host/wire/src/lib.rs b/host/wire/src/lib.rs index 31138ee4..5b12f7e4 100644 --- a/host/wire/src/lib.rs +++ b/host/wire/src/lib.rs @@ -40,8 +40,10 @@ pub(crate) const WAT_IMPORT_TAG: u8 = 2; pub(crate) const IMPORT_SUSPENDING: u8 = 1 << 0; pub(crate) const IMPORT_HAS_OUTPUT: u8 = 1 << 1; pub(crate) const IMPORT_HAS_BINDING: u8 = 1 << 2; +pub(crate) const IMPORT_CLOSURE_FACTORY: u8 = 1 << 3; #[cfg(feature = "alloc")] -pub(crate) const IMPORT_FLAGS: u8 = IMPORT_SUSPENDING | IMPORT_HAS_OUTPUT | IMPORT_HAS_BINDING; +pub(crate) const IMPORT_FLAGS: u8 = + IMPORT_SUSPENDING | IMPORT_HAS_OUTPUT | IMPORT_HAS_BINDING | IMPORT_CLOSURE_FACTORY; pub(crate) const IMPORT_OUTPUT_DIRECT: u8 = 1 << 0; pub(crate) const IMPORT_OUTPUT_RESULT: u8 = 1 << 1; diff --git a/host/wire/src/model.rs b/host/wire/src/model.rs index 65b8f52f..7ef96d6e 100644 --- a/host/wire/src/model.rs +++ b/host/wire/src/model.rs @@ -4,7 +4,10 @@ use alloc::{rc::Rc, vec::Vec}; pub use crate::PointerWidth; pub use crate::abi::ResultLayout; -use crate::abi::{RefType, WatIndexType, WatType}; +use crate::{ + WireImportKind, + abi::{RefType, WatIndexType, WatType}, +}; /// One primitive `Wasm` `ABI` slot. #[derive(Clone, Debug, Eq, PartialEq)] @@ -199,6 +202,7 @@ pub struct ImportBinding<'a> { pub struct Import<'a> { pub module: &'a str, pub name: &'a str, + pub kind: WireImportKind, pub inputs: Vec>, pub output: Option>, pub binding: Option>, diff --git a/host/wire/src/schema.rs b/host/wire/src/schema.rs index 3e545c50..22f8bba9 100644 --- a/host/wire/src/schema.rs +++ b/host/wire/src/schema.rs @@ -185,11 +185,19 @@ impl WireImportOutput { } } +/// The role of one generated `Wasm` adapter. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WireImportKind { + Normal, + ClosureFactory, +} + /// One semantic JavaScript import. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WireImport { pub(crate) module: &'static str, pub(crate) name: &'static str, + pub(crate) kind: WireImportKind, pub(crate) inputs: &'static [WireImportInput], pub(crate) output: Option, pub(crate) binding: Option, @@ -210,12 +218,27 @@ impl WireImport { Self { module, name, + kind: WireImportKind::Normal, inputs, output, binding, suspending, } } + + #[must_use] + pub const fn closure_factory( + module: &'static str, + name: &'static str, + inputs: &'static [WireImportInput], + output: Option, + binding: Option, + suspending: bool, + ) -> Self { + let mut import = Self::new(module, name, inputs, output, binding, suspending); + import.kind = WireImportKind::ClosureFactory; + import + } } /// Type-level data shared by exported arguments with the same Rust type. diff --git a/host/wire/src/tests.rs b/host/wire/src/tests.rs index 8222b129..d3b39a16 100644 --- a/host/wire/src/tests.rs +++ b/host/wire/src/tests.rs @@ -117,7 +117,7 @@ const IMPORT_TYPE_TABLE: WireImportTypeTable = WireImportTypeTable::new( ); const IMPORT_EMBEDS: &[JsEmbed] = &[JsEmbed::new("js_sys", "identity")]; const IMPORTS: &[WireImport] = &[ - WireImport::new( + WireImport::closure_factory( "js_sys", "number.identity", &[WireImportInput::new("arg0", 0)], @@ -248,6 +248,8 @@ fn imports() { ] ); assert!(imports[1].suspending); + assert_eq!(imports[0].kind, WireImportKind::ClosureFactory); + assert_eq!(imports[1].kind, WireImportKind::Normal); let Some(ImportCatch::Wasm(catch)) = &group.catch else { panic!("expected Wasm catch metadata"); }; From 5c5388bf8f9c86dcee7fee4a85c3640e265e2fba Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:07:48 +0800 Subject: [PATCH 17/21] Refine Wire encoding and rendering --- client/e2e/examples/closure.rs | 3 +- client/e2e/examples/future.rs | 2 +- client/e2e/examples/jspi.rs | 9 +- client/e2e/examples/string.rs | 42 +--- client/e2e/examples/vec.rs | 14 -- client/js-sys/src/builtins/iterator.rs | 4 +- client/js-sys/src/builtins/mod.rs | 4 +- client/js-sys/src/hazard.rs | 189 +---------------- .../js-sys/src/interop/js/async_iterator.rs | 2 +- client/js-sys/src/interop/js/iterator.rs | 2 +- client/js-sys/src/interop/mod.rs | 2 + client/js-sys/src/interop/primitive.rs | 8 +- client/js-sys/src/interop/result.rs | 196 ++++++++++++++++++ client/js-sys/src/interop/string.rs | 4 +- client/js-sys/src/runtime/exception.rs | 3 +- client/js-sys/src/util.rs | 6 +- client/js-sys/src/wire/export.rs | 24 +-- client/js-sys/src/wire/import.rs | 22 +- client/js-sys/src/wire/mod.rs | 30 ++- client/js-sys/tests/hazard.rs | 50 ++++- client/web-sys/src/console.gen.rs | 26 +-- host/Cargo.toml | 4 +- host/js-sys-bindgen/Cargo.toml | 9 - host/js-sys-bindgen/src/closure.rs | 34 +-- host/js-sys-bindgen/src/export.rs | 23 +- host/js-sys-bindgen/src/function.rs | 88 ++++---- host/js-sys-bindgen/src/function/js.rs | 35 ++-- host/js-sys-bindgen/src/macro.rs | 49 ++--- .../src/tests/macro/function.rs | 8 +- host/js-sys-bindgen/src/tests/macro/member.rs | 2 +- host/js-sys-bindgen/src/tests/macro/mod.rs | 8 + host/ld-shared/src/lib.rs | 29 --- host/ld/src/js.rs | 11 +- host/ld/src/post.rs | 3 +- host/ld/src/pre.rs | 19 +- host/ld/src/wire/export/js.rs | 38 +++- host/ld/src/wire/export/wat.rs | 145 ++++++++++--- host/ld/src/wire/import/js.rs | 6 +- host/ld/src/wire/import/wat.rs | 170 ++++++++++++--- host/ld/src/wire/js.rs | 69 ++++++ host/ld/src/wire/mod.rs | 6 +- host/ld/src/wire/wat.rs | 44 +++- host/macro/tests/ui/custom_section.stderr | 45 +--- host/wire/Cargo.toml | 8 +- host/wire/src/abi.rs | 22 +- host/wire/src/decode/export.rs | 13 +- host/wire/src/decode/import.rs | 32 +-- host/wire/src/decode/mod.rs | 50 ++++- host/wire/src/decode/value.rs | 18 +- host/wire/src/encode/export.rs | 15 +- host/wire/src/encode/import.rs | 26 +-- host/wire/src/encode/mod.rs | 23 +- host/wire/src/lib.rs | 8 +- host/wire/src/model.rs | 24 +-- host/wire/src/schema.rs | 28 +-- host/wire/src/tests.rs | 120 ++++++++--- 56 files changed, 1095 insertions(+), 779 deletions(-) create mode 100644 client/js-sys/src/interop/result.rs diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs index 9f22e386..5d32895a 100644 --- a/client/e2e/examples/closure.rs +++ b/client/e2e/examples/closure.rs @@ -13,8 +13,7 @@ fn main() { // ;; exports["closure_unref_during_call"]() // ;; exports["closure_owned"](20) === 21 // ;; exports["closure_owned_lifecycle"]() - // ;; exports["closure_once"](20) === 21 - // ;; (() => { try { exports["closure_once_again"](20); return false } catch (error) { return error.message === "FnOnce called more than once" } })() + // ;; (() => { if (exports["closure_once"](20) !== 21) return false; try { exports["closure_once_again"](20); return false } catch (error) { return error.message === "FnOnce called more than once" } })() // ;; exports["closure_option_ref"](20) // ;; exports["closure_option_owned"](20) // ;; (() => { const callback = exports["closure_return"](2); return callback(40) === 42 })() diff --git a/client/e2e/examples/future.rs b/client/e2e/examples/future.rs index 17058d7c..93aacdab 100644 --- a/client/e2e/examples/future.rs +++ b/client/e2e/examples/future.rs @@ -8,7 +8,7 @@ fn main() { // ;; await (async () => { const value = {}; return await exports["shared_promise"](Promise.resolve(value)) === value })() // ;; await (async () => { exports["spawn_local_start"](); if (exports["spawn_local_done"]()) return false; await Promise.resolve(); return exports["spawn_local_done"]() })() // ;; await (async () => { const value = {}; return await exports["self_wake"](value) === value })() - // ;; await (async () => { const { promise, resolve } = Promise.withResolvers(); exports["drop_js_future"](promise); resolve(); await Promise.resolve(); return true })() + // ;; (() => { let settle; const thenable = { then(resolve) { settle = resolve } }; exports["drop_js_future"](thenable); if (typeof settle !== "function") return false; try { settle({}); return true } catch { return false } })() } use core::future::Future; diff --git a/client/e2e/examples/jspi.rs b/client/e2e/examples/jspi.rs index f4d875ed..f3bcd824 100644 --- a/client/e2e/examples/jspi.rs +++ b/client/e2e/examples/jspi.rs @@ -3,9 +3,7 @@ fn main() { // ;; await exports["jspi_block_on"]() === "resolved" // ;; await exports["jspi_u32"](0xffff_ffff) === 0xffff_ffff // ;; await exports["jspi_u128"](1n << 96n) === 1n << 96n - // ;; typeof exports["jspi_result"] !== "function" || await exports["jspi_result"](42) === 42 - // ;; typeof exports["jspi_result"] !== "function" || await (async () => { try { await exports["jspi_result"](-1); return false } catch (error) { return error === -1 } })() - // ;; typeof exports["jspi_result"] !== "function" || await (async () => { const [first, second] = await Promise.allSettled([exports["jspi_result"](-1), exports["jspi_result"](-2)]); return first.reason === -1 && second.reason === -2 })() + // ;; await (async () => { const enabled = exports["jspi_has_exception_handling"](); const result = exports["jspi_result"]; if ((typeof result === "function") !== enabled) return false; if (!enabled) return true; if (await result(42) !== 42) return false; const [first, second] = await Promise.allSettled([result(-1), result(-2)]); return first.status === "rejected" && first.reason === -1 && second.status === "rejected" && second.reason === -2 })() // ;; await (async () => { const value = { answer: 42 }; return await exports["jspi_js_value"](value) === value })() } @@ -76,6 +74,11 @@ fn jspi_u128(value: u128) -> u128 { suspend_u128(value) } +#[js_sys] +fn jspi_has_exception_handling() -> bool { + cfg!(target_feature = "exception-handling") +} + #[cfg(target_feature = "exception-handling")] #[js_sys(promising)] fn jspi_result(value: i32) -> Result { diff --git a/client/e2e/examples/string.rs b/client/e2e/examples/string.rs index ec3b3935..16ca2f15 100644 --- a/client/e2e/examples/string.rs +++ b/client/e2e/examples/string.rs @@ -1,27 +1,16 @@ #[rustfmt::skip] fn main() { - // ;; exports["rust_string"]() // ;; exports["rust_js_string"]() === "Hello from Rust! 🦀" // ;; exports["identity"]("Hello from JavaScript! 🦀") === "Hello from JavaScript! 🦀" // ;; exports["borrowed_js_string"]("borrowed") === true - // ;; exports["borrowed_js_value"]("value") === true - // ;; exports["borrowed_js_array"]([1, 2, 3]) === 3 // ;; exports["optional_js_string"](false) === undefined // ;; exports["optional_js_string"](true) === "optional" - // ;; exports["roundtrip"]("", "") - // ;; exports["roundtrip"]("Hello, World!", "Hello, World!") - // ;; exports["roundtrip"]("你好,世界!🦀", "你好,世界!🦀") - // ;; exports["roundtrip"]("a\0b", "a\0b") - // ;; exports["roundtrip"]("\ud800", "\ufffd") - // ;; (() => { const value = "js-bindgen 🦀 ".repeat(8_192); return exports["roundtrip"](value, value) })() // ;; exports["owned_roundtrip"]("") === "" - // ;; exports["owned_roundtrip"]("Hello from JavaScript! 🦀") === "Hello from JavaScript! 🦀" - // ;; exports["owned_roundtrip"]("\ufeffleading byte-order mark") === "\ufeffleading byte-order mark" + // ;; exports["owned_roundtrip"]("a\0b 你好 🦀") === "a\0b 你好 🦀" // ;; exports["owned_roundtrip"]("\ud800") === "\ufffd" // ;; (() => { const value = "owned 🦀 ".repeat(32_768); return exports["owned_roundtrip"](value) === value })() // ;; exports["optional_owned_roundtrip"](undefined) === undefined // ;; exports["optional_owned_roundtrip"](null) === undefined - // ;; exports["optional_owned_roundtrip"]("") === "" // ;; exports["optional_owned_roundtrip"]("optional 🦀") === "optional 🦀" // ;; exports["result_owned_string"](true) === "ok" // ;; (() => { try { exports["result_owned_string"](false); return false } catch (error) { return error === "owned error" } })() @@ -33,7 +22,7 @@ fn main() { // ;; (() => { try { exports["import_result_string"]("error"); return false } catch (error) { return error === "string error" } })() } -use js_sys::{Array, JsString, JsValue, js_sys}; +use js_sys::{JsString, JsValue, js_sys}; js_sys::js_bindgen::embed_js!( module = "string", @@ -53,12 +42,6 @@ extern "js-sys" { fn import_result_string_raw(value: String) -> Result; } -#[expect(clippy::cmp_owned, reason = "checked")] -#[js_sys] -fn rust_string() -> bool { - JsString::from("Hello from Rust! 🦀") == "Hello from Rust! 🦀" -} - #[js_sys] fn rust_js_string() -> JsString { JsString::from("Hello from Rust! 🦀") @@ -74,32 +57,11 @@ fn borrowed_js_string(value: &JsString) -> bool { value.eq(&"borrowed") } -#[js_sys] -fn borrowed_js_value(value: &JsValue) -> bool { - let expected = JsString::from("value"); - value == expected.as_ref() -} - -#[js_sys] -fn borrowed_js_array(value: &Array) -> u32 { - value.length() -} - #[js_sys] fn optional_js_string(some: bool) -> Option { some.then(|| JsString::from("optional")) } -#[expect(clippy::cmp_owned, reason = "checked")] -#[js_sys] -fn roundtrip(value: JsString, expected: JsString) -> bool { - let rust_value = String::from(&value); - let rust_expected = String::from(&expected); - drop(value); - drop(expected); - JsString::from(rust_value.as_str()) == rust_expected -} - #[js_sys] fn owned_roundtrip(value: String) -> String { value diff --git a/client/e2e/examples/vec.rs b/client/e2e/examples/vec.rs index 43163db6..a4e6d9d1 100644 --- a/client/e2e/examples/vec.rs +++ b/client/e2e/examples/vec.rs @@ -1,13 +1,9 @@ #[rustfmt::skip] fn main() { // ;; (() => { const first = {}; const last = {}; const input = [first, null, last]; const result = exports["js_value_roundtrip"](input); return result !== input && Array.isArray(result) && result.length === 3 && result[0] === first && result[1] === null && result[2] === last })() - // ;; (() => { const result = exports["js_value_roundtrip"]([]); return Array.isArray(result) && result.length === 0 })() // ;; (() => { try { exports["js_value_roundtrip"](new Uint32Array()); return false } catch (error) { return error instanceof TypeError } })() // ;; (() => { const input = new Uint32Array([0, 1, 0xffffffff]); const result = exports["u32_roundtrip"](input); return result !== input && result instanceof Uint32Array && result.length === 3 && result[0] === 0 && result[1] === 1 && result[2] === 0xffffffff })() - // ;; (() => { const result = exports["u32_roundtrip"](new Uint32Array()); return result instanceof Uint32Array && result.length === 0 })() // ;; (() => { try { exports["u32_roundtrip"]([]); return false } catch (error) { return error instanceof TypeError } })() - // ;; (() => { const result = exports["i8_roundtrip"](new Int8Array([-128, -1, 0, 127])); return result instanceof Int8Array && result.join() === '-128,-1,0,127' })() - // ;; (() => { const result = exports["u16_roundtrip"](new Uint16Array([0, 1, 0x8000, 0xffff])); return result instanceof Uint16Array && result.join() === '0,1,32768,65535' })() // ;; (() => { const result = exports["i64_roundtrip"](new BigInt64Array([-(1n << 63n), -1n, 0n, (1n << 63n) - 1n])); return result instanceof BigInt64Array && result[0] === -(1n << 63n) && result[3] === (1n << 63n) - 1n })() // ;; (() => { const result = exports["u64_roundtrip"](new BigUint64Array([0n, 1n, 18446744073709551615n])); return result instanceof BigUint64Array && result[2] === 18446744073709551615n })() // ;; (() => { const result = exports["f32_roundtrip"](new Float32Array([Math.fround(1 / 3), -0, Infinity, NaN])); return result instanceof Float32Array && result[0] === Math.fround(1 / 3) && Object.is(result[1], -0) && result[2] === Infinity && Number.isNaN(result[3]) })() @@ -31,16 +27,6 @@ fn u32_roundtrip(value: Vec) -> Vec { value } -#[js_sys] -fn i8_roundtrip(value: Vec) -> Vec { - value -} - -#[js_sys] -fn u16_roundtrip(value: Vec) -> Vec { - value -} - #[js_sys] fn i64_roundtrip(value: Vec) -> Vec { value diff --git a/client/js-sys/src/builtins/iterator.rs b/client/js-sys/src/builtins/iterator.rs index 4551a6af..aea74aeb 100644 --- a/client/js-sys/src/builtins/iterator.rs +++ b/client/js-sys/src/builtins/iterator.rs @@ -253,10 +253,10 @@ extern "js-sys" { #[crate::js_sys(js_sys = crate)] extern "js-sys" { #[js_sys(js_embed = "iterator.from")] - pub(crate) fn iterator_from(value: &JsValue) -> Result, JsValue>; + pub fn iterator_from(value: &JsValue) -> Result, JsValue>; #[js_sys(js_embed = "async_iterator.from")] - pub(crate) fn async_iterator_from(value: &JsValue) -> Result, JsValue>; + pub fn async_iterator_from(value: &JsValue) -> Result, JsValue>; } macro_rules! impl_wrapper { diff --git a/client/js-sys/src/builtins/mod.rs b/client/js-sys/src/builtins/mod.rs index bacf424a..79117a75 100644 --- a/client/js-sys/src/builtins/mod.rs +++ b/client/js-sys/src/builtins/mod.rs @@ -14,7 +14,7 @@ mod function; mod generator; mod global; mod intl; -pub(crate) mod iterator; +mod iterator; mod json; mod map; mod math; @@ -59,7 +59,7 @@ pub use global::{ pub use intl::Intl; pub use iterator::{ AsyncIterable, AsyncIterator, Iterable, IteratorResult, IteratorZipKeyedOptions, - IteratorZipMode, IteratorZipOptions, JsIterator, + IteratorZipMode, IteratorZipOptions, JsIterator, async_iterator_from, iterator_from, }; pub use json::JSON; pub use map::Map; diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 12eef9a2..45960518 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -7,7 +7,6 @@ pub use js_bindgen_wire::abi::{ }; use crate::JsValue; -use crate::runtime::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; // Wasm `ABI` carriers. @@ -338,194 +337,8 @@ where } } -// Result returns. - -/// The return `ABI` for exporting [`Result`] to JavaScript. -/// -/// The first two slots carry the successful value. The remaining two carry the -/// error discriminant and table index. -#[doc(hidden)] -pub struct ResultIntoJsAbi { - value: Result::Abi>, -} - -const RESULT_DISCRIMINANT_LOCAL: WatLocal = - WatLocal::new("js_sys.result.discriminant", WatType::I32); -const RESULT_ERROR_WAT_CONV: &str = "\ - local.set $js_sys.externref.index - local.get $js_sys.result.discriminant - if (result externref) - local.get $js_sys.externref.index - table.get $js_sys.import.externref.table (@reloc) - local.get $js_sys.externref.index - i32.const 2 - i32.ge_u - if - local.get $js_sys.externref.index - call $js_sys.externref.release (@reloc) - end - else - ref.null extern - end"; - -/// The discriminant of an exported [`Result`]. -#[doc(hidden)] -#[repr(transparent)] -pub struct ResultDiscriminantAbi(u32); - -// SAFETY: The transparent `i32` discriminant is also recorded in a local for -// the following error slot conversion. -unsafe impl Slot for ResultDiscriminantAbi { - const WAT_TYPE: Option = Some(WatType::I32); - const INTO_JS_WAT_CONV: Option = Some(WatConv::new( - &[], - &[RESULT_DISCRIMINANT_LOCAL], - "local.tee $js_sys.result.discriminant", - WatType::I32, - )); -} - -/// An owned `externref` table index transferred by a [`Result`] error. -/// -/// The preceding [`ResultDiscriminantAbi`] controls whether the index is taken -/// from the table. Successful results produce a null placeholder without -/// accessing the table. #[doc(hidden)] -#[repr(transparent)] -pub struct ResultErrorAbi(::Abi); - -// SAFETY: `JsValue` uses a transparent `i32` table index as its Rust `ABI`. The -// preceding result discriminant is recorded before this conversion runs. -unsafe impl Slot for ResultErrorAbi { - const WAT_TYPE: Option = Some(WatType::I32); - const INTO_JS_WAT_CONV: Option = Some(WatConv::new( - WAT_TAKE_IMPORTS, - &[WAT_INDEX_LOCAL], - RESULT_ERROR_WAT_CONV, - WatType::ExternRef, - )); -} - -// SAFETY: The first two slots match the successful value's `ABI`. The third -// is the error discriminant and the fourth transfers an owned error table -// index. -unsafe impl WasmAbi for ResultIntoJsAbi -where - T: WasmAbi, - T::Slot1: Default, - T::Slot2: Default, -{ - type Slot1 = T::Slot1; - type Slot2 = T::Slot2; - type Slot3 = ResultDiscriminantAbi; - type Slot4 = ResultErrorAbi; - - fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { - match self.value { - Ok(value) => { - let (slot1, slot2, _, _) = value.split(); - ( - slot1, - slot2, - ResultDiscriminantAbi(0), - ResultErrorAbi(JsValue::UNDEFINED.into_abi()), - ) - } - Err(error) => ( - Default::default(), - Default::default(), - ResultDiscriminantAbi(1), - ResultErrorAbi(error), - ), - } - } - - fn join( - slot1: Self::Slot1, - slot2: Self::Slot2, - is_error: Self::Slot3, - error: Self::Slot4, - ) -> Self { - let value = if is_error.0 == 0 { - Ok(T::join(slot1, slot2, EmptySlot::new(), EmptySlot::new())) - } else { - Err(error.0) - }; - - Self { value } - } -} - -// SAFETY: `ResultIntoJsAbi` is returned through a hidden pointer. -unsafe impl ReturnAbi for ResultIntoJsAbi -where - T: WasmAbi, - T::Slot1: Default, - T::Slot2: Default, -{ - const MODE: ReturnMode = ReturnMode::Indirect; - const RESULT_LAYOUT: Option = { - let discriminant = if T::Slot1::WAT_TYPE.is_none() { - 0 - } else if T::Slot2::WAT_TYPE.is_none() { - 1 - } else { - 2 - }; - - Some(ResultLayout::new(discriminant, discriminant + 1)) - }; -} - -impl ReturnIntoJS for Result -where - T: IntoJS, - E: Into, - T::Abi: WasmAbi, - ::Slot1: Default, - ::Slot2: Default, -{ - const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); - - type Abi = ResultIntoJsAbi; - - fn into_return_abi(self) -> Self::Abi { - let value = match self { - Ok(value) => Ok(value.into_abi()), - Err(error) => Err(error.into().into_abi()), - }; - - ResultIntoJsAbi { value } - } -} - -impl ReturnFromJS for Result -where - T: FromJS, - T::Abi: ReturnAbi, -{ - const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); - const JS_SRET: Option = T::JS_SRET; - - type Abi = T::Abi; - - fn from_return_abi(raw: MaybeUninit>) -> Self { - if let Some(error) = crate::runtime::exception::take() { - #[cfg(not(target_feature = "exception-handling"))] - if ::MODE.is_direct() { - // SAFETY: A direct Wasm return is always initialized. On the - // exception path it contains only the JavaScript fallback value. - drop(T::from_abi(unsafe { raw.assume_init() }.join())); - } - - Err(error) - } else { - // SAFETY: Without a stored exception, the JavaScript import - // initialized its successful return value. - Ok(T::from_abi(unsafe { raw.assume_init() }.join())) - } - } -} +pub use crate::interop::{ResultDiscriminantAbi, ResultErrorAbi, ResultIntoJsAbi}; // Borrowed and cast JavaScript values. diff --git a/client/js-sys/src/interop/js/async_iterator.rs b/client/js-sys/src/interop/js/async_iterator.rs index 46aec7c4..4f407b83 100644 --- a/client/js-sys/src/interop/js/async_iterator.rs +++ b/client/js-sys/src/interop/js/async_iterator.rs @@ -3,7 +3,7 @@ use core::pin::Pin; use core::task::{Context, Poll}; use super::iterator::{CapturedNext, async_iterator_next_cached, read_result}; -use crate::builtins::iterator::async_iterator_from; +use crate::builtins::async_iterator_from; use crate::hazard::JsCast; use crate::runtime::JsFuture; use crate::{AsyncIterator, IteratorResult, JsValue}; diff --git a/client/js-sys/src/interop/js/iterator.rs b/client/js-sys/src/interop/js/iterator.rs index 288d86e8..32eb33b1 100644 --- a/client/js-sys/src/interop/js/iterator.rs +++ b/client/js-sys/src/interop/js/iterator.rs @@ -1,7 +1,7 @@ use crate::builtins::Intl::{SegmentData, Segments}; -use crate::builtins::iterator::iterator_from; use crate::builtins::{ Array, AsyncIterator, Function, IteratorResult, JsIterator, JsString, Map, Promise, Set, + iterator_from, }; use crate::hazard::JsCast; use crate::{JsValue, js_sys}; diff --git a/client/js-sys/src/interop/mod.rs b/client/js-sys/src/interop/mod.rs index 85cfcffc..503e1c67 100644 --- a/client/js-sys/src/interop/mod.rs +++ b/client/js-sys/src/interop/mod.rs @@ -1,5 +1,6 @@ mod js; mod primitive; +mod result; mod slice; mod string; mod vec; @@ -8,3 +9,4 @@ pub use js::{ ArrayIntoIter, ArrayIter, AsyncIter, JsIntoIter, JsIter, TryFromArrayError, TypedArray, TypedArrayCopyError, TypedArrayIntoIter, TypedArrayIter, try_async_iter, try_iter, }; +pub use result::{ResultDiscriminantAbi, ResultErrorAbi, ResultIntoJsAbi}; diff --git a/client/js-sys/src/interop/primitive.rs b/client/js-sys/src/interop/primitive.rs index 1d67ba96..32ecc89d 100644 --- a/client/js-sys/src/interop/primitive.rs +++ b/client/js-sys/src/interop/primitive.rs @@ -348,9 +348,7 @@ unsafe impl FromJS for u128 { .slot2("$value >> 64n") .with_embed("js_sys", "numeric.128.encode"), ); - const JS_SRET: Option = Some(Sret::Slots( - "this.#jsEmbed.js_sys['numeric.128.encode']", - )); + const JS_SRET: Option = Some(Sret::Slots("this.#jsEmbed.js_sys['numeric.128.encode']")); type Abi = Self; @@ -414,9 +412,7 @@ unsafe impl FromJS for i128 { .slot2("$value >> 64n") .with_embed("js_sys", "numeric.128.encode"), ); - const JS_SRET: Option = Some(Sret::Slots( - "this.#jsEmbed.js_sys['numeric.128.encode']", - )); + const JS_SRET: Option = Some(Sret::Slots("this.#jsEmbed.js_sys['numeric.128.encode']")); type Abi = Self; diff --git a/client/js-sys/src/interop/result.rs b/client/js-sys/src/interop/result.rs new file mode 100644 index 00000000..580ded68 --- /dev/null +++ b/client/js-sys/src/interop/result.rs @@ -0,0 +1,196 @@ +use core::mem::MaybeUninit; + +use crate::JsValue; +use crate::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, ResultLayout, ReturnAbi, ReturnConv, + ReturnFromJS, ReturnIntoJS, ReturnMode, Slot, Sret, WasmAbi, WasmRet, WatConv, WatLocal, + WatType, +}; +use crate::runtime::externref::{WAT_INDEX_LOCAL, WAT_TAKE_IMPORTS}; + +/// The return `ABI` for exporting [`Result`] to JavaScript. +/// +/// The first two slots carry the successful value. The remaining two carry the +/// error discriminant and table index. +#[doc(hidden)] +pub struct ResultIntoJsAbi { + value: Result::Abi>, +} + +const RESULT_DISCRIMINANT_LOCAL: WatLocal = + WatLocal::new("js_sys.result.discriminant", WatType::I32); +const RESULT_ERROR_WAT_CONV: &str = "\ + local.set $js_sys.externref.index + local.get $js_sys.result.discriminant + if (result externref) + local.get $js_sys.externref.index + table.get $js_sys.import.externref.table (@reloc) + local.get $js_sys.externref.index + i32.const 2 + i32.ge_u + if + local.get $js_sys.externref.index + call $js_sys.externref.release (@reloc) + end + else + ref.null extern + end"; + +/// The discriminant of an exported [`Result`]. +#[doc(hidden)] +#[repr(transparent)] +pub struct ResultDiscriminantAbi(u32); + +// SAFETY: The transparent `i32` discriminant is also recorded in a local for +// the following error slot conversion. +unsafe impl Slot for ResultDiscriminantAbi { + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + &[], + &[RESULT_DISCRIMINANT_LOCAL], + "local.tee $js_sys.result.discriminant", + WatType::I32, + )); +} + +/// An owned `externref` table index transferred by a [`Result`] error. +/// +/// The preceding [`ResultDiscriminantAbi`] controls whether the index is taken +/// from the table. Successful results produce a null placeholder without +/// accessing the table. +#[doc(hidden)] +#[repr(transparent)] +pub struct ResultErrorAbi(::Abi); + +// SAFETY: `JsValue` uses a transparent `i32` table index as its Rust `ABI`. The +// preceding result discriminant is recorded before this conversion runs. +unsafe impl Slot for ResultErrorAbi { + const WAT_TYPE: Option = Some(WatType::I32); + const INTO_JS_WAT_CONV: Option = Some(WatConv::new( + WAT_TAKE_IMPORTS, + &[WAT_INDEX_LOCAL], + RESULT_ERROR_WAT_CONV, + WatType::ExternRef, + )); +} + +// SAFETY: The first two slots match the successful value's `ABI`. The third +// is the error discriminant and the fourth transfers an owned error table +// index. +unsafe impl WasmAbi for ResultIntoJsAbi +where + T: WasmAbi, + T::Slot1: Default, + T::Slot2: Default, +{ + type Slot1 = T::Slot1; + type Slot2 = T::Slot2; + type Slot3 = ResultDiscriminantAbi; + type Slot4 = ResultErrorAbi; + + fn split(self) -> (Self::Slot1, Self::Slot2, Self::Slot3, Self::Slot4) { + match self.value { + Ok(value) => { + let (slot1, slot2, _, _) = value.split(); + ( + slot1, + slot2, + ResultDiscriminantAbi(0), + ResultErrorAbi(JsValue::UNDEFINED.into_abi()), + ) + } + Err(error) => ( + Default::default(), + Default::default(), + ResultDiscriminantAbi(1), + ResultErrorAbi(error), + ), + } + } + + fn join( + slot1: Self::Slot1, + slot2: Self::Slot2, + is_error: Self::Slot3, + error: Self::Slot4, + ) -> Self { + let value = if is_error.0 == 0 { + Ok(T::join(slot1, slot2, EmptySlot::new(), EmptySlot::new())) + } else { + Err(error.0) + }; + + Self { value } + } +} + +// SAFETY: `ResultIntoJsAbi` is returned through a hidden pointer. +unsafe impl ReturnAbi for ResultIntoJsAbi +where + T: WasmAbi, + T::Slot1: Default, + T::Slot2: Default, +{ + const MODE: ReturnMode = ReturnMode::Indirect; + const RESULT_LAYOUT: Option = { + let discriminant = if T::Slot1::WAT_TYPE.is_none() { + 0 + } else if T::Slot2::WAT_TYPE.is_none() { + 1 + } else { + 2 + }; + + Some(ResultLayout::new(discriminant, discriminant + 1)) + }; +} + +impl ReturnIntoJS for Result +where + T: IntoJS, + E: Into, + T::Abi: WasmAbi, + ::Slot1: Default, + ::Slot2: Default, +{ + const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); + + type Abi = ResultIntoJsAbi; + + fn into_return_abi(self) -> Self::Abi { + let value = match self { + Ok(value) => Ok(value.into_abi()), + Err(error) => Err(error.into().into_abi()), + }; + + ResultIntoJsAbi { value } + } +} + +impl ReturnFromJS for Result +where + T: FromJS, + T::Abi: ReturnAbi, +{ + const JS_CONV: ReturnConv = ReturnConv::Result(T::JS_CONV); + const JS_SRET: Option = T::JS_SRET; + + type Abi = T::Abi; + + fn from_return_abi(raw: MaybeUninit>) -> Self { + if let Some(error) = crate::runtime::exception::take() { + #[cfg(not(target_feature = "exception-handling"))] + if ::MODE.is_direct() { + // SAFETY: A direct Wasm return is always initialized. On the + // exception path it contains only the JavaScript fallback value. + drop(T::from_abi(unsafe { raw.assume_init() }.join())); + } + + Err(error) + } else { + // SAFETY: Without a stored exception, the JavaScript import + // initialized its successful return value. + Ok(T::from_abi(unsafe { raw.assume_init() }.join())) + } + } +} diff --git a/client/js-sys/src/interop/string.rs b/client/js-sys/src/interop/string.rs index 3b48ed0b..5257abb9 100644 --- a/client/js-sys/src/interop/string.rs +++ b/client/js-sys/src/interop/string.rs @@ -425,9 +425,7 @@ unsafe impl FromJS for String { .prepare("this.#jsEmbed.js_sys['string.from_js'].slots($value)") .with_embed("js_sys", "string.from_js"), ); - const JS_SRET: Option = Some(Sret::Value( - "this.#jsEmbed.js_sys['string.from_js'].sret", - )); + const JS_SRET: Option = Some(Sret::Value("this.#jsEmbed.js_sys['string.from_js'].sret")); type Abi = StringAbi; diff --git a/client/js-sys/src/runtime/exception.rs b/client/js-sys/src/runtime/exception.rs index 1238c074..7ee763da 100644 --- a/client/js-sys/src/runtime/exception.rs +++ b/client/js-sys/src/runtime/exception.rs @@ -1,12 +1,13 @@ use core::cell::Cell; +use js_bindgen_wire::WireImportCatch; + use super::externref; use crate::JsValue; #[cfg(not(target_feature = "exception-handling"))] use crate::hazard::{JsCatch, JsEmbed}; #[cfg(target_feature = "exception-handling")] use crate::hazard::{WatCatch, WatImport, WatImportKind, WatType}; -use js_bindgen_wire::WireImportCatch; #[cfg(not(target_feature = "exception-handling"))] const JS_CATCH_EMBEDS: &[JsEmbed] = &[JsEmbed::new("js_sys", "externref.table")]; diff --git a/client/js-sys/src/util.rs b/client/js-sys/src/util.rs index f99ee1e4..8c135c55 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -147,7 +147,7 @@ unsafe impl Slot for PtrConst { const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; } -// SAFETY: The JavaScript conversion matches the WAT boundary representation. +// SAFETY: The JavaScript conversion matches the JavaScript-facing WAT type. unsafe impl IntoJS for PtrConst { const JS_CONV: Option = JsPointerType::JS_CONV; @@ -188,7 +188,7 @@ unsafe impl Slot for PtrMut { const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; } -// SAFETY: The JavaScript conversion matches the WAT boundary representation. +// SAFETY: The JavaScript conversion matches the JavaScript-facing WAT type. unsafe impl IntoJS for PtrMut { const JS_CONV: Option = JsPointerType::JS_CONV; @@ -244,7 +244,7 @@ unsafe impl Slot for PtrLength { const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; } -// SAFETY: The JavaScript conversion matches the WAT boundary representation. +// SAFETY: The JavaScript conversion matches the JavaScript-facing WAT type. unsafe impl IntoJS for PtrLength { const JS_CONV: Option = JsPointerType::JS_CONV; diff --git a/client/js-sys/src/wire/export.rs b/client/js-sys/src/wire/export.rs index 4960079a..6011d340 100644 --- a/client/js-sys/src/wire/export.rs +++ b/client/js-sys/src/wire/export.rs @@ -1,9 +1,8 @@ use crate::ClosureHeader; -use crate::hazard::{FromJS, ReturnAbi, ReturnIntoJS, Slot, WasmRet}; +use crate::hazard::{FromJS, ReturnAbi, ReturnIntoJS, WasmRet}; use crate::wire::{ - FromJsSlot1, FromJsSlot2, FromJsSlot3, FromJsSlot4, ReturnSlot1, ReturnSlot2, ReturnSlot3, - ReturnSlot4, WireExport, WireExportInput, WireExportInputType, WireExportOutput, - WireExportOutputType, wat_slot, + WireExport, WireExportInput, WireExportInputType, WireExportOutput, WireExportOutputType, + from_js_slots, into_js_slots, }; trait MetadataFor: 'static { @@ -11,27 +10,14 @@ trait MetadataFor: 'static { } impl MetadataFor for WireExportInputType { - const VALUE: &'static Self = &Self::new( - [ - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - ], - T::JS_CONV, - ); + const VALUE: &'static Self = &Self::new(from_js_slots::(), T::JS_CONV); } impl MetadataFor for WireExportOutputType { const VALUE: &'static Self = &{ let mode = ::MODE; let result_layout = ::RESULT_LAYOUT; - let slots = [ - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - ]; + let slots = into_js_slots::(); let (frame_size, slot_offsets) = if mode.is_direct() { (0, [0; 4]) } else { diff --git a/client/js-sys/src/wire/import.rs b/client/js-sys/src/wire/import.rs index d3abc958..e6d44674 100644 --- a/client/js-sys/src/wire/import.rs +++ b/client/js-sys/src/wire/import.rs @@ -1,8 +1,7 @@ -use crate::hazard::{IntoJS, ReturnAbi, ReturnFromJS, Slot}; +use crate::hazard::{IntoJS, ReturnAbi, ReturnFromJS}; use crate::util::PtrMut; use crate::wire::{ - InputSlot1, InputSlot2, InputSlot3, InputSlot4, OutputSlot1, OutputSlot2, OutputSlot3, - OutputSlot4, WireImportCatch, WireImportInputType, WireImportOutputType, wat_slot, + WireImportCatch, WireImportInputType, WireImportOutputType, from_js_slots, into_js_slots, }; trait MetadataFor: 'static { @@ -10,15 +9,7 @@ trait MetadataFor: 'static { } impl MetadataFor for WireImportInputType { - const VALUE: &'static Self = &Self::new( - [ - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - wat_slot::>( as Slot>::INTO_JS_WAT_CONV), - ], - T::JS_CONV, - ); + const VALUE: &'static Self = &Self::new(into_js_slots::(), T::JS_CONV); } impl MetadataFor for WireImportOutputType { @@ -27,12 +18,7 @@ impl MetadataFor for WireImportOutputType { ::MODE, T::JS_CONV, T::JS_SRET, - [ - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - wat_slot::>( as Slot>::FROM_JS_WAT_CONV), - ], + from_js_slots::(), ) }; } diff --git a/client/js-sys/src/wire/mod.rs b/client/js-sys/src/wire/mod.rs index 6ae2033a..787bdc51 100644 --- a/client/js-sys/src/wire/mod.rs +++ b/client/js-sys/src/wire/mod.rs @@ -2,6 +2,8 @@ mod export; mod import; mod r#macro; +use core::mem::MaybeUninit; + pub use export::*; pub use import::*; pub use js_bindgen_wire::abi::{JsCatch, JsEmbed, WatCatch}; @@ -21,13 +23,28 @@ use crate::hazard::{ pub(crate) const fn wat_slot(conversion: Option) -> Option { match S::WAT_TYPE { - Some(abi) => Some(WatSlot::new(abi, conversion)), + Some(rust) => Some(WatSlot::new(rust, conversion)), None => None, } } -use core::mem::MaybeUninit; +pub(crate) const fn into_js_slots() -> [Option; 4] { + [ + wat_slot::(::INTO_JS_WAT_CONV), + wat_slot::(::INTO_JS_WAT_CONV), + wat_slot::(::INTO_JS_WAT_CONV), + wat_slot::(::INTO_JS_WAT_CONV), + ] +} +pub(crate) const fn from_js_slots() -> [Option; 4] { + [ + wat_slot::(::FROM_JS_WAT_CONV), + wat_slot::(::FROM_JS_WAT_CONV), + wat_slot::(::FROM_JS_WAT_CONV), + wat_slot::(::FROM_JS_WAT_CONV), + ] +} // Rust `ABI` shims used by generated import and export functions. pub type InputSlot1 = <::Abi as WasmAbi>::Slot1; @@ -40,17 +57,8 @@ pub type FromJsSlot2 = <::Abi as WasmAbi>::Slot2; pub type FromJsSlot3 = <::Abi as WasmAbi>::Slot3; pub type FromJsSlot4 = <::Abi as WasmAbi>::Slot4; -pub type OutputSlot1 = <::Abi as WasmAbi>::Slot1; -pub type OutputSlot2 = <::Abi as WasmAbi>::Slot2; -pub type OutputSlot3 = <::Abi as WasmAbi>::Slot3; -pub type OutputSlot4 = <::Abi as WasmAbi>::Slot4; pub type OutputRet = MaybeUninit::Abi>>; -pub type ReturnSlot1 = <::Abi as WasmAbi>::Slot1; -pub type ReturnSlot2 = <::Abi as WasmAbi>::Slot2; -pub type ReturnSlot3 = <::Abi as WasmAbi>::Slot3; -pub type ReturnSlot4 = <::Abi as WasmAbi>::Slot4; - #[must_use] #[inline] pub fn split_input( diff --git a/client/js-sys/tests/hazard.rs b/client/js-sys/tests/hazard.rs index 8df62c97..f1d5bf3e 100644 --- a/client/js-sys/tests/hazard.rs +++ b/client/js-sys/tests/hazard.rs @@ -1,6 +1,8 @@ use js_bindgen_test::test; -use js_sys::hazard::{EmptySlot, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv, WatType}; -use js_sys::js_sys; +use js_sys::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, Slot, WasmAbi, WatConv, WatType, +}; +use js_sys::{Closure, closure, js_sys}; js_bindgen::embed_js!( module = "hazard", @@ -13,6 +15,17 @@ js_bindgen::embed_js!( "(value) => value.length === 4 &&", "value[0] === 1 && value[1] === 2 && value[2] === 3 && value[3] === 4", ); +js_bindgen::embed_js!( + module = "hazard", + name = "invoke_quad", + "(callback) => callback([1, 2, 3, 4])", +); +js_bindgen::embed_js!(module = "hazard", name = "identity", "value => value"); + +#[js_sys] +fn arg0(arg0: i32) -> i32 { + arg0 +} #[repr(transparent)] struct NumberSlot(u32); @@ -22,6 +35,8 @@ unsafe impl Slot for NumberSlot { const WAT_TYPE: Option = Some(WatType::I32); const INTO_JS_WAT_CONV: Option = Some(WatConv::new(&[], &[], "f64.convert_i32_u", WatType::F64)); + const FROM_JS_WAT_CONV: Option = + Some(WatConv::new(&[], &[], "i32.trunc_sat_f64_u", WatType::F64)); } struct Pair(u32, u32); @@ -99,6 +114,23 @@ unsafe impl IntoJS for Quad { } } +// SAFETY: The JavaScript conversion splits a four-element numeric array into +// the four `NumberSlot` carriers expected by `Quad`. +unsafe impl FromJS for Quad { + const JS_CONV: Option = Some( + FromJsConv::slot1("$value[0]") + .slot2("$value[1]") + .slot3("$value[2]") + .slot4("$value[3]"), + ); + + type Abi = Self; + + fn from_abi(raw: Self::Abi) -> Self { + raw + } +} + #[test] fn input_slot_conversions() { #[js_sys] @@ -113,3 +145,17 @@ fn input_slot_conversions() { assert!(pair(Pair(1, 2))); assert!(quad(Quad(1, 2, 3, 4))); } + +#[test] +fn from_js_slot_conversions() { + #[js_sys] + extern "js-sys" { + #[js_sys(js_embed = "invoke_quad")] + fn invoke_quad(callback: &Closure bool>) -> bool; + } + + let callback = closure!(dyn FnMut(Quad) -> bool, |Quad(a, b, c, d)| { + (a, b, c, d) == (1, 2, 3, 4) + }); + assert!(invoke_quad(&callback)); +} diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs index 858a68f8..ae965645 100644 --- a/client/web-sys/src/console.gen.rs +++ b/client/web-sys/src/console.gen.rs @@ -9,16 +9,16 @@ use js_sys::hazard::JsCast; pub fn log0() { unsafe extern "C" { #[link_name = "web_sys.console.log0"] - fn log0(); + fn __import_(); } - { unsafe { log0() } }; + { unsafe { __import_() } }; } pub fn log(data: &[T]) { unsafe extern "C" { #[link_name = "web_sys.console.log"] - fn log( + fn __import_( arg0_0: wire::InputSlot1<&[JsValue]>, arg0_1: wire::InputSlot2<&[JsValue]>, arg0_2: wire::InputSlot3<&[JsValue]>, @@ -28,14 +28,14 @@ pub fn log(data: &[T]) { { let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { wire::split_input_as::<&[JsValue]>(data) }; - unsafe { log(arg0_0, arg0_1, arg0_2, arg0_3) } + unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3) } }; } pub fn log2(data1: &JsValue, data2: &JsValue) { unsafe extern "C" { #[link_name = "web_sys.console.log2"] - fn log2( + fn __import_( arg0_0: wire::InputSlot1<&JsValue>, arg0_1: wire::InputSlot2<&JsValue>, arg0_2: wire::InputSlot3<&JsValue>, @@ -50,14 +50,14 @@ pub fn log2(data1: &JsValue, data2: &JsValue) { { let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data1); let (arg1_0, arg1_1, arg1_2, arg1_3) = wire::split_input::<&JsValue>(data2); - unsafe { log2(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } + unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } }; } pub fn error(data: &JsValue) { unsafe extern "C" { #[link_name = "web_sys.console.error"] - fn error( + fn __import_( arg0_0: wire::InputSlot1<&JsValue>, arg0_1: wire::InputSlot2<&JsValue>, arg0_2: wire::InputSlot3<&JsValue>, @@ -67,14 +67,14 @@ pub fn error(data: &JsValue) { { let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data); - unsafe { error(arg0_0, arg0_1, arg0_2, arg0_3) } + unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3) } }; } pub fn error1(data: &JsValue) -> u128 { unsafe extern "C" { #[link_name = "web_sys.console.error1"] - fn error1( + fn __import_( arg0_0: wire::InputSlot1<&JsValue>, arg0_1: wire::InputSlot2<&JsValue>, arg0_2: wire::InputSlot3<&JsValue>, @@ -84,7 +84,7 @@ pub fn error1(data: &JsValue) -> u128 { wire::join_output({ let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data); - unsafe { error1(arg0_0, arg0_1, arg0_2, arg0_3) } + unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3) } }) } const _: () = { @@ -94,7 +94,7 @@ const _: () = { &[wire::wire_import_output_type::()], wire::wire_import_catch(), ); - pub const WIRE: wire::Wire = wire::Wire::imports( + const _WIRE: wire::Wire = wire::Wire::imports( TABLE, &[ wire::WireImport::new( @@ -172,9 +172,9 @@ const _: () = { ), ], ); - pub const LEN: ::core::primitive::usize = wire::wire_blob_len(&WIRE); + const _LEN: ::core::primitive::usize = wire::wire_blob_len(&_WIRE); #[used] #[unsafe(link_section = "js_bindgen.wire")] - pub static WIRE_SECTION: wire::WireBlob = wire::WireBlob::new(&WIRE); + static _WIRE_SECTION: wire::WireBlob<_LEN> = wire::WireBlob::new(&_WIRE); }; diff --git a/host/Cargo.toml b/host/Cargo.toml index 086f5cd1..019490e3 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -57,7 +57,7 @@ itertools = { version = "0.15", default-features = false } js-bindgen-cli-lib = { path = "cli-lib" } js-bindgen-ld-shared = { path = "ld-shared" } js-bindgen-shared = { path = "shared" } -js-bindgen-wire = { version = "0.1.0", path = "wire" } +js-bindgen-wire = { path = "wire" } js-sys-bindgen = { path = "js-sys-bindgen" } memmap2 = "0.9" mime = "0.3" @@ -87,7 +87,7 @@ wasm-encoder = { version = "0.253", default-features = false, features = ["wasmp wasmparser = { version = "0.253", default-features = false } weedle2 = "5" windows-sys = "0.61" -xxhash-rust = { version = "0.8", features = ["xxh3"] } +xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } [workspace.lints.clippy] alloc_instead_of_core = "warn" diff --git a/host/js-sys-bindgen/Cargo.toml b/host/js-sys-bindgen/Cargo.toml index 797e3943..3765004d 100644 --- a/host/js-sys-bindgen/Cargo.toml +++ b/host/js-sys-bindgen/Cargo.toml @@ -26,17 +26,8 @@ weedle2 = { workspace = true, optional = true } xxhash-rust = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } -cargo_metadata = { workspace = true } -foldhash = { workspace = true } -hashbrown = { workspace = true } -indoc = { workspace = true } inline-snap = { workspace = true } -itertools = { workspace = true, features = ["use_alloc"] } -js-bindgen-ld-shared = { workspace = true } prettyplease = { workspace = true } -tempfile = { workspace = true } -wasmparser = { workspace = true } [features] file = ["dep:foldhash", "dep:hashbrown"] diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index e583ef3e..c7ccd983 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -12,8 +12,9 @@ use syn::{ use xxhash_rust::xxh3::xxh3_128; use crate::export::{ExportAbi, lower_abi}; +use crate::function; use crate::hygiene::Hygiene; -use crate::{function, r#macro::render_import_groups}; +use crate::r#macro::render_import_groups; mod keyword { syn::custom_keyword!(js_sys); @@ -172,23 +173,22 @@ pub(crate) fn closure_with( ) } - #[expect(dead_code, reason = "stored in a custom section")] - pub const WIRE: #js_sys::wire::Wire = - #js_sys::wire::Wire::exports(&[ - #js_sys::wire::wire_closure_export::( - #crate_name, - #call_name, - &[#(#wire_inputs),*], - #wire_output, - ), - ]); - #[expect(dead_code, reason = "stored in a custom section")] - pub const LEN: ::core::primitive::usize = #js_sys::wire::wire_blob_len(&WIRE); + const _: () = { + const _WIRE: #js_sys::wire::Wire = + #js_sys::wire::Wire::exports(&[ + #js_sys::wire::wire_closure_export::( + #crate_name, + #call_name, + &[#(#wire_inputs),*], + #wire_output, + ), + ]); + const _LEN: ::core::primitive::usize = #js_sys::wire::wire_blob_len(&_WIRE); - #[expect(dead_code, reason = "stored in a custom section")] - #[unsafe(link_section = "js_bindgen.wire")] - pub static WIRE_SECTION: #js_sys::wire::WireBlob = - #js_sys::wire::WireBlob::new(&WIRE); + #[unsafe(link_section = "js_bindgen.wire")] + static _WIRE_SECTION: #js_sys::wire::WireBlob<_LEN> = + #js_sys::wire::WireBlob::new(&_WIRE); + }; #js_sys::js_bindgen::embed_js! { module = #crate_name, diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index c3b59e39..e4cb8805 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -1,6 +1,6 @@ use std::env; -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote_spanned}; use syn::ext::IdentExt; use syn::parse::Parser; @@ -117,14 +117,15 @@ pub(crate) fn r#macro( const _: () = { #[unsafe(export_name = #raw_export_name)] - extern "C" fn export_raw( + extern "C" fn __export_( #(#raw_inputs),* ) #raw_output { #raw_body } + }; - #[expect(dead_code, reason = "stored in a custom section")] - pub const WIRE: #macro_path::Wire = + const _: () = { + const _WIRE: #macro_path::Wire = #macro_path::Wire::exports(&[ #macro_path::WireExport::#descriptor_constructor( #crate_name, @@ -134,13 +135,11 @@ pub(crate) fn r#macro( #wire_output, ), ]); - #[expect(dead_code, reason = "stored in a custom section")] - pub const LEN: ::core::primitive::usize = #macro_path::wire_blob_len(&WIRE); + const _LEN: ::core::primitive::usize = #macro_path::wire_blob_len(&_WIRE); - #[expect(dead_code, reason = "stored in a custom section")] #[unsafe(link_section = "js_bindgen.wire")] - pub static WIRE_SECTION: #macro_path::WireBlob = - #macro_path::WireBlob::new(&WIRE); + static _WIRE_SECTION: #macro_path::WireBlob<_LEN> = + #macro_path::WireBlob::new(&_WIRE); }; }) } @@ -168,7 +167,7 @@ pub(crate) fn lower_abi<'a>( for (index, ty) in inputs.into_iter().enumerate() { let span = ty.span(); - let argument = format_ident!("arg{index}", span = span); + let argument = format_ident!("arg{index}", span = Span::mixed_site()); let parameter = LitStr::new(&argument.to_string(), span); let reference = match ty { Type::Reference(reference) if reference.mutability.is_some() => { @@ -192,7 +191,7 @@ pub(crate) fn lower_abi<'a>( let mut slots = Vec::new(); for slot in 1_usize..=4 { - let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = span); + let slot_ident = format_ident!("arg{index}_{}", slot - 1, span = Span::mixed_site()); let slot_alias = format_ident!("FromJsSlot{slot}", span = span); let raw_type = quote_spanned!(span=> #js_sys::wire::#slot_alias<#js_ty>); @@ -202,7 +201,7 @@ pub(crate) fn lower_abi<'a>( } if let Some(reference) = reference { - let anchor = format_ident!("arg{index}_anchor", span = span); + let anchor = format_ident!("arg{index}_anchor", span = Span::mixed_site()); let ty = &reference.elem; join_inputs.push(quote_spanned! {span=> diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index ceba9b07..d461c6a2 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -25,22 +25,26 @@ pub(crate) struct FunctionImport { pub(crate) cfg_attrs: Vec, pub(crate) module: LitStr, pub(crate) name: LitStr, - pub(crate) input_names: Vec, - pub(crate) input_types: Vec, + pub(crate) inputs: Vec, pub(crate) output_type: Option, pub(crate) binding: Option, pub(crate) suspending: bool, - pub(crate) kind: FunctionImportKind, + pub(crate) shim_kind: ImportShimKind, pub(crate) macro_path: Path, } +pub(crate) struct FunctionImportInput { + pub(crate) name: LitStr, + pub(crate) ty: Type, +} + #[derive(Clone, Copy)] -pub(crate) enum FunctionImportKind { +pub(crate) enum ImportShimKind { Normal, ClosureFactory, } -/// JavaScript binding data shared by the flat and direct Wire emitters. +/// JavaScript call data stored in an import Wire record. pub(crate) struct FunctionBinding { pub(crate) direct: Option, pub(crate) call: String, @@ -147,16 +151,10 @@ pub(crate) fn expand( let import_name = plan.binding.import_name(namespace, &sig.ident); let link_name = format!("{crate_}.{import_name}"); let macro_path = hygiene.r#macro(&cfg_attrs, span); - let import = plan.import_descriptor(¯o_path, crate_, &import_name, &cfg_attrs, span); - let FunctionPlan { - inputs, - output_ty, - output_abi_override, - impl_generic_params, - binding, - .. - } = plan; - let ident = &sig.ident; + let inputs = &plan.inputs; + let output_ty = &plan.output_ty; + let output_abi_override = &plan.output_abi_override; + let foreign_ident = Ident::new("__import_", Span::mixed_site()); let split_inputs = inputs.iter().map(|input| { let InputArg { abi_type, @@ -199,7 +197,7 @@ pub(crate) fn expand( let foreign_call = quote_spanned! {span=> { #(#split_inputs)* - unsafe { #ident(#(#foreign_input_names),*) } + unsafe { #foreign_ident(#(#foreign_input_names),*) } }}; let foreign_call = if let Some(output_abi_ty) = output_abi_override.as_ref() { let output_ty = output_ty.as_ref().expect("validated during parsing"); @@ -219,14 +217,15 @@ pub(crate) fn expand( #vis #sig { unsafe extern "C" { #[link_name = #link_name] - fn #ident(#(#foreign_input_names: #foreign_input_tys),*) #foreign_output; + fn #foreign_ident(#(#foreign_input_names: #foreign_input_tys),*) #foreign_output; } #foreign_call } }; - let item = if let Some(owner) = binding.owner() { + let item = if let Some(owner) = plan.binding.owner() { + let impl_generic_params = &plan.impl_generic_params; quote_spanned! {span=> impl #impl_generic_params #owner { #item_fn @@ -235,6 +234,7 @@ pub(crate) fn expand( } else { item_fn }; + let import = plan.into_import_descriptor(macro_path, crate_, &import_name, cfg_attrs, span); Ok((item, import)) } @@ -245,7 +245,7 @@ pub(crate) fn expand_closure_factory( item: ForeignItemFn, ) -> Result<(TokenStream, FunctionImport)> { let (function, mut import) = expand(hygiene, None, crate_, &HashMap::new(), item)?; - import.kind = FunctionImportKind::ClosureFactory; + import.shim_kind = ImportShimKind::ClosureFactory; Ok((function, import)) } @@ -644,12 +644,12 @@ impl FunctionPlan { } } - fn import_descriptor( - &self, - macro_path: &Path, + fn into_import_descriptor( + self, + macro_path: Path, crate_: &str, import_name: &str, - cfg_attrs: &[Attribute], + cfg_attrs: Vec, span: Span, ) -> FunctionImport { let Self { @@ -660,55 +660,51 @@ impl FunctionPlan { suspending, .. } = self; - let output_abi_ty = output_abi_override.as_ref().or(output_ty.as_ref()); - let input_tys: Vec<_> = inputs.iter().map(|input| &input.abi_type).collect(); + let output_type = output_abi_override.or(output_ty); let mut required_embeds = Vec::new(); - if let ForeignItem::Embed(name) = binding { + if let ForeignItem::Embed(name) = &binding { required_embeds.push(quote_spanned!(span=> #macro_path::JsEmbed::new(#crate_, #name) )); } let binding = match binding { - ForeignItem::Generate { - direct_wrapper, - direct_call, - indirect_call, - .. - } => Some(FunctionBinding { - direct: (!direct_wrapper).then(|| direct_call.clone()), - call: indirect_call.clone(), + ForeignItem::Generate { direct, call, .. } => Some(FunctionBinding { + direct, + call, required_embeds, }), ForeignItem::Embed(name) => { let path = format!("this.#jsEmbed.{crate_}['{name}']"); - let arguments = join_input_slots(inputs); - let indirect_call = format!("{path}({arguments})"); + let arguments = join_input_slots(&inputs); + let call = format!("{path}({arguments})"); Some(FunctionBinding { direct: Some(path), - call: indirect_call, + call, required_embeds, }) } ForeignItem::Import => None, }; FunctionImport { - cfg_attrs: cfg_attrs.to_vec(), + cfg_attrs, module: LitStr::new(crate_, span), name: LitStr::new(import_name, span), - input_names: inputs - .iter() - .map(|input| input.descriptor_name.clone()) + inputs: inputs + .into_iter() + .map(|input| FunctionImportInput { + name: input.descriptor_name, + ty: input.abi_type, + }) .collect(), - input_types: input_tys.into_iter().cloned().collect(), - output_type: output_abi_ty.cloned(), + output_type, binding, - suspending: *suspending, - kind: FunctionImportKind::Normal, - macro_path: macro_path.clone(), + suspending, + shim_kind: ImportShimKind::Normal, + macro_path, } } } diff --git a/host/js-sys-bindgen/src/function/js.rs b/host/js-sys-bindgen/src/function/js.rs index 5712ba1c..8c267df0 100644 --- a/host/js-sys-bindgen/src/function/js.rs +++ b/host/js-sys-bindgen/src/function/js.rs @@ -6,14 +6,11 @@ pub(super) enum ForeignItem { /// Rust type receiving the generated method, if this is not a free /// function. owner: Option, - /// Whether the direct conversion path must wrap `direct_call` in a - /// function. - direct_wrapper: bool, - /// Function reference or expression used by the direct conversion path. - direct_call: String, - /// Expression used when argument or result conversion requires a + /// Function reference usable without a wrapper. + direct: Option, + /// Call expression used when argument or result conversion requires a /// wrapper. - indirect_call: String, + call: String, }, Embed(String), Import, @@ -51,23 +48,17 @@ impl ForeignItem { inputs: &[String], ) -> Self { let arguments = Self::arguments(inputs, receiver, variadic); - let indirect_call = format!("{path}({arguments})"); + let call = format!("{path}({arguments})"); // Only a bare global function can be passed directly. Calls through a // `namespace`, instance, or static member need a wrapper to preserve their // receiver; `variadic` calls need one to emit the spread expression. - let direct_wrapper = namespace.is_some() || owner.is_some() || variadic; - let direct_call = if direct_wrapper { - indirect_call.clone() - } else { - path.to_owned() - }; + let direct = (namespace.is_none() && owner.is_none() && !variadic).then(|| path.to_owned()); Self::Generate { owner, - direct_wrapper, - direct_call, - indirect_call, + direct, + call, } } @@ -77,9 +68,8 @@ impl ForeignItem { Self::Generate { owner: Some(owner), - direct_wrapper: true, - direct_call: call.clone(), - indirect_call: call, + direct: None, + call, } } @@ -135,9 +125,8 @@ impl ForeignItem { fn expression(owner: Option, expression: String) -> Self { Self::Generate { owner, - direct_wrapper: true, - direct_call: expression.clone(), - indirect_call: expression, + direct: None, + call: expression, } } diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index 255d7c60..be8f7608 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -271,9 +271,9 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream let mut input_types = Vec::::new(); let mut output_types = Vec::::new(); for import in &imports { - for ty in &import.input_types { - if !input_types.contains(ty) { - input_types.push(ty.clone()); + for input in &import.inputs { + if !input_types.contains(&input.ty) { + input_types.push(input.ty.clone()); } } if let Some(ty) = &import.output_type @@ -286,26 +286,26 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream for import in &imports { let module = &import.module; let name = &import.name; - let input_names = &import.input_names; let suspending = import.suspending; - let constructor = match import.kind { - crate::function::FunctionImportKind::Normal => { + let constructor = match import.shim_kind { + crate::function::ImportShimKind::Normal => { quote::quote!(#macro_path::WireImport::new) } - crate::function::FunctionImportKind::ClosureFactory => { + crate::function::ImportShimKind::ClosureFactory => { quote::quote!(#macro_path::WireImport::closure_factory) } }; - let input_indices: Vec<_> = import - .input_types - .iter() - .map(|ty| { - input_types - .iter() - .position(|candidate| candidate == ty) - .expect("every input type was collected") - }) - .collect(); + let wire_inputs = import.inputs.iter().map(|input| { + let name = &input.name; + let index = input_types + .iter() + .position(|candidate| candidate == &input.ty) + .expect("every input type was collected"); + + quote::quote!( + #macro_path::WireImportInput::new(#name, #index) + ) + }); let output_index = if let Some(ty) = &import.output_type { let index = output_types .iter() @@ -317,11 +317,6 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream } else { quote::quote!(::core::option::Option::None) }; - let wire_inputs = input_names.iter().zip(input_indices).map(|(name, index)| { - quote::quote!( - #macro_path::WireImportInput::new(#name, #index) - ) - }); let binding = if let Some(binding) = &import.binding { let direct = if let Some(direct) = &binding.direct { let direct = LitStr::new(direct, module.span()); @@ -371,15 +366,15 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream &[#(#output_type_descriptors),*], #macro_path::wire_import_catch(), ); - pub const WIRE: #macro_path::Wire = + const _WIRE: #macro_path::Wire = #macro_path::Wire::imports(TABLE, &[#(#wire_descriptors),*]); - pub const LEN: ::core::primitive::usize = - #macro_path::wire_blob_len(&WIRE); + const _LEN: ::core::primitive::usize = + #macro_path::wire_blob_len(&_WIRE); #[used] #[unsafe(link_section = "js_bindgen.wire")] - pub static WIRE_SECTION: #macro_path::WireBlob = - #macro_path::WireBlob::new(&WIRE); + static _WIRE_SECTION: #macro_path::WireBlob<_LEN> = + #macro_path::WireBlob::new(&_WIRE); }; }); output diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index 5b23d9cf..be2e4038 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -36,8 +36,8 @@ fn binding_options() { ); assert!(output.contains("renamed::wire::InputSlot1")); - assert!(output.contains("globalThis.console.log(arg0_0)")); - assert!(output.contains("globalThis.console.warn(arg0_0)")); + super::assert_javascript(&output, "globalThis.console.log(arg0_0)"); + super::assert_javascript(&output, "globalThis.console.warn(arg0_0)"); assert!(output.contains("join_output_as::")); assert!(output.contains("#[cfg(all())]")); @@ -53,8 +53,8 @@ fn binding_options() { } }, ); - assert!(output.contains("test_crate.imported")); - assert!(output.contains("this.#jsEmbed.test_crate['embed']")); + super::assert_javascript(&output, "test_crate.imported"); + super::assert_javascript(&output, "this.#jsEmbed.test_crate['embed']"); } #[test] diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index e670520d..18d6d455 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -59,7 +59,7 @@ fn member_operations() { "delete arg0_0[arg1_0]", "arg0_0.push(arg1_0, ...arg2_0)", ] { - assert!(output.contains(operation)); + super::assert_javascript(&output, operation); } } diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index b5349369..7d24488d 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -12,3 +12,11 @@ fn macro_error(input: syn::ItemForeignMod) -> String { error.to_string() } + +fn assert_javascript(output: &str, expected: &str) { + let literal = format!("{expected:?}"); + assert!( + output.contains(&literal), + "generated output does not contain the complete JavaScript expression {literal}" + ); +} diff --git a/host/ld-shared/src/lib.rs b/host/ld-shared/src/lib.rs index a23f97e8..c6af44c0 100644 --- a/host/ld-shared/src/lib.rs +++ b/host/ld-shared/src/lib.rs @@ -11,7 +11,6 @@ use wasmparser::CustomSectionReader; pub const WAT_SECTION: &str = "js_bindgen.wat"; pub const IMPORT_SECTION: &str = "js_bindgen.import"; -pub const WIRE_SECTION: &str = "js_bindgen.wire"; /// Creates a relocatable Wasm object from the WAT input. pub fn wat_to_object(wasm64: bool, wat: &str) -> rwat::Result> { @@ -147,34 +146,6 @@ impl<'cs> Iterator for JsBindgenWatSectionParser<'cs> { } } -#[derive(Clone)] -pub struct JsBindgenWireSectionParser<'cs>(CustomSectionParser<'cs>); - -impl<'cs> JsBindgenWireSectionParser<'cs> { - #[must_use] - pub fn new(custom_section: &CustomSectionReader<'cs>) -> Self { - Self(CustomSectionParser::new(custom_section)) - } -} - -impl Debug for JsBindgenWireSectionParser<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let rest: Vec<_> = self.clone().collect(); - - f.debug_tuple("JsBindgenWireSectionParser") - .field(&rest.as_slice()) - .finish() - } -} - -impl<'cs> Iterator for JsBindgenWireSectionParser<'cs> { - type Item = &'cs [u8]; - - fn next(&mut self) -> Option { - self.0.next() - } -} - #[derive(Clone)] pub struct JsBindgenJsSectionParser<'cs>(CustomSectionParser<'cs>); diff --git a/host/ld/src/js.rs b/host/ld/src/js.rs index 8e70d969..a31c2c07 100644 --- a/host/ld/src/js.rs +++ b/host/ld/src/js.rs @@ -65,8 +65,8 @@ impl JsStore { if let Some(previous) = definitions.get(name) { if previous != &binding { bail!( - "found multiple JS imports for `{module}:{name}`\n\tJS Import 1:\n{previous:?}\n\tJS Import \ - 2:\n{binding:?}", + "found multiple JS imports for `{module}:{name}`\n\tJS Import \ + 1:\n{previous:?}\n\tJS Import 2:\n{binding:?}", ); } } else { @@ -112,8 +112,8 @@ impl JsStore { if let Some(previous) = self.export.get(name) { bail!( - "found multiple JS exports named `{name}` from `{}` and `{}`\n\tJS Export 1:\n{:?}\n\tJS Export \ - 2:\n{:?}", + "found multiple JS exports named `{name}` from `{}` and `{}`\n\tJS Export \ + 1:\n{:?}\n\tJS Export 2:\n{:?}", previous.module, definition.module, previous.binding, @@ -156,7 +156,8 @@ impl JsStore { return Ok(false); } bail!( - "found incompatible closure exports named `{name}` from `{}` and `{}`\n\tClosure 1:\n{:?}\n\tClosure 2:\n{:?}", + "found incompatible closure exports named `{name}` from `{}` and `{}`\n\tClosure \ + 1:\n{:?}\n\tClosure 2:\n{:?}", previous.module, definition.module, previous, diff --git a/host/ld/src/post.rs b/host/ld/src/post.rs index 87fee72d..2890e94f 100644 --- a/host/ld/src/post.rs +++ b/host/ld/src/post.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result, bail}; use js_bindgen_cli_lib::{JS_OUTPUT_SECTION, MainMemory}; -use js_bindgen_ld_shared::{IMPORT_SECTION, WAT_SECTION, WIRE_SECTION}; +use js_bindgen_ld_shared::{IMPORT_SECTION, WAT_SECTION}; use js_bindgen_shared::{IS_COMPAT_SECTION, IS_TEST_SECTION}; +use js_bindgen_wire::WIRE_SECTION; use wasm_encoder::{ CustomSection, EntityType, ExportSection, ImportSection, Module, ProducersField, ProducersSection, RawSection, Section, diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index a25444ae..6cfafef2 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -3,13 +3,11 @@ use std::fs; use std::path::Path; use std::time::SystemTime; -use anyhow::Result; +use anyhow::{Context, Result}; use js_bindgen_cli_lib::MainMemory; -use js_bindgen_ld_shared::{ - IMPORT_SECTION, JsBindgenWatSectionParser, JsBindgenWireSectionParser, WAT_SECTION, - WIRE_SECTION, -}; +use js_bindgen_ld_shared::{IMPORT_SECTION, JsBindgenWatSectionParser, WAT_SECTION}; use js_bindgen_shared::ReadFile; +use js_bindgen_wire::{WIRE_SECTION, WireRecords}; use wasmparser::{Parser, Payload}; use crate::args::Arguments; @@ -192,8 +190,15 @@ fn process_object( } } Payload::CustomSection(c) if c.name() == WIRE_SECTION => { - for blob in JsBindgenWireSectionParser::new(c) { - match wire::decode_and_render(blob)? { + for (index, blob) in WireRecords::new(c.data()).enumerate() { + let context = || { + format!( + "invalid wire record {index} in `{}`", + archive_path.display(), + ) + }; + let blob = blob.with_context(context)?; + match wire::decode_and_render(blob).with_context(context)? { RenderedRecord::Imports(rendered) => { for import in rendered.bindings { js_store.add_js_import( diff --git a/host/ld/src/wire/export/js.rs b/host/ld/src/wire/export/js.rs index 24839524..d6c48284 100644 --- a/host/ld/src/wire/export/js.rs +++ b/host/ld/src/wire/export/js.rs @@ -1,7 +1,7 @@ use js_bindgen_wire::model::{Export, ExportInput, ExportOutput}; use crate::wire::JsBinding; -use crate::wire::js::{Placeholder, render_template}; +use crate::wire::js::{Placeholder, quote_string, render_template}; /// Renders one decoded Rust export or closure dispatcher. pub(super) fn render<'a>(export: &Export<'a>) -> JsBinding<'a> { @@ -14,7 +14,7 @@ pub(super) fn render<'a>(export: &Export<'a>) -> JsBinding<'a> { } fn render_export(export: &Export<'_>) -> String { - let function = format!("wasmExports['{}']", export.name); + let function = format!("wasmExports[{}]", quote_string(export.name)); let callable = if export.promising { format!("WebAssembly.promising({function})") } else { @@ -73,11 +73,13 @@ fn render_promising( if prepares.is_empty() { format!( - "(() => {{\n const $promising = {callable}\n return ({parameters}) => $promising({arguments}){then}\n}})()" + "(() => {{\n const $promising = {callable}\n return ({parameters}) => \ + $promising({arguments}){then}\n}})()" ) } else { format!( - "(() => {{\n const $promising = {callable}\n return ({parameters}) => {{\n{prepares} return $promising({arguments}){then}\n }}\n}})()" + "(() => {{\n const $promising = {callable}\n return ({parameters}) => \ + {{\n{prepares} return $promising({arguments}){then}\n }}\n}})()" ) } } @@ -192,3 +194,31 @@ fn render_output_slot(output: &ExportOutput<'_>, slot: usize) -> String { fn render_ret(index: usize) -> String { format!("ret[{index}]") } + +#[cfg(test)] +mod tests { + use js_bindgen_wire::PointerWidth; + use js_bindgen_wire::model::{Callee, Export}; + + use super::render_export; + + #[test] + fn quotes_export_names() { + const NAME: &str = "single' double\" slash\\ line\n雪"; + let export = Export { + module: "test", + name: NAME, + pointer_width: PointerWidth::Wasm32, + inputs: Vec::new(), + output: None, + embeds: Vec::new(), + promising: false, + callee: Callee::Symbol { name: "test" }, + }; + + assert_eq!( + render_export(&export), + "wasmExports[\"single' double\\\" slash\\\\ line\\n雪\"]", + ); + } +} diff --git a/host/ld/src/wire/export/wat.rs b/host/ld/src/wire/export/wat.rs index 34e73c5f..ef9bb0b4 100644 --- a/host/ld/src/wire/export/wat.rs +++ b/host/ld/src/wire/export/wat.rs @@ -1,9 +1,10 @@ use std::fmt::Write; -use crate::wire::wat::{WatImports, WatLocals, write_conversion}; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{Callee, Export, ExportInput, ExportInputKind, ExportOutput, Slot}; +use crate::wire::wat::{WatImports, WatLocals, quote_string, write_conversion}; + struct ExportRenderer<'export, 'wire> { index: usize, export: &'export Export<'wire>, @@ -46,9 +47,8 @@ pub(super) fn render(exports: &[Export<'_>]) -> Option { imports.insert( "js_sys.closure.table", format!( - "(import \"env\" \"__indirect_function_table\" (table \ - $js_sys.closure.table (@sym (name \"__indirect_function_table\")) \ - {pointer_type} 0 funcref))", + "(import \"env\" \"__indirect_function_table\" (table $js_sys.closure.table (@sym \ + (name \"__indirect_function_table\")) {pointer_type} 0 funcref))", ), ); } @@ -62,8 +62,8 @@ pub(super) fn render(exports: &[Export<'_>]) -> Option { imports.insert( "__stack_pointer", format!( - "(import \"env\" \"__stack_pointer\" \ - (global $__stack_pointer (mut {pointer_type})))", + "(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut \ + {pointer_type})))", ), ); } @@ -130,18 +130,19 @@ impl ExportRenderer<'_, '_> { } parameters.push_str(" (param"); for slot in &input.slots { - write!(parameters, " {}", slot.abi).expect("writing to a String cannot fail"); + write!(parameters, " {}", slot.rust).expect("writing to a String cannot fail"); } parameters.push(')'); } let result = match self.export.output.as_ref() { - Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.abi), + Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.rust), _ => String::new(), }; format!( - "(import \"env\" \"symbol\" (func ${identifier} \ - (@sym (name \"{symbol}\")){retptr}{parameters}{result}))", + "(import \"env\" \"symbol\" (func ${identifier} (@sym (name \ + {})){retptr}{parameters}{result}))", + quote_string(symbol), ) } @@ -158,18 +159,17 @@ impl ExportRenderer<'_, '_> { } parameters.push_str(" (param"); for slot in &input.slots { - write!(parameters, " {}", slot.abi).expect("writing to a String cannot fail"); + write!(parameters, " {}", slot.rust).expect("writing to a String cannot fail"); } parameters.push(')'); } let result = match self.export.output.as_ref() { - Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.abi), + Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.rust), _ => String::new(), }; format!( - "(type $js_sys.closure.call.{} (func{retptr} \ - (param {}){parameters}{result}))", + "(type $js_sys.closure.call.{} (func{retptr} (param {}){parameters}{result}))", self.index, self.pointer_type(), ) @@ -182,11 +182,11 @@ impl ExportRenderer<'_, '_> { .iter() .flat_map(|input| { input.slots.iter().enumerate().map(|(slot_index, slot)| { - let boundary = slot.boundary(); + let js = slot.js(); if input.kind == ExportInputKind::ClosureData { - format!(" (param $data {boundary})") + format!(" (param $data {js})") } else { - format!(" (param ${}_{slot_index} {boundary})", input.name) + format!(" (param ${}_{slot_index} {js})", input.name) } }) }) @@ -196,12 +196,12 @@ impl ExportRenderer<'_, '_> { .output .as_ref() .map_or_else(String::new, |output| match output { - ExportOutput::Direct { slot, .. } => format!(" (result {})", slot.boundary()), + ExportOutput::Direct { slot, .. } => format!(" (result {})", slot.js()), ExportOutput::Indirect { frame, .. } => { let types = frame .slots .iter() - .map(|frame_slot| frame_slot.slot.boundary().as_str()) + .map(|frame_slot| frame_slot.slot.js().as_str()) .collect::>() .join(" "); if types.is_empty() { @@ -213,8 +213,9 @@ impl ExportRenderer<'_, '_> { }); let mut wat = format!( - "(func $js_sys.export.{} (@sym (name \"{}\")){parameters}{result}", - self.index, self.export.name, + "(func $js_sys.export.{} (@sym (name {})){parameters}{result}", + self.index, + quote_string(self.export.name), ); self.write_prologue(&mut wat); self.write_call(&mut wat); @@ -259,8 +260,8 @@ impl ExportRenderer<'_, '_> { if let Some(ExportOutput::Indirect { frame, .. }) = self.export.output.as_ref() { write!( wat, - "\n global.get $__stack_pointer\n {}.const {}\n \ - {}.sub\n local.tee $retptr\n global.set $__stack_pointer", + "\n global.get $__stack_pointer\n {}.const {}\n {}.sub\n local.tee $retptr\n \ + global.set $__stack_pointer", self.pointer_type(), frame.size, self.pointer_type(), @@ -297,10 +298,8 @@ impl ExportRenderer<'_, '_> { } write!( wat, - "\n local.get $js_sys.closure.data\n \ - {}.load offset={call_shim_offset}\n \ - call_indirect $js_sys.closure.table \ - (type $js_sys.closure.call.{}) (@reloc)", + "\n local.get $js_sys.closure.data\n {}.load offset={call_shim_offset}\n \ + call_indirect $js_sys.closure.table (type $js_sys.closure.call.{}) (@reloc)", self.pointer_type(), self.index, ) @@ -322,7 +321,7 @@ impl ExportRenderer<'_, '_> { write!( wat, "\n local.get $retptr\n {}.load offset={}", - frame_slot.slot.abi, frame_slot.offset, + frame_slot.slot.rust, frame_slot.offset, ) .expect("writing to a String cannot fail"); if let Some(instruction) = frame_slot.slot.instruction() { @@ -331,8 +330,8 @@ impl ExportRenderer<'_, '_> { } write!( wat, - "\n local.get $retptr\n {}.const {}\n \ - {}.add\n global.set $__stack_pointer", + "\n local.get $retptr\n {}.const {}\n {}.add\n global.set \ + $__stack_pointer", self.pointer_type(), frame.size, self.pointer_type(), @@ -374,3 +373,89 @@ fn write_abi_arguments(wat: &mut String, input: &ExportInput<'_>) { } } } + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use js_bindgen_wire::PointerWidth; + use js_bindgen_wire::abi::WatType; + use js_bindgen_wire::model::{ + Callee, Export, ExportInput, ExportInputKind, ExportOutput, Slot, WatConversion, + }; + + use super::render; + + #[test] + fn arbitrary_names_produce_valid_wat() { + const NAME: &str = "single' double\" slash\\ line\n雪"; + let export = Export { + module: NAME, + name: NAME, + pointer_width: PointerWidth::Wasm32, + inputs: Vec::new(), + output: None, + embeds: Vec::new(), + promising: false, + callee: Callee::Symbol { name: NAME }, + }; + + let wat = render(core::slice::from_ref(&export)).expect("one export produces WAT"); + js_bindgen_ld_shared::wat_to_object(false, &wat).expect("escaped WAT should parse"); + } + + #[test] + fn converted_slots_use_js_types_for_shim_and_rust_types_for_symbol() { + let input = Slot { + rust: WatType::I32, + wat: Some(WatConversion { + js: WatType::ExternRef, + imports: Rc::from([]), + locals: Rc::from([]), + instruction: "drop\n i32.const 0", + }), + }; + let output = Slot { + rust: WatType::I32, + wat: Some(WatConversion { + js: WatType::ExternRef, + imports: Rc::from([]), + locals: Rc::from([]), + instruction: "drop\n ref.null extern", + }), + }; + let export = Export { + module: "test", + name: "converted", + pointer_width: PointerWidth::Wasm32, + inputs: vec![ExportInput { + kind: ExportInputKind::Value, + name: "value", + slots: vec![input], + conversion: None, + }], + output: Some(ExportOutput::Direct { + slot: output, + js_conversion: None, + }), + embeds: Vec::new(), + promising: false, + callee: Callee::Symbol { name: "test.raw" }, + }; + + let wat = render(core::slice::from_ref(&export)).expect("one export produces WAT"); + assert_eq!( + wat, + r#"(import "env" "symbol" (func $js_sys.export.symbol.0 (@sym (name "test.raw")) (param i32) (result i32))) +(func $js_sys.export.0 (@sym (name "converted")) (param $value_0 externref) (result externref) + local.get $value_0 + drop + i32.const 0 + call $js_sys.export.symbol.0 (@reloc) + drop + ref.null extern +)"#, + ); + js_bindgen_ld_shared::wat_to_object(false, &wat).expect("rendered WAT should parse"); + } +} diff --git a/host/ld/src/wire/import/js.rs b/host/ld/src/wire/import/js.rs index 49f2af95..0b4d43a7 100644 --- a/host/ld/src/wire/import/js.rs +++ b/host/ld/src/wire/import/js.rs @@ -5,10 +5,8 @@ use js_bindgen_wire::model::{ ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, }; -use crate::wire::{ - JsBinding, - js::{Placeholder, render_template}, -}; +use crate::wire::JsBinding; +use crate::wire::js::{Placeholder, render_template}; /// Renders every import which has a generated JavaScript binding. pub(super) fn render<'a>(group: &ImportGroup<'a>) -> Vec> { diff --git a/host/ld/src/wire/import/wat.rs b/host/ld/src/wire/import/wat.rs index 908d9e99..0d570a5e 100644 --- a/host/ld/src/wire/import/wat.rs +++ b/host/ld/src/wire/import/wat.rs @@ -1,13 +1,14 @@ +use std::collections::HashMap; use std::fmt::Write; -use js_bindgen_wire::WireImportKind; +use js_bindgen_wire::ImportShimKind; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{ Import, ImportCatch, ImportErrorMode, ImportGroup, ImportOutput, ImportOutputAbi, Slot, WatCatch, }; -use crate::wire::wat::{WatImports, WatLocals, write_conversion}; +use crate::wire::wat::{WatImports, WatLocals, quote_string, write_conversion}; /// Renders the imported functions followed by their Rust `ABI` shims. pub(super) fn render(group: &ImportGroup<'_>) -> Option { @@ -20,8 +21,12 @@ pub(super) fn render(group: &ImportGroup<'_>) -> Option { Some(ImportCatch::JavaScript(_)) | None => None, }; let mut imports = WatImports::default(); + let mut boundaries = HashMap::new(); let mut shims = Vec::with_capacity(group.imports.len()); - for import in &group.imports { + for (index, import) in group.imports.iter().enumerate() { + let boundary_index = *boundaries + .entry((import.module, import.name)) + .or_insert(index); let catches = import .output .as_ref() @@ -29,7 +34,7 @@ pub(super) fn render(group: &ImportGroup<'_>) -> Option { let catch = catches.then(|| { group_catch.expect("a Wasm-catching Result import has no Wasm catch metadata") }); - render_wat_import(&mut imports, import); + render_wat_import(&mut imports, boundary_index, import); let mut locals = WatLocals::default(); for slot in conversion_slots(import) { @@ -41,6 +46,8 @@ pub(super) fn render(group: &ImportGroup<'_>) -> Option { locals.extend(&catch.locals); } shims.push(Shim { + index, + boundary_index, import, catch, locals, @@ -56,6 +63,8 @@ pub(super) fn render(group: &ImportGroup<'_>) -> Option { } struct Shim<'group, 'wire> { + index: usize, + boundary_index: usize, import: &'group Import<'wire>, catch: Option<&'group WatCatch<'wire>>, locals: WatLocals<'wire>, @@ -67,30 +76,32 @@ struct Shim<'group, 'wire> { // ```wat // ;; fn notify(value: u32) // (import "js_sys" "notify" -// (func $js_sys.import.notify +// (func $js_sys.import.boundary.0 // (@sym (name "js_sys.import.notify")) // (param i32))) // // ;; fn identity(value: u32) -> u32 // (import "js_sys" "identity" -// (func $js_sys.import.identity +// (func $js_sys.import.boundary.1 // (@sym (name "js_sys.import.identity")) // (param i32) // (result i32))) // // ;; fn wide(value: u128) -> u128 // (import "js_sys" "wide" -// (func $js_sys.import.wide +// (func $js_sys.import.boundary.2 // (@sym (name "js_sys.import.wide")) // (param $retptr i32) // (param i64 i64))) // ``` -fn render_wat_import(imports: &mut WatImports, import: &Import<'_>) { - let identifier = format!("{}.import.{}", import.module, import.name); +fn render_wat_import(imports: &mut WatImports, index: usize, import: &Import<'_>) { + let identifier = boundary_identifier(index); + let symbol = boundary_symbol(import); let mut wat = format!( - "(import \"{}\" \"{}\" (func ${identifier} \ - (@sym (name \"{identifier}\"))", - import.module, import.name, + "(import {} {} (func ${identifier} (@sym (name {}))", + quote_string(import.module), + quote_string(import.name), + quote_string(&symbol), ); if let Some(ImportOutput { @@ -98,7 +109,7 @@ fn render_wat_import(imports: &mut WatImports, import: &Import<'_>) { .. }) = import.output.as_ref() { - write!(wat, " (param $retptr {})", retptr.slot.boundary()) + write!(wat, " (param $retptr {})", retptr.slot.js()) .expect("writing to a String cannot fail"); } @@ -106,7 +117,7 @@ fn render_wat_import(imports: &mut WatImports, import: &Import<'_>) { .inputs .iter() .flat_map(|input| &input.slots) - .map(Slot::boundary) + .map(Slot::js) .map(WatType::as_str) .collect::>(); if !input_types.is_empty() { @@ -118,46 +129,53 @@ fn render_wat_import(imports: &mut WatImports, import: &Import<'_>) { .. }) = import.output.as_ref() { - write!(wat, " (result {})", slot.boundary()).expect("writing to a String cannot fail"); + write!(wat, " (result {})", slot.js()).expect("writing to a String cannot fail"); } wat.push_str("))"); - imports.insert(&identifier, wat); + imports.insert(&symbol, wat); } // The three imports shown above are exposed to Rust through these `ABI` shims: // // ```wat // ;; fn notify(value: u32) -// (func $js_sys.notify (@sym) (param $value_0 i32) +// (func $js_sys.import.shim.0 (@sym (name "js_sys.notify")) (param $value_0 i32) // local.get $value_0 -// call $js_sys.import.notify (@reloc) +// call $js_sys.import.boundary.0 (@reloc) // ) // // ;; fn identity(value: u32) -> u32 -// (func $js_sys.identity (@sym) (param $value_0 i32) (result i32) +// (func $js_sys.import.shim.1 (@sym (name "js_sys.identity")) (param $value_0 i32) (result i32) // local.get $value_0 -// call $js_sys.import.identity (@reloc) +// call $js_sys.import.boundary.1 (@reloc) // ) // // ;; fn wide(value: u128) -> u128 -// (func $js_sys.wide (@sym) (param $retptr i32) (param $value_0 i64) (param $value_1 i64) +// (func $js_sys.import.shim.2 (@sym (name "js_sys.wide")) (param $retptr i32) (param $value_0 i64) (param $value_1 i64) // local.get $retptr // local.get $value_0 // local.get $value_1 -// call $js_sys.import.wide (@reloc) +// call $js_sys.import.boundary.2 (@reloc) // ) // ``` fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { let Shim { + index, + boundary_index, import, catch, locals, } = shim; - write!(wat, "(func ${}.{} (@sym)", import.module, import.name) - .expect("writing to a String cannot fail"); - if import.kind == WireImportKind::ClosureFactory { - write!(wat, " (@comdat \"{}.{}\")", import.module, import.name) + write!( + wat, + "(func ${} (@sym (name {}))", + shim_identifier(index), + quote_string(&shim_symbol(import)), + ) + .expect("writing to a String cannot fail"); + if import.shim_kind == ImportShimKind::ClosureFactory { + write!(wat, " (@comdat {})", quote_string(&shim_symbol(import))) .expect("writing to a String cannot fail"); } @@ -166,13 +184,13 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { .. }) = import.output.as_ref() { - write!(wat, " (param $retptr {})", retptr.slot.abi) + write!(wat, " (param $retptr {})", retptr.slot.rust) .expect("writing to a String cannot fail"); } for input in &import.inputs { for (index, slot) in input.slots.iter().enumerate() { - write!(wat, " (param ${}_{index} {})", input.name, slot.abi) + write!(wat, " (param ${}_{index} {})", input.name, slot.rust) .expect("writing to a String cannot fail"); } } @@ -182,7 +200,7 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { .. }) = import.output.as_ref() { - write!(wat, " (result {})", slot.abi).expect("writing to a String cannot fail"); + write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); } let locals = locals.render(); @@ -215,8 +233,8 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { write!( wat, - "\n call ${}.import.{} (@reloc)", - import.module, import.name, + "\n call ${} (@reloc)", + boundary_identifier(boundary_index), ) .expect("writing to a String cannot fail"); @@ -236,7 +254,7 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { .. }) = import.output.as_ref() { - let zero = slot.abi.zero(); + let zero = slot.rust.zero(); write!(wat, "\n {zero}").expect("writing to a String cannot fail"); } } @@ -244,6 +262,22 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { wat.push_str("\n)"); } +fn boundary_identifier(index: usize) -> String { + format!("js_sys.import.boundary.{index}") +} + +fn shim_identifier(index: usize) -> String { + format!("js_sys.import.shim.{index}") +} + +fn boundary_symbol(import: &Import<'_>) -> String { + format!("{}.import.{}", import.module, import.name) +} + +fn shim_symbol(import: &Import<'_>) -> String { + format!("{}.{}", import.module, import.name) +} + fn conversion_slots<'import, 'wire>( import: &'import Import<'wire>, ) -> impl Iterator> { @@ -267,3 +301,75 @@ fn write_slot_get(wat: &mut String, local: &str, slot: &Slot<'_>) { write_conversion(wat, instruction); } } + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use js_bindgen_wire::ImportShimKind; + use js_bindgen_wire::abi::WatType; + use js_bindgen_wire::model::{Import, ImportGroup, ImportInput, Slot, WatConversion}; + + use super::render; + + #[test] + fn arbitrary_names_produce_valid_wat() { + const NAME: &str = "single' double\" slash\\ line\n雪"; + let group = ImportGroup { + catch: None, + imports: vec![Import { + module: NAME, + name: NAME, + shim_kind: ImportShimKind::ClosureFactory, + inputs: Vec::new(), + output: None, + binding: None, + suspending: false, + }], + }; + + let wat = render(&group).expect("one import produces WAT"); + js_bindgen_ld_shared::wat_to_object(false, &wat).expect("escaped WAT should parse"); + } + + #[test] + fn converted_slot_uses_js_type_for_import_and_rust_type_for_shim() { + let group = ImportGroup { + catch: None, + imports: vec![Import { + module: "test", + name: "converted", + shim_kind: ImportShimKind::Normal, + inputs: vec![ImportInput { + name: "value", + slots: vec![Slot { + rust: WatType::I32, + wat: Some(WatConversion { + js: WatType::ExternRef, + imports: Rc::from([]), + locals: Rc::from([]), + instruction: "drop\n ref.null extern", + }), + }], + js_conversion: None, + }], + output: None, + binding: None, + suspending: false, + }], + }; + + let wat = render(&group).expect("one import produces WAT"); + assert_eq!( + wat, + r#"(import "test" "converted" (func $js_sys.import.boundary.0 (@sym (name "test.import.converted")) (param externref))) +(func $js_sys.import.shim.0 (@sym (name "test.converted")) (param $value_0 i32) + local.get $value_0 + drop + ref.null extern + call $js_sys.import.boundary.0 (@reloc) +)"#, + ); + js_bindgen_ld_shared::wat_to_object(false, &wat).expect("rendered WAT should parse"); + } +} diff --git a/host/ld/src/wire/js.rs b/host/ld/src/wire/js.rs index ee5a2bc2..8b79d287 100644 --- a/host/ld/src/wire/js.rs +++ b/host/ld/src/wire/js.rs @@ -17,6 +17,36 @@ const PLACEHOLDERS: [(&str, Placeholder); 6] = [ ("$slot4", Placeholder::Slot(3)), ]; +/// Quotes one JavaScript string literal. +/// +/// JavaScript export names are valid strings rather than identifiers, so they +/// must not be interpolated directly into generated source. +pub(super) fn quote_string(value: &str) -> String { + let mut output = String::with_capacity(value.len() + 2); + output.push('"'); + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{0008}' => output.push_str("\\b"), + '\u{000c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + '\u{2028}' => output.push_str("\\u2028"), + '\u{2029}' => output.push_str("\\u2029"), + character if character <= '\u{001f}' => { + use std::fmt::Write; + write!(output, "\\u{:04x}", u32::from(character)) + .expect("writing to a String cannot fail"); + } + character => output.push(character), + } + } + output.push('"'); + output +} + /// Renders a conversion template using the supplied placeholder resolver. /// /// Unrecognized `$` sequences are copied without interpretation. @@ -49,3 +79,42 @@ pub(super) fn render_template( output } + +#[cfg(test)] +mod tests { + use std::fmt::Write; + + use super::{Placeholder, quote_string, render_template}; + + #[test] + fn quotes_javascript_strings() { + assert_eq!( + quote_string("single' double\" slash\\ line\n雪\u{2028}"), + "\"single' double\\\" slash\\\\ line\\n雪\\u2028\"", + ); + } + + #[test] + fn renders_placeholders() { + let cases = [ + ("$value", ""), + ("$prepared", ""), + ( + "$slot1, $slot2, $slot3, $slot4", + ", , , ", + ), + ("雪 $unknown $$value", "雪 $unknown $"), + ]; + + for (template, expected) in cases { + let rendered = render_template(template, |output, placeholder| match placeholder { + Placeholder::Value => output.push_str(""), + Placeholder::Prepared => output.push_str(""), + Placeholder::Slot(index) => { + write!(output, "").expect("writing to a String cannot fail"); + } + }); + assert_eq!(rendered, expected, "template: {template}"); + } + } +} diff --git a/host/ld/src/wire/mod.rs b/host/ld/src/wire/mod.rs index fdd02731..1c988640 100644 --- a/host/ld/src/wire/mod.rs +++ b/host/ld/src/wire/mod.rs @@ -5,10 +5,8 @@ mod import; mod js; mod wat; -use js_bindgen_wire::{ - Error, decode, - model::{Embed, Record}, -}; +use js_bindgen_wire::model::{Embed, Record}; +use js_bindgen_wire::{Error, decode}; /// One JavaScript binding ready for the linker store. #[derive(Debug, Eq, PartialEq)] diff --git a/host/ld/src/wire/wat.rs b/host/ld/src/wire/wat.rs index 8d0cddbf..33a11bce 100644 --- a/host/ld/src/wire/wat.rs +++ b/host/ld/src/wire/wat.rs @@ -6,6 +6,25 @@ use std::fmt::Write; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{WatImport, WatImportKind, WatLocal}; +/// Quotes a WAT string as UTF-8 bytes. +/// +/// Byte escapes keep arbitrary Unicode and control characters independent of +/// how the WAT source text is parsed. +pub(super) fn quote_string(value: &str) -> String { + let mut output = String::with_capacity(value.len() + 2); + output.push('"'); + for &byte in value.as_bytes() { + match byte { + b'"' => output.push_str("\\\""), + b'\\' => output.push_str("\\\\"), + 0x20..=0x7e => output.push(char::from(byte)), + byte => write!(output, "\\{byte:02x}").expect("writing to a String cannot fail"), + } + } + output.push('"'); + output +} + #[derive(Default)] pub(super) struct WatImports { indices: HashMap, @@ -87,8 +106,13 @@ pub(super) fn write_conversion(wat: &mut String, conversion: &str) { } fn write_import(wat: &mut String, import: &WatImport<'_>) { - write!(wat, "(import \"{}\" \"{}\" (", import.module, import.name) - .expect("writing to a String cannot fail"); + write!( + wat, + "(import {} {} (", + quote_string(import.module), + quote_string(import.name), + ) + .expect("writing to a String cannot fail"); match &import.kind { WatImportKind::Function { @@ -127,7 +151,8 @@ fn write_import(wat: &mut String, import: &WatImport<'_>) { fn write_symbol(wat: &mut String, name: Option<&str>) { if let Some(name) = name { - write!(wat, "(@sym (name \"{name}\"))").expect("writing to a String cannot fail"); + write!(wat, "(@sym (name {}))", quote_string(name)) + .expect("writing to a String cannot fail"); } else { wat.push_str("(@sym)"); } @@ -149,3 +174,16 @@ fn write_separator(wat: &mut String) { wat.push('\n'); } } + +#[cfg(test)] +mod tests { + use super::quote_string; + + #[test] + fn quotes_wat_strings_as_bytes() { + assert_eq!( + quote_string("single' double\" slash\\ line\n雪"), + "\"single' double\\\" slash\\\\ line\\0a\\e9\\9b\\aa\"", + ); + } +} diff --git a/host/macro/tests/ui/custom_section.stderr b/host/macro/tests/ui/custom_section.stderr index a3f441b7..75a2a00c 100644 --- a/host/macro/tests/ui/custom_section.stderr +++ b/host/macro/tests/ui/custom_section.stderr @@ -21,28 +21,19 @@ error[E0308]: mismatched types --> tests/ui/custom_section.rs:6:50 | 6 | js_bindgen::unsafe_global_wat!("{}", interpolate Bar); - | -------------------------------------------------^^^- - | | | - | | expected `&str`, found `Bar` - | expected because of the type of the constant + | ^^^ expected `&str`, found `Bar` error[E0308]: mismatched types --> tests/ui/custom_section.rs:9:50 | 9 | js_bindgen::unsafe_global_wat!("{}", interpolate 42); - | -------------------------------------------------^^- - | | | - | | expected `&str`, found integer - | expected because of the type of the constant + | ^^ expected `&str`, found integer error[E0308]: mismatched types --> tests/ui/custom_section.rs:12:73 | 12 | js_bindgen::import_js!(module = "foo", name = "bar", required_embeds = [42], ""); - | ------------------------------------------------------------------------^^------ - | | | - | | expected `(&str, &str)`, found integer - | expected because of the type of the constant + | ^^ expected `(&str, &str)`, found integer | = note: expected tuple `(&'static str, &'static str)` found type `{integer}` @@ -51,10 +42,7 @@ error[E0308]: mismatched types --> tests/ui/custom_section.rs:15:72 | 15 | js_bindgen::embed_js!(module = "foo", name = "bar", required_embeds = [42], ""); - | -----------------------------------------------------------------------^^------ - | | | - | | expected `(&str, &str)`, found integer - | expected because of the type of the constant + | ^^ expected `(&str, &str)`, found integer | = note: expected tuple `(&'static str, &'static str)` found type `{integer}` @@ -63,10 +51,7 @@ error[E0308]: mismatched types --> tests/ui/custom_section.rs:18:73 | 18 | js_bindgen::import_js!(module = "foo", name = "bar", required_embeds = ["qux"], ""); - | ------------------------------------------------------------------------^^^^^------ - | | | - | | expected `(&str, &str)`, found `&str` - | expected because of the type of the constant + | ^^^^^ expected `(&str, &str)`, found `&str` | = note: expected tuple `(&'static str, &'static str)` found reference `&'static str` @@ -74,14 +59,8 @@ error[E0308]: mismatched types error[E0308]: mismatched types --> tests/ui/custom_section.rs:24:21 | -21 | / js_bindgen::import_js!( -22 | | module = "foo", -23 | | name = "bar", -24 | | required_embeds = [("qux")], - | | ^^^^^^^ expected `(&str, &str)`, found `&str` -25 | | "" -26 | | ); - | |_- expected because of the type of the constant +24 | required_embeds = [("qux")], + | ^^^^^^^ expected `(&str, &str)`, found `&str` | = note: expected tuple `(&'static str, &'static str)` found reference `&'static str` @@ -89,14 +68,8 @@ error[E0308]: mismatched types error[E0308]: mismatched types --> tests/ui/custom_section.rs:32:21 | -29 | / js_bindgen::import_js!( -30 | | module = "foo", -31 | | name = "bar", -32 | | required_embeds = [("qux",)], - | | ^^^^^^^^ expected a tuple with 2 elements, found one with 1 element -33 | | "" -34 | | ); - | |_- expected because of the type of the constant +32 | required_embeds = [("qux",)], + | ^^^^^^^^ expected a tuple with 2 elements, found one with 1 element | = note: expected tuple `(&'static str, &'static str)` found tuple `(&'static str,)` diff --git a/host/wire/Cargo.toml b/host/wire/Cargo.toml index 6910f386..4bad2e54 100644 --- a/host/wire/Cargo.toml +++ b/host/wire/Cargo.toml @@ -6,13 +6,13 @@ rust-version = { workspace = true } license = { workspace = true } include = { workspace = true } -[features] -default = [] -alloc = [] - [lib] bench = false doctest = false +[features] +default = [] +alloc = [] + [lints] workspace = true diff --git a/host/wire/src/abi.rs b/host/wire/src/abi.rs index ff1127bd..9a4b2450 100644 --- a/host/wire/src/abi.rs +++ b/host/wire/src/abi.rs @@ -33,7 +33,8 @@ impl WatType { } } - /// Returns an instruction that places this type's default value on the stack. + /// Returns an instruction that places this type's default value on the + /// stack. #[must_use] #[cfg(feature = "alloc")] pub const fn zero(self) -> &'static str { @@ -318,7 +319,7 @@ pub struct WatConv { pub imports: &'static [WatImport], pub locals: &'static [WatLocal], pub instruction: &'static str, - pub boundary: WatType, + pub js: WatType, } impl WatConv { @@ -327,33 +328,34 @@ impl WatConv { imports: &'static [WatImport], locals: &'static [WatLocal], instruction: &'static str, - boundary: WatType, + js: WatType, ) -> Self { Self { imports, locals, instruction, - boundary, + js, } } } -/// One primitive `WebAssembly` slot and its boundary conversion. +/// The Rust-facing type of one primitive `WebAssembly` slot and its optional +/// JavaScript-facing conversion. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WatSlot { - pub abi: WatType, + pub rust: WatType, pub wat: Option, } impl WatSlot { #[must_use] - pub const fn new(abi: WatType, wat: Option) -> Self { - Self { abi, wat } + pub const fn new(rust: WatType, wat: Option) -> Self { + Self { rust, wat } } #[must_use] - pub const fn plain(abi: WatType) -> Self { - Self::new(abi, None) + pub const fn plain(rust: WatType) -> Self { + Self::new(rust, None) } } diff --git a/host/wire/src/decode/export.rs b/host/wire/src/decode/export.rs index c7e1596a..e49422ba 100644 --- a/host/wire/src/decode/export.rs +++ b/host/wire/src/decode/export.rs @@ -1,16 +1,15 @@ use alloc::vec::Vec; +use super::{Decode, Decoder, value}; +use crate::model::{ + Callee, Embed, Export, ExportInput, ExportInputConversion, ExportInputKind, ExportOutput, + FrameSlot, ResultLayout, ReturnFrame, +}; use crate::{ EXPORT_FLAGS, EXPORT_HAS_OUTPUT, EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_FLAGS, EXPORT_OUTPUT_RESULT, EXPORT_PROMISING, Error, PointerWidth, SLOT_COUNT, - model::{ - Callee, Embed, Export, ExportInput, ExportInputConversion, ExportInputKind, ExportOutput, - FrameSlot, ResultLayout, ReturnFrame, - }, }; -use super::{Decode, Decoder, value}; - pub(super) fn decode<'a>( decoder: &mut Decoder<'a>, pointer_width: PointerWidth, @@ -46,7 +45,7 @@ impl<'a> Export<'a> { if matches!(callee, Callee::Closure { .. }) { decoder.ensure( inputs.first().is_some_and(|input| { - input.slots.len() == 1 && input.slots[0].abi == pointer_width.wat_type() + input.slots.len() == 1 && input.slots[0].rust == pointer_width.wat_type() }), "closure export data must occupy one pointer slot", )?; diff --git a/host/wire/src/decode/import.rs b/host/wire/src/decode/import.rs index c1739c1f..206711b8 100644 --- a/host/wire/src/decode/import.rs +++ b/host/wire/src/decode/import.rs @@ -1,19 +1,19 @@ -use alloc::{rc::Rc, vec::Vec}; +use alloc::rc::Rc; +use alloc::vec::Vec; +use super::{Decode, Decoder, value}; +use crate::abi::WatType; +use crate::model::{ + DirectImportConversion, Embed, Import, ImportBinding, ImportCatch, ImportErrorMode, + ImportGroup, ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, + Slot, WatCatch, +}; use crate::{ Error, IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_CLOSURE_FACTORY, IMPORT_FLAGS, IMPORT_HAS_BINDING, IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_FLAGS, - IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, PointerWidth, WireImportKind, - abi::WatType, - model::{ - DirectImportConversion, Embed, Import, ImportBinding, ImportCatch, ImportErrorMode, - ImportGroup, ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, - JsCatch, Slot, WatCatch, - }, + IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, ImportShimKind, PointerWidth, }; -use super::{Decode, Decoder, value}; - struct InputType<'a> { slots: Vec>, js_conversion: Option<&'a str>, @@ -42,7 +42,7 @@ pub(super) fn decode<'a>( } else { let pointer = InputType::decode(decoder)?; decoder.ensure( - pointer.slots.len() == 1 && pointer.slots[0].abi == pointer_width.wat_type(), + pointer.slots.len() == 1 && pointer.slots[0].rust == pointer_width.wat_type(), "import return pointer does not match the target pointer width", )?; Some(pointer) @@ -188,7 +188,7 @@ impl<'a> OutputType<'a> { if result { decoder.ensure( matches!( - slots[0].abi, + slots[0].rust, WatType::I32 | WatType::I64 | WatType::F32 | WatType::F64 ), "unsupported direct Result return slot", @@ -246,10 +246,10 @@ impl<'a> Import<'a> { let module = decoder.string()?; let name = decoder.string()?; let flags = decoder.flags("import", IMPORT_FLAGS)?; - let kind = if flags & IMPORT_CLOSURE_FACTORY == 0 { - WireImportKind::Normal + let shim_kind = if flags & IMPORT_CLOSURE_FACTORY == 0 { + ImportShimKind::Normal } else { - WireImportKind::ClosureFactory + ImportShimKind::ClosureFactory }; let input_count = decoder.count("import input")?; @@ -319,7 +319,7 @@ impl<'a> Import<'a> { Ok(Self { module, name, - kind, + shim_kind, inputs, output, binding, diff --git a/host/wire/src/decode/mod.rs b/host/wire/src/decode/mod.rs index 11bb1b81..2a48cf8b 100644 --- a/host/wire/src/decode/mod.rs +++ b/host/wire/src/decode/mod.rs @@ -4,9 +4,11 @@ mod export; mod import; mod value; +use core::mem::size_of; use core::{fmt, str}; -use crate::{KIND_EXPORT, KIND_IMPORT, MAGIC, PointerWidth, VERSION, model::Record}; +use crate::model::Record; +use crate::{KIND_EXPORT, KIND_IMPORT, MAGIC, PointerWidth, VERSION}; /// A type that can be decoded from a wire record. pub(crate) trait Decode<'de>: Sized { @@ -21,6 +23,52 @@ pub fn decode(bytes: &[u8]) -> Result, Error> { Ok(record) } +/// Iterates over the length-prefixed records concatenated in a wire custom +/// section. +pub struct WireRecords<'a> { + bytes: &'a [u8], + position: usize, +} + +impl<'a> WireRecords<'a> { + #[must_use] + pub const fn new(bytes: &'a [u8]) -> Self { + Self { bytes, position: 0 } + } + + fn unexpected_end(&mut self, offset: usize, needed: usize) -> Result<&'a [u8], Error> { + self.position = self.bytes.len(); + Err(Error::new(offset, ErrorKind::UnexpectedEnd { needed })) + } +} + +impl<'a> Iterator for WireRecords<'a> { + type Item = Result<&'a [u8], Error>; + + fn next(&mut self) -> Option { + if self.position == self.bytes.len() { + return None; + } + + let length_offset = self.position; + let Some(length_end) = length_offset.checked_add(size_of::()) else { + return Some(self.unexpected_end(length_offset, size_of::())); + }; + let Some(length) = self.bytes.get(length_offset..length_end) else { + return Some(self.unexpected_end(length_offset, size_of::())); + }; + let length = u32::from_le_bytes([length[0], length[1], length[2], length[3]]) as usize; + let Some(record_end) = length_end.checked_add(length) else { + return Some(self.unexpected_end(length_end, length)); + }; + let Some(record) = self.bytes.get(length_end..record_end) else { + return Some(self.unexpected_end(length_end, length)); + }; + self.position = record_end; + Some(Ok(record)) + } +} + impl<'de> Decode<'de> for Record<'de> { fn decode(decoder: &mut Decoder<'de>) -> Result { let magic_offset = decoder.position(); diff --git a/host/wire/src/decode/value.rs b/host/wire/src/decode/value.rs index 4d2aea1b..33e2b0bc 100644 --- a/host/wire/src/decode/value.rs +++ b/host/wire/src/decode/value.rs @@ -1,12 +1,10 @@ -use alloc::{rc::Rc, vec::Vec}; - -use crate::{ - Error, ErrorKind, SLOT_COUNT, WAT_IMPORT_FUNCTION, WAT_IMPORT_TABLE, WAT_IMPORT_TAG, - abi::{RefType, WatIndexType, WatType}, - model::{Embed, Slot, WatConversion, WatImport, WatImportKind, WatLocal}, -}; +use alloc::rc::Rc; +use alloc::vec::Vec; use super::{Decode, Decoder}; +use crate::abi::{RefType, WatIndexType, WatType}; +use crate::model::{Embed, Slot, WatConversion, WatImport, WatImportKind, WatLocal}; +use crate::{Error, ErrorKind, SLOT_COUNT, WAT_IMPORT_FUNCTION, WAT_IMPORT_TABLE, WAT_IMPORT_TAG}; pub(super) type WireSlots<'a> = [Option>; SLOT_COUNT]; @@ -24,18 +22,18 @@ pub(super) enum Sret<'a> { impl<'de> Decode<'de> for Slot<'de> { fn decode(decoder: &mut Decoder<'de>) -> Result { - let abi = decode_wat_type(decoder)?; + let rust = decode_wat_type(decoder)?; let wat = if decoder.boolean("slot WAT presence")? { Some(WatConversion { imports: decode_imports(decoder)?, locals: decode_locals(decoder)?, instruction: decoder.string()?, - boundary: decode_wat_type(decoder)?, + js: decode_wat_type(decoder)?, }) } else { None }; - Ok(Self { abi, wat }) + Ok(Self { rust, wat }) } } diff --git a/host/wire/src/encode/export.rs b/host/wire/src/encode/export.rs index 636f1331..0a74ca10 100644 --- a/host/wire/src/encode/export.rs +++ b/host/wire/src/encode/export.rs @@ -1,12 +1,11 @@ use core::mem::size_of; +use super::{Encoder, Sizer, flag}; use crate::{ - EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_RESULT, WireExport, WireExportCallee, WireExportInput, - WireExportOutput, WireExportOutputType, + EXPORT_HAS_OUTPUT, EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_RESULT, EXPORT_PROMISING, WireExport, + WireExportCallee, WireExportInput, WireExportOutput, WireExportOutputType, }; -use super::{Encoder, Sizer}; - impl Encoder { pub(super) const fn exports(&mut self, exports: &[WireExport]) { self.count(exports.len()); @@ -113,10 +112,6 @@ impl WireExportOutputType { } } -const fn flag(enabled: bool, value: u8) -> u8 { - if enabled { value } else { 0 } -} - impl WireExportOutput { const fn encode(self, encoder: &mut Encoder) { self.ty.encode(encoder); @@ -131,7 +126,9 @@ impl WireExport { const fn encode(&self, encoder: &mut Encoder) { encoder.string(self.module); encoder.string(self.name); - encoder.u8(self.flags); + let flags = + flag(self.promising, EXPORT_PROMISING) | flag(self.output.is_some(), EXPORT_HAS_OUTPUT); + encoder.u8(flags); self.callee.encode(encoder); encoder.count(self.inputs.len()); let mut index = 0; diff --git a/host/wire/src/encode/import.rs b/host/wire/src/encode/import.rs index 1cd3b9c0..aab4455e 100644 --- a/host/wire/src/encode/import.rs +++ b/host/wire/src/encode/import.rs @@ -1,15 +1,14 @@ use core::mem::size_of; +use super::{Encoder, Sizer, flag, wire_u32}; +use crate::abi::{JsCatch, WatCatch}; use crate::{ IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_CLOSURE_FACTORY, IMPORT_HAS_BINDING, - IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, WireImport, - WireImportBinding, WireImportCatch, WireImportInput, WireImportInputType, WireImportKind, - WireImportOutput, WireImportOutputType, WireImportTypeTable, - abi::{JsCatch, WatCatch}, + IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, + ImportShimKind, WireImport, WireImportBinding, WireImportCatch, WireImportInput, + WireImportInputType, WireImportOutput, WireImportOutputType, WireImportTypeTable, }; -use super::{Encoder, Sizer}; - impl Encoder { pub(super) const fn imports(&mut self, table: &WireImportTypeTable, imports: &[WireImport]) { table.encode(self); @@ -270,7 +269,7 @@ impl WireImport { | flag(self.output.is_some(), IMPORT_HAS_OUTPUT) | flag(self.binding.is_some(), IMPORT_HAS_BINDING) | flag( - matches!(self.kind, WireImportKind::ClosureFactory), + matches!(self.shim_kind, ImportShimKind::ClosureFactory), IMPORT_CLOSURE_FACTORY, )); encoder.count(self.inputs.len()); @@ -304,16 +303,3 @@ impl WireImport { } } } - -const fn flag(enabled: bool, value: u8) -> u8 { - if enabled { value } else { 0 } -} - -#[expect( - clippy::cast_possible_truncation, - reason = "the function asserts that the value fits in u32" -)] -const fn wire_u32(value: usize) -> u32 { - assert!(value <= u32::MAX as usize); - value as u32 -} diff --git a/host/wire/src/encode/mod.rs b/host/wire/src/encode/mod.rs index fe28e8bb..b1f57e49 100644 --- a/host/wire/src/encode/mod.rs +++ b/host/wire/src/encode/mod.rs @@ -5,13 +5,11 @@ mod import; use core::mem::size_of; -use crate::{ - MAGIC, VERSION, WAT_IMPORT_FUNCTION, WAT_IMPORT_TABLE, WAT_IMPORT_TAG, Wire, - abi::{ - FromJsConv, IntoJsConv, JsEmbed, Sret, WatImport, WatImportKind, WatLocal, WatSlot, WatType, - }, - schema::WireKind, +use crate::abi::{ + FromJsConv, IntoJsConv, JsEmbed, Sret, WatImport, WatImportKind, WatLocal, WatSlot, WatType, }; +use crate::schema::WireKind; +use crate::{MAGIC, VERSION, WAT_IMPORT_FUNCTION, WAT_IMPORT_TABLE, WAT_IMPORT_TAG, Wire}; /// A raw, self-contained wire record without custom-section framing. #[derive(Clone, Copy)] @@ -38,7 +36,8 @@ impl WireRecord { } } -/// A length-prefixed wire record stored in the `js_bindgen.wire` custom section. +/// A length-prefixed wire record stored in the `js_bindgen.wire` custom +/// section. #[repr(C)] pub struct WireBlob { record_len: [u8; 4], @@ -320,7 +319,7 @@ impl Sizer { impl WatSlot { const fn encode(&self, encoder: &mut Encoder) { - encoder.u8(self.abi.tag()); + encoder.u8(self.rust.tag()); match self.wat { Some(wat) => { encoder.u8(1); @@ -337,7 +336,7 @@ impl WatSlot { index += 1; } encoder.string(wat.instruction); - encoder.u8(wat.boundary.tag()); + encoder.u8(wat.js.tag()); } None => encoder.u8(0), } @@ -526,11 +525,15 @@ const fn template_mask(templates: &[Option<&str>; 4]) -> u8 { clippy::cast_possible_truncation, reason = "the function asserts that the value fits in u32" )] -const fn wire_u32(value: usize) -> u32 { +pub(crate) const fn wire_u32(value: usize) -> u32 { assert!(value <= u32::MAX as usize); value as u32 } +pub(crate) const fn flag(enabled: bool, value: u8) -> u8 { + if enabled { value } else { 0 } +} + #[expect( clippy::cast_possible_truncation, reason = "the function asserts that the value fits in u32" diff --git a/host/wire/src/lib.rs b/host/wire/src/lib.rs index 5b12f7e4..3a5ad56a 100644 --- a/host/wire/src/lib.rs +++ b/host/wire/src/lib.rs @@ -16,18 +16,20 @@ mod decode; #[cfg(feature = "alloc")] pub mod model; +#[cfg(feature = "alloc")] +pub use decode::{Error, ErrorKind, WireRecords, decode}; pub use encode::{WireBlob, WireRecord, wire_blob_len}; pub use schema::*; -#[cfg(feature = "alloc")] -pub use decode::{Error, ErrorKind, decode}; - /// Identifies a wire record independently of its payload kind. pub const MAGIC: [u8; 8] = *b"JBGWIRE\0"; /// The single protocol version used by imports and exports. pub const VERSION: u16 = 1; +/// Custom section containing length-prefixed wire records. +pub const WIRE_SECTION: &str = "js_bindgen.wire"; + #[cfg(feature = "alloc")] pub(crate) const SLOT_COUNT: usize = 4; pub(crate) const KIND_IMPORT: u8 = 0; diff --git a/host/wire/src/model.rs b/host/wire/src/model.rs index 7ef96d6e..2d8c0f09 100644 --- a/host/wire/src/model.rs +++ b/host/wire/src/model.rs @@ -1,27 +1,26 @@ //! Canonical model consumed by JavaScript and `WAT` `renderers`. -use alloc::{rc::Rc, vec::Vec}; +use alloc::rc::Rc; +use alloc::vec::Vec; +use crate::ImportShimKind; pub use crate::PointerWidth; pub use crate::abi::ResultLayout; -use crate::{ - WireImportKind, - abi::{RefType, WatIndexType, WatType}, -}; +use crate::abi::{RefType, WatIndexType, WatType}; -/// One primitive `Wasm` `ABI` slot. +/// One primitive `Wasm` slot. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Slot<'a> { - pub abi: WatType, + pub rust: WatType, pub wat: Option>, } impl<'a> Slot<'a> { #[must_use] - pub fn boundary(&self) -> WatType { + pub fn js(&self) -> WatType { self.wat .as_ref() - .map_or(self.abi, |conversion| conversion.boundary) + .map_or(self.rust, |conversion| conversion.js) } #[must_use] @@ -44,10 +43,11 @@ impl<'a> Slot<'a> { } } -/// `WAT` required to translate one slot across the JavaScript boundary. +/// `WAT` required to translate one slot between its Rust- and +/// JavaScript-facing types. #[derive(Clone, Debug, Eq, PartialEq)] pub struct WatConversion<'a> { - pub boundary: WatType, + pub js: WatType, pub imports: Rc<[WatImport<'a>]>, pub locals: Rc<[WatLocal<'a>]>, pub instruction: &'a str, @@ -202,7 +202,7 @@ pub struct ImportBinding<'a> { pub struct Import<'a> { pub module: &'a str, pub name: &'a str, - pub kind: WireImportKind, + pub shim_kind: ImportShimKind, pub inputs: Vec>, pub output: Option>, pub binding: Option>, diff --git a/host/wire/src/schema.rs b/host/wire/src/schema.rs index 22f8bba9..5f0b3fac 100644 --- a/host/wire/src/schema.rs +++ b/host/wire/src/schema.rs @@ -2,12 +2,9 @@ use core::mem::size_of; -use crate::{ - EXPORT_HAS_OUTPUT, EXPORT_PROMISING, - abi::{ - FromJsConv, IntoJsConv, JsCatch, JsEmbed, ResultLayout, ReturnConv, ReturnMode, Sret, - WatCatch, WatSlot, WatType, - }, +use crate::abi::{ + FromJsConv, IntoJsConv, JsCatch, JsEmbed, ResultLayout, ReturnConv, ReturnMode, Sret, WatCatch, + WatSlot, WatType, }; /// The target's native pointer width. @@ -93,7 +90,8 @@ impl WireImportOutputType { pub enum WireImportCatch { /// JavaScript wraps the call in `try`/`catch` and records the exception. JavaScript(JsCatch), - /// Wasm exception handling catches and records the exception in the `ABI` shim. + /// Wasm exception handling catches and records the exception in the `ABI` + /// shim. Wasm(WatCatch), } @@ -187,7 +185,7 @@ impl WireImportOutput { /// The role of one generated `Wasm` adapter. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum WireImportKind { +pub enum ImportShimKind { Normal, ClosureFactory, } @@ -197,7 +195,7 @@ pub enum WireImportKind { pub struct WireImport { pub(crate) module: &'static str, pub(crate) name: &'static str, - pub(crate) kind: WireImportKind, + pub(crate) shim_kind: ImportShimKind, pub(crate) inputs: &'static [WireImportInput], pub(crate) output: Option, pub(crate) binding: Option, @@ -218,7 +216,7 @@ impl WireImport { Self { module, name, - kind: WireImportKind::Normal, + shim_kind: ImportShimKind::Normal, inputs, output, binding, @@ -236,7 +234,7 @@ impl WireImport { suspending: bool, ) -> Self { let mut import = Self::new(module, name, inputs, output, binding, suspending); - import.kind = WireImportKind::ClosureFactory; + import.shim_kind = ImportShimKind::ClosureFactory; import } } @@ -342,7 +340,7 @@ pub struct WireExport { pub(crate) inputs: &'static [WireExportInput], pub(crate) output: Option, pub(crate) callee: WireExportCallee, - pub(crate) flags: u8, + pub(crate) promising: bool, } impl WireExport { @@ -361,7 +359,7 @@ impl WireExport { inputs, output, callee, - flags: flag(promising, EXPORT_PROMISING) | flag(output.is_some(), EXPORT_HAS_OUTPUT), + promising, } } @@ -506,7 +504,3 @@ const fn validate_imports(table: &WireImportTypeTable, imports: &[WireImport]) { index += 1; } } - -const fn flag(enabled: bool, value: u8) -> u8 { - if enabled { value } else { 0 } -} diff --git a/host/wire/src/tests.rs b/host/wire/src/tests.rs index d3b39a16..a8588802 100644 --- a/host/wire/src/tests.rs +++ b/host/wire/src/tests.rs @@ -1,4 +1,5 @@ -use alloc::{vec, vec::Vec}; +use alloc::vec; +use alloc::vec::Vec; use crate::abi::{ FromJsConv, IntoJsConv, JsCatch as AbiJsCatch, JsEmbed, RefType, ReturnConv, ReturnMode, Sret, @@ -189,20 +190,31 @@ fn imports() { }; let imports = &group.imports; assert_eq!(imports.len(), 2); + assert_eq!(imports[0].module, "js_sys"); assert_eq!(imports[0].name, "number.identity"); + assert_eq!(imports[0].inputs[0].name, "arg0"); + assert_eq!(imports[0].inputs[0].js_conversion, Some("$slot1")); + let binding = imports[0].binding.as_ref().unwrap(); + assert_eq!(binding.direct_expression, Some("globalThis.identity")); + assert_eq!(binding.call_expression, "globalThis.identity(arg0)"); assert_eq!( - imports[0] - .binding - .as_ref() - .unwrap() - .embeds - .iter() - .map(|embed| embed.name) - .collect::>(), - ["input.convert", "identity"] + binding.embeds, + [ + Embed { + module: "js_sys", + name: "input.convert", + }, + Embed { + module: "js_sys", + name: "identity", + }, + ] ); - let conversion = imports[0].inputs[0].slots[0].wat.as_ref().unwrap(); - assert_eq!(conversion.boundary, WatType::ExternRef); + let slot = &imports[0].inputs[0].slots[0]; + assert_eq!(slot.rust, WatType::I32); + assert_eq!(slot.js(), WatType::ExternRef); + let conversion = slot.wat.as_ref().unwrap(); + assert_eq!(conversion.js, WatType::ExternRef); assert_eq!(conversion.imports.len(), 4); assert_eq!(conversion.imports[0].identifier, "support.function"); assert!(matches!( @@ -248,8 +260,8 @@ fn imports() { ] ); assert!(imports[1].suspending); - assert_eq!(imports[0].kind, WireImportKind::ClosureFactory); - assert_eq!(imports[1].kind, WireImportKind::Normal); + assert_eq!(imports[0].shim_kind, ImportShimKind::ClosureFactory); + assert_eq!(imports[1].shim_kind, ImportShimKind::Normal); let Some(ImportCatch::Wasm(catch)) = &group.catch else { panic!("expected Wasm catch metadata"); }; @@ -260,21 +272,28 @@ fn imports() { "(try_table (catch $support.exception $support.catch)" ); assert_eq!(catch.catch, ") local.set $support.index"); + let binding = imports[1].binding.as_ref().unwrap(); + assert_eq!(binding.direct_expression, None); + assert_eq!(binding.call_expression, "globalThis.wide()"); assert_eq!( - imports[1] - .binding - .as_ref() - .unwrap() - .embeds - .iter() - .map(|embed| embed.name) - .collect::>(), - ["retptr.convert", "output.convert"] + binding.embeds, + [ + Embed { + module: "js_sys", + name: "retptr.convert", + }, + Embed { + module: "js_sys", + name: "output.convert", + }, + ] ); - assert!(matches!( - imports[1].output.as_ref().unwrap().abi, - ImportOutputAbi::Indirect { .. } - )); + let ImportOutputAbi::Indirect { retptr, .. } = &imports[1].output.as_ref().unwrap().abi else { + panic!("expected indirect output"); + }; + assert_eq!(retptr.slot.rust, WatType::I32); + assert_eq!(retptr.slot.js(), WatType::I32); + assert_eq!(retptr.js_conversion, Some("$slot1 >>> 0")); } #[test] @@ -331,13 +350,6 @@ fn catch_payload_is_omitted_without_result_types() { panic!("expected imports"); }; assert!(group.catch.is_none()); - assert!(group.imports.is_empty()); - assert!( - !RECORD - .as_bytes() - .windows("catch ($error)".len()) - .any(|window| window == b"catch ($error)") - ); } #[test] @@ -346,7 +358,19 @@ fn exports() { panic!("expected exports"); }; assert_eq!(exports.len(), 2); + assert_eq!(exports[0].module, "exports"); + assert_eq!(exports[0].name, "foo"); assert_eq!(exports[0].pointer_width, PointerWidth::Wasm64); + assert_eq!(exports[0].callee, Callee::Symbol { name: "foo.raw" }); + assert_eq!(exports[0].inputs[0].name, "arg0"); + assert_eq!(exports[0].inputs[0].kind, ExportInputKind::Value); + assert_eq!(exports[0].inputs[0].slots[0].rust, WatType::I32); + assert_eq!(exports[0].inputs[0].slots[0].js(), WatType::I32); + let Some(ExportOutput::Direct { slot, .. }) = &exports[0].output else { + panic!("expected direct output"); + }; + assert_eq!(slot.rust, WatType::I32); + assert_eq!(slot.js(), WatType::I32); assert!(matches!( exports[1].callee, Callee::Closure { @@ -396,3 +420,31 @@ fn invalid_records() { &ErrorKind::TrailingBytes(1) ); } + +#[test] +fn record_stream() { + let first = IMPORT_RECORD.as_bytes(); + let second = EXPORT_RECORD.as_bytes(); + let mut section = Vec::new(); + section.extend_from_slice(&u32::try_from(first.len()).unwrap().to_le_bytes()); + section.extend_from_slice(first); + section.extend_from_slice(&u32::try_from(second.len()).unwrap().to_le_bytes()); + section.extend_from_slice(second); + + let records = WireRecords::new(§ion) + .collect::, _>>() + .unwrap(); + assert_eq!(records, [first.as_slice(), second.as_slice()]); + + let header_error = WireRecords::new(&[0, 0, 0]).next().unwrap().unwrap_err(); + assert_eq!(header_error.offset(), 0); + assert_eq!(header_error.kind(), &ErrorKind::UnexpectedEnd { needed: 4 }); + + let truncated = [5, 0, 0, 0, 1, 2]; + let payload_error = WireRecords::new(&truncated).next().unwrap().unwrap_err(); + assert_eq!(payload_error.offset(), 4); + assert_eq!( + payload_error.kind(), + &ErrorKind::UnexpectedEnd { needed: 5 } + ); +} From 8c58e658223714a2d13e92580c07688e3c473cbb Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:07:53 +0800 Subject: [PATCH 18/21] Remove generated binding pipeline --- .github/workflows/ci.yaml | 35 -- client/Cargo.toml | 1 - client/js-sys/src/runtime/future/queue.rs | 4 - client/web-sys/build.rs | 66 ---- client/web-sys/src/console.gen.rs | 180 --------- .../src/{console.js-sys.rs => console.rs} | 3 +- client/web-sys/src/lib.rs | 2 - host/Cargo.toml | 3 - host/cargo-js-sys/Cargo.toml | 35 -- host/cargo-js-sys/src/js_sys.rs | 370 ------------------ host/cargo-js-sys/src/main.rs | 57 --- host/dev/src/check.rs | 8 +- host/dev/src/client/check.rs | 89 +---- host/dev/src/client/mod.rs | 2 +- host/js-sys-bindgen/Cargo.toml | 1 - host/js-sys-bindgen/src/file.rs | 95 ----- host/js-sys-bindgen/src/function.rs | 2 +- host/js-sys-bindgen/src/hygiene.rs | 34 +- host/js-sys-bindgen/src/lib.rs | 4 - host/js-sys-bindgen/src/macro.rs | 35 +- 20 files changed, 36 insertions(+), 990 deletions(-) delete mode 100644 client/web-sys/build.rs delete mode 100644 client/web-sys/src/console.gen.rs rename client/web-sys/src/{console.js-sys.rs => console.rs} (88%) delete mode 100644 host/cargo-js-sys/Cargo.toml delete mode 100644 host/cargo-js-sys/src/js_sys.rs delete mode 100644 host/cargo-js-sys/src/main.rs delete mode 100644 host/js-sys-bindgen/src/file.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b280e532..f172f9dd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,11 +83,6 @@ jobs: cache-bin: false - name: Prepare Output Directory run: mkdir ${{ runner.temp }}/host-tools - - name: Build `cargo-js-sys` - working-directory: host - run: | - cargo ${{ matrix.os.sub-command }} build -p cargo-js-sys ${{ matrix.os.args }} --release - cp target/${{ matrix.os.path }}release/cargo-js-sys${{ matrix.os.exe }} ${{ runner.temp }}/host-tools/ - name: Build `js-bindgen-ld` working-directory: host run: | @@ -529,33 +524,3 @@ jobs: - name: Test working-directory: host run: js-bindgen-dev host test -v - - cargo-js-sys: - name: Check `cargo-js-sys` - - needs: host-tools - - runs-on: ubuntu-latest - - timeout-minutes: 20 - - env: - JBG_DEV_TOOLS: 1 - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Download Host Tools - uses: actions/download-artifact@v8 - with: - name: host-tools-linux - path: ${{ runner.temp }}/host-tools/ - - name: Install Host Tools - run: | - chmod +x ${{ runner.temp }}/host-tools/* - echo "${{ runner.temp }}/host-tools" >> $GITHUB_PATH - - name: Check - working-directory: client - run: cargo-js-sys js-sys -c --workspace -v diff --git a/client/Cargo.toml b/client/Cargo.toml index f85faff4..71548949 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -13,7 +13,6 @@ edition = "2024" rust-version = "1.85" license = "MIT OR Apache-2.0" include = [ - "!/src/**/*.js-sys.rs", "!/src/**/tests/**", "/*.ron", "/Cargo.toml", diff --git a/client/js-sys/src/runtime/future/queue.rs b/client/js-sys/src/runtime/future/queue.rs index c41d8766..0df0a705 100644 --- a/client/js-sys/src/runtime/future/queue.rs +++ b/client/js-sys/src/runtime/future/queue.rs @@ -12,10 +12,6 @@ js_bindgen::embed_js!( #[crate::js_sys(js_sys = crate)] extern "js-sys" { - #[expect( - clippy::unnecessary_operation, - reason = "the generated wrapper calls a side-effect-only import" - )] #[js_sys(js_embed = "future.schedule")] fn schedule(); } diff --git a/client/web-sys/build.rs b/client/web-sys/build.rs deleted file mode 100644 index 0c567cc0..00000000 --- a/client/web-sys/build.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! This file is not shipped to Crates.io, but it is present when depending on -//! `web-sys` via `git` or `path`. - -use std::io::ErrorKind; -use std::path::Path; -use std::process::Command; -use std::{env, fs, panic, process}; - -fn main() { - if option_env!("JBG_DEV").is_none_or(|value| value != "1") - || option_env!("CI").is_some_and(|value| value == "true") - { - return; - } - - if search_dir(&env::current_dir().unwrap(), false) { - let status = Command::new("cargo") - .env_remove("CARGO_ENCODED_RUSTFLAGS") - .current_dir("../../host") - .arg("+stable") - .arg("run") - .args(["-p", "cargo-js-sys"]) - .arg("--") - .arg("-q") - .arg("js-sys") - .args(["--manifest-path", "../client/web-sys/Cargo.toml"]) - .status() - .unwrap(); - - if !status.success() { - process::exit(status.code().unwrap_or(1)) - } - } -} - -fn search_dir(dir: &Path, mut any: bool) -> bool { - for entry in fs::read_dir(dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - - if path.is_file() && path.as_os_str().as_encoded_bytes().ends_with(b".js-sys.rs") { - println!("cargo::rerun-if-changed={}", path.display()); - - if !any { - let r#gen = path.with_extension("").with_extension("gen.rs"); - - match fs::metadata(r#gen) { - Ok(meta) => { - let gen_mtime = meta.modified().unwrap(); - let js_sys_mtime = fs::metadata(&path).unwrap().modified().unwrap(); - - if gen_mtime < js_sys_mtime { - any = true; - } - } - Err(error) if error.kind() == ErrorKind::NotFound => any = true, - Err(error) => panic::panic_any(error), - } - } - } else if path.is_dir() { - any |= search_dir(&path, any); - } - } - - any -} diff --git a/client/web-sys/src/console.gen.rs b/client/web-sys/src/console.gen.rs deleted file mode 100644 index ae965645..00000000 --- a/client/web-sys/src/console.gen.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use js_sys::wire; -use js_sys::JsValue; -use js_sys::hazard::JsCast; - -pub fn log0() { - unsafe extern "C" { - #[link_name = "web_sys.console.log0"] - fn __import_(); - } - - { unsafe { __import_() } }; -} - -pub fn log(data: &[T]) { - unsafe extern "C" { - #[link_name = "web_sys.console.log"] - fn __import_( - arg0_0: wire::InputSlot1<&[JsValue]>, - arg0_1: wire::InputSlot2<&[JsValue]>, - arg0_2: wire::InputSlot3<&[JsValue]>, - arg0_3: wire::InputSlot4<&[JsValue]>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = unsafe { wire::split_input_as::<&[JsValue]>(data) }; - unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3) } - }; -} - -pub fn log2(data1: &JsValue, data2: &JsValue) { - unsafe extern "C" { - #[link_name = "web_sys.console.log2"] - fn __import_( - arg0_0: wire::InputSlot1<&JsValue>, - arg0_1: wire::InputSlot2<&JsValue>, - arg0_2: wire::InputSlot3<&JsValue>, - arg0_3: wire::InputSlot4<&JsValue>, - arg1_0: wire::InputSlot1<&JsValue>, - arg1_1: wire::InputSlot2<&JsValue>, - arg1_2: wire::InputSlot3<&JsValue>, - arg1_3: wire::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data1); - let (arg1_0, arg1_1, arg1_2, arg1_3) = wire::split_input::<&JsValue>(data2); - unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3, arg1_0, arg1_1, arg1_2, arg1_3) } - }; -} - -pub fn error(data: &JsValue) { - unsafe extern "C" { - #[link_name = "web_sys.console.error"] - fn __import_( - arg0_0: wire::InputSlot1<&JsValue>, - arg0_1: wire::InputSlot2<&JsValue>, - arg0_2: wire::InputSlot3<&JsValue>, - arg0_3: wire::InputSlot4<&JsValue>, - ); - } - - { - let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data); - unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3) } - }; -} - -pub fn error1(data: &JsValue) -> u128 { - unsafe extern "C" { - #[link_name = "web_sys.console.error1"] - fn __import_( - arg0_0: wire::InputSlot1<&JsValue>, - arg0_1: wire::InputSlot2<&JsValue>, - arg0_2: wire::InputSlot3<&JsValue>, - arg0_3: wire::InputSlot4<&JsValue>, - ) -> wire::OutputRet; - } - - wire::join_output({ - let (arg0_0, arg0_1, arg0_2, arg0_3) = wire::split_input::<&JsValue>(data); - unsafe { __import_(arg0_0, arg0_1, arg0_2, arg0_3) } - }) -} -const _: () = { - const TABLE: &wire::WireImportTypeTable = &wire::WireImportTypeTable::new( - wire::wire_import_retptr_type(), - &[wire::wire_import_input_type::<&[JsValue]>(), wire::wire_import_input_type::<&JsValue>()], - &[wire::wire_import_output_type::()], - wire::wire_import_catch(), - ); - const _WIRE: wire::Wire = wire::Wire::imports( - TABLE, - &[ - wire::WireImport::new( - "web_sys", - "console.log0", - &[], - ::core::option::Option::None, - ::core::option::Option::Some( - wire::WireImportBinding::new( - ::core::option::Option::None, - "globalThis.console.log()", - &[], - ), - ), - false, - ), - wire::WireImport::new( - "web_sys", - "console.log", - &[wire::WireImportInput::new("arg0", 0usize)], - ::core::option::Option::None, - ::core::option::Option::Some( - wire::WireImportBinding::new( - ::core::option::Option::None, - "globalThis.console.log(arg0_0)", - &[], - ), - ), - false, - ), - wire::WireImport::new( - "web_sys", - "console.log2", - &[ - wire::WireImportInput::new("arg0", 1usize), - wire::WireImportInput::new("arg1", 1usize), - ], - ::core::option::Option::None, - ::core::option::Option::Some( - wire::WireImportBinding::new( - ::core::option::Option::None, - "globalThis.console.log(arg0_0, arg1_0)", - &[], - ), - ), - false, - ), - wire::WireImport::new( - "web_sys", - "console.error", - &[wire::WireImportInput::new("arg0", 1usize)], - ::core::option::Option::None, - ::core::option::Option::Some( - wire::WireImportBinding::new( - ::core::option::Option::None, - "globalThis.console.error(arg0_0)", - &[], - ), - ), - false, - ), - wire::WireImport::new( - "web_sys", - "console.error1", - &[wire::WireImportInput::new("arg0", 1usize)], - ::core::option::Option::Some(wire::WireImportOutput::new(0usize)), - ::core::option::Option::Some( - wire::WireImportBinding::new( - ::core::option::Option::None, - "globalThis.console.error1(arg0_0)", - &[], - ), - ), - false, - ), - ], - ); - const _LEN: ::core::primitive::usize = wire::wire_blob_len(&_WIRE); - - #[used] - #[unsafe(link_section = "js_bindgen.wire")] - static _WIRE_SECTION: wire::WireBlob<_LEN> = wire::WireBlob::new(&_WIRE); -}; diff --git a/client/web-sys/src/console.js-sys.rs b/client/web-sys/src/console.rs similarity index 88% rename from client/web-sys/src/console.js-sys.rs rename to client/web-sys/src/console.rs index 97ea2252..ff3fd5e0 100644 --- a/client/web-sys/src/console.js-sys.rs +++ b/client/web-sys/src/console.rs @@ -1,5 +1,5 @@ -use js_sys::JsValue; use js_sys::hazard::JsCast; +use js_sys::{JsValue, js_sys}; #[js_sys(namespace = "console")] extern "js-sys" { @@ -13,5 +13,6 @@ extern "js-sys" { pub fn error(data: &JsValue); + #[must_use] pub fn error1(data: &JsValue) -> u128; } diff --git a/client/web-sys/src/lib.rs b/client/web-sys/src/lib.rs index fd8ddd60..75faf108 100644 --- a/client/web-sys/src/lib.rs +++ b/client/web-sys/src/lib.rs @@ -1,7 +1,5 @@ #![no_std] -#[rustfmt::skip] -#[path ="console.gen.rs"] pub mod console; pub use js_sys; diff --git a/host/Cargo.toml b/host/Cargo.toml index 019490e3..c754ee53 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "3" members = [ - "cargo-js-sys", "cargo-shim", "cli", "cli-lib", @@ -37,14 +36,12 @@ include = [ ] [workspace.dependencies] -annotate-snippets = { version = "0.12", default-features = false } anstyle = { version = "1", default-features = false } anyhow = "1" argfile = { version = "1", features = ["response"] } axum = { version = "0.8", default-features = false, features = ["http1", "http2", "json", "tokio"] } cargo_metadata = "0.23" clap = { version = "4", features = ["derive"] } -clap-cargo = { version = "0.18", features = ["cargo_metadata"] } dtor = { version = "1", default-features = false, features = ["proc_macro"] } fantoccini = { version = "0.22", default-features = false, features = ["rustls-tls"] } foldhash = { version = "0.2", default-features = false } diff --git a/host/cargo-js-sys/Cargo.toml b/host/cargo-js-sys/Cargo.toml deleted file mode 100644 index bf04187b..00000000 --- a/host/cargo-js-sys/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "cargo-js-sys" -version = "0.1.0" -edition = { workspace = true } -rust-version = "1.91" -license = { workspace = true } -include = { workspace = true } - -[package.metadata.dev] -require-feature = true - -[[bin]] -bench = false -name = "cargo-js-sys" -test = false - -[dependencies] -annotate-snippets = { workspace = true, optional = true } -anstyle = { workspace = true } -anyhow = { workspace = true } -cargo_metadata = { workspace = true } -clap = { workspace = true } -clap-cargo = { workspace = true } -js-bindgen-shared = { workspace = true, features = ["memmap"] } -js-sys-bindgen = { workspace = true } -prettyplease = { workspace = true } -proc-macro2 = { workspace = true, features = ["span-locations"] } -similar-asserts = { workspace = true } - -[features] -default = ["js-sys"] -js-sys = ["dep:annotate-snippets", "js-sys-bindgen/file"] - -[lints] -workspace = true diff --git a/host/cargo-js-sys/src/js_sys.rs b/host/cargo-js-sys/src/js_sys.rs deleted file mode 100644 index 57ce436f..00000000 --- a/host/cargo-js-sys/src/js_sys.rs +++ /dev/null @@ -1,370 +0,0 @@ -use std::ops::{ControlFlow, Deref}; -use std::path::Path; -use std::str::FromStr; -use std::{fs, process, str}; - -use annotate_snippets::renderer::DecorStyle; -use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet}; -use anstyle::{AnsiColor, Style}; -use anyhow::Result; -use clap::Args; -use clap_cargo::{Manifest, Workspace}; -use js_bindgen_shared::ReadFile; -use js_sys_bindgen::syn::{self, Error, parse_quote}; -use similar_asserts::SimpleDiff; - -use crate::GlobalArgs; - -#[derive(Args)] -pub(crate) struct JsSys { - #[command(flatten)] - manifest: Manifest, - #[command(flatten)] - workspace: Workspace, - path: Option, -} - -#[derive(Clone)] -struct PathWrapper(String); - -impl JsSys { - pub(crate) fn run(self, global_args: GlobalArgs) -> Result<()> { - let mut summary = Summary::new(); - let mut success = true; - - let metadata = self.manifest.metadata(); - let metadata = metadata.exec()?; - let (packages, _) = self.workspace.partition_packages(&metadata); - let num_packages = packages.len(); - - for package in packages { - let js_sys: Option = - if let Some(path) = self.path.as_ref().map(PathWrapper::path) { - Some(path) - } else if package.name == "js-sys" { - Some(parse_quote!(crate)) - } else if let Some(package) = package - .dependencies - .iter() - .find(|dependency| dependency.name == "js-sys") - { - Some( - syn::parse_str( - &package - .rename - .as_ref() - .unwrap_or(&package.name) - .replace('-', "_"), - ) - .unwrap(), - ) - } else if let Some(package) = package - .dependencies - .iter() - .find(|dependency| dependency.name == "web-sys") - { - let web_sys = package - .rename - .as_ref() - .unwrap_or(&package.name) - .replace('-', "_"); - Some(syn::parse_str(&format!("{web_sys}::js_sys")).unwrap()) - } else { - None - }; - - let crate_ = package.name.replace('-', "_"); - - let base = if num_packages > 1 { - metadata.workspace_root.as_std_path() - } else { - package - .manifest_path - .parent() - .expect("package manifest should be in a directory") - .as_std_path() - }; - - for target in package - .targets - .iter() - .filter(|target| !target.is_custom_build()) - { - let dir = target - .src_path - .parent() - .expect("target source file should be in a directory") - .as_std_path(); - - let mut state = State { - summary: &mut summary, - base, - global_args, - package: &package.name, - crate_: &crate_, - js_sys: js_sys.as_ref(), - }; - - match state.process(dir)? { - ControlFlow::Continue(value) => success &= value, - ControlFlow::Break(()) => { - success = false; - break; - } - } - } - } - - if !global_args.quiet { - println!(); - - let style = Style::new().bold(); - println!( - "{style}{:>9}:{style:#} Total {}, {} {}, Unchanged {}, Errors {}", - "Summary", - summary.generated + summary.unchanged + summary.errors, - if global_args.check { - "Checked" - } else if global_args.dry_run { - "Planned" - } else { - "Generated" - }, - summary.generated, - summary.unchanged, - summary.errors - ); - } - - if !success { - process::exit(1); - } - - Ok(()) - } -} - -struct State<'a> { - summary: &'a mut Summary, - base: &'a Path, - global_args: GlobalArgs, - package: &'a str, - crate_: &'a str, - js_sys: Option<&'a syn::Path>, -} - -struct Summary { - generated: usize, - unchanged: usize, - errors: usize, -} - -impl Summary { - fn new() -> Self { - Self { - generated: 0, - unchanged: 0, - errors: 0, - } - } -} - -impl State<'_> { - fn process(&mut self, dir: &Path) -> Result> { - let mut success = true; - - for entry in fs::read_dir(dir)? { - let entry = entry?.path(); - let relative_entry = entry.strip_prefix(self.base).unwrap_or(&entry); - - if entry.is_file() - && let Some(file) = entry.file_name() - && file.as_encoded_bytes().ends_with(b".js-sys.rs") - { - let Some(js_sys) = self.js_sys else { - let style = Style::new().bold().fg_color(Some(AnsiColor::Red.into())); - eprintln!( - "{style}Error:{style:#} can't find `js-sys` in dependencies for `{}`, \ - provide it manually via `--path`", - self.package - ); - return Ok(ControlFlow::Break(())); - }; - let Some(output) = self.generate(js_sys, &entry, relative_entry)? else { - success = false; - continue; - }; - - success &= self.output(&entry, relative_entry, &output)?; - } else if entry.is_dir() { - match self.process(&entry)? { - ControlFlow::Continue(value) => success &= value, - ControlFlow::Break(()) => return Ok(ControlFlow::Break(())), - } - } - } - - Ok(ControlFlow::Continue(success)) - } - - fn generate( - &mut self, - js_sys: &syn::Path, - entry: &Path, - relative_entry: &Path, - ) -> Result> { - let input = ReadFile::new(entry)?; - let input = str::from_utf8(&input)?; - let output = match js_sys_bindgen::file(input, self.crate_, Some(js_sys.clone())) { - Ok(output) => output, - Err(error) => { - let path = relative_entry.to_string_lossy(); - let style = Style::new().bold().fg_color(Some(AnsiColor::Red.into())); - - let errors: Vec<_> = error - .into_iter() - .map(|error| { - Level::ERROR - .no_name() - .secondary_title(format!("{style}{:>9}:{style:#} {error}", "Error")) - .element( - Snippet::source(input) - .line_start(error.span().start().line) - .path(&path) - .annotation( - AnnotationKind::Primary.span(error.span().byte_range()), - ), - ) - }) - .collect(); - - let output = Renderer::styled() - .decor_style(DecorStyle::Unicode) - .render(&errors); - eprintln!("{output}"); - - self.summary.errors += 1; - - return Ok(None); - } - }; - - Ok(Some(prettyplease::unparse(&output))) - } - - fn output(&mut self, entry: &Path, relative_entry: &Path, output: &str) -> Result { - let output_file = entry.with_extension("").with_extension("gen.rs"); - let relative_output_file = output_file.strip_prefix(self.base).unwrap_or(&output_file); - let exists = output_file.exists(); - - if exists && !output_file.is_file() { - let style = Style::new().bold().fg_color(Some(AnsiColor::Red.into())); - eprintln!( - "{style}{:>9}:{style:#} output file exists but is not a file: {}", - "Error", - relative_output_file.display() - ); - - self.summary.errors += 1; - - return Ok(false); - } - - let current = exists.then(|| ReadFile::new(&output_file)).transpose()?; - let current = if let Some(current) = ¤t { - match str::from_utf8(current.deref()) { - Ok(current) => Some(current), - Err(error) => { - let style = Style::new().bold().fg_color(Some(AnsiColor::Red.into())); - eprintln!( - "{style}{:>9}:{style:#} output file exists but is not UTF-8: {}\n\t{error}", - "Error", - relative_output_file.display(), - ); - - self.summary.errors += 1; - - return Ok(false); - } - } - } else { - None - }; - - let feedback = |color: AnsiColor, text: &str| { - let style = Style::new().fg_color(Some(color.into())); - println!( - "{style}{:>9}:{style:#} {} -> {}", - text, - relative_entry.display(), - relative_output_file.display() - ); - }; - - if self.global_args.check { - let Some(current) = current else { - feedback(AnsiColor::Red, "Missing"); - self.summary.errors += 1; - return Ok(false); - }; - - if current == output { - if self.global_args.verbose { - feedback(AnsiColor::Green, "Checked"); - } - - self.summary.generated += 1; - Ok(true) - } else { - feedback(AnsiColor::Red, "Different"); - eprintln!( - "{}", - SimpleDiff::from_str(current, output, "current", "expected") - ); - - self.summary.errors += 1; - Ok(false) - } - } else if current.is_none_or(|current| current != output) { - if !self.global_args.dry_run { - fs::write(&output_file, output)?; - } - - if !self.global_args.quiet || self.global_args.check { - feedback( - AnsiColor::Green, - if self.global_args.dry_run { - "Planned" - } else { - "Generated" - }, - ); - } - - self.summary.generated += 1; - Ok(true) - } else { - if self.global_args.verbose { - feedback(AnsiColor::BrightBlack, "Unchanged"); - } - - self.summary.unchanged += 1; - Ok(true) - } - } -} - -impl FromStr for PathWrapper { - type Err = Error; - - fn from_str(s: &str) -> Result { - syn::parse_str::(s)?; - Ok(Self(s.to_owned())) - } -} - -impl PathWrapper { - fn path(&self) -> syn::Path { - syn::parse_str::(&self.0).unwrap() - } -} diff --git a/host/cargo-js-sys/src/main.rs b/host/cargo-js-sys/src/main.rs deleted file mode 100644 index 945f07b0..00000000 --- a/host/cargo-js-sys/src/main.rs +++ /dev/null @@ -1,57 +0,0 @@ -#[cfg(feature = "js-sys")] -mod js_sys; - -use anyhow::Result; -use clap::builder::ArgPredicate; -use clap::{Args, Parser, Subcommand}; -use clap_cargo::style::CLAP_STYLING; - -#[cfg(feature = "js-sys")] -use crate::js_sys::JsSys; - -#[cfg(not(any(feature = "js-sys")))] -compile_error!("pick at least one crate feature"); - -#[derive(Parser)] -#[command(name = "cargo", bin_name = "cargo", version, about, long_about = None, styles = CLAP_STYLING)] -struct Cli { - #[command(flatten)] - global_args: GlobalArgs, - #[command(subcommand)] - commands: Commands, -} - -#[derive(Args, Clone, Copy)] -struct GlobalArgs { - #[arg(global = true, short, long, conflicts_with = "verbose")] - quiet: bool, - #[arg(global = true, short, long)] - verbose: bool, - #[arg( - global = true, - short = 'n', - long, - conflicts_with = "check", - default_value_if("check", ArgPredicate::IsPresent, Some("true")) - )] - dry_run: bool, - #[arg(global = true, short = 'c', long)] - check: bool, -} - -#[derive(Subcommand)] -enum Commands { - #[cfg(feature = "js-sys")] - JsSys(JsSys), -} - -fn main() -> Result<()> { - let cli = Cli::parse(); - - match cli.commands { - #[cfg(feature = "js-sys")] - Commands::JsSys(js_sys) => js_sys.run(cli.global_args)?, - } - - Ok(()) -} diff --git a/host/dev/src/check.rs b/host/dev/src/check.rs index 9c563eda..ecc46bc4 100644 --- a/host/dev/src/check.rs +++ b/host/dev/src/check.rs @@ -9,7 +9,7 @@ use clap::builder::PossibleValue; use clap::{Args, ValueEnum}; use strum::{EnumIter, IntoEnumIterator}; -use crate::client::{self, Client, ClientTool}; +use crate::client::Client; use crate::command; use crate::host::{self, Host, HostTool}; @@ -26,7 +26,6 @@ enum_with_all!(enum Tools, Tool(Tool), "tools"); #[derive(Clone, Copy, Eq, PartialEq)] enum Tool { Shared(CheckTool), - Client(ClientTool), Host(HostTool), Zizmor, } @@ -75,14 +74,13 @@ impl Check { match tool { Tool::Shared(tool) => match tool { CheckTool::Clippy | CheckTool::RustSec => { - client_tools.push(client::Tool::Shared(tool)); + client_tools.push(tool); host_tools.push(host::Tool::Shared(tool)); } CheckTool::Tombi => root_tools.push(RootTool::Tombi), CheckTool::CargoSpellcheck => root_tools.push(RootTool::CargoSpellcheck), CheckTool::Typos => root_tools.push(RootTool::Typos), }, - Tool::Client(tool) => client_tools.push(client::Tool::Client(tool)), Tool::Host(tool) => host_tools.push(host::Tool::Host(tool)), Tool::Zizmor => root_tools.push(RootTool::Zizmor), } @@ -150,7 +148,6 @@ impl ValueEnum for Tool { static VALUES: LazyLock> = LazyLock::new(|| { CheckTool::iter() .map(Tool::Shared) - .chain(ClientTool::iter().map(Tool::Client)) .chain(HostTool::iter().map(Tool::Host)) .chain(iter::once(Tool::Zizmor)) .collect() @@ -162,7 +159,6 @@ impl ValueEnum for Tool { fn to_possible_value(&self) -> Option { match self { Self::Shared(tool) => tool.to_possible_value(), - Self::Client(tool) => tool.to_possible_value(), Self::Host(tool) => tool.to_possible_value(), Self::Zizmor => Some(PossibleValue::new("zizmor")), } diff --git a/host/dev/src/client/check.rs b/host/dev/src/client/check.rs index 9a9978f2..209d03d8 100644 --- a/host/dev/src/client/check.rs +++ b/host/dev/src/client/check.rs @@ -1,14 +1,8 @@ -use std::env; -use std::iter::Copied; use std::process::Command; -use std::slice::Iter; -use std::sync::LazyLock; use std::time::Instant; use anyhow::Result; -use clap::builder::PossibleValue; -use clap::{Args, ValueEnum}; -use strum::{EnumIter, IntoEnumIterator}; +use clap::Args; use super::permutation::Profile; use super::{ClientArgs, metadata}; @@ -25,16 +19,7 @@ pub struct Check { enum_with_all!(pub enum Tools, Tool(Tool), "tools"); -#[derive(Clone, Copy, Eq, PartialEq)] -pub enum Tool { - Shared(CheckTool), - Client(ClientTool), -} - -#[derive(Clone, Copy, EnumIter, Eq, PartialEq, ValueEnum)] -pub enum ClientTool { - CargoJsSys, -} +pub type Tool = CheckTool; impl Default for Check { fn default() -> Self { @@ -51,12 +36,12 @@ impl Check { } pub fn execute(self, verbose: bool) -> Result<()> { - let tools = Tool::from_tools(self.tools)?; + let tools = CheckTool::from_tools(self.tools)?; let start = Instant::now(); for tool in tools { match tool { - Tool::Shared(CheckTool::Clippy) => { + CheckTool::Clippy => { let commands = [ CargoCommand { title: "Check", @@ -79,50 +64,26 @@ impl Check { ]; metadata::run(self.args.clone(), &commands, Profile::Dev, verbose)?; } - Tool::Client(ClientTool::CargoJsSys) => { - let mut command = - if env::var_os("JBG_DEV_TOOLS").is_some_and(|value| value == "1") { - Command::new("cargo-js-sys") - } else { - let mut command = Command::new("cargo"); - command.arg("build").args(["-p", "cargo-js-sys"]); - - command::run("Build `cargo-js-sys`", command, verbose)?; - - let mut command = Command::new("cargo"); - command.arg("run").args(["-p", "cargo-js-sys"]).arg("--"); - command - }; - - command - .arg("js-sys") - .args(["--manifest-path", "../client/Cargo.toml"]) - .arg("--workspace") - .arg("-c") - .arg("-v"); - - command::run("Check `cargo-js-sys`", command, verbose)?; - } - Tool::Shared(CheckTool::RustSec) => { + CheckTool::RustSec => { let mut command = Command::new("cargo"); command.current_dir("../client").arg("audit"); command::run("RustSec", command, verbose)?; } - Tool::Shared(CheckTool::Tombi) => { + CheckTool::Tombi => { let mut command = Command::new("tombi"); command .current_dir("../client") .args(["lint", "--error-on-warnings", "."]); command::run("Tombi Lint", command, verbose)?; } - Tool::Shared(CheckTool::CargoSpellcheck) => { + CheckTool::CargoSpellcheck => { let mut command = Command::new("cargo"); command .current_dir("../client") .args(["spellcheck", "-m", "1"]); command::run("`cargo-spellcheck`", command, verbose)?; } - Tool::Shared(CheckTool::Typos) => { + CheckTool::Typos => { let mut command = Command::new("typos"); command.current_dir("../client"); command::run("Typos", command, verbose)?; @@ -136,37 +97,3 @@ impl Check { Ok(()) } } - -impl Default for Tool { - fn default() -> Self { - Self::Shared(CheckTool::default()) - } -} - -impl IntoEnumIterator for Tool { - type Iterator = Copied>; - - fn iter() -> Self::Iterator { - Self::value_variants().iter().copied() - } -} - -impl ValueEnum for Tool { - fn value_variants<'a>() -> &'a [Self] { - static VALUES: LazyLock> = LazyLock::new(|| { - CheckTool::iter() - .map(Tool::Shared) - .chain(ClientTool::iter().map(Tool::Client)) - .collect() - }); - - &VALUES - } - - fn to_possible_value(&self) -> Option { - match self { - Self::Shared(tool) => tool.to_possible_value(), - Self::Client(tool) => tool.to_possible_value(), - } - } -} diff --git a/host/dev/src/client/mod.rs b/host/dev/src/client/mod.rs index 2c84b7e8..65e334e1 100644 --- a/host/dev/src/client/mod.rs +++ b/host/dev/src/client/mod.rs @@ -13,8 +13,8 @@ use anyhow::Result; use clap::{Args, Subcommand, ValueEnum}; use strum::EnumIter; +pub use self::check::Tool; use self::check::{Check, Tools}; -pub use self::check::{ClientTool, Tool}; use self::fmt::Fmt; use self::permutation::{Profile, Toolchain}; use self::test::Test; diff --git a/host/js-sys-bindgen/Cargo.toml b/host/js-sys-bindgen/Cargo.toml index 3765004d..72959dca 100644 --- a/host/js-sys-bindgen/Cargo.toml +++ b/host/js-sys-bindgen/Cargo.toml @@ -30,7 +30,6 @@ inline-snap = { workspace = true } prettyplease = { workspace = true } [features] -file = ["dep:foldhash", "dep:hashbrown"] web-idl = ["dep:foldhash", "dep:hashbrown", "dep:weedle2"] [lints] diff --git a/host/js-sys-bindgen/src/file.rs b/host/js-sys-bindgen/src/file.rs deleted file mode 100644 index b2b92826..00000000 --- a/host/js-sys-bindgen/src/file.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::mem; - -use proc_macro2::TokenStream; -use syn::{Error, File, Item, ItemMod, Meta, Path, Result, parse_quote}; - -use crate::hygiene::ImportManager; -use crate::r#macro::{self, ErrorStack}; - -pub fn file(input: &str, crate_: &str, js_sys: Option) -> Result { - let mut file: File = syn::parse_str(input)?; - let mut imports = ImportManager::new(js_sys); - let mut error = ErrorStack::new(); - process_items( - mem::take(&mut file.items), - &mut file.items, - crate_, - &mut imports, - &mut error, - ); - - file.items = imports.iter().map(Item::from).chain(file.items).collect(); - - file.attrs = [ - parse_quote!(#![doc = " This file was generated by `js-sys-bindgen`."]), - parse_quote!(#![allow(warnings)]), - ] - .into_iter() - .chain(file.attrs) - .collect(); - - if let Some(error) = error.resolve() { - Err(error) - } else { - Ok(file) - } -} - -fn process_items( - items: Vec, - output: &mut Vec, - crate_: &str, - imports: &mut ImportManager, - error: &mut ErrorStack, -) { - for item in items { - match item { - item @ (Item::ExternCrate(_) | Item::Use(_)) => output.push(item), - Item::ForeignMod(mut foreign_mod) => { - let js_sys = foreign_mod - .attrs - .extract_if(.., |attr| attr.path().is_ident("js_sys")) - .next(); - - if let Some(js_sys) = js_sys { - let attr = match js_sys.meta { - Meta::Path(_) => TokenStream::new(), - Meta::List(list) => list.tokens, - Meta::NameValue(name_value) => { - error.push(Error::new_spanned( - name_value, - "found unsupported `js_sys` attribute syntax", - )); - continue; - } - }; - - match r#macro::expand_file(attr, foreign_mod, crate_, imports) { - Ok(items) => match items.into_items() { - Ok(mut items) => output.append(&mut items), - Err(e) => error.push(e), - }, - Err((_, e)) => { - error.push(e); - } - } - } else { - error.push(Error::new_spanned( - foreign_mod, - "`js_sys` attribute not found", - )); - } - } - Item::Mod( - mut r#mod @ ItemMod { - content: Some(_), .. - }, - ) => { - let items = &mut r#mod.content.as_mut().unwrap().1; - process_items(mem::take(items), items, crate_, imports, error); - output.push(r#mod.into()); - } - item => error.push(Error::new_spanned(item, "item not supported")), - } - } -} diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index d461c6a2..ef22bcbc 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -209,7 +209,7 @@ pub(crate) fn expand( } else if output_ty.is_some() { quote_spanned!(span=> #macro_path::join_output(#foreign_call)) } else { - quote_spanned!(span=> #foreign_call;) + foreign_call }; let item_fn = quote_spanned! {span=> diff --git a/host/js-sys-bindgen/src/hygiene.rs b/host/js-sys-bindgen/src/hygiene.rs index 27d850dc..71fb5b54 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -1,24 +1,24 @@ -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] use foldhash::fast::FixedState; -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] use hashbrown::{HashMap, HashSet}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; use syn::{Attribute, Ident, Path, parse_quote_spanned}; -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] use syn::{ItemUse, parse_quote}; pub(crate) enum Hygiene<'a> { - /// File generation mode: emit short paths and record the required `use` + /// Source generation mode: emit short paths and record the required `use` /// items. - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Imports(&'a mut ImportManager), /// Procedural macro mode: emit paths qualified through the selected crate. Qualified { js_sys: Option<&'a Path> }, } #[cfg_attr( - not(any(feature = "file", feature = "web-idl")), + not(feature = "web-idl"), expect( unused_variables, reason = "attributes are only consumed by source-generation hygiene" @@ -43,7 +43,7 @@ impl Hygiene<'_> { fn js_sys_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { imports.js_sys_push(attrs, ident.clone()); parse_quote_spanned!(span=> #ident) @@ -56,7 +56,7 @@ impl Hygiene<'_> { fn hazard_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { imports.hazard_push(attrs, ident.clone()); parse_quote_spanned!(span=> #ident) @@ -69,7 +69,7 @@ impl Hygiene<'_> { pub(crate) fn as_ref(&mut self, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Hygiene::Imports(_) => { parse_quote_spanned!(span=> AsRef) } @@ -81,7 +81,7 @@ impl Hygiene<'_> { pub(crate) fn deref(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { imports.deref.insert(attrs.to_vec()); parse_quote_spanned!(span=> Deref) @@ -94,7 +94,7 @@ impl Hygiene<'_> { pub(crate) fn phantom_data(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { imports .phantom_data @@ -109,7 +109,7 @@ impl Hygiene<'_> { pub(crate) fn from(&mut self, span: Span) -> Path { match self { - #[cfg(any(feature = "file", feature = "web-idl"))] + #[cfg(feature = "web-idl")] Hygiene::Imports(_) => { parse_quote_spanned!(span=> From) } @@ -128,12 +128,12 @@ impl Hygiene<'_> { } } -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] type FixedHashMap = HashMap; -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] type FixedHashSet = HashSet; -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] pub(crate) struct ImportManager { js_sys: Path, deref: FixedHashSet>, @@ -142,7 +142,7 @@ pub(crate) struct ImportManager { hazard_imports: FixedHashMap, FixedHashSet>, } -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] impl ImportManager { #[must_use] pub(crate) fn new(js_sys: Option) -> Self { @@ -207,7 +207,7 @@ impl ImportManager { } } -#[cfg(any(feature = "file", feature = "web-idl"))] +#[cfg(feature = "web-idl")] impl ToTokens for ImportManager { fn to_tokens(&self, tokens: &mut TokenStream) { for item_use in self.iter() { diff --git a/host/js-sys-bindgen/src/lib.rs b/host/js-sys-bindgen/src/lib.rs index ba4d63e9..76040b24 100644 --- a/host/js-sys-bindgen/src/lib.rs +++ b/host/js-sys-bindgen/src/lib.rs @@ -1,7 +1,5 @@ mod closure; mod export; -#[cfg(feature = "file")] -mod file; mod function; mod hygiene; mod r#macro; @@ -14,8 +12,6 @@ mod web_idl; pub use syn; pub use crate::closure::closure; -#[cfg(feature = "file")] -pub use crate::file::file; pub use crate::r#macro::r#macro; #[cfg(feature = "web-idl")] pub use crate::web_idl::web_idl; diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index be8f7608..9708b14c 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -3,15 +3,13 @@ use std::env; use proc_macro2::TokenStream; use quote::ToTokens; -#[cfg(any(feature = "file", test))] +#[cfg(test)] use syn::File; use syn::parse::Parser; use syn::{Attribute, Error, ForeignItem, Item, ItemForeignMod, LitStr, Path, meta}; use crate::function::{FunctionImport, expand}; use crate::hygiene::Hygiene; -#[cfg(feature = "file")] -use crate::hygiene::ImportManager; use crate::r#type::{Type, TypeOptions}; pub fn r#macro(attr: TokenStream, item: TokenStream) -> Result { @@ -42,24 +40,6 @@ pub fn r#macro(attr: TokenStream, item: TokenStream) -> Result Result, Error)> { - let (_, namespace, error) = parse_block_options(attr, false); - - expand_foreign_mod( - foreign_mod, - crate_name, - namespace.as_deref(), - Hygiene::Imports(imports), - error, - ) -} - #[cfg(test)] pub(crate) fn expand_for_test( attr: TokenStream, @@ -74,7 +54,7 @@ fn expand_proc_macro( foreign_mod: ItemForeignMod, crate_name: &str, ) -> Result, Error)> { - let (js_sys, namespace, error) = parse_block_options(attr, true); + let (js_sys, namespace, error) = parse_block_options(attr); expand_foreign_mod( foreign_mod, @@ -87,10 +67,7 @@ fn expand_proc_macro( ) } -fn parse_block_options( - attr: TokenStream, - allow_js_sys_path: bool, -) -> (Option, Option, ErrorStack) { +fn parse_block_options(attr: TokenStream) -> (Option, Option, ErrorStack) { let mut error = ErrorStack::new(); let mut js_sys: Option = None; let mut namespace: Option = None; @@ -99,9 +76,7 @@ fn parse_block_options( if meta.path.is_ident("js_sys") { // The block-level `js_sys` option selects the crate path used by // every generated item in this foreign module. - if !allow_js_sys_path { - Err(meta.error("`js_sys` attribute only allowed with proc-macro hygiene")) - } else if js_sys.is_some() { + if js_sys.is_some() { Err(meta.error("duplicate attribute")) } else { js_sys = Some(meta.value()?.parse()?); @@ -227,7 +202,7 @@ impl GeneratedItems { self.0 } - #[cfg(any(feature = "file", test))] + #[cfg(test)] pub(crate) fn into_items(self) -> Result, Error> { Ok(syn::parse2::(self.0)?.items) } From 58be4c2b7cbf97eccdf8d4bb2d67cc9549283449 Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 15:08:00 +0800 Subject: [PATCH 19/21] Finalize Wire lowering and integration tests --- .github/workflows/ci.yaml | 2 +- client/e2e/examples/closure.rs | 13 + client/js-sys/src/wire/closure.rs | 43 ++ client/js-sys/src/wire/export.rs | 79 ++-- client/js-sys/src/wire/mod.rs | 10 +- client/js-sys/tests/builtins.rs | 45 ++ host/js-sys-bindgen/src/closure.rs | 125 +++--- host/js-sys-bindgen/src/export.rs | 3 +- host/js-sys-bindgen/src/function.rs | 141 ++----- host/js-sys-bindgen/src/function/js.rs | 279 +++++++++---- host/js-sys-bindgen/src/macro.rs | 35 +- host/js-sys-bindgen/src/tests/closure.rs | 2 +- .../src/tests/macro/function.rs | 102 +++-- host/js-sys-bindgen/src/tests/macro/member.rs | 90 ++-- host/js-sys-bindgen/src/tests/macro/mod.rs | 8 - host/js-sys-bindgen/src/tests/macro/type.rs | 92 +++- host/ld/Cargo.toml | 1 + host/ld/src/args.rs | 2 +- host/ld/src/js.rs | 27 +- host/ld/src/pre.rs | 107 ++--- host/ld/src/wire/closure.rs | 72 ++++ host/ld/src/wire/export/js.rs | 292 ++++++------- host/ld/src/wire/export/mod.rs | 30 +- host/ld/src/wire/export/wat.rs | 269 ++++++------ host/ld/src/wire/import/js.rs | 393 ++++++++++++++---- host/ld/src/wire/import/mod.rs | 28 +- host/ld/src/wire/import/wat.rs | 217 +++++----- host/ld/src/wire/js.rs | 40 +- host/ld/src/wire/mod.rs | 36 +- host/ld/src/wire/wat.rs | 69 ++- host/wire/src/decode/closure.rs | 121 ++++++ host/wire/src/decode/export.rs | 93 ++--- host/wire/src/decode/import.rs | 123 ++++-- host/wire/src/decode/mod.rs | 14 +- host/wire/src/encode/closure.rs | 64 +++ host/wire/src/encode/export.rs | 144 ++++--- host/wire/src/encode/import.rs | 129 ++++-- host/wire/src/encode/mod.rs | 4 + host/wire/src/lib.rs | 41 +- host/wire/src/model.rs | 78 +++- host/wire/src/schema.rs | 288 ++++++++----- host/wire/src/tests.rs | 386 ++++++++++++----- 42 files changed, 2584 insertions(+), 1553 deletions(-) create mode 100644 client/js-sys/src/wire/closure.rs create mode 100644 client/js-sys/tests/builtins.rs create mode 100644 host/ld/src/wire/closure.rs create mode 100644 host/wire/src/decode/closure.rs create mode 100644 host/wire/src/encode/closure.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f172f9dd..d4de41d5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -257,7 +257,7 @@ jobs: runs-on: ${{ matrix.runner.os }} - timeout-minutes: 20 + timeout-minutes: 30 strategy: fail-fast: false diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs index 5d32895a..48c22a61 100644 --- a/client/e2e/examples/closure.rs +++ b/client/e2e/examples/closure.rs @@ -3,6 +3,7 @@ fn main() { // ;; exports["closure_i32"](20) === 43 // ;; exports["closure_same_signature"](20) === 4280 // ;; exports["closure_macro_repetition"](20) === 84 + // ;; exports["closure_macro_mixed_signatures"]() // ;; exports["closure_u128"](1n << 96n) === (1n << 96n) + 1n // ;; (() => { const value = {}; return exports["closure_js_value"](value) === value })() // ;; exports["closure_js_string_ref"]("closure") === "closure" @@ -37,6 +38,12 @@ macro_rules! repeated_closures { }; } +macro_rules! repeated_typed_closures { + ($($ty:ty),+ $(,)?) => { + ($(closure!(dyn FnMut($ty) -> $ty, |value| value)),+) + }; +} + impl Drop for DropCounter { fn drop(&mut self) { DROPS.fetch_add(1, Ordering::Relaxed); @@ -212,6 +219,12 @@ fn closure_macro_repetition(value: i32) -> i32 { invoke_i32_twice(&first, value) + invoke_i32_twice(&second, value) } +#[js_sys] +fn closure_macro_mixed_signatures() -> bool { + let _ = repeated_typed_closures!(i32, u128); + true +} + #[js_sys] fn closure_u128(value: u128) -> u128 { let callback = closure!(dyn FnMut(u128) -> u128, move |value| value + 1); diff --git a/client/js-sys/src/wire/closure.rs b/client/js-sys/src/wire/closure.rs new file mode 100644 index 00000000..1f7e4513 --- /dev/null +++ b/client/js-sys/src/wire/closure.rs @@ -0,0 +1,43 @@ +use crate::runtime::JsValue; + +use super::{ + JsEmbed, Wire, WireClosure, WireClosureFactory, WireExportInput, WireExportOutput, + wire_import_input_type, wire_import_output_type, +}; + +/// The JavaScript lifetime and invocation semantics of a Rust closure. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub enum ClosureKind { + Shared, + Mutable, + Once, +} + +/// Builds the wire description for a generated closure. +#[doc(hidden)] +#[must_use] +pub const fn wire_closure( + raw_symbol: &'static str, + kind: ClosureKind, + call_shim_offset: usize, + inputs: &'static [WireExportInput], + output: Option, +) -> Wire { + let helper = match kind { + ClosureKind::Shared => JsEmbed::new("js_sys", "closure.make"), + ClosureKind::Mutable => JsEmbed::new("js_sys", "closure.make_mut"), + ClosureKind::Once => JsEmbed::new("js_sys", "closure.make_once"), + }; + Wire::closure(WireClosure::new( + WireClosureFactory::new( + raw_symbol, + helper, + wire_import_input_type::(), + wire_import_output_type::(), + ), + call_shim_offset, + inputs, + output, + )) +} diff --git a/client/js-sys/src/wire/export.rs b/client/js-sys/src/wire/export.rs index 6011d340..0f61814a 100644 --- a/client/js-sys/src/wire/export.rs +++ b/client/js-sys/src/wire/export.rs @@ -1,7 +1,6 @@ -use crate::ClosureHeader; -use crate::hazard::{FromJS, ReturnAbi, ReturnIntoJS, WasmRet}; +use crate::hazard::{FromJS, ReturnAbi, ReturnConv, ReturnIntoJS, ReturnMode, WasmRet}; use crate::wire::{ - WireExport, WireExportInput, WireExportInputType, WireExportOutput, WireExportOutputType, + WireExportInput, WireExportInputType, WireExportOutput, WireExportOutputType, WireReturnFrame, from_js_slots, into_js_slots, }; @@ -16,40 +15,42 @@ impl MetadataFor for WireExportInputType { impl MetadataFor for WireExportOutputType { const VALUE: &'static Self = &{ let mode = ::MODE; - let result_layout = ::RESULT_LAYOUT; let slots = into_js_slots::(); - let (frame_size, slot_offsets) = if mode.is_direct() { - (0, [0; 4]) - } else { - // LLVM keeps the Wasm stack pointer 16-byte aligned. - let size = core::mem::size_of::>(); - ( - (size + 15) & !15, - [ - WasmRet::::slot_offset::<0>(), - WasmRet::::slot_offset::<1>(), - WasmRet::::slot_offset::<2>(), - WasmRet::::slot_offset::<3>(), - ], - ) - }; - - Self::new( - mode, - T::JS_CONV, - slots, - frame_size, - slot_offsets, - result_layout, - ) + match mode { + ReturnMode::Direct => { + let ReturnConv::Value(conversion) = T::JS_CONV else { + panic!("a direct export cannot return Result"); + }; + Self::direct(slots, conversion) + } + ReturnMode::Indirect => { + // LLVM keeps the Wasm stack pointer 16-byte aligned. + let size = core::mem::size_of::>(); + let frame = WireReturnFrame::new( + (size + 15) & !15, + [ + WasmRet::::slot_offset::<0>(), + WasmRet::::slot_offset::<1>(), + WasmRet::::slot_offset::<2>(), + WasmRet::::slot_offset::<3>(), + ], + ); + Self::indirect( + slots, + T::JS_CONV, + frame, + ::RESULT_LAYOUT, + ) + } + } }; } /// Builds the wire descriptor for one exported argument. #[doc(hidden)] #[must_use] -pub const fn wire_export_input(name: &'static str) -> WireExportInput { - WireExportInput::new(name, >::VALUE) +pub const fn wire_export_input() -> WireExportInput { + WireExportInput::new(>::VALUE) } /// Builds the result reference for one JavaScript-facing Wasm export. @@ -58,21 +59,3 @@ pub const fn wire_export_input(name: &'static str) -> WireExportInput pub const fn wire_export_output() -> WireExportOutput { WireExportOutput::new(>::VALUE) } - -/// Builds a closure dispatcher export using the closure header's call shim. -#[doc(hidden)] -#[must_use] -pub const fn wire_closure_export( - module: &'static str, - name: &'static str, - inputs: &'static [WireExportInput], - output: Option, -) -> WireExport { - WireExport::new_closure( - module, - name, - ClosureHeader::call_shim_offset::(), - inputs, - output, - ) -} diff --git a/client/js-sys/src/wire/mod.rs b/client/js-sys/src/wire/mod.rs index 787bdc51..96fa6711 100644 --- a/client/js-sys/src/wire/mod.rs +++ b/client/js-sys/src/wire/mod.rs @@ -1,17 +1,19 @@ +mod closure; mod export; mod import; mod r#macro; use core::mem::MaybeUninit; +pub use closure::*; pub use export::*; pub use import::*; pub use js_bindgen_wire::abi::{JsCatch, JsEmbed, WatCatch}; pub use js_bindgen_wire::{ - Wire, WireBlob, WireExport, WireExportInput, WireExportInputType, WireExportOutput, - WireExportOutputType, WireImport, WireImportBinding, WireImportCatch, WireImportInput, - WireImportInputType, WireImportOutput, WireImportOutputType, WireImportTypeTable, - wire_blob_len, + Wire, WireBlob, WireClosure, WireClosureFactory, WireExport, WireExportInput, + WireExportInputType, WireExportOutput, WireExportOutputType, WireGlobalPath, WireImport, + WireImportBinding, WireImportCatch, WireImportInput, WireImportInputType, WireImportOutput, + WireImportOutputType, WireImportTypeTable, WireReturnFrame, wire_blob_len, }; pub use r#macro::*; diff --git a/client/js-sys/tests/builtins.rs b/client/js-sys/tests/builtins.rs new file mode 100644 index 00000000..edae3b9f --- /dev/null +++ b/client/js-sys/tests/builtins.rs @@ -0,0 +1,45 @@ +use js_bindgen_test::test; +use js_sys::{JsValue, js_sys}; + +js_bindgen::embed_js!( + module = "builtins", + name = "binding.install", + "() => {{", + " globalThis.JsSysBindingTest = class {{", + " constructor(...values) {{ this.length = values.length }}", + " }}", + " globalThis.JsSysBindingTest.value = 0", + "}}", +); + +#[js_sys] +extern "js-sys" { + #[js_sys(js_embed = "binding.install")] + fn install(); + + #[js_sys(js_name = "JsSysBindingTest")] + type TestBinding; + + #[js_sys(constructor, variadic)] + fn new(values: &[JsValue]) -> TestBinding; + + #[js_sys(static_of = TestBinding, getter = "value")] + fn static_value() -> i32; + + #[js_sys(static_of = TestBinding, setter = "value")] + fn set_static_value(value: i32); + + #[js_sys(getter)] + fn length(self: &TestBinding) -> u32; +} + +#[test] +fn binding_shapes() { + install(); + + TestBinding::set_static_value(42); + assert_eq!(TestBinding::static_value(), 42); + + let value = TestBinding::new(&[JsValue::NULL, JsValue::UNDEFINED]); + assert_eq!(value.length(), 2); +} diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index c7ccd983..3224633f 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -12,9 +12,6 @@ use syn::{ use xxhash_rust::xxh3::xxh3_128; use crate::export::{ExportAbi, lower_abi}; -use crate::function; -use crate::hygiene::Hygiene; -use crate::r#macro::render_import_groups; mod keyword { syn::custom_keyword!(js_sys); @@ -42,34 +39,33 @@ pub(crate) fn closure_with( let signature = Signature::parse(&trait_object)?; let span = trait_object.span(); let js_sys = js_sys.unwrap_or_else(|| parse_quote_spanned!(span=> ::js_sys)); - // The package identity is part of the descriptor, so the hash is deterministic - // and does not depend on macro expansion order or parallel compilation. - let symbol_id = closure_symbol_hash(crate_name, package_name, package_version, &trait_object); - let call_name_value = format!("closure_call_{symbol_id}"); - let call_name = syn::LitStr::new(&call_name_value, span); - let factory_ident = format_ident!("closure_new_{symbol_id}", span = span); - let factory_name = syn::LitStr::new(&format!("closure.new.{symbol_id}"), span); - let factory_embed = syn::LitStr::new(signature.kind.factory_embed(), span); - let closure = signature.closure_type(&trait_object); - let factory_js = syn::LitStr::new( - &format!( - "(data) => this.#jsEmbed.js_sys['{}'](data, this.#jsExports['{call_name_value}'])", - signature.kind.factory_embed(), - ), - span, + let trait_syntax_hash = format!( + "{:032x}", + xxh3_128(trait_object.to_token_stream().to_string().as_bytes()), ); - let factory_item = parse_quote_spanned! {span=> - #[js_sys(js_embed = #factory_name)] - fn #factory_ident( - data: ::core::primitive::usize, - ) -> #js_sys::JsValue; + let trait_syntax_hash = syn::LitStr::new(&trait_syntax_hash, span); + // This name only correlates the Rust factory call with this closure's raw + // linker import. The linker derives dispatcher identity from the semantic + // Wire `ABI` rather than from source tokens. + let factory_raw_symbol = quote_spanned! {span=> + ::core::concat!( + #crate_name, ".", + #package_name, "@", #package_version, ":", + ::core::module_path!(), ":", + ::core::line!(), ":", ::core::column!(), ":", + #trait_syntax_hash, + ) }; - let mut hygiene = Hygiene::Qualified { - js_sys: Some(&js_sys), + let wire_kind = match signature.kind { + ClosureKind::Shared => { + quote_spanned!(span=> #js_sys::wire::ClosureKind::Shared) + } + ClosureKind::Mutable => { + quote_spanned!(span=> #js_sys::wire::ClosureKind::Mutable) + } + ClosureKind::Once => quote_spanned!(span=> #js_sys::wire::ClosureKind::Once), }; - let (factory_function, factory_import) = - function::expand_closure_factory(&mut hygiene, crate_name, factory_item)?; - let factory_wire = render_import_groups(vec![factory_import]); + let closure = signature.closure_type(&trait_object); let inputs: Vec<_> = signature.inputs.iter().collect(); let output = signature.output.as_ref(); let ExportAbi { @@ -84,7 +80,7 @@ pub(crate) fn closure_with( wire_inputs.insert( 0, quote_spanned! {span=> - #js_sys::wire::wire_export_input::<::core::primitive::usize>("data") + #js_sys::wire::wire_export_input::<::core::primitive::usize>() }, ); let closure_bound = if signature.kind == ClosureKind::Shared { @@ -175,14 +171,13 @@ pub(crate) fn closure_with( const _: () = { const _WIRE: #js_sys::wire::Wire = - #js_sys::wire::Wire::exports(&[ - #js_sys::wire::wire_closure_export::( - #crate_name, - #call_name, - &[#(#wire_inputs),*], - #wire_output, - ), - ]); + #js_sys::wire::wire_closure( + #factory_raw_symbol, + #wire_kind, + #js_sys::ClosureHeader::call_shim_offset::(), + &[#(#wire_inputs),*], + #wire_output, + ); const _LEN: ::core::primitive::usize = #js_sys::wire::wire_blob_len(&_WIRE); #[unsafe(link_section = "js_bindgen.wire")] @@ -190,18 +185,24 @@ pub(crate) fn closure_with( #js_sys::wire::WireBlob::new(&_WIRE); }; - #js_sys::js_bindgen::embed_js! { - module = #crate_name, - name = #factory_name, - required_embeds = [("js_sys", #factory_embed)], - #factory_js, - } - - #factory_function - #factory_wire - let allocation = allocate(#expression); - let value = #factory_ident(allocation.data()); + let value = { + unsafe extern "C" { + #[link_name = #factory_raw_symbol] + fn __import_( + arg0_0: #js_sys::wire::InputSlot1<::core::primitive::usize>, + arg0_1: #js_sys::wire::InputSlot2<::core::primitive::usize>, + arg0_2: #js_sys::wire::InputSlot3<::core::primitive::usize>, + arg0_3: #js_sys::wire::InputSlot4<::core::primitive::usize>, + ) -> #js_sys::wire::OutputRet<#js_sys::JsValue>; + } + let (arg0_0, arg0_1, arg0_2, arg0_3) = + #js_sys::wire::split_input::<::core::primitive::usize>(allocation.data()); + + #js_sys::wire::join_output(unsafe { + __import_(arg0_0, arg0_1, arg0_2, arg0_3) + }) + }; allocation.forget(); // SAFETY: `value` is created by the matching closure factory above. unsafe { #js_sys::Closure::<#closure>::from_js_value(value) } @@ -209,26 +210,6 @@ pub(crate) fn closure_with( }) } -fn closure_symbol_hash( - crate_name: &str, - package_name: &str, - package_version: &str, - trait_object: &TypeTraitObject, -) -> String { - let mut descriptor = String::from("closure-v1\0"); - for value in [ - crate_name, - package_name, - package_version, - &trait_object.to_token_stream().to_string(), - ] { - descriptor.push_str(value); - descriptor.push('\0'); - } - - format!("{:032x}", xxh3_128(descriptor.as_bytes())) -} - struct ClosureInput { js_sys: Option, trait_object: TypeTraitObject, @@ -289,14 +270,6 @@ impl ClosureKind { )), } } - - fn factory_embed(self) -> &'static str { - match self { - Self::Shared => "closure.make", - Self::Mutable => "closure.make_mut", - Self::Once => "closure.make_once", - } - } } impl Signature { diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index e4cb8805..20d2b03e 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -168,7 +168,6 @@ pub(crate) fn lower_abi<'a>( for (index, ty) in inputs.into_iter().enumerate() { let span = ty.span(); let argument = format_ident!("arg{index}", span = Span::mixed_site()); - let parameter = LitStr::new(&argument.to_string(), span); let reference = match ty { Type::Reference(reference) if reference.mutability.is_some() => { return Err(Error::new_spanned( @@ -215,7 +214,7 @@ pub(crate) fn lower_abi<'a>( } wire_inputs.push(quote_spanned! {span=> - #js_sys::wire::wire_export_input::<#js_ty>(#parameter) + #js_sys::wire::wire_export_input::<#js_ty>() }); arguments.push(argument); } diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index ef22bcbc..9416ac94 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -17,7 +17,7 @@ use crate::hygiene::Hygiene; mod js; mod options; -use js::ForeignItem; +use js::{ForeignItem, FunctionBinding}; use options::{BindingKind, FunctionOptions}; /// Backend-independent description of one JavaScript import. @@ -29,28 +29,13 @@ pub(crate) struct FunctionImport { pub(crate) output_type: Option, pub(crate) binding: Option, pub(crate) suspending: bool, - pub(crate) shim_kind: ImportShimKind, pub(crate) macro_path: Path, } pub(crate) struct FunctionImportInput { - pub(crate) name: LitStr, pub(crate) ty: Type, } -#[derive(Clone, Copy)] -pub(crate) enum ImportShimKind { - Normal, - ClosureFactory, -} - -/// JavaScript call data stored in an import Wire record. -pub(crate) struct FunctionBinding { - pub(crate) direct: Option, - pub(crate) call: String, - pub(crate) required_embeds: Vec, -} - struct FunctionPlan { inputs: Vec, output_ty: Option, @@ -63,7 +48,6 @@ struct FunctionPlan { struct InputArg { abi_type: Type, rust_name: Ident, - descriptor_name: syn::LitStr, slot_names: [Ident; 4], uses_abi_override: bool, } @@ -77,28 +61,12 @@ impl InputArg { Self { abi_type, rust_name, - descriptor_name: syn::LitStr::new(&base, span), slot_names: [slot_name(0), slot_name(1), slot_name(2), slot_name(3)], uses_abi_override, } } } -fn join_input_slots(inputs: &[InputArg]) -> String { - let mut inputs = inputs.iter(); - let Some(first) = inputs.next() else { - return String::new(); - }; - let mut output = first.slot_names[0].to_string(); - - for input in inputs { - output.push_str(", "); - output.push_str(&input.slot_names[0].to_string()); - } - - output -} - pub(crate) fn expand( hygiene: &mut Hygiene<'_>, namespace: Option<&str>, @@ -239,16 +207,6 @@ pub(crate) fn expand( Ok((item, import)) } -pub(crate) fn expand_closure_factory( - hygiene: &mut Hygiene<'_>, - crate_: &str, - item: ForeignItemFn, -) -> Result<(TokenStream, FunctionImport)> { - let (function, mut import) = expand(hygiene, None, crate_, &HashMap::new(), item)?; - import.shim_kind = ImportShimKind::ClosureFactory; - Ok((function, import)) -} - impl FunctionPlan { fn parse( hygiene: &mut Hygiene<'_>, @@ -264,8 +222,7 @@ impl FunctionPlan { let output_abi_override = options.return_abi.clone(); let (inputs, self_ty) = Self::parse_inputs(hygiene, sig, cfg_attrs, span, external_implementation)?; - let binding = - Self::resolve_binding(options, sig, self_ty, namespace, js_names, &inputs, span)?; + let binding = Self::resolve_binding(options, sig, self_ty, namespace, js_names, span)?; let output_ty = match &sig.output { ReturnType::Default => None, ReturnType::Type(_, ty) => Some(*ty.clone()), @@ -385,7 +342,6 @@ impl FunctionPlan { self_ty: Option, namespace: Option<&str>, js_names: &HashMap, - inputs: &[InputArg], span: Span, ) -> Result { let FunctionOptions { @@ -403,10 +359,6 @@ impl FunctionPlan { binding => binding, }; - let js_inputs: Vec<_> = inputs - .iter() - .map(|input| input.slot_names[0].to_string()) - .collect(); let argument_count = sig.inputs.len() - usize::from(self_ty.is_some()); if matches!(&binding, BindingKind::Constructor) && self_ty.is_some() { @@ -446,9 +398,8 @@ impl FunctionPlan { // selects the Rust `impl` owner and JavaScript invokes it with `new`. let owner = Self::constructor_type(&sig.output)?; let name = js_name.unwrap_or_else(|| Self::type_js_name(&owner, js_names)); - let path = ForeignItem::global_path(namespace, &name); - Ok(ForeignItem::constructor(owner, &path, variadic, &js_inputs)) + Ok(ForeignItem::constructor(owner, namespace, &name, variadic)) } BindingKind::IndexingGetter => { if argument_count != 1 || !matches!(&sig.output, ReturnType::Type(..)) { @@ -460,7 +411,6 @@ impl FunctionPlan { Ok(ForeignItem::indexing_getter( self_ty.expect("validated above"), - &js_inputs, )) } BindingKind::IndexingSetter => { @@ -470,7 +420,6 @@ impl FunctionPlan { Ok(ForeignItem::indexing_setter( self_ty.expect("validated above"), - &js_inputs, )) } BindingKind::IndexingDeleter => { @@ -480,7 +429,6 @@ impl FunctionPlan { Ok(ForeignItem::indexing_deleter( self_ty.expect("validated above"), - &js_inputs, )) } BindingKind::Getter(name) => { @@ -490,10 +438,15 @@ impl FunctionPlan { "`getter` requires no arguments and a return value", )); } - let (owner, path, _) = - Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); - - Ok(ForeignItem::getter(owner, path)) + let (owner, object, receiver) = Self::member_target(static_of, self_ty, js_names); + + Ok(ForeignItem::getter( + owner, + namespace, + object.as_deref(), + &name, + receiver, + )) } BindingKind::Setter(name) => { if argument_count != 1 || !matches!(&sig.output, ReturnType::Default) { @@ -502,18 +455,27 @@ impl FunctionPlan { "`setter` requires one argument and no return value", )); } - let (owner, path, receiver) = - Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); - - Ok(ForeignItem::setter(owner, &path, receiver, &js_inputs)) + let (owner, object, receiver) = Self::member_target(static_of, self_ty, js_names); + + Ok(ForeignItem::setter( + owner, + namespace, + object.as_deref(), + &name, + receiver, + )) } BindingKind::Call => { let name = js_name.unwrap_or_else(|| sig.ident.to_string()); - let (owner, path, receiver) = - Self::member_path(static_of, self_ty, &name, namespace, js_names, &js_inputs); + let (owner, object, receiver) = Self::member_target(static_of, self_ty, js_names); Ok(ForeignItem::call( - owner, &path, receiver, variadic, namespace, &js_inputs, + owner, + namespace, + object.as_deref(), + &name, + receiver, + variadic, )) } BindingKind::Embed(_) | BindingKind::Import => { @@ -522,25 +484,21 @@ impl FunctionPlan { } } - fn member_path( + fn member_target( static_of: Option, self_ty: Option, - name: &str, - namespace: Option<&str>, js_names: &HashMap, - inputs: &[String], - ) -> (Option, String, bool) { + ) -> (Option, Option, bool) { // `static_of` attaches the declaration to a type, an explicit `self` // parameter makes it an instance member, and neither means a global. if let Some(owner) = static_of { let type_name = Self::type_js_name(&owner, js_names); - let path = ForeignItem::global_path(namespace, &format!("{type_name}.{name}")); - (Some(owner), path, false) + (Some(owner), Some(type_name), false) } else if let Some(owner) = self_ty { - (Some(owner), format!("{}.{name}", inputs[0]), true) + (Some(owner), None, true) } else { - (None, ForeignItem::global_path(namespace, name), false) + (None, None, false) } } @@ -662,31 +620,12 @@ impl FunctionPlan { } = self; let output_type = output_abi_override.or(output_ty); - let mut required_embeds = Vec::new(); - - if let ForeignItem::Embed(name) = &binding { - required_embeds.push(quote_spanned!(span=> - #macro_path::JsEmbed::new(#crate_, #name) - )); - } - let binding = match binding { - ForeignItem::Generate { direct, call, .. } => Some(FunctionBinding { - direct, - call, - required_embeds, + ForeignItem::Generate { binding, .. } => Some(binding), + ForeignItem::Embed(name) => Some(FunctionBinding::CallEmbed { + module: crate_.to_owned(), + name, }), - ForeignItem::Embed(name) => { - let path = format!("this.#jsEmbed.{crate_}['{name}']"); - let arguments = join_input_slots(&inputs); - let call = format!("{path}({arguments})"); - - Some(FunctionBinding { - direct: Some(path), - call, - required_embeds, - }) - } ForeignItem::Import => None, }; FunctionImport { @@ -695,15 +634,11 @@ impl FunctionPlan { name: LitStr::new(import_name, span), inputs: inputs .into_iter() - .map(|input| FunctionImportInput { - name: input.descriptor_name, - ty: input.abi_type, - }) + .map(|input| FunctionImportInput { ty: input.abi_type }) .collect(), output_type, binding, suspending, - shim_kind: ImportShimKind::Normal, macro_path, } } diff --git a/host/js-sys-bindgen/src/function/js.rs b/host/js-sys-bindgen/src/function/js.rs index 8c267df0..9f79b89f 100644 --- a/host/js-sys-bindgen/src/function/js.rs +++ b/host/js-sys-bindgen/src/function/js.rs @@ -1,4 +1,140 @@ -use syn::{Ident, Path}; +use proc_macro2::{Span, TokenStream}; +use quote::quote_spanned; +use syn::{Ident, LitStr, Path}; + +/// A global JavaScript path referenced by an imported operation. +pub(crate) struct FunctionGlobalPath { + namespace: Option, + object: Option, + name: String, +} + +impl FunctionGlobalPath { + fn new(namespace: Option<&str>, object: Option<&str>, name: &str) -> Self { + Self { + namespace: namespace.map(str::to_owned), + object: object.map(str::to_owned), + name: name.to_owned(), + } + } + + fn wire(&self, macro_path: &Path, span: Span) -> TokenStream { + let namespace = self.namespace.as_ref().map_or_else( + || quote_spanned!(span=> ::core::option::Option::None), + |namespace| { + let namespace = LitStr::new(namespace, span); + quote_spanned!(span=> ::core::option::Option::Some(#namespace)) + }, + ); + let object = self.object.as_ref().map_or_else( + || quote_spanned!(span=> ::core::option::Option::None), + |object| { + let object = LitStr::new(object, span); + quote_spanned!(span=> ::core::option::Option::Some(#object)) + }, + ); + let name = LitStr::new(&self.name, span); + + quote_spanned! {span=> + #macro_path::WireGlobalPath::new(#namespace, #object, #name) + } + } +} + +/// Semantic JavaScript operation stored in an import Wire record. +pub(crate) enum FunctionBinding { + CallGlobal { + target: FunctionGlobalPath, + variadic: bool, + }, + CallMethod { + name: String, + variadic: bool, + }, + Construct { + target: FunctionGlobalPath, + variadic: bool, + }, + GetGlobal(FunctionGlobalPath), + GetMember(String), + SetGlobal(FunctionGlobalPath), + SetMember(String), + IndexGet, + IndexSet, + IndexDelete, + CallEmbed { + module: String, + name: String, + }, +} + +impl FunctionBinding { + pub(crate) fn wire(&self, macro_path: &Path, span: Span) -> TokenStream { + match self { + Self::CallGlobal { target, variadic } => { + let target = target.wire(macro_path, span); + quote_spanned! {span=> + #macro_path::WireImportBinding::CallGlobal { + target: #target, + variadic: #variadic, + } + } + } + Self::CallMethod { name, variadic } => { + let name = LitStr::new(name, span); + quote_spanned! {span=> + #macro_path::WireImportBinding::CallMethod { + name: #name, + variadic: #variadic, + } + } + } + Self::Construct { target, variadic } => { + let target = target.wire(macro_path, span); + quote_spanned! {span=> + #macro_path::WireImportBinding::Construct { + target: #target, + variadic: #variadic, + } + } + } + Self::GetGlobal(target) => { + let target = target.wire(macro_path, span); + quote_spanned!(span=> #macro_path::WireImportBinding::GetGlobal(#target)) + } + Self::GetMember(name) => { + let name = LitStr::new(name, span); + quote_spanned!(span=> #macro_path::WireImportBinding::GetMember(#name)) + } + Self::SetGlobal(target) => { + let target = target.wire(macro_path, span); + quote_spanned!(span=> #macro_path::WireImportBinding::SetGlobal(#target)) + } + Self::SetMember(name) => { + let name = LitStr::new(name, span); + quote_spanned!(span=> #macro_path::WireImportBinding::SetMember(#name)) + } + Self::IndexGet => { + quote_spanned!(span=> #macro_path::WireImportBinding::IndexGet) + } + Self::IndexSet => { + quote_spanned!(span=> #macro_path::WireImportBinding::IndexSet) + } + Self::IndexDelete => { + quote_spanned!(span=> #macro_path::WireImportBinding::IndexDelete) + } + Self::CallEmbed { module, name } => { + let module = LitStr::new(module, span); + let name = LitStr::new(name, span); + quote_spanned! {span=> + #macro_path::WireImportBinding::CallEmbed( + #macro_path::JsEmbed::new(#module, #name), + ) + } + } + } + } +} /// The final JavaScript binding selected for a foreign function. pub(super) enum ForeignItem { @@ -6,11 +142,7 @@ pub(super) enum ForeignItem { /// Rust type receiving the generated method, if this is not a free /// function. owner: Option, - /// Function reference usable without a wrapper. - direct: Option, - /// Call expression used when argument or result conversion requires a - /// wrapper. - call: String, + binding: FunctionBinding, }, Embed(String), Import, @@ -41,77 +173,92 @@ impl ForeignItem { pub(super) fn call( owner: Option, - path: &str, + namespace: Option<&str>, + object: Option<&str>, + name: &str, receiver: bool, variadic: bool, - namespace: Option<&str>, - inputs: &[String], ) -> Self { - let arguments = Self::arguments(inputs, receiver, variadic); - let call = format!("{path}({arguments})"); - - // Only a bare global function can be passed directly. Calls through a - // `namespace`, instance, or static member need a wrapper to preserve their - // receiver; `variadic` calls need one to emit the spread expression. - let direct = (namespace.is_none() && owner.is_none() && !variadic).then(|| path.to_owned()); + let binding = if receiver { + FunctionBinding::CallMethod { + name: name.to_owned(), + variadic, + } + } else { + FunctionBinding::CallGlobal { + target: FunctionGlobalPath::new(namespace, object, name), + variadic, + } + }; - Self::Generate { - owner, - direct, - call, - } + Self::Generate { owner, binding } } - pub(super) fn constructor(owner: Path, path: &str, variadic: bool, inputs: &[String]) -> Self { - let arguments = Self::arguments(inputs, false, variadic); - let call = format!("new {path}({arguments})"); - + pub(super) fn constructor( + owner: Path, + namespace: Option<&str>, + name: &str, + variadic: bool, + ) -> Self { Self::Generate { owner: Some(owner), - direct: None, - call, + binding: FunctionBinding::Construct { + target: FunctionGlobalPath::new(namespace, None, name), + variadic, + }, } } - pub(super) fn getter(owner: Option, path: String) -> Self { - Self::expression(owner, path) - } - - pub(super) fn setter( + pub(super) fn getter( owner: Option, - path: &str, + namespace: Option<&str>, + object: Option<&str>, + name: &str, receiver: bool, - inputs: &[String], ) -> Self { - let arguments = Self::arguments(inputs, receiver, false); - let call = format!("{path} = {arguments}"); + let binding = if receiver { + FunctionBinding::GetMember(name.to_owned()) + } else { + FunctionBinding::GetGlobal(FunctionGlobalPath::new(namespace, object, name)) + }; - Self::expression(owner, call) + Self::Generate { owner, binding } } - pub(super) fn indexing_getter(owner: Path, inputs: &[String]) -> Self { - let call = format!("{}[{}]", inputs[0], inputs[1]); + pub(super) fn setter( + owner: Option, + namespace: Option<&str>, + object: Option<&str>, + name: &str, + receiver: bool, + ) -> Self { + let binding = if receiver { + FunctionBinding::SetMember(name.to_owned()) + } else { + FunctionBinding::SetGlobal(FunctionGlobalPath::new(namespace, object, name)) + }; - Self::expression(Some(owner), call) + Self::Generate { owner, binding } } - pub(super) fn indexing_setter(owner: Path, inputs: &[String]) -> Self { - let call = format!("{}[{}] = {}", inputs[0], inputs[1], inputs[2]); - - Self::expression(Some(owner), call) + pub(super) fn indexing_getter(owner: Path) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::IndexGet, + } } - pub(super) fn indexing_deleter(owner: Path, inputs: &[String]) -> Self { - let call = format!("delete {}[{}]", inputs[0], inputs[1]); - - Self::expression(Some(owner), call) + pub(super) fn indexing_setter(owner: Path) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::IndexSet, + } } - pub(super) fn global_path(namespace: Option<&str>, name: &str) -> String { - if let Some(namespace) = namespace { - format!("globalThis.{namespace}.{name}") - } else { - format!("globalThis.{name}") + pub(super) fn indexing_deleter(owner: Path) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::IndexDelete, } } @@ -121,30 +268,4 @@ impl ForeignItem { .map(|segment| &segment.ident) .expect("static and instance bindings always have an owner") } - - fn expression(owner: Option, expression: String) -> Self { - Self::Generate { - owner, - direct: None, - call: expression, - } - } - - fn arguments(inputs: &[String], receiver: bool, variadic: bool) -> String { - let inputs = if receiver { &inputs[1..] } else { inputs }; - - if variadic { - let (last, inputs) = inputs - .split_last() - .expect("variadic bindings always have an argument"); - - if inputs.is_empty() { - format!("...{last}") - } else { - format!("{}, ...{last}", inputs.join(", ")) - } - } else { - inputs.join(", ") - } - } } diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index 9708b14c..7663aa5f 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -214,7 +214,7 @@ struct ImportGroup { macro_path: Path, } -pub(crate) fn render_import_groups(imports: Vec) -> TokenStream { +fn render_import_groups(imports: Vec) -> TokenStream { let mut groups: Vec = Vec::new(); for import in imports { @@ -262,24 +262,13 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream let module = &import.module; let name = &import.name; let suspending = import.suspending; - let constructor = match import.shim_kind { - crate::function::ImportShimKind::Normal => { - quote::quote!(#macro_path::WireImport::new) - } - crate::function::ImportShimKind::ClosureFactory => { - quote::quote!(#macro_path::WireImport::closure_factory) - } - }; let wire_inputs = import.inputs.iter().map(|input| { - let name = &input.name; let index = input_types .iter() .position(|candidate| candidate == &input.ty) .expect("every input type was collected"); - quote::quote!( - #macro_path::WireImportInput::new(#name, #index) - ) + quote::quote!(#macro_path::WireImportInput::new(#index)) }); let output_index = if let Some(ty) = &import.output_type { let index = output_types @@ -293,27 +282,14 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream quote::quote!(::core::option::Option::None) }; let binding = if let Some(binding) = &import.binding { - let direct = if let Some(direct) = &binding.direct { - let direct = LitStr::new(direct, module.span()); - quote::quote!(::core::option::Option::Some(#direct)) - } else { - quote::quote!(::core::option::Option::None) - }; - let call = LitStr::new(&binding.call, module.span()); - let embeds = &binding.required_embeds; - quote::quote! { - ::core::option::Option::Some(#macro_path::WireImportBinding::new( - #direct, - #call, - &[#(#embeds),*], - )) - } + let binding = binding.wire(¯o_path, module.span()); + quote::quote!(::core::option::Option::Some(#binding)) } else { quote::quote!(::core::option::Option::None) }; wire_descriptors.push(quote::quote! { - #constructor( + #macro_path::WireImport::new( #module, #name, &[#(#wire_inputs),*], @@ -346,7 +322,6 @@ pub(crate) fn render_import_groups(imports: Vec) -> TokenStream const _LEN: ::core::primitive::usize = #macro_path::wire_blob_len(&_WIRE); - #[used] #[unsafe(link_section = "js_bindgen.wire")] static _WIRE_SECTION: #macro_path::WireBlob<_LEN> = #macro_path::WireBlob::new(&_WIRE); diff --git a/host/js-sys-bindgen/src/tests/closure.rs b/host/js-sys-bindgen/src/tests/closure.rs index ba5173ed..08003639 100644 --- a/host/js-sys-bindgen/src/tests/closure.rs +++ b/host/js-sys-bindgen/src/tests/closure.rs @@ -8,7 +8,7 @@ fn expand(input: TokenStream) -> String { } #[test] -fn symbols_are_stable_and_disambiguated() { +fn factory_correlations_are_stable_and_disambiguated() { let input = quote!(dyn FnMut(i32) -> i32, move |value| value + 1); assert_eq!(expand(input.clone()), expand(input)); diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index be2e4038..e91e2ab8 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -1,60 +1,74 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::Item; +use std::collections::HashMap; -fn expand(attr: TokenStream, input: syn::ItemForeignMod) -> String { - let items = crate::r#macro::expand_for_test(attr, input, "test_crate") - .unwrap() - .into_items() - .unwrap(); +use proc_macro2::{Span, TokenStream}; +use syn::Item; - prettyplease::unparse(&syn::File { - shebang: None, - attrs: Vec::new(), - items, - }) +fn expand_function( + namespace: Option<&str>, + js_sys: Option<&syn::Path>, + function: syn::ForeignItemFn, +) -> (TokenStream, crate::function::FunctionImport) { + let mut hygiene = crate::hygiene::Hygiene::Qualified { js_sys }; + crate::function::expand( + &mut hygiene, + namespace, + "test_crate", + &HashMap::new(), + function, + ) + .unwrap() } #[test] -fn binding_options() { - let output = expand( - quote!(js_sys = renamed, namespace = "console"), +fn function_descriptor_preserves_binding_options() { + let js_sys: syn::Path = syn::parse_quote!(renamed); + let (_, import) = expand_function( + Some("console"), + Some(&js_sys), syn::parse_quote! { - extern "js-sys" { - pub fn log(value: &JsValue); - - #[js_sys(js_name = "warn")] - pub fn renamed_log(value: &JsValue); - - #[js_sys(return_abi = JsValue)] - pub fn value() -> JsTest; - - #[cfg(all())] - pub fn configured(); - } + #[cfg(all())] + #[js_sys(js_name = "warn", return_abi = JsValue)] + pub fn log(value: &JsValue) -> JsTest; }, ); - assert!(output.contains("renamed::wire::InputSlot1")); - super::assert_javascript(&output, "globalThis.console.log(arg0_0)"); - super::assert_javascript(&output, "globalThis.console.warn(arg0_0)"); - assert!(output.contains("join_output_as::")); - assert!(output.contains("#[cfg(all())]")); + assert_eq!(import.module.value(), "test_crate"); + assert_eq!(import.name.value(), "console.log"); + assert_eq!(import.macro_path, syn::parse_quote!(renamed::wire)); + assert_eq!(import.inputs[0].ty, syn::parse_quote!(&JsValue)); + assert_eq!(import.output_type, Some(syn::parse_quote!(JsValue))); + assert_eq!(import.cfg_attrs.len(), 1); + assert!(import.cfg_attrs[0].path().is_ident("cfg")); - let output = expand( - TokenStream::new(), - syn::parse_quote! { - extern "js-sys" { - #[js_sys(js_import)] - pub fn imported(); + let binding: syn::Expr = syn::parse2( + import + .binding + .as_ref() + .expect("generated import has a binding") + .wire(&import.macro_path, Span::call_site()), + ) + .unwrap(); + let expected: syn::Expr = syn::parse_quote! { + renamed::wire::WireImportBinding::CallGlobal { + target: renamed::wire::WireGlobalPath::new( + ::core::option::Option::Some("console"), + ::core::option::Option::None, + "warn" + ), + variadic: false, + } + }; + assert_eq!(binding, expected); - #[js_sys(js_embed = "embed")] - pub fn embedded(); - } + let (_, imported) = expand_function( + None, + None, + syn::parse_quote! { + #[js_sys(js_import)] + pub fn imported(); }, ); - super::assert_javascript(&output, "test_crate.imported"); - super::assert_javascript(&output, "this.#jsEmbed.test_crate['embed']"); + assert!(imported.binding.is_none()); } #[test] diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index 18d6d455..c22396f6 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -1,66 +1,42 @@ -use proc_macro2::TokenStream; +use std::collections::HashMap; -fn generated_rust(input: syn::ItemForeignMod) -> String { - let items = crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate") - .unwrap() - .into_items() - .unwrap(); - - prettyplease::unparse(&syn::File { - shebang: None, - attrs: Vec::new(), - items, - }) -} +use proc_macro2::Span; #[test] -fn member_operations() { - let output = generated_rust(syn::parse_quote! { - extern "js-sys" { - #[js_sys(js_name = "JavaScriptType")] - pub type RustType; - - #[js_sys(constructor)] - pub fn new() -> RustType; - +fn static_getter_uses_the_javascript_type_name() { + let mut js_names = HashMap::new(); + js_names.insert("RustType".to_owned(), "JavaScriptType".to_owned()); + let mut hygiene = crate::hygiene::Hygiene::Qualified { js_sys: None }; + let (_, import) = crate::function::expand( + &mut hygiene, + None, + "test_crate", + &js_names, + syn::parse_quote! { #[js_sys(static_of = RustType, getter = "value")] pub fn static_value() -> i32; + }, + ) + .unwrap(); - pub fn call(self: &RustType); - - #[js_sys(getter = "value")] - pub fn value(self: &RustType) -> i32; - - #[js_sys(setter)] - pub fn set_value(self: &RustType, value: i32); - - #[js_sys(indexing_getter)] - pub fn get(self: &RustType, index: u32) -> JsValue; - - #[js_sys(indexing_setter)] - pub fn set(self: &RustType, index: u32, value: &JsValue); - - #[js_sys(indexing_deleter)] - pub fn delete(self: &RustType, index: u32); - - #[js_sys(variadic)] - pub fn push(self: &RustType, first: &JsValue, rest: &[JsValue]); - } - }); - - for operation in [ - "new globalThis.JavaScriptType()", - "globalThis.JavaScriptType.value", - "arg0_0.call()", - "arg0_0.value", - "arg0_0.value = arg1_0", - "arg0_0[arg1_0]", - "arg0_0[arg1_0] = arg2_0", - "delete arg0_0[arg1_0]", - "arg0_0.push(arg1_0, ...arg2_0)", - ] { - super::assert_javascript(&output, operation); - } + let binding: syn::Expr = syn::parse2( + import + .binding + .as_ref() + .expect("generated member has a binding") + .wire(&import.macro_path, Span::call_site()), + ) + .unwrap(); + let expected: syn::Expr = syn::parse_quote! { + ::js_sys::wire::WireImportBinding::GetGlobal( + ::js_sys::wire::WireGlobalPath::new( + ::core::option::Option::None, + ::core::option::Option::Some("JavaScriptType"), + "value" + ) + ) + }; + assert_eq!(binding, expected); } #[test] diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index 7d24488d..b5349369 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -12,11 +12,3 @@ fn macro_error(input: syn::ItemForeignMod) -> String { error.to_string() } - -fn assert_javascript(output: &str, expected: &str) { - let literal = format!("{expected:?}"); - assert!( - output.contains(&literal), - "generated output does not contain the complete JavaScript expression {literal}" - ); -} diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index 12324634..e4c41aec 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -1,4 +1,5 @@ use proc_macro2::TokenStream; +use syn::{GenericArgument, PathArguments, Type}; fn expand(input: syn::ItemForeignMod) -> Vec { crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate") @@ -15,15 +16,55 @@ fn generic_options_and_extends() { pub type Child; } }); - let output = prettyplease::unparse(&syn::File { - shebang: None, - attrs: Vec::new(), - items: output, - }); + let child = output + .iter() + .find_map(|item| match item { + syn::Item::Struct(item) if item.ident == "Child" => Some(item), + _ => None, + }) + .expect("Child struct was generated"); + assert_eq!(child.generics, syn::parse_quote!()); + + let impls: Vec<_> = output + .iter() + .filter_map(|item| match item { + syn::Item::Impl(item) => Some(item), + _ => None, + }) + .collect(); + assert!(impls.iter().any(|item| { + trait_argument(item, "AsRef").is_some_and(|argument| type_is(argument, "JsTest")) + && type_is(&item.self_ty, "Child") + })); + assert!(impls.iter().any(|item| { + trait_argument(item, "From").is_some_and(|argument| type_is(argument, "Child")) + && type_is(&item.self_ty, "JsTest") + })); +} + +fn trait_argument<'a>(item: &'a syn::ItemImpl, name: &str) -> Option<&'a Type> { + let (_, path, _) = item.trait_.as_ref()?; + let segment = path.segments.last()?; + if segment.ident != name { + return None; + } + let PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return None; + }; + match arguments.args.first()? { + GenericArgument::Type(ty) => Some(ty), + _ => None, + } +} - assert!(output.contains("pub struct Child")); - assert!(output.contains("impl ::core::convert::AsRef for Child")); - assert!(output.contains("impl ::core::convert::From> for JsTest")); +fn type_is(ty: &Type, name: &str) -> bool { + let Type::Path(ty) = ty else { + return false; + }; + ty.path + .segments + .last() + .is_some_and(|segment| segment.ident == name) } #[test] @@ -37,15 +78,30 @@ fn attributes_are_scoped_and_duplicate_names_do_not_panic() { } }); - assert_eq!(output.len(), 15); - for (index, item) in output.into_iter().enumerate() { - let attrs = match item { - syn::Item::Struct(item) => item.attrs, - syn::Item::Impl(item) => item.attrs, - item => panic!("unexpected generated item: {item:?}"), - }; - let has_cfg_attr = attrs.iter().any(|attr| attr.path().is_ident("cfg_attr")); + let structs: Vec<_> = output + .iter() + .filter_map(|item| match item { + syn::Item::Struct(item) => Some(item), + _ => None, + }) + .collect(); + assert_eq!(structs.len(), 3); + assert_eq!(structs[0].ident, "First"); + assert_eq!(structs[1].ident, "Duplicate"); + assert_eq!(structs[2].ident, "Duplicate"); - assert_eq!(has_cfg_attr, index == 0); - } + let configured: Vec<_> = output + .iter() + .filter(|item| { + match item { + syn::Item::Struct(item) => &item.attrs, + syn::Item::Impl(item) => &item.attrs, + item => panic!("unexpected generated item: {item:?}"), + } + .iter() + .any(|attr| attr.path().is_ident("cfg_attr")) + }) + .collect(); + assert_eq!(configured.len(), 1); + assert!(matches!(configured[0], syn::Item::Struct(item) if item.ident == "First")); } diff --git a/host/ld/Cargo.toml b/host/ld/Cargo.toml index 0f789008..66e8549b 100644 --- a/host/ld/Cargo.toml +++ b/host/ld/Cargo.toml @@ -22,6 +22,7 @@ js-bindgen-wire = { workspace = true, features = ["alloc"] } postcard = { workspace = true, features = ["alloc"] } wasm-encoder = { workspace = true } wasmparser = { workspace = true } +xxhash-rust = { workspace = true } [lints] workspace = true diff --git a/host/ld/src/args.rs b/host/ld/src/args.rs index a0113a7e..d6746619 100644 --- a/host/ld/src/args.rs +++ b/host/ld/src/args.rs @@ -178,7 +178,7 @@ mod tests { use crate::args::Arguments; #[test] - fn test_custom() { + fn separates_custom_flags_from_linker_flags() { let args = &["--web".into(), "--no-entry".into()]; let args = Arguments::new(args); assert!(args.web()); diff --git a/host/ld/src/js.rs b/host/ld/src/js.rs index a31c2c07..b66e57e5 100644 --- a/host/ld/src/js.rs +++ b/host/ld/src/js.rs @@ -34,17 +34,25 @@ struct JsEmbed { #[derive(Debug, PartialEq, Eq)] struct JsExport { - module: String, binding: JsWithEmbeds, kind: JsExportKind, } #[derive(Debug, PartialEq, Eq)] enum JsExportKind { - Symbol, + Symbol { module: String }, Closure { shim: String }, } +impl JsExport { + fn origin(&self) -> &str { + match &self.kind { + JsExportKind::Symbol { module } => module, + JsExportKind::Closure { .. } => "Rust closure", + } + } +} + impl JsStore { pub fn add_js_import( &mut self, @@ -99,7 +107,6 @@ impl JsStore { embeds: impl IntoIterator, ) -> Result<()> { let definition = JsExport { - module: module.to_owned(), binding: JsWithEmbeds { js, embeds: embeds @@ -107,15 +114,17 @@ impl JsStore { .map(|(module, name)| JsEmbed { module, name }) .collect(), }, - kind: JsExportKind::Symbol, + kind: JsExportKind::Symbol { + module: module.to_owned(), + }, }; if let Some(previous) = self.export.get(name) { bail!( "found multiple JS exports named `{name}` from `{}` and `{}`\n\tJS Export \ 1:\n{:?}\n\tJS Export 2:\n{:?}", - previous.module, - definition.module, + previous.origin(), + definition.origin(), previous.binding, definition.binding ); @@ -131,14 +140,12 @@ impl JsStore { pub fn add_closure_export( &mut self, - module: &str, name: &str, js: String, embeds: impl IntoIterator, shim: &str, ) -> Result { let definition = JsExport { - module: module.to_owned(), binding: JsWithEmbeds { js, embeds: embeds @@ -158,8 +165,8 @@ impl JsStore { bail!( "found incompatible closure exports named `{name}` from `{}` and `{}`\n\tClosure \ 1:\n{:?}\n\tClosure 2:\n{:?}", - previous.module, - definition.module, + previous.origin(), + definition.origin(), previous, definition, ); diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index 6cfafef2..c32f9a9b 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -9,10 +9,11 @@ use js_bindgen_ld_shared::{IMPORT_SECTION, JsBindgenWatSectionParser, WAT_SECTIO use js_bindgen_shared::ReadFile; use js_bindgen_wire::{WIRE_SECTION, WireRecords}; use wasmparser::{Parser, Payload}; +use xxhash_rust::xxh3::xxh3_128; use crate::args::Arguments; use crate::js::JsStore; -use crate::wire::{self, RenderedExport, RenderedRecord}; +use crate::wire::{self, RenderedRecord}; pub struct PreOutput<'args> { pub add_args: Vec, @@ -145,12 +146,9 @@ fn process_object( object: &[u8], object_mtime: Option, ) -> Result<()> { - // Multiple files from the same object file need different names. - let mut file_counter = 0; - - let mut next_wasm_object = || { - file_counter += 1; - archive_path.with_added_extension(format!("wasm.{file_counter}.o")) + let wasm_object_path = |wat: &str| { + let wat_hash = xxh3_128(wat.as_bytes()); + archive_path.with_added_extension(format!("wasm.{wat_hash:032x}.o")) }; for payload in Parser::new(0).parse_all(object) { @@ -166,7 +164,7 @@ fn process_object( match &payload { Payload::CustomSection(c) if c.name() == WAT_SECTION => { for wat in JsBindgenWatSectionParser::new(c) { - let wasm_path = next_wasm_object(); + let wasm_path = wasm_object_path(wat); let wasm_bytes = compile_wat(&wasm_path, wasm64, wat, object_mtime)?; let exist_file; @@ -212,20 +210,60 @@ fn process_object( } if let Some(wat) = rendered.wat { - let wasm_path = next_wasm_object(); + let wasm_path = wasm_object_path(&wat); compile_wat(&wasm_path, wasm64, &wat, object_mtime)?; add_args.push(wasm_path.into()); } } - RenderedRecord::Exports(exports) => { - for export in exports { - let Some((name, shim)) = register_export(js_store, export)? else { - continue; - }; - + RenderedRecord::Exports(rendered) => { + for binding in rendered.bindings { + let name = binding.name; + js_store.add_symbol_export( + binding.module, + name, + binding.js, + binding.embeds.into_iter().map(|embed| { + (embed.module.to_owned(), embed.name.to_owned()) + }), + )?; add_args.push(format!("--export={name}").into()); - let wasm_path = next_wasm_object(); - compile_wat(&wasm_path, wasm64, &shim, object_mtime)?; + } + + if let Some(wat) = rendered.wat { + let wasm_path = wasm_object_path(&wat); + compile_wat(&wasm_path, wasm64, &wat, object_mtime)?; + add_args.push(wasm_path.into()); + } + } + RenderedRecord::Closure(closure) => { + let factory_module = closure.factory_module; + let factory = closure.factory; + js_store.add_js_import( + factory_module, + &factory.name, + factory.js, + factory + .embeds + .into_iter() + .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), + )?; + let wasm_path = wasm_object_path(&factory.wat); + compile_wat(&wasm_path, wasm64, &factory.wat, object_mtime)?; + add_args.push(wasm_path.into()); + + let call = closure.call; + let call_inserted = js_store.add_closure_export( + &call.name, + call.js, + call.embeds + .into_iter() + .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), + &call.wat, + )?; + if call_inserted { + add_args.push(format!("--export={}", call.name).into()); + let wasm_path = wasm_object_path(&call.wat); + compile_wat(&wasm_path, wasm64, &call.wat, object_mtime)?; add_args.push(wasm_path.into()); } } @@ -247,41 +285,6 @@ fn process_object( Ok(()) } -fn register_export<'wire>( - js_store: &mut JsStore, - export: RenderedExport<'wire>, -) -> Result> { - match export { - RenderedExport::Symbol { binding, shim } => { - let name = binding.name; - js_store.add_symbol_export( - binding.module, - name, - binding.js, - binding - .embeds - .into_iter() - .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), - )?; - Ok(Some((name, shim))) - } - RenderedExport::Closure { binding, shim } => { - let name = binding.name; - let inserted = js_store.add_closure_export( - binding.module, - name, - binding.js, - binding - .embeds - .into_iter() - .map(|embed| (embed.module.to_owned(), embed.name.to_owned())), - &shim, - )?; - Ok(inserted.then_some((name, shim))) - } - } -} - fn main_memory<'args>( arch: Arch, wasm_ld_args: &Arguments<'args>, diff --git a/host/ld/src/wire/closure.rs b/host/ld/src/wire/closure.rs new file mode 100644 index 00000000..406515a3 --- /dev/null +++ b/host/ld/src/wire/closure.rs @@ -0,0 +1,72 @@ +//! Rendering and semantic identity for Rust closures. + +use js_bindgen_wire::model::{Callee, Closure, Export}; +use xxhash_rust::xxh3::Xxh3; + +use super::{RenderedClosureShim, export, import}; + +pub(crate) struct RenderedClosure<'a> { + pub(crate) factory_module: &'a str, + pub(crate) factory: RenderedClosureShim<'a>, + pub(crate) call: RenderedClosureShim<'a>, +} + +pub(super) fn render<'a>(closure: &Closure<'a>) -> RenderedClosure<'a> { + let call_hash = fingerprint([closure.call_identity]); + let call_name = format!("closure_call_{call_hash:032x}"); + let call = render_call(closure, &call_name); + + let call_hash = call_hash.to_le_bytes(); + let factory_hash = fingerprint([ + closure.factory.helper.module.as_bytes(), + closure.factory.helper.name.as_bytes(), + call_hash.as_slice(), + ]); + let factory_name = format!("closure_new_{factory_hash:032x}"); + let factory = import::render_closure_factory(&closure.factory, &factory_name, &call_name); + + RenderedClosure { + factory_module: closure.factory.helper.module, + factory, + call, + } +} + +fn render_call<'a>(closure: &Closure<'a>, name: &str) -> RenderedClosureShim<'a> { + let export = Export { + // The export renderer only uses this field when building an ordinary + // `JsBinding`; closure dispatchers have no public module of their own. + module: closure.factory.helper.module, + name, + pointer_width: closure.pointer_width, + inputs: closure.inputs.clone(), + output: closure.output.clone(), + embeds: closure.embeds.clone(), + promising: false, + callee: Callee::Closure { + call_shim_offset: closure.call_shim_offset, + }, + }; + let rendered = export::render(core::slice::from_ref(&export)); + let [binding] = rendered + .bindings + .try_into() + .expect("one closure produces one dispatcher"); + RenderedClosureShim::new( + name, + binding.js, + closure.embeds.clone(), + rendered + .wat + .expect("one closure produces one Wasm dispatcher"), + ) +} + +fn fingerprint<'a>(parts: impl IntoIterator) -> u128 { + let mut hasher = Xxh3::new(); + for part in parts { + hasher.update(&(part.len() as u64).to_le_bytes()); + hasher.update(part); + } + hasher.digest128() +} diff --git a/host/ld/src/wire/export/js.rs b/host/ld/src/wire/export/js.rs index d6c48284..fa613b72 100644 --- a/host/ld/src/wire/export/js.rs +++ b/host/ld/src/wire/export/js.rs @@ -1,138 +1,159 @@ -use js_bindgen_wire::model::{Export, ExportInput, ExportOutput}; +use js_bindgen_wire::model::{Export, ExportOutput}; use crate::wire::JsBinding; use crate::wire::js::{Placeholder, quote_string, render_template}; +use super::input_names; + /// Renders one decoded Rust export or closure dispatcher. pub(super) fn render<'a>(export: &Export<'a>) -> JsBinding<'a> { - JsBinding { - module: export.module, - name: export.name, - js: render_export(export), - embeds: export.embeds.clone(), - } -} - -fn render_export(export: &Export<'_>) -> String { let function = format!("wasmExports[{}]", quote_string(export.name)); let callable = if export.promising { format!("WebAssembly.promising({function})") } else { function }; + let passthrough = !export.inputs.iter().any(|input| input.conversion.is_some()) + && export + .output + .as_ref() + .is_none_or(|output| output.js_conversion().is_none() && output.result().is_none()); + let js = if passthrough { + callable + } else { + let indent = if export.promising { " " } else { " " }; + let call = PreparedCall::new(export, callable, indent); + if export.promising { + render_promising(&call) + } else { + render_sync(&call) + } + }; - if is_passthrough(export) { - return callable; + JsBinding { + module: export.module, + name: export.name, + js, + embeds: export.embeds.clone(), } +} - let parameters = export - .inputs - .iter() - .map(|input| input.name) - .collect::>() - .join(", "); - let arguments = render_arguments(&export.inputs); +/// The wrapper signature and Rust `ABI` call assembled in one input pass. +struct PreparedCall<'export, 'wire> { + export: &'export Export<'wire>, + callable: String, + parameters: String, + arguments: String, + prepares: String, +} - if export.promising { - render_promising(export, &callable, ¶meters, &arguments) - } else { - render_sync(export, &callable, ¶meters, &arguments) +impl<'export, 'wire> PreparedCall<'export, 'wire> { + fn new(export: &'export Export<'wire>, callable: String, indent: &str) -> Self { + let names = input_names(&export.inputs); + let parameters = names.join(", "); + let mut arguments = String::new(); + let mut prepares = String::new(); + let mut wrote_argument = false; + + for (input, name) in export.inputs.iter().zip(&names) { + if let Some(conversion) = input.conversion.as_ref() { + for expression in &conversion.expressions { + if wrote_argument { + arguments.push_str(", "); + } + arguments.push_str(&render_template(expression, |rendered, placeholder| { + match placeholder { + Placeholder::Value => rendered.push_str(name), + Placeholder::Prepared => { + rendered.push_str(name); + rendered.push_str("$prepared"); + } + Placeholder::Slot(_) => {} + } + })); + wrote_argument = true; + } + + if let Some(prepare) = conversion.prepare.filter(|prepare| !prepare.is_empty()) { + let prepare = render_template(prepare, |rendered, placeholder| { + if placeholder == Placeholder::Value { + rendered.push_str(name); + } + }); + prepares.push_str(indent); + prepares.push_str("const "); + prepares.push_str(name); + prepares.push_str("$prepared = "); + prepares.push_str(&prepare); + prepares.push('\n'); + } + } else if !input.slots.is_empty() { + if wrote_argument { + arguments.push_str(", "); + } + arguments.push_str(name); + wrote_argument = true; + } + } + + Self { + export, + callable, + parameters, + arguments, + prepares, + } } } -fn render_sync(export: &Export<'_>, callable: &str, parameters: &str, arguments: &str) -> String { - let prepares = render_prepares(&export.inputs, " "); - let call = format!("{callable}({arguments})"); - if let Some(output) = export.output.as_ref() { +fn render_sync(call: &PreparedCall<'_, '_>) -> String { + let invoke = format!("{}({})", call.callable, call.arguments); + if let Some(output) = call.export.output.as_ref() { format!( - "({parameters}) => {{\n{prepares} const ret = {call}\n{}\n}}", + "({}) => {{\n{} const ret = {invoke}\n{}\n}}", + call.parameters, + call.prepares, render_output(output, " ") ) } else { - format!("({parameters}) => {{\n{prepares} {call}\n}}") + format!( + "({}) => {{\n{} {invoke}\n}}", + call.parameters, call.prepares + ) } } -fn render_promising( - export: &Export<'_>, - callable: &str, - parameters: &str, - arguments: &str, -) -> String { - let prepares = render_prepares(&export.inputs, " "); - let then = export.output.as_ref().map_or_else(String::new, |output| { - if output.js_conversion().is_none() && output.result().is_none() { - String::new() - } else { - format!( - ".then(ret => {{\n{}\n }})", - render_output(output, " ") - ) - } - }); +fn render_promising(call: &PreparedCall<'_, '_>) -> String { + let then = call + .export + .output + .as_ref() + .map_or_else(String::new, |output| { + if output.js_conversion().is_none() && output.result().is_none() { + String::new() + } else { + format!( + ".then(ret => {{\n{}\n }})", + render_output(output, " ") + ) + } + }); - if prepares.is_empty() { + if call.prepares.is_empty() { format!( - "(() => {{\n const $promising = {callable}\n return ({parameters}) => \ - $promising({arguments}){then}\n}})()" + "(() => {{\n const $promising = {}\n return ({}) => \ + $promising({}){then}\n}})()", + call.callable, call.parameters, call.arguments ) } else { format!( - "(() => {{\n const $promising = {callable}\n return ({parameters}) => \ - {{\n{prepares} return $promising({arguments}){then}\n }}\n}})()" + "(() => {{\n const $promising = {}\n return ({}) => \ + {{\n{} return $promising({}){then}\n }}\n}})()", + call.callable, call.parameters, call.prepares, call.arguments ) } } -fn is_passthrough(export: &Export<'_>) -> bool { - !export.inputs.iter().any(|input| input.conversion.is_some()) - && export - .output - .as_ref() - .is_none_or(|output| output.js_conversion().is_none() && output.result().is_none()) -} - -fn render_arguments(inputs: &[ExportInput<'_>]) -> String { - let mut arguments = String::new(); - let mut wrote_argument = false; - for input in inputs { - if let Some(conversion) = input.conversion.as_ref() { - for expression in &conversion.expressions { - if wrote_argument { - arguments.push_str(", "); - } - arguments.push_str(&render_input_template(expression, input.name)); - wrote_argument = true; - } - } else if !input.slots.is_empty() { - if wrote_argument { - arguments.push_str(", "); - } - arguments.push_str(input.name); - wrote_argument = true; - } - } - arguments -} - -fn render_prepares(inputs: &[ExportInput<'_>], indent: &str) -> String { - inputs - .iter() - .filter_map(|input| { - let prepare = input - .conversion - .as_ref() - .and_then(|conversion| conversion.prepare) - .filter(|prepare| !prepare.is_empty())?; - Some(format!( - "{indent}const {}$prepared = {}\n", - input.name, - render_prepare_template(prepare, input.name) - )) - }) - .collect() -} - fn render_output(output: &ExportOutput<'_>, indent: &str) -> String { let result = if let Some(result) = output.result() { format!( @@ -144,81 +165,28 @@ fn render_output(output: &ExportOutput<'_>, indent: &str) -> String { }; let expression = output.js_conversion().map_or_else( || render_output_slot(output, 0), - |template| render_output_template(template, output), + |template| { + render_template(template, |rendered, placeholder| { + if let Placeholder::Slot(slot) = placeholder { + rendered.push_str(&render_output_slot(output, slot)); + } + }) + }, ); format!("{result}{indent}return {expression}") } -fn render_input_template(template: &str, name: &str) -> String { - render_template(template, |rendered, placeholder| match placeholder { - Placeholder::Value => rendered.push_str(name), - Placeholder::Prepared => { - rendered.push_str(name); - rendered.push_str("$prepared"); - } - Placeholder::Slot(_) => {} - }) -} - -fn render_prepare_template(template: &str, value: &str) -> String { - render_template(template, |rendered, placeholder| { - if placeholder == Placeholder::Value { - rendered.push_str(value); - } - }) -} - -fn render_output_template(template: &str, output: &ExportOutput<'_>) -> String { - render_template(template, |rendered, placeholder| { - if let Placeholder::Slot(slot) = placeholder { - rendered.push_str(&render_output_slot(output, slot)); - } - }) -} - fn render_output_slot(output: &ExportOutput<'_>, slot: usize) -> String { if let Some(result) = output.result() { if slot < usize::from(result.discriminant) { - return render_ret(slot); + return format!("ret[{slot}]"); } } else if output.is_direct() { if slot == 0 { return "ret".to_owned(); } } else { - return render_ret(slot); + return format!("ret[{slot}]"); } String::new() } - -fn render_ret(index: usize) -> String { - format!("ret[{index}]") -} - -#[cfg(test)] -mod tests { - use js_bindgen_wire::PointerWidth; - use js_bindgen_wire::model::{Callee, Export}; - - use super::render_export; - - #[test] - fn quotes_export_names() { - const NAME: &str = "single' double\" slash\\ line\n雪"; - let export = Export { - module: "test", - name: NAME, - pointer_width: PointerWidth::Wasm32, - inputs: Vec::new(), - output: None, - embeds: Vec::new(), - promising: false, - callee: Callee::Symbol { name: "test" }, - }; - - assert_eq!( - render_export(&export), - "wasmExports[\"single' double\\\" slash\\\\ line\\n雪\"]", - ); - } -} diff --git a/host/ld/src/wire/export/mod.rs b/host/ld/src/wire/export/mod.rs index d2d24dff..a06558e1 100644 --- a/host/ld/src/wire/export/mod.rs +++ b/host/ld/src/wire/export/mod.rs @@ -3,21 +3,27 @@ mod js; mod wat; -use js_bindgen_wire::model::{Callee, Export}; +use js_bindgen_wire::model::{Export, ExportInput, ExportInputKind}; -use crate::wire::RenderedExport; +use crate::wire::RenderedGroup; -pub(super) fn render<'a>(exports: &[Export<'a>]) -> Vec> { - exports - .iter() - .map(|export| { - let binding = js::render(export); - let shim = wat::render(core::slice::from_ref(export)) - .expect("one export always produces a Wasm shim"); +pub(super) fn render<'a>(exports: &[Export<'a>]) -> RenderedGroup<'a> { + RenderedGroup { + bindings: exports.iter().map(js::render).collect(), + wat: wat::render(exports), + } +} - match export.callee { - Callee::Symbol { .. } => RenderedExport::Symbol { binding, shim }, - Callee::Closure { .. } => RenderedExport::Closure { binding, shim }, +fn input_names(inputs: &[ExportInput<'_>]) -> Vec { + let mut value_index = 0; + inputs + .iter() + .map(|input| match input.kind { + ExportInputKind::ClosureData => "data".to_owned(), + ExportInputKind::Value => { + let name = format!("arg{value_index}"); + value_index += 1; + name } }) .collect() diff --git a/host/ld/src/wire/export/wat.rs b/host/ld/src/wire/export/wat.rs index ef9bb0b4..4eef6739 100644 --- a/host/ld/src/wire/export/wat.rs +++ b/host/ld/src/wire/export/wat.rs @@ -3,19 +3,15 @@ use std::fmt::Write; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{Callee, Export, ExportInput, ExportInputKind, ExportOutput, Slot}; -use crate::wire::wat::{WatImports, WatLocals, quote_string, write_conversion}; +use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; + +use super::input_names; struct ExportRenderer<'export, 'wire> { index: usize, export: &'export Export<'wire>, } -impl<'export, 'wire> ExportRenderer<'export, 'wire> { - fn new(index: usize, export: &'export Export<'wire>) -> Self { - Self { index, export } - } -} - /// Renders one WAT module fragment containing all decoded exports. pub(super) fn render(exports: &[Export<'_>]) -> Option { if exports.is_empty() { @@ -23,16 +19,19 @@ pub(super) fn render(exports: &[Export<'_>]) -> Option { } let pointer_type = exports[0].pointer_type(); - let mut items = Vec::with_capacity(exports.len() * 2 + 2); let mut imports = WatImports::default(); + let mut has_closure = false; + let mut has_indirect = false; + // WAT requires imports before the types and functions which use them. for (index, export) in exports.iter().enumerate() { - let renderer = ExportRenderer::new(index, export); + let renderer = ExportRenderer { index, export }; + has_closure |= renderer.is_closure(); + has_indirect |= renderer.is_indirect(); for_each_conversion_slot(export, |slot| { imports.extend(slot.imports()); }); if let Callee::Symbol { name } = renderer.export.callee { - let mut identifier = String::new(); - renderer.write_symbol_identifier(&mut identifier); + let identifier = format!("js_sys.export.symbol.{index}"); imports.insert( &identifier, renderer.render_symbol_import(&identifier, name), @@ -40,10 +39,7 @@ pub(super) fn render(exports: &[Export<'_>]) -> Option { } } - if exports - .iter() - .any(|export| matches!(export.callee, Callee::Closure { .. })) - { + if has_closure { imports.insert( "js_sys.closure.table", format!( @@ -53,12 +49,7 @@ pub(super) fn render(exports: &[Export<'_>]) -> Option { ); } - if exports.iter().any(|export| { - export - .output - .as_ref() - .is_some_and(|output| !output.is_direct()) - }) { + if has_indirect { imports.insert( "__stack_pointer", format!( @@ -68,23 +59,26 @@ pub(super) fn render(exports: &[Export<'_>]) -> Option { ); } - let imports = imports.render(); - if !imports.is_empty() { - items.push(imports); - } + let mut wat = imports.render(); for (index, export) in exports.iter().enumerate() { - let renderer = ExportRenderer::new(index, export); + let renderer = ExportRenderer { index, export }; if renderer.is_closure() { - items.push(renderer.render_closure_type()); + if !wat.is_empty() { + wat.push('\n'); + } + renderer.write_closure_type(&mut wat); } } for (index, export) in exports.iter().enumerate() { - items.push(ExportRenderer::new(index, export).render_shim()); + if !wat.is_empty() { + wat.push('\n'); + } + ExportRenderer { index, export }.write_shim(&mut wat); } - Some(items.join("\n")) + Some(wat) } impl ExportRenderer<'_, '_> { @@ -112,116 +106,101 @@ impl ExportRenderer<'_, '_> { .expect("a closure export has one data slot") } - fn write_symbol_identifier(&self, output: &mut String) { - write!(output, "js_sys.export.symbol.{}", self.index) - .expect("writing to a String cannot fail"); - } - fn render_symbol_import(&self, identifier: &str, symbol: &str) -> String { - let retptr = if self.is_indirect() { - format!(" (param {})", self.pointer_type()) - } else { - String::new() - }; - let mut parameters = String::new(); + let mut wat = format!( + "(import \"env\" \"symbol\" (func ${identifier} (@sym (name {}))", + quoted(symbol), + ); + if self.is_indirect() { + write!(wat, " (param {})", self.pointer_type()) + .expect("writing to a String cannot fail"); + } for input in &self.export.inputs { if input.slots.is_empty() { continue; } - parameters.push_str(" (param"); + wat.push_str(" (param"); for slot in &input.slots { - write!(parameters, " {}", slot.rust).expect("writing to a String cannot fail"); + write!(wat, " {}", slot.rust).expect("writing to a String cannot fail"); } - parameters.push(')'); + wat.push(')'); } - let result = match self.export.output.as_ref() { - Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.rust), - _ => String::new(), - }; - - format!( - "(import \"env\" \"symbol\" (func ${identifier} (@sym (name \ - {})){retptr}{parameters}{result}))", - quote_string(symbol), - ) + if let Some(ExportOutput::Direct { slot, .. }) = self.export.output.as_ref() { + write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); + } + wat.push_str("))"); + wat } - fn render_closure_type(&self) -> String { - let retptr = if self.is_indirect() { - format!(" (param {})", self.pointer_type()) - } else { - String::new() - }; - let mut parameters = String::new(); + fn write_closure_type(&self, wat: &mut String) { + write!(wat, "(type $js_sys.closure.call.{} (func", self.index) + .expect("writing to a String cannot fail"); + if self.is_indirect() { + write!(wat, " (param {})", self.pointer_type()) + .expect("writing to a String cannot fail"); + } + write!(wat, " (param {})", self.pointer_type()).expect("writing to a String cannot fail"); for input in &self.export.inputs { if input.kind != ExportInputKind::Value || input.slots.is_empty() { continue; } - parameters.push_str(" (param"); + wat.push_str(" (param"); for slot in &input.slots { - write!(parameters, " {}", slot.rust).expect("writing to a String cannot fail"); + write!(wat, " {}", slot.rust).expect("writing to a String cannot fail"); } - parameters.push(')'); + wat.push(')'); } - let result = match self.export.output.as_ref() { - Some(ExportOutput::Direct { slot, .. }) => format!(" (result {})", slot.rust), - _ => String::new(), - }; + if let Some(ExportOutput::Direct { slot, .. }) = self.export.output.as_ref() { + write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); + } + wat.push_str("))"); + } - format!( - "(type $js_sys.closure.call.{} (func{retptr} (param {}){parameters}{result}))", + fn write_shim(&self, wat: &mut String) { + let names = input_names(&self.export.inputs); + write!( + wat, + "(func $js_sys.export.{} (@sym (name {}))", self.index, - self.pointer_type(), + quoted(self.export.name), ) - } - - fn render_shim(&self) -> String { - let parameters = self - .export - .inputs - .iter() - .flat_map(|input| { - input.slots.iter().enumerate().map(|(slot_index, slot)| { - let js = slot.js(); - if input.kind == ExportInputKind::ClosureData { - format!(" (param $data {js})") - } else { - format!(" (param ${}_{slot_index} {js})", input.name) - } - }) - }) - .collect::(); - let result = self - .export - .output - .as_ref() - .map_or_else(String::new, |output| match output { - ExportOutput::Direct { slot, .. } => format!(" (result {})", slot.js()), + .expect("writing to a String cannot fail"); + if self.is_closure() { + let comdat = closure_comdat(self.export.name); + write!(wat, " (@comdat {})", quoted(&comdat)).expect("writing to a String cannot fail"); + } + for (input, name) in self.export.inputs.iter().zip(&names) { + for (slot_index, slot) in input.slots.iter().enumerate() { + let js = slot.js(); + if input.kind == ExportInputKind::ClosureData { + write!(wat, " (param ${name} {js})") + } else { + write!(wat, " (param ${name}_{slot_index} {js})") + } + .expect("writing to a String cannot fail"); + } + } + if let Some(output) = self.export.output.as_ref() { + match output { + ExportOutput::Direct { slot, .. } => { + write!(wat, " (result {})", slot.js()) + .expect("writing to a String cannot fail"); + } ExportOutput::Indirect { frame, .. } => { - let types = frame - .slots - .iter() - .map(|frame_slot| frame_slot.slot.js().as_str()) - .collect::>() - .join(" "); - if types.is_empty() { - " (result)".to_owned() - } else { - format!(" (result {types})") + wat.push_str(" (result"); + for frame_slot in &frame.slots { + write!(wat, " {}", frame_slot.slot.js()) + .expect("writing to a String cannot fail"); } + wat.push(')'); } - }); + } + } - let mut wat = format!( - "(func $js_sys.export.{} (@sym (name {})){parameters}{result}", - self.index, - quote_string(self.export.name), - ); - self.write_prologue(&mut wat); - self.write_call(&mut wat); - self.write_epilogue(&mut wat); + self.write_prologue(wat); + self.write_call(wat, &names); + self.write_epilogue(wat); wat.push_str("\n)"); - wat } fn write_prologue(&self, wat: &mut String) { @@ -242,18 +221,12 @@ impl ExportRenderer<'_, '_> { for_each_conversion_slot(self.export, |slot| { locals.extend(slot.locals()); }); - let locals = locals.render(); - if !locals.is_empty() { - wat.push('\n'); - wat.push_str(&locals); - } + locals.write_into(wat); if self.is_closure() { let slot = self.closure_data_slot(); wat.push_str("\n local.get $data"); - if let Some(instruction) = slot.instruction() { - write_conversion(wat, instruction); - } + write_conversion(wat, slot.instruction()); wat.push_str("\n local.set $js_sys.closure.data"); } @@ -270,31 +243,31 @@ impl ExportRenderer<'_, '_> { } } - fn write_call(&self, wat: &mut String) { + fn write_call(&self, wat: &mut String, names: &[String]) { match self.export.callee { Callee::Symbol { .. } => { if self.is_indirect() { wat.push_str("\n local.get $retptr"); } - for input in &self.export.inputs { - write_abi_arguments(wat, input); + for (input, name) in self.export.inputs.iter().zip(names) { + write_abi_arguments(wat, input, name); } - wat.push_str("\n call $"); - self.write_symbol_identifier(wat); - wat.push_str(" (@reloc)"); + write!( + wat, + "\n call $js_sys.export.symbol.{} (@reloc)", + self.index, + ) + .expect("writing to a String cannot fail"); } Callee::Closure { call_shim_offset } => { if self.is_indirect() { wat.push_str("\n local.get $retptr"); } wat.push_str("\n local.get $js_sys.closure.data"); - for input in self - .export - .inputs - .iter() - .filter(|input| input.kind == ExportInputKind::Value) - { - write_abi_arguments(wat, input); + for (input, name) in self.export.inputs.iter().zip(names) { + if input.kind == ExportInputKind::Value { + write_abi_arguments(wat, input, name); + } } write!( wat, @@ -312,9 +285,7 @@ impl ExportRenderer<'_, '_> { if let Some(output) = self.export.output.as_ref() { match output { ExportOutput::Direct { slot, .. } => { - if let Some(instruction) = slot.instruction() { - write_conversion(wat, instruction); - } + write_conversion(wat, slot.instruction()); } ExportOutput::Indirect { frame, .. } => { for frame_slot in &frame.slots { @@ -324,9 +295,7 @@ impl ExportRenderer<'_, '_> { frame_slot.slot.rust, frame_slot.offset, ) .expect("writing to a String cannot fail"); - if let Some(instruction) = frame_slot.slot.instruction() { - write_conversion(wat, instruction); - } + write_conversion(wat, frame_slot.slot.instruction()); } write!( wat, @@ -343,6 +312,10 @@ impl ExportRenderer<'_, '_> { } } +fn closure_comdat(name: &str) -> String { + format!("js_sys.closure.call.{name}") +} + fn for_each_conversion_slot<'export, 'wire>( export: &'export Export<'wire>, mut visit: impl FnMut(&'export Slot<'wire>), @@ -364,13 +337,10 @@ fn for_each_conversion_slot<'export, 'wire>( } } -fn write_abi_arguments(wat: &mut String, input: &ExportInput<'_>) { +fn write_abi_arguments(wat: &mut String, input: &ExportInput<'_>, name: &str) { for (slot_index, slot) in input.slots.iter().enumerate() { - write!(wat, "\n local.get ${}_{slot_index}", input.name) - .expect("writing to a String cannot fail"); - if let Some(instruction) = slot.instruction() { - write_conversion(wat, instruction); - } + write!(wat, "\n local.get ${name}_{slot_index}").expect("writing to a String cannot fail"); + write_conversion(wat, slot.instruction()); } } @@ -430,7 +400,6 @@ mod tests { pointer_width: PointerWidth::Wasm32, inputs: vec![ExportInput { kind: ExportInputKind::Value, - name: "value", slots: vec![input], conversion: None, }], @@ -447,8 +416,8 @@ mod tests { assert_eq!( wat, r#"(import "env" "symbol" (func $js_sys.export.symbol.0 (@sym (name "test.raw")) (param i32) (result i32))) -(func $js_sys.export.0 (@sym (name "converted")) (param $value_0 externref) (result externref) - local.get $value_0 +(func $js_sys.export.0 (@sym (name "converted")) (param $arg0_0 externref) (result externref) + local.get $arg0_0 drop i32.const 0 call $js_sys.export.symbol.0 (@reloc) diff --git a/host/ld/src/wire/import/js.rs b/host/ld/src/wire/import/js.rs index 0b4d43a7..6ed718dd 100644 --- a/host/ld/src/wire/import/js.rs +++ b/host/ld/src/wire/import/js.rs @@ -1,12 +1,14 @@ use std::fmt::Write; use js_bindgen_wire::model::{ - DirectImportConversion, Import, ImportBinding, ImportCatch, ImportErrorMode, ImportGroup, - ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, + DirectImportConversion, Embed, GlobalPath, Import, ImportBindingKind, ImportCatch, + ImportErrorMode, ImportGroup, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, }; use crate::wire::JsBinding; -use crate::wire::js::{Placeholder, render_template}; +use crate::wire::js::{JsPath, Placeholder, render_template}; + +use super::input_name; /// Renders every import which has a generated JavaScript binding. pub(super) fn render<'a>(group: &ImportGroup<'a>) -> Vec> { @@ -35,45 +37,93 @@ pub(super) fn render<'a>(group: &ImportGroup<'a>) -> Vec> { Some(JsBinding { module: import.module, name: import.name, - js: render_binding(import, binding, catch), + js: render_function( + import, + render_operation(binding.kind, import.inputs.len()), + catch, + ), embeds, }) }) .collect() } -fn render_binding( +pub(super) fn render_closure_factory( + import: &Import<'_>, + helper: Embed<'_>, + call_name: &str, +) -> String { + let mut arguments = input_values(import.inputs.len()); + arguments.push( + JsPath::new("this.#jsExports") + .property(call_name) + .into_string(), + ); + let expression = format!("{}({})", embed_path(helper), arguments.join(", ")); + + render_function( + import, + RenderedOperation { + expression, + direct_callable: None, + }, + None, + ) +} + +fn render_function( import: &Import<'_>, - binding: &ImportBinding<'_>, + operation: RenderedOperation, catch: Option<&JsCatch<'_>>, ) -> String { debug_assert!(!import.suspending || catch.is_none()); - let output_needs_wrapper = import - .output - .as_ref() - .is_some_and(|output| output_needs_wrapper(output, catch)); + let output_needs_wrapper = import.output.as_ref().is_some_and(|output| { + catch.is_some() + || match &output.abi { + ImportOutputAbi::Direct { conversion, .. } => conversion.is_some(), + ImportOutputAbi::Indirect { .. } => true, + } + }); let needs_wrapper = import .inputs .iter() .any(|input| input.js_conversion.is_some()) || output_needs_wrapper; let await_output = import.suspending && output_needs_wrapper; - let parameters = render_parameters(import); + let RenderedOperation { + expression, + direct_callable, + } = operation; + if !needs_wrapper && let Some(direct) = direct_callable { + if import.suspending { + return format!("new WebAssembly.Suspending({direct})"); + } + return direct; + } + + let PreparedInputs { + parameters, + conversions, + } = prepare_inputs(import); let js = if needs_wrapper { - let conversions = render_input_conversions(&import.inputs); let asynchronous = if await_output { "async " } else { "" }; let body = if let Some(output) = import.output.as_ref() { - render_output(output, binding.call_expression, await_output, catch) + match &output.abi { + ImportOutputAbi::Direct { conversion, .. } => { + render_direct_output(&expression, conversion.as_ref(), await_output, catch) + } + ImportOutputAbi::Indirect { retptr, writer } => { + render_indirect_output(&expression, retptr, writer, await_output, catch) + } + } } else { let return_ = if import.suspending { "return " } else { "" }; - format!(" {return_}{}\n}}", binding.call_expression) + format!(" {return_}{expression}\n}}") }; format!("{asynchronous}({parameters}) => {{\n{conversions}{body}") - } else if let Some(direct) = binding.direct_expression { - direct.to_owned() } else { - format!("({parameters}) => {}", binding.call_expression) + format!("({parameters}) => {expression}") }; if import.suspending { @@ -83,15 +133,108 @@ fn render_binding( } } -fn output_needs_wrapper(output: &ImportOutput<'_>, catch: Option<&JsCatch<'_>>) -> bool { - catch.is_some() - || match output.abi { - ImportOutputAbi::Direct { conversion, .. } => conversion.is_some(), - ImportOutputAbi::Indirect { .. } => true, +struct RenderedOperation { + expression: String, + // Present only when the operation itself can be installed as the import. + direct_callable: Option, +} + +fn render_operation(kind: ImportBindingKind<'_>, input_count: usize) -> RenderedOperation { + let values = input_values(input_count); + let (expression, direct_callable) = match kind { + ImportBindingKind::CallGlobal { target, variadic } => { + let is_direct = target.namespace.is_none() && target.object.is_none() && !variadic; + let target = global_path(target); + let arguments = render_arguments(&values, false, variadic); + let direct = is_direct.then(|| target.to_string()); + (format!("{target}({arguments})"), direct) + } + ImportBindingKind::CallMethod { name, variadic } => { + let arguments = render_arguments(&values, true, variadic); + let method = JsPath::new(&values[0]).property(name); + (format!("{method}({arguments})"), None) + } + ImportBindingKind::Construct { target, variadic } => { + let target = global_path(target); + let arguments = render_arguments(&values, false, variadic); + (format!("new {target}({arguments})"), None) + } + ImportBindingKind::GetGlobal(target) => (global_path(target).into_string(), None), + ImportBindingKind::GetMember(name) => { + (JsPath::new(&values[0]).property(name).into_string(), None) + } + ImportBindingKind::SetGlobal(target) => { + (format!("{} = {}", global_path(target), values[0]), None) + } + ImportBindingKind::SetMember(name) => ( + format!("{} = {}", JsPath::new(&values[0]).property(name), values[1]), + None, + ), + ImportBindingKind::IndexGet => (format!("{}[{}]", values[0], values[1]), None), + ImportBindingKind::IndexSet => ( + format!("{}[{}] = {}", values[0], values[1], values[2]), + None, + ), + ImportBindingKind::IndexDelete => (format!("delete {}[{}]", values[0], values[1]), None), + ImportBindingKind::CallEmbed(embed) => { + let target = embed_path(embed); + let arguments = values.join(", "); + let direct = Some(target.to_string()); + (format!("{target}({arguments})"), direct) + } + }; + + RenderedOperation { + expression, + direct_callable, + } +} + +fn global_path(target: GlobalPath<'_>) -> JsPath { + let mut path = JsPath::new("globalThis"); + if let Some(namespace) = target.namespace { + for component in namespace.split('.') { + path = path.property(component); } + } + if let Some(object) = target.object { + path = path.property(object); + } + path.property(target.name) +} + +fn embed_path(embed: Embed<'_>) -> JsPath { + JsPath::new("this.#jsEmbed") + .property(embed.module) + .property(embed.name) +} + +fn input_values(count: usize) -> Vec { + (0..count).map(|index| format!("arg{index}_0")).collect() +} + +fn render_arguments(inputs: &[String], receiver: bool, variadic: bool) -> String { + let inputs = if receiver { &inputs[1..] } else { inputs }; + if !variadic { + return inputs.join(", "); + } + + let (last, inputs) = inputs + .split_last() + .expect("a variadic import must have an argument"); + if inputs.is_empty() { + format!("...{last}") + } else { + format!("{}, ...{last}", inputs.join(", ")) + } } -fn render_parameters(import: &Import<'_>) -> String { +struct PreparedInputs { + parameters: String, + conversions: String, +} + +fn prepare_inputs(import: &Import<'_>) -> PreparedInputs { let mut parameters = Vec::new(); if import .output @@ -100,57 +243,38 @@ fn render_parameters(import: &Import<'_>) -> String { { parameters.push("$retptr".to_owned()); } - parameters.extend( - import - .inputs - .iter() - .flat_map(|input| (0..input.slots.len()).map(|slot| format!("{}_{slot}", input.name))), - ); - parameters.join(", ") -} - -fn render_input_conversions(inputs: &[ImportInput<'_>]) -> String { - inputs - .iter() - .filter_map(|input| { - let template = input.js_conversion?; + let mut conversions = String::new(); + for (index, input) in import.inputs.iter().enumerate() { + let name = input_name(index); + parameters.extend((0..input.slots.len()).map(|slot| format!("{name}_{slot}"))); + if let Some(template) = input.js_conversion { let declaration = if input.slots.is_empty() { "const " } else { "" }; - let expression = render_input_template(template, input.name); - Some(format!( - " {declaration}{}_0 = {expression}\n", - input.name - )) - }) - .collect() -} - -fn render_output( - output: &ImportOutput<'_>, - call: &str, - await_output: bool, - catch: Option<&JsCatch<'_>>, -) -> String { - match &output.abi { - ImportOutputAbi::Direct { conversion, .. } => { - render_direct_output(call, conversion.as_ref(), await_output, catch) - } - ImportOutputAbi::Indirect { retptr, writer } => { - render_indirect_output(call, retptr, writer, await_output, catch) + let expression = render_template(template, |rendered, placeholder| { + if let Placeholder::Slot(slot) = placeholder { + write!(rendered, "{name}_{slot}").expect("writing to a String cannot fail"); + } + }); + writeln!(conversions, " {declaration}{name}_0 = {expression}") + .expect("writing to a String cannot fail"); } } + PreparedInputs { + parameters: parameters.join(", "), + conversions, + } } fn render_direct_output( - call: &str, + expression: &str, conversion: Option<&DirectImportConversion<'_>>, await_output: bool, catch: Option<&JsCatch<'_>>, ) -> String { let indent = if catch.is_some() { " " } else { " " }; - let call = if await_output { - format!("await ({call})") + let expression = if await_output { + format!("await ({expression})") } else { - call.to_owned() + expression.to_owned() }; let mut js = if catch.is_some() { " try {\n".to_owned() @@ -159,7 +283,7 @@ fn render_direct_output( }; if let Some(conversion) = conversion { - write!(js, "{indent}const $ret = {call}").expect("writing to a String cannot fail"); + write!(js, "{indent}const $ret = {expression}").expect("writing to a String cannot fail"); js.push_str(&render_prepare(conversion.prepare, indent)); write!( js, @@ -168,7 +292,7 @@ fn render_direct_output( ) .expect("writing to a String cannot fail"); } else { - write!(js, "{indent}return {call}").expect("writing to a String cannot fail"); + write!(js, "{indent}return {expression}").expect("writing to a String cannot fail"); } js.push_str(catch.map_or("\n}", |catch| catch.direct)); @@ -176,7 +300,7 @@ fn render_direct_output( } fn render_indirect_output( - call: &str, + expression: &str, retptr: &ImportRetptr<'_>, writer: &ImportWriter<'_>, await_output: bool, @@ -187,19 +311,26 @@ fn render_indirect_output( if let Some(template) = retptr.js_conversion && !template.is_empty() { - writeln!(js, " $retptr = {}", render_retptr_template(template)) - .expect("writing to a String cannot fail"); + let expression = render_template(template, |rendered, placeholder| match placeholder { + Placeholder::Slot(0) => rendered.push_str("$retptr"), + Placeholder::Value => rendered.push_str("$value"), + Placeholder::Prepared => rendered.push_str("$prepared"), + Placeholder::Slot(slot) => { + write!(rendered, "$slot{}", slot + 1).expect("writing to a String cannot fail"); + } + }); + writeln!(js, " $retptr = {expression}").expect("writing to a String cannot fail"); } if catch.is_some() { js.push_str(" try {\n"); } - let call = if await_output { - format!("await ({call})") + let expression = if await_output { + format!("await ({expression})") } else { - call.to_owned() + expression.to_owned() }; - write!(js, "{indent}const $ret = {call}").expect("writing to a String cannot fail"); + write!(js, "{indent}const $ret = {expression}").expect("writing to a String cannot fail"); match writer { ImportWriter::Slots { @@ -238,14 +369,6 @@ fn render_prepare(prepare: Option<&str>, indent: &str) -> String { }) } -fn render_input_template(template: &str, name: &str) -> String { - render_template(template, |rendered, placeholder| { - if let Placeholder::Slot(slot) = placeholder { - write!(rendered, "{name}_{slot}").expect("writing to a String cannot fail"); - } - }) -} - fn render_result_template(template: &str) -> String { render_template(template, |rendered, placeholder| match placeholder { Placeholder::Value => rendered.push_str("$ret"), @@ -254,13 +377,109 @@ fn render_result_template(template: &str) -> String { }) } -fn render_retptr_template(template: &str) -> String { - render_template(template, |rendered, placeholder| match placeholder { - Placeholder::Slot(0) => rendered.push_str("$retptr"), - Placeholder::Value => rendered.push_str("$value"), - Placeholder::Prepared => rendered.push_str("$prepared"), - Placeholder::Slot(slot) => { - write!(rendered, "$slot{}", slot + 1).expect("writing to a String cannot fail"); +#[cfg(test)] +mod tests { + use super::*; + + const BARE: GlobalPath<'static> = GlobalPath { + namespace: None, + object: None, + name: "identity-value", + }; + + #[test] + fn renders_representative_call_shapes() { + let cases = [ + ( + ImportBindingKind::CallGlobal { + target: BARE, + variadic: false, + }, + 1, + "globalThis[\"identity-value\"](arg0_0)", + Some("globalThis[\"identity-value\"]"), + ), + ( + ImportBindingKind::CallMethod { + name: "push-value", + variadic: true, + }, + 3, + "arg0_0[\"push-value\"](arg1_0, ...arg2_0)", + None, + ), + ( + ImportBindingKind::GetMember("data-value"), + 1, + "arg0_0[\"data-value\"]", + None, + ), + ( + ImportBindingKind::SetMember("data-value"), + 2, + "arg0_0[\"data-value\"] = arg1_0", + None, + ), + ( + ImportBindingKind::CallEmbed(Embed { + module: "test", + name: "map", + }), + 1, + "this.#jsEmbed[\"test\"][\"map\"](arg0_0)", + Some("this.#jsEmbed[\"test\"][\"map\"]"), + ), + ]; + + for (kind, input_count, expression, direct_callable) in cases { + let rendered = render_operation(kind, input_count); + assert_eq!(rendered.expression, expression); + assert_eq!(rendered.direct_callable.as_deref(), direct_callable); } - }) + } + + #[test] + fn renders_namespace_segments_and_single_properties() { + let path = GlobalPath { + namespace: Some("Temporal.Now"), + object: Some("object.with.dot"), + name: "method-name", + }; + + assert_eq!( + global_path(path).into_string(), + "globalThis[\"Temporal\"][\"Now\"][\"object.with.dot\"][\"method-name\"]", + ); + } + + #[test] + fn preserves_the_bare_global_direct_path() { + let mut import = Import { + module: "test", + name: "identity", + inputs: Vec::new(), + output: None, + binding: None, + suspending: false, + }; + let operation = || { + render_operation( + ImportBindingKind::CallGlobal { + target: BARE, + variadic: false, + }, + 0, + ) + }; + + assert_eq!( + render_function(&import, operation(), None), + "globalThis[\"identity-value\"]", + ); + import.suspending = true; + assert_eq!( + render_function(&import, operation(), None), + "new WebAssembly.Suspending(globalThis[\"identity-value\"])", + ); + } } diff --git a/host/ld/src/wire/import/mod.rs b/host/ld/src/wire/import/mod.rs index 4a13900f..89932fce 100644 --- a/host/ld/src/wire/import/mod.rs +++ b/host/ld/src/wire/import/mod.rs @@ -3,9 +3,9 @@ mod js; mod wat; -use js_bindgen_wire::model::ImportGroup; +use js_bindgen_wire::model::{ClosureFactory, Import, ImportGroup}; -use super::RenderedGroup; +use super::{RenderedClosureShim, RenderedGroup}; pub(super) fn render<'a>(group: &ImportGroup<'a>) -> RenderedGroup<'a> { RenderedGroup { @@ -13,3 +13,27 @@ pub(super) fn render<'a>(group: &ImportGroup<'a>) -> RenderedGroup<'a> { wat: wat::render(group), } } + +fn input_name(index: usize) -> String { + format!("arg{index}") +} + +pub(super) fn render_closure_factory<'a>( + factory: &ClosureFactory<'a>, + name: &str, + call_name: &str, +) -> RenderedClosureShim<'a> { + let import = Import { + module: factory.helper.module, + name, + inputs: vec![factory.input.clone()], + output: Some(factory.output.clone()), + binding: None, + suspending: false, + }; + let js = js::render_closure_factory(&import, factory.helper, call_name); + let wat = wat::render_closure_factory(&import, factory.raw_symbol); + let mut embeds = factory.embeds.clone(); + embeds.push(factory.helper); + RenderedClosureShim::new(name, js, embeds, wat) +} diff --git a/host/ld/src/wire/import/wat.rs b/host/ld/src/wire/import/wat.rs index 0d570a5e..88503248 100644 --- a/host/ld/src/wire/import/wat.rs +++ b/host/ld/src/wire/import/wat.rs @@ -1,29 +1,44 @@ use std::collections::HashMap; use std::fmt::Write; -use js_bindgen_wire::ImportShimKind; -use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{ Import, ImportCatch, ImportErrorMode, ImportGroup, ImportOutput, ImportOutputAbi, Slot, WatCatch, }; -use crate::wire::wat::{WatImports, WatLocals, quote_string, write_conversion}; +use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; + +use super::input_name; /// Renders the imported functions followed by their Rust `ABI` shims. pub(super) fn render(group: &ImportGroup<'_>) -> Option { - if group.imports.is_empty() { + render_impl(&group.imports, group.catch.as_ref(), None) +} + +pub(super) fn render_closure_factory(import: &Import<'_>, raw_symbol: &str) -> String { + render_impl(core::slice::from_ref(import), None, Some(raw_symbol)) + .expect("one closure factory produces one Wasm import shim") +} + +fn render_impl( + group_imports: &[Import<'_>], + group_catch: Option<&ImportCatch<'_>>, + raw_shim_symbol: Option<&str>, +) -> Option { + if group_imports.is_empty() { return None; } - let group_catch = match group.catch.as_ref() { + let group_catch = match group_catch { Some(ImportCatch::Wasm(catch)) => Some(catch), Some(ImportCatch::JavaScript(_)) | None => None, }; let mut imports = WatImports::default(); let mut boundaries = HashMap::new(); - let mut shims = Vec::with_capacity(group.imports.len()); - for (index, import) in group.imports.iter().enumerate() { + let mut shims = Vec::with_capacity(group_imports.len()); + // Conversion imports must precede every function body, so collect each + // shim's inputs while the shared import set is still being built. + for (index, import) in group_imports.iter().enumerate() { let boundary_index = *boundaries .entry((import.module, import.name)) .or_insert(index); @@ -45,10 +60,21 @@ pub(super) fn render(group: &ImportGroup<'_>) -> Option { imports.extend(&catch.imports); locals.extend(&catch.locals); } + let (symbol, comdat) = raw_shim_symbol.map_or_else( + || (shim_symbol(import), None), + |raw_symbol| { + ( + raw_symbol.to_owned(), + Some(factory_comdat(raw_symbol, import.name)), + ) + }, + ); shims.push(Shim { index, boundary_index, import, + symbol, + comdat, catch, locals, }); @@ -66,42 +92,19 @@ struct Shim<'group, 'wire> { index: usize, boundary_index: usize, import: &'group Import<'wire>, + symbol: String, + comdat: Option, catch: Option<&'group WatCatch<'wire>>, locals: WatLocals<'wire>, } -// On Wasm32, imports without a return value, with a direct return, and with an -// indirect return respectively render as: -// -// ```wat -// ;; fn notify(value: u32) -// (import "js_sys" "notify" -// (func $js_sys.import.boundary.0 -// (@sym (name "js_sys.import.notify")) -// (param i32))) -// -// ;; fn identity(value: u32) -> u32 -// (import "js_sys" "identity" -// (func $js_sys.import.boundary.1 -// (@sym (name "js_sys.import.identity")) -// (param i32) -// (result i32))) -// -// ;; fn wide(value: u128) -> u128 -// (import "js_sys" "wide" -// (func $js_sys.import.boundary.2 -// (@sym (name "js_sys.import.wide")) -// (param $retptr i32) -// (param i64 i64))) -// ``` fn render_wat_import(imports: &mut WatImports, index: usize, import: &Import<'_>) { - let identifier = boundary_identifier(index); let symbol = boundary_symbol(import); let mut wat = format!( - "(import {} {} (func ${identifier} (@sym (name {}))", - quote_string(import.module), - quote_string(import.name), - quote_string(&symbol), + "(import {} {} (func $js_sys.import.boundary.{index} (@sym (name {}))", + quoted(import.module), + quoted(import.name), + quoted(&symbol), ); if let Some(ImportOutput { @@ -113,15 +116,16 @@ fn render_wat_import(imports: &mut WatImports, index: usize, import: &Import<'_> .expect("writing to a String cannot fail"); } - let input_types = import - .inputs - .iter() - .flat_map(|input| &input.slots) - .map(Slot::js) - .map(WatType::as_str) - .collect::>(); - if !input_types.is_empty() { - write!(wat, " (param {})", input_types.join(" ")).expect("writing to a String cannot fail"); + let mut wrote_parameter = false; + for slot in import.inputs.iter().flat_map(|input| &input.slots) { + if !wrote_parameter { + wat.push_str(" (param"); + wrote_parameter = true; + } + write!(wat, " {}", slot.js()).expect("writing to a String cannot fail"); + } + if wrote_parameter { + wat.push(')'); } if let Some(ImportOutput { @@ -136,47 +140,24 @@ fn render_wat_import(imports: &mut WatImports, index: usize, import: &Import<'_> imports.insert(&symbol, wat); } -// The three imports shown above are exposed to Rust through these `ABI` shims: -// -// ```wat -// ;; fn notify(value: u32) -// (func $js_sys.import.shim.0 (@sym (name "js_sys.notify")) (param $value_0 i32) -// local.get $value_0 -// call $js_sys.import.boundary.0 (@reloc) -// ) -// -// ;; fn identity(value: u32) -> u32 -// (func $js_sys.import.shim.1 (@sym (name "js_sys.identity")) (param $value_0 i32) (result i32) -// local.get $value_0 -// call $js_sys.import.boundary.1 (@reloc) -// ) -// -// ;; fn wide(value: u128) -> u128 -// (func $js_sys.import.shim.2 (@sym (name "js_sys.wide")) (param $retptr i32) (param $value_0 i64) (param $value_1 i64) -// local.get $retptr -// local.get $value_0 -// local.get $value_1 -// call $js_sys.import.boundary.2 (@reloc) -// ) -// ``` fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { let Shim { index, boundary_index, import, + symbol, + comdat, catch, locals, } = shim; write!( wat, - "(func ${} (@sym (name {}))", - shim_identifier(index), - quote_string(&shim_symbol(import)), + "(func $js_sys.import.shim.{index} (@sym (name {}))", + quoted(&symbol), ) .expect("writing to a String cannot fail"); - if import.shim_kind == ImportShimKind::ClosureFactory { - write!(wat, " (@comdat {})", quote_string(&shim_symbol(import))) - .expect("writing to a String cannot fail"); + if let Some(comdat) = comdat { + write!(wat, " (@comdat {})", quoted(&comdat)).expect("writing to a String cannot fail"); } if let Some(ImportOutput { @@ -188,9 +169,10 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { .expect("writing to a String cannot fail"); } - for input in &import.inputs { - for (index, slot) in input.slots.iter().enumerate() { - write!(wat, " (param ${}_{index} {})", input.name, slot.rust) + for (input_index, input) in import.inputs.iter().enumerate() { + let name = input_name(input_index); + for (slot_index, slot) in input.slots.iter().enumerate() { + write!(wat, " (param ${name}_{slot_index} {})", slot.rust) .expect("writing to a String cannot fail"); } } @@ -203,11 +185,7 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); } - let locals = locals.render(); - if !locals.is_empty() { - wat.push('\n'); - wat.push_str(&locals); - } + locals.write_into(wat); if let Some(catch) = catch { wat.push_str(catch.try_); @@ -218,23 +196,22 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { .. }) = import.output.as_ref() { - write_slot_get(wat, "$retptr", &retptr.slot); + wat.push_str("\n local.get $retptr"); + write_conversion(wat, retptr.slot.instruction()); } - for input in &import.inputs { - for (index, slot) in input.slots.iter().enumerate() { - write!(wat, "\n local.get ${}_{index}", input.name) + for (input_index, input) in import.inputs.iter().enumerate() { + let name = input_name(input_index); + for (slot_index, slot) in input.slots.iter().enumerate() { + write!(wat, "\n local.get ${name}_{slot_index}") .expect("writing to a String cannot fail"); - if let Some(instruction) = slot.instruction() { - write_conversion(wat, instruction); - } + write_conversion(wat, slot.instruction()); } } write!( wat, - "\n call ${} (@reloc)", - boundary_identifier(boundary_index), + "\n call $js_sys.import.boundary.{boundary_index} (@reloc)", ) .expect("writing to a String cannot fail"); @@ -242,9 +219,8 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { abi: ImportOutputAbi::Direct { slot, .. }, .. }) = import.output.as_ref() - && let Some(instruction) = slot.instruction() { - write_conversion(wat, instruction); + write_conversion(wat, slot.instruction()); } if let Some(catch) = catch { @@ -262,14 +238,6 @@ fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { wat.push_str("\n)"); } -fn boundary_identifier(index: usize) -> String { - format!("js_sys.import.boundary.{index}") -} - -fn shim_identifier(index: usize) -> String { - format!("js_sys.import.shim.{index}") -} - fn boundary_symbol(import: &Import<'_>) -> String { format!("{}.import.{}", import.module, import.name) } @@ -278,6 +246,13 @@ fn shim_symbol(import: &Import<'_>) -> String { format!("{}.{}", import.module, import.name) } +fn factory_comdat(raw_symbol: &str, canonical_factory_name: &str) -> String { + format!( + "js_sys.closure.factory.{}.{raw_symbol}.{canonical_factory_name}", + raw_symbol.len(), + ) +} + fn conversion_slots<'import, 'wire>( import: &'import Import<'wire>, ) -> impl Iterator> { @@ -295,22 +270,14 @@ fn conversion_slots<'import, 'wire>( .chain(result) } -fn write_slot_get(wat: &mut String, local: &str, slot: &Slot<'_>) { - write!(wat, "\n local.get {local}").expect("writing to a String cannot fail"); - if let Some(instruction) = slot.instruction() { - write_conversion(wat, instruction); - } -} - #[cfg(test)] mod tests { use std::rc::Rc; - use js_bindgen_wire::ImportShimKind; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{Import, ImportGroup, ImportInput, Slot, WatConversion}; - use super::render; + use super::{factory_comdat, quoted, render, render_closure_factory}; #[test] fn arbitrary_names_produce_valid_wat() { @@ -320,7 +287,6 @@ mod tests { imports: vec![Import { module: NAME, name: NAME, - shim_kind: ImportShimKind::ClosureFactory, inputs: Vec::new(), output: None, binding: None, @@ -329,9 +295,32 @@ mod tests { }; let wat = render(&group).expect("one import produces WAT"); + assert!(!wat.contains("@comdat")); js_bindgen_ld_shared::wat_to_object(false, &wat).expect("escaped WAT should parse"); } + #[test] + fn closure_factory_uses_raw_symbol_and_canonical_name_in_comdat() { + let canonical_name = "closure_new_mut_0123456789abcdef"; + let raw_symbol = "crate.raw_factory"; + let import = Import { + module: "js_sys", + name: canonical_name, + inputs: Vec::new(), + output: None, + binding: None, + suspending: false, + }; + + let wat = render_closure_factory(&import, raw_symbol); + assert!(wat.contains("(@sym (name \"crate.raw_factory\"))")); + assert!(wat.contains(&format!( + "(@comdat {})", + quoted(&factory_comdat(raw_symbol, canonical_name)), + ))); + js_bindgen_ld_shared::wat_to_object(false, &wat).expect("rendered WAT should parse"); + } + #[test] fn converted_slot_uses_js_type_for_import_and_rust_type_for_shim() { let group = ImportGroup { @@ -339,9 +328,7 @@ mod tests { imports: vec![Import { module: "test", name: "converted", - shim_kind: ImportShimKind::Normal, inputs: vec![ImportInput { - name: "value", slots: vec![Slot { rust: WatType::I32, wat: Some(WatConversion { @@ -363,8 +350,8 @@ mod tests { assert_eq!( wat, r#"(import "test" "converted" (func $js_sys.import.boundary.0 (@sym (name "test.import.converted")) (param externref))) -(func $js_sys.import.shim.0 (@sym (name "test.converted")) (param $value_0 i32) - local.get $value_0 +(func $js_sys.import.shim.0 (@sym (name "test.converted")) (param $arg0_0 i32) + local.get $arg0_0 drop ref.null extern call $js_sys.import.boundary.0 (@reloc) diff --git a/host/ld/src/wire/js.rs b/host/ld/src/wire/js.rs index 8b79d287..a57a13ca 100644 --- a/host/ld/src/wire/js.rs +++ b/host/ld/src/wire/js.rs @@ -17,6 +17,33 @@ const PLACEHOLDERS: [(&str, Placeholder); 6] = [ ("$slot4", Placeholder::Slot(3)), ]; +/// A JavaScript expression extended through safely quoted property accesses. +pub(super) struct JsPath(String); + +impl JsPath { + pub(super) fn new(root: &str) -> Self { + Self(root.to_owned()) + } + + #[must_use] + pub(super) fn property(mut self, name: &str) -> Self { + self.0.push('['); + self.0.push_str("e_string(name)); + self.0.push(']'); + self + } + + pub(super) fn into_string(self) -> String { + self.0 + } +} + +impl std::fmt::Display for JsPath { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.0) + } +} + /// Quotes one JavaScript string literal. /// /// JavaScript export names are valid strings rather than identifiers, so they @@ -84,7 +111,18 @@ pub(super) fn render_template( mod tests { use std::fmt::Write; - use super::{Placeholder, quote_string, render_template}; + use super::{JsPath, Placeholder, quote_string, render_template}; + + #[test] + fn builds_javascript_property_paths() { + assert_eq!( + JsPath::new("root") + .property("one.two") + .property("quote\"") + .into_string(), + "root[\"one.two\"][\"quote\\\"\"]", + ); + } #[test] fn quotes_javascript_strings() { diff --git a/host/ld/src/wire/mod.rs b/host/ld/src/wire/mod.rs index 1c988640..6623e793 100644 --- a/host/ld/src/wire/mod.rs +++ b/host/ld/src/wire/mod.rs @@ -1,5 +1,6 @@ //! Rendering of decoded `js-sys` wire records. +mod closure; mod export; mod import; mod js; @@ -23,27 +24,38 @@ pub(crate) struct RenderedGroup<'a> { pub(crate) wat: Option, } -/// One Rust export with its JavaScript binding and Wasm boundary shim. -pub(crate) enum RenderedExport<'a> { - Symbol { - binding: JsBinding<'a>, - shim: String, - }, - Closure { - binding: JsBinding<'a>, - shim: String, - }, +/// One generated closure binding and its matching Wasm shim. +pub(crate) struct RenderedClosureShim<'a> { + pub(crate) name: String, + pub(crate) js: String, + pub(crate) embeds: Vec>, + pub(crate) wat: String, +} + +impl<'a> RenderedClosureShim<'a> { + fn new(name: &str, js: String, mut embeds: Vec>, wat: String) -> Self { + embeds.sort_unstable_by_key(|embed| (embed.module, embed.name)); + embeds.dedup_by_key(|embed| (embed.module, embed.name)); + Self { + name: name.to_owned(), + js, + embeds, + wat, + } + } } -/// Whether a rendered group describes imports or exports. +/// One rendered wire record. pub(crate) enum RenderedRecord<'a> { Imports(RenderedGroup<'a>), - Exports(Vec>), + Exports(RenderedGroup<'a>), + Closure(closure::RenderedClosure<'a>), } pub(crate) fn decode_and_render(bytes: &[u8]) -> Result, Error> { Ok(match decode(bytes)? { Record::Imports(group) => RenderedRecord::Imports(import::render(&group)), Record::Exports(exports) => RenderedRecord::Exports(export::render(&exports)), + Record::Closure(item) => RenderedRecord::Closure(closure::render(&item)), }) } diff --git a/host/ld/src/wire/wat.rs b/host/ld/src/wire/wat.rs index 33a11bce..472d26e8 100644 --- a/host/ld/src/wire/wat.rs +++ b/host/ld/src/wire/wat.rs @@ -1,28 +1,34 @@ //! Shared WAT emission helpers. use std::collections::HashMap; -use std::fmt::Write; +use std::fmt::{self, Display, Formatter, Write}; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{WatImport, WatImportKind, WatLocal}; -/// Quotes a WAT string as UTF-8 bytes. +/// Formats a quoted WAT string as UTF-8 bytes. /// /// Byte escapes keep arbitrary Unicode and control characters independent of /// how the WAT source text is parsed. -pub(super) fn quote_string(value: &str) -> String { - let mut output = String::with_capacity(value.len() + 2); - output.push('"'); - for &byte in value.as_bytes() { - match byte { - b'"' => output.push_str("\\\""), - b'\\' => output.push_str("\\\\"), - 0x20..=0x7e => output.push(char::from(byte)), - byte => write!(output, "\\{byte:02x}").expect("writing to a String cannot fail"), +pub(super) fn quoted(value: &str) -> impl Display + '_ { + Quoted(value) +} + +struct Quoted<'a>(&'a str); + +impl Display for Quoted<'_> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_char('"')?; + for &byte in self.0.as_bytes() { + match byte { + b'"' => formatter.write_str("\\\"")?, + b'\\' => formatter.write_str("\\\\")?, + 0x20..=0x7e => formatter.write_char(char::from(byte))?, + byte => write!(formatter, "\\{byte:02x}")?, + } } + formatter.write_char('"') } - output.push('"'); - output } #[derive(Default)] @@ -54,12 +60,7 @@ impl WatImports { } pub(super) fn render(self) -> String { - let mut wat = String::new(); - for entry in self.entries { - write_separator(&mut wat); - wat.push_str(&entry); - } - wat + self.entries.join("\n") } } @@ -87,19 +88,18 @@ impl<'wire> WatLocals<'wire> { } } - pub(super) fn render(self) -> String { - let mut wat = String::new(); + pub(super) fn write_into(self, wat: &mut String) { for local in self.entries { - write_separator(&mut wat); - write!(wat, " (local ${} {})", local.name, local.ty) + write!(wat, "\n (local ${} {})", local.name, local.ty) .expect("writing to a String cannot fail"); } - wat } } -pub(super) fn write_conversion(wat: &mut String, conversion: &str) { - if !conversion.is_empty() { +pub(super) fn write_conversion(wat: &mut String, conversion: Option<&str>) { + if let Some(conversion) = conversion + && !conversion.is_empty() + { wat.push_str("\n "); wat.push_str(conversion); } @@ -109,8 +109,8 @@ fn write_import(wat: &mut String, import: &WatImport<'_>) { write!( wat, "(import {} {} (", - quote_string(import.module), - quote_string(import.name), + quoted(import.module), + quoted(import.name), ) .expect("writing to a String cannot fail"); @@ -151,8 +151,7 @@ fn write_import(wat: &mut String, import: &WatImport<'_>) { fn write_symbol(wat: &mut String, name: Option<&str>) { if let Some(name) = name { - write!(wat, "(@sym (name {}))", quote_string(name)) - .expect("writing to a String cannot fail"); + write!(wat, "(@sym (name {}))", quoted(name)).expect("writing to a String cannot fail"); } else { wat.push_str("(@sym)"); } @@ -169,20 +168,14 @@ fn write_types(wat: &mut String, kind: &str, types: &[WatType]) { wat.push(')'); } -fn write_separator(wat: &mut String) { - if !wat.is_empty() { - wat.push('\n'); - } -} - #[cfg(test)] mod tests { - use super::quote_string; + use super::quoted; #[test] fn quotes_wat_strings_as_bytes() { assert_eq!( - quote_string("single' double\" slash\\ line\n雪"), + quoted("single' double\" slash\\ line\n雪").to_string(), "\"single' double\\\" slash\\\\ line\\0a\\e9\\9b\\aa\"", ); } diff --git a/host/wire/src/decode/closure.rs b/host/wire/src/decode/closure.rs new file mode 100644 index 00000000..a6fa7955 --- /dev/null +++ b/host/wire/src/decode/closure.rs @@ -0,0 +1,121 @@ +use alloc::vec::Vec; + +use super::import::{InputType, OutputType}; +use super::{Decode, Decoder}; +use crate::abi::WatType; +use crate::model::{ + Closure, ClosureFactory, Embed, ExportInput, ExportInputKind, ExportOutput, ImportErrorMode, + ImportInput, ImportOutput, ImportOutputAbi, +}; +use crate::{CLOSURE_FLAGS, CLOSURE_HAS_OUTPUT, Error, PointerWidth}; + +pub(super) fn decode<'a>( + decoder: &mut Decoder<'a>, + pointer_width: PointerWidth, +) -> Result, Error> { + Closure::decode_with(decoder, pointer_width) +} + +impl<'a> Closure<'a> { + fn decode_with(decoder: &mut Decoder<'a>, pointer_width: PointerWidth) -> Result { + let factory = ClosureFactory::decode_with(decoder, pointer_width)?; + + let call_identity_start = decoder.position(); + let flags = decoder.flags("closure", CLOSURE_FLAGS)?; + let call_shim_offset = decoder.u64()?; + + let input_count = decoder.count("closure dispatcher input")?; + let mut inputs = Vec::with_capacity(input_count); + let mut embeds = Vec::new(); + for index in 0..input_count { + let kind = if index == 0 { + ExportInputKind::ClosureData + } else { + ExportInputKind::Value + }; + let (input, embed) = ExportInput::decode_with(decoder, kind)?; + inputs.push(input); + embeds.extend(embed); + } + decoder.ensure( + inputs.first().is_some_and(|input| { + input.slots.len() == 1 && input.slots[0].rust == pointer_width.wat_type() + }), + "closure dispatcher data must occupy one native pointer slot", + )?; + decoder.ensure( + factory.input.slots[0].js() == inputs[0].slots[0].js(), + "closure factory and dispatcher data boundary types differ", + )?; + + let output = if flags & CLOSURE_HAS_OUTPUT != 0 { + let (output, embed) = ExportOutput::decode_with(decoder)?; + embeds.extend(embed); + Some(output) + } else { + None + }; + let call_identity = decoder.raw_from(call_identity_start)?; + + Ok(Self { + factory, + call_identity, + pointer_width, + inputs, + output, + embeds, + call_shim_offset, + }) + } +} + +impl<'a> ClosureFactory<'a> { + fn decode_with(decoder: &mut Decoder<'a>, pointer_width: PointerWidth) -> Result { + let raw_symbol = decoder.string()?; + decoder.ensure( + !raw_symbol.is_empty(), + "closure factory raw symbol is empty", + )?; + let helper = Embed { + module: decoder.string()?, + name: decoder.string()?, + }; + + let input_type = InputType::decode(decoder)?; + decoder.ensure( + input_type.slots.len() == 1 && input_type.slots[0].rust == pointer_width.wat_type(), + "closure factory data must occupy one native pointer slot", + )?; + let input = ImportInput::from_type(&input_type); + + let output_type = OutputType::decode_with(decoder, None)?; + decoder.ensure( + !output_type.result, + "closure factory output cannot be a Result", + )?; + decoder.ensure( + matches!( + &output_type.abi, + ImportOutputAbi::Direct { + slot, + conversion: None, + } if slot.rust == WatType::I32 && slot.js() == WatType::ExternRef + ), + "closure factory output must be a direct, infallible JsValue", + )?; + + let mut embeds = Vec::new(); + embeds.extend(input_type.embed); + embeds.extend(output_type.embeds.into_iter().flatten()); + Ok(Self { + raw_symbol, + helper, + input, + output: ImportOutput { + abi: output_type.abi, + error: ImportErrorMode::Infallible, + }, + embeds, + }) + } +} diff --git a/host/wire/src/decode/export.rs b/host/wire/src/decode/export.rs index e49422ba..84134279 100644 --- a/host/wire/src/decode/export.rs +++ b/host/wire/src/decode/export.rs @@ -1,13 +1,13 @@ use alloc::vec::Vec; -use super::{Decode, Decoder, value}; +use super::{Decoder, value}; use crate::model::{ Callee, Embed, Export, ExportInput, ExportInputConversion, ExportInputKind, ExportOutput, FrameSlot, ResultLayout, ReturnFrame, }; use crate::{ EXPORT_FLAGS, EXPORT_HAS_OUTPUT, EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_FLAGS, - EXPORT_OUTPUT_RESULT, EXPORT_PROMISING, Error, PointerWidth, SLOT_COUNT, + EXPORT_OUTPUT_RESULT, EXPORT_PROMISING, Error, PointerWidth, }; pub(super) fn decode<'a>( @@ -27,29 +27,18 @@ impl<'a> Export<'a> { let module = decoder.string()?; let name = decoder.string()?; let flags = decoder.flags("export", EXPORT_FLAGS)?; - let callee = Callee::decode(decoder)?; + let symbol = decoder.string()?; + decoder.ensure(!symbol.is_empty(), "export callee has an empty symbol name")?; + let callee = Callee::Symbol { name: symbol }; let input_count = decoder.count("export input")?; let mut inputs = Vec::with_capacity(input_count); let mut embeds = Vec::new(); - for index in 0..input_count { - let kind = if matches!(callee, Callee::Closure { .. }) && index == 0 { - ExportInputKind::ClosureData - } else { - ExportInputKind::Value - }; - let (input, embed) = ExportInput::decode_with(decoder, kind)?; + for _ in 0..input_count { + let (input, embed) = ExportInput::decode_with(decoder, ExportInputKind::Value)?; inputs.push(input); embeds.extend(embed); } - if matches!(callee, Callee::Closure { .. }) { - decoder.ensure( - inputs.first().is_some_and(|input| { - input.slots.len() == 1 && input.slots[0].rust == pointer_width.wat_type() - }), - "closure export data must occupy one pointer slot", - )?; - } let output = if flags & EXPORT_HAS_OUTPUT != 0 { let (output, embed) = ExportOutput::decode_with(decoder)?; @@ -72,28 +61,11 @@ impl<'a> Export<'a> { } } -impl<'a> Decode<'a> for Callee<'a> { - fn decode(decoder: &mut Decoder<'a>) -> Result { - match decoder.tag("export callee", 1)? { - 0 => { - let name = decoder.string()?; - decoder.ensure(!name.is_empty(), "export callee has an empty symbol name")?; - Ok(Self::Symbol { name }) - } - 1 => Ok(Self::Closure { - call_shim_offset: decoder.u64()?, - }), - _ => unreachable!(), - } - } -} - impl<'a> ExportInput<'a> { - fn decode_with( + pub(super) fn decode_with( decoder: &mut Decoder<'a>, kind: ExportInputKind, ) -> Result<(Self, Option>), Error> { - let name = decoder.string()?; let slots = value::slots(decoder)?; let conversion = value::optional_from_js_conversion(decoder)?; let (conversion, embed) = if let Some(conversion) = conversion { @@ -115,7 +87,6 @@ impl<'a> ExportInput<'a> { Ok(( Self { kind, - name, slots, conversion, }, @@ -125,7 +96,9 @@ impl<'a> ExportInput<'a> { } impl<'a> ExportOutput<'a> { - fn decode_with(decoder: &mut Decoder<'a>) -> Result<(Self, Option>), Error> { + pub(super) fn decode_with( + decoder: &mut Decoder<'a>, + ) -> Result<(Self, Option>), Error> { let flags = decoder.flags("export output", EXPORT_OUTPUT_FLAGS)?; let slots = value::slots(decoder)?; let conversion = value::optional_into_js_conversion(decoder)?; @@ -133,32 +106,8 @@ impl<'a> ExportOutput<'a> { Some((embed, template)) => (embed, Some(template)), None => (None, None), }; - let frame_size = decoder.u64()?; - let slot_offsets = [ - decoder.u64()?, - decoder.u64()?, - decoder.u64()?, - decoder.u64()?, - ]; let direct = flags & EXPORT_OUTPUT_DIRECT != 0; let result = flags & EXPORT_OUTPUT_RESULT != 0; - let result_layout = if result { - Some(ResultLayout { - discriminant: decoder.u8()?, - error: decoder.u8()?, - }) - } else { - None - }; - - decoder.ensure( - !direct || frame_size == 0 && slot_offsets == [0; SLOT_COUNT], - "direct export output unexpectedly has a return frame", - )?; - decoder.ensure( - direct || frame_size != 0, - "indirect export output is missing its return frame", - )?; let output = if direct { let slots = value::compact(&slots); decoder.ensure(slots.len() == 1, "direct export output must have one slot")?; @@ -168,6 +117,26 @@ impl<'a> ExportOutput<'a> { js_conversion, } } else { + let frame_size = decoder.u64()?; + decoder.ensure( + frame_size != 0, + "indirect export output is missing its return frame", + )?; + let mut slot_offsets = [0; 4]; + // The slot mask determines which offsets are present in the record. + for (slot, offset) in slots.iter().zip(&mut slot_offsets) { + if slot.is_some() { + *offset = decoder.u64()?; + } + } + let result_layout = if result { + Some(ResultLayout { + discriminant: decoder.u8()?, + error: decoder.u8()?, + }) + } else { + None + }; // Result control slots can follow empty value slots, as in `Result<()>`, // so export outputs cannot require a contiguous `ABI` prefix. let frame_slots: Vec<_> = slots diff --git a/host/wire/src/decode/import.rs b/host/wire/src/decode/import.rs index 206711b8..b4d632ec 100644 --- a/host/wire/src/decode/import.rs +++ b/host/wire/src/decode/import.rs @@ -4,26 +4,30 @@ use alloc::vec::Vec; use super::{Decode, Decoder, value}; use crate::abi::WatType; use crate::model::{ - DirectImportConversion, Embed, Import, ImportBinding, ImportCatch, ImportErrorMode, - ImportGroup, ImportInput, ImportOutput, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, - Slot, WatCatch, + DirectImportConversion, Embed, GlobalPath, Import, ImportBinding, ImportBindingKind, + ImportCatch, ImportErrorMode, ImportGroup, ImportInput, ImportOutput, ImportOutputAbi, + ImportRetptr, ImportWriter, JsCatch, Slot, WatCatch, }; use crate::{ - Error, IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_CLOSURE_FACTORY, IMPORT_FLAGS, - IMPORT_HAS_BINDING, IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_FLAGS, - IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, ImportShimKind, PointerWidth, + Error, IMPORT_BINDING_CALL_EMBED, IMPORT_BINDING_CALL_GLOBAL, IMPORT_BINDING_CALL_METHOD, + IMPORT_BINDING_CONSTRUCT, IMPORT_BINDING_GET_GLOBAL, IMPORT_BINDING_GET_MEMBER, + IMPORT_BINDING_INDEX_DELETE, IMPORT_BINDING_INDEX_GET, IMPORT_BINDING_INDEX_SET, + IMPORT_BINDING_MAX, IMPORT_BINDING_SET_GLOBAL, IMPORT_BINDING_SET_MEMBER, + IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_FLAGS, IMPORT_HAS_BINDING, + IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_FLAGS, IMPORT_OUTPUT_RESULT, + IMPORT_SUSPENDING, PointerWidth, }; -struct InputType<'a> { - slots: Vec>, +pub(super) struct InputType<'a> { + pub(super) slots: Vec>, js_conversion: Option<&'a str>, - embed: Option>, + pub(super) embed: Option>, } -struct OutputType<'a> { - abi: ImportOutputAbi<'a>, - embeds: [Option>; 2], - result: bool, +pub(super) struct OutputType<'a> { + pub(super) abi: ImportOutputAbi<'a>, + pub(super) embeds: [Option>; 2], + pub(super) result: bool, } pub(super) fn decode<'a>( @@ -52,7 +56,7 @@ pub(super) fn decode<'a>( let Some(pointer) = pointer_type.as_ref() else { return decoder.invalid("a non-empty output table has no pointer type"); }; - output_types.push(OutputType::decode_with(decoder, pointer)?); + output_types.push(OutputType::decode_with(decoder, Some(pointer))?); } let catch = if output_types.iter().any(|output| output.result) { Some(decode_catch(decoder)?) @@ -137,7 +141,10 @@ impl<'a> Decode<'a> for InputType<'a> { } impl<'a> OutputType<'a> { - fn decode_with(decoder: &mut Decoder<'a>, pointer: &InputType<'a>) -> Result { + pub(super) fn decode_with( + decoder: &mut Decoder<'a>, + pointer: Option<&InputType<'a>>, + ) -> Result { let flags = decoder.flags("import output", IMPORT_OUTPUT_FLAGS)?; let direct = flags & IMPORT_OUTPUT_DIRECT != 0; @@ -178,10 +185,6 @@ impl<'a> OutputType<'a> { has_conversion || sret.is_none(), "import output has a writer without a JavaScript conversion", )?; - decoder.ensure( - pointer.slots.len() == 1, - "import return pointer must have one slot", - )?; let abi = if direct { let slots = value::compact(&slots); decoder.ensure(slots.len() == 1, "direct import output must have one slot")?; @@ -211,6 +214,13 @@ impl<'a> OutputType<'a> { }, } } else { + let Some(pointer) = pointer else { + return decoder.invalid("indirect import output has no return pointer type"); + }; + decoder.ensure( + pointer.slots.len() == 1, + "import return pointer must have one slot", + )?; decoder.ensure( templates[0].is_some(), "indirect import output has no conversion", @@ -227,7 +237,11 @@ impl<'a> OutputType<'a> { } }; - let pointer_embed = if direct { None } else { pointer.embed }; + let pointer_embed = if direct { + None + } else { + pointer.and_then(|pointer| pointer.embed) + }; Ok(Self { abi, embeds: [pointer_embed, embed], @@ -246,21 +260,14 @@ impl<'a> Import<'a> { let module = decoder.string()?; let name = decoder.string()?; let flags = decoder.flags("import", IMPORT_FLAGS)?; - let shim_kind = if flags & IMPORT_CLOSURE_FACTORY == 0 { - ImportShimKind::Normal - } else { - ImportShimKind::ClosureFactory - }; - let input_count = decoder.count("import input")?; let mut inputs = Vec::with_capacity(input_count); let mut conversion_embeds = Vec::new(); for _ in 0..input_count { - let name = decoder.string()?; let index = decoder.u32()?; let ty = value::table_entry(decoder, input_types, index, "import input type")?; conversion_embeds.extend(ty.embed); - inputs.push(ImportInput::from_type(name, ty)); + inputs.push(ImportInput::from_type(ty)); } let output = if flags & IMPORT_HAS_OUTPUT != 0 { @@ -319,7 +326,6 @@ impl<'a> Import<'a> { Ok(Self { module, name, - shim_kind, inputs, output, binding, @@ -329,9 +335,8 @@ impl<'a> Import<'a> { } impl<'a> ImportInput<'a> { - fn from_type(name: &'a str, ty: &InputType<'a>) -> Self { + pub(super) fn from_type(ty: &InputType<'a>) -> Self { Self { - name, slots: ty.slots.clone(), js_conversion: ty.js_conversion, } @@ -340,20 +345,52 @@ impl<'a> ImportInput<'a> { impl<'a> Decode<'a> for ImportBinding<'a> { fn decode(decoder: &mut Decoder<'a>) -> Result { - let direct_expression = decoder.optional_string()?; - let call_expression = decoder.string()?; - let embed_count = decoder.count("import embed")?; - let mut embeds = Vec::with_capacity(embed_count); - for _ in 0..embed_count { - embeds.push(Embed { - module: decoder.string()?, - name: decoder.string()?, - }); + let kind = ImportBindingKind::decode(decoder)?; + let mut embeds = Vec::new(); + if let ImportBindingKind::CallEmbed(embed) = kind { + embeds.push(embed); } + Ok(Self { kind, embeds }) + } +} + +impl<'a> Decode<'a> for GlobalPath<'a> { + fn decode(decoder: &mut Decoder<'a>) -> Result { Ok(Self { - direct_expression, - call_expression, - embeds, + namespace: decoder.optional_string()?, + object: decoder.optional_string()?, + name: decoder.string()?, + }) + } +} + +impl<'a> Decode<'a> for ImportBindingKind<'a> { + fn decode(decoder: &mut Decoder<'a>) -> Result { + Ok(match decoder.tag("import binding", IMPORT_BINDING_MAX)? { + IMPORT_BINDING_CALL_GLOBAL => Self::CallGlobal { + target: GlobalPath::decode(decoder)?, + variadic: decoder.boolean("variadic global call")?, + }, + IMPORT_BINDING_CALL_METHOD => Self::CallMethod { + name: decoder.string()?, + variadic: decoder.boolean("variadic method call")?, + }, + IMPORT_BINDING_CONSTRUCT => Self::Construct { + target: GlobalPath::decode(decoder)?, + variadic: decoder.boolean("variadic constructor call")?, + }, + IMPORT_BINDING_GET_GLOBAL => Self::GetGlobal(GlobalPath::decode(decoder)?), + IMPORT_BINDING_GET_MEMBER => Self::GetMember(decoder.string()?), + IMPORT_BINDING_SET_GLOBAL => Self::SetGlobal(GlobalPath::decode(decoder)?), + IMPORT_BINDING_SET_MEMBER => Self::SetMember(decoder.string()?), + IMPORT_BINDING_INDEX_GET => Self::IndexGet, + IMPORT_BINDING_INDEX_SET => Self::IndexSet, + IMPORT_BINDING_INDEX_DELETE => Self::IndexDelete, + IMPORT_BINDING_CALL_EMBED => Self::CallEmbed(Embed { + module: decoder.string()?, + name: decoder.string()?, + }), + _ => unreachable!(), }) } } diff --git a/host/wire/src/decode/mod.rs b/host/wire/src/decode/mod.rs index 2a48cf8b..6c1c90a6 100644 --- a/host/wire/src/decode/mod.rs +++ b/host/wire/src/decode/mod.rs @@ -1,14 +1,16 @@ //! Allocation-backed decoding of wire records. +mod closure; mod export; mod import; mod value; +use alloc::boxed::Box; use core::mem::size_of; use core::{fmt, str}; use crate::model::Record; -use crate::{KIND_EXPORT, KIND_IMPORT, MAGIC, PointerWidth, VERSION}; +use crate::{KIND_CLOSURE, KIND_EXPORT, KIND_IMPORT, MAGIC, PointerWidth, VERSION}; /// A type that can be decoded from a wire record. pub(crate) trait Decode<'de>: Sized { @@ -98,6 +100,9 @@ impl<'de> Decode<'de> for Record<'de> { match decoder.u8()? { KIND_IMPORT => import::decode(decoder, pointer_width).map(Self::Imports), KIND_EXPORT => export::decode(decoder, pointer_width).map(Self::Exports), + KIND_CLOSURE => closure::decode(decoder, pointer_width) + .map(Box::new) + .map(Self::Closure), kind => Err(Error::new(kind_offset, ErrorKind::UnknownRecordKind(kind))), } } @@ -120,6 +125,13 @@ impl<'de> Decoder<'de> { self.position } + /// Returns the already-validated record bytes consumed since `start`. + pub(crate) fn raw_from(&self, start: usize) -> Result<&'de [u8], Error> { + self.bytes + .get(start..self.position) + .ok_or_else(|| Error::new(start, ErrorKind::InvalidValue("invalid decoder byte range"))) + } + pub(crate) fn u8(&mut self) -> Result { Ok(self.bytes(1)?[0]) } diff --git a/host/wire/src/encode/closure.rs b/host/wire/src/encode/closure.rs new file mode 100644 index 00000000..b95dec4d --- /dev/null +++ b/host/wire/src/encode/closure.rs @@ -0,0 +1,64 @@ +use core::mem::size_of; + +use super::{Encoder, Sizer, flag}; +use crate::{CLOSURE_HAS_OUTPUT, WireClosure, WireClosureFactory}; + +impl Encoder { + pub(super) const fn closure(&mut self, closure: &WireClosure) { + closure.encode(self); + } +} + +impl Sizer { + pub(super) const fn closure(&mut self, closure: &WireClosure) { + closure.size(self); + } +} + +impl WireClosureFactory { + const fn encode(self, encoder: &mut Encoder) { + encoder.string(self.raw_symbol); + encoder.string(self.helper.module); + encoder.string(self.helper.name); + self.input.encode(encoder); + self.output.encode(encoder); + } + + const fn size(self, sizer: &mut Sizer) { + sizer.string(self.raw_symbol); + sizer.string(self.helper.module); + sizer.string(self.helper.name); + self.input.size(sizer); + self.output.size(sizer); + } +} + +impl WireClosure { + const fn encode(&self, encoder: &mut Encoder) { + self.factory.encode(encoder); + encoder.u8(flag(self.output.is_some(), CLOSURE_HAS_OUTPUT)); + encoder.u64(self.call_shim_offset as u64); + encoder.count(self.inputs.len()); + let mut index = 0; + while index < self.inputs.len() { + self.inputs[index].encode(encoder); + index += 1; + } + if let Some(output) = self.output { + output.encode(encoder); + } + } + + const fn size(&self, sizer: &mut Sizer) { + self.factory.size(sizer); + sizer.add(size_of::() + size_of::() + size_of::()); + let mut index = 0; + while index < self.inputs.len() { + self.inputs[index].size(sizer); + index += 1; + } + if let Some(output) = self.output { + output.size(sizer); + } + } +} diff --git a/host/wire/src/encode/export.rs b/host/wire/src/encode/export.rs index 0a74ca10..6d6424c3 100644 --- a/host/wire/src/encode/export.rs +++ b/host/wire/src/encode/export.rs @@ -1,9 +1,11 @@ use core::mem::size_of; use super::{Encoder, Sizer, flag}; +use crate::abi::WatSlot; +use crate::schema::WireExportOutputKind; use crate::{ EXPORT_HAS_OUTPUT, EXPORT_OUTPUT_DIRECT, EXPORT_OUTPUT_RESULT, EXPORT_PROMISING, WireExport, - WireExportCallee, WireExportInput, WireExportOutput, WireExportOutputType, + WireExportInput, WireExportOutput, WireExportOutputType, WireReturnFrame, }; impl Encoder { @@ -28,32 +30,8 @@ impl Sizer { } } -impl WireExportCallee { - const fn encode(self, encoder: &mut Encoder) { - match self { - Self::Symbol(symbol) => { - encoder.u8(0); - encoder.string(symbol); - } - Self::Closure { call_shim_offset } => { - encoder.u8(1); - encoder.u64(call_shim_offset as u64); - } - } - } - - const fn size(self, sizer: &mut Sizer) { - sizer.add(1); - match self { - Self::Symbol(symbol) => sizer.string(symbol), - Self::Closure { .. } => sizer.add(size_of::()), - } - } -} - impl WireExportInput { - const fn encode(self, encoder: &mut Encoder) { - encoder.string(self.name); + pub(super) const fn encode(self, encoder: &mut Encoder) { encoder.slots(&self.ty.slots); match self.ty.conversion { Some(conversion) => { @@ -64,8 +42,7 @@ impl WireExportInput { } } - const fn size(self, sizer: &mut Sizer) { - sizer.string(self.name); + pub(super) const fn size(self, sizer: &mut Sizer) { sizer.slots(&self.ty.slots); sizer.add(1); if let Some(conversion) = self.ty.conversion { @@ -76,48 +53,99 @@ impl WireExportInput { impl WireExportOutputType { const fn encode(&self, encoder: &mut Encoder) { - encoder.u8(flag(self.mode.is_direct(), EXPORT_OUTPUT_DIRECT) - | flag(self.conversion.is_result(), EXPORT_OUTPUT_RESULT)); - encoder.slots(&self.slots); - match self.conversion.conversion() { - Some(conversion) => { - encoder.u8(1); - conversion.encode(encoder); + match self.kind { + WireExportOutputKind::Direct { slots, conversion } => { + encoder.u8(EXPORT_OUTPUT_DIRECT); + encoder.slots(&slots); + match conversion { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + } + WireExportOutputKind::Indirect { + slots, + conversion, + frame, + result, + } => { + encoder.u8(flag(conversion.is_result(), EXPORT_OUTPUT_RESULT)); + encoder.slots(&slots); + match conversion.conversion() { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + frame.encode(encoder, &slots); + if let Some(result) = result { + encoder.u8(result.discriminant); + encoder.u8(result.error); + } } - None => encoder.u8(0), - } - encoder.u64(self.frame_size as u64); - let mut index = 0; - while index < self.slot_offsets.len() { - encoder.u64(self.slot_offsets[index] as u64); - index += 1; - } - if let Some(result) = self.result { - encoder.u8(result.discriminant); - encoder.u8(result.error); } } const fn size(&self, sizer: &mut Sizer) { - sizer.add(1); - sizer.slots(&self.slots); - sizer.add(1); - if let Some(conversion) = self.conversion.conversion() { - conversion.size(sizer); + match self.kind { + WireExportOutputKind::Direct { slots, conversion } => { + sizer.add(1); + sizer.slots(&slots); + sizer.add(1); + if let Some(conversion) = conversion { + conversion.size(sizer); + } + } + WireExportOutputKind::Indirect { + slots, + conversion, + result, + .. + } => { + sizer.add(1); + sizer.slots(&slots); + sizer.add(1); + if let Some(conversion) = conversion.conversion() { + conversion.size(sizer); + } + sizer.add(size_of::()); + let mut index = 0; + while index < slots.len() { + if slots[index].is_some() { + sizer.add(size_of::()); + } + index += 1; + } + if result.is_some() { + sizer.add(2); + } + } } - sizer.add(5 * size_of::()); - if self.result.is_some() { - sizer.add(2); + } +} + +impl WireReturnFrame { + const fn encode(self, encoder: &mut Encoder, slots: &[Option; 4]) { + encoder.u64(self.size as u64); + let mut index = 0; + while index < slots.len() { + if slots[index].is_some() { + encoder.u64(self.slot_offsets[index] as u64); + } + index += 1; } } } impl WireExportOutput { - const fn encode(self, encoder: &mut Encoder) { + pub(super) const fn encode(self, encoder: &mut Encoder) { self.ty.encode(encoder); } - const fn size(self, sizer: &mut Sizer) { + pub(super) const fn size(self, sizer: &mut Sizer) { self.ty.size(sizer); } } @@ -129,7 +157,7 @@ impl WireExport { let flags = flag(self.promising, EXPORT_PROMISING) | flag(self.output.is_some(), EXPORT_HAS_OUTPUT); encoder.u8(flags); - self.callee.encode(encoder); + encoder.string(self.symbol); encoder.count(self.inputs.len()); let mut index = 0; while index < self.inputs.len() { @@ -145,7 +173,7 @@ impl WireExport { sizer.string(self.module); sizer.string(self.name); sizer.add(1); - self.callee.size(sizer); + sizer.string(self.symbol); sizer.add(size_of::()); let mut index = 0; while index < self.inputs.len() { diff --git a/host/wire/src/encode/import.rs b/host/wire/src/encode/import.rs index aab4455e..481e136e 100644 --- a/host/wire/src/encode/import.rs +++ b/host/wire/src/encode/import.rs @@ -3,10 +3,14 @@ use core::mem::size_of; use super::{Encoder, Sizer, flag, wire_u32}; use crate::abi::{JsCatch, WatCatch}; use crate::{ - IMPORT_CATCH_JAVASCRIPT, IMPORT_CATCH_WASM, IMPORT_CLOSURE_FACTORY, IMPORT_HAS_BINDING, - IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, - ImportShimKind, WireImport, WireImportBinding, WireImportCatch, WireImportInput, - WireImportInputType, WireImportOutput, WireImportOutputType, WireImportTypeTable, + IMPORT_BINDING_CALL_EMBED, IMPORT_BINDING_CALL_GLOBAL, IMPORT_BINDING_CALL_METHOD, + IMPORT_BINDING_CONSTRUCT, IMPORT_BINDING_GET_GLOBAL, IMPORT_BINDING_GET_MEMBER, + IMPORT_BINDING_INDEX_DELETE, IMPORT_BINDING_INDEX_GET, IMPORT_BINDING_INDEX_SET, + IMPORT_BINDING_SET_GLOBAL, IMPORT_BINDING_SET_MEMBER, IMPORT_CATCH_JAVASCRIPT, + IMPORT_CATCH_WASM, IMPORT_HAS_BINDING, IMPORT_HAS_OUTPUT, IMPORT_OUTPUT_DIRECT, + IMPORT_OUTPUT_RESULT, IMPORT_SUSPENDING, WireGlobalPath, WireImport, WireImportBinding, + WireImportCatch, WireImportInput, WireImportInputType, WireImportOutput, WireImportOutputType, + WireImportTypeTable, }; impl Encoder { @@ -165,7 +169,7 @@ impl WatCatch { } impl WireImportInputType { - const fn encode(&self, encoder: &mut Encoder) { + pub(super) const fn encode(&self, encoder: &mut Encoder) { encoder.slots(&self.slots); match self.conversion { Some(conversion) => { @@ -176,7 +180,7 @@ impl WireImportInputType { } } - const fn size(&self, sizer: &mut Sizer) { + pub(super) const fn size(&self, sizer: &mut Sizer) { sizer.slots(&self.slots); sizer.add(1); if let Some(conversion) = self.conversion { @@ -186,7 +190,7 @@ impl WireImportInputType { } impl WireImportOutputType { - const fn encode(&self, encoder: &mut Encoder) { + pub(super) const fn encode(&self, encoder: &mut Encoder) { encoder.u8(flag(self.mode.is_direct(), IMPORT_OUTPUT_DIRECT) | flag(self.conversion.is_result(), IMPORT_OUTPUT_RESULT)); encoder.slots(&self.slots); @@ -202,7 +206,7 @@ impl WireImportOutputType { } } - const fn size(&self, sizer: &mut Sizer) { + pub(super) const fn size(&self, sizer: &mut Sizer) { sizer.add(1); sizer.slots(&self.slots); sizer.add(1); @@ -216,15 +220,9 @@ impl WireImportOutputType { } impl WireImportInput { - const fn encode(&self, encoder: &mut Encoder) { - encoder.string(self.name); + const fn encode(self, encoder: &mut Encoder) { encoder.u32(wire_u32(self.type_index)); } - - const fn size(&self, sizer: &mut Sizer) { - sizer.string(self.name); - sizer.add(size_of::()); - } } impl WireImportOutput { @@ -235,43 +233,92 @@ impl WireImportOutput { impl WireImportBinding { const fn encode(self, encoder: &mut Encoder) { - encoder.optional_string(self.direct); - encoder.string(self.call); - encoder.count(self.required_embeds.len()); - let mut index = 0; - while index < self.required_embeds.len() { - let embed = self.required_embeds[index]; - encoder.string(embed.module); - encoder.string(embed.name); - index += 1; + match self { + Self::CallGlobal { target, variadic } => { + encoder.u8(IMPORT_BINDING_CALL_GLOBAL); + target.encode(encoder); + encoder.u8(variadic as u8); + } + Self::CallMethod { name, variadic } => { + encoder.u8(IMPORT_BINDING_CALL_METHOD); + encoder.string(name); + encoder.u8(variadic as u8); + } + Self::Construct { target, variadic } => { + encoder.u8(IMPORT_BINDING_CONSTRUCT); + target.encode(encoder); + encoder.u8(variadic as u8); + } + Self::GetGlobal(target) => { + encoder.u8(IMPORT_BINDING_GET_GLOBAL); + target.encode(encoder); + } + Self::GetMember(name) => { + encoder.u8(IMPORT_BINDING_GET_MEMBER); + encoder.string(name); + } + Self::SetGlobal(target) => { + encoder.u8(IMPORT_BINDING_SET_GLOBAL); + target.encode(encoder); + } + Self::SetMember(name) => { + encoder.u8(IMPORT_BINDING_SET_MEMBER); + encoder.string(name); + } + Self::IndexGet => encoder.u8(IMPORT_BINDING_INDEX_GET), + Self::IndexSet => encoder.u8(IMPORT_BINDING_INDEX_SET), + Self::IndexDelete => encoder.u8(IMPORT_BINDING_INDEX_DELETE), + Self::CallEmbed(embed) => { + encoder.u8(IMPORT_BINDING_CALL_EMBED); + encoder.string(embed.module); + encoder.string(embed.name); + } } } const fn size(self, sizer: &mut Sizer) { - sizer.optional_string(self.direct); - sizer.string(self.call); - sizer.add(size_of::()); - let mut index = 0; - while index < self.required_embeds.len() { - let embed = self.required_embeds[index]; - sizer.string(embed.module); - sizer.string(embed.name); - index += 1; + sizer.add(size_of::()); + match self { + Self::CallGlobal { target, .. } | Self::Construct { target, .. } => { + target.size(sizer); + sizer.add(size_of::()); + } + Self::CallMethod { name, .. } => { + sizer.string(name); + sizer.add(size_of::()); + } + Self::GetGlobal(target) | Self::SetGlobal(target) => target.size(sizer), + Self::GetMember(name) | Self::SetMember(name) => sizer.string(name), + Self::IndexGet | Self::IndexSet | Self::IndexDelete => {} + Self::CallEmbed(embed) => { + sizer.string(embed.module); + sizer.string(embed.name); + } } } } +impl WireGlobalPath { + const fn encode(self, encoder: &mut Encoder) { + encoder.optional_string(self.namespace); + encoder.optional_string(self.object); + encoder.string(self.name); + } + + const fn size(self, sizer: &mut Sizer) { + sizer.optional_string(self.namespace); + sizer.optional_string(self.object); + sizer.string(self.name); + } +} + impl WireImport { const fn encode(&self, encoder: &mut Encoder) { encoder.string(self.module); encoder.string(self.name); encoder.u8(flag(self.suspending, IMPORT_SUSPENDING) | flag(self.output.is_some(), IMPORT_HAS_OUTPUT) - | flag(self.binding.is_some(), IMPORT_HAS_BINDING) - | flag( - matches!(self.shim_kind, ImportShimKind::ClosureFactory), - IMPORT_CLOSURE_FACTORY, - )); + | flag(self.binding.is_some(), IMPORT_HAS_BINDING)); encoder.count(self.inputs.len()); let mut index = 0; while index < self.inputs.len() { @@ -290,11 +337,7 @@ impl WireImport { sizer.string(self.module); sizer.string(self.name); sizer.add(1 + size_of::()); - let mut index = 0; - while index < self.inputs.len() { - self.inputs[index].size(sizer); - index += 1; - } + sizer.add(self.inputs.len() * size_of::()); if self.output.is_some() { sizer.add(size_of::()); } diff --git a/host/wire/src/encode/mod.rs b/host/wire/src/encode/mod.rs index b1f57e49..b95cb9fd 100644 --- a/host/wire/src/encode/mod.rs +++ b/host/wire/src/encode/mod.rs @@ -1,5 +1,6 @@ //! Constant serialization of static wire descriptions. +mod closure; mod export; mod import; @@ -64,6 +65,7 @@ pub const fn wire_blob_len(wire: &Wire) -> usize { match &wire.kind { WireKind::Imports { table, imports } => sizer.imports(table, imports), WireKind::Exports(exports) => sizer.exports(exports), + WireKind::Closure(closure) => sizer.closure(closure), } sizer.position } @@ -74,6 +76,7 @@ const fn encode(wire: &Wire) -> [u8; N] { match &wire.kind { WireKind::Imports { table, imports } => encoder.imports(table, imports), WireKind::Exports(exports) => encoder.exports(exports), + WireKind::Closure(closure) => encoder.closure(closure), } assert!(encoder.position == N); encoder.bytes @@ -84,6 +87,7 @@ impl WireKind { match self { Self::Imports { .. } => crate::KIND_IMPORT, Self::Exports(_) => crate::KIND_EXPORT, + Self::Closure(_) => crate::KIND_CLOSURE, } } } diff --git a/host/wire/src/lib.rs b/host/wire/src/lib.rs index 3a5ad56a..7c4004b0 100644 --- a/host/wire/src/lib.rs +++ b/host/wire/src/lib.rs @@ -1,4 +1,20 @@ //! The wire protocol shared by `js-sys` and `js-bindgen-ld`. +//! +//! Data passes through the following stages: +//! +//! ```text +//! ABI -> schema -> const encode -> decode -> model -> ld render +//! ``` +//! +//! - `abi` defines the common vocabulary, such as Wasm slots, conversions, +//! and return modes. +//! - `schema` combines those values into static descriptions that `js-sys` +//! can construct during constant evaluation. +//! - `encode` writes each description into a compact byte record without +//! allocation. +//! - `decode` validates records read from object files. +//! - `model` owns the decoded form used by host tools. +//! - `js-bindgen-ld` renders that model into JavaScript and WAT shims. #![no_std] @@ -24,7 +40,7 @@ pub use schema::*; /// Identifies a wire record independently of its payload kind. pub const MAGIC: [u8; 8] = *b"JBGWIRE\0"; -/// The single protocol version used by imports and exports. +/// The single protocol version used by imports, exports, and closures. pub const VERSION: u16 = 1; /// Custom section containing length-prefixed wire records. @@ -34,6 +50,7 @@ pub const WIRE_SECTION: &str = "js_bindgen.wire"; pub(crate) const SLOT_COUNT: usize = 4; pub(crate) const KIND_IMPORT: u8 = 0; pub(crate) const KIND_EXPORT: u8 = 1; +pub(crate) const KIND_CLOSURE: u8 = 2; pub(crate) const WAT_IMPORT_FUNCTION: u8 = 0; pub(crate) const WAT_IMPORT_TABLE: u8 = 1; @@ -42,10 +59,8 @@ pub(crate) const WAT_IMPORT_TAG: u8 = 2; pub(crate) const IMPORT_SUSPENDING: u8 = 1 << 0; pub(crate) const IMPORT_HAS_OUTPUT: u8 = 1 << 1; pub(crate) const IMPORT_HAS_BINDING: u8 = 1 << 2; -pub(crate) const IMPORT_CLOSURE_FACTORY: u8 = 1 << 3; #[cfg(feature = "alloc")] -pub(crate) const IMPORT_FLAGS: u8 = - IMPORT_SUSPENDING | IMPORT_HAS_OUTPUT | IMPORT_HAS_BINDING | IMPORT_CLOSURE_FACTORY; +pub(crate) const IMPORT_FLAGS: u8 = IMPORT_SUSPENDING | IMPORT_HAS_OUTPUT | IMPORT_HAS_BINDING; pub(crate) const IMPORT_OUTPUT_DIRECT: u8 = 1 << 0; pub(crate) const IMPORT_OUTPUT_RESULT: u8 = 1 << 1; @@ -55,6 +70,20 @@ pub(crate) const IMPORT_OUTPUT_FLAGS: u8 = IMPORT_OUTPUT_DIRECT | IMPORT_OUTPUT_ pub(crate) const IMPORT_CATCH_JAVASCRIPT: u8 = 0; pub(crate) const IMPORT_CATCH_WASM: u8 = 1; +pub(crate) const IMPORT_BINDING_CALL_GLOBAL: u8 = 0; +pub(crate) const IMPORT_BINDING_CALL_METHOD: u8 = 1; +pub(crate) const IMPORT_BINDING_CONSTRUCT: u8 = 2; +pub(crate) const IMPORT_BINDING_GET_GLOBAL: u8 = 3; +pub(crate) const IMPORT_BINDING_GET_MEMBER: u8 = 4; +pub(crate) const IMPORT_BINDING_SET_GLOBAL: u8 = 5; +pub(crate) const IMPORT_BINDING_SET_MEMBER: u8 = 6; +pub(crate) const IMPORT_BINDING_INDEX_GET: u8 = 7; +pub(crate) const IMPORT_BINDING_INDEX_SET: u8 = 8; +pub(crate) const IMPORT_BINDING_INDEX_DELETE: u8 = 9; +pub(crate) const IMPORT_BINDING_CALL_EMBED: u8 = 10; +#[cfg(feature = "alloc")] +pub(crate) const IMPORT_BINDING_MAX: u8 = IMPORT_BINDING_CALL_EMBED; + pub(crate) const EXPORT_PROMISING: u8 = 1 << 0; pub(crate) const EXPORT_HAS_OUTPUT: u8 = 1 << 1; #[cfg(feature = "alloc")] @@ -65,5 +94,9 @@ pub(crate) const EXPORT_OUTPUT_RESULT: u8 = 1 << 1; #[cfg(feature = "alloc")] pub(crate) const EXPORT_OUTPUT_FLAGS: u8 = EXPORT_OUTPUT_DIRECT | EXPORT_OUTPUT_RESULT; +pub(crate) const CLOSURE_HAS_OUTPUT: u8 = 1 << 0; +#[cfg(feature = "alloc")] +pub(crate) const CLOSURE_FLAGS: u8 = CLOSURE_HAS_OUTPUT; + #[cfg(all(test, feature = "alloc"))] mod tests; diff --git a/host/wire/src/model.rs b/host/wire/src/model.rs index 2d8c0f09..2c66ebcd 100644 --- a/host/wire/src/model.rs +++ b/host/wire/src/model.rs @@ -1,9 +1,9 @@ //! Canonical model consumed by JavaScript and `WAT` `renderers`. +use alloc::boxed::Box; use alloc::rc::Rc; use alloc::vec::Vec; -use crate::ImportShimKind; pub use crate::PointerWidth; pub use crate::abi::ResultLayout; use crate::abi::{RefType, WatIndexType, WatType}; @@ -119,10 +119,9 @@ pub enum ImportCatch<'a> { Wasm(WatCatch<'a>), } -/// One named argument passed from Rust to an imported JavaScript function. +/// One argument passed from Rust to an imported JavaScript function. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ImportInput<'a> { - pub name: &'a str, pub slots: Vec>, pub js_conversion: Option<&'a str>, } @@ -189,11 +188,43 @@ impl ImportOutput<'_> { } } -/// JavaScript call expressions and required embeds for one import. +/// One decoded path rooted at JavaScript's global object. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct GlobalPath<'a> { + pub namespace: Option<&'a str>, + pub object: Option<&'a str>, + pub name: &'a str, +} + +/// The JavaScript operation performed by one decoded import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ImportBindingKind<'a> { + CallGlobal { + target: GlobalPath<'a>, + variadic: bool, + }, + CallMethod { + name: &'a str, + variadic: bool, + }, + Construct { + target: GlobalPath<'a>, + variadic: bool, + }, + GetGlobal(GlobalPath<'a>), + GetMember(&'a str), + SetGlobal(GlobalPath<'a>), + SetMember(&'a str), + IndexGet, + IndexSet, + IndexDelete, + CallEmbed(Embed<'a>), +} + +/// One JavaScript import operation and all source fragments it requires. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ImportBinding<'a> { - pub direct_expression: Option<&'a str>, - pub call_expression: &'a str, + pub kind: ImportBindingKind<'a>, pub embeds: Vec>, } @@ -202,7 +233,6 @@ pub struct ImportBinding<'a> { pub struct Import<'a> { pub module: &'a str, pub name: &'a str, - pub shim_kind: ImportShimKind, pub inputs: Vec>, pub output: Option>, pub binding: Option>, @@ -234,7 +264,6 @@ pub struct ExportInputConversion<'a> { #[derive(Clone, Debug, Eq, PartialEq)] pub struct ExportInput<'a> { pub kind: ExportInputKind, - pub name: &'a str, pub slots: Vec>, pub conversion: Option>, } @@ -318,9 +347,40 @@ impl Export<'_> { } } -/// One decoded import or export group. +/// One decoded closure factory import. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClosureFactory<'a> { + pub raw_symbol: &'a str, + pub helper: Embed<'a>, + pub input: ImportInput<'a>, + pub output: ImportOutput<'a>, + pub embeds: Vec>, +} + +/// One decoded closure factory and its matching Wasm dispatcher. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Closure<'a> { + pub factory: ClosureFactory<'a>, + /// Encoded dispatcher flags, call shim offset, inputs, and output. + pub call_identity: &'a [u8], + pub pointer_width: PointerWidth, + pub inputs: Vec>, + pub output: Option>, + pub embeds: Vec>, + pub call_shim_offset: u64, +} + +impl Closure<'_> { + #[must_use] + pub const fn pointer_type(&self) -> WatType { + self.pointer_width.wat_type() + } +} + +/// One decoded import, export, or closure record. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Record<'a> { Imports(ImportGroup<'a>), Exports(Vec>), + Closure(Box>), } diff --git a/host/wire/src/schema.rs b/host/wire/src/schema.rs index 5f0b3fac..58a06fd1 100644 --- a/host/wire/src/schema.rs +++ b/host/wire/src/schema.rs @@ -133,43 +133,67 @@ impl WireImportTypeTable { } } -/// One named argument accepted by a JavaScript import. +/// One argument accepted by a JavaScript import. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WireImportInput { - pub(crate) name: &'static str, pub(crate) type_index: usize, } impl WireImportInput { #[must_use] - pub const fn new(name: &'static str, type_index: usize) -> Self { - Self { name, type_index } + pub const fn new(type_index: usize) -> Self { + Self { type_index } } } -/// JavaScript binding data for one import. +/// One path rooted at JavaScript's global object. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct WireImportBinding { - pub(crate) direct: Option<&'static str>, - pub(crate) call: &'static str, - pub(crate) required_embeds: &'static [JsEmbed], +pub struct WireGlobalPath { + pub(crate) namespace: Option<&'static str>, + pub(crate) object: Option<&'static str>, + pub(crate) name: &'static str, } -impl WireImportBinding { +impl WireGlobalPath { #[must_use] pub const fn new( - direct: Option<&'static str>, - call: &'static str, - required_embeds: &'static [JsEmbed], + namespace: Option<&'static str>, + object: Option<&'static str>, + name: &'static str, ) -> Self { Self { - direct, - call, - required_embeds, + namespace, + object, + name, } } } +/// The JavaScript operation performed by one import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WireImportBinding { + CallGlobal { + target: WireGlobalPath, + variadic: bool, + }, + CallMethod { + name: &'static str, + variadic: bool, + }, + Construct { + target: WireGlobalPath, + variadic: bool, + }, + GetGlobal(WireGlobalPath), + GetMember(&'static str), + SetGlobal(WireGlobalPath), + SetMember(&'static str), + IndexGet, + IndexSet, + IndexDelete, + CallEmbed(JsEmbed), +} + /// The result type referenced by one JavaScript import. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WireImportOutput { @@ -183,19 +207,11 @@ impl WireImportOutput { } } -/// The role of one generated `Wasm` adapter. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ImportShimKind { - Normal, - ClosureFactory, -} - /// One semantic JavaScript import. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WireImport { pub(crate) module: &'static str, pub(crate) name: &'static str, - pub(crate) shim_kind: ImportShimKind, pub(crate) inputs: &'static [WireImportInput], pub(crate) output: Option, pub(crate) binding: Option, @@ -216,27 +232,12 @@ impl WireImport { Self { module, name, - shim_kind: ImportShimKind::Normal, inputs, output, binding, suspending, } } - - #[must_use] - pub const fn closure_factory( - module: &'static str, - name: &'static str, - inputs: &'static [WireImportInput], - output: Option, - binding: Option, - suspending: bool, - ) -> Self { - let mut import = Self::new(module, name, inputs, output, binding, suspending); - import.shim_kind = ImportShimKind::ClosureFactory; - import - } } /// Type-level data shared by exported arguments with the same Rust type. @@ -253,49 +254,104 @@ impl WireExportInputType { } } -/// One named argument accepted by a JavaScript-facing `Wasm` export. +/// One argument accepted by a JavaScript-facing `Wasm` export. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WireExportInput { - pub(crate) name: &'static str, pub(crate) ty: &'static WireExportInputType, } impl WireExportInput { #[must_use] - pub const fn new(name: &'static str, ty: &'static WireExportInputType) -> Self { - Self { name, ty } + pub const fn new(ty: &'static WireExportInputType) -> Self { + Self { ty } } } /// Type-level data shared by exported results with the same Rust type. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WireExportOutputType { - pub(crate) mode: ReturnMode, - pub(crate) conversion: ReturnConv, - pub(crate) slots: [Option; 4], - pub(crate) frame_size: usize, + pub(crate) kind: WireExportOutputKind, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WireExportOutputKind { + Direct { + slots: [Option; 4], + conversion: Option, + }, + Indirect { + slots: [Option; 4], + conversion: ReturnConv, + frame: WireReturnFrame, + result: Option, + }, +} + +/// Stack storage used by an indirect Rust return. +/// +/// Each offset corresponds to the same position in the output slot array; +/// offsets for empty slots are ignored. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireReturnFrame { + pub(crate) size: usize, pub(crate) slot_offsets: [usize; 4], - pub(crate) result: Option, +} + +impl WireReturnFrame { + #[must_use] + pub const fn new(size: usize, slot_offsets: [usize; 4]) -> Self { + assert!(size != 0); + Self { size, slot_offsets } + } } impl WireExportOutputType { #[must_use] - pub const fn new( - mode: ReturnMode, - conversion: ReturnConv, + pub const fn direct(slots: [Option; 4], conversion: Option) -> Self { + let mut slot_count = 0; + let mut index = 0; + while index < slots.len() { + if slots[index].is_some() { + slot_count += 1; + } + index += 1; + } + assert!(slot_count == 1); + Self { + kind: WireExportOutputKind::Direct { slots, conversion }, + } + } + + #[must_use] + pub const fn indirect( slots: [Option; 4], - frame_size: usize, - slot_offsets: [usize; 4], + conversion: ReturnConv, + frame: WireReturnFrame, result: Option, ) -> Self { assert!(conversion.is_result() == result.is_some()); + let mut slot_count = 0; + let mut index = 0; + while index < slots.len() { + if slots[index].is_some() { + assert!(frame.slot_offsets[index] < frame.size); + slot_count += 1; + } + index += 1; + } + if let Some(result) = result { + let discriminant = result.discriminant as usize; + let error = result.error as usize; + assert!(error == discriminant + 1); + assert!(error + 1 == slot_count); + } Self { - mode, - conversion, - slots, - frame_size, - slot_offsets, - result, + kind: WireExportOutputKind::Indirect { + slots, + conversion, + frame, + result, + }, } } } @@ -313,22 +369,59 @@ impl WireExportOutput { } } -/// Describes how an exported `Wasm` shim reaches Rust code. +/// The raw linker symbol and boundary `ABI` of a closure factory. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum WireExportCallee { - Symbol(&'static str), - Closure { call_shim_offset: usize }, +pub struct WireClosureFactory { + pub(crate) raw_symbol: &'static str, + pub(crate) helper: JsEmbed, + pub(crate) input: &'static WireImportInputType, + pub(crate) output: &'static WireImportOutputType, } -impl WireExportCallee { +impl WireClosureFactory { #[must_use] - const fn symbol(name: &'static str) -> Self { - Self::Symbol(name) + pub const fn new( + raw_symbol: &'static str, + helper: JsEmbed, + input: &'static WireImportInputType, + output: &'static WireImportOutputType, + ) -> Self { + Self { + raw_symbol, + helper, + input, + output, + } } +} + +/// One semantic closure factory and its matching Wasm dispatcher. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireClosure { + pub(crate) factory: WireClosureFactory, + pub(crate) call_shim_offset: usize, + pub(crate) inputs: &'static [WireExportInput], + pub(crate) output: Option, +} +impl WireClosure { #[must_use] - const fn closure(call_shim_offset: usize) -> Self { - Self::Closure { call_shim_offset } + pub const fn new( + factory: WireClosureFactory, + call_shim_offset: usize, + inputs: &'static [WireExportInput], + output: Option, + ) -> Self { + assert!( + !inputs.is_empty(), + "closure dispatchers require a data input" + ); + Self { + factory, + call_shim_offset, + inputs, + output, + } } } @@ -339,7 +432,7 @@ pub struct WireExport { pub(crate) name: &'static str, pub(crate) inputs: &'static [WireExportInput], pub(crate) output: Option, - pub(crate) callee: WireExportCallee, + pub(crate) symbol: &'static str, pub(crate) promising: bool, } @@ -350,7 +443,7 @@ impl WireExport { name: &'static str, inputs: &'static [WireExportInput], output: Option, - callee: WireExportCallee, + symbol: &'static str, promising: bool, ) -> Self { Self { @@ -358,7 +451,7 @@ impl WireExport { name, inputs, output, - callee, + symbol, promising, } } @@ -371,14 +464,7 @@ impl WireExport { inputs: &'static [WireExportInput], output: Option, ) -> Self { - Self::new( - module, - name, - inputs, - output, - WireExportCallee::symbol(symbol), - false, - ) + Self::new(module, name, inputs, output, symbol, false) } #[must_use] @@ -389,33 +475,7 @@ impl WireExport { inputs: &'static [WireExportInput], output: Option, ) -> Self { - Self::new( - module, - name, - inputs, - output, - WireExportCallee::symbol(symbol), - true, - ) - } - - #[must_use] - pub const fn new_closure( - module: &'static str, - name: &'static str, - call_shim_offset: usize, - inputs: &'static [WireExportInput], - output: Option, - ) -> Self { - assert!(!inputs.is_empty(), "closure exports require a data input"); - Self::new( - module, - name, - inputs, - output, - WireExportCallee::closure(call_shim_offset), - false, - ) + Self::new(module, name, inputs, output, symbol, true) } } @@ -426,9 +486,10 @@ pub(crate) enum WireKind { imports: &'static [WireImport], }, Exports(&'static [WireExport]), + Closure(WireClosure), } -/// One import or export group encoded into a wire record. +/// One import, export, or closure payload encoded into a wire record. #[derive(Clone, Copy)] pub struct Wire { pub(crate) pointer_width: PointerWidth, @@ -472,6 +533,19 @@ impl Wire { kind: WireKind::Exports(exports), } } + + #[must_use] + pub const fn closure(closure: WireClosure) -> Self { + Self::closure_with(PointerWidth::native(), closure) + } + + #[must_use] + pub(crate) const fn closure_with(pointer_width: PointerWidth, closure: WireClosure) -> Self { + Self { + pointer_width, + kind: WireKind::Closure(closure), + } + } } const fn validate_imports(table: &WireImportTypeTable, imports: &[WireImport]) { diff --git a/host/wire/src/tests.rs b/host/wire/src/tests.rs index a8588802..7f3e2e07 100644 --- a/host/wire/src/tests.rs +++ b/host/wire/src/tests.rs @@ -116,18 +116,18 @@ const IMPORT_TYPE_TABLE: WireImportTypeTable = WireImportTypeTable::new( IMPORT_OUTPUT_TYPES, WAT_CATCH, ); -const IMPORT_EMBEDS: &[JsEmbed] = &[JsEmbed::new("js_sys", "identity")]; +const IDENTITY_PATH: WireGlobalPath = WireGlobalPath::new(None, None, "identity"); +const WIDE_PATH: WireGlobalPath = WireGlobalPath::new(None, None, "wide"); const IMPORTS: &[WireImport] = &[ - WireImport::closure_factory( + WireImport::new( "js_sys", "number.identity", - &[WireImportInput::new("arg0", 0)], + &[WireImportInput::new(0)], Some(WireImportOutput::new(0)), - Some(WireImportBinding::new( - Some("globalThis.identity"), - "globalThis.identity(arg0)", - IMPORT_EMBEDS, - )), + Some(WireImportBinding::CallGlobal { + target: IDENTITY_PATH, + variadic: false, + }), false, ), WireImport::new( @@ -135,7 +135,10 @@ const IMPORTS: &[WireImport] = &[ "wide.suspending", &[], Some(WireImportOutput::new(1)), - Some(WireImportBinding::new(None, "globalThis.wide()", &[])), + Some(WireImportBinding::CallGlobal { + target: WIDE_PATH, + variadic: false, + }), true, ), ]; @@ -145,44 +148,57 @@ const IMPORT_RECORD: WireRecord = WireRecord::new(&IMPORT_WIRE); const EXPORT_I32_INPUT: WireExportInputType = WireExportInputType::new([I32, None, None, None], None); -const EXPORT_PTR_INPUT: WireExportInputType = - WireExportInputType::new([I64, None, None, None], None); -const EXPORT_DIRECT_OUTPUT: WireExportOutputType = WireExportOutputType::new( - ReturnMode::Direct, - ReturnConv::Value(None), - [I32, None, None, None], - 0, - [0; 4], - None, -); -const EXPORT_RESULT_UNIT_OUTPUT: WireExportOutputType = WireExportOutputType::new( - ReturnMode::Indirect, - ReturnConv::Result(None), +const EXPORT_DIRECT_OUTPUT: WireExportOutputType = + WireExportOutputType::direct([I32, None, None, None], None); +const EXPORT_RESULT_UNIT_OUTPUT: WireExportOutputType = WireExportOutputType::indirect( [None, None, I32, I32], - 16, - [0, 0, 0, 8], + ReturnConv::Result(None), + WireReturnFrame::new(16, [0, 0, 0, 8]), Some(ResultLayout::new(0, 1)), ); -const EXPORTS: &[WireExport] = &[ - WireExport::new_symbol( - "exports", - "foo", - "foo.raw", - &[WireExportInput::new("arg0", &EXPORT_I32_INPUT)], - Some(WireExportOutput::new(&EXPORT_DIRECT_OUTPUT)), - ), - WireExport::new_closure( - "exports", - "closure", - 0x1_0000_000c, - &[WireExportInput::new("data", &EXPORT_PTR_INPUT)], - Some(WireExportOutput::new(&EXPORT_RESULT_UNIT_OUTPUT)), - ), -]; +const EXPORTS: &[WireExport] = &[WireExport::new_symbol( + "exports", + "foo", + "foo.raw", + &[WireExportInput::new(&EXPORT_I32_INPUT)], + Some(WireExportOutput::new(&EXPORT_DIRECT_OUTPUT)), +)]; const EXPORT_WIRE: Wire = Wire::exports_with(PointerWidth::Wasm64, EXPORTS); const EXPORT_LEN: usize = wire_blob_len(&EXPORT_WIRE); const EXPORT_RECORD: WireRecord = WireRecord::new(&EXPORT_WIRE); +const CLOSURE_FACTORY_INPUT_TYPE: WireImportInputType = WireImportInputType::new( + [I64, None, None, None], + Some(IntoJsConv::new("BigInt.asUintN(64, $slot1)")), +); +const CLOSURE_FACTORY_OUTPUT_TYPE: WireImportOutputType = WireImportOutputType::new( + ReturnMode::Direct, + ReturnConv::Value(None), + None, + [CONVERTED_I32, None, None, None], +); +const CLOSURE_FACTORY: WireClosureFactory = WireClosureFactory::new( + "closures.example@1.0.0:module:10:4:mutable", + JsEmbed::new("js_sys", "closure.make_mut"), + &CLOSURE_FACTORY_INPUT_TYPE, + &CLOSURE_FACTORY_OUTPUT_TYPE, +); +const CLOSURE_DISPATCHER_DATA_TYPE: WireExportInputType = + WireExportInputType::new([I64, None, None, None], None); +const CLOSURE_INPUTS: &[WireExportInput] = &[ + WireExportInput::new(&CLOSURE_DISPATCHER_DATA_TYPE), + WireExportInput::new(&EXPORT_I32_INPUT), +]; +const CLOSURE: WireClosure = WireClosure::new( + CLOSURE_FACTORY, + 8, + CLOSURE_INPUTS, + Some(WireExportOutput::new(&EXPORT_RESULT_UNIT_OUTPUT)), +); +const CLOSURE_WIRE: Wire = Wire::closure_with(PointerWidth::Wasm64, CLOSURE); +const CLOSURE_LEN: usize = wire_blob_len(&CLOSURE_WIRE); +const CLOSURE_RECORD: WireRecord = WireRecord::new(&CLOSURE_WIRE); + #[test] fn imports() { let Record::Imports(group) = decode(IMPORT_RECORD.as_bytes()).unwrap() else { @@ -190,33 +206,22 @@ fn imports() { }; let imports = &group.imports; assert_eq!(imports.len(), 2); - assert_eq!(imports[0].module, "js_sys"); - assert_eq!(imports[0].name, "number.identity"); - assert_eq!(imports[0].inputs[0].name, "arg0"); + assert_eq!( + (imports[0].module, imports[0].name), + ("js_sys", "number.identity") + ); assert_eq!(imports[0].inputs[0].js_conversion, Some("$slot1")); let binding = imports[0].binding.as_ref().unwrap(); - assert_eq!(binding.direct_expression, Some("globalThis.identity")); - assert_eq!(binding.call_expression, "globalThis.identity(arg0)"); assert_eq!( binding.embeds, - [ - Embed { - module: "js_sys", - name: "input.convert", - }, - Embed { - module: "js_sys", - name: "identity", - }, - ] + [Embed { + module: "js_sys", + name: "input.convert", + }] ); let slot = &imports[0].inputs[0].slots[0]; - assert_eq!(slot.rust, WatType::I32); - assert_eq!(slot.js(), WatType::ExternRef); + assert_eq!((slot.rust, slot.js()), (WatType::I32, WatType::ExternRef)); let conversion = slot.wat.as_ref().unwrap(); - assert_eq!(conversion.js, WatType::ExternRef); - assert_eq!(conversion.imports.len(), 4); - assert_eq!(conversion.imports[0].identifier, "support.function"); assert!(matches!( &conversion.imports[0].kind, WatImportKind::Function { @@ -224,15 +229,6 @@ fn imports() { results, } if parameters == &[WatType::I32] && results == &[WatType::ExternRef] )); - assert!(matches!( - &conversion.imports[1].kind, - WatImportKind::Table { - index_type: WatIndexType::I32, - minimum: 2, - maximum: None, - element: RefType::ExternRef, - } - )); assert!(matches!( &conversion.imports[2].kind, WatImportKind::Table { @@ -260,8 +256,6 @@ fn imports() { ] ); assert!(imports[1].suspending); - assert_eq!(imports[0].shim_kind, ImportShimKind::ClosureFactory); - assert_eq!(imports[1].shim_kind, ImportShimKind::Normal); let Some(ImportCatch::Wasm(catch)) = &group.catch else { panic!("expected Wasm catch metadata"); }; @@ -273,8 +267,6 @@ fn imports() { ); assert_eq!(catch.catch, ") local.set $support.index"); let binding = imports[1].binding.as_ref().unwrap(); - assert_eq!(binding.direct_expression, None); - assert_eq!(binding.call_expression, "globalThis.wide()"); assert_eq!( binding.embeds, [ @@ -291,32 +283,113 @@ fn imports() { let ImportOutputAbi::Indirect { retptr, .. } = &imports[1].output.as_ref().unwrap().abi else { panic!("expected indirect output"); }; - assert_eq!(retptr.slot.rust, WatType::I32); - assert_eq!(retptr.slot.js(), WatType::I32); assert_eq!(retptr.js_conversion, Some("$slot1 >>> 0")); } #[test] -fn javascript_catch_roundtrip() { - const TABLE: WireImportTypeTable = WireImportTypeTable::new( +fn import_bindings() { + const fn import(name: &'static str, binding: WireImportBinding) -> WireImport { + WireImport::new("test", name, &[], None, Some(binding), false) + } + + const TARGET: WireGlobalPath = WireGlobalPath::new(Some("namespace"), Some("Object"), "member"); + const EMBED: JsEmbed = JsEmbed::new("support", "call"); + const TABLE: WireImportTypeTable = + WireImportTypeTable::new(&IMPORT_RETPTR_TYPE, &[], &[], JS_CATCH); + const IMPORTS: &[WireImport] = &[ + import( + "call_global", + WireImportBinding::CallGlobal { + target: TARGET, + variadic: true, + }, + ), + import( + "call_method", + WireImportBinding::CallMethod { + name: "method", + variadic: false, + }, + ), + import("index_get", WireImportBinding::IndexGet), + import("call_embed", WireImportBinding::CallEmbed(EMBED)), + ]; + const WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &TABLE, IMPORTS); + const LEN: usize = wire_blob_len(&WIRE); + const RECORD: WireRecord = WireRecord::new(&WIRE); + + let Record::Imports(group) = decode(RECORD.as_bytes()).unwrap() else { + panic!("expected imports"); + }; + let kinds: Vec<_> = group + .imports + .iter() + .map(|import| import.binding.as_ref().unwrap().kind) + .collect(); + let target = GlobalPath { + namespace: Some("namespace"), + object: Some("Object"), + name: "member", + }; + assert_eq!( + kinds, + [ + ImportBindingKind::CallGlobal { + target, + variadic: true, + }, + ImportBindingKind::CallMethod { + name: "method", + variadic: false, + }, + ImportBindingKind::IndexGet, + ImportBindingKind::CallEmbed(Embed { + module: "support", + name: "call", + }), + ] + ); + let embed_binding = group.imports.last().unwrap().binding.as_ref().unwrap(); + assert_eq!( + embed_binding.embeds, + [Embed { + module: "support", + name: "call", + }] + ); +} + +#[test] +fn javascript_catch() { + const RESULT_TABLE: WireImportTypeTable = WireImportTypeTable::new( &IMPORT_RETPTR_TYPE, IMPORT_INPUT_TYPES, IMPORT_OUTPUT_TYPES, JS_CATCH, ); - const IMPORTS: &[WireImport] = &[WireImport::new( + const RESULT_IMPORTS: &[WireImport] = &[WireImport::new( "support", "fallible", &[], Some(WireImportOutput::new(1)), - Some(WireImportBinding::new(None, "fallible()", &[])), + Some(WireImportBinding::CallGlobal { + target: WireGlobalPath::new(None, None, "fallible"), + variadic: false, + }), false, )]; - const WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &TABLE, IMPORTS); - const LEN: usize = wire_blob_len(&WIRE); - const RECORD: WireRecord = WireRecord::new(&WIRE); + const RESULT_WIRE: Wire = + Wire::imports_with(PointerWidth::Wasm32, &RESULT_TABLE, RESULT_IMPORTS); + const RESULT_LEN: usize = wire_blob_len(&RESULT_WIRE); + const RESULT_RECORD: WireRecord = WireRecord::new(&RESULT_WIRE); + const PLAIN_OUTPUTS: &[&WireImportOutputType] = &[&IMPORT_OUTPUT_TYPE_DIRECT]; + const PLAIN_TABLE: WireImportTypeTable = + WireImportTypeTable::new(&IMPORT_RETPTR_TYPE, &[], PLAIN_OUTPUTS, JS_CATCH); + const PLAIN_WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &PLAIN_TABLE, &[]); + const PLAIN_LEN: usize = wire_blob_len(&PLAIN_WIRE); + const PLAIN_RECORD: WireRecord = WireRecord::new(&PLAIN_WIRE); - let Record::Imports(group) = decode(RECORD.as_bytes()).unwrap() else { + let Record::Imports(group) = decode(RESULT_RECORD.as_bytes()).unwrap() else { panic!("expected imports"); }; let Some(ImportCatch::JavaScript(catch)) = group.catch else { @@ -335,18 +408,8 @@ fn javascript_catch_roundtrip() { group.imports[0].output.as_ref().unwrap().error, ImportErrorMode::CatchInJavaScript ); -} -#[test] -fn catch_payload_is_omitted_without_result_types() { - const OUTPUT_TYPES: &[&WireImportOutputType] = &[&IMPORT_OUTPUT_TYPE_DIRECT]; - const TABLE: WireImportTypeTable = - WireImportTypeTable::new(&IMPORT_RETPTR_TYPE, &[], OUTPUT_TYPES, JS_CATCH); - const WIRE: Wire = Wire::imports_with(PointerWidth::Wasm32, &TABLE, &[]); - const LEN: usize = wire_blob_len(&WIRE); - const RECORD: WireRecord = WireRecord::new(&WIRE); - - let Record::Imports(group) = decode(RECORD.as_bytes()).unwrap() else { + let Record::Imports(group) = decode(PLAIN_RECORD.as_bytes()).unwrap() else { panic!("expected imports"); }; assert!(group.catch.is_none()); @@ -357,37 +420,132 @@ fn exports() { let Record::Exports(exports) = decode(EXPORT_RECORD.as_bytes()).unwrap() else { panic!("expected exports"); }; - assert_eq!(exports.len(), 2); - assert_eq!(exports[0].module, "exports"); - assert_eq!(exports[0].name, "foo"); + assert_eq!(exports.len(), 1); + assert_eq!((exports[0].module, exports[0].name), ("exports", "foo")); assert_eq!(exports[0].pointer_width, PointerWidth::Wasm64); assert_eq!(exports[0].callee, Callee::Symbol { name: "foo.raw" }); - assert_eq!(exports[0].inputs[0].name, "arg0"); assert_eq!(exports[0].inputs[0].kind, ExportInputKind::Value); assert_eq!(exports[0].inputs[0].slots[0].rust, WatType::I32); - assert_eq!(exports[0].inputs[0].slots[0].js(), WatType::I32); let Some(ExportOutput::Direct { slot, .. }) = &exports[0].output else { panic!("expected direct output"); }; assert_eq!(slot.rust, WatType::I32); - assert_eq!(slot.js(), WatType::I32); - assert!(matches!( - exports[1].callee, - Callee::Closure { - call_shim_offset: 0x1_0000_000c +} + +#[test] +fn closure() { + let Record::Closure(closure) = decode(CLOSURE_RECORD.as_bytes()).unwrap() else { + panic!("expected a closure"); + }; + assert_eq!( + closure.factory.raw_symbol, + "closures.example@1.0.0:module:10:4:mutable" + ); + assert_eq!( + closure.factory.helper, + Embed { + module: "js_sys", + name: "closure.make_mut", } - )); - let Some(ExportOutput::Indirect { frame, result, .. }) = &exports[1].output else { - panic!("expected indirect output"); + ); + assert_eq!( + closure.factory.input.js_conversion, + Some("BigInt.asUintN(64, $slot1)") + ); + assert!(closure.factory.embeds.is_empty()); + let ImportOutputAbi::Direct { slot, conversion } = &closure.factory.output.abi else { + panic!("expected a direct factory output"); }; - assert_eq!(frame.slots.len(), 2); + assert!(conversion.is_none()); + assert_eq!((slot.rust, slot.js()), (WatType::I32, WatType::ExternRef)); + + assert_eq!(closure.pointer_width, PointerWidth::Wasm64); assert_eq!( - *result, - Some(ResultLayout { - discriminant: 0, - error: 1 - }) + closure + .inputs + .iter() + .map(|input| input.kind) + .collect::>(), + [ExportInputKind::ClosureData, ExportInputKind::Value] ); + assert_eq!(closure.call_shim_offset, 8); + let Some(ExportOutput::Indirect { frame, result, .. }) = &closure.output else { + panic!("expected an indirect dispatcher output"); + }; + assert_eq!(frame.size, 16); + assert_eq!( + frame + .slots + .iter() + .map(|slot| slot.offset) + .collect::>(), + [0, 8] + ); + assert_eq!(*result, Some(ResultLayout::new(0, 1))); +} + +#[test] +fn closure_identity() { + const OTHER_ORIGIN: WireClosureFactory = WireClosureFactory::new( + "another.call.site", + JsEmbed::new("js_sys", "closure.make_mut"), + &CLOSURE_FACTORY_INPUT_TYPE, + &CLOSURE_FACTORY_OUTPUT_TYPE, + ); + const OTHER_ORIGIN_CLOSURE: WireClosure = WireClosure::new( + OTHER_ORIGIN, + 8, + CLOSURE_INPUTS, + Some(WireExportOutput::new(&EXPORT_RESULT_UNIT_OUTPUT)), + ); + const OTHER_ORIGIN_WIRE: Wire = Wire::closure_with(PointerWidth::Wasm64, OTHER_ORIGIN_CLOSURE); + const OTHER_ORIGIN_LEN: usize = wire_blob_len(&OTHER_ORIGIN_WIRE); + const OTHER_ORIGIN_RECORD: WireRecord = WireRecord::new(&OTHER_ORIGIN_WIRE); + + const OTHER_HELPER: WireClosureFactory = WireClosureFactory::new( + "another.call.site", + JsEmbed::new("js_sys", "closure.make_once"), + &CLOSURE_FACTORY_INPUT_TYPE, + &CLOSURE_FACTORY_OUTPUT_TYPE, + ); + const OTHER_HELPER_CLOSURE: WireClosure = WireClosure::new( + OTHER_HELPER, + 8, + CLOSURE_INPUTS, + Some(WireExportOutput::new(&EXPORT_RESULT_UNIT_OUTPUT)), + ); + const OTHER_HELPER_WIRE: Wire = Wire::closure_with(PointerWidth::Wasm64, OTHER_HELPER_CLOSURE); + const OTHER_HELPER_LEN: usize = wire_blob_len(&OTHER_HELPER_WIRE); + const OTHER_HELPER_RECORD: WireRecord = WireRecord::new(&OTHER_HELPER_WIRE); + + const OTHER_CALL: WireClosure = WireClosure::new( + CLOSURE_FACTORY, + 16, + CLOSURE_INPUTS, + Some(WireExportOutput::new(&EXPORT_RESULT_UNIT_OUTPUT)), + ); + const OTHER_CALL_WIRE: Wire = Wire::closure_with(PointerWidth::Wasm64, OTHER_CALL); + const OTHER_CALL_LEN: usize = wire_blob_len(&OTHER_CALL_WIRE); + const OTHER_CALL_RECORD: WireRecord = WireRecord::new(&OTHER_CALL_WIRE); + + let Record::Closure(original) = decode(CLOSURE_RECORD.as_bytes()).unwrap() else { + panic!("expected a closure"); + }; + let Record::Closure(other_origin) = decode(OTHER_ORIGIN_RECORD.as_bytes()).unwrap() else { + panic!("expected a closure"); + }; + let Record::Closure(other_helper) = decode(OTHER_HELPER_RECORD.as_bytes()).unwrap() else { + panic!("expected a closure"); + }; + let Record::Closure(other_call) = decode(OTHER_CALL_RECORD.as_bytes()).unwrap() else { + panic!("expected a closure"); + }; + + assert_eq!(original.call_identity, other_origin.call_identity); + assert_ne!(original.factory.raw_symbol, other_origin.factory.raw_symbol); + assert_ne!(original.factory.helper, other_helper.factory.helper); + assert_eq!(original.call_identity, other_helper.call_identity); + assert_ne!(original.call_identity, other_call.call_identity); } #[test] @@ -406,6 +564,8 @@ fn invalid_records() { &ErrorKind::UnknownRecordKind(0xff) ); + // Eight bytes is a valid pointer width, but it conflicts with this record's + // 32-bit return pointer. let mut pointer_width = IMPORT_RECORD.as_bytes().to_vec(); pointer_width[10] = 8; assert!(matches!( From c20f6ffd3f78c4190175ceff2c30f0be2cf46c6d Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 20:05:32 +0800 Subject: [PATCH 20/21] Refine constructor and unit return handling --- client/e2e/examples/closure.rs | 8 + client/e2e/examples/primitive.rs | 5 + client/js-sys/src/builtins/array_buffer.rs | 8 +- client/js-sys/src/builtins/data_view.rs | 6 +- client/js-sys/src/builtins/date.rs | 2 +- .../src/builtins/finalization_registry.rs | 2 +- client/js-sys/src/builtins/function.rs | 4 +- client/js-sys/src/builtins/intl/collator.rs | 4 +- .../src/builtins/intl/date_time_format.rs | 4 +- .../js-sys/src/builtins/intl/display_names.rs | 2 +- .../src/builtins/intl/duration_format.rs | 4 +- .../js-sys/src/builtins/intl/list_format.rs | 4 +- client/js-sys/src/builtins/intl/locale.rs | 4 +- .../js-sys/src/builtins/intl/number_format.rs | 4 +- .../js-sys/src/builtins/intl/plural_rules.rs | 4 +- .../src/builtins/intl/relative_time_format.rs | 4 +- client/js-sys/src/builtins/intl/segmenter.rs | 4 +- client/js-sys/src/builtins/map.rs | 2 +- client/js-sys/src/builtins/proxy.rs | 2 +- client/js-sys/src/builtins/regexp.rs | 6 +- client/js-sys/src/builtins/set.rs | 2 +- .../js-sys/src/builtins/temporal/duration.rs | 20 +- .../js-sys/src/builtins/temporal/instant.rs | 2 +- .../src/builtins/temporal/plain_date.rs | 4 +- .../src/builtins/temporal/plain_date_time.rs | 16 +- .../src/builtins/temporal/plain_month_day.rs | 6 +- .../src/builtins/temporal/plain_time.rs | 12 +- .../src/builtins/temporal/plain_year_month.rs | 6 +- .../src/builtins/temporal/zoned_date_time.rs | 4 +- client/js-sys/src/builtins/typed_array.rs | 16 +- client/js-sys/src/builtins/weak_map.rs | 5 +- client/js-sys/src/builtins/weak_ref.rs | 5 +- client/js-sys/src/builtins/weak_set.rs | 5 +- .../src/builtins/webassembly/exception.rs | 6 +- .../js-sys/src/builtins/webassembly/global.rs | 4 +- .../src/builtins/webassembly/instance.rs | 4 +- .../js-sys/src/builtins/webassembly/jspi.rs | 2 +- .../js-sys/src/builtins/webassembly/memory.rs | 2 +- .../js-sys/src/builtins/webassembly/module.rs | 4 +- .../js-sys/src/builtins/webassembly/table.rs | 4 +- client/js-sys/src/hazard.rs | 13 +- client/js-sys/src/wire/export.rs | 6 + host/js-sys-bindgen/src/closure.rs | 3 - host/js-sys-bindgen/src/export.rs | 3 - host/js-sys-bindgen/src/function.rs | 250 +++++++++++++----- host/js-sys-bindgen/src/function/js.rs | 37 ++- host/js-sys-bindgen/src/function/options.rs | 33 ++- host/js-sys-bindgen/src/macro.rs | 11 +- .../src/tests/macro/function.rs | 150 ++++++++++- host/js-sys-bindgen/src/tests/macro/member.rs | 22 ++ host/js-sys-bindgen/src/tests/macro/type.rs | 93 ++++++- host/js-sys-bindgen/src/type.rs | 57 +++- host/ld/src/wire/closure.rs | 2 +- host/ld/src/wire/export/js.rs | 33 ++- host/ld/src/wire/export/wat.rs | 54 +++- host/wire/src/decode/export.rs | 11 +- host/wire/src/model.rs | 8 +- host/wire/src/schema.rs | 7 +- host/wire/src/tests.rs | 35 ++- 59 files changed, 818 insertions(+), 222 deletions(-) diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs index 48c22a61..1c1e5be7 100644 --- a/client/e2e/examples/closure.rs +++ b/client/e2e/examples/closure.rs @@ -18,6 +18,7 @@ fn main() { // ;; exports["closure_option_ref"](20) // ;; exports["closure_option_owned"](20) // ;; (() => { const callback = exports["closure_return"](2); return callback(40) === 42 })() + // ;; exports["closure_unit_alias"]()() === undefined // ;; exports["closure_error_lifecycle"]() // ;; exports["closure_once_lifecycle"]() // ;; typeof globalThis.gc !== "function" || typeof FinalizationRegistry === "undefined" || await (async () => { let callback = exports["closure_finalization"](); const unref = callback.unref; callback = null; for (let i = 0; i < 100 && exports["closure_finalization_drops"]() === 0; i++) { globalThis.gc(); await new Promise(resolve => globalThis.setTimeout(resolve, 0)) } if (exports["closure_finalization_drops"]() !== 1) return false; unref(); return exports["closure_finalization_drops"]() === 1 })() @@ -28,6 +29,8 @@ use std::sync::atomic::{AtomicU32, Ordering}; use js_sys::{Closure, JsString, JsValue, closure, js_sys}; +type Unit = (); + static DROPS: AtomicU32 = AtomicU32::new(0); struct DropCounter; @@ -356,6 +359,11 @@ fn closure_return(offset: i32) -> Closure i32> { closure!(dyn FnMut(i32) -> i32, move |value| value + offset) } +#[js_sys] +fn closure_unit_alias() -> Closure Unit> { + closure!(dyn FnMut() -> Unit, || {}) +} + #[js_sys] fn closure_error_lifecycle() -> bool { DROPS.store(0, Ordering::Relaxed); diff --git a/client/e2e/examples/primitive.rs b/client/e2e/examples/primitive.rs index 50d0bfa9..41bd45bf 100644 --- a/client/e2e/examples/primitive.rs +++ b/client/e2e/examples/primitive.rs @@ -15,6 +15,7 @@ fn main() { // ;; exports["add_u128_ref"](1n << 96n, 3n) === (1n << 96n) + 3n // ;; exports["not_bool_ref"](false) === true // ;; (() => { const value = exports["usize_max"](); return value === (typeof value === "bigint" ? 0xffff_ffff_ffff_ffffn : 0xffff_ffff) })() + // ;; exports["unit_alias"]() === undefined // ;; exports["option_bool"](undefined) === undefined // ;; exports["option_bool"](false) === true // ;; exports["option_unit"](undefined) === undefined @@ -64,6 +65,7 @@ fn main() { use js_sys::{JsString, JsValue, js_sys}; type JsResult = Result; +type Unit = (); js_sys::js_bindgen::embed_js!( module = "primitive", @@ -71,6 +73,9 @@ js_sys::js_bindgen::embed_js!( "(value) => value", ); +#[js_sys] +fn unit_alias() -> Unit {} + js_sys::js_bindgen::embed_js!( module = "primitive", name = "result.unit", diff --git a/client/js-sys/src/builtins/array_buffer.rs b/client/js-sys/src/builtins/array_buffer.rs index b7e6abb9..6bdf5f12 100644 --- a/client/js-sys/src/builtins/array_buffer.rs +++ b/client/js-sys/src/builtins/array_buffer.rs @@ -37,11 +37,11 @@ extern "js-sys" { pub type ArrayBuffer; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer) - #[js_sys(constructor)] + #[js_sys(constructor = ArrayBuffer)] pub fn new(byte_length: f64) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/ArrayBuffer) - #[js_sys(constructor)] + #[js_sys(constructor = ArrayBuffer)] pub fn new_with_options( byte_length: f64, options: &ArrayBufferOptions, @@ -112,11 +112,11 @@ extern "js-sys" { pub type SharedArrayBuffer; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer) - #[js_sys(constructor)] + #[js_sys(constructor = SharedArrayBuffer)] pub fn new(byte_length: f64) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer) - #[js_sys(constructor)] + #[js_sys(constructor = SharedArrayBuffer)] pub fn new_with_options( byte_length: f64, options: &ArrayBufferOptions, diff --git a/client/js-sys/src/builtins/data_view.rs b/client/js-sys/src/builtins/data_view.rs index 6e01d7ba..0de8c47c 100644 --- a/client/js-sys/src/builtins/data_view.rs +++ b/client/js-sys/src/builtins/data_view.rs @@ -9,15 +9,15 @@ extern "js-sys" { pub type DataView; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView) - #[js_sys(constructor)] + #[js_sys(constructor = DataView)] pub fn new(buffer: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView) - #[js_sys(constructor)] + #[js_sys(constructor = DataView)] pub fn new_with_offset(buffer: &JsValue, byte_offset: f64) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView) - #[js_sys(constructor)] + #[js_sys(constructor = DataView)] pub fn new_with_offset_and_length( buffer: &JsValue, byte_offset: f64, diff --git a/client/js-sys/src/builtins/date.rs b/client/js-sys/src/builtins/date.rs index 97cbdf7f..8bcb018c 100644 --- a/client/js-sys/src/builtins/date.rs +++ b/client/js-sys/src/builtins/date.rs @@ -15,7 +15,7 @@ extern "js-sys" { pub fn new() -> Date; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) - #[js_sys(constructor)] + #[js_sys(constructor = Date)] pub fn new_with_value(value: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date) diff --git a/client/js-sys/src/builtins/finalization_registry.rs b/client/js-sys/src/builtins/finalization_registry.rs index 7f43ecbe..de827f1e 100644 --- a/client/js-sys/src/builtins/finalization_registry.rs +++ b/client/js-sys/src/builtins/finalization_registry.rs @@ -9,7 +9,7 @@ extern "js-sys" { pub type FinalizationRegistry; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry/FinalizationRegistry) - #[js_sys(constructor)] + #[js_sys(constructor = FinalizationRegistry)] pub fn new(cleanup_callback: &Function) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry/register) diff --git a/client/js-sys/src/builtins/function.rs b/client/js-sys/src/builtins/function.rs index dc302de9..abadd6b4 100644 --- a/client/js-sys/src/builtins/function.rs +++ b/client/js-sys/src/builtins/function.rs @@ -9,11 +9,11 @@ extern "js-sys" { pub type Function; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) - #[js_sys(constructor)] + #[js_sys(constructor = Function)] pub fn new_no_args(body: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) - #[js_sys(constructor)] + #[js_sys(constructor = Function)] pub fn new_with_args(args: &str, body: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) diff --git a/client/js-sys/src/builtins/intl/collator.rs b/client/js-sys/src/builtins/intl/collator.rs index d19205a2..cbb73c21 100644 --- a/client/js-sys/src/builtins/intl/collator.rs +++ b/client/js-sys/src/builtins/intl/collator.rs @@ -108,11 +108,11 @@ extern "js-sys" { pub fn new() -> Collator; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator) - #[js_sys(constructor)] + #[js_sys(constructor = Collator)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator) - #[js_sys(constructor)] + #[js_sys(constructor = Collator)] pub fn new_with_locales_and_options( locales: &JsValue, options: &CollatorOptions, diff --git a/client/js-sys/src/builtins/intl/date_time_format.rs b/client/js-sys/src/builtins/intl/date_time_format.rs index 97cea3cc..e0e8cb70 100644 --- a/client/js-sys/src/builtins/intl/date_time_format.rs +++ b/client/js-sys/src/builtins/intl/date_time_format.rs @@ -207,11 +207,11 @@ extern "js-sys" { pub fn new() -> DateTimeFormat; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) - #[js_sys(constructor)] + #[js_sys(constructor = DateTimeFormat)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) - #[js_sys(constructor)] + #[js_sys(constructor = DateTimeFormat)] pub fn new_with_locales_and_options( locales: &JsValue, options: &DateTimeFormatOptions, diff --git a/client/js-sys/src/builtins/intl/display_names.rs b/client/js-sys/src/builtins/intl/display_names.rs index 34cfe686..ba986769 100644 --- a/client/js-sys/src/builtins/intl/display_names.rs +++ b/client/js-sys/src/builtins/intl/display_names.rs @@ -133,7 +133,7 @@ extern "js-sys" { pub type DisplayNamesResolvedOptions; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames) - #[js_sys(constructor)] + #[js_sys(constructor = DisplayNames)] pub fn new(locales: &JsValue, options: &DisplayNamesOptions) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/supportedLocalesOf) diff --git a/client/js-sys/src/builtins/intl/duration_format.rs b/client/js-sys/src/builtins/intl/duration_format.rs index 03142ee6..a94c34ae 100644 --- a/client/js-sys/src/builtins/intl/duration_format.rs +++ b/client/js-sys/src/builtins/intl/duration_format.rs @@ -168,11 +168,11 @@ extern "js-sys" { pub fn new() -> DurationFormat; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat) - #[js_sys(constructor)] + #[js_sys(constructor = DurationFormat)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat) - #[js_sys(constructor)] + #[js_sys(constructor = DurationFormat)] pub fn new_with_locales_and_options( locales: &JsValue, options: &DurationFormatOptions, diff --git a/client/js-sys/src/builtins/intl/list_format.rs b/client/js-sys/src/builtins/intl/list_format.rs index 98c7766b..0c1f4bea 100644 --- a/client/js-sys/src/builtins/intl/list_format.rs +++ b/client/js-sys/src/builtins/intl/list_format.rs @@ -86,11 +86,11 @@ extern "js-sys" { pub fn new() -> ListFormat; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat) - #[js_sys(constructor)] + #[js_sys(constructor = ListFormat)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat) - #[js_sys(constructor)] + #[js_sys(constructor = ListFormat)] pub fn new_with_locales_and_options( locales: &JsValue, options: &ListFormatOptions, diff --git a/client/js-sys/src/builtins/intl/locale.rs b/client/js-sys/src/builtins/intl/locale.rs index b5eb4353..b9b353f0 100644 --- a/client/js-sys/src/builtins/intl/locale.rs +++ b/client/js-sys/src/builtins/intl/locale.rs @@ -113,11 +113,11 @@ extern "js-sys" { pub type TextInfo; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale) - #[js_sys(constructor)] + #[js_sys(constructor = Locale)] pub fn new(tag: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale) - #[js_sys(constructor)] + #[js_sys(constructor = Locale)] pub fn new_with_options(tag: &str, options: &LocaleOptions) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/baseName) diff --git a/client/js-sys/src/builtins/intl/number_format.rs b/client/js-sys/src/builtins/intl/number_format.rs index e2260224..3996f9ae 100644 --- a/client/js-sys/src/builtins/intl/number_format.rs +++ b/client/js-sys/src/builtins/intl/number_format.rs @@ -308,11 +308,11 @@ extern "js-sys" { pub fn new() -> NumberFormat; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) - #[js_sys(constructor)] + #[js_sys(constructor = NumberFormat)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) - #[js_sys(constructor)] + #[js_sys(constructor = NumberFormat)] pub fn new_with_locales_and_options( locales: &JsValue, options: &NumberFormatOptions, diff --git a/client/js-sys/src/builtins/intl/plural_rules.rs b/client/js-sys/src/builtins/intl/plural_rules.rs index dcca7122..96954663 100644 --- a/client/js-sys/src/builtins/intl/plural_rules.rs +++ b/client/js-sys/src/builtins/intl/plural_rules.rs @@ -210,11 +210,11 @@ extern "js-sys" { pub fn new() -> PluralRules; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules) - #[js_sys(constructor)] + #[js_sys(constructor = PluralRules)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules) - #[js_sys(constructor)] + #[js_sys(constructor = PluralRules)] pub fn new_with_locales_and_options( locales: &JsValue, options: &PluralRulesOptions, diff --git a/client/js-sys/src/builtins/intl/relative_time_format.rs b/client/js-sys/src/builtins/intl/relative_time_format.rs index f2d6ea87..9c2c1c32 100644 --- a/client/js-sys/src/builtins/intl/relative_time_format.rs +++ b/client/js-sys/src/builtins/intl/relative_time_format.rs @@ -111,11 +111,11 @@ extern "js-sys" { pub fn new() -> RelativeTimeFormat; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat) - #[js_sys(constructor)] + #[js_sys(constructor = RelativeTimeFormat)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat) - #[js_sys(constructor)] + #[js_sys(constructor = RelativeTimeFormat)] pub fn new_with_locales_and_options( locales: &JsValue, options: &RelativeTimeFormatOptions, diff --git a/client/js-sys/src/builtins/intl/segmenter.rs b/client/js-sys/src/builtins/intl/segmenter.rs index ce7ff3e1..99827925 100644 --- a/client/js-sys/src/builtins/intl/segmenter.rs +++ b/client/js-sys/src/builtins/intl/segmenter.rs @@ -64,11 +64,11 @@ extern "js-sys" { pub fn new() -> Segmenter; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter) - #[js_sys(constructor)] + #[js_sys(constructor = Segmenter)] pub fn new_with_locales(locales: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter) - #[js_sys(constructor)] + #[js_sys(constructor = Segmenter)] pub fn new_with_locales_and_options( locales: &JsValue, options: &SegmenterOptions, diff --git a/client/js-sys/src/builtins/map.rs b/client/js-sys/src/builtins/map.rs index c7919c2b..45f7913c 100644 --- a/client/js-sys/src/builtins/map.rs +++ b/client/js-sys/src/builtins/map.rs @@ -21,7 +21,7 @@ extern "js-sys" { pub fn new_typed() -> Map; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) - #[js_sys(constructor, return_abi = Result)] + #[js_sys(constructor = Map, return_abi = Result)] pub fn new_from_iterable( #[js_sys(type = &JsValue)] entries: &I, ) -> Result, JsValue>; diff --git a/client/js-sys/src/builtins/proxy.rs b/client/js-sys/src/builtins/proxy.rs index f951fda9..a09adfbb 100644 --- a/client/js-sys/src/builtins/proxy.rs +++ b/client/js-sys/src/builtins/proxy.rs @@ -14,7 +14,7 @@ extern "js-sys" { pub type ProxyRevocable; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) - #[js_sys(constructor)] + #[js_sys(constructor = Proxy)] pub fn new(target: &JsValue, handler: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable) diff --git a/client/js-sys/src/builtins/regexp.rs b/client/js-sys/src/builtins/regexp.rs index 3e6f50e4..1fa1b06e 100644 --- a/client/js-sys/src/builtins/regexp.rs +++ b/client/js-sys/src/builtins/regexp.rs @@ -9,11 +9,11 @@ extern "js-sys" { pub type RegExp; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) - #[js_sys(constructor)] + #[js_sys(constructor = RegExp)] pub fn new(pattern: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) - #[js_sys(constructor)] + #[js_sys(constructor = RegExp)] pub fn new_with_flags(pattern: &str, flags: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) @@ -22,7 +22,7 @@ extern "js-sys" { pub fn new_from_regexp(pattern: &RegExp) -> RegExp; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) - #[js_sys(constructor)] + #[js_sys(constructor = RegExp)] pub fn new_from_regexp_with_flags(pattern: &RegExp, flags: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape) diff --git a/client/js-sys/src/builtins/set.rs b/client/js-sys/src/builtins/set.rs index b9c60f6e..8f154c83 100644 --- a/client/js-sys/src/builtins/set.rs +++ b/client/js-sys/src/builtins/set.rs @@ -21,7 +21,7 @@ extern "js-sys" { pub fn new_typed() -> Set; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) - #[js_sys(constructor, return_abi = Result)] + #[js_sys(constructor = Set, return_abi = Result)] pub fn new_from_iterable>( #[js_sys(type = &JsValue)] items: &I, ) -> Result, JsValue>; diff --git a/client/js-sys/src/builtins/temporal/duration.rs b/client/js-sys/src/builtins/temporal/duration.rs index e19b26fd..285675f7 100644 --- a/client/js-sys/src/builtins/temporal/duration.rs +++ b/client/js-sys/src/builtins/temporal/duration.rs @@ -13,15 +13,15 @@ extern "js-sys" { pub fn new() -> Duration; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years(years: f64) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months(years: f64, months: f64) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks( years: f64, months: f64, @@ -29,7 +29,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks_days( years: f64, months: f64, @@ -38,7 +38,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks_days_hours( years: f64, months: f64, @@ -48,7 +48,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks_days_hours_minutes( years: f64, months: f64, @@ -59,7 +59,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/Duration) - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks_days_hours_minutes_seconds( years: f64, months: f64, @@ -75,7 +75,7 @@ extern "js-sys" { clippy::too_many_arguments, reason = "matches the JavaScript constructor" )] - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks_days_hours_minutes_seconds_milliseconds( years: f64, months: f64, @@ -92,7 +92,7 @@ extern "js-sys" { clippy::too_many_arguments, reason = "matches the JavaScript constructor" )] - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_years_months_weeks_days_hours_minutes_seconds_milliseconds_microseconds( years: f64, months: f64, @@ -110,7 +110,7 @@ extern "js-sys" { clippy::too_many_arguments, reason = "matches the JavaScript constructor" )] - #[js_sys(constructor)] + #[js_sys(constructor = Duration)] pub fn new_with_values( years: f64, months: f64, diff --git a/client/js-sys/src/builtins/temporal/instant.rs b/client/js-sys/src/builtins/temporal/instant.rs index f93b12fb..07aedc45 100644 --- a/client/js-sys/src/builtins/temporal/instant.rs +++ b/client/js-sys/src/builtins/temporal/instant.rs @@ -10,7 +10,7 @@ extern "js-sys" { pub type Instant; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/Instant) - #[js_sys(constructor)] + #[js_sys(constructor = Instant)] pub fn new(epoch_nanoseconds: &BigInt) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/from) diff --git a/client/js-sys/src/builtins/temporal/plain_date.rs b/client/js-sys/src/builtins/temporal/plain_date.rs index 35e9a4eb..1eedc937 100644 --- a/client/js-sys/src/builtins/temporal/plain_date.rs +++ b/client/js-sys/src/builtins/temporal/plain_date.rs @@ -13,11 +13,11 @@ extern "js-sys" { pub type PlainDate; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/PlainDate) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDate)] pub fn new(iso_year: i32, iso_month: u32, iso_day: u32) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/PlainDate) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDate)] pub fn new_with_calendar( iso_year: i32, iso_month: u32, diff --git a/client/js-sys/src/builtins/temporal/plain_date_time.rs b/client/js-sys/src/builtins/temporal/plain_date_time.rs index c46e12a5..ad4a20de 100644 --- a/client/js-sys/src/builtins/temporal/plain_date_time.rs +++ b/client/js-sys/src/builtins/temporal/plain_date_time.rs @@ -12,11 +12,11 @@ extern "js-sys" { pub type PlainDateTime; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new(iso_year: i32, iso_month: u32, iso_day: u32) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_year_month_day_hour( iso_year: i32, iso_month: u32, @@ -25,7 +25,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_year_month_day_hour_minute( iso_year: i32, iso_month: u32, @@ -35,7 +35,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_year_month_day_hour_minute_second( iso_year: i32, iso_month: u32, @@ -46,7 +46,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/PlainDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_year_month_day_hour_minute_second_millisecond( iso_year: i32, iso_month: u32, @@ -62,7 +62,7 @@ extern "js-sys" { clippy::too_many_arguments, reason = "matches the JavaScript constructor" )] - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_year_month_day_hour_minute_second_millisecond_microsecond( iso_year: i32, iso_month: u32, @@ -79,7 +79,7 @@ extern "js-sys" { clippy::too_many_arguments, reason = "matches the JavaScript constructor" )] - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_year_month_day_hour_minute_second_millisecond_microsecond_nanosecond( iso_year: i32, iso_month: u32, @@ -97,7 +97,7 @@ extern "js-sys" { clippy::too_many_arguments, reason = "matches the JavaScript constructor" )] - #[js_sys(constructor)] + #[js_sys(constructor = PlainDateTime)] pub fn new_with_values( iso_year: i32, iso_month: u32, diff --git a/client/js-sys/src/builtins/temporal/plain_month_day.rs b/client/js-sys/src/builtins/temporal/plain_month_day.rs index 824dec45..43137b6c 100644 --- a/client/js-sys/src/builtins/temporal/plain_month_day.rs +++ b/client/js-sys/src/builtins/temporal/plain_month_day.rs @@ -9,11 +9,11 @@ extern "js-sys" { pub type PlainMonthDay; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/PlainMonthDay) - #[js_sys(constructor)] + #[js_sys(constructor = PlainMonthDay)] pub fn new(iso_month: u32, iso_day: u32) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/PlainMonthDay) - #[js_sys(constructor)] + #[js_sys(constructor = PlainMonthDay)] pub fn new_with_calendar( iso_month: u32, iso_day: u32, @@ -21,7 +21,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay/PlainMonthDay) - #[js_sys(constructor)] + #[js_sys(constructor = PlainMonthDay)] pub fn new_with_calendar_and_reference_year( iso_month: u32, iso_day: u32, diff --git a/client/js-sys/src/builtins/temporal/plain_time.rs b/client/js-sys/src/builtins/temporal/plain_time.rs index 2dcfed2b..3c8aa8c8 100644 --- a/client/js-sys/src/builtins/temporal/plain_time.rs +++ b/client/js-sys/src/builtins/temporal/plain_time.rs @@ -14,15 +14,15 @@ extern "js-sys" { pub fn new() -> PlainTime; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainTime)] pub fn new_with_hour(hour: u32) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainTime)] pub fn new_with_hour_minute(hour: u32, minute: u32) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainTime)] pub fn new_with_hour_minute_second( hour: u32, minute: u32, @@ -30,7 +30,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainTime)] pub fn new_with_hour_minute_second_millisecond( hour: u32, minute: u32, @@ -39,7 +39,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainTime)] pub fn new_with_hour_minute_second_millisecond_microsecond( hour: u32, minute: u32, @@ -49,7 +49,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/PlainTime) - #[js_sys(constructor)] + #[js_sys(constructor = PlainTime)] pub fn new_with_values( hour: u32, minute: u32, diff --git a/client/js-sys/src/builtins/temporal/plain_year_month.rs b/client/js-sys/src/builtins/temporal/plain_year_month.rs index 81201130..b845d38b 100644 --- a/client/js-sys/src/builtins/temporal/plain_year_month.rs +++ b/client/js-sys/src/builtins/temporal/plain_year_month.rs @@ -10,11 +10,11 @@ extern "js-sys" { pub type PlainYearMonth; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/PlainYearMonth) - #[js_sys(constructor)] + #[js_sys(constructor = PlainYearMonth)] pub fn new(iso_year: i32, iso_month: u32) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/PlainYearMonth) - #[js_sys(constructor)] + #[js_sys(constructor = PlainYearMonth)] pub fn new_with_calendar( iso_year: i32, iso_month: u32, @@ -22,7 +22,7 @@ extern "js-sys" { ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth/PlainYearMonth) - #[js_sys(constructor)] + #[js_sys(constructor = PlainYearMonth)] pub fn new_with_calendar_and_reference_day( iso_year: i32, iso_month: u32, diff --git a/client/js-sys/src/builtins/temporal/zoned_date_time.rs b/client/js-sys/src/builtins/temporal/zoned_date_time.rs index e63a6394..aa7def29 100644 --- a/client/js-sys/src/builtins/temporal/zoned_date_time.rs +++ b/client/js-sys/src/builtins/temporal/zoned_date_time.rs @@ -13,11 +13,11 @@ extern "js-sys" { pub type ZonedDateTime; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/ZonedDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = ZonedDateTime)] pub fn new(epoch_nanoseconds: &BigInt, time_zone: &str) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/ZonedDateTime) - #[js_sys(constructor)] + #[js_sys(constructor = ZonedDateTime)] pub fn new_with_calendar( epoch_nanoseconds: &BigInt, time_zone: &str, diff --git a/client/js-sys/src/builtins/typed_array.rs b/client/js-sys/src/builtins/typed_array.rs index a96b2b02..b56b7ce9 100644 --- a/client/js-sys/src/builtins/typed_array.rs +++ b/client/js-sys/src/builtins/typed_array.rs @@ -992,24 +992,24 @@ macro_rules! typed_array { pub type $name; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = $name)] pub fn new(value: &JsValue) -> Result<$name, JsValue>; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = $name)] pub fn new_with_length( length: f64, ) -> Result<$name, JsValue>; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = $name)] pub fn new_with_byte_offset( buffer: &JsValue, byte_offset: f64, ) -> Result<$name, JsValue>; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = $name)] pub fn new_with_byte_offset_and_length( buffer: &JsValue, byte_offset: f64, @@ -1143,22 +1143,22 @@ extern "js-sys" { pub type Float16Array; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = Float16Array)] pub fn new(value: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = Float16Array)] pub fn new_with_length(length: f64) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = Float16Array)] pub fn new_with_byte_offset( buffer: &JsValue, byte_offset: f64, ) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/TypedArray) - #[js_sys(constructor)] + #[js_sys(constructor = Float16Array)] pub fn new_with_byte_offset_and_length( buffer: &JsValue, byte_offset: f64, diff --git a/client/js-sys/src/builtins/weak_map.rs b/client/js-sys/src/builtins/weak_map.rs index a8db9afd..778a7be7 100644 --- a/client/js-sys/src/builtins/weak_map.rs +++ b/client/js-sys/src/builtins/weak_map.rs @@ -21,7 +21,10 @@ extern "js-sys" { pub fn new_typed() -> WeakMap; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/WeakMap) - #[js_sys(constructor, return_abi = Result)] + #[js_sys( + constructor = WeakMap, + return_abi = Result + )] pub fn new_from_iterable( #[js_sys(type = &JsValue)] entries: &I, ) -> Result, JsValue>; diff --git a/client/js-sys/src/builtins/weak_ref.rs b/client/js-sys/src/builtins/weak_ref.rs index 1e76de32..f1052ad4 100644 --- a/client/js-sys/src/builtins/weak_ref.rs +++ b/client/js-sys/src/builtins/weak_ref.rs @@ -11,7 +11,10 @@ extern "js-sys" { pub type WeakRef; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef/WeakRef) - #[js_sys(constructor, return_abi = Result)] + #[js_sys( + constructor = WeakRef, + return_abi = Result + )] pub fn new(#[js_sys(type = &JsValue)] target: &T) -> Result, JsValue>; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef/deref) diff --git a/client/js-sys/src/builtins/weak_set.rs b/client/js-sys/src/builtins/weak_set.rs index 7048b6a7..57815bb5 100644 --- a/client/js-sys/src/builtins/weak_set.rs +++ b/client/js-sys/src/builtins/weak_set.rs @@ -21,7 +21,10 @@ extern "js-sys" { pub fn new_typed() -> WeakSet; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/WeakSet) - #[js_sys(constructor, return_abi = Result)] + #[js_sys( + constructor = WeakSet, + return_abi = Result + )] pub fn new_from_iterable>( #[js_sys(type = &JsValue)] values: &I, ) -> Result, JsValue>; diff --git a/client/js-sys/src/builtins/webassembly/exception.rs b/client/js-sys/src/builtins/webassembly/exception.rs index e19100a4..090c00d9 100644 --- a/client/js-sys/src/builtins/webassembly/exception.rs +++ b/client/js-sys/src/builtins/webassembly/exception.rs @@ -79,7 +79,7 @@ extern "js-sys" { pub type Tag; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/Tag) - #[js_sys(constructor)] + #[js_sys(constructor = Tag)] pub fn new(descriptor: &TagDescriptor) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Tag/type) @@ -93,11 +93,11 @@ extern "js-sys" { pub type Exception; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception) - #[js_sys(constructor)] + #[js_sys(constructor = Exception)] pub fn new(tag: &Tag, payload: &[JsValue]) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Exception/Exception) - #[js_sys(constructor)] + #[js_sys(constructor = Exception)] pub fn new_with_options( tag: &Tag, payload: &[JsValue], diff --git a/client/js-sys/src/builtins/webassembly/global.rs b/client/js-sys/src/builtins/webassembly/global.rs index 083d659e..97c5a436 100644 --- a/client/js-sys/src/builtins/webassembly/global.rs +++ b/client/js-sys/src/builtins/webassembly/global.rs @@ -95,11 +95,11 @@ extern "js-sys" { pub type Global; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global) - #[js_sys(constructor)] + #[js_sys(constructor = Global)] pub fn new(descriptor: &GlobalDescriptor) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global/Global) - #[js_sys(constructor)] + #[js_sys(constructor = Global)] pub fn new_with_value( descriptor: &GlobalDescriptor, value: &JsValue, diff --git a/client/js-sys/src/builtins/webassembly/instance.rs b/client/js-sys/src/builtins/webassembly/instance.rs index c4339857..a3b580d3 100644 --- a/client/js-sys/src/builtins/webassembly/instance.rs +++ b/client/js-sys/src/builtins/webassembly/instance.rs @@ -9,11 +9,11 @@ extern "js-sys" { pub type Instance; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance/Instance) - #[js_sys(constructor)] + #[js_sys(constructor = Instance)] pub fn new(module: &Module) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance/Instance) - #[js_sys(constructor)] + #[js_sys(constructor = Instance)] pub fn new_with_imports(module: &Module, imports: &Object) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Instance/exports) diff --git a/client/js-sys/src/builtins/webassembly/jspi.rs b/client/js-sys/src/builtins/webassembly/jspi.rs index ee427d49..835db22a 100644 --- a/client/js-sys/src/builtins/webassembly/jspi.rs +++ b/client/js-sys/src/builtins/webassembly/jspi.rs @@ -8,7 +8,7 @@ extern "js-sys" { pub type Suspending; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Suspending/Suspending) - #[js_sys(constructor)] + #[js_sys(constructor = Suspending)] pub fn new(function: &Function) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/promising_static) diff --git a/client/js-sys/src/builtins/webassembly/memory.rs b/client/js-sys/src/builtins/webassembly/memory.rs index 172fcb07..5971c82c 100644 --- a/client/js-sys/src/builtins/webassembly/memory.rs +++ b/client/js-sys/src/builtins/webassembly/memory.rs @@ -89,7 +89,7 @@ extern "js-sys" { pub type Memory; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/Memory) - #[js_sys(constructor)] + #[js_sys(constructor = Memory)] pub fn new(descriptor: &MemoryDescriptor) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory/buffer) diff --git a/client/js-sys/src/builtins/webassembly/module.rs b/client/js-sys/src/builtins/webassembly/module.rs index 632de7dd..426f90e5 100644 --- a/client/js-sys/src/builtins/webassembly/module.rs +++ b/client/js-sys/src/builtins/webassembly/module.rs @@ -63,11 +63,11 @@ extern "js-sys" { pub type Module; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/Module) - #[js_sys(constructor)] + #[js_sys(constructor = Module)] pub fn new(bytes: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/Module) - #[js_sys(constructor)] + #[js_sys(constructor = Module)] pub fn new_with_options(bytes: &JsValue, options: &CompileOptions) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module/customSections_static) diff --git a/client/js-sys/src/builtins/webassembly/table.rs b/client/js-sys/src/builtins/webassembly/table.rs index 7730d46f..2283a8dd 100644 --- a/client/js-sys/src/builtins/webassembly/table.rs +++ b/client/js-sys/src/builtins/webassembly/table.rs @@ -133,11 +133,11 @@ extern "js-sys" { pub type Table; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table) - #[js_sys(constructor)] + #[js_sys(constructor = Table)] pub fn new(descriptor: &TableDescriptor) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/Table) - #[js_sys(constructor)] + #[js_sys(constructor = Table)] pub fn new_with_value(descriptor: &TableDescriptor, value: &JsValue) -> Result; /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table/length) diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 45960518..1d857174 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -48,9 +48,10 @@ pub unsafe trait WasmAbi: Sized { /// /// # Safety /// -/// `MODE` must match the `ABI` of [`WasmRet`]. A direct return must use -/// exactly one non-empty slot. An indirect return uses the target's native -/// pointer type for its hidden return parameter. +/// `MODE` must match the `ABI` of [`WasmRet`]. A direct return uses at +/// most one non-empty slot; only [`EmptySlot`] may use none, in which case the +/// JavaScript result is `undefined`. An indirect return uses the target's +/// native pointer type for its hidden return parameter. pub unsafe trait ReturnAbi: WasmAbi { const MODE: ReturnMode; const RESULT_LAYOUT: Option = None; @@ -117,6 +118,12 @@ unsafe impl Slot for EmptySlot { const WAT_TYPE: Option = None; } +// SAFETY: Returning `WasmRet` has no Wasm result slot. It is still a +// direct C `ABI` return: no hidden return pointer is present. +unsafe impl ReturnAbi for EmptySlot { + const MODE: ReturnMode = ReturnMode::Direct; +} + // SAFETY: Every non-empty `Slot` is a complete single-slot `ABI` carrier. // `EmptySlot` maps to an entirely empty carrier. unsafe impl WasmAbi for T { diff --git a/client/js-sys/src/wire/export.rs b/client/js-sys/src/wire/export.rs index 0f61814a..5c27abef 100644 --- a/client/js-sys/src/wire/export.rs +++ b/client/js-sys/src/wire/export.rs @@ -21,6 +21,12 @@ impl MetadataFor for WireExportOutputType { let ReturnConv::Value(conversion) = T::JS_CONV else { panic!("a direct export cannot return Result"); }; + let conversion = if matches!(slots, [None, None, None, None]) { + // A zero-slot Wasm return is already JavaScript `undefined`. + None + } else { + conversion + }; Self::direct(slots, conversion) } ReturnMode::Indirect => { diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs index 3224633f..befb61c6 100644 --- a/host/js-sys-bindgen/src/closure.rs +++ b/host/js-sys-bindgen/src/closure.rs @@ -306,9 +306,6 @@ impl Signature { }; let output = match &arguments.output { ReturnType::Default => None, - ReturnType::Type(_, output) if matches!(&**output, Type::Tuple(tuple) if tuple.elems.is_empty()) => { - None - } ReturnType::Type(_, output) => Some(*output.clone()), }; diff --git a/host/js-sys-bindgen/src/export.rs b/host/js-sys-bindgen/src/export.rs index 20d2b03e..3c1c5613 100644 --- a/host/js-sys-bindgen/src/export.rs +++ b/host/js-sys-bindgen/src/export.rs @@ -67,9 +67,6 @@ pub(crate) fn r#macro( ); let crate_name = LitStr::new(&crate_name, span); let output_ty = match &function.sig.output { - ReturnType::Type(_, ty) if matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty()) => { - None - } ReturnType::Type(_, ty) => Some(ty.as_ref()), ReturnType::Default => None, }; diff --git a/host/js-sys-bindgen/src/function.rs b/host/js-sys-bindgen/src/function.rs index 9416ac94..53e7209a 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -1,15 +1,15 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::mem; use std::ops::DerefMut; use std::string::ToString; -use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned}; +use proc_macro2::{Span, TokenStream, TokenTree}; +use quote::{ToTokens, quote, quote_spanned}; use syn::spanned::Spanned; use syn::{ - Attribute, Error, FnArg, ForeignItemFn, GenericArgument, GenericParam, Generics, Ident, LitStr, - Pat, PatIdent, PatType, Path, PathArguments, Receiver, Result, ReturnType, Signature, Token, - Type, TypePath, TypeReference, parse_quote, + Attribute, Error, FnArg, ForeignItemFn, GenericParam, Generics, Ident, LitStr, Pat, PatIdent, + PatType, Path, Receiver, Result, ReturnType, Signature, Token, TraitBoundModifier, Type, + TypeParamBound, TypePath, TypeReference, WherePredicate, parse_quote, }; use crate::hygiene::Hygiene; @@ -18,7 +18,7 @@ mod js; mod options; use js::{ForeignItem, FunctionBinding}; -use options::{BindingKind, FunctionOptions}; +use options::{BindingKind, ConstructorOwner, FunctionOptions}; /// Backend-independent description of one JavaScript import. pub(crate) struct FunctionImport { @@ -361,7 +361,7 @@ impl FunctionPlan { let argument_count = sig.inputs.len() - usize::from(self_ty.is_some()); - if matches!(&binding, BindingKind::Constructor) && self_ty.is_some() { + if matches!(&binding, BindingKind::Constructor(_)) && self_ty.is_some() { return Err(Error::new( span, "`constructor` cannot be used with a `self` parameter", @@ -393,10 +393,15 @@ impl FunctionPlan { } match binding { - BindingKind::Constructor => { + BindingKind::Constructor(owner) => { // `constructor` applies to this foreign function. Its return type - // selects the Rust `impl` owner and JavaScript invokes it with `new`. - let owner = Self::constructor_type(&sig.output)?; + // selects the Rust `impl` owner unless one was supplied explicitly, + // and JavaScript invokes it with `new`. + let output = Self::constructor_output(&sig.output)?; + let owner = match owner { + ConstructorOwner::Infer => Self::constructor_type(output)?, + ConstructorOwner::Explicit(owner) => owner, + }; let name = js_name.unwrap_or_else(|| Self::type_js_name(&owner, js_names)); Ok(ForeignItem::constructor(owner, namespace, &name, variadic)) @@ -502,7 +507,7 @@ impl FunctionPlan { } } - fn constructor_type(output: &ReturnType) -> Result { + fn constructor_output(output: &ReturnType) -> Result<&Type> { let ReturnType::Type(_, output) = output else { return Err(Error::new_spanned( output, @@ -510,84 +515,85 @@ impl FunctionPlan { )); }; - Self::constructor_type_from(output) + Ok(output) } - fn type_js_name(owner: &Path, js_names: &HashMap) -> String { - let rust_name = owner - .segments - .last() - .expect("a type path always contains a segment") - .ident - .to_string(); - - js_names.get(&rust_name).cloned().unwrap_or(rust_name) - } - - fn constructor_type_from(output: &Type) -> Result { + fn constructor_type(output: &Type) -> Result { let Type::Path(TypePath { qself: None, path }) = output else { return Err(Error::new_spanned( output, - "`constructor` requires a path return type", + "`constructor` requires a path return type or an explicit owner", )); }; - let segment = path + + Ok(path.clone()) + } + + fn type_js_name(owner: &Path, js_names: &HashMap) -> String { + let rust_name = owner .segments .last() - .expect("a type path always contains a segment"); + .expect("a type path always contains a segment") + .ident + .to_string(); - if segment.ident == "Result" - && let PathArguments::AngleBracketed(arguments) = &segment.arguments - && let Some(GenericArgument::Type(output)) = arguments.args.first() - { - return Self::constructor_type_from(output); + if owner.leading_colon.is_none() && owner.segments.len() == 1 { + js_names.get(&rust_name).cloned().unwrap_or(rust_name) + } else { + rust_name } - - Ok(path.clone()) } // Extract type generics from signature that are part of `impl `. fn impl_generic_params(binding: &ForeignItem, generics: &mut Generics) -> TokenStream { if let Some(owner) = binding.owner() { - let mut fn_generic_params: Vec<_> = - mem::take(&mut generics.params).into_iter().collect(); - - let impl_generic_params: Vec<_> = fn_generic_params - .extract_if(.., |param| { - for path in &owner.segments { - if let PathArguments::AngleBracketed(args) = &path.arguments { - for arg in &args.args { - match (&*param, arg) { - ( - GenericParam::Lifetime(param), - GenericArgument::Lifetime(arg), - ) if ¶m.lifetime == arg => { - return true; - } - ( - GenericParam::Type(param), - GenericArgument::Type(Type::Path(TypePath { - qself: None, - path, - })), - ) => { - if let Some(arg) = path.get_ident() - && ¶m.ident == arg - { - return true; - } - } - _ => (), - } - } - } + let params: Vec<_> = mem::take(&mut generics.params).into_iter().collect(); + let mut identifiers = HashMap::new(); + let mut lifetimes = HashMap::new(); + + for (index, param) in params.iter().enumerate() { + match param { + GenericParam::Lifetime(param) => { + lifetimes.insert(param.lifetime.ident.to_string(), index); + } + GenericParam::Type(param) => { + identifiers.insert(param.ident.to_string(), index); } + GenericParam::Const(param) => { + identifiers.insert(param.ident.to_string(), index); + } + } + } - false - }) - .collect(); + let mut impl_indices = HashSet::new(); + for segment in &owner.segments { + Self::collect_generic_references( + segment.arguments.to_token_stream(), + &identifiers, + &lifetimes, + &mut impl_indices, + ); + } + let mut impl_generic_params = Vec::new(); + let mut fn_generic_params = Vec::new(); + let mut method_predicates = Vec::new(); + for (index, param) in params.into_iter().enumerate() { + if impl_indices.contains(&index) { + let (param, predicates) = Self::prepare_impl_generic(param); + impl_generic_params.push(param); + method_predicates.extend(predicates); + } else { + fn_generic_params.push(param); + } + } generics.params = fn_generic_params.into_iter().collect(); + if !method_predicates.is_empty() { + generics + .make_where_clause() + .predicates + .extend(method_predicates); + } if impl_generic_params.is_empty() { TokenStream::new() @@ -602,6 +608,110 @@ impl FunctionPlan { } } + fn prepare_impl_generic(param: GenericParam) -> (GenericParam, Vec) { + match param { + GenericParam::Lifetime(mut param) => { + let bounds = mem::take(&mut param.bounds); + param.colon_token = None; + let predicates = if bounds.is_empty() { + Vec::new() + } else { + let lifetime = ¶m.lifetime; + vec![parse_quote!(#lifetime: #bounds)] + }; + + (GenericParam::Lifetime(param), predicates) + } + GenericParam::Type(mut param) => { + let bounds = mem::take(&mut param.bounds); + let mut impl_bounds = + syn::punctuated::Punctuated::::new(); + let mut method_bounds = + syn::punctuated::Punctuated::::new(); + + for bound in bounds { + if matches!( + &bound, + TypeParamBound::Trait(bound) + if matches!(bound.modifier, TraitBoundModifier::Maybe(_)) + && bound.path.is_ident("Sized") + ) { + impl_bounds.push(bound); + } else { + method_bounds.push(bound); + } + } + + param.colon_token = + (!impl_bounds.is_empty()).then_some(Token![:](param.ident.span())); + param.bounds = impl_bounds; + param.eq_token = None; + param.default = None; + let predicates = if method_bounds.is_empty() { + Vec::new() + } else { + let ident = ¶m.ident; + vec![parse_quote!(#ident: #method_bounds)] + }; + + (GenericParam::Type(param), predicates) + } + GenericParam::Const(mut param) => { + param.eq_token = None; + param.default = None; + + (GenericParam::Const(param), Vec::new()) + } + } + } + + fn collect_generic_references( + tokens: TokenStream, + identifiers: &HashMap, + lifetimes: &HashMap, + references: &mut HashSet, + ) { + let tokens: Vec<_> = tokens.into_iter().collect(); + let mut index = 0; + + while index < tokens.len() { + match &tokens[index] { + TokenTree::Group(group) => Self::collect_generic_references( + group.stream(), + identifiers, + lifetimes, + references, + ), + TokenTree::Punct(punct) + if punct.as_char() == '\'' + && matches!(tokens.get(index + 1), Some(TokenTree::Ident(_))) => + { + let Some(TokenTree::Ident(ident)) = tokens.get(index + 1) else { + unreachable!("validated by the match guard"); + }; + if let Some(parameter) = lifetimes.get(&ident.to_string()) { + references.insert(*parameter); + } + index += 1; + } + TokenTree::Ident(ident) => { + let is_qualified_path_segment = index >= 2 + && matches!(&tokens[index - 1], TokenTree::Punct(punct) if punct.as_char() == ':') + && matches!(&tokens[index - 2], TokenTree::Punct(punct) if punct.as_char() == ':'); + let is_member = index >= 1 + && matches!(&tokens[index - 1], TokenTree::Punct(punct) if punct.as_char() == '.'); + if !is_qualified_path_segment + && !is_member && let Some(parameter) = identifiers.get(&ident.to_string()) + { + references.insert(*parameter); + } + } + TokenTree::Literal(_) | TokenTree::Punct(_) => (), + } + index += 1; + } + } + fn into_import_descriptor( self, macro_path: Path, diff --git a/host/js-sys-bindgen/src/function/js.rs b/host/js-sys-bindgen/src/function/js.rs index 9f79b89f..5fc3e868 100644 --- a/host/js-sys-bindgen/src/function/js.rs +++ b/host/js-sys-bindgen/src/function/js.rs @@ -1,6 +1,9 @@ +use std::fmt::Write; + use proc_macro2::{Span, TokenStream}; -use quote::quote_spanned; -use syn::{Ident, LitStr, Path}; +use quote::{ToTokens, quote_spanned}; +use syn::{Ident, LitStr, Path, PathArguments}; +use xxhash_rust::xxh3::xxh3_128; /// A global JavaScript path referenced by an imported operation. pub(crate) struct FunctionGlobalPath { @@ -262,10 +265,30 @@ impl ForeignItem { } } - fn owner_name(&self) -> &Ident { - self.owner() - .and_then(|owner| owner.segments.last()) - .map(|segment| &segment.ident) - .expect("static and instance bindings always have an owner") + fn owner_name(&self) -> String { + let owner = self + .owner() + .expect("static and instance bindings always have an owner"); + let mut name = String::new(); + for (index, segment) in owner.segments.iter().enumerate() { + if index != 0 { + name.push_str("::"); + } + write!(name, "{}", segment.ident).expect("writing to a String cannot fail"); + } + + if owner + .segments + .iter() + .any(|segment| !matches!(segment.arguments, PathArguments::None)) + { + // Generic arguments are part of the Rust owner identity, but are not + // suitable as raw linker symbol text. + let syntax = owner.to_token_stream().to_string(); + write!(name, "${:032x}", xxh3_128(syntax.as_bytes())) + .expect("writing to a String cannot fail"); + } + + name } } diff --git a/host/js-sys-bindgen/src/function/options.rs b/host/js-sys-bindgen/src/function/options.rs index 449e53c5..57bd14d0 100644 --- a/host/js-sys-bindgen/src/function/options.rs +++ b/host/js-sys-bindgen/src/function/options.rs @@ -21,7 +21,7 @@ pub(super) struct FunctionOptions { /// The mutually exclusive JavaScript binding selected by the attributes. pub(super) enum BindingKind { Call, - Constructor, + Constructor(ConstructorOwner), Getter(String), Setter(String), IndexingGetter, @@ -31,6 +31,11 @@ pub(super) enum BindingKind { Import, } +pub(super) enum ConstructorOwner { + Infer, + Explicit(Path), +} + impl BindingKind { pub(super) fn is_external(&self) -> bool { matches!(self, Self::Embed(_) | Self::Import) @@ -42,7 +47,7 @@ struct RawFunctionOptions { js_name: Option, static_of: Option, variadic: bool, - constructor: bool, + constructor: Option, getter: Option, setter: Option, indexing_getter: bool, @@ -79,7 +84,21 @@ impl FunctionOptions { } else if meta.path.is_ident("variadic") { parse_flag(&meta, "variadic", &mut options.variadic) } else if meta.path.is_ident("constructor") { - parse_flag(&meta, "constructor", &mut options.constructor) + let owner = if meta.input.peek(syn::Token![=]) { + ConstructorOwner::Explicit(meta.value()?.parse()?) + } else if meta.input.peek(syn::token::Paren) { + return Err(meta.error( + "`constructor` supports only `constructor` or `constructor = Owner`", + )); + } else { + ConstructorOwner::Infer + }; + + if options.constructor.replace(owner).is_some() { + Err(meta.error("duplicate attribute")) + } else { + Ok(()) + } } else if meta.path.is_ident("getter") { let name = if meta.input.is_empty() { rust_name.to_string() @@ -144,7 +163,7 @@ impl FunctionOptions { impl RawFunctionOptions { fn validate(&self, rust_name: &Ident) -> Result<()> { let source_count = usize::from(self.import) + usize::from(self.embed.is_some()); - let operation_count = usize::from(self.constructor) + let operation_count = usize::from(self.constructor.is_some()) + usize::from(self.getter.is_some()) + usize::from(self.setter.is_some()) + usize::from(self.indexing_getter) @@ -176,7 +195,7 @@ impl RawFunctionOptions { "JavaScript operations are mutually exclusive", )); } - if self.constructor && self.static_of.is_some() { + if self.constructor.is_some() && self.static_of.is_some() { return Err(Error::new_spanned( rust_name, "`constructor` cannot be combined with `static_of`", @@ -218,8 +237,8 @@ impl RawFunctionOptions { BindingKind::Import } else if let Some(embed) = embed { BindingKind::Embed(embed) - } else if constructor { - BindingKind::Constructor + } else if let Some(owner) = constructor { + BindingKind::Constructor(owner) } else if let Some(getter) = getter { BindingKind::Getter(getter) } else if let Some(setter) = setter { diff --git a/host/js-sys-bindgen/src/macro.rs b/host/js-sys-bindgen/src/macro.rs index 7663aa5f..16db9868 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -143,7 +143,16 @@ fn expand_foreign_mod( let rust_name = item.ident.to_string(); let js_name = options.js_name.clone().unwrap_or_else(|| rust_name.clone()); - js_names.insert(rust_name.clone(), js_name); + if let Some(previous) = js_names.get(&rust_name) { + if previous != &js_name { + error.push(Error::new_spanned( + &item.ident, + format!("conflicting JavaScript names for `{rust_name}`"), + )); + } + } else { + js_names.insert(rust_name.clone(), js_name); + } type_options.push_back(options); } } diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index e91e2ab8..696401fb 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use proc_macro2::{Span, TokenStream}; -use syn::Item; +use quote::ToTokens; +use syn::{GenericParam, Item}; fn expand_function( namespace: Option<&str>, @@ -71,6 +72,144 @@ fn function_descriptor_preserves_binding_options() { assert!(imported.binding.is_none()); } +#[test] +fn constructor_generics() { + let (output, _) = expand_function( + None, + None, + syn::parse_quote! { + #[js_sys(constructor = Owner<'a, Wrapper, N>)] + pub fn new< + 'a: 'b, + 'b, + U, + I, + T: ?Sized + Dependency = Fallback, + const N: usize = 4, + >( + value: I, + ) -> Option, N>>; + }, + ); + let item: syn::ItemImpl = syn::parse2(output).unwrap(); + let expected_owner: syn::Type = syn::parse_quote!(Owner<'a, Wrapper, N>); + assert_eq!(*item.self_ty, expected_owner); + assert_eq!(generic_names(&item.generics), ["'a", "T", "N"]); + let type_param = item + .generics + .params + .iter() + .find_map(|param| match param { + GenericParam::Type(param) => Some(param), + _ => None, + }) + .expect("T is an impl generic"); + assert_eq!(type_param.bounds.to_token_stream().to_string(), "? Sized"); + assert!(type_param.default.is_none()); + let const_param = item + .generics + .params + .iter() + .find_map(|param| match param { + GenericParam::Const(param) => Some(param), + _ => None, + }) + .expect("N is an impl generic"); + assert!(const_param.default.is_none()); + + let method = item + .items + .iter() + .find_map(|item| match item { + syn::ImplItem::Fn(item) => Some(item), + _ => None, + }) + .expect("constructor impl contains its method"); + assert_eq!(generic_names(&method.sig.generics), ["'b", "U", "I"]); + let where_clause = method + .sig + .generics + .where_clause + .as_ref() + .expect("impl generic bounds move to the method"); + let predicates = where_clause.predicates.to_token_stream().to_string(); + assert!(predicates.contains("'a : 'b")); + assert!(predicates.contains("T : Dependency < U >")); +} + +#[test] +fn qualified_owner_generics() { + let (output, _) = expand_function( + None, + None, + syn::parse_quote! { + #[js_sys(constructor = Owner)] + pub fn new() -> Option>; + }, + ); + let item: syn::ItemImpl = syn::parse2(output).unwrap(); + assert_eq!(generic_names(&item.generics), ["U"]); + let method = item + .items + .iter() + .find_map(|item| match item { + syn::ImplItem::Fn(item) => Some(item), + _ => None, + }) + .expect("constructor impl contains its method"); + assert_eq!(generic_names(&method.sig.generics), ["T"]); +} + +#[test] +fn qualified_owner_js_name() { + let mut js_names = HashMap::new(); + js_names.insert("Owner".to_owned(), "LocalOwner".to_owned()); + let mut hygiene = crate::hygiene::Hygiene::Qualified { js_sys: None }; + let (_, import) = crate::function::expand( + &mut hygiene, + None, + "test_crate", + &js_names, + syn::parse_quote! { + #[js_sys(constructor = other::Owner)] + pub fn new() -> Option; + }, + ) + .unwrap(); + + let binding: syn::Expr = syn::parse2( + import + .binding + .as_ref() + .expect("generated constructor has a binding") + .wire(&import.macro_path, Span::call_site()), + ) + .unwrap(); + let expected: syn::Expr = syn::parse_quote! { + ::js_sys::wire::WireImportBinding::Construct { + target: ::js_sys::wire::WireGlobalPath::new( + ::core::option::Option::None, + ::core::option::Option::None, + "Owner" + ), + variadic: false, + } + }; + assert_eq!(binding, expected); +} + +fn generic_names(generics: &syn::Generics) -> Vec { + generics + .params + .iter() + .map(|param| match param { + GenericParam::Lifetime(param) => param.lifetime.to_string(), + GenericParam::Type(param) => param.ident.to_string(), + GenericParam::Const(param) => param.ident.to_string(), + }) + .collect() +} + #[test] fn preserves_successful_functions_after_an_error() { let input = syn::parse_quote! { @@ -171,4 +310,13 @@ fn invalid_options() { }, "duplicate attribute" ); + assert_error!( + { + extern "js-sys" { + #[js_sys(constructor = JsTest)] + pub fn new(); + } + }, + "`constructor` requires a return type" + ); } diff --git a/host/js-sys-bindgen/src/tests/macro/member.rs b/host/js-sys-bindgen/src/tests/macro/member.rs index c22396f6..9a375608 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -39,6 +39,28 @@ fn static_getter_uses_the_javascript_type_name() { assert_eq!(binding, expected); } +#[test] +fn owner_import_names() { + fn import_name(owner: &syn::Path) -> String { + let function: syn::ForeignItemFn = syn::parse_quote! { + pub fn value(self: &#owner) -> i32; + }; + + let mut hygiene = crate::hygiene::Hygiene::Qualified { js_sys: None }; + let (_, import) = + crate::function::expand(&mut hygiene, None, "test_crate", &HashMap::new(), function) + .unwrap(); + import.name.value() + } + + assert_eq!(import_name(&syn::parse_quote!(a::Value)), "a::Value.value"); + assert_eq!(import_name(&syn::parse_quote!(b::Value)), "b::Value.value"); + assert_ne!( + import_name(&syn::parse_quote!(Value)), + import_name(&syn::parse_quote!(Value)) + ); +} + #[test] fn invalid_member_options() { let variadic = syn::parse_quote! { diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index e4c41aec..11759ac9 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use syn::{GenericArgument, PathArguments, Type}; +use syn::{Attribute, Fields, GenericArgument, PathArguments, Type}; fn expand(input: syn::ItemForeignMod) -> Vec { crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate") @@ -42,6 +42,80 @@ fn generic_options_and_extends() { })); } +#[test] +fn generic_marker() { + let output = expand(syn::parse_quote! { + extern "js-sys" { + pub type Generic<'a, T: ?Sized, U: ?Sized, const N: usize>; + } + }); + let generic = output + .iter() + .find_map(|item| match item { + syn::Item::Struct(item) if item.ident == "Generic" => Some(item), + _ => None, + }) + .expect("Generic struct was generated"); + let Fields::Named(fields) = &generic.fields else { + panic!("a generic wrapper must use named fields"); + }; + let marker = fields + .named + .iter() + .find(|field| field.ident.as_ref().is_some_and(|ident| ident == "_type")) + .expect("generic wrapper has a marker field"); + + assert_eq!( + marker.ty, + syn::parse_quote! { + ( + ::core::marker::PhantomData<&'a ()>, + ::core::marker::PhantomData, + ::core::marker::PhantomData, + ) + } + ); +} + +#[test] +fn impl_cfg() { + let output = expand(syn::parse_quote! { + extern "js-sys" { + #[cfg(any())] + #[cfg_attr( + all(), + derive(Clone), + cfg(unix), + cfg_attr(all(), allow(dead_code), cfg(target_arch = "wasm32")), + )] + #[cfg_attr(all(), derive(Debug))] + pub type Conditional; + } + }); + let expected: Vec = vec![ + syn::parse_quote!(#[cfg(any())]), + syn::parse_quote! { + #[cfg_attr( + all(), + cfg(unix), + cfg_attr(all(), cfg(target_arch = "wasm32")) + )] + }, + ]; + let impls: Vec<_> = output + .iter() + .filter_map(|item| match item { + syn::Item::Impl(item) => Some(item), + _ => None, + }) + .collect(); + + assert!(!impls.is_empty()); + for item in impls { + assert_eq!(item.attrs, expected); + } +} + fn trait_argument<'a>(item: &'a syn::ItemImpl, name: &str) -> Option<&'a Type> { let (_, path, _) = item.trait_.as_ref()?; let segment = path.segments.last()?; @@ -105,3 +179,20 @@ fn attributes_are_scoped_and_duplicate_names_do_not_panic() { assert_eq!(configured.len(), 1); assert!(matches!(configured[0], syn::Item::Struct(item) if item.ident == "First")); } + +#[test] +fn conflicting_js_names() { + let input = syn::parse_quote! { + extern "js-sys" { + #[js_sys(js_name = "First")] + pub type Value; + #[js_sys(js_name = "Second")] + pub type Value; + } + }; + + assert_eq!( + super::macro_error(input), + "conflicting JavaScript names for `Value`" + ); +} diff --git a/host/js-sys-bindgen/src/type.rs b/host/js-sys-bindgen/src/type.rs index 4025035f..f612dd3c 100644 --- a/host/js-sys-bindgen/src/type.rs +++ b/host/js-sys-bindgen/src/type.rs @@ -1,9 +1,10 @@ use proc_macro2::TokenStream; use quote::{ToTokens, quote, quote_spanned}; +use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{ - Error, Fields, ForeignItemType, Item, ItemImpl, ItemStruct, LitStr, Path, Token, - parse_quote_spanned, + Attribute, Error, Fields, ForeignItemType, Item, ItemImpl, ItemStruct, LitStr, Meta, Path, + Token, parse_quote_spanned, }; use crate::hygiene::Hygiene; @@ -79,9 +80,7 @@ impl Type { } = item; let mut item_attrs = attrs; - let mut cfgs: Vec<_> = item_attrs - .extract_if(.., |attr| attr.path().is_ident("cfg")) - .collect(); + let cfgs: Vec<_> = item_attrs.iter().filter_map(impl_cfg_attr).collect(); let js_value = hygiene.js_value(&cfgs, span); let js_cast = hygiene.js_cast(&cfgs, span); @@ -105,17 +104,17 @@ impl Type { .filter_map(|param| match param { syn::GenericParam::Lifetime(param) => { let lifetime = ¶m.lifetime; - Some(quote_spanned!(span=> &#lifetime ())) + Some(quote_spanned!(span=> #phantom_data<&#lifetime ()>)) } syn::GenericParam::Type(param) => { let ident = ¶m.ident; - Some(quote_spanned!(span=> #ident)) + Some(quote_spanned!(span=> #phantom_data<#ident>)) } syn::GenericParam::Const(_) => None, }) .collect(); let marker_type = match marker_types.as_slice() { - [] => quote!(()), + [] => quote!(#phantom_data<()>), [ty] => quote!(#ty), types => quote!((#(#types,)*)), }; @@ -124,7 +123,7 @@ impl Type { Fields::Named(parse_quote_spanned! {span=> { value: #js_value, - _type: #phantom_data<#marker_type>, + _type: #marker_type, } }), None, @@ -204,7 +203,6 @@ impl Type { }); } - item_attrs.append(&mut cfgs); item_attrs.push(parse_quote_spanned! {span=>#[repr(transparent)]}); let r#struct = ItemStruct { @@ -221,6 +219,45 @@ impl Type { } } +/// Copies only attributes which can remove a generated implementation. +/// +/// A type's ordinary attributes belong on the generated `struct`. Direct `cfg` +/// attributes must also gate its implementations. For `cfg_attr`, retain only +/// nested `cfg` attributes (including recursively nested `cfg_attr`) so an +/// unrelated attribute such as `derive` is not applied to an `impl` item. +fn impl_cfg_attr(attr: &Attribute) -> Option { + let meta = impl_cfg_meta(&attr.meta)?; + let mut attr = attr.clone(); + attr.meta = meta; + Some(attr) +} + +fn impl_cfg_meta(meta: &Meta) -> Option { + if meta.path().is_ident("cfg") { + Some(meta.clone()) + } else if meta.path().is_ident("cfg_attr") { + let Meta::List(list) = meta else { + return None; + }; + let arguments = list + .parse_args_with(Punctuated::::parse_terminated) + .ok()?; + let mut arguments = arguments.into_iter(); + let predicate = arguments.next()?; + let attributes: Vec<_> = arguments.filter_map(|meta| impl_cfg_meta(&meta)).collect(); + + if attributes.is_empty() { + return None; + } + + let mut list = list.clone(); + list.tokens = quote!(#predicate #(, #attributes)*); + Some(Meta::List(list)) + } else { + None + } +} + impl IntoIterator for Type { type Item = Item; type IntoIter = std::vec::IntoIter; diff --git a/host/ld/src/wire/closure.rs b/host/ld/src/wire/closure.rs index 406515a3..563eb072 100644 --- a/host/ld/src/wire/closure.rs +++ b/host/ld/src/wire/closure.rs @@ -34,7 +34,7 @@ pub(super) fn render<'a>(closure: &Closure<'a>) -> RenderedClosure<'a> { fn render_call<'a>(closure: &Closure<'a>, name: &str) -> RenderedClosureShim<'a> { let export = Export { - // The export renderer only uses this field when building an ordinary + // The export `renderer` only uses this field when building an ordinary // `JsBinding`; closure dispatchers have no public module of their own. module: closure.factory.helper.module, name, diff --git a/host/ld/src/wire/export/js.rs b/host/ld/src/wire/export/js.rs index fa613b72..3dc2e93b 100644 --- a/host/ld/src/wire/export/js.rs +++ b/host/ld/src/wire/export/js.rs @@ -108,7 +108,9 @@ impl<'export, 'wire> PreparedCall<'export, 'wire> { fn render_sync(call: &PreparedCall<'_, '_>) -> String { let invoke = format!("{}({})", call.callable, call.arguments); - if let Some(output) = call.export.output.as_ref() { + if let Some(output) = call.export.output.as_ref() + && !output.is_void() + { format!( "({}) => {{\n{} const ret = {invoke}\n{}\n}}", call.parameters, @@ -181,7 +183,7 @@ fn render_output_slot(output: &ExportOutput<'_>, slot: usize) -> String { if slot < usize::from(result.discriminant) { return format!("ret[{slot}]"); } - } else if output.is_direct() { + } else if output.is_direct() && !output.is_void() { if slot == 0 { return "ret".to_owned(); } @@ -190,3 +192,30 @@ fn render_output_slot(output: &ExportOutput<'_>, slot: usize) -> String { } String::new() } + +#[cfg(test)] +mod tests { + use js_bindgen_wire::PointerWidth; + use js_bindgen_wire::model::{Callee, Export, ExportOutput}; + + use super::render; + + #[test] + fn void_passthrough() { + let export = Export { + module: "test", + name: "unit", + pointer_width: PointerWidth::Wasm32, + inputs: Vec::new(), + output: Some(ExportOutput::Direct { + slot: None, + js_conversion: None, + }), + embeds: Vec::new(), + promising: false, + callee: Callee::Symbol { name: "unit.raw" }, + }; + + assert_eq!(render(&export).js, r#"wasmExports["unit"]"#); + } +} diff --git a/host/ld/src/wire/export/wat.rs b/host/ld/src/wire/export/wat.rs index 4eef6739..f46dd984 100644 --- a/host/ld/src/wire/export/wat.rs +++ b/host/ld/src/wire/export/wat.rs @@ -125,7 +125,10 @@ impl ExportRenderer<'_, '_> { } wat.push(')'); } - if let Some(ExportOutput::Direct { slot, .. }) = self.export.output.as_ref() { + if let Some(ExportOutput::Direct { + slot: Some(slot), .. + }) = self.export.output.as_ref() + { write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); } wat.push_str("))"); @@ -150,7 +153,10 @@ impl ExportRenderer<'_, '_> { } wat.push(')'); } - if let Some(ExportOutput::Direct { slot, .. }) = self.export.output.as_ref() { + if let Some(ExportOutput::Direct { + slot: Some(slot), .. + }) = self.export.output.as_ref() + { write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); } wat.push_str("))"); @@ -182,10 +188,13 @@ impl ExportRenderer<'_, '_> { } if let Some(output) = self.export.output.as_ref() { match output { - ExportOutput::Direct { slot, .. } => { + ExportOutput::Direct { + slot: Some(slot), .. + } => { write!(wat, " (result {})", slot.js()) .expect("writing to a String cannot fail"); } + ExportOutput::Direct { slot: None, .. } => {} ExportOutput::Indirect { frame, .. } => { wat.push_str(" (result"); for frame_slot in &frame.slots { @@ -284,9 +293,12 @@ impl ExportRenderer<'_, '_> { fn write_epilogue(&self, wat: &mut String) { if let Some(output) = self.export.output.as_ref() { match output { - ExportOutput::Direct { slot, .. } => { + ExportOutput::Direct { + slot: Some(slot), .. + } => { write_conversion(wat, slot.instruction()); } + ExportOutput::Direct { slot: None, .. } => {} ExportOutput::Indirect { frame, .. } => { for frame_slot in &frame.slots { write!( @@ -327,7 +339,10 @@ fn for_each_conversion_slot<'export, 'wire>( } if let Some(output) = export.output.as_ref() { match output { - ExportOutput::Direct { slot, .. } => visit(slot), + ExportOutput::Direct { + slot: Some(slot), .. + } => visit(slot), + ExportOutput::Direct { slot: None, .. } => {} ExportOutput::Indirect { frame, .. } => { for frame_slot in &frame.slots { visit(&frame_slot.slot); @@ -374,6 +389,33 @@ mod tests { js_bindgen_ld_shared::wat_to_object(false, &wat).expect("escaped WAT should parse"); } + #[test] + fn void_output() { + let export = Export { + module: "test", + name: "unit", + pointer_width: PointerWidth::Wasm32, + inputs: Vec::new(), + output: Some(ExportOutput::Direct { + slot: None, + js_conversion: None, + }), + embeds: Vec::new(), + promising: false, + callee: Callee::Symbol { name: "unit.raw" }, + }; + + let wat = render(core::slice::from_ref(&export)).expect("one export produces WAT"); + assert_eq!( + wat, + r#"(import "env" "symbol" (func $js_sys.export.symbol.0 (@sym (name "unit.raw")))) +(func $js_sys.export.0 (@sym (name "unit")) + call $js_sys.export.symbol.0 (@reloc) +)"#, + ); + js_bindgen_ld_shared::wat_to_object(false, &wat).expect("rendered WAT should parse"); + } + #[test] fn converted_slots_use_js_types_for_shim_and_rust_types_for_symbol() { let input = Slot { @@ -404,7 +446,7 @@ mod tests { conversion: None, }], output: Some(ExportOutput::Direct { - slot: output, + slot: Some(output), js_conversion: None, }), embeds: Vec::new(), diff --git a/host/wire/src/decode/export.rs b/host/wire/src/decode/export.rs index 84134279..b92f3f24 100644 --- a/host/wire/src/decode/export.rs +++ b/host/wire/src/decode/export.rs @@ -110,10 +110,17 @@ impl<'a> ExportOutput<'a> { let result = flags & EXPORT_OUTPUT_RESULT != 0; let output = if direct { let slots = value::compact(&slots); - decoder.ensure(slots.len() == 1, "direct export output must have one slot")?; + decoder.ensure( + slots.len() <= 1, + "direct export output has more than one slot", + )?; decoder.ensure(!result, "direct export output cannot be a Result")?; + decoder.ensure( + !slots.is_empty() || js_conversion.is_none(), + "zero-slot export output cannot have a JavaScript conversion", + )?; ExportOutput::Direct { - slot: slots[0].clone(), + slot: slots.into_iter().next(), js_conversion, } } else { diff --git a/host/wire/src/model.rs b/host/wire/src/model.rs index 2c66ebcd..194cbacf 100644 --- a/host/wire/src/model.rs +++ b/host/wire/src/model.rs @@ -286,7 +286,8 @@ pub struct ReturnFrame<'a> { #[derive(Clone, Debug, Eq, PartialEq)] pub enum ExportOutput<'a> { Direct { - slot: Slot<'a>, + /// The sole direct result, or `None` for a zero-slot return. + slot: Option>, js_conversion: Option<&'a str>, }, Indirect { @@ -302,6 +303,11 @@ impl<'a> ExportOutput<'a> { matches!(self, Self::Direct { .. }) } + #[must_use] + pub const fn is_void(&self) -> bool { + matches!(self, Self::Direct { slot: None, .. }) + } + #[must_use] pub const fn js_conversion(&self) -> Option<&'a str> { match self { diff --git a/host/wire/src/schema.rs b/host/wire/src/schema.rs index 58a06fd1..90496183 100644 --- a/host/wire/src/schema.rs +++ b/host/wire/src/schema.rs @@ -306,6 +306,10 @@ impl WireReturnFrame { } impl WireExportOutputType { + /// Describes a C `ABI` return with zero or one direct Wasm result slots. + /// + /// A zero-slot return has no JavaScript conversion because the native Wasm + /// result is already `undefined`. #[must_use] pub const fn direct(slots: [Option; 4], conversion: Option) -> Self { let mut slot_count = 0; @@ -316,7 +320,8 @@ impl WireExportOutputType { } index += 1; } - assert!(slot_count == 1); + assert!(slot_count <= 1); + assert!(slot_count != 0 || conversion.is_none()); Self { kind: WireExportOutputKind::Direct { slots, conversion }, } diff --git a/host/wire/src/tests.rs b/host/wire/src/tests.rs index 7f3e2e07..c45ce7f1 100644 --- a/host/wire/src/tests.rs +++ b/host/wire/src/tests.rs @@ -150,19 +150,30 @@ const EXPORT_I32_INPUT: WireExportInputType = WireExportInputType::new([I32, None, None, None], None); const EXPORT_DIRECT_OUTPUT: WireExportOutputType = WireExportOutputType::direct([I32, None, None, None], None); +const EXPORT_VOID_OUTPUT: WireExportOutputType = + WireExportOutputType::direct([None, None, None, None], None); const EXPORT_RESULT_UNIT_OUTPUT: WireExportOutputType = WireExportOutputType::indirect( [None, None, I32, I32], ReturnConv::Result(None), WireReturnFrame::new(16, [0, 0, 0, 8]), Some(ResultLayout::new(0, 1)), ); -const EXPORTS: &[WireExport] = &[WireExport::new_symbol( - "exports", - "foo", - "foo.raw", - &[WireExportInput::new(&EXPORT_I32_INPUT)], - Some(WireExportOutput::new(&EXPORT_DIRECT_OUTPUT)), -)]; +const EXPORTS: &[WireExport] = &[ + WireExport::new_symbol( + "exports", + "foo", + "foo.raw", + &[WireExportInput::new(&EXPORT_I32_INPUT)], + Some(WireExportOutput::new(&EXPORT_DIRECT_OUTPUT)), + ), + WireExport::new_symbol( + "exports", + "unit", + "unit.raw", + &[], + Some(WireExportOutput::new(&EXPORT_VOID_OUTPUT)), + ), +]; const EXPORT_WIRE: Wire = Wire::exports_with(PointerWidth::Wasm64, EXPORTS); const EXPORT_LEN: usize = wire_blob_len(&EXPORT_WIRE); const EXPORT_RECORD: WireRecord = WireRecord::new(&EXPORT_WIRE); @@ -420,7 +431,7 @@ fn exports() { let Record::Exports(exports) = decode(EXPORT_RECORD.as_bytes()).unwrap() else { panic!("expected exports"); }; - assert_eq!(exports.len(), 1); + assert_eq!(exports.len(), 2); assert_eq!((exports[0].module, exports[0].name), ("exports", "foo")); assert_eq!(exports[0].pointer_width, PointerWidth::Wasm64); assert_eq!(exports[0].callee, Callee::Symbol { name: "foo.raw" }); @@ -429,7 +440,15 @@ fn exports() { let Some(ExportOutput::Direct { slot, .. }) = &exports[0].output else { panic!("expected direct output"); }; + let slot = slot.as_ref().expect("expected one direct output slot"); assert_eq!(slot.rust, WatType::I32); + assert_eq!((exports[1].module, exports[1].name), ("exports", "unit")); + assert!( + exports[1] + .output + .as_ref() + .is_some_and(ExportOutput::is_void) + ); } #[test] From 182ddcded42d5f75abfb10acece5b5b16cfbe47f Mon Sep 17 00:00:00 2001 From: Noz Wu Date: Mon, 10 Aug 2026 22:07:48 +0800 Subject: [PATCH 21/21] Fix CI --- client/js-sys/src/wire/closure.rs | 3 +-- client/js-sys/tests/typed_array.rs | 6 +++--- client/js-sys/tests/vec.rs | 4 ++-- host/ld/src/wire/export/js.rs | 10 ++++------ host/ld/src/wire/export/wat.rs | 3 +-- host/ld/src/wire/import/js.rs | 3 +-- host/ld/src/wire/import/wat.rs | 3 +-- host/wire/src/lib.rs | 8 ++++---- 8 files changed, 17 insertions(+), 23 deletions(-) diff --git a/client/js-sys/src/wire/closure.rs b/client/js-sys/src/wire/closure.rs index 1f7e4513..4560cbfe 100644 --- a/client/js-sys/src/wire/closure.rs +++ b/client/js-sys/src/wire/closure.rs @@ -1,9 +1,8 @@ -use crate::runtime::JsValue; - use super::{ JsEmbed, Wire, WireClosure, WireClosureFactory, WireExportInput, WireExportOutput, wire_import_input_type, wire_import_output_type, }; +use crate::runtime::JsValue; /// The JavaScript lifetime and invocation semantics of a Rust closure. #[doc(hidden)] diff --git a/client/js-sys/tests/typed_array.rs b/client/js-sys/tests/typed_array.rs index 37fcd2fc..20d07a40 100644 --- a/client/js-sys/tests/typed_array.rs +++ b/client/js-sys/tests/typed_array.rs @@ -1,4 +1,4 @@ -#![expect( +#![allow( clippy::float_cmp, reason = "typed-array copies must preserve exact values" )] @@ -6,8 +6,8 @@ use js_bindgen_test::test; use js_sys::hazard::JsCast; use js_sys::{ - ArrayBuffer, BigInt64Array, BigUint64Array, Float16Array, Float64Array, Int8Array, - TypedArray, TypedArrayCopyError, Uint8Array, Uint8ClampedArray, Uint32Array, js_sys, + ArrayBuffer, BigInt64Array, BigUint64Array, Float16Array, Float64Array, Int8Array, TypedArray, + TypedArrayCopyError, Uint8Array, Uint8ClampedArray, Uint32Array, js_sys, }; js_bindgen::embed_js!( diff --git a/client/js-sys/tests/vec.rs b/client/js-sys/tests/vec.rs index 6d81a8a1..e5025465 100644 --- a/client/js-sys/tests/vec.rs +++ b/client/js-sys/tests/vec.rs @@ -125,13 +125,13 @@ fn js_value_roundtrip() { assert_eq!(result[0], "first"); assert_eq!(result[1], ""); assert_eq!(result[2], "第三个 🦀"); - assert!(js_value_identity(Vec::new()).is_empty()); + assert_eq!(js_value_identity(Vec::new()), Vec::::new()); } #[test] fn u32_roundtrip() { assert_eq!(u32_identity(vec![0, 1, u32::MAX]), [0, 1, u32::MAX]); - assert!(u32_identity(Vec::new()).is_empty()); + assert_eq!(u32_identity(Vec::new()), Vec::::new()); } #[test] diff --git a/host/ld/src/wire/export/js.rs b/host/ld/src/wire/export/js.rs index 3dc2e93b..18246abf 100644 --- a/host/ld/src/wire/export/js.rs +++ b/host/ld/src/wire/export/js.rs @@ -1,10 +1,9 @@ use js_bindgen_wire::model::{Export, ExportOutput}; +use super::input_names; use crate::wire::JsBinding; use crate::wire::js::{Placeholder, quote_string, render_template}; -use super::input_names; - /// Renders one decoded Rust export or closure dispatcher. pub(super) fn render<'a>(export: &Export<'a>) -> JsBinding<'a> { let function = format!("wasmExports[{}]", quote_string(export.name)); @@ -143,14 +142,13 @@ fn render_promising(call: &PreparedCall<'_, '_>) -> String { if call.prepares.is_empty() { format!( - "(() => {{\n const $promising = {}\n return ({}) => \ - $promising({}){then}\n}})()", + "(() => {{\n const $promising = {}\n return ({}) => $promising({}){then}\n}})()", call.callable, call.parameters, call.arguments ) } else { format!( - "(() => {{\n const $promising = {}\n return ({}) => \ - {{\n{} return $promising({}){then}\n }}\n}})()", + "(() => {{\n const $promising = {}\n return ({}) => {{\n{} return \ + $promising({}){then}\n }}\n}})()", call.callable, call.parameters, call.prepares, call.arguments ) } diff --git a/host/ld/src/wire/export/wat.rs b/host/ld/src/wire/export/wat.rs index f46dd984..658fbd43 100644 --- a/host/ld/src/wire/export/wat.rs +++ b/host/ld/src/wire/export/wat.rs @@ -3,9 +3,8 @@ use std::fmt::Write; use js_bindgen_wire::abi::WatType; use js_bindgen_wire::model::{Callee, Export, ExportInput, ExportInputKind, ExportOutput, Slot}; -use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; - use super::input_names; +use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; struct ExportRenderer<'export, 'wire> { index: usize, diff --git a/host/ld/src/wire/import/js.rs b/host/ld/src/wire/import/js.rs index 6ed718dd..05976f26 100644 --- a/host/ld/src/wire/import/js.rs +++ b/host/ld/src/wire/import/js.rs @@ -5,11 +5,10 @@ use js_bindgen_wire::model::{ ImportErrorMode, ImportGroup, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, }; +use super::input_name; use crate::wire::JsBinding; use crate::wire::js::{JsPath, Placeholder, render_template}; -use super::input_name; - /// Renders every import which has a generated JavaScript binding. pub(super) fn render<'a>(group: &ImportGroup<'a>) -> Vec> { let group_catch = match group.catch.as_ref() { diff --git a/host/ld/src/wire/import/wat.rs b/host/ld/src/wire/import/wat.rs index 88503248..704aaf0c 100644 --- a/host/ld/src/wire/import/wat.rs +++ b/host/ld/src/wire/import/wat.rs @@ -6,9 +6,8 @@ use js_bindgen_wire::model::{ WatCatch, }; -use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; - use super::input_name; +use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; /// Renders the imported functions followed by their Rust `ABI` shims. pub(super) fn render(group: &ImportGroup<'_>) -> Option { diff --git a/host/wire/src/lib.rs b/host/wire/src/lib.rs index 7c4004b0..660b901b 100644 --- a/host/wire/src/lib.rs +++ b/host/wire/src/lib.rs @@ -6,10 +6,10 @@ //! ABI -> schema -> const encode -> decode -> model -> ld render //! ``` //! -//! - `abi` defines the common vocabulary, such as Wasm slots, conversions, -//! and return modes. -//! - `schema` combines those values into static descriptions that `js-sys` -//! can construct during constant evaluation. +//! - `abi` defines the common vocabulary, such as Wasm slots, conversions, and +//! return modes. +//! - `schema` combines those values into static descriptions that `js-sys` can +//! construct during constant evaluation. //! - `encode` writes each description into a compact byte record without //! allocation. //! - `decode` validates records read from object files.