diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54173e2a..d4de41d5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,16 +83,16 @@ 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: | 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 +139,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 +205,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 @@ -255,7 +257,7 @@ jobs: runs-on: ${{ matrix.runner.os }} - timeout-minutes: 20 + timeout-minutes: 30 strategy: fail-fast: false @@ -276,6 +278,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 } @@ -521,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/.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/.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/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/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..62977882 --- /dev/null +++ b/benchmarks/Cargo.lock @@ -0,0 +1,228 @@ +# 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 = "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 = "js-bindgen" +version = "0.0.0" +dependencies = [ + "js-bindgen-macro", +] + +[[package]] +name = "js-bindgen-benchmark" +version = "0.0.0" +dependencies = [ + "js-sys 0.0.0", +] + +[[package]] +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", +] + +[[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" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "xxhash-rust", +] + +[[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 = "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" +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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[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 = [ + "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", +] + +[[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", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" 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..e16f37e7 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,29 @@ +# Benchmarks + +Run the raw call benchmark with Node.js: + +```console +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`. + +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. + +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 new file mode 100644 index 00000000..9989d6a3 --- /dev/null +++ b/benchmarks/bench.mjs @@ -0,0 +1,606 @@ +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) +} + +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. +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 fails = 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 (!allowFailure) { + throw new Error(`cannot infer parameters for ${exportName}`, { + cause: error, + }) + } + + 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 + + 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")), + asynchronous, + result, + fails, + } + } +} + +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, fails, asynchronous) { + const parameterCount = inputs.length + const id = benchmarkId++ + const parameters = Array.from({ length: parameterCount }, (_, index) => `arg${index}`) + const invocation = `call(${parameters.join(", ")})` + const await_ = asynchronous ? "await " : "" + const measuredCall = fails + ? ` + try { + result = ${await_}${invocation}; + } catch (error) { + result = error; + }` + : `result = ${await_}${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} + ${asynchronous ? "async " : ""}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 wrappedExports = await loadImplementation(implementation) + const wrappedCall = wrappedExports.wrapped[workerBenchmark] + + if (typeof wrappedCall !== "function") { + throw new Error(`missing JS export: ${implementation.name}:${workerBenchmark}`) + } + + const wrapped = await inferArguments(workerBenchmark, wrappedCall, true) + const returnsReference = + (typeof wrapped.result === "object" && wrapped.result !== null) || + typeof wrapped.result === "function" + 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] + 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, + asynchronous, + fails, + kinds, + stats, + }) + ) +} + +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")) { + 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}`, + missing.length === 0 ? undefined : `missing: ${missing.join(", ")}`, + extra.length === 0 ? undefined : `extra: ${extra.join(", ")}`, + ] + .filter(Boolean) + .join("\n") + ) + } + } + + 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 expectedAsynchronous + let expectedKinds + 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 (expectedFailure !== undefined && expectedFailure !== result.fails) { + throw new Error( + `failure behavior mismatch for ${exportName}: ${expectedFailure} != ${result.fails}` + ) + } + + expectedAsynchronous = result.asynchronous + expectedKinds = result.kinds + expectedFailure = result.fails + 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..17c18faa --- /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" } diff --git a/benchmarks/js-bindgen/src/lib.rs b/benchmarks/js-bindgen/src/lib.rs new file mode 100644 index 00000000..1f41f1b1 --- /dev/null +++ b/benchmarks/js-bindgen/src/lib.rs @@ -0,0 +1,655 @@ +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, JsFuture, JsValue, Promise, Uint32Array, closure, future_to_promise, 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::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", + "(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")] + 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 = "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; + + #[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; + + #[js_sys(js_embed = "pending_promise")] + fn pending_promise() -> Promise; +} + +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); + 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)) +} + +#[js_sys] +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 +} + +#[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_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) +} + +#[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_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(&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/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..8f8dddf0 --- /dev/null +++ b/benchmarks/wasm-bindgen/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wasm-bindgen-benchmark" +edition = { workspace = true } +license = { workspace = true } +publish = { workspace = true } + +[lib] +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 new file mode 100644 index 00000000..9cad7562 --- /dev/null +++ b/benchmarks/wasm-bindgen/src/lib.rs @@ -0,0 +1,638 @@ +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, Uint32Array}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::{JsFuture, future_to_promise}; + +#[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(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; }")] +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_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); }" +)] +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; +} + +#[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); + 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)) +} + +#[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_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 +} + +#[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_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) +} + +#[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_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(&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/Cargo.toml b/client/Cargo.toml index 408a38b1..71548949 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,26 +6,28 @@ publish = false [workspace] resolver = "3" -members = ["js-bindgen", "js-sys", "test", "web-sys"] +members = ["e2e", "js-bindgen", "js-sys", "test", "wabii", "web-sys"] [workspace.package] edition = "2024" rust-version = "1.85" 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" } +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/Cargo.toml b/client/e2e/Cargo.toml new file mode 100644 index 00000000..b50a2c9b --- /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 } + +[lints] +workspace = true diff --git a/client/e2e/examples/closure.rs b/client/e2e/examples/closure.rs new file mode 100644 index 00000000..1c1e5be7 --- /dev/null +++ b/client/e2e/examples/closure.rs @@ -0,0 +1,406 @@ +#[rustfmt::skip] +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" + // ;; 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"]() + // ;; (() => { 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 })() + // ;; 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 })() +} + +use std::cell::Cell; +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; + +macro_rules! repeated_closures { + ($($offset:expr),+ $(,)?) => { + ($(closure!(dyn FnMut(i32) -> i32, move |value| value + $offset)),+) + }; +} + +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); + } +} + +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::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")] + 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_js_string_ref( + callback: &Closure JsString>, + value: &JsString, + ) -> JsString; + + #[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(js_embed = "release.twice")] + fn release_twice() -> 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_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_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); + + 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_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| { + 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_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); + 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_unit_alias() -> Closure Unit> { + closure!(dyn FnMut() -> Unit, || {}) +} + +#[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/e2e/examples/future.rs b/client/e2e/examples/future.rs new file mode 100644 index 00000000..93aacdab --- /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 })() + // ;; (() => { 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; +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/e2e/examples/jspi.rs b/client/e2e/examples/jspi.rs new file mode 100644 index 00000000..f3bcd824 --- /dev/null +++ b/client/e2e/examples/jspi.rs @@ -0,0 +1,91 @@ +#[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 + // ;; 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 })() +} + +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) +} + +#[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 { + suspend_result(value) +} + +#[js_sys(promising)] +fn jspi_js_value(value: JsValue) -> JsValue { + value +} diff --git a/client/e2e/examples/primitive.rs b/client/e2e/examples/primitive.rs new file mode 100644 index 00000000..41bd45bf --- /dev/null +++ b/client/e2e/examples/primitive.rs @@ -0,0 +1,313 @@ +#[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["unit_alias"]() === undefined + // ;; 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 + // ;; 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 + // ;; (() => { 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 + // ;; (() => { 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; +type Unit = (); + +js_sys::js_bindgen::embed_js!( + module = "primitive", + name = "option.i32", + "(value) => value", +); + +#[js_sys] +fn unit_alias() -> Unit {} + +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", + "(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 = "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; + + #[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_unit(value: Option<()>) -> Option<()> { + 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 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 + .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..16ca2f15 --- /dev/null +++ b/client/e2e/examples/string.rs @@ -0,0 +1,101 @@ +#[rustfmt::skip] +fn main() { + // ;; exports["rust_js_string"]() === "Hello from Rust! 🦀" + // ;; exports["identity"]("Hello from JavaScript! 🦀") === "Hello from JavaScript! 🦀" + // ;; exports["borrowed_js_string"]("borrowed") === true + // ;; exports["optional_js_string"](false) === undefined + // ;; exports["optional_js_string"](true) === "optional" + // ;; exports["owned_roundtrip"]("") === "" + // ;; 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"]("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::{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; + + #[js_sys(js_embed = "result.js_string")] + fn import_result_string_raw(value: String) -> Result; +} + +#[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 optional_js_string(some: bool) -> Option { + some.then(|| JsString::from("optional")) +} + +#[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 { + 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) +} + +#[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..a4e6d9d1 --- /dev/null +++ b/client/e2e/examples/vec.rs @@ -0,0 +1,73 @@ +#[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 })() + // ;; (() => { 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 })() + // ;; (() => { try { exports["u32_roundtrip"]([]); return false } catch (error) { return error instanceof TypeError } })() + // ;; (() => { 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 } })() + // ;; exports["sum_u32_slice"](new Uint32Array([1, 2, 3, 0xffffffff])) === 5 + // ;; exports["join_string_slice"](['first', '', '第三个 🦀']) === 'first||第三个 🦀' +} + +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 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 +} + +#[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/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-sys/Cargo.toml b/client/js-sys/Cargo.toml index 3ed0d20b..d086958f 100644 --- a/client/js-sys/Cargo.toml +++ b/client/js-sys/Cargo.toml @@ -12,15 +12,13 @@ test = false [dependencies] js-bindgen = { workspace = true } -js-sys-macro = { workspace = true, optional = true } +js-bindgen-wire = { workspace = 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 c36a2307..00000000 --- a/client/js-sys/src/array/array.gen.rs +++ /dev/null @@ -1,366 +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::{InputJsConv, OutputJsConv, OutputWatConv, Input, InputWatConv, Output, 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 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.value) - } -} - -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; - - type Type = ::Type; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: Output::from_raw(raw), - _type: PhantomData, - } - } -} - -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), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "length", - required_embeds = [ - r#macro::js_input_embed::<&JsValue>(), - r#macro::js_output_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, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.length"] - fn length(this: <&JsValue as Input>::Type) -> ::Type; - } - - Output::from_raw(unsafe { length(Input::into_raw(self)) }) - } -} - -pub(super) unsafe fn array_js_value_decode( - array: PtrConst, - 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 >), - } - - 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::>(), - ], - "{}{}{}{}", - 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, - ), - } - - 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; - } - - Output::from_raw(unsafe { array_js_value_decode(Input::into_raw(array), Input::into_raw(len)) }) -} - -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! { - "(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), - } - - 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::(), - ], - "{}{}{}{}{}{}{}", - 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, - ), - } - - 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; - } - - 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), - ) - }) -} - -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 >), - } - - 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::>(), - ], - "{}{}{}{}", - 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, - ), - } - - 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; - } - - Output::from_raw(unsafe { array_u32_decode(Input::into_raw(array), Input::into_raw(len)) }) -} - -pub(super) unsafe fn array_u32_encode( - array: &JsArray, - ptr: PtrMut, - 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), - } - - 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::(), - ], - "{}{}{}{}{}", - 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, - ), - } - - 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; - } - - Output::from_raw(unsafe { - array_u32_encode(Input::into_raw(array), Input::into_raw(ptr), Input::into_raw(len)) - }) -} 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/array/mod.rs b/client/js-sys/src/array/mod.rs deleted file mode 100644 index c3862d45..00000000 --- a/client/js-sys/src/array/mod.rs +++ /dev/null @@ -1,339 +0,0 @@ -#[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::externref::ExternrefTable; -use crate::hazard::{Input, InputJsConv, InputWatConv, JsCast}; -use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; - -impl JsArray { - #[must_use] - pub fn as_any(&self) -> &JsArray { - JsArray::unchecked_from_ref(self.as_ref()) - } - - #[must_use] - pub fn into_any(self) -> JsArray { - JsArray::unchecked_from(self.into()) - } -} - -impl From<&[T; N]> for JsArray -where - Self: for<'a> From<&'a [T]>, -{ - fn from(value: &[T; N]) -> Self { - value.as_slice().into() - } -} - -// SAFETY: Implementation. -unsafe impl<'a, T, const N: usize> Input for &'a [T; N] -where - &'a [T]: Input, -{ - 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; - - type Type = <&'a [T] as Input>::Type; - - fn into_raw(self) -> Self::Type { - self.as_slice().into_raw() - } -} - -#[derive(Debug)] -#[non_exhaustive] -pub struct TryFromJsArrayError; - -impl Display for TryFromJsArrayError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("length did not match") - } -} - -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(); - - // SAFETY: Parameters are correct. - let result = unsafe { - array::array_js_value_encode( - self.as_any(), - PtrMut::new(slice), - PtrLength::new(slice), - externref.ptr, - externref.len, - ) - }; - - if result { - ExternrefTable::report_used_slots(slice.len()); - Ok(()) - } else { - Err(TryFromJsArrayError) - } - } - - 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 externref = ExternrefTable::current_ptr(); - - // SAFETY: Parameters are correct. - let result = unsafe { - array::array_js_value_encode( - self.as_any(), - PtrMut::from_uninit_slice(js_slice), - PtrLength::from_uninit_slice(js_slice), - externref.ptr, - externref.len, - ) - }; - - if result { - ExternrefTable::report_used_slots(js_slice.len()); - // SAFETY: Correctly initialized in JS. - Ok(unsafe { assume_init_mut(slice) }) - } else { - Err(TryFromJsArrayError) - } - } - - pub fn to_array(&self) -> Result<[T; N], TryFromJsArrayError> { - let mut array: MaybeUninit<[T; N]> = MaybeUninit::uninit(); - let externref = ExternrefTable::current_ptr(); - let js_array = JsValue::from_mut_uninit_array(&mut array); - - // SAFETY: Parameters are correct. - let result = unsafe { - array::array_js_value_encode( - self.as_any(), - PtrMut::from_uninit_array(js_array), - PtrLength::from_uninit_array(js_array), - externref.ptr, - externref.len, - ) - }; - - if result { - ExternrefTable::report_used_slots(N); - // SAFETY: Correctly initialized in JS. - Ok(unsafe { array.assume_init() }) - } else { - Err(TryFromJsArrayError) - } - } -} - -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) => {{", - " if (array.length !== arrLen) return false", - "", - " const table = this.#jsEmbed.js_sys['externref.table']", - "", - // Default value helps browsers to optimize. - " let tableIndex = 0", - " if (arrLen > refLen) {{", - " tableIndex = table.grow(arrLen - refLen)", - " }}", - "", - " let refIndex = refLen - 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]", - " refIndex--", - " }} else {{", - " elemIndex = tableIndex", - " tableIndex++", - " }}", - "", - " table.set(elemIndex, array[arrayIndex])", - " this.#jsEmbed.js_sys['view.setInt32'](arrPtr + arrayIndex * 4, [elemIndex])", - " }}", - "", - " return true", - "}}", -); - -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 = - unsafe { array::array_js_value_decode(PtrConst::new(slice), PtrLength::new(slice)) }; - - Self::unchecked_from(result.into()) - } -} - -// 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)", - "}}", - ); - - ExternSlice::new(JsValue::from_slice(self)) - } -} - -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)) }; - - if result { - Ok(()) - } else { - Err(TryFromJsArrayError) - } - } - - pub fn to_uninit_slice<'slice>( - &self, - slice: &'slice mut [MaybeUninit], - ) -> Result<&'slice mut [u32], TryFromJsArrayError> { - // SAFETY: Parameters are correct. - let result = unsafe { - array::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) - } - } - - #[must_use] - pub fn to_array(&self) -> Option<[u32; N]> { - let mut array: MaybeUninit<[u32; N]> = MaybeUninit::uninit(); - - // SAFETY: Parameters are correct. - let result = unsafe { - array::array_u32_encode( - self, - PtrMut::from_uninit_array(&mut array), - PtrLength::from_uninit_array(&array), - ) - }; - - if result { - // SAFETY: Correctly initialized in JS. - Some(unsafe { array.assume_init() }) - } else { - None - } - } -} - -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 From<&[u32]> for JsArray { - fn from(value: &[u32]) -> Self { - // SAFETY: Parameters are correct. - unsafe { array::array_u32_decode(PtrConst::new(value), PtrLength::new(value)) } - } -} - -// 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)", - "}}", - ); - - ExternSlice::new(self) - } -} - -// 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/bigint/bigint.gen.rs b/client/js-sys/src/bigint/bigint.gen.rs deleted file mode 100644 index 9cb49c10..00000000 --- a/client/js-sys/src/bigint/bigint.gen.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use crate::JsValue; -use crate::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; - -#[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 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) - } -} - -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; - - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) - } -} diff --git a/client/js-sys/src/bigint/bigint.js-sys.rs b/client/js-sys/src/bigint/bigint.js-sys.rs deleted file mode 100644 index eadff82e..00000000 --- a/client/js-sys/src/bigint/bigint.js-sys.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[js_sys] -extern "js-sys" { - pub type JsBigInt; -} 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/builtins/array.rs b/client/js-sys/src/builtins/array.rs new file mode 100644 index 00000000..73d3f858 --- /dev/null +++ b/client/js-sys/src/builtins/array.rs @@ -0,0 +1,615 @@ +use core::fmt::{self, Formatter}; + +use super::{Function, Iterable, JsIterator, Number, Object, Promise}; +use crate::JsValue; +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 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 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; + + /// [`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; + + /// [`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_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] + #[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; + + /// [`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; + + /// [`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; + + /// [`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; + + /// [`MDN` documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/keys) + #[must_use] + pub fn keys(self: &Array) -> JsIterator>; + + /// [`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; + + /// [`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; + + /// [`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; + + /// [`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; + + /// [`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>; +} + +#[crate::js_sys(js_sys = crate)] +extern "js-sys" { + #[must_use] + #[js_sys(indexing_getter)] + pub fn get(self: &Array, index: u32) -> JsValue; + + #[must_use] + #[js_sys(indexing_getter, return_abi = JsValue)] + pub fn get_unchecked(self: &Array, index: u32) -> T; + + #[js_sys(indexing_setter)] + pub fn set(self: &Array, index: u32, #[js_sys(type = &JsValue)] value: &T); + + #[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; +} + +#[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; + + #[js_sys(js_embed = "array.symbol_unscopables")] + fn array_symbol_unscopables(array: &Array) -> JsValue; +} + +impl Clone for Array { + fn clone(&self) -> Self { + Self::unchecked_from(>::as_ref(self).clone()) + } +} + +impl fmt::Debug for Array { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(>::as_ref(self), f) + } +} + +impl Default for Array { + fn default() -> Self { + Self::new_typed() + } +} + +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()) + } + + /// [`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()) + } +} + +impl Array { + #[must_use] + pub fn as_untyped(&self) -> &Array { + Array::unchecked_from_ref(self.as_ref()) + } + + #[must_use] + pub fn into_untyped(self) -> Array { + Array::unchecked_from(self.into()) + } +} + +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())) + } + + /// [`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() + } +} + +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..6bdf5f12 --- /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 = 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 = ArrayBuffer)] + 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 = 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 = SharedArrayBuffer)] + 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 new file mode 100644 index 00000000..ec8acccb --- /dev/null +++ b/client/js-sys/src/builtins/bigint.rs @@ -0,0 +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")] + #[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..0de8c47c --- /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 = 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 = 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 = DataView)] + 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..8bcb018c --- /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 = 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) + #[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 new file mode 100644 index 00000000..6f43e690 --- /dev/null +++ b/client/js-sys/src/builtins/error.rs @@ -0,0 +1,215 @@ +use super::{Array, 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)] + 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 { + /// [`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()); + ret.set_cause(cause); + ret + } +} + +#[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; + + /// [`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/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)] + 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; +} + +#[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..de827f1e --- /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 = FinalizationRegistry)] + 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/function.rs b/client/js-sys/src/builtins/function.rs new file mode 100644 index 00000000..abadd6b4 --- /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 = 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 = 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) + 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/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..cbb73c21 --- /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 = 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 = Collator)] + 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..e0e8cb70 --- /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 = 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 = DateTimeFormat)] + 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..ba986769 --- /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 = 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) + #[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..a94c34ae --- /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 = 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 = DurationFormat)] + 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..0c1f4bea --- /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 = 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 = ListFormat)] + 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..b9b353f0 --- /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 = 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 = 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) + #[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..3996f9ae --- /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 = 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 = NumberFormat)] + 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..96954663 --- /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 = 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 = PluralRules)] + 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..9c2c1c32 --- /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 = 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 = RelativeTimeFormat)] + 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..99827925 --- /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 = 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 = Segmenter)] + 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..aea74aeb --- /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 fn iterator_from(value: &JsValue) -> Result, JsValue>; + + #[js_sys(js_embed = "async_iterator.from")] + pub 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..45f7913c --- /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 = Map, 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 new file mode 100644 index 00000000..79117a75 --- /dev/null +++ b/client/js-sys/src/builtins/mod.rs @@ -0,0 +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; +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::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 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, async_iterator_from, iterator_from, +}; +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 new file mode 100644 index 00000000..8b72841a --- /dev/null +++ b/client/js-sys/src/builtins/number.rs @@ -0,0 +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")] + #[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 new file mode 100644 index 00000000..44c88fab --- /dev/null +++ b/client/js-sys/src/builtins/object.rs @@ -0,0 +1,250 @@ +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/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 { + fn default() -> Self { + 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/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/builtins/proxy.rs b/client/js-sys/src/builtins/proxy.rs new file mode 100644 index 00000000..a09adfbb --- /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 = 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) + #[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..1fa1b06e --- /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 = 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 = 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) + #[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 = 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) + #[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..8f154c83 --- /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 = Set, 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 new file mode 100644 index 00000000..86138163 --- /dev/null +++ b/client/js-sys/src/builtins/string.rs @@ -0,0 +1,412 @@ +use js_sys_macro::js_sys; + +use super::{Array, Function, Intl, Iterable, JsIterator, Object, RegExp, RegExpMatchArray}; +use crate::JsValue; + +#[js_sys(js_sys = crate)] +extern "js-sys" { + /// [`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 {} + +#[js_sys(js_sys = crate)] +extern "js-sys" { + #[js_sys(js_name = "String")] + fn string_constructor(value: &JsValue) -> Result; + + #[js_sys(js_embed = "string.iterator")] + fn string_iterator(value: &JsString) -> JsIterator; +} + +js_bindgen::embed_js!( + module = "js_sys", + name = "string.iterator", + "value => value[Symbol.iterator]()" +); + +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) + } + + /// [`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 Iterable for JsString { + type Item = Self; +} 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..285675f7 --- /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 = 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 = 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 = Duration)] + 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 = Duration)] + 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 = Duration)] + 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 = Duration)] + 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 = Duration)] + 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 = Duration)] + 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 = Duration)] + 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 = Duration)] + 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..07aedc45 --- /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 = 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) + #[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..1eedc937 --- /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 = 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 = PlainDate)] + 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..ad4a20de --- /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 = 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 = PlainDateTime)] + 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 = PlainDateTime)] + 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 = PlainDateTime)] + 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 = PlainDateTime)] + 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 = PlainDateTime)] + 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 = PlainDateTime)] + 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 = PlainDateTime)] + 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..43137b6c --- /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 = 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 = PlainMonthDay)] + 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 = PlainMonthDay)] + 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..3c8aa8c8 --- /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 = 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 = 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 = PlainTime)] + 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 = PlainTime)] + 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 = PlainTime)] + 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 = PlainTime)] + 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..b845d38b --- /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 = 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 = PlainYearMonth)] + 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 = PlainYearMonth)] + 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..aa7def29 --- /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 = 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 = ZonedDateTime)] + 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..b56b7ce9 --- /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 = $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 = $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 = $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 = $name)] + 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 = 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 = 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 = 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 = Float16Array)] + 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..778a7be7 --- /dev/null +++ b/client/js-sys/src/builtins/weak_map.rs @@ -0,0 +1,102 @@ +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 = WeakMap, + 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..f1052ad4 --- /dev/null +++ b/client/js-sys/src/builtins/weak_ref.rs @@ -0,0 +1,42 @@ +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 = 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) + #[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..57815bb5 --- /dev/null +++ b/client/js-sys/src/builtins/weak_set.rs @@ -0,0 +1,70 @@ +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 = WeakSet, + 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..090c00d9 --- /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 = Tag)] + 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 = 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 = Exception)] + 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..97c5a436 --- /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 = 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 = Global)] + 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..a3b580d3 --- /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 = 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 = 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) + #[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..835db22a --- /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 = Suspending)] + 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..5971c82c --- /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 = Memory)] + 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..426f90e5 --- /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 = 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 = 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) + #[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..2283a8dd --- /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 = 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 = 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) + #[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/externref.rs b/client/js-sys/src/externref.rs deleted file mode 100644 index f886f1af..00000000 --- a/client/js-sys/src/externref.rs +++ /dev/null @@ -1,126 +0,0 @@ -use alloc::vec::Vec; -use core::cell::RefCell; - -use crate::panic::panic; -use crate::util::PtrConst; - -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)))", - "(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.remove (@sym) (param $index i32)", - " local.get $index", - " ref.null extern", - " table.set $js_sys.import.externref.table (@reloc)", - ")", -); - -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", - "}})()" -); - -js_bindgen::import_js!( - module = "js_sys", - name = "externref.table", - required_embeds = [("js_sys", "externref.table")], - "this.#jsEmbed.js_sys['externref.table']", -); - -unsafe extern "C" { - #[link_name = "js_sys.externref.grow"] - safe fn grow(size: i32) -> i32; - #[link_name = "js_sys.externref.remove"] - safe fn remove(index: i32); -} - -thread_local! { - pub(crate) static EXTERNREF_TABLE: RefCell = RefCell::new(ExternrefTable::new()); -} - -pub(crate) struct ExternrefTable(Vec); - -pub(crate) struct ExternrefTablePtr { - pub(crate) ptr: PtrConst, - pub(crate) len: i32, -} - -impl ExternrefTable { - const fn new() -> Self { - Self(Vec::new()) - } - - 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, - } - } - } - - pub(crate) fn remove(&mut self, index: i32) { - self.0.try_reserve(1).expect("failure to grow memory"); - - self.0.push(index); - remove(index); - } - - /// 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(), - } - }) - } - - /// 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); - }); - } -} - -#[unsafe(export_name = "js_sys.externref.next")] -extern "C" fn next() -> i32 { - EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().next()) -} diff --git a/client/js-sys/src/hazard.rs b/client/js-sys/src/hazard.rs index 6bfeb3ae..1d857174 100644 --- a/client/js-sys/src/hazard.rs +++ b/client/js-sys/src/hazard.rs @@ -1,81 +1,384 @@ -use core::mem::ManuallyDrop; +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; +// Wasm `ABI` carriers. + +/// 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. Only [`EmptySlot`] may use +/// `None`. +pub unsafe trait Slot { + const WAT_TYPE: Option; + 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; +} + +/// A [`WasmAbi`] that can be returned through the Rust `extern "C"` `ABI`. +/// +/// # Safety +/// +/// `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; +} + +/// 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, +} + +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: 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 { + 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())) + } + } +} + +// Rust-to-JavaScript conversions. + +/// # 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; +} + +/// 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 /// -/// 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; +/// `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 +/// describe JavaScript control flow, such as throwing an error. +pub trait ReturnIntoJS { + const JS_CONV: ReturnConv; - type Type; + type Abi: ReturnAbi; - fn into_raw(self) -> Self::Type; + fn into_return_abi(self) -> Self::Abi; } -pub struct InputWatConv { - pub import: Option<&'static str>, - pub conv: &'static str, - pub r#type: &'static str, +impl ReturnIntoJS for T +where + T: IntoJS, + T::Abi: ReturnAbi, +{ + const JS_CONV: ReturnConv = ReturnConv::Value(T::JS_CONV); + + type Abi = T::Abi; + + fn into_return_abi(self) -> Self::Abi { + self.into_abi() + } } -pub struct InputJsConv { - pub embed: Option<(&'static str, &'static str)>, - pub pre: &'static str, - pub post: Option<&'static str>, +// JavaScript-to-Rust conversions. + +/// # Safety +/// +/// `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; + + fn from_abi(raw: Self::Abi) -> Self; } +/// 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 /// -/// 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; +/// `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; + + fn from_option_abi(raw: Self::Abi) -> Option; +} + +// 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; + const JS_SRET: Option = >::JS_SRET; + + type Abi = >::Abi; + + fn from_abi(raw: Self::Abi) -> Self { + >::from_option_abi(raw) + } +} + +/// 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 `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; + + 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); + const JS_SRET: Option = T::JS_SRET; - type Type; + type Abi = T::Abi; - fn from_raw(raw: Self::Type) -> Self; + fn from_return_abi(raw: MaybeUninit>) -> Self { + // SAFETY: An ordinary JavaScript import always initializes its return + // value before the shim returns. + T::from_abi(unsafe { raw.assume_init() }.join()) + } } -pub struct OutputWatConv { - pub import: Option<&'static str>, - pub direct: bool, - pub conv: &'static str, - pub r#type: &'static str, +#[doc(hidden)] +pub use crate::interop::{ResultDiscriminantAbi, ResultErrorAbi, ResultIntoJsAbi}; + +// 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 +/// function call and provides the reference passed to that function. +pub trait RefFromJS { + type Anchor: FromJS + core::borrow::Borrow; } -pub struct OutputJsConv { - pub embed: Option<(&'static str, &'static str)>, - pub pre: &'static str, - pub post: &'static str, +impl RefFromJS for T { + type Anchor = T; } /// # Safety /// -/// This MUST only be implemented on types that are `#[transparent]` over a -/// [`JsValue`]. (TODO) +/// This must only be implemented for types that are transparent over +/// [`JsValue`]. 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. - - union Transmute { - from: ManuallyDrop, - to: ManuallyDrop, - } + 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 } + } - let transmute = Transmute { - from: ManuallyDrop::new(value), - }; + #[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/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..4f407b83 --- /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::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..32eb33b1 --- /dev/null +++ b/client/js-sys/src/interop/js/iterator.rs @@ -0,0 +1,325 @@ +use crate::builtins::Intl::{SegmentData, Segments}; +use crate::builtins::{ + Array, AsyncIterator, Function, IteratorResult, JsIterator, JsString, Map, Promise, Set, + iterator_from, +}; +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 new file mode 100644 index 00000000..503e1c67 --- /dev/null +++ b/client/js-sys/src/interop/mod.rs @@ -0,0 +1,12 @@ +mod js; +mod primitive; +mod result; +mod slice; +mod string; +mod vec; + +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 new file mode 100644 index 00000000..32ecc89d --- /dev/null +++ b/client/js-sys/src/interop/primitive.rs @@ -0,0 +1,732 @@ +use crate::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, OptionFromAbi, OptionIntoAbi, ReturnAbi, + ReturnMode, Slot, Sret, WasmAbi, WatType, +}; +use crate::wire::const_concat; + +macro_rules! slot { + ($wat:expr, $($ty:ty),+ $(,)?) => {$( + // SAFETY: The declared WAT type describes this primitive `ABI` slot. + unsafe impl Slot for $ty { + const WAT_TYPE: Option = Some($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 shim 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, + 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!( + "$slot1 === ", + $js_sentinel, + " ? undefined : ", + $to_js + ))); + + type Abi = $carrier; + + fn into_option_abi(value: Option<$ty>) -> Self::Abi { + value.map_or($sentinel, |value| { + sentinel_option!(@into_carrier $into_carrier, value, $carrier) + }) + } + } + + // SAFETY: The sentinel is decoded before the carrier is converted back. + unsafe impl OptionFromAbi<$ty> for $ty { + const JS_CONV: Option = Some(FromJsConv::slot1(const_concat!( + "$value == null ? ", + $js_sentinel, + " : ", + $from_js + ))); + + type Abi = $carrier; + + #[expect( + clippy::allow_attributes, + reason = "one macro body covers carriers with different cast lints" + )] + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "JavaScript normalizes the carrier to this type's value range" + )] + fn from_option_abi(raw: Self::Abi) -> Option<$ty> { + if raw == $sentinel { + None + } else { + Some(raw as $ty) + } + } + } + )+}; + (@into_carrier widen, $value:ident, $carrier:ty) => { + <$carrier>::from($value) + }; + (@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:ident => { + decode: $decode:expr, + encode: $encode: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. + 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 OptionIntoAbi<$ty> for $ty { + const JS_CONV: Option = Some($decode); + + type Abi = Option<$ty>; + + 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 OptionFromAbi<$ty> for $ty { + const JS_CONV: Option = { + 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>; + + fn from_option_abi(raw: Self::Abi) -> Option<$ty> { + raw + } + } + )+}; +} + +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!(WatType::I32, isize, usize); +#[cfg(target_arch = "wasm64")] +slot!(WatType::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 !== 0")); + + 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") + .with_embed("js_sys", "numeric.128.encode"), + ); + const JS_SRET: Option = Some(Sret::Slots("this.#jsEmbed.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") + .with_embed("js_sys", "numeric.128.encode"), + ); + const JS_SRET: Option = Some(Sret::Slots("this.#jsEmbed.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 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; + +// 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", + into_carrier: widen, + to_js: "$slot1", + from_js: "$value", + types: [i8, u8, i16, u16], +} + +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "9007199254740991", + into_carrier: widen, + to_js: "$slot1", + from_js: "$value >> 0", + types: [i32], +} + +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "9007199254740991", + into_carrier: widen, + to_js: "$slot1", + from_js: "$value >>> 0", + types: [u32], +} + +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "9007199254740991", + 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: "9007199254740991", + into_carrier: pointer, + to_js: "$slot1", + from_js: "$value >> 0", + types: [isize], +} + +#[cfg(target_arch = "wasm32")] +sentinel_option! { + carrier: f64, + sentinel: F64_OPTION_SENTINEL, + js_sentinel: "9007199254740991", + into_carrier: pointer, + to_js: "$slot1", + from_js: "$value >>> 0", + types: [usize], +} + +indirect_option! { + f64 => { + decode: IntoJsConv::new("$slot1 === 0 ? undefined : $slot2"), + encode: "optional.f64.encode", + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0 : $value"), + None, + None, + ], + }, + i64 => { + decode: IntoJsConv::new("$slot1 === 0 ? undefined : $slot2"), + encode: "optional.i64.encode", + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], + }, + u64 => { + decode: IntoJsConv::new("$slot1 === 0 ? undefined : BigInt.asUintN(64, $slot2)"), + encode: "optional.u64.encode", + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], + }, +} + +#[cfg(target_arch = "wasm64")] +indirect_option! { + isize => { + decode: IntoJsConv::new("$slot1 === 0 ? undefined : $slot2"), + encode: "optional.i64.encode", + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], + }, + usize => { + decode: IntoJsConv::new("$slot1 === 0 ? undefined : BigInt.asUintN(64, $slot2)"), + encode: "optional.u64.encode", + slots: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + None, + None, + ], + }, +} + +indirect_option! { + u128 => { + 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: [ + Some("$value == null ? 0 : 1"), + Some("$value == null ? 0n : $value"), + Some("$value == null ? 0n : $value >> 64n"), + None, + ], + }, + i128 => { + 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: [ + 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.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.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.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 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/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/slice.rs b/client/js-sys/src/interop/slice.rs new file mode 100644 index 00000000..b6b5afea --- /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 new file mode 100644 index 00000000..5257abb9 --- /dev/null +++ b/client/js-sys/src/interop/string.rs @@ -0,0 +1,463 @@ +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: 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", + " }}", + "}})()", +); + +#[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.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", + "(() => {{", + " 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", + " }}", + "}})()", +); + +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(crate::const_concat!( + "this.#jsEmbed.js_sys['string.decode'](", + JS_PTR_LEN_ARGS, + ")" + )) + .with_embed("js_sys", "string.decode"), + ); + + type Abi = ExternSlice; + + fn into_abi(self) -> Self::Abi { + 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)") + .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; + + 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)") + .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; + + 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..e708a02b --- /dev/null +++ b/client/js-sys/src/interop/vec.rs @@ -0,0 +1,603 @@ +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::hazard::{ + EmptySlot, FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, RefFromJS, 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; + const JS_SRET: Sret; + + 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); + const JS_SRET: Option = Some(T::JS_SRET); + + 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() + } +} + +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 +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)") + .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; + + 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)") + .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; + + 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)" + )) + .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" + )); + + 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 d9d71b84..bf50dc8d 100644 --- a/client/js-sys/src/lib.rs +++ b/client/js-sys/src/lib.rs @@ -9,28 +9,44 @@ extern crate alloc; #[macro_use] mod util; -mod array; -mod bigint; -mod externref; + +// JavaScript standard built-in objects. +mod builtins; +// `Runtime` support for Rust and JavaScript `interop`. +mod runtime; + pub mod hazard; +// JavaScript `ABI` implementations for Rust types and Rust-facing APIs for +// JavaScript values. +mod interop; #[doc(hidden)] -pub mod r#macro; -mod number; -mod numeric; -mod panic; -mod string; -mod value; +pub mod wire; +pub use builtins::{ + 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; -#[cfg(feature = "macro")] -pub use js_sys_macro::js_sys; - -pub use crate::array::{JsArray, TryFromJsArrayError}; -pub use crate::bigint::JsBigInt; -pub use crate::number::JsNumber; -pub use crate::panic::{UnwrapThrowExt, panic}; -pub use crate::string::JsString; -pub use crate::value::JsValue; +pub use js_sys_macro::{closure, js_sys}; +pub use runtime::{ + Closure, ClosureAllocation, ClosureHeader, JsFuture, JsValue, UnwrapThrowExt, block_on, + 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/macro.rs b/client/js-sys/src/macro.rs deleted file mode 100644 index fe3c8da7..00000000 --- a/client/js-sys/src/macro.rs +++ /dev/null @@ -1,385 +0,0 @@ -#[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] - } -} 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 dc460790..00000000 --- a/client/js-sys/src/number/number.gen.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use core::marker::PhantomData; -use crate::JsValue; -use crate::hazard::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; - -#[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 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.value) - } -} - -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; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: Output::from_raw(raw), - _type: PhantomData, - } - } -} diff --git a/client/js-sys/src/number/number.js-sys.rs b/client/js-sys/src/number/number.js-sys.rs deleted file mode 100644 index 3cf3c262..00000000 --- a/client/js-sys/src/number/number.js-sys.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[js_sys] -extern "js-sys" { - pub type JsNumber; -} 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/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 new file mode 100644 index 00000000..9bad2ca0 --- /dev/null +++ b/client/js-sys/src/runtime/closure.rs @@ -0,0 +1,353 @@ +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::builtins::Function; +use crate::hazard::{IntoJS, IntoJsConv, JsCast}; + +#[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)] +#[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 } + } + + /// 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. + let drop = unsafe { (*pointer).drop }; + // SAFETY: The same caller guarantee satisfies the stored drop function. + unsafe { drop(pointer) }; + } + + /// 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) }; + } +} + +#[crate::js_sys(js_sys = crate)] +fn closure_drop(data: usize) { + 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::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) => {{", + " 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", + "}}", +); + +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 {{", + " release()", + " }}", + " }}", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", + "}}", +); + +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", + " release()", + " }}", + " }}", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", + "}}", +); + +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", + " release()", + " }}", + " }}", + " const release = this.#jsEmbed.js_sys['closure.own'](callback, state)", + " return callback", + "}}", +); + +/// An owned Rust closure exposed as a JavaScript function. +#[repr(transparent)] +pub struct Closure { + value: JsValue, + _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 { + &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 unsafe 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_unref(&self.value); + } +} diff --git a/client/js-sys/src/runtime/exception.rs b/client/js-sys/src/runtime/exception.rs new file mode 100644 index 00000000..7ee763da --- /dev/null +++ b/client/js-sys/src/runtime/exception.rs @@ -0,0 +1,129 @@ +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}; + +#[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) }; +} + +#[cfg(target_feature = "exception-handling")] +js_bindgen::import_js!( + module = "js_sys", + name = "exception.tag", + "WebAssembly.JSTag", +); + +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/runtime/externref.rs b/client/js-sys/src/runtime/externref.rs new file mode 100644 index 00000000..79d44c19 --- /dev/null +++ b/client/js-sys/src/runtime/externref.rs @@ -0,0 +1,334 @@ +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, RefType, WatImport, WatImportKind, WatIndexType, WatLocal, WatType}; +use crate::util::{PtrConst, PtrLength}; + +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) + 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))", + "(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.remove (@sym) (param $index i32)", + " local.get $index", + " ref.null extern", + " table.set $js_sys.import.externref.table (@reloc)", + ")", +); + +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", + "}})()" +); + +js_bindgen::import_js!( + module = "js_sys", + name = "externref.table", + required_embeds = [("js_sys", "externref.table")], + "this.#jsEmbed.js_sys['externref.table']", +); + +unsafe extern "C" { + #[link_name = "js_sys.externref.grow"] + safe fn grow(size: i32) -> i32; + #[link_name = "js_sys.externref.remove"] + safe fn remove(index: i32); +} + +struct Slab { + data: Vec, + head: usize, + base: usize, + table_len: usize, +} + +impl Slab { + const fn new() -> Self { + Self { + data: Vec::new(), + head: 0, + base: 0, + table_len: 0, + } + } + + // `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.table_len { + 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.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.table_len { + 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 + } + + #[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"), + } + } +} + +// 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()) }; +} + +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) -> PtrLength { + PtrLength::new(&self.slots) + } + + 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 { + fn drop(&mut self) { + if !self.committed { + let mut slab = EXTERNREF_SLAB.0.borrow_mut(); + for &index in &self.slots { + remove(index); + 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 reserved = ReservedSlots { + slots, + committed: false, + }; + + let mut slab = EXTERNREF_SLAB.0.borrow_mut(); + while reserved.slots.len() < count { + reserved.slots.push(index_to_abi(slab.alloc())); + } + + 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"))] +#[inline] +pub(crate) fn reserve() -> i32 { + 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 { + 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()) +} + +#[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/runtime/future/jspi/atomic.rs b/client/js-sys/src/runtime/future/jspi/atomic.rs new file mode 100644 index 00000000..6fe65ae8 --- /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..baef08ff --- /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 new file mode 100644 index 00000000..38bb9d49 --- /dev/null +++ b/client/js-sys/src/runtime/future/mod.rs @@ -0,0 +1,192 @@ +//! Bridges JavaScript promises and Rust futures. + +mod jspi; +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}; + +pub use jspi::block_on; + +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..0df0a705 --- /dev/null +++ b/client/js-sys/src/runtime/future/queue.rs @@ -0,0 +1,93 @@ +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" { + #[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..499ebc9e --- /dev/null +++ b/client/js-sys/src/runtime/future/task/atomic.rs @@ -0,0 +1,186 @@ +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::sync::Arc; +use core::cell::RefCell; +use core::future::Future; +use core::pin::Pin; +use core::sync::atomic::{AtomicI32, Ordering}; +use core::task::{Context, 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 signal(&self) { + if self.state.swap(AWAKE, Ordering::SeqCst) == AWAKE { + return; + } + + notify(PtrConst::from_ref(&self.state)); + } +} + +impl alloc::task::Wake for Wake { + fn wake(self: Arc) { + self.signal(); + } + + fn wake_by_ref(self: &Arc) { + self.signal(); + } +} + +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(); + let waker = Waker::from(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.signal(); + 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::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 new file mode 100644 index 00000000..b329ed04 --- /dev/null +++ b/client/js-sys/src/runtime/mod.rs @@ -0,0 +1,14 @@ +mod allocator; +pub(crate) mod closure; +pub(crate) mod exception; +pub(crate) mod externref; +mod future; +mod panic; +mod value; + +pub use closure::Closure; +#[doc(hidden)] +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/panic.rs b/client/js-sys/src/runtime/panic.rs similarity index 88% rename from client/js-sys/src/panic.rs rename to client/js-sys/src/runtime/panic.rs index e07db521..b9fda854 100644 --- a/client/js-sys/src/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 new file mode 100644 index 00000000..a2537f41 --- /dev/null +++ b/client/js-sys/src/runtime/value.rs @@ -0,0 +1,283 @@ +use core::marker::PhantomData; +use core::mem::{ManuallyDrop, MaybeUninit}; +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_IMPORTS, WAT_TAKE_CONV, WAT_TAKE_IMPORTS, release, +}; +use crate::hazard::{ + FromJS, FromJsConv, IntoJS, IntoJsConv, JsCast, OptionFromAbi, OptionIntoAbi, ReturnAbi, + ReturnMode, Slot, WatConv, WatType, +}; + +#[derive(Debug)] +#[repr(transparent)] +pub struct JsValue { + index: i32, + _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)] +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: 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. +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: 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: 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. +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); + + pub(crate) const fn new(index: i32) -> Self { + Self { + index, + _local: PhantomData, + } + } + + 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(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(crate) fn from_uninit_slice_mut( + slice: &mut [MaybeUninit], + ) -> &mut [MaybeUninit] { + let ptr: *mut MaybeUninit = slice.as_mut_ptr().cast(); + // SAFETY: `JsCast` assumes that `T` is `#[transparent]` over a `JsValue`. + unsafe { slice::from_raw_parts_mut(ptr, slice.len()) } + } + + // MSRV: This functionality will be removed in v1.95 when the standard library + // has more convenient functions to cast `MaybeUninit` arrays. + pub(crate) fn from_mut_uninit_array( + array: &mut MaybeUninit<[T; N]>, + ) -> &mut MaybeUninit<[Self; N]> { + let ptr: *mut MaybeUninit<[Self; N]> = array.as_mut_ptr().cast(); + // SAFETY: `JsCast` assumes that `T` is `#[transparent]` over a `JsValue`. + unsafe { ptr.as_mut() }.unwrap() + } +} + +impl Clone for JsValue { + #[inline] + fn clone(&self) -> Self { + 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)))", + "(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", + " 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(index: i32) -> i32; + } + + Self::new(clone(self.index)) + } +} + +impl Drop for JsValue { + #[inline] + fn drop(&mut self) { + if u32::from_ne_bytes(self.index.to_ne_bytes()) >= 2 { + release(self.index); + } + } +} + +// 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; + + fn into_abi(self) -> Self::Abi { + JsValueRefAbi(self.unchecked_as_ref().index) + } +} + +// SAFETY: `JsValue` is transparently represented by itself. +unsafe impl JsCast for JsValue {} + +// SAFETY: The owned table index is transferred to JavaScript and recycled +// after the WAT shim 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) + } +} + +// 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_abi(raw: Self::Abi) -> Self { + T::unchecked_from(JsValue::new(raw.0)) + } +} + +// 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 into_option_abi(value: Option) -> Self::Abi { + value.map_or(Self(JsValue::UNDEFINED.index), |value| { + IntoJS::into_abi(value) + }) + } +} + +// 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; + + type Abi = OptionalJsValueAbi; + + fn into_option_abi(value: Option) -> Self::Abi { + match value { + None => OptionalJsValueAbi(JsValue::UNDEFINED.index), + Some(value) => { + let Self(index) = IntoJS::into_abi(value); + OptionalJsValueAbi(index) + } + } + } +} + +// 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_option_abi(raw: Self::Abi) -> Option { + (raw.0 != JsValue::UNDEFINED.index).then(|| T::from_abi(Self(raw.0))) + } +} + +impl PartialEq for JsValue { + fn eq(&self, other: &Self) -> bool { + js_bindgen::embed_js!( + module = "js_sys", + name = "js_value.partial_eq", + "(value1, value2) => value1 === value2", + ); + + js_value_partial_eq(self, other) + } +} diff --git a/client/js-sys/src/string/mod.rs b/client/js-sys/src/string/mod.rs deleted file mode 100644 index 74378bff..00000000 --- a/client/js-sys/src/string/mod.rs +++ /dev/null @@ -1,212 +0,0 @@ -#[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 crate::JsValue; -use crate::hazard::{Input, InputJsConv, InputWatConv}; -use crate::util::{ExternSlice, PtrConst, PtrLength, PtrMut}; - -impl JsString { - #[must_use] - pub fn new(value: &JsValue) -> Self { - string::string_constructor(value) - } -} - -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::string_eq( - self, - PtrConst::new(other.as_bytes()), - PtrLength::new(other.as_bytes()), - ) - } - } -} - -impl PartialEq for JsString { - fn eq(&self, other: &String) -> bool { - self.eq(&other.as_str()) - } -} - -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( - PtrConst::new(value.as_bytes()), - PtrLength::new(value.as_bytes()), - ) - } - } -} - -impl From<&JsString> for String { - fn from(value: &JsString) -> Self { - js_bindgen::embed_js!( - module = "js_sys", - name = "string.utf8_length", - "(string) => new TextEncoder().encode(string).length", - ); - - #[cfg(any(not(target_feature = "atomics"), js_sys_target_feature = "sab"))] - js_bindgen::embed_js!( - module = "js_sys", - name = "string.encode", - "(string, ptr, len) => {{", - " const view = new Uint8Array(this.#memory.buffer, ptr, len)", - " new TextEncoder().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.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)", - " }} else {{", - " const bytes = new TextEncoder().encode(string)", - " new Uint8Array(this.#memory.buffer).set(bytes, ptr)", - " }}", - "}}", - ); - - let len = string::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::string_encode( - value, - PtrMut::new(&mut vec), - PtrLength::from_uninit_slice(vec.spare_capacity_mut()), - ); - } - - // SAFETY: - unsafe { - vec.set_len(len); - Self::from_utf8_unchecked(vec) - } - } -} - -#[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", - "", - " const array = new WebAssembly.Memory({{ initial: 0, maximum: 0, shared: true }})", - " try {{", - " new TextDecoder().decode(array)", - " return true", - " }} catch {{", - " return false", - " }}", - "}})()", -); - -// 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 deleted file mode 100644 index b88ed367..00000000 --- a/client/js-sys/src/string/string.gen.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![allow(warnings)] - -use crate::{js_bindgen, r#macro, JsValue}; -use crate::hazard::{InputJsConv, OutputJsConv, OutputWatConv, Input, InputWatConv, Output, 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 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) - } -} - -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; - - type Type = ::Type; - - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) - } -} - -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), - } - - js_bindgen::import_js! { - module = "js_sys", - name = "string_constructor", - required_embeds = [ - r#macro::js_input_embed::<&JsValue>(), - r#macro::js_output_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, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_constructor"] - fn string_constructor(value: <&JsValue as Input>::Type) -> ::Type; - } - - Output::from_raw(unsafe { string_constructor(Input::into_raw(value)) }) -} - -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), - } - - 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::(), - ], - "{}{}{}{}{}", - 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, - ), - } - - 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; - } - - Output::from_raw(unsafe { - string_eq(Input::into_raw(string), Input::into_raw(array), Input::into_raw(len)) - }) -} - -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), - } - - 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::(), - ], - "{}{}{}{}", - 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, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_decode"] - fn string_decode( - array: as Input>::Type, - len: as Input>::Type, - ) -> ::Type; - } - - Output::from_raw(unsafe { string_decode(Input::into_raw(array), Input::into_raw(len)) }) -} - -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), - } - - 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::(), - ], - "{}{}{}", - 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, - ), - } - - unsafe extern "C" { - #[link_name = "js_sys.string_utf8_length"] - fn string_utf8_length(string: <&JsString as Input>::Type) -> ::Type; - } - - Output::from_raw(unsafe { string_utf8_length(Input::into_raw(string)) }) -} - -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 >), - } - - 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_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), - ), - } - - 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, - ); - } - - unsafe { string_encode(Input::into_raw(string), Input::into_raw(array), Input::into_raw(len)) }; -} 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/util.rs b/client/js-sys/src/util.rs index b30b4d00..8c135c55 100644 --- a/client/js-sys/src/util.rs +++ b/client/js-sys/src/util.rs @@ -1,17 +1,18 @@ 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, WatType}; 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"))] @@ -34,41 +35,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 +78,89 @@ 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>() - ); -}; +#[cfg(target_arch = "wasm32")] +type JsPointerType = u32; +#[cfg(target_arch = "wasm64")] +type JsPointerType = f64; -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, -); +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::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` +// 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")] -type WatUsizeType = u32; +pub(crate) const JS_PTR_LEN_ARGS: &str = "$slot1 >>> 0, $slot2 >>> 0"; #[cfg(target_arch = "wasm64")] -type WatUsizeType = f64; +pub(crate) const JS_PTR_LEN_ARGS: &str = "$slot1, $slot2"; #[cfg(target_arch = "wasm32")] -pub(crate) const WAT_PTR_TYPE: &str = "i32"; +pub(crate) const JS_OPTION_PTR_LEN_ARGS: &str = "$slot2 >>> 0, $slot3 >>> 0"; #[cfg(target_arch = "wasm64")] -pub(crate) const WAT_PTR_TYPE: &str = "i64"; +pub(crate) const JS_OPTION_PTR_LEN_ARGS: &str = "$slot2, $slot3"; #[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: value.as_ptr(), + } + } + pub(crate) fn from_ref(value: &T) -> Self { Self { - ptr, - _ty: PhantomData, + 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: 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 shim converts it to `f64` without losing precision. +unsafe impl Slot for PtrConst { + const WAT_TYPE: Option = WAT_PTR_TYPE; + const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; +} - #[cfg(target_arch = "wasm32")] - type Type = *const T; - #[cfg(target_arch = "wasm64")] - type Type = f64; +// SAFETY: The JavaScript conversion matches the JavaScript-facing WAT type. +unsafe impl IntoJS for PtrConst { + const JS_CONV: Option = JsPointerType::JS_CONV; - fn into_raw(self) -> Self::Type { - self.ptr + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self } } #[repr(transparent)] pub(crate) struct PtrMut { - ptr: ::Type, - _ty: PhantomData, + ptr: *mut T, } impl PtrMut { @@ -165,83 +177,81 @@ 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 shim converts it to `f64` without losing precision. +unsafe impl Slot for PtrMut { + const WAT_TYPE: Option = WAT_PTR_TYPE; + const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; +} - #[cfg(target_arch = "wasm32")] - type Type = *mut T; - #[cfg(target_arch = "wasm64")] - type Type = f64; +// SAFETY: The JavaScript conversion matches the JavaScript-facing WAT type. +unsafe impl IntoJS for PtrMut { + const JS_CONV: Option = JsPointerType::JS_CONV; - fn into_raw(self) -> Self::Type { - self.ptr + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self } } #[repr(transparent)] -pub(crate) struct PtrLength { - len: ::Type, +pub struct PtrLength { + len: usize, _ty: PhantomData, } 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 { - #[cfg(target_arch = "wasm64")] - #[expect( - clippy::cast_precision_loss, - reason = "can't be larger than `MAX_SAFE_INTEGER`" - )] - let len = len as ::Type; - + 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: 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 +// shim converts it to `f64` without losing precision. +unsafe impl Slot for PtrLength { + const WAT_TYPE: Option = WAT_PTR_TYPE; + const INTO_JS_WAT_CONV: Option = PTR_INTO_JS_WAT_CONV; +} - #[cfg(target_arch = "wasm32")] - type Type = usize; - #[cfg(target_arch = "wasm64")] - type Type = f64; +// SAFETY: The JavaScript conversion matches the JavaScript-facing WAT type. +unsafe impl IntoJS for PtrLength { + const JS_CONV: Option = JsPointerType::JS_CONV; - fn into_raw(self) -> Self::Type { - self.len + type Abi = Self; + + fn into_abi(self) -> Self::Abi { + self } } @@ -250,9 +260,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;", "}})()", ); @@ -289,23 +299,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")] @@ -329,14 +339,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")] @@ -355,16 +365,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")] @@ -385,26 +395,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"))] @@ -420,19 +430,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")] @@ -443,16 +453,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")] @@ -464,8 +474,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/src/value/mod.rs b/client/js-sys/src/value/mod.rs deleted file mode 100644 index d5f122a2..00000000 --- a/client/js-sys/src/value/mod.rs +++ /dev/null @@ -1,148 +0,0 @@ -#[rustfmt::skip] -#[path ="value.gen.rs"] -mod value; - -use core::marker::PhantomData; -use core::mem::MaybeUninit; -use core::slice; - -use crate::externref::EXTERNREF_TABLE; -use crate::hazard::{Input, InputWatConv, JsCast, Output, OutputWatConv}; - -#[derive(Debug)] -#[repr(transparent)] -pub struct JsValue { - index: i32, - _local: PhantomData<*const ()>, -} - -impl JsValue { - pub const UNDEFINED: Self = Self::new(0); - pub const NULL: Self = Self::new(1); - - const fn new(index: i32) -> Self { - Self { - index, - _local: PhantomData, - } - } - - pub 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] { - 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( - slice: &mut [MaybeUninit], - ) -> &mut [MaybeUninit] { - let ptr: *mut MaybeUninit = slice.as_mut_ptr().cast(); - // SAFETY: `JsCast` assumes that `T` is `#[transparent]` over a `JsValue`. - unsafe { slice::from_raw_parts_mut(ptr, slice.len()) } - } - - // MSRV: This functionality will be removed in v1.95 when the standard library - // has more convenient functions to cast `MaybeUninit` arrays. - pub(crate) fn from_mut_uninit_array( - array: &mut MaybeUninit<[T; N]>, - ) -> &mut MaybeUninit<[Self; N]> { - let ptr: *mut MaybeUninit<[Self; N]> = array.as_mut_ptr().cast(); - // SAFETY: `JsCast` assumes that `T` is `#[transparent]` over a `JsValue`. - unsafe { ptr.as_mut() }.unwrap() - } -} - -impl Clone for JsValue { - 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)))", - "(func $js_sys.js_value.clone (@sym) (param $index i32) (result i32)", - " local.get $index", - " call $js_sys.externref.get (@reloc)", - " call $js_sys.externref.insert (@reloc)", - ")", - ); - - unsafe extern "C" { - #[link_name = "js_sys.js_value.clone"] - safe fn clone(size: i32) -> i32; - } - - if self.index > 1 { - Self::new(clone(self.index)) - } else { - Self::new(self.index) - } - } -} - -impl Drop for JsValue { - fn drop(&mut self) { - if self.index > 1 { - EXTERNREF_TABLE.with(|table| table.try_borrow_mut().unwrap().remove(self.index)); - } - } -} - -// 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", - }); - - type Type = i32; - - fn into_raw(self) -> Self::Type { - self.index - } -} - -// SAFETY: The OG type. -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", - }); - - type Type = i32; - - fn from_raw(raw: Self::Type) -> Self { - Self::new(raw) - } -} - -impl PartialEq for JsValue { - fn eq(&self, other: &Self) -> bool { - js_bindgen::embed_js!( - module = "js_sys", - name = "js_value.partial_eq", - "(value1, value2) => value1 === value2", - ); - - value::js_value_partial_eq(self, other) - } -} 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 cc646c88..00000000 --- a/client/js-sys/src/value/value.gen.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![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), - } - - 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::(), - ], - "{}{}{}{}", - 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, - ), - } - - 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; - } - - Output::from_raw(unsafe { - js_value_partial_eq(Input::into_raw(value1), Input::into_raw(value2)) - }) -} 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/js-sys/src/wire/closure.rs b/client/js-sys/src/wire/closure.rs new file mode 100644 index 00000000..4560cbfe --- /dev/null +++ b/client/js-sys/src/wire/closure.rs @@ -0,0 +1,42 @@ +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)] +#[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 new file mode 100644 index 00000000..5c27abef --- /dev/null +++ b/client/js-sys/src/wire/export.rs @@ -0,0 +1,67 @@ +use crate::hazard::{FromJS, ReturnAbi, ReturnConv, ReturnIntoJS, ReturnMode, WasmRet}; +use crate::wire::{ + WireExportInput, WireExportInputType, WireExportOutput, WireExportOutputType, WireReturnFrame, + from_js_slots, into_js_slots, +}; + +trait MetadataFor: 'static { + const VALUE: &'static Self; +} + +impl MetadataFor for WireExportInputType { + const VALUE: &'static Self = &Self::new(from_js_slots::(), T::JS_CONV); +} + +impl MetadataFor for WireExportOutputType { + const VALUE: &'static Self = &{ + let mode = ::MODE; + let slots = into_js_slots::(); + match mode { + ReturnMode::Direct => { + 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 => { + // 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() -> WireExportInput { + WireExportInput::new(>::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) +} diff --git a/client/js-sys/src/wire/import.rs b/client/js-sys/src/wire/import.rs new file mode 100644 index 00000000..e6d44674 --- /dev/null +++ b/client/js-sys/src/wire/import.rs @@ -0,0 +1,52 @@ +use crate::hazard::{IntoJS, ReturnAbi, ReturnFromJS}; +use crate::util::PtrMut; +use crate::wire::{ + WireImportCatch, WireImportInputType, WireImportOutputType, from_js_slots, into_js_slots, +}; + +trait MetadataFor: 'static { + const VALUE: &'static Self; +} + +impl MetadataFor for WireImportInputType { + const VALUE: &'static Self = &Self::new(into_js_slots::(), T::JS_CONV); +} + +impl MetadataFor for WireImportOutputType { + const VALUE: &'static Self = &{ + Self::new( + ::MODE, + T::JS_CONV, + T::JS_SRET, + from_js_slots::(), + ) + }; +} + +/// 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..96fa6711 --- /dev/null +++ b/client/js-sys/src/wire/mod.rs @@ -0,0 +1,133 @@ +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, WireClosure, WireClosureFactory, WireExport, WireExportInput, + WireExportInputType, WireExportOutput, WireExportOutputType, WireGlobalPath, WireImport, + WireImportBinding, WireImportCatch, WireImportInput, WireImportInputType, WireImportOutput, + WireImportOutputType, WireImportTypeTable, WireReturnFrame, 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(rust) => Some(WatSlot::new(rust, conversion)), + None => None, + } +} + +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; +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 OutputRet = MaybeUninit::Abi>>; + +#[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/array.rs b/client/js-sys/tests/array.rs index 6263ba28..aec2674f 100644 --- a/client/js-sys/tests/array.rs +++ b/client/js-sys/tests/array.rs @@ -1,30 +1,201 @@ use core::array; +use core::mem::MaybeUninit; use js_bindgen_test::test; -use js_sys::{JsArray, 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]", + " }}", + "}})", +); +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) -> 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!(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_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); + let returned_array: [JsValue; 42] = js_array.to_array().unwrap(); assert_eq!(rust_array, returned_array); 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] @@ -32,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/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/client/js-sys/tests/future.rs b/client/js-sys/tests/future.rs new file mode 100644 index 00000000..5c97a95a --- /dev/null +++ b/client/js-sys/tests/future.rs @@ -0,0 +1,77 @@ +use core::cell::Cell; +use std::rc::Rc; + +use js_bindgen_test::test; +use js_sys::{JsString, JsValue, Promise, future_to_promise, spawn_local}; + +#[test] +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] +#[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() { + // Regression test: generated async-test exports include their module path. + Promise::resolve(&JsValue::UNDEFINED).await.unwrap(); + } +} + +mod second { + use js_bindgen_test::test; + use js_sys::{JsValue, Promise}; + + #[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 new file mode 100644 index 00000000..f1d5bf3e --- /dev/null +++ b/client/js-sys/tests/hazard.rs @@ -0,0 +1,161 @@ +use js_bindgen_test::test; +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", + 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", +); +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); + +// SAFETY: `NumberSlot` is an i32 carrier converted to a JS Number on input. +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); + +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 + } +} + +// 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] + 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))); +} + +#[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/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 58646775..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,18 +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); -signed!(JsNumber, i8, i16, i32); -signed!(JsBigInt, i64, i128); +#[cfg(target_arch = "wasm32")] +unsigned!(Number, usize); +#[cfg(target_arch = "wasm64")] +unsigned!(BigInt, usize); + +signed!(Number, i8, i16, i32); +signed!(BigInt, i64, i128); + +#[cfg(target_arch = "wasm32")] +signed!(Number, isize); +#[cfg(target_arch = "wasm64")] +signed!(BigInt, isize); diff --git a/client/js-sys/tests/optional.rs b/client/js-sys/tests/optional.rs new file mode 100644 index 00000000..64dd2964 --- /dev/null +++ b/client/js-sys/tests/optional.rs @@ -0,0 +1,132 @@ +#![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::{Array, 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()); + + // 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!(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 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] + 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<&Array>) -> 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).unwrap(), "test"); + + 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()); + + 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/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..20d07a40 --- /dev/null +++ b/client/js-sys/tests/typed_array.rs @@ -0,0 +1,221 @@ +#![allow( + 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 751a10c8..a95de1b5 100644 --- a/client/js-sys/tests/value.rs +++ b/client/js-sys/tests/value.rs @@ -1,9 +1,26 @@ 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_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 string = JsString::new(&JsValue::UNDEFINED); + let value = JsValue::UNDEFINED.clone(); + let string = JsString::new(&value).unwrap(); let string = String::from(&string); assert_eq!(string, "undefined"); @@ -11,15 +28,28 @@ fn undefined() { #[test] fn null() { - let string = JsString::new(&JsValue::NULL); + let value = JsValue::NULL.clone(); + let string = JsString::new(&value).unwrap(); let string = String::from(&string); assert_eq!(string, "null"); } #[test] -fn clone() { +fn nan_strict_equality() { + let value = nan(); + assert!(!PartialEq::eq(&value, &value)); +} + +#[test] +fn externref_reuse() { let value = JsString::from("Hello, World!"); - let value = value.clone(); - assert_eq!(value, "Hello, World!"); + let values: Vec<_> = (0..512).map(|_| value.clone()).collect(); + 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..e5025465 --- /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_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_eq!(u32_identity(Vec::new()), Vec::::new()); +} + +#[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/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/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/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/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 e6d01ee6..00000000 --- a/client/web-sys/src/console.gen.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! This file was generated by `js-sys-bindgen`. - -#![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)", ")", - } - - js_bindgen::import_js! (module = "web_sys", name = "console.log0", "globalThis.console.log"); - - unsafe extern "C" { - #[link_name = "web_sys.console.log0"] - fn 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]), - } - - 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]), - ), - } - - unsafe extern "C" { - #[link_name = "web_sys.console.log"] - fn log(data: <&[JsValue] as Input>::Type); - } - - unsafe { log(Input::into_raw(data)) }; -} - -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), - } - - 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), - ), - } - - unsafe extern "C" { - #[link_name = "web_sys.console.log2"] - fn log2(data1: <&JsValue as Input>::Type, data2: <&JsValue as Input>::Type); - } - - unsafe { log2(Input::into_raw(data1), Input::into_raw(data2)) }; -} - -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), - } - - 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), - ), - } - - unsafe extern "C" { - #[link_name = "web_sys.console.error"] - fn error(data: <&JsValue as Input>::Type); - } - - unsafe { error(Input::into_raw(data)) }; -} diff --git a/client/web-sys/src/console.js-sys.rs b/client/web-sys/src/console.rs similarity index 77% rename from client/web-sys/src/console.js-sys.rs rename to client/web-sys/src/console.rs index 278353b6..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" { @@ -12,4 +12,7 @@ extern "js-sys" { pub fn log2(data1: &JsValue, data2: &JsValue); 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 8e7a6f94..c754ee53 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -1,8 +1,8 @@ [workspace] resolver = "3" members = [ - "cargo-js-sys", "cargo-shim", + "cli", "cli-lib", "dev", "inline-snap", @@ -17,6 +17,7 @@ members = [ "test-macro", "wasm-ld-opt", "web-driver", + "wire", ] default-members = ["dev"] @@ -35,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 } @@ -55,6 +54,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 = { path = "wire" } js-sys-bindgen = { path = "js-sys-bindgen" } memmap2 = "0.9" mime = "0.3" @@ -66,6 +66,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"] } @@ -83,7 +84,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/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/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..b5d231c7 100644 --- a/host/cli-lib/src/js/imports.mjs +++ b/host/cli-lib/src/js/imports.mjs @@ -1,9 +1,14 @@ 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 #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; @@ -53,8 +58,15 @@ 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; + // Export wrappers generated by `js-sys` use this stable binding. + const wasmExports = instance.exports; + this.#jsExports = Object.assign(Object.create(null), wasmExports, JBG_PLACEHOLDER_JS_EXPORT); + return { + instance, + exports: this.#jsExports, + }; }); } static async instantiateStreaming(...args) { diff --git a/host/cli-lib/src/js/imports.mts b/host/cli-lib/src/js/imports.mts index 1fb18279..f9fce793 100644 --- a/host/cli-lib/src/js/imports.mts +++ b/host/cli-lib/src/js/imports.mts @@ -2,13 +2,24 @@ 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 #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 @@ -61,20 +72,32 @@ 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 + + // Export wrappers generated by `js-sys` use this stable binding. + const wasmExports = instance.exports + this.#jsExports = Object.assign( + Object.create(null) as WebAssembly.Instance["exports"], + wasmExports, + JBG_PLACEHOLDER_JS_EXPORT + ) + return { + instance, + exports: this.#jsExports, + } }) } 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..3aa69825 --- /dev/null +++ b/host/cli/src/main.rs @@ -0,0 +1,142 @@ +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()))?; + + 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..19a7b61f 100644 --- a/host/dev/Cargo.toml +++ b/host/dev/Cargo.toml @@ -14,7 +14,10 @@ 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 } [dev-dependencies] cargo_metadata = { workspace = true, features = ["builder"] } 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/e2e.rs b/host/dev/src/client/e2e.rs new file mode 100644 index 00000000..faaa2322 --- /dev/null +++ b/host/dev/src/client/e2e.rs @@ -0,0 +1,228 @@ +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 => { + if example.name == "closure" { + command.arg("--expose-gc"); + } + 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 + ), + }; + // 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\nconst timer = \ + globalThis.setInterval(() => {{}}, 0x7fffffff)\ntry {{\n", + self.name + ); + + for test in &self.tests { + script.push_str(" assert("); + script.push_str(test); + script.push_str(", "); + write!(script, "{test:?}").unwrap(); + script.push_str(")\n\n"); + } + script.push_str("} finally {\n globalThis.clearInterval(timer)\n}\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..65e334e1 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; @@ -12,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; @@ -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..0807cbb7 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,20 +122,34 @@ 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 { + // `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; } @@ -143,9 +157,31 @@ 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 + .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)?; + } + } } } @@ -308,7 +344,7 @@ impl ValueEnum for Runner { } #[derive(Clone, Copy, EnumCount, EnumIter, Eq, PartialEq)] -enum Engine { +pub(super) enum Engine { Deno, NodeJs, Bun, @@ -323,7 +359,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/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/js-sys-bindgen/Cargo.toml b/host/js-sys-bindgen/Cargo.toml index d33df660..72959dca 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 = [ @@ -24,22 +23,14 @@ syn = { workspace = true, features = [ "printing", ] } weedle2 = { workspace = true, optional = true } +xxhash-rust = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } -cargo_metadata = { workspace = true } -indoc = { workspace = true } inline-snap = { workspace = true } -js-bindgen-ld-shared = { workspace = true } prettyplease = { workspace = true } -similar-asserts = { workspace = true } -tempfile = { workspace = true } -wasmparser = { workspace = true } [features] -file = ["macro"] -macro = [] -web-idl = ["dep:weedle2"] +web-idl = ["dep:foldhash", "dep:hashbrown", "dep:weedle2"] [lints] workspace = true diff --git a/host/js-sys-bindgen/src/closure.rs b/host/js-sys-bindgen/src/closure.rs new file mode 100644 index 00000000..befb61c6 --- /dev/null +++ b/host/js-sys-bindgen/src/closure.rs @@ -0,0 +1,332 @@ +use std::env; + +use proc_macro2::TokenStream; +use quote::{ToTokens, 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, +}; +use xxhash_rust::xxh3::xxh3_128; + +use crate::export::{ExportAbi, lower_abi}; + +mod keyword { + syn::custom_keyword!(js_sys); +} + +pub fn closure(input: TokenStream) -> Result { + let crate_name = env::var("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` not found"); + 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) +} + +pub(crate) fn closure_with( + input: TokenStream, + crate_name: &str, + package_name: &str, + package_version: &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 trait_syntax_hash = format!( + "{:032x}", + xxh3_128(trait_object.to_token_stream().to_string().as_bytes()), + ); + 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 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 closure = signature.closure_type(&trait_object); + let inputs: Vec<_> = signature.inputs.iter().collect(); + let output = signature.output.as_ref(); + let ExportAbi { + raw_types, + raw_inputs, + join_inputs, + arguments, + mut wire_inputs, + raw_output, + wire_output, + } = lower_abi(inputs.iter().copied(), output, &js_sys)?; + wire_inputs.insert( + 0, + quote_spanned! {span=> + #js_sys::wire::wire_export_input::<::core::primitive::usize>() + }, + ); + let closure_bound = if signature.kind == ClosureKind::Shared { + if let Some(output) = output { + quote_spanned!(span=> ::core::ops::Fn(#(#inputs),*) -> #output) + } else { + quote_spanned!(span=> ::core::ops::Fn(#(#inputs),*)) + } + } else { + if let Some(output) = output { + quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*) -> #output) + } else { + quote_spanned!(span=> ::core::ops::FnMut(#(#inputs),*)) + } + }; + let callback_call = if signature.kind == ClosureKind::Shared { + quote_spanned! {span=> + let callback = unsafe { + &*#js_sys::ClosureHeader::callback::(pointer) + }; + callback(#(#arguments),*) + } + } else { + quote_spanned! {span=> + let callback = unsafe { + &mut *#js_sys::ClosureHeader::callback::(pointer) + }; + callback(#(#arguments),*) + } + }; + let call_body = if output.is_some() { + quote_spanned! {span=> + #(#join_inputs)* + #js_sys::wire::return_to_js({ + #callback_call + }) + } + } else { + quote_spanned! {span=> + #(#join_inputs)* + #callback_call; + } + }; + 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 CallShim = unsafe extern "C" fn( + *mut #js_sys::ClosureHeader, + #(#raw_types),* + ) #raw_output; + + #[allow(clippy::undocumented_unsafe_blocks)] + unsafe extern "C" fn call_raw( + pointer: *mut #js_sys::ClosureHeader, + #(#raw_inputs),* + ) #raw_output + where + F: #closure_bound, + { + #call_body + } + + fn allocate( + callback: F, + ) -> #js_sys::ClosureAllocation + where + F: #closure_bound + 'static, + { + #js_sys::ClosureAllocation::new( + callback, + call_raw:: as CallShim, + ) + } + + const _: () = { + const _WIRE: #js_sys::wire::Wire = + #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")] + static _WIRE_SECTION: #js_sys::wire::WireBlob<_LEN> = + #js_sys::wire::WireBlob::new(&_WIRE); + }; + + let allocation = allocate(#expression); + 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) } + } + }) +} + +struct ClosureInput { + js_sys: Option, + trait_object: TypeTraitObject, + expression: Expr, +} + +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::()?; + 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: Option, +} + +#[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`", + )), + } + } +} + +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 = match &arguments.output { + ReturnType::Default => None, + ReturnType::Type(_, output) => Some(*output.clone()), + }; + + Ok(Self { + kind, + inputs: arguments.inputs.clone(), + output, + }) + } + + 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/export.rs b/host/js-sys-bindgen/src/export.rs new file mode 100644 index 00000000..3c1c5613 --- /dev/null +++ b/host/js-sys-bindgen/src/export.rs @@ -0,0 +1,304 @@ +use std::env; + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote_spanned}; +use syn::ext::IdentExt; +use syn::parse::Parser; +use syn::spanned::Spanned; +use syn::{Error, Expr, 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; + let mut js_name: Option = None; + let mut promising = false; + + 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 { + 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 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")) + } + }) + .parse2(attr)?; + + validate(function)?; + + let span = function.span(); + let js_sys: Path = js_sys.unwrap_or_else(|| parse_quote!(::js_sys)); + let macro_path: Path = parse_quote!(#js_sys::wire); + let ident = &function.sig.ident; + 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, + ); + let crate_name = LitStr::new(&crate_name, span); + let output_ty = match &function.sig.output { + ReturnType::Type(_, ty) => Some(ty.as_ref()), + ReturnType::Default => None, + }; + let inputs = function.sig.inputs.iter().map(|input| { + let FnArg::Typed(input) = input else { + unreachable!(); + }; + input.ty.as_ref() + }); + let ExportAbi { + raw_inputs, + join_inputs, + arguments, + wire_inputs, + raw_output, + wire_output, + .. + } = lower_abi(inputs, output_ty, &js_sys)?; + + let call = if function.sig.unsafety.is_some() { + quote_spanned!(span=> unsafe { #ident(#(#arguments),*) }) + } else { + quote_spanned!(span=> #ident(#(#arguments),*)) + }; + 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)* + #macro_path::return_to_js(#call) + } + } else { + quote_spanned! {span=> + #(#join_inputs)* + #call; + } + }; + let descriptor_constructor = if promising { + format_ident!("new_symbol_promising", span = span) + } else { + format_ident!("new_symbol", span = span) + }; + + Ok(quote_spanned! {span=> + #function + + const _: () = { + #[unsafe(export_name = #raw_export_name)] + extern "C" fn __export_( + #(#raw_inputs),* + ) #raw_output { + #raw_body + } + }; + + const _: () = { + 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, + ), + ]); + const _LEN: ::core::primitive::usize = #macro_path::wire_blob_len(&_WIRE); + + #[unsafe(link_section = "js_bindgen.wire")] + static _WIRE_SECTION: #macro_path::WireBlob<_LEN> = + #macro_path::WireBlob::new(&_WIRE); + }; + }) +} + +pub(crate) struct ExportAbi { + pub raw_types: Vec, + pub raw_inputs: Vec, + pub join_inputs: Vec, + pub arguments: Vec, + pub wire_inputs: Vec, + pub raw_output: TokenStream, + pub wire_output: 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 wire_inputs = Vec::new(); + + for (index, ty) in inputs.into_iter().enumerate() { + let span = ty.span(); + let argument = format_ident!("arg{index}", span = Span::mixed_site()); + 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::mixed_site()); + let slot_alias = format_ident!("FromJsSlot{slot}", span = span); + 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)); + slots.push(slot_ident); + } + + if let Some(reference) = reference { + let anchor = format_ident!("arg{index}_anchor", span = Span::mixed_site()); + let ty = &reference.elem; + + join_inputs.push(quote_spanned! {span=> + 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::wire::join_from_js::<#js_ty>(#(#slots),*); + }); + } + + wire_inputs.push(quote_spanned! {span=> + #js_sys::wire::wire_export_input::<#js_ty>() + }); + arguments.push(argument); + } + + 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()=> + -> #js_sys::hazard::WasmRet< + <#output as #js_sys::hazard::ReturnIntoJS>::Abi + > + }, + quote_spanned! {output.span()=> + ::core::option::Option::Some( + #js_sys::wire::wire_export_output::<#output>() + ) + }, + ) + }, + ); + + Ok(ExportAbi { + raw_types, + raw_inputs, + join_inputs, + arguments, + wire_inputs, + raw_output, + wire_output, + }) +} + +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/file.rs b/host/js-sys-bindgen/src/file.rs deleted file mode 100644 index 47682d37..00000000 --- a/host/js-sys-bindgen/src/file.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::mem; - -use proc_macro2::TokenStream; -use syn::{Error, File, Item, ItemMod, Meta, Path, Result, parse_quote}; - -use crate::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::internal(attr, foreign_mod, Some(crate_), Some(imports)) { - Ok(mut items) => output.append(&mut items), - 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 b543ade8..53e7209a 100644 --- a/host/js-sys-bindgen/src/function.rs +++ b/host/js-sys-bindgen/src/function.rs @@ -1,237 +1,282 @@ -use std::borrow::Cow; -use std::ops::{Deref, DerefMut}; -use std::str::FromStr; +use std::collections::{HashMap, HashSet}; +use std::mem; +use std::ops::DerefMut; use std::string::ToString; -use std::{iter, mem}; -use itertools::Itertools; -use proc_macro2::{Span, TokenStream}; +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, 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, GenericParam, Generics, Ident, LitStr, Pat, PatIdent, + PatType, Path, Receiver, Result, ReturnType, Signature, Token, TraitBoundModifier, Type, + TypeParamBound, TypePath, TypeReference, WherePredicate, parse_quote, }; -use crate::Hygiene; +use crate::hygiene::Hygiene; -pub enum Function { - Fn(ItemFn), - Impl(ItemImpl), -} +mod js; +mod options; -#[derive(Eq, PartialEq)] -pub enum FunctionJsOutput { - Generate { - js_name: Option, - property: bool, - }, - Embed(String), - Import, -} +use js::{ForeignItem, FunctionBinding}; +use options::{BindingKind, ConstructorOwner, FunctionOptions}; -struct State<'a> { - crate_: &'a str, - namespace: Option<&'a str>, - js_bindgen: Path, - r#macro: Option, - input: Path, - output: Path, - import_name: String, - foreign_name: String, - input_tys: Vec, - output_ty: Vec, - extern_input_names: Vec, - intern_input_names: Vec, - impl_generic_params: TokenStream, - r#type: OutputType, - span: Span, +/// Backend-independent description of one JavaScript import. +pub(crate) struct FunctionImport { + pub(crate) cfg_attrs: Vec, + pub(crate) module: LitStr, + pub(crate) name: LitStr, + pub(crate) inputs: Vec, + pub(crate) output_type: Option, + pub(crate) binding: Option, + pub(crate) suspending: bool, + pub(crate) macro_path: Path, } -enum OutputType { - Generate { - js_name: Option, - member: Option, - }, - Embed(String), - Import, +pub(crate) struct FunctionImportInput { + pub(crate) ty: Type, } -struct Member { - self_ty: Path, - r#type: MemberType, +struct FunctionPlan { + inputs: Vec, + output_ty: Option, + output_abi_override: Option, + impl_generic_params: TokenStream, + binding: ForeignItem, + suspending: bool, } -enum MemberType { - Method, - Getter, - Setter, +struct InputArg { + abi_type: Type, + rust_name: Ident, + slot_names: [Ident; 4], + uses_abi_override: bool, } -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", - )); +impl InputArg { + 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); + + Self { + abi_type, + rust_name, + slot_names: [slot_name(0), slot_name(1), slot_name(2), slot_name(3)], + uses_abi_override, } + } +} - if let Some(asyncness) = item.sig.asyncness { - return Err(Error::new_spanned( - asyncness, - "`async` 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(variadic) = &item.sig.variadic { - return Err(Error::new_spanned( - variadic, - "variadic functions are not supported", - )); - } + if let Some(asyncness) = item.sig.asyncness { + return Err(Error::new_spanned( + asyncness, + "`async` functions are not supported", + )); + } - let span = item.span(); - let ForeignItemFn { - attrs, - vis, - mut sig, - .. - } = item; - - let mut state = State::parse( - crate_, js_output, namespace, hygiene, &attrs, &mut sig, span, - )?; - let wat = state.wat(); - let js = state.js(); - let State { - input, - output, - foreign_name, - input_tys, - output_ty, - extern_input_names, - intern_input_names, - impl_generic_params, - r#type, - .. - } = state; - let ident = &sig.ident; + if let Some(variadic) = &item.sig.variadic { + return Err(Error::new_spanned( + variadic, + "variadic functions are not supported", + )); + } - 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=> ;)); + 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 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, + rust_name, + slot_names: [slot1, slot2, slot3, slot4], + uses_abi_override, + .. + } = input; + let split_input = if *uses_abi_override { + quote_spanned!(span=> unsafe { + #macro_path::split_input_as::<#abi_type>(#rust_name) + }) } else { - foreign_call = quote_spanned! (span=> #output::from_raw(#foreign_call)); - } - - let item_fn = parse_quote_spanned! {span=> - #(#attrs)* - #vis #sig { - #wat - - #js - - unsafe extern "C" { - #[link_name = #foreign_name] - fn #ident(#(#extern_input_names: <#input_tys as #input>::Type),*) #( -> <#output_ty as #output>::Type)*; - } - - #foreign_call - } + quote_spanned!(span=> #macro_path::split_input::<#abi_type>(#rust_name)) }; - 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 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>), + ); + + let foreign_call = quote_spanned! {span=> { + #(#split_inputs)* + 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"); + + 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 { + foreign_call + }; + + let item_fn = quote_spanned! {span=> + #(#attrs)* + #vis #sig { + unsafe extern "C" { + #[link_name = #link_name] + fn #foreign_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) = plan.binding.owner() { + let impl_generic_params = &plan.impl_generic_params; + quote_spanned! {span=> + impl #impl_generic_params #owner { + #item_fn + } } - } -} + } else { + item_fn + }; + let import = plan.into_import_descriptor(macro_path, crate_, &import_name, cfg_attrs, span); -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 suspending = options.suspending; + 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 = 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()), }; - let foreign_name = format!("{crate_}.{import_name}"); + if output_abi_override.is_some() && output_ty.is_none() { + return Err(Error::new(span, "`return_abi` requires a return value")); + } - let mut self_ty = None; + let impl_generic_params = Self::impl_generic_params(&binding, &mut sig.generics); - let input_tys = sig + Ok(Self { + inputs, + output_ty, + output_abi_override, + impl_generic_params, + binding, + suspending, + }) + } + + 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() - .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() { - 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(()) @@ -242,16 +287,20 @@ impl<'a> State<'a> { })?; } - if let Some(r#type) = r#type { - Ok(r#type) - } else { - Ok(*ty.clone()) - } + let uses_abi_override = abi_override.is_some(); + let abi_type = abi_override.unwrap_or_else(|| *ty.clone()); + + Ok(InputArg::new( + index, + abi_type, + ident.clone(), + uses_abi_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 +312,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`", @@ -271,140 +320,280 @@ 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 }) + let js_value = hygiene.js_value(cfg_attrs, span); + Ok(InputArg::new( + index, + parse_quote! { #and_token #js_value }, + (*self_token).into(), + true, + )) } else { Err(Error::new_spanned(arg, "unsupported arguments found")) } }) .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, + span: Span, + ) -> Result { + let FunctionOptions { + js_name, + static_of, + variadic, + binding, + return_abi: _, + suspending: _, + } = options; + + let binding = match binding { + BindingKind::Import => return Ok(ForeignItem::Import), + BindingKind::Embed(embed) => return Ok(ForeignItem::Embed(embed)), + binding => binding, + }; - None + let argument_count = sig.inputs.len() - usize::from(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", + )); + } + 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 matches!( + &binding, + BindingKind::IndexingGetter + | BindingKind::IndexingSetter + | BindingKind::IndexingDeleter + ) && self_ty.is_none() + { + return Err(Error::new( + span, + "indexing operations require a `self` parameter", + )); + } + + match binding { + BindingKind::Constructor(owner) => { + // `constructor` applies to this foreign function. Its return type + // 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)); - OutputType::Generate { js_name, member } + Ok(ForeignItem::constructor(owner, namespace, &name, variadic)) } - FunctionJsOutput::Embed(embed) => OutputType::Embed(embed), - FunctionJsOutput::Import => OutputType::Import, - }; + 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 output_ty = match &sig.output { - ReturnType::Default => Vec::new(), - ReturnType::Type(_, ty) => vec![*ty.clone()], - }; + Ok(ForeignItem::indexing_getter( + self_ty.expect("validated above"), + )) + } + BindingKind::IndexingSetter => { + if argument_count != 2 { + return Err(Error::new(span, "`indexing_setter` requires two arguments")); + } - 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!() + Ok(ForeignItem::indexing_setter( + self_ty.expect("validated above"), + )) + } + BindingKind::IndexingDeleter => { + if argument_count != 1 { + return Err(Error::new(span, "`indexing_deleter` requires one argument")); } - }) - .collect(); - let impl_generic_params = Self::impl_generic_params(&r#type, &mut sig.generics); + Ok(ForeignItem::indexing_deleter( + self_ty.expect("validated above"), + )) + } + 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, 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) { + return Err(Error::new( + span, + "`setter` requires one argument and no return value", + )); + } + 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, object, receiver) = Self::member_target(static_of, self_ty, js_names); + + Ok(ForeignItem::call( + owner, + namespace, + object.as_deref(), + &name, + receiver, + variadic, + )) + } + BindingKind::Embed(_) | BindingKind::Import => { + unreachable!("external bindings returned above") + } + } + } - 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)) + fn member_target( + static_of: Option, + self_ty: Option, + js_names: &HashMap, + ) -> (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); + + (Some(owner), Some(type_name), false) + } else if let Some(owner) = self_ty { + (Some(owner), None, true) } else { - None + (None, None, false) + } + } + + fn constructor_output(output: &ReturnType) -> Result<&Type> { + let ReturnType::Type(_, output) = output else { + return Err(Error::new_spanned( + output, + "`constructor` requires a return type", + )); }; - let input = hygiene.input(outer_attrs, span); - let output = hygiene.output(outer_attrs, span); - Ok(Self { - crate_, - namespace, - js_bindgen, - r#macro, - input, - output, - import_name, - foreign_name, - input_tys, - output_ty, - extern_input_names, - intern_input_names, - impl_generic_params, - r#type, - span, - }) + Ok(output) + } + + 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 or an explicit owner", + )); + }; + + 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") + .ident + .to_string(); + + if owner.leading_colon.is_none() && owner.segments.len() == 1 { + js_names.get(&rust_name).cloned().unwrap_or(rust_name) + } else { + rust_name + } } // 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() { - 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 { - 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; - } - } - _ => (), - } - } - } + fn impl_generic_params(binding: &ForeignItem, generics: &mut Generics) -> TokenStream { + if let Some(owner) = binding.owner() { + 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() @@ -419,270 +608,148 @@ impl<'a> State<'a> { } } - fn wat(&self) -> Stmt { - let Self { - crate_, - js_bindgen, - r#macro, - input, - import_name, - foreign_name, - input_tys, - output_ty, - intern_input_names, - span, - .. - } = self; - - let mut input_imports = Vec::new(); + 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)] + }; - for ty in input_tys { - if !input_imports.contains(&ty) { - input_imports.push(ty); + (GenericParam::Lifetime(param), predicates) } - } - - 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(); + 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); + } + } - 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#"{}","#); - } + 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)] + }; - 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)*), + (GenericParam::Type(param), predicates) } - }; + GenericParam::Const(mut param) => { + param.eq_token = None; + param.default = None; - 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),)* + (GenericParam::Const(param), Vec::new()) } } } - fn js(&mut self) -> Option { - let Self { - crate_, - js_bindgen, - r#macro, - import_name, - input_tys, - output_ty, - intern_input_names, - r#type, - span, - .. - } = self; - - let js_path = match r#type { - OutputType::Generate { js_name, member } => { - let base = if member.is_some() { - "self" - } 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}") + 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); } - } else { - format!("{base}.{import_name}") + 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(_) => (), } - OutputType::Embed(name) => { - format!("this.#jsEmbed.{crate_}['{name}']") - } - OutputType::Import => return None, - }; - - let mut unique_inputs = Vec::new(); - - for ty in input_tys.iter() { - if !unique_inputs.contains(&ty) { - unique_inputs.push(ty); - } - } - - let mut required_embeds = Vec::new(); - - if let OutputType::Embed(name) = &r#type { - 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>())); - } - - for ty in output_ty.iter() { - required_embeds.push(quote_spanned!(*span=> #r#macro::js_output_embed::<#ty>())); + index += 1; } - - let required_embeds = if required_embeds.is_empty() { - [].as_slice() - } else { - &[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 call_input_names_joined = if r#type.member().is_some() { - Cow::Owned(intern_input_names.iter().skip(1).join(", ")) - } else { - Cow::Borrowed(&input_names_joined) - }; - let input_conv = intern_input_names.iter().map(ToString::to_string); - - let direct_fn_open = if r#type.member().is_none() { - String::new() - } else { - format!("({input_names_joined}) => ") - }; - 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}")), - } - } else { - Cow::Borrowed(&js_path) - }; - 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), - } - } 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),*), - } - }; - - 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 - } - }) } -} -impl OutputType { - fn member(&self) -> Option<&Member> { - if let Self::Generate { member, .. } = self { - member.as_ref() - } else { - None + fn into_import_descriptor( + self, + macro_path: Path, + crate_: &str, + import_name: &str, + cfg_attrs: Vec, + span: Span, + ) -> FunctionImport { + let Self { + inputs, + output_ty, + output_abi_override, + binding, + suspending, + .. + } = self; + let output_type = output_abi_override.or(output_ty); + + let binding = match binding { + ForeignItem::Generate { binding, .. } => Some(binding), + ForeignItem::Embed(name) => Some(FunctionBinding::CallEmbed { + module: crate_.to_owned(), + name, + }), + ForeignItem::Import => None, + }; + FunctionImport { + cfg_attrs, + module: LitStr::new(crate_, span), + name: LitStr::new(import_name, span), + inputs: inputs + .into_iter() + .map(|input| FunctionImportInput { ty: input.abi_type }) + .collect(), + output_type, + binding, + suspending, + macro_path, } } } 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..5fc3e868 --- /dev/null +++ b/host/js-sys-bindgen/src/function/js.rs @@ -0,0 +1,294 @@ +use std::fmt::Write; + +use proc_macro2::{Span, TokenStream}; +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 { + 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 { + Generate { + /// Rust type receiving the generated method, if this is not a free + /// function. + owner: Option, + binding: FunctionBinding, + }, + 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, + namespace: Option<&str>, + object: Option<&str>, + name: &str, + receiver: bool, + variadic: bool, + ) -> Self { + let binding = if receiver { + FunctionBinding::CallMethod { + name: name.to_owned(), + variadic, + } + } else { + FunctionBinding::CallGlobal { + target: FunctionGlobalPath::new(namespace, object, name), + variadic, + } + }; + + Self::Generate { owner, binding } + } + + pub(super) fn constructor( + owner: Path, + namespace: Option<&str>, + name: &str, + variadic: bool, + ) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::Construct { + target: FunctionGlobalPath::new(namespace, None, name), + variadic, + }, + } + } + + pub(super) fn getter( + owner: Option, + namespace: Option<&str>, + object: Option<&str>, + name: &str, + receiver: bool, + ) -> Self { + let binding = if receiver { + FunctionBinding::GetMember(name.to_owned()) + } else { + FunctionBinding::GetGlobal(FunctionGlobalPath::new(namespace, object, name)) + }; + + Self::Generate { owner, binding } + } + + 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::Generate { owner, binding } + } + + pub(super) fn indexing_getter(owner: Path) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::IndexGet, + } + } + + pub(super) fn indexing_setter(owner: Path) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::IndexSet, + } + } + + pub(super) fn indexing_deleter(owner: Path) -> Self { + Self::Generate { + owner: Some(owner), + binding: FunctionBinding::IndexDelete, + } + } + + 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 new file mode 100644 index 00000000..57bd14d0 --- /dev/null +++ b/host/js-sys-bindgen/src/function/options.rs @@ -0,0 +1,292 @@ +use syn::{Attribute, Error, Ident, LitStr, Path, Result, Type}; + +/// Options read from `#[js_sys(...)]` on a foreign function declaration. +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, + /// 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(ConstructorOwner), + Getter(String), + Setter(String), + IndexingGetter, + IndexingSetter, + IndexingDeleter, + Embed(String), + Import, +} + +pub(super) enum ConstructorOwner { + Infer, + Explicit(Path), +} + +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: Option, + 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 = RawFunctionOptions::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") { + 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() + } 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("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(); + + 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 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 { + Err(meta.error("unsupported attribute")) + } + })?; + } + + options.validate(rust_name)?; + 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.is_some()) + + usize::from(self.getter.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 + || 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 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, + "JavaScript operations are mutually exclusive", + )); + } + if self.constructor.is_some() && self.static_of.is_some() { + return Err(Error::new_spanned( + rust_name, + "`constructor` cannot be combined with `static_of`", + )); + } + if (has_property || has_indexing) && self.js_name.is_some() { + return Err(Error::new_spanned( + rust_name, + "`js_name` cannot be combined with a property operation", + )); + } + if (has_property || has_indexing) && self.variadic { + return Err(Error::new_spanned( + rust_name, + "`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 let Some(owner) = constructor { + BindingKind::Constructor(owner) + } 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<()> { + if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) { + 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 45038edc..71fb5b54 100644 --- a/host/js-sys-bindgen/src/hygiene.rs +++ b/host/js-sys-bindgen/src/hygiene.rs @@ -1,201 +1,140 @@ -use std::borrow::Cow; - +#[cfg(feature = "web-idl")] use foldhash::fast::FixedState; +#[cfg(feature = "web-idl")] 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(feature = "web-idl")] +use syn::{ItemUse, parse_quote}; + +pub(crate) enum Hygiene<'a> { + /// Source generation mode: emit short paths and record the required `use` + /// items. + #[cfg(feature = "web-idl")] 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(feature = "web-idl"), + 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), - } - } - - 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), - } + self.js_sys_item(attrs, &parse_quote_spanned!(span=> JsValue), 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 js_cast(&mut self, attrs: &[Attribute], span: Span) -> Path { + self.hazard_item(attrs, &parse_quote_spanned!(span=> JsCast), 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_into(&mut self, attrs: &[Attribute], span: Span) -> Path { + self.hazard_item(attrs, &parse_quote_spanned!(span=> IntoJS), 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) - } - } + pub(crate) fn r#macro(&mut self, attrs: &[Attribute], span: Span) -> Path { + self.js_sys_item(attrs, &parse_quote_spanned!(span=> wire), span) } - pub(crate) fn output(&mut self, attrs: &[Attribute], span: Span) -> Path { + fn js_sys_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> Output)); - parse_quote_spanned!(span=> Output) + imports.js_sys_push(attrs, ident.clone()); + parse_quote_spanned!(span=> #ident) } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::Output), span) + Hygiene::Qualified { js_sys } => { + Self::with_js_sys(*js_sys, &ident.to_token_stream(), span) } } } - pub(crate) fn output_wat_conv(&mut self, attrs: &[Attribute], span: Span) -> Path { + fn hazard_item(&mut self, attrs: &[Attribute], ident: &Ident, span: Span) -> Path { match self { + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> OutputWatConv)); - parse_quote_spanned!(span=> OutputWatConv) + imports.hazard_push(attrs, ident.clone()); + parse_quote_spanned!(span=> #ident) } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::OutputWatConv), span) + Hygiene::Qualified { js_sys } => { + Self::with_js_sys(*js_sys, "e!(hazard::#ident), span) } } } - pub(crate) fn output_js_conv(&mut self, attrs: &[Attribute], span: Span) -> Path { + pub(crate) fn as_ref(&mut self, span: Span) -> Path { match self { - Hygiene::Imports(imports) => { - imports.hazard_push(attrs, parse_quote_spanned!(span=> OutputJsConv)); - parse_quote_spanned!(span=> OutputJsConv) + #[cfg(feature = "web-idl")] + Hygiene::Imports(_) => { + parse_quote_spanned!(span=> AsRef) } - Hygiene::Hygiene { js_sys } => { - Self::with_js_sys(*js_sys, "e!(hazard::OutputJsConv), span) + Hygiene::Qualified { .. } => { + parse_quote_spanned!(span=> ::core::convert::AsRef) } } } - pub(crate) fn r#macro(&mut self, attrs: &[Attribute], span: Span) -> Path { + pub(crate) fn deref(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { + #[cfg(feature = "web-idl")] Hygiene::Imports(imports) => { - imports.js_sys_push(attrs, parse_quote_spanned!(span=> r#macro)); - parse_quote_spanned!(span=> r#macro) + imports.deref.insert(attrs.to_vec()); + parse_quote_spanned!(span=> Deref) } - 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 { - Hygiene::Imports(_) => { - parse_quote_spanned!(span=> AsRef) - } - Hygiene::Hygiene { .. } => { - parse_quote_spanned!(span=> ::core::convert::AsRef) + Hygiene::Qualified { .. } => { + parse_quote_spanned!(span=> ::core::ops::Deref) } } } pub(crate) fn phantom_data(&mut self, attrs: &[Attribute], span: Span) -> Path { match self { + #[cfg(feature = "web-idl")] 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) } } } - 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 { + #[cfg(feature = "web-idl")] Hygiene::Imports(_) => { parse_quote_spanned!(span=> From) } - Hygiene::Hygiene { .. } => { + Hygiene::Qualified { .. } => { parse_quote_spanned!(span=> ::core::convert::From) } } } - 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)), - 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(feature = "web-idl")] type FixedHashMap = HashMap; +#[cfg(feature = "web-idl")] type FixedHashSet = HashSet; -pub struct ImportManager { +#[cfg(feature = "web-idl")] +pub(crate) struct ImportManager { js_sys: Path, deref: FixedHashSet>, phantom_data: FixedHashSet>, @@ -203,9 +142,10 @@ pub struct ImportManager { hazard_imports: FixedHashMap, FixedHashSet>, } +#[cfg(feature = "web-idl")] 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(), @@ -215,7 +155,7 @@ impl ImportManager { } } - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.phantom_data .iter() .map(|attr| { @@ -267,6 +207,7 @@ impl ImportManager { } } +#[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 2a513783..76040b24 100644 --- a/host/js-sys-bindgen/src/lib.rs +++ b/host/js-sys-bindgen/src/lib.rs @@ -1,8 +1,7 @@ -#[cfg(feature = "file")] -mod file; +mod closure; +mod export; mod function; mod hygiene; -#[cfg(feature = "macro")] mod r#macro; #[cfg(test)] mod tests; @@ -10,16 +9,9 @@ mod r#type; #[cfg(feature = "web-idl")] mod web_idl; -pub use proc_macro2; -pub use quote; pub use syn; -#[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::closure::closure; 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 bdcb5821..16db9868 100644 --- a/host/js-sys-bindgen/src/macro.rs +++ b/host/js-sys-bindgen/src/macro.rs @@ -1,57 +1,90 @@ +use std::collections::{HashMap, VecDeque}; use std::env; use proc_macro2::TokenStream; use quote::ToTokens; +#[cfg(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; +use crate::r#type::{Type, TypeOptions}; -pub fn r#macro( +pub fn r#macro(attr: TokenStream, item: TokenStream) -> Result { + match syn::parse2(item).map_err(Error::into_compile_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) + } + item => Err( + Error::new_spanned(item, "expected an extern block or function").into_compile_error(), + ), + } +} + +#[cfg(test)] +pub(crate) fn expand_for_test( attr: TokenStream, - 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 - } - }) + foreign_mod: ItemForeignMod, + crate_name: &str, +) -> Result, Error)> { + expand_proc_macro(attr, foreign_mod, crate_name) } -pub(crate) fn internal( +fn expand_proc_macro( 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, +) -> Result, Error)> { + let (js_sys, namespace, error) = parse_block_options(attr); + 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) -> (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() { - Err(meta.error("`js_sys` attribute only allowed with proc-macro hygiene")) - } else if js_sys.is_some() { + // The block-level `js_sys` option selects the crate path used by + // every generated item in this foreign module. + if js_sys.is_some() { Err(meta.error("duplicate attribute")) } else { js_sys = Some(meta.value()?.parse()?); 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 { @@ -67,14 +100,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")) @@ -85,13 +120,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, @@ -99,71 +137,45 @@ pub(crate) fn internal( )); } - for item in foreign_mod.items { - match item { - ForeignItem::Fn(mut item) => { - let mut js_output = FunctionJsOutput::default(); + 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()); - 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); - } + 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); + } + } - 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()), + for item in foreign_mod.items { + match item { + ForeignItem::Fn(item) => { + match expand(&mut hygiene, namespace, crate_name, &js_names, item) { + Ok((function, import)) => { + output.push(&function); + function_imports.push(import); + } 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( @@ -174,6 +186,8 @@ pub(crate) fn internal( } } + output.extend(render_import_groups(function_imports)); + if let Some(error) = error.resolve() { Err((Some(output), error)) } else { @@ -181,6 +195,151 @@ 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(test)] + pub(crate) fn into_items(self) -> Result, Error> { + Ok(syn::parse2::(self.0)?.items) + } +} + +struct ImportGroup { + cfg_attrs: Vec, + imports: Vec, + 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.imports.push(import); + } else { + groups.push(ImportGroup { + cfg_attrs: import.cfg_attrs.clone(), + macro_path: import.macro_path.clone(), + imports: vec![import], + }); + } + } + + groups + .into_iter() + .fold(TokenStream::new(), |mut output, group| { + let ImportGroup { + cfg_attrs, + imports, + macro_path, + } = group; + let mut input_types = Vec::::new(); + let mut output_types = Vec::::new(); + for import in &imports { + for input in &import.inputs { + if !input_types.contains(&input.ty) { + input_types.push(input.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 suspending = import.suspending; + let wire_inputs = import.inputs.iter().map(|input| { + let index = input_types + .iter() + .position(|candidate| candidate == &input.ty) + .expect("every input type was collected"); + + quote::quote!(#macro_path::WireImportInput::new(#index)) + }); + 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 binding = if let Some(binding) = &import.binding { + 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! { + #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 _: () = { + 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(), + ); + const _WIRE: #macro_path::Wire = + #macro_path::Wire::imports(TABLE, &[#(#wire_descriptors),*]); + const _LEN: ::core::primitive::usize = + #macro_path::wire_blob_len(&_WIRE); + + #[unsafe(link_section = "js_bindgen.wire")] + static _WIRE_SECTION: #macro_path::WireBlob<_LEN> = + #macro_path::WireBlob::new(&_WIRE); + }; + }); + 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..08003639 --- /dev/null +++ b/host/js-sys-bindgen/src/tests/closure.rs @@ -0,0 +1,54 @@ +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 factory_correlations_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", + "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 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/export.rs b/host/js-sys-bindgen/src/tests/macro/export.rs new file mode 100644 index 00000000..70be4a12 --- /dev/null +++ b/host/js-sys-bindgen/src/tests/macro/export.rs @@ -0,0 +1,27 @@ +use proc_macro2::TokenStream; +use quote::quote; + +#[test] +fn invalid_export_options() { + let function: syn::ItemFn = syn::parse_quote! { + fn answer() -> i32 { + 42 + } + }; + 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"); + + let borrowed: syn::ItemFn = syn::parse_quote! { + fn echo(value: &JsString) -> &JsString { + value + } + }; + let error = + crate::export::r#macro(TokenStream::new(), &borrowed, Some("test_crate")).unwrap_err(); + assert_eq!(error.to_string(), "cannot return a borrowed reference"); +} diff --git a/host/js-sys-bindgen/src/tests/macro/function.rs b/host/js-sys-bindgen/src/tests/macro/function.rs index cc52a3bb..696401fb 100644 --- a/host/js-sys-bindgen/src/tests/macro/function.rs +++ b/host/js-sys-bindgen/src/tests/macro/function.rs @@ -1,526 +1,322 @@ +use std::collections::HashMap; + +use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; +use syn::{GenericParam, Item}; + +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 basic() { - test!( - {}, - { - extern "js-sys" { - pub fn log(data: &JsValue); - } +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! { + #[cfg(all())] + #[js_sys(js_name = "warn", return_abi = JsValue)] + pub fn log(value: &JsValue) -> JsTest; }, - { - 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), - } - - ::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), - ), - } - - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); - } + ); - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; - } + 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 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); + + let (_, imported) = expand_function( + None, + None, + syn::parse_quote! { + #[js_sys(js_import)] + pub fn imported(); }, - "(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 - call $js_sys.externref.get (@reloc) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", ); + assert!(imported.binding.is_none()); } #[test] -fn namespace() { - test!( - { namespace = "console" }, - { - extern "js-sys" { - pub fn log(data: &JsValue); - } - }, - { - 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), - } - - ::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), - ), - } - - unsafe extern "C" { - #[link_name = "test_crate.console.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); - } - - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; - } +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>>; }, - "(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 - call $js_sys.externref.get (@reloc) - call $test_crate.import.console.log (@reloc) - )", - "globalThis.console.log", ); + 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 js_sys() { - test!( - { js_sys = js_sys }, - { - extern "js-sys" { - pub fn log(data: &JsValue); - } - }, - { - 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), - } - - 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), - ), - } - - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(data: <&JsValue as js_sys::hazard::Input>::Type); - } - - unsafe { log(js_sys::hazard::Input::into_raw(data)) }; - } +fn qualified_owner_generics() { + let (output, _) = expand_function( + None, + None, + syn::parse_quote! { + #[js_sys(constructor = Owner)] + pub fn new() -> Option>; }, - "(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 - call $js_sys.externref.get (@reloc) - call $test_crate.import.log (@reloc) - )", - "globalThis.log", ); + 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 two_parameters() { - test!( - {}, - { - extern "js-sys" { - pub fn log(data1: &JsValue, data2: &JsValue); - } +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; }, - { - 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), - } - - ::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), - ), - } + ) + .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); +} - 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, - ); - } +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() +} - unsafe { - log( - ::js_sys::hazard::Input::into_raw(data1), - ::js_sys::hazard::Input::into_raw(data2), - ) - }; - } - }, - "(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))) - (func $test_crate.log (@sym) (param $data1 i32) (param $data2 i32) - local.get $data1 - call $js_sys.externref.get (@reloc) - local.get $data2 - call $js_sys.externref.get (@reloc) - call $test_crate.import.log (@reloc) - )", - "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(TokenStream::new(), input, "test_crate").unwrap_err() + else { + panic!("expected the successful function to be preserved"); + }; + + 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 empty() { - 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 "js-sys" { + extern "C" { pub fn log(); } }, - { - 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)", ")", - } - - ::js_sys::js_bindgen::import_js!( - module = "test_crate", - name = "log", - "globalThis.log" - ); - - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(); - } - - unsafe { log() }; - } - }, - "(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", + "expected `js-sys` ABI" ); -} - -#[test] -fn js_name() { - test!( - {}, + assert_error!( { extern "js-sys" { - #[js_sys(js_name = "log")] - pub fn logx(data: &JsValue); - } - }, - { - 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), - } - - ::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), - ), - } - - unsafe extern "C" { - #[link_name = "test_crate.logx"] - fn logx(data: <&JsValue as ::js_sys::hazard::Input>::Type); - } - - unsafe { logx(::js_sys::hazard::Input::into_raw(data)) }; + #[js_sys(js_name = "renamed", js_import)] + pub fn log(); } }, - "(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 - call $js_sys.externref.get (@reloc) - call $test_crate.import.logx (@reloc) - )", - "globalThis.log", + "`js_import` and `js_embed` cannot be combined with JavaScript binding options" ); -} - -#[test] -fn js_import() { - test!( - {}, + assert_error!( { extern "js-sys" { - #[js_sys(js_import)] - pub fn log(data: &JsValue); + #[js_sys(js_import, suspending)] + pub fn wait(); } }, - { - 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), - } - - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); - } - - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; - } - }, - "(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 - call $js_sys.externref.get (@reloc) - call $test_crate.import.log (@reloc) - )", - None, + "`suspending` cannot be combined with `js_import`; provide a `WebAssembly.Suspending` \ + import directly" ); -} - -#[test] -fn js_embed() { - test!( - {}, + assert_error!( { extern "js-sys" { - #[js_sys(js_embed = "embed")] - pub fn log(data: &JsValue); + #[js_sys(suspending, suspending)] + pub fn wait(); } }, - { - 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), - } - - ::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_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), - ), - } - - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(data: <&JsValue as ::js_sys::hazard::Input>::Type); - } - - unsafe { log(::js_sys::hazard::Input::into_raw(data)) }; - } - }, - "(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 - call $js_sys.externref.get (@reloc) - call $test_crate.import.log (@reloc) - )", - "this.#jsEmbed.test_crate['embed']", + "duplicate attribute" ); -} - -#[test] -fn r#return() { - test!( - {}, + assert_error!( { extern "js-sys" { - pub fn is_nan() -> JsValue; + pub fn log( + #[js_sys(type = i32)] + #[js_sys(type = u32)] + value: i32, + ); } }, + "duplicate attribute" + ); + assert_error!( { - 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), - } - - ::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, - ), - } - - unsafe extern "C" { - #[link_name = "test_crate.is_nan"] - fn is_nan() -> ::Type; - } - - ::js_sys::hazard::Output::from_raw(unsafe { is_nan() }) + extern "js-sys" { + #[js_sys(return_abi = JsValue)] + pub fn value(); } }, - "(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) - call $test_crate.import.is_nan (@reloc) - call $js_sys.externref.insert (@reloc) - )", - "globalThis.is_nan", + "`return_abi` requires a return value" ); -} - -#[test] -fn cfg() { - test!( - {}, + assert_error!( { extern "js-sys" { - #[cfg(all())] - pub fn log(); + #[js_sys(return_abi = JsValue, return_abi = JsTest)] + pub fn value() -> JsValue; } }, + "duplicate attribute" + ); + assert_error!( { - #[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)", ")", - } - - ::js_sys::js_bindgen::import_js!( - module = "test_crate", - name = "log", - "globalThis.log" - ); - - unsafe extern "C" { - #[link_name = "test_crate.log"] - fn log(); - } - - unsafe { log() }; + extern "js-sys" { + #[js_sys(constructor = JsTest)] + pub fn new(); } }, - "(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", + "`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 2233224e..9a375608 100644 --- a/host/js-sys-bindgen/src/tests/macro/member.rs +++ b/host/js-sys-bindgen/src/tests/macro/member.rs @@ -1,308 +1,87 @@ -#[test] -fn method() { - test!( - {}, - { - extern "js-sys" { - pub fn test(self: &JsTest); - } - }, - { - 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), - } - - ::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), - ), - } +use std::collections::HashMap; - unsafe extern "C" { - #[link_name = "test_crate.test"] - fn test(this: <&::js_sys::JsValue as ::js_sys::hazard::Input>::Type); - } - - unsafe { test(::js_sys::hazard::Input::into_raw(self)) }; - } - } - }, - "(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))) - (func $test_crate.test (@sym) (param $self i32) - local.get $self - call $js_sys.externref.get (@reloc) - call $test_crate.import.test (@reloc) - )", - "(self) => self.test()", - ); -} +use proc_macro2::Span; #[test] -fn method_par() { - test!( - {}, - { - extern "js-sys" { - pub fn test(self: &JsTest, par1: &JsValue, par2: &JsValue); - } +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; }, - { - 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), - } - - ::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_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), - ), - } + ) + .unwrap(); - 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, - ); - } - - unsafe { - test( - ::js_sys::hazard::Input::into_raw(self), - ::js_sys::hazard::Input::into_raw(par1), - ::js_sys::hazard::Input::into_raw(par2), - ) - }; - } - } - }, - "(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))) - (func $test_crate.test (@sym) (param $self i32) (param $par1 i32) (param $par2 i32) - local.get $self - call $js_sys.externref.get (@reloc) - local.get $par1 - call $js_sys.externref.get (@reloc) - local.get $par2 - call $js_sys.externref.get (@reloc) - call $test_crate.import.test (@reloc) - )", - "(self, par1, par2) => self.test(par1, par2)", - ); + 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] -fn getter() { - test!( - {}, - { - extern "js-sys" { - #[js_sys(property)] - pub fn test(self: &JsTest) -> JsValue; - } - }, - { - 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), - } - - ::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::(), - ], - "{}{}{}", - 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, - ), - } +fn owner_import_names() { + fn import_name(owner: &syn::Path) -> String { + let function: syn::ForeignItemFn = syn::parse_quote! { + pub fn value(self: &#owner) -> i32; + }; - unsafe extern "C" { - #[link_name = "test_crate.test"] - fn test( - this: <&::js_sys::JsValue as ::js_sys::hazard::Input>::Type, - ) -> ::Type; - } + 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() + } - ::js_sys::hazard::Output::from_raw(unsafe { - test(::js_sys::hazard::Input::into_raw(self)) - }) - } - } - }, - "(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))) - (func $test_crate.test (@sym) (param ) (param $self i32) (result i32) - local.get $self - call $js_sys.externref.get (@reloc) - call $test_crate.import.test (@reloc) - call $js_sys.externref.insert (@reloc) - )", - "(self) => self.test", + 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 setter() { - test!( - {}, - { - extern "js-sys" { - #[js_sys(property)] - pub fn test(self: &JsTest, value: &JsValue); - } - }, - { - 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), - } - - ::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>(), - ], - "{}{}{}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), - ), - } - - 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, - ); - } +fn invalid_member_options() { + let variadic = syn::parse_quote! { + extern "js-sys" { + #[js_sys(variadic)] + pub fn call(); + } + }; + assert_eq!( + super::macro_error(variadic), + "`variadic` requires at least one argument" + ); - unsafe { - test( - ::js_sys::hazard::Input::into_raw(self), - ::js_sys::hazard::Input::into_raw(value), - ) - }; - } - } - }, - "(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))) - (func $test_crate.test (@sym) (param $self i32) (param $value i32) - local.get $self - call $js_sys.externref.get (@reloc) - local.get $value - call $js_sys.externref.get (@reloc) - call $test_crate.import.test (@reloc) - )", - "(self, value) => self.test = value", + let setter = syn::parse_quote! { + extern "js-sys" { + #[js_sys(setter)] + pub fn update(self: &JsTest, value: i32); + } + }; + assert_eq!( + super::macro_error(setter), + "`setter` cannot infer a field name; use `setter = \"field\"`" ); } diff --git a/host/js-sys-bindgen/src/tests/macro/mod.rs b/host/js-sys-bindgen/src/tests/macro/mod.rs index ad739296..b5349369 100644 --- a/host/js-sys-bindgen/src/tests/macro/mod.rs +++ b/host/js-sys-bindgen/src/tests/macro/mod.rs @@ -1,247 +1,14 @@ -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::{JsBindgenJsSectionParser, JsBindgenWatSectionParser}; use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::parse_quote; -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::internal(attr.clone(), foreign_mod, Some("test_crate"), None).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; mod r#type; -fn inner(tmp: &Path, source: &str) -> Result<(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::internal( - TokenStream::new(), - parse_quote! { extern "js-sys" { pub type JsTest; } }, - Some("test_crate"), - None, - ) - .unwrap(); - let js_test: TokenStream = js_test.into_iter().fold(TokenStream::new(), |mut acc, x| { - x.to_tokens(&mut acc); - acc - }); - - 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} - - {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; - - 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() == "js_bindgen.wat" => { - 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() == "js_bindgen.import" => { - 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()); - } - _ => (), - } - } - - Ok(()) - })??; - } - } - } +fn macro_error(input: syn::ItemForeignMod) -> String { + let (_, error) = r#macro::expand_for_test(TokenStream::new(), input, "test_crate").unwrap_err(); - Ok((wat_output, js_import_output)) + error.to_string() } diff --git a/host/js-sys-bindgen/src/tests/macro/type.rs b/host/js-sys-bindgen/src/tests/macro/type.rs index 1d8bc531..11759ac9 100644 --- a/host/js-sys-bindgen/src/tests/macro/type.rs +++ b/host/js-sys-bindgen/src/tests/macro/type.rs @@ -1,265 +1,198 @@ -#[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::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; - - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; - - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.0) - } - } - - 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; - - fn from_raw(raw: Self::Type) -> Self { - Self(::js_sys::hazard::Output::from_raw(raw)) - } - } - }, - None, - None, - ); +use proc_macro2::TokenStream; +use syn::{Attribute, Fields, GenericArgument, PathArguments, Type}; + +fn expand(input: syn::ItemForeignMod) -> Vec { + crate::r#macro::expand_for_test(TokenStream::new(), input, "test_crate") + .unwrap() + .into_items() + .unwrap() } #[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::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; - - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; - - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.value) - } - } - - 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; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: ::js_sys::hazard::Output::from_raw(raw), - _type: ::core::marker::PhantomData, - } - } - } - }, - None, - None, - ); +fn generic_options_and_extends() { + let output = expand(syn::parse_quote! { + extern "js-sys" { + #[js_sys(extends = JsTest)] + pub type Child; + } + }); + 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") + })); } #[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::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; - - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; - - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.value) - } - } - - 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; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: ::js_sys::hazard::Output::from_raw(raw), - _type: ::core::marker::PhantomData, - } - } - } - }, - None, - None, +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 r#trait() { - test!( - {}, - { - extern "js-sys" { - pub type JsString; - } +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")) + )] }, - { - #[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::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; - - type Type = <&'static ::js_sys::JsValue as ::js_sys::hazard::Input>::Type; - - fn into_raw(self) -> Self::Type { - ::js_sys::hazard::Input::into_raw(&self.value) - } - } + ]; + 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); + } +} - unsafe impl ::js_sys::hazard::JsCast for JsString {} +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, + } +} - 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; +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) +} - type Type = <::js_sys::JsValue as ::js_sys::hazard::Output>::Type; +#[test] +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 First; + pub type Duplicate; + pub type Duplicate; + } + }); + + 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"); + + 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")); +} - fn from_raw(raw: Self::Type) -> Self { - Self { - value: ::js_sys::hazard::Output::from_raw(raw), - _type: ::core::marker::PhantomData, - } - } - } - }, - None, - None, +#[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/tests/mod.rs b/host/js-sys-bindgen/src/tests/mod.rs index a9150b6d..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; @@ -7,8 +8,7 @@ macro_rules! test { }; } -#[cfg(feature = "macro")] +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 fe1eef38..00000000 --- a/host/js-sys-bindgen/src/tests/type.rs +++ /dev/null @@ -1,138 +0,0 @@ -use syn::parse_quote; - -use crate::{Hygiene, ImportManager, 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::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; - - #[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 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) - } - } - - 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; - - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) - } - } - }, - ); -} - -#[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::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; - - #[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 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.value) - } - } - - 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; - - fn from_raw(raw: Self::Type) -> Self { - Self { - value: Output::from_raw(raw), - _type: PhantomData, - } - } - } - }, - ); -} diff --git a/host/js-sys-bindgen/src/tests/web_idl.rs b/host/js-sys-bindgen/src/tests/web_idl.rs index e637548c..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::{Input, InputWatConv, InputJsConv, OutputJsConv, Output, JsCast, OutputWatConv}; + use js_sys::hazard::{IntoJS, JsCast}; #[repr(transparent)] struct Test(JsValue); @@ -25,29 +25,13 @@ 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; - - type Type = <&'static JsValue as Input>::Type; - - fn into_raw(self) -> Self::Type { - Input::into_raw(&self.0) - } - } - 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 IntoJS for Test { + type Abi = ::Abi; - fn from_raw(raw: Self::Type) -> Self { - Self(Output::from_raw(raw)) + fn into_abi(self) -> Self::Abi { + IntoJS::into_abi(JsValue::from(self)) } } }, diff --git a/host/js-sys-bindgen/src/type.rs b/host/js-sys-bindgen/src/type.rs index a39d4f99..f612dd3c 100644 --- a/host/js-sys-bindgen/src/type.rs +++ b/host/js-sys-bindgen/src/type.rs @@ -1,20 +1,75 @@ -use std::array; - use proc_macro2::TokenStream; -use quote::{ToTokens, quote_spanned}; +use quote::{ToTokens, quote, quote_spanned}; +use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{Fields, ForeignItemType, Item, ItemImpl, ItemStruct, Token, parse_quote_spanned}; +use syn::{ + Attribute, Error, Fields, ForeignItemType, Item, ItemImpl, ItemStruct, LitStr, Meta, Path, + Token, parse_quote_spanned, +}; + +use crate::hygiene::Hygiene; -use crate::Hygiene; +pub(crate) struct Type { + r#struct: ItemStruct, + impls: Vec, +} -pub struct Type { - pub r#struct: ItemStruct, - pub impls: [ItemImpl; 5], +#[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); + } + } + + options + } } impl Type { + #[cfg(feature = "web-idl")] + #[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, @@ -25,54 +80,58 @@ 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 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 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); + let marker_types: Vec<_> = generics + .params + .iter() + .filter_map(|param| match param { + syn::GenericParam::Lifetime(param) => { + let lifetime = ¶m.lifetime; + Some(quote_spanned!(span=> #phantom_data<&#lifetime ()>)) + } + syn::GenericParam::Type(param) => { + let ident = ¶m.ident; + Some(quote_spanned!(span=> #phantom_data<#ident>)) + } + syn::GenericParam::Const(_) => None, + }) + .collect(); + let marker_type = match marker_types.as_slice() { + [] => quote!(#phantom_data<()>), + [ty] => quote!(#ty), + types => quote!((#(#types,)*)), + }; ( Fields::Named(parse_quote_spanned! {span=> { value: #js_value, - _type: #phantom_data #gen_type, + _type: #marker_type, } }), None, quote_spanned! {span=>value}, - quote_spanned! {span=> - Self { - value: #output::from_raw(raw), - _type: #phantom_data, - } - }, ) }; - let impls = [ + let mut impls = vec![ parse_quote_spanned! {span=> #(#cfgs)* impl #gen_impl #as_ref<#js_value> for #ident #gen_type #gen_where { @@ -89,41 +148,61 @@ 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) - } - } - }, parse_quote_spanned! {span=> #(#cfgs)* unsafe impl #gen_impl #js_cast for #ident #gen_type #gen_where {} }, 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 #into_js for #ident #gen_type #gen_where { + type Abi = <#js_value as #into_js>::Abi; - fn from_raw(raw: Self::Type) -> Self { - #from_raw + fn into_abi(self) -> Self::Abi { + #into_js::into_abi(#js_value::from(self)) } } }, ]; - item_attrs.append(&mut cfgs); + 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.push(parse_quote_spanned! {span=>#[repr(transparent)]}); let r#struct = ItemStruct { @@ -140,21 +219,54 @@ 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 = array::IntoIter; + type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - let [impl_1, impl_2, impl_3, impl_4, impl_5] = self.impls; - [ - Item::from(self.r#struct), - impl_1.into(), - impl_2.into(), - impl_3.into(), - impl_4.into(), - impl_5.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 e6c12fd9..34f596df 100644 --- a/host/js-sys-macro/src/lib.rs +++ b/host/js-sys-macro/src/lib.rs @@ -1,8 +1,16 @@ +use js_sys_bindgen::syn::Error; use proc_macro::TokenStream; +#[proc_macro] +pub fn closure(input: TokenStream) -> TokenStream { + 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..c6af44c0 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 @@ -266,11 +269,9 @@ impl<'cs> Iterator for CustomSectionParser<'cs> { if let Some(length) = self.data.split_off(..4) { let length = u32::from_le_bytes(length.try_into().unwrap()) as usize; - let data = self.data.split_off(..length).unwrap_or_else(|| { + Some(self.data.split_off(..length).unwrap_or_else(|| { panic!("invalid length encoding in custom section `{}`", self.name) - }); - - Some(data) + })) } else if self.data.is_empty() { None } else { diff --git a/host/ld/Cargo.toml b/host/ld/Cargo.toml index 24a51841..66e8549b 100644 --- a/host/ld/Cargo.toml +++ b/host/ld/Cargo.toml @@ -18,9 +18,11 @@ 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 } +xxhash-rust = { workspace = true } [lints] workspace = true diff --git a/host/ld/src/args.rs b/host/ld/src/args.rs index 217fa7cf..d6746619 100644 --- a/host/ld/src/args.rs +++ b/host/ld/src/args.rs @@ -173,18 +173,22 @@ impl<'args> Arguments<'args> { #[cfg(test)] mod tests { - use std::ffi::OsString; + use std::ffi::OsStr; 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()); - 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 f9254b9f..b66e57e5 100644 --- a/host/ld/src/js.rs +++ b/host/ld/src/js.rs @@ -11,28 +11,181 @@ 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, } +#[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 { + binding: JsWithEmbeds, + kind: JsExportKind, +} + +#[derive(Debug, PartialEq, Eq)] +enum JsExportKind { + 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, + 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 { + binding: JsWithEmbeds { + js, + embeds: embeds + .into_iter() + .map(|(module, name)| JsEmbed { module, name }) + .collect(), + }, + 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.origin(), + definition.origin(), + 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, + name: &str, + js: String, + embeds: impl IntoIterator, + shim: &str, + ) -> Result { + let definition = JsExport { + 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.origin(), + definition.origin(), + 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 - .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()) @@ -60,39 +213,15 @@ impl JsStore { pub fn add_js_imports(&mut self, custom_section: &CustomSectionReader<'_>) -> Result<()> { for import in JsBindgenJsSectionParser::new(custom_section) { - 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(), import.js.to_owned()); - - for embed in import.embeds { - self.require_js_embed(embed.into()); - } - } 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 - ); - } + 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(()) @@ -100,6 +229,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) @@ -108,29 +258,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 - ); } } @@ -145,8 +277,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) @@ -180,6 +313,11 @@ impl JsStore { main_memory, js_import: self.import, js_embed: self.embed, + js_export: self + .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 d805f2ee..2890e94f 100644 --- a/host/ld/src/post.rs +++ b/host/ld/src/post.rs @@ -1,9 +1,11 @@ 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 js_bindgen_wire::WIRE_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,9 +59,24 @@ pub fn processing( import_section.append_to(&mut wasm_output); } + // 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(); + + 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 matches!(c.name(), WAT_SECTION | IMPORT_SECTION | WIRE_SECTION) => {} Payload::CustomSection(c) if c.name() == "js_bindgen.embed" => (), // Register ourselves in the producer section. Payload::CustomSection(c) if c.name() == "producers" => { diff --git a/host/ld/src/pre.rs b/host/ld/src/pre.rs index 7a747eda..c32f9a9b 100644 --- a/host/ld/src/pre.rs +++ b/host/ld/src/pre.rs @@ -3,13 +3,17 @@ 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::JsBindgenWatSectionParser; +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 xxhash_rust::xxh3::xxh3_128; use crate::args::Arguments; use crate::js::JsStore; +use crate::wire::{self, RenderedRecord}; pub struct PreOutput<'args> { pub add_args: Vec, @@ -65,7 +69,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, matches!(arch, Arch::Wasm64), @@ -95,6 +99,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( @@ -105,8 +146,10 @@ 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 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) { let payload = match payload { @@ -119,32 +162,116 @@ 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 = - archive_path.with_added_extension(format!("wasm.{file_counter}.o")); - - // 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)?; - } + let wasm_path = wasm_object_path(wat); + let wasm_bytes = compile_wat(&wasm_path, wasm64, wat, object_mtime)?; + + 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)?)?, + )?; add_args.push(wasm_path.into()); } } + Payload::CustomSection(c) if c.name() == WIRE_SECTION => { + 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( + 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 = wasm_object_path(&wat); + compile_wat(&wasm_path, wasm64, &wat, object_mtime)?; + add_args.push(wasm_path.into()); + } + } + 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()); + } + + 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()); + } + } + } + } + } // 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/ld/src/wire/closure.rs b/host/ld/src/wire/closure.rs new file mode 100644 index 00000000..563eb072 --- /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 new file mode 100644 index 00000000..18246abf --- /dev/null +++ b/host/ld/src/wire/export/js.rs @@ -0,0 +1,219 @@ +use js_bindgen_wire::model::{Export, ExportOutput}; + +use super::input_names; +use crate::wire::JsBinding; +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> { + 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) + } + }; + + JsBinding { + module: export.module, + name: export.name, + js, + embeds: export.embeds.clone(), + } +} + +/// 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, +} + +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(call: &PreparedCall<'_, '_>) -> String { + let invoke = format!("{}({})", call.callable, call.arguments); + if let Some(output) = call.export.output.as_ref() + && !output.is_void() + { + format!( + "({}) => {{\n{} const ret = {invoke}\n{}\n}}", + call.parameters, + call.prepares, + render_output(output, " ") + ) + } else { + format!( + "({}) => {{\n{} {invoke}\n}}", + call.parameters, call.prepares + ) + } +} + +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 call.prepares.is_empty() { + format!( + "(() => {{\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}})()", + call.callable, call.parameters, call.prepares, call.arguments + ) + } +} + +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_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_output_slot(output: &ExportOutput<'_>, slot: usize) -> String { + if let Some(result) = output.result() { + if slot < usize::from(result.discriminant) { + return format!("ret[{slot}]"); + } + } else if output.is_direct() && !output.is_void() { + if slot == 0 { + return "ret".to_owned(); + } + } else { + return format!("ret[{slot}]"); + } + 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/mod.rs b/host/ld/src/wire/export/mod.rs new file mode 100644 index 00000000..a06558e1 --- /dev/null +++ b/host/ld/src/wire/export/mod.rs @@ -0,0 +1,30 @@ +//! Host-side rendering for Rust exports and closure dispatchers. + +mod js; +mod wat; + +use js_bindgen_wire::model::{Export, ExportInput, ExportInputKind}; + +use crate::wire::RenderedGroup; + +pub(super) fn render<'a>(exports: &[Export<'a>]) -> RenderedGroup<'a> { + RenderedGroup { + bindings: exports.iter().map(js::render).collect(), + wat: wat::render(exports), + } +} + +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 new file mode 100644 index 00000000..658fbd43 --- /dev/null +++ b/host/ld/src/wire/export/wat.rs @@ -0,0 +1,471 @@ +use std::fmt::Write; + +use js_bindgen_wire::abi::WatType; +use js_bindgen_wire::model::{Callee, Export, ExportInput, ExportInputKind, ExportOutput, Slot}; + +use super::input_names; +use crate::wire::wat::{WatImports, WatLocals, quoted, write_conversion}; + +struct ExportRenderer<'export, 'wire> { + index: usize, + export: &'export Export<'wire>, +} + +/// 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 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 { 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 identifier = format!("js_sys.export.symbol.{index}"); + imports.insert( + &identifier, + renderer.render_symbol_import(&identifier, name), + ); + } + } + + if has_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 has_indirect { + imports.insert( + "__stack_pointer", + format!( + "(import \"env\" \"__stack_pointer\" (global $__stack_pointer (mut \ + {pointer_type})))", + ), + ); + } + + let mut wat = imports.render(); + + for (index, export) in exports.iter().enumerate() { + let renderer = ExportRenderer { index, export }; + if renderer.is_closure() { + if !wat.is_empty() { + wat.push('\n'); + } + renderer.write_closure_type(&mut wat); + } + } + + for (index, export) in exports.iter().enumerate() { + if !wat.is_empty() { + wat.push('\n'); + } + ExportRenderer { index, export }.write_shim(&mut wat); + } + + Some(wat) +} + +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 render_symbol_import(&self, identifier: &str, symbol: &str) -> String { + 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; + } + wat.push_str(" (param"); + for slot in &input.slots { + write!(wat, " {}", slot.rust).expect("writing to a String cannot fail"); + } + wat.push(')'); + } + 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("))"); + wat + } + + 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; + } + wat.push_str(" (param"); + for slot in &input.slots { + write!(wat, " {}", slot.rust).expect("writing to a String cannot fail"); + } + wat.push(')'); + } + 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("))"); + } + + fn write_shim(&self, wat: &mut String) { + let names = input_names(&self.export.inputs); + write!( + wat, + "(func $js_sys.export.{} (@sym (name {}))", + self.index, + quoted(self.export.name), + ) + .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: 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 { + write!(wat, " {}", frame_slot.slot.js()) + .expect("writing to a String cannot fail"); + } + wat.push(')'); + } + } + } + + self.write_prologue(wat); + self.write_call(wat, &names); + self.write_epilogue(wat); + wat.push_str("\n)"); + } + + 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()); + }); + locals.write_into(wat); + + if self.is_closure() { + let slot = self.closure_data_slot(); + wat.push_str("\n local.get $data"); + write_conversion(wat, slot.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, names: &[String]) { + match self.export.callee { + Callee::Symbol { .. } => { + if self.is_indirect() { + wat.push_str("\n local.get $retptr"); + } + for (input, name) in self.export.inputs.iter().zip(names) { + write_abi_arguments(wat, input, name); + } + 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, name) in self.export.inputs.iter().zip(names) { + if input.kind == ExportInputKind::Value { + write_abi_arguments(wat, input, name); + } + } + 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: Some(slot), .. + } => { + write_conversion(wat, slot.instruction()); + } + ExportOutput::Direct { slot: None, .. } => {} + ExportOutput::Indirect { frame, .. } => { + for frame_slot in &frame.slots { + write!( + wat, + "\n local.get $retptr\n {}.load offset={}", + frame_slot.slot.rust, frame_slot.offset, + ) + .expect("writing to a String cannot fail"); + write_conversion(wat, frame_slot.slot.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 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>), +) { + 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: Some(slot), .. + } => visit(slot), + ExportOutput::Direct { slot: None, .. } => {} + ExportOutput::Indirect { frame, .. } => { + for frame_slot in &frame.slots { + visit(&frame_slot.slot); + } + } + } + } +} + +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 ${name}_{slot_index}").expect("writing to a String cannot fail"); + write_conversion(wat, slot.instruction()); + } +} + +#[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 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 { + 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, + slots: vec![input], + conversion: None, + }], + output: Some(ExportOutput::Direct { + slot: Some(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 $arg0_0 externref) (result externref) + local.get $arg0_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 new file mode 100644 index 00000000..05976f26 --- /dev/null +++ b/host/ld/src/wire/import/js.rs @@ -0,0 +1,484 @@ +use std::fmt::Write; + +use js_bindgen_wire::model::{ + DirectImportConversion, Embed, GlobalPath, Import, ImportBindingKind, ImportCatch, + ImportErrorMode, ImportGroup, ImportOutputAbi, ImportRetptr, ImportWriter, JsCatch, +}; + +use super::input_name; +use crate::wire::JsBinding; +use crate::wire::js::{JsPath, 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_function( + import, + render_operation(binding.kind, import.inputs.len()), + catch, + ), + embeds, + }) + }) + .collect() +} + +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<'_>, + 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| { + 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 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 asynchronous = if await_output { "async " } else { "" }; + let body = if let Some(output) = import.output.as_ref() { + 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_}{expression}\n}}") + }; + format!("{asynchronous}({parameters}) => {{\n{conversions}{body}") + } else { + format!("({parameters}) => {expression}") + }; + + if import.suspending { + format!("new WebAssembly.Suspending({js})") + } else { + js + } +} + +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(", ")) + } +} + +struct PreparedInputs { + parameters: String, + conversions: String, +} + +fn prepare_inputs(import: &Import<'_>) -> PreparedInputs { + let mut parameters = Vec::new(); + if import + .output + .as_ref() + .is_some_and(|output| !output.is_direct()) + { + parameters.push("$retptr".to_owned()); + } + 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_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( + expression: &str, + conversion: Option<&DirectImportConversion<'_>>, + await_output: bool, + catch: Option<&JsCatch<'_>>, +) -> String { + let indent = if catch.is_some() { " " } else { " " }; + let expression = if await_output { + format!("await ({expression})") + } else { + expression.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 = {expression}").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 {expression}").expect("writing to a String cannot fail"); + } + + js.push_str(catch.map_or("\n}", |catch| catch.direct)); + js +} + +fn render_indirect_output( + expression: &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() + { + 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 expression = if await_output { + format!("await ({expression})") + } else { + expression.to_owned() + }; + write!(js, "{indent}const $ret = {expression}").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_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(_) => {} + }) +} + +#[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 new file mode 100644 index 00000000..89932fce --- /dev/null +++ b/host/ld/src/wire/import/mod.rs @@ -0,0 +1,39 @@ +//! Rendering of JavaScript imports and their Wasm `ABI` shims. + +mod js; +mod wat; + +use js_bindgen_wire::model::{ClosureFactory, Import, ImportGroup}; + +use super::{RenderedClosureShim, RenderedGroup}; + +pub(super) fn render<'a>(group: &ImportGroup<'a>) -> RenderedGroup<'a> { + RenderedGroup { + bindings: js::render(group), + 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 new file mode 100644 index 00000000..704aaf0c --- /dev/null +++ b/host/ld/src/wire/import/wat.rs @@ -0,0 +1,361 @@ +use std::collections::HashMap; +use std::fmt::Write; + +use js_bindgen_wire::model::{ + Import, ImportCatch, ImportErrorMode, ImportGroup, ImportOutput, ImportOutputAbi, Slot, + WatCatch, +}; + +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 { + 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 { + 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()); + // 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); + 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, boundary_index, 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); + } + 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, + }); + } + + let mut wat = imports.render(); + for shim in shims { + wat.push('\n'); + render_shim(&mut wat, shim); + } + Some(wat) +} + +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>, +} + +fn render_wat_import(imports: &mut WatImports, index: usize, import: &Import<'_>) { + let symbol = boundary_symbol(import); + let mut wat = format!( + "(import {} {} (func $js_sys.import.boundary.{index} (@sym (name {}))", + quoted(import.module), + quoted(import.name), + quoted(&symbol), + ); + + if let Some(ImportOutput { + abi: ImportOutputAbi::Indirect { retptr, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (param $retptr {})", retptr.slot.js()) + .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 { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (result {})", slot.js()).expect("writing to a String cannot fail"); + } + + wat.push_str("))"); + imports.insert(&symbol, wat); +} + +fn render_shim(wat: &mut String, shim: Shim<'_, '_>) { + let Shim { + index, + boundary_index, + import, + symbol, + comdat, + catch, + locals, + } = shim; + write!( + wat, + "(func $js_sys.import.shim.{index} (@sym (name {}))", + quoted(&symbol), + ) + .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 { + abi: ImportOutputAbi::Indirect { retptr, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (param $retptr {})", retptr.slot.rust) + .expect("writing to a String cannot fail"); + } + + 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"); + } + } + + if let Some(ImportOutput { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + { + write!(wat, " (result {})", slot.rust).expect("writing to a String cannot fail"); + } + + locals.write_into(wat); + + if let Some(catch) = catch { + wat.push_str(catch.try_); + } + + if let Some(ImportOutput { + abi: ImportOutputAbi::Indirect { retptr, .. }, + .. + }) = import.output.as_ref() + { + wat.push_str("\n local.get $retptr"); + write_conversion(wat, retptr.slot.instruction()); + } + + 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"); + write_conversion(wat, slot.instruction()); + } + } + + write!( + wat, + "\n call $js_sys.import.boundary.{boundary_index} (@reloc)", + ) + .expect("writing to a String cannot fail"); + + if let Some(ImportOutput { + abi: ImportOutputAbi::Direct { slot, .. }, + .. + }) = import.output.as_ref() + { + write_conversion(wat, slot.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.rust.zero(); + write!(wat, "\n {zero}").expect("writing to a String cannot fail"); + } + } + + wat.push_str("\n)"); +} + +fn boundary_symbol(import: &Import<'_>) -> String { + format!("{}.import.{}", import.module, import.name) +} + +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> { + 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) +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use js_bindgen_wire::abi::WatType; + use js_bindgen_wire::model::{Import, ImportGroup, ImportInput, Slot, WatConversion}; + + use super::{factory_comdat, quoted, render, render_closure_factory}; + + #[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, + inputs: Vec::new(), + output: None, + binding: None, + suspending: false, + }], + }; + + 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 { + catch: None, + imports: vec![Import { + module: "test", + name: "converted", + inputs: vec![ImportInput { + 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 $arg0_0 i32) + local.get $arg0_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 new file mode 100644 index 00000000..a57a13ca --- /dev/null +++ b/host/ld/src/wire/js.rs @@ -0,0 +1,158 @@ +//! 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)), +]; + +/// 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 +/// 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. +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 +} + +#[cfg(test)] +mod tests { + use std::fmt::Write; + + 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() { + 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 new file mode 100644 index 00000000..6623e793 --- /dev/null +++ b/host/ld/src/wire/mod.rs @@ -0,0 +1,61 @@ +//! Rendering of decoded `js-sys` wire records. + +mod closure; +mod export; +mod import; +mod js; +mod wat; + +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)] +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 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, + } + } +} + +/// One rendered wire record. +pub(crate) enum RenderedRecord<'a> { + Imports(RenderedGroup<'a>), + 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 new file mode 100644 index 00000000..472d26e8 --- /dev/null +++ b/host/ld/src/wire/wat.rs @@ -0,0 +1,182 @@ +//! Shared WAT emission helpers. + +use std::collections::HashMap; +use std::fmt::{self, Display, Formatter, Write}; + +use js_bindgen_wire::abi::WatType; +use js_bindgen_wire::model::{WatImport, WatImportKind, WatLocal}; + +/// 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 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('"') + } +} + +#[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 { + self.entries.join("\n") + } +} + +#[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 write_into(self, wat: &mut String) { + for local in self.entries { + write!(wat, "\n (local ${} {})", local.name, local.ty) + .expect("writing to a String cannot fail"); + } + } +} + +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); + } +} + +fn write_import(wat: &mut String, import: &WatImport<'_>) { + write!( + wat, + "(import {} {} (", + quoted(import.module), + quoted(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 {}))", quoted(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(')'); +} + +#[cfg(test)] +mod tests { + use super::quoted; + + #[test] + fn quotes_wat_strings_as_bytes() { + assert_eq!( + quoted("single' double\" slash\\ line\n雪").to_string(), + "\"single' double\\\" slash\\\\ line\\0a\\e9\\9b\\aa\"", + ); + } +} 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)); 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" diff --git a/host/runner/src/js/shared/shared.mjs b/host/runner/src/js/shared/shared.mjs index 1a88d2d1..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) { @@ -176,7 +181,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..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 @@ -230,7 +244,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 +258,7 @@ export async function run( interceptFlag = true try { - testFn() + await testFn() result = { success: true } } catch (error) { result = { 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/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 }) } 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/host/wire/Cargo.toml b/host/wire/Cargo.toml new file mode 100644 index 00000000..4bad2e54 --- /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 } + +[lib] +bench = false +doctest = false + +[features] +default = [] +alloc = [] + +[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..9a4b2450 --- /dev/null +++ b/host/wire/src/abi.rs @@ -0,0 +1,491 @@ +//! 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 js: WatType, +} + +impl WatConv { + #[must_use] + pub const fn new( + imports: &'static [WatImport], + locals: &'static [WatLocal], + instruction: &'static str, + js: WatType, + ) -> Self { + Self { + imports, + locals, + instruction, + js, + } + } +} + +/// 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 rust: WatType, + pub wat: Option, +} + +impl WatSlot { + #[must_use] + pub const fn new(rust: WatType, wat: Option) -> Self { + Self { rust, wat } + } + + #[must_use] + pub const fn plain(rust: WatType) -> Self { + Self::new(rust, 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/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 new file mode 100644 index 00000000..b92f3f24 --- /dev/null +++ b/host/wire/src/decode/export.rs @@ -0,0 +1,176 @@ +use alloc::vec::Vec; + +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, +}; + +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 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 _ in 0..input_count { + let (input, embed) = ExportInput::decode_with(decoder, ExportInputKind::Value)?; + inputs.push(input); + embeds.extend(embed); + } + + 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> ExportInput<'a> { + pub(super) fn decode_with( + decoder: &mut Decoder<'a>, + kind: ExportInputKind, + ) -> Result<(Self, Option>), Error> { + 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, + slots, + conversion, + }, + embed, + )) + } +} + +impl<'a> ExportOutput<'a> { + 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)?; + let (embed, js_conversion) = match conversion { + Some((embed, template)) => (embed, Some(template)), + None => (None, None), + }; + let direct = flags & EXPORT_OUTPUT_DIRECT != 0; + let result = flags & EXPORT_OUTPUT_RESULT != 0; + let output = if direct { + let slots = value::compact(&slots); + 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.into_iter().next(), + 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 + .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..b4d632ec --- /dev/null +++ b/host/wire/src/decode/import.rs @@ -0,0 +1,396 @@ +use alloc::rc::Rc; +use alloc::vec::Vec; + +use super::{Decode, Decoder, value}; +use crate::abi::WatType; +use crate::model::{ + DirectImportConversion, Embed, GlobalPath, Import, ImportBinding, ImportBindingKind, + ImportCatch, ImportErrorMode, ImportGroup, ImportInput, ImportOutput, ImportOutputAbi, + ImportRetptr, ImportWriter, JsCatch, Slot, WatCatch, +}; +use crate::{ + 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, +}; + +pub(super) struct InputType<'a> { + pub(super) slots: Vec>, + js_conversion: Option<&'a str>, + pub(super) embed: Option>, +} + +pub(super) struct OutputType<'a> { + pub(super) abi: ImportOutputAbi<'a>, + pub(super) embeds: [Option>; 2], + pub(super) 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].rust == 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, Some(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> { + 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; + 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", + )?; + 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].rust, + 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 { + 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", + )?; + 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.and_then(|pointer| 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 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(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> { + pub(super) fn from_type(ty: &InputType<'a>) -> Self { + Self { + slots: ty.slots.clone(), + js_conversion: ty.js_conversion, + } + } +} + +impl<'a> Decode<'a> for ImportBinding<'a> { + fn decode(decoder: &mut Decoder<'a>) -> Result { + 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 { + 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 new file mode 100644 index 00000000..6c1c90a6 --- /dev/null +++ b/host/wire/src/decode/mod.rs @@ -0,0 +1,405 @@ +//! 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_CLOSURE, KIND_EXPORT, KIND_IMPORT, MAGIC, PointerWidth, VERSION}; + +/// 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) +} + +/// 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(); + 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_CLOSURE => closure::decode(decoder, pointer_width) + .map(Box::new) + .map(Self::Closure), + 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 + } + + /// 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]) + } + + 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..33e2b0bc --- /dev/null +++ b/host/wire/src/decode/value.rs @@ -0,0 +1,303 @@ +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]; + +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 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()?, + js: decode_wat_type(decoder)?, + }) + } else { + None + }; + Ok(Self { rust, 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/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 new file mode 100644 index 00000000..6d6424c3 --- /dev/null +++ b/host/wire/src/encode/export.rs @@ -0,0 +1,187 @@ +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, + WireExportInput, WireExportOutput, WireExportOutputType, WireReturnFrame, +}; + +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 WireExportInput { + pub(super) const fn encode(self, encoder: &mut Encoder) { + encoder.slots(&self.ty.slots); + match self.ty.conversion { + Some(conversion) => { + encoder.u8(1); + conversion.encode(encoder); + } + None => encoder.u8(0), + } + } + + pub(super) const fn size(self, sizer: &mut Sizer) { + 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) { + 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); + } + } + } + } + + const fn size(&self, sizer: &mut 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); + } + } + } + } +} + +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 { + pub(super) const fn encode(self, encoder: &mut Encoder) { + self.ty.encode(encoder); + } + + pub(super) 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); + let flags = + flag(self.promising, EXPORT_PROMISING) | flag(self.output.is_some(), EXPORT_HAS_OUTPUT); + encoder.u8(flags); + encoder.string(self.symbol); + 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); + sizer.string(self.symbol); + 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..481e136e --- /dev/null +++ b/host/wire/src/encode/import.rs @@ -0,0 +1,348 @@ +use core::mem::size_of; + +use super::{Encoder, Sizer, flag, wire_u32}; +use crate::abi::{JsCatch, WatCatch}; +use crate::{ + 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 { + 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 { + pub(super) 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), + } + } + + pub(super) 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 { + 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); + 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); + } + } + + pub(super) 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.u32(wire_u32(self.type_index)); + } +} + +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) { + 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.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)); + 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::()); + sizer.add(self.inputs.len() * size_of::()); + if self.output.is_some() { + sizer.add(size_of::()); + } + if let Some(binding) = self.binding { + binding.size(sizer); + } + } +} diff --git a/host/wire/src/encode/mod.rs b/host/wire/src/encode/mod.rs new file mode 100644 index 00000000..b95cb9fd --- /dev/null +++ b/host/wire/src/encode/mod.rs @@ -0,0 +1,548 @@ +//! Constant serialization of static wire descriptions. + +mod closure; +mod export; +mod import; + +use core::mem::size_of; + +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)] +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), + WireKind::Closure(closure) => sizer.closure(closure), + } + 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), + WireKind::Closure(closure) => encoder.closure(closure), + } + 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, + Self::Closure(_) => crate::KIND_CLOSURE, + } + } +} + +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.rust.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.js.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" +)] +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" +)] +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..660b901b --- /dev/null +++ b/host/wire/src/lib.rs @@ -0,0 +1,102 @@ +//! 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] + +#[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; + +#[cfg(feature = "alloc")] +pub use decode::{Error, ErrorKind, WireRecords, decode}; +pub use encode::{WireBlob, WireRecord, wire_blob_len}; +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, exports, and closures. +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; +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; +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 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")] +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; + +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 new file mode 100644 index 00000000..194cbacf --- /dev/null +++ b/host/wire/src/model.rs @@ -0,0 +1,392 @@ +//! Canonical model consumed by JavaScript and `WAT` `renderers`. + +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::vec::Vec; + +pub use crate::PointerWidth; +pub use crate::abi::ResultLayout; +use crate::abi::{RefType, WatIndexType, WatType}; + +/// One primitive `Wasm` slot. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Slot<'a> { + pub rust: WatType, + pub wat: Option>, +} + +impl<'a> Slot<'a> { + #[must_use] + pub fn js(&self) -> WatType { + self.wat + .as_ref() + .map_or(self.rust, |conversion| conversion.js) + } + + #[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 between its Rust- and +/// JavaScript-facing types. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WatConversion<'a> { + pub js: 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 argument passed from Rust to an imported JavaScript function. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ImportInput<'a> { + 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 { .. }) + } +} + +/// 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 kind: ImportBindingKind<'a>, + 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 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 { + /// The sole direct result, or `None` for a zero-slot return. + slot: Option>, + 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 is_void(&self) -> bool { + matches!(self, Self::Direct { slot: None, .. }) + } + + #[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 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 new file mode 100644 index 00000000..90496183 --- /dev/null +++ b/host/wire/src/schema.rs @@ -0,0 +1,585 @@ +//! Static, const-constructible wire descriptions. + +use core::mem::size_of; + +use crate::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 argument accepted by a JavaScript import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireImportInput { + pub(crate) type_index: usize, +} + +impl WireImportInput { + #[must_use] + pub const fn new(type_index: usize) -> Self { + Self { type_index } + } +} + +/// One path rooted at JavaScript's global object. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireGlobalPath { + pub(crate) namespace: Option<&'static str>, + pub(crate) object: Option<&'static str>, + pub(crate) name: &'static str, +} + +impl WireGlobalPath { + #[must_use] + pub const fn new( + namespace: Option<&'static str>, + object: Option<&'static str>, + name: &'static str, + ) -> Self { + Self { + 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 { + 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 argument accepted by a JavaScript-facing `Wasm` export. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireExportInput { + pub(crate) ty: &'static WireExportInputType, +} + +impl WireExportInput { + #[must_use] + 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) 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], +} + +impl WireReturnFrame { + #[must_use] + pub const fn new(size: usize, slot_offsets: [usize; 4]) -> Self { + assert!(size != 0); + Self { size, slot_offsets } + } +} + +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; + let mut index = 0; + while index < slots.len() { + if slots[index].is_some() { + slot_count += 1; + } + index += 1; + } + assert!(slot_count <= 1); + assert!(slot_count != 0 || conversion.is_none()); + Self { + kind: WireExportOutputKind::Direct { slots, conversion }, + } + } + + #[must_use] + pub const fn indirect( + slots: [Option; 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 { + kind: WireExportOutputKind::Indirect { + slots, + conversion, + frame, + 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 } + } +} + +/// The raw linker symbol and boundary `ABI` of a closure factory. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WireClosureFactory { + pub(crate) raw_symbol: &'static str, + pub(crate) helper: JsEmbed, + pub(crate) input: &'static WireImportInputType, + pub(crate) output: &'static WireImportOutputType, +} + +impl WireClosureFactory { + #[must_use] + 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] + 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, + } + } +} + +/// 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) symbol: &'static str, + pub(crate) promising: bool, +} + +impl WireExport { + #[must_use] + const fn new( + module: &'static str, + name: &'static str, + inputs: &'static [WireExportInput], + output: Option, + symbol: &'static str, + promising: bool, + ) -> Self { + Self { + module, + name, + inputs, + output, + symbol, + promising, + } + } + + #[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, 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, symbol, true) + } +} + +#[derive(Clone, Copy)] +pub(crate) enum WireKind { + Imports { + table: &'static WireImportTypeTable, + imports: &'static [WireImport], + }, + Exports(&'static [WireExport]), + Closure(WireClosure), +} + +/// One import, export, or closure payload 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), + } + } + + #[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]) { + 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; + } +} diff --git a/host/wire/src/tests.rs b/host/wire/src/tests.rs new file mode 100644 index 00000000..c45ce7f1 --- /dev/null +++ b/host/wire/src/tests.rs @@ -0,0 +1,629 @@ +use alloc::vec; +use alloc::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 IDENTITY_PATH: WireGlobalPath = WireGlobalPath::new(None, None, "identity"); +const WIDE_PATH: WireGlobalPath = WireGlobalPath::new(None, None, "wide"); +const IMPORTS: &[WireImport] = &[ + WireImport::new( + "js_sys", + "number.identity", + &[WireImportInput::new(0)], + Some(WireImportOutput::new(0)), + Some(WireImportBinding::CallGlobal { + target: IDENTITY_PATH, + variadic: false, + }), + false, + ), + WireImport::new( + "js_sys", + "wide.suspending", + &[], + Some(WireImportOutput::new(1)), + Some(WireImportBinding::CallGlobal { + target: WIDE_PATH, + variadic: false, + }), + 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_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)), + ), + 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); + +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 { + panic!("expected imports"); + }; + let imports = &group.imports; + assert_eq!(imports.len(), 2); + 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.embeds, + [Embed { + module: "js_sys", + name: "input.convert", + }] + ); + let slot = &imports[0].inputs[0].slots[0]; + assert_eq!((slot.rust, slot.js()), (WatType::I32, WatType::ExternRef)); + let conversion = slot.wat.as_ref().unwrap(); + assert!(matches!( + &conversion.imports[0].kind, + WatImportKind::Function { + parameters, + results, + } if parameters == &[WatType::I32] && results == &[WatType::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"); + let binding = imports[1].binding.as_ref().unwrap(); + assert_eq!( + binding.embeds, + [ + Embed { + module: "js_sys", + name: "retptr.convert", + }, + Embed { + module: "js_sys", + name: "output.convert", + }, + ] + ); + let ImportOutputAbi::Indirect { retptr, .. } = &imports[1].output.as_ref().unwrap().abi else { + panic!("expected indirect output"); + }; + assert_eq!(retptr.js_conversion, Some("$slot1 >>> 0")); +} + +#[test] +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 RESULT_IMPORTS: &[WireImport] = &[WireImport::new( + "support", + "fallible", + &[], + Some(WireImportOutput::new(1)), + Some(WireImportBinding::CallGlobal { + target: WireGlobalPath::new(None, None, "fallible"), + variadic: false, + }), + false, + )]; + 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(RESULT_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 + ); + + let Record::Imports(group) = decode(PLAIN_RECORD.as_bytes()).unwrap() else { + panic!("expected imports"); + }; + assert!(group.catch.is_none()); +} + +#[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].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].kind, ExportInputKind::Value); + assert_eq!(exports[0].inputs[0].slots[0].rust, WatType::I32); + 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] +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", + } + ); + 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!(conversion.is_none()); + assert_eq!((slot.rust, slot.js()), (WatType::I32, WatType::ExternRef)); + + assert_eq!(closure.pointer_width, PointerWidth::Wasm64); + assert_eq!( + 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] +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) + ); + + // 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!( + 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) + ); +} + +#[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 } + ); +} 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..93d08c1a --- /dev/null +++ b/web/playground/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "playground" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[[bench]] +harness = false +name = "conv" + +[dependencies] +js-sys = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +js-bindgen = { workspace = true } + +[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..886d436c --- /dev/null +++ b/web/playground/src/main.rs @@ -0,0 +1,65 @@ +use std::time::Instant; + +use js_sys::hazard::JsCast; +use js_sys::{Function, JsString, JsValue, Promise, closure}; + +fn main() { + let ins = Instant::now(); + + 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 }; + + let f1 = js_sys::block_on(f1); + let f2 = js_sys::block_on(f2); + + println!("future1: {f1}, future2: {f2:?}, cost: {:?}", ins.elapsed()); +} + +#[cfg(test)] +mod tests { + use js_sys::{JsValue, Promise, block_on}; + + #[test] + #[should_panic] + fn test1() { + panic!() + } + + #[test] + #[ignore = "test2"] + fn test2() { + panic!() + } + + #[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"); + }); + } +} 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"